summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2019-04-07 21:50:36 +0900
committerGitHub <noreply@github.com>2019-04-07 21:50:36 +0900
commitf0a29721c9fb10f97faf386bc9d6b1b2fad97895 (patch)
treeb5c1d38d698589bb444c0881a431391db91eb5bc
parentUpdate README.md [AUTOGEN] (#4639) (diff)
downloadsharkey-f0a29721c9fb10f97faf386bc9d6b1b2fad97895.tar.gz
sharkey-f0a29721c9fb10f97faf386bc9d6b1b2fad97895.tar.bz2
sharkey-f0a29721c9fb10f97faf386bc9d6b1b2fad97895.zip
Use PostgreSQL instead of MongoDB (#4572)
* wip * Update note.ts * Update timeline.ts * Update core.ts * wip * Update generate-visibility-query.ts * wip * wip * wip * wip * wip * Update global-timeline.ts * wip * wip * wip * Update vote.ts * wip * wip * Update create.ts * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update files.ts * wip * wip * Update CONTRIBUTING.md * wip * wip * wip * wip * wip * wip * wip * wip * Update read-notification.ts * wip * wip * wip * wip * wip * wip * wip * Update cancel.ts * wip * wip * wip * Update show.ts * wip * wip * Update gen-id.ts * Update create.ts * Update id.ts * wip * wip * wip * wip * wip * wip * wip * Docker: Update files about Docker (#4599) * Docker: Use cache if files used by `yarn install` was not updated This patch reduces the number of times to installing node_modules. For example, `yarn install` step will be skipped when only ".config/default.yml" is updated. * Docker: Migrate MongoDB to Postgresql Misskey uses Postgresql as a database instead of Mongodb since version 11. * Docker: Uncomment about data persistence This patch will save a lot of databases. * wip * wip * wip * Update activitypub.ts * wip * wip * wip * Update logs.ts * wip * Update drive-file.ts * Update register.ts * wip * wip * Update mentions.ts * wip * wip * wip * Update recommendation.ts * wip * Update index.ts * wip * Update recommendation.ts * Doc: Update docker.ja.md and docker.en.md (#1) (#4608) Update how to set up misskey. * wip * :v: * wip * Update note.ts * Update postgre.ts * wip * wip * wip * wip * Update add-file.ts * wip * wip * wip * Clean up * Update logs.ts * wip * :pizza: * wip * Ad notes * wip * Update api-visibility.ts * Update note.ts * Update add-file.ts * tests * tests * Update postgre.ts * Update utils.ts * wip * wip * Refactor * wip * Refactor * wip * wip * Update show-users.ts * Update update-instance.ts * wip * Update feed.ts * Update outbox.ts * Update outbox.ts * Update user.ts * wip * Update list.ts * Update update-hashtag.ts * wip * Update update-hashtag.ts * Refactor * Update update.ts * wip * wip * :v: * clean up * docs * Update push.ts * wip * Update api.ts * wip * :v: * Update make-pagination-query.ts * :v: * Delete hashtags.ts * Update instances.ts * Update instances.ts * Update create.ts * Update search.ts * Update reversi-game.ts * Update signup.ts * Update user.ts * id * Update example.yml * :art: * objectid * fix * reversi * reversi * Fix bug of chart engine * Add test of chart engine * Improve test * Better testing * Improve chart engine * Refactor * Add test of chart engine * Refactor * Add chart test * Fix bug * コミットし忘れ * Refactoring * :v: * Add tests * Add test * Extarct note tests * Refactor * 存在しないユーザーにメンションできなくなっていた問題を修正 * Fix bug * Update update-meta.ts * Fix bug * Update mention.vue * Fix bug * Update meta.ts * Update CONTRIBUTING.md * Fix bug * Fix bug * Fix bug * Clean up * Clean up * Update notification.ts * Clean up * Add mute tests * Add test * Refactor * Add test * Fix test * Refactor * Refactor * Add tests * Update utils.ts * Update utils.ts * Fix test * Update package.json * Update update.ts * Update manifest.ts * Fix bug * Fix bug * Add test * :art: * Update endpoint permissions * Updaye permisison * Update person.ts #4299 * データベースと同期しないように * Fix bug * Fix bug * Update reversi-game.ts * Use a feature of Node v11.7.0 to extract a public key (#4644) * wip * wip * :v: * Refactoring #1540 * test * test * test * test * test * test * test * Fix bug * Fix test * :sushi: * wip * #4471 * Add test for #4335 * Refactor * Fix test * Add tests * :clock4: * Fix bug * Add test * Add test * rename * Fix bug
-rw-r--r--.config/docker_example.env5
-rw-r--r--.config/example.yml105
-rw-r--r--.config/mongo_initdb_example.js13
-rw-r--r--[-rwxr-xr-x].dockerignore4
-rw-r--r--.gitignore5
-rw-r--r--CONTRIBUTING.md58
-rw-r--r--Dockerfile3
-rw-r--r--binding.gyp9
-rw-r--r--cli/migration/2.0.0.js57
-rw-r--r--cli/migration/2.4.0.js71
-rw-r--r--cli/migration/5.0.0.js9
-rw-r--r--cli/migration/7.0.0.js134
-rw-r--r--cli/migration/8.0.0.js144
-rw-r--r--docker-compose.yml24
-rw-r--r--docs/backup.fr.md22
-rw-r--r--docs/backup.md22
-rw-r--r--docs/docker.en.md34
-rw-r--r--docs/docker.ja.md36
-rw-r--r--docs/setup.en.md20
-rw-r--r--docs/setup.fr.md12
-rw-r--r--docs/setup.ja.md20
-rw-r--r--gulpfile.ts1
-rw-r--r--index.js2
-rw-r--r--locales/ja-JP.yml5
-rw-r--r--package.json28
-rw-r--r--src/@types/deepcopy.d.ts19
-rw-r--r--src/@types/escape-regexp.d.ts7
-rw-r--r--src/argv.ts3
-rw-r--r--src/boot/index.ts77
-rw-r--r--src/boot/master.ts176
-rw-r--r--src/boot/worker.ts20
-rw-r--r--src/client/app/admin/views/drive.vue2
-rw-r--r--src/client/app/admin/views/hashtags.vue8
-rw-r--r--src/client/app/admin/views/instance.vue19
-rw-r--r--src/client/app/admin/views/logs.vue2
-rw-r--r--src/client/app/admin/views/users.vue18
-rw-r--r--src/client/app/auth/views/form.vue16
-rw-r--r--src/client/app/common/define-widget.ts15
-rw-r--r--src/client/app/common/scripts/note-mixin.ts4
-rw-r--r--src/client/app/common/scripts/note-subscriber.ts22
-rw-r--r--src/client/app/common/views/components/avatar.vue13
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.game.vue42
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.room.vue49
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.vue2
-rw-r--r--src/client/app/common/views/components/instance.vue2
-rw-r--r--src/client/app/common/views/components/mention.vue2
-rw-r--r--src/client/app/common/views/components/poll.vue8
-rw-r--r--src/client/app/common/views/components/reactions-viewer.vue2
-rw-r--r--src/client/app/common/views/components/settings/notification.vue2
-rw-r--r--src/client/app/common/views/components/settings/profile.vue8
-rw-r--r--src/client/app/common/views/components/settings/theme.vue33
-rw-r--r--src/client/app/common/views/components/signup.vue2
-rw-r--r--src/client/app/common/views/components/trends.vue4
-rw-r--r--src/client/app/common/views/components/user-list-editor.vue6
-rw-r--r--src/client/app/common/views/components/user-menu.vue2
-rw-r--r--src/client/app/common/views/deck/deck.column-core.vue2
-rw-r--r--src/client/app/common/views/deck/deck.hashtag-tl.vue2
-rw-r--r--src/client/app/common/views/deck/deck.notification.vue2
-rw-r--r--src/client/app/common/views/deck/deck.tl-column.vue6
-rw-r--r--src/client/app/common/views/deck/deck.tl.vue6
-rw-r--r--src/client/app/common/views/deck/deck.vue18
-rw-r--r--src/client/app/common/views/pages/explore.vue6
-rw-r--r--src/client/app/common/views/pages/followers.vue26
-rw-r--r--src/client/app/common/views/pages/following.vue27
-rw-r--r--src/client/app/common/views/pages/share.vue2
-rw-r--r--src/client/app/common/views/widgets/server.info.vue2
-rw-r--r--src/client/app/desktop/views/components/drive.file.vue2
-rw-r--r--src/client/app/desktop/views/components/note.vue4
-rw-r--r--src/client/app/desktop/views/components/notifications.vue2
-rw-r--r--src/client/app/desktop/views/components/user-list-window.vue2
-rw-r--r--src/client/app/desktop/views/components/user-lists-window.vue2
-rw-r--r--src/client/app/desktop/views/home/home.vue17
-rw-r--r--src/client/app/desktop/views/home/tag.vue2
-rw-r--r--src/client/app/desktop/views/home/timeline.core.vue8
-rw-r--r--src/client/app/desktop/views/home/timeline.vue10
-rw-r--r--src/client/app/desktop/views/home/user/user.header.vue6
-rw-r--r--src/client/app/desktop/views/pages/welcome.vue8
-rw-r--r--src/client/app/dev/views/new-app.vue24
-rw-r--r--src/client/app/mios.ts15
-rw-r--r--src/client/app/mobile/views/components/drive.file-detail.vue2
-rw-r--r--src/client/app/mobile/views/components/drive.file.vue2
-rw-r--r--src/client/app/mobile/views/components/notification-preview.vue2
-rw-r--r--src/client/app/mobile/views/components/notification.vue2
-rw-r--r--src/client/app/mobile/views/pages/home.timeline.vue8
-rw-r--r--src/client/app/mobile/views/pages/home.vue12
-rw-r--r--src/client/app/mobile/views/pages/tag.vue2
-rw-r--r--src/client/app/mobile/views/pages/user-list.vue2
-rw-r--r--src/client/app/mobile/views/pages/user-lists.vue2
-rw-r--r--src/client/app/mobile/views/pages/user/index.vue10
-rw-r--r--src/client/app/mobile/views/pages/welcome.vue8
-rw-r--r--src/client/app/mobile/views/pages/widgets.vue7
-rw-r--r--src/client/app/store.ts118
-rw-r--r--src/config/types.ts4
-rw-r--r--src/crypto_key.cc111
-rw-r--r--src/crypto_key.d.ts2
-rw-r--r--src/daemons/notes-stats-child.ts40
-rw-r--r--src/db/mongodb.ts39
-rw-r--r--src/db/postgre.ts137
-rw-r--r--src/docs/reversi-bot.ja-JP.md6
-rw-r--r--src/docs/stream.ja-JP.md2
-rw-r--r--src/index.ts279
-rw-r--r--src/init.ts16
-rw-r--r--src/mfm/toHtml.ts4
-rw-r--r--src/misc/aid.ts26
-rw-r--r--src/misc/aidc.ts26
-rw-r--r--src/misc/cafy-id.ts29
-rw-r--r--src/misc/check-mongodb.ts37
-rw-r--r--src/misc/fetch-meta.ts43
-rw-r--r--src/misc/fetch-proxy-account.ts8
-rw-r--r--src/misc/gen-id.ts22
-rw-r--r--src/misc/get-drive-file-url.ts31
-rw-r--r--src/misc/get-notification-summary.ts2
-rw-r--r--src/misc/get-user-name.ts4
-rw-r--r--src/misc/get-user-summary.ts9
-rw-r--r--src/misc/is-duplicate-key-value-error.ts3
-rw-r--r--src/misc/is-objectid.ts5
-rw-r--r--src/misc/is-quote.ts6
-rw-r--r--src/misc/nyaize.ts9
-rw-r--r--src/misc/object-id.ts26
-rw-r--r--src/misc/reaction-lib.ts4
-rw-r--r--src/misc/should-mute-this-note.ts13
-rw-r--r--src/models/abuse-user-report.ts52
-rw-r--r--src/models/access-token.ts16
-rw-r--r--src/models/app.ts102
-rw-r--r--src/models/auth-session.ts49
-rw-r--r--src/models/blocking.ts56
-rw-r--r--src/models/drive-file-thumbnail.ts29
-rw-r--r--src/models/drive-file-webpublic.ts29
-rw-r--r--src/models/drive-file.ts232
-rw-r--r--src/models/drive-folder.ts75
-rw-r--r--src/models/emoji.ts21
-rw-r--r--src/models/entities/abuse-user-report.ts41
-rw-r--r--src/models/entities/access-token.ts45
-rw-r--r--src/models/entities/app.ts60
-rw-r--r--src/models/entities/auth-session.ts39
-rw-r--r--src/models/entities/blocking.ts42
-rw-r--r--src/models/entities/drive-file.ts154
-rw-r--r--src/models/entities/drive-folder.ts49
-rw-r--r--src/models/entities/emoji.ts46
-rw-r--r--src/models/entities/follow-request.ts85
-rw-r--r--src/models/entities/following.ts80
-rw-r--r--src/models/entities/games/reversi/game.ts133
-rw-r--r--src/models/entities/games/reversi/matching.ts35
-rw-r--r--src/models/entities/hashtag.ts87
-rw-r--r--src/models/entities/instance.ts132
-rw-r--r--src/models/entities/log.ts46
-rw-r--r--src/models/entities/messaging-message.ts64
-rw-r--r--src/models/entities/meta.ts264
-rw-r--r--src/models/entities/muting.ts42
-rw-r--r--src/models/entities/note-favorite.ts35
-rw-r--r--src/models/entities/note-reaction.ts42
-rw-r--r--src/models/entities/note-unread.ts43
-rw-r--r--src/models/entities/note-watching.ts52
-rw-r--r--src/models/entities/note.ts236
-rw-r--r--src/models/entities/notification.ts94
-rw-r--r--src/models/entities/poll-vote.ts40
-rw-r--r--src/models/entities/poll.ts67
-rw-r--r--src/models/entities/registration-tickets.ts17
-rw-r--r--src/models/entities/signin.ts35
-rw-r--r--src/models/entities/sw-subscription.ts37
-rw-r--r--src/models/entities/user-keypair.ts24
-rw-r--r--src/models/entities/user-list-joining.ts41
-rw-r--r--src/models/entities/user-list.ts33
-rw-r--r--src/models/entities/user-note-pinings.ts35
-rw-r--r--src/models/entities/user-publickey.ts30
-rw-r--r--src/models/entities/user-service-linking.ts108
-rw-r--r--src/models/entities/user.ts297
-rw-r--r--src/models/favorite.ts65
-rw-r--r--src/models/follow-request.ts66
-rw-r--r--src/models/following.ts27
-rw-r--r--src/models/games/reversi/game.ts111
-rw-r--r--src/models/games/reversi/matching.ts45
-rw-r--r--src/models/hashtag.ts63
-rw-r--r--src/models/id.ts4
-rw-r--r--src/models/index.ts74
-rw-r--r--src/models/instance.ts90
-rw-r--r--src/models/log.ts19
-rw-r--r--src/models/messaging-message.ts75
-rw-r--r--src/models/meta.ts257
-rw-r--r--src/models/mute.ts56
-rw-r--r--src/models/note-reaction.ts51
-rw-r--r--src/models/note-unread.ts19
-rw-r--r--src/models/note-watching.ts15
-rw-r--r--src/models/note.ts418
-rw-r--r--src/models/notification.ts120
-rw-r--r--src/models/poll-vote.ts17
-rw-r--r--src/models/registration-tickets.ts12
-rw-r--r--src/models/repositories/abuse-user-report.ts32
-rw-r--r--src/models/repositories/app.ts36
-rw-r--r--src/models/repositories/auth-session.ts19
-rw-r--r--src/models/repositories/blocking.ts28
-rw-r--r--src/models/repositories/drive-file.ts113
-rw-r--r--src/models/repositories/drive-folder.ts49
-rw-r--r--src/models/repositories/follow-request.ts19
-rw-r--r--src/models/repositories/following.ts44
-rw-r--r--src/models/repositories/games/reversi/game.ts49
-rw-r--r--src/models/repositories/games/reversi/matching.ts27
-rw-r--r--src/models/repositories/messaging-message.ts37
-rw-r--r--src/models/repositories/muting.ts28
-rw-r--r--src/models/repositories/note-favorite.ts25
-rw-r--r--src/models/repositories/note-reaction.ts18
-rw-r--r--src/models/repositories/note.ts210
-rw-r--r--src/models/repositories/notification.ts47
-rw-r--r--src/models/repositories/signin.ts11
-rw-r--r--src/models/repositories/user-list.ts16
-rw-r--r--src/models/repositories/user.ts198
-rw-r--r--src/models/signin.ts34
-rw-r--r--src/models/sw-subscription.ts13
-rw-r--r--src/models/user-list.ts41
-rw-r--r--src/models/user.ts438
-rw-r--r--src/queue/index.ts17
-rw-r--r--src/queue/processors/db/delete-drive-files.ts34
-rw-r--r--src/queue/processors/db/delete-notes.ts55
-rw-r--r--src/queue/processors/db/export-blocking.ts36
-rw-r--r--src/queue/processors/db/export-following.ts36
-rw-r--r--src/queue/processors/db/export-mute.ts36
-rw-r--r--src/queue/processors/db/export-notes.ts48
-rw-r--r--src/queue/processors/db/export-user-lists.ts29
-rw-r--r--src/queue/processors/db/import-following.ts27
-rw-r--r--src/queue/processors/db/import-user-lists.ts41
-rw-r--r--src/queue/processors/db/index.ts2
-rw-r--r--src/queue/processors/deliver.ts26
-rw-r--r--src/queue/processors/inbox.ts52
-rw-r--r--src/remote/activitypub/kernel/accept/follow.ts10
-rw-r--r--src/remote/activitypub/kernel/accept/index.ts2
-rw-r--r--src/remote/activitypub/kernel/add/index.ts4
-rw-r--r--src/remote/activitypub/kernel/announce/index.ts2
-rw-r--r--src/remote/activitypub/kernel/announce/note.ts10
-rw-r--r--src/remote/activitypub/kernel/block/index.ts10
-rw-r--r--src/remote/activitypub/kernel/create/image.ts2
-rw-r--r--src/remote/activitypub/kernel/create/index.ts2
-rw-r--r--src/remote/activitypub/kernel/create/note.ts2
-rw-r--r--src/remote/activitypub/kernel/delete/index.ts6
-rw-r--r--src/remote/activitypub/kernel/delete/note.ts8
-rw-r--r--src/remote/activitypub/kernel/follow.ts10
-rw-r--r--src/remote/activitypub/kernel/index.ts2
-rw-r--r--src/remote/activitypub/kernel/like.ts11
-rw-r--r--src/remote/activitypub/kernel/reject/follow.ts10
-rw-r--r--src/remote/activitypub/kernel/reject/index.ts2
-rw-r--r--src/remote/activitypub/kernel/remove/index.ts4
-rw-r--r--src/remote/activitypub/kernel/undo/block.ts10
-rw-r--r--src/remote/activitypub/kernel/undo/follow.ts24
-rw-r--r--src/remote/activitypub/kernel/undo/index.ts2
-rw-r--r--src/remote/activitypub/kernel/undo/like.ts11
-rw-r--r--src/remote/activitypub/kernel/update/index.ts2
-rw-r--r--src/remote/activitypub/misc/get-note-html.ts6
-rw-r--r--src/remote/activitypub/models/image.ts25
-rw-r--r--src/remote/activitypub/models/note.ts88
-rw-r--r--src/remote/activitypub/models/person.ts242
-rw-r--r--src/remote/activitypub/models/question.ts41
-rw-r--r--src/remote/activitypub/perform.ts2
-rw-r--r--src/remote/activitypub/renderer/accept.ts4
-rw-r--r--src/remote/activitypub/renderer/add.ts4
-rw-r--r--src/remote/activitypub/renderer/announce.ts6
-rw-r--r--src/remote/activitypub/renderer/block.ts4
-rw-r--r--src/remote/activitypub/renderer/create.ts6
-rw-r--r--src/remote/activitypub/renderer/delete.ts4
-rw-r--r--src/remote/activitypub/renderer/document.ts10
-rw-r--r--src/remote/activitypub/renderer/emoji.ts4
-rw-r--r--src/remote/activitypub/renderer/follow-user.ts14
-rw-r--r--src/remote/activitypub/renderer/follow.ts9
-rw-r--r--src/remote/activitypub/renderer/image.ts10
-rw-r--r--src/remote/activitypub/renderer/key.ts13
-rw-r--r--src/remote/activitypub/renderer/like.ts10
-rw-r--r--src/remote/activitypub/renderer/mention.ts9
-rw-r--r--src/remote/activitypub/renderer/note.ts89
-rw-r--r--src/remote/activitypub/renderer/person.ts41
-rw-r--r--src/remote/activitypub/renderer/question.ts19
-rw-r--r--src/remote/activitypub/renderer/reject.ts4
-rw-r--r--src/remote/activitypub/renderer/remove.ts4
-rw-r--r--src/remote/activitypub/renderer/undo.ts6
-rw-r--r--src/remote/activitypub/renderer/update.ts6
-rw-r--r--src/remote/activitypub/renderer/vote.ts21
-rw-r--r--src/remote/activitypub/request.ts17
-rw-r--r--src/remote/activitypub/resolver.ts2
-rw-r--r--src/remote/resolve-user.ts21
-rw-r--r--src/server/activitypub.ts116
-rw-r--r--src/server/activitypub/featured.ts21
-rw-r--r--src/server/activitypub/followers.ts38
-rw-r--r--src/server/activitypub/following.ts41
-rw-r--r--src/server/activitypub/outbox.ts70
-rw-r--r--src/server/api/authenticate.ts24
-rw-r--r--src/server/api/call.ts6
-rw-r--r--src/server/api/common/generate-mute-query.ts36
-rw-r--r--src/server/api/common/generate-native-user-token.ts2
-rw-r--r--src/server/api/common/generate-visibility-query.ts40
-rw-r--r--src/server/api/common/get-friends.ts49
-rw-r--r--src/server/api/common/get-hide-users.ts25
-rw-r--r--src/server/api/common/getters.ts42
-rw-r--r--src/server/api/common/make-pagination-query.ts28
-rw-r--r--src/server/api/common/read-messaging-message.ts76
-rw-r--r--src/server/api/common/read-notification.ts74
-rw-r--r--src/server/api/common/signin.ts2
-rw-r--r--src/server/api/define.ts8
-rw-r--r--src/server/api/endpoints/admin/abuse-user-reports.ts30
-rw-r--r--src/server/api/endpoints/admin/drive/files.ts34
-rw-r--r--src/server/api/endpoints/admin/drive/show-file.ts9
-rw-r--r--src/server/api/endpoints/admin/emoji/add.ts8
-rw-r--r--src/server/api/endpoints/admin/emoji/list.ts6
-rw-r--r--src/server/api/endpoints/admin/emoji/remove.ts12
-rw-r--r--src/server/api/endpoints/admin/emoji/update.ts24
-rw-r--r--src/server/api/endpoints/admin/federation/remove-all-following.ts11
-rw-r--r--src/server/api/endpoints/admin/federation/update-instance.ts17
-rw-r--r--src/server/api/endpoints/admin/invite.ts6
-rw-r--r--src/server/api/endpoints/admin/logs.ts66
-rw-r--r--src/server/api/endpoints/admin/moderators/add.ts19
-rw-r--r--src/server/api/endpoints/admin/moderators/remove.ts19
-rw-r--r--src/server/api/endpoints/admin/remove-abuse-user-report.ts15
-rw-r--r--src/server/api/endpoints/admin/reset-password.ts17
-rw-r--r--src/server/api/endpoints/admin/show-user.ts9
-rw-r--r--src/server/api/endpoints/admin/show-users.ts74
-rw-r--r--src/server/api/endpoints/admin/silence-user.ts19
-rw-r--r--src/server/api/endpoints/admin/suspend-user.ts31
-rw-r--r--src/server/api/endpoints/admin/unsilence-user.ts19
-rw-r--r--src/server/api/endpoints/admin/unsuspend-user.ts19
-rw-r--r--src/server/api/endpoints/admin/unverify-user.ts19
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts58
-rw-r--r--src/server/api/endpoints/admin/update-remote-user.ts13
-rw-r--r--src/server/api/endpoints/admin/verify-user.ts19
-rw-r--r--src/server/api/endpoints/aggregation/hashtags.ts72
-rw-r--r--src/server/api/endpoints/ap/show.ts59
-rw-r--r--src/server/api/endpoints/app/create.ts10
-rw-r--r--src/server/api/endpoints/app/show.ts13
-rw-r--r--src/server/api/endpoints/auth/accept.ts30
-rw-r--r--src/server/api/endpoints/auth/session/generate.ts11
-rw-r--r--src/server/api/endpoints/auth/session/show.ts6
-rw-r--r--src/server/api/endpoints/auth/session/userkey.ts34
-rw-r--r--src/server/api/endpoints/blocking/create.ts25
-rw-r--r--src/server/api/endpoints/blocking/delete.ts22
-rw-r--r--src/server/api/endpoints/blocking/list.ts39
-rw-r--r--src/server/api/endpoints/charts/active-users.ts10
-rw-r--r--src/server/api/endpoints/charts/drive.ts6
-rw-r--r--src/server/api/endpoints/charts/federation.ts10
-rw-r--r--src/server/api/endpoints/charts/hashtag.ts10
-rw-r--r--src/server/api/endpoints/charts/instance.ts10
-rw-r--r--src/server/api/endpoints/charts/network.ts10
-rw-r--r--src/server/api/endpoints/charts/notes.ts6
-rw-r--r--src/server/api/endpoints/charts/user/drive.ts9
-rw-r--r--src/server/api/endpoints/charts/user/following.ts11
-rw-r--r--src/server/api/endpoints/charts/user/notes.ts9
-rw-r--r--src/server/api/endpoints/charts/user/reactions.ts13
-rw-r--r--src/server/api/endpoints/charts/users.ts6
-rw-r--r--src/server/api/endpoints/drive.ts26
-rw-r--r--src/server/api/endpoints/drive/files.ts48
-rw-r--r--src/server/api/endpoints/drive/files/attached-notes.ts23
-rw-r--r--src/server/api/endpoints/drive/files/check-existence.ts13
-rw-r--r--src/server/api/endpoints/drive/files/create.ts11
-rw-r--r--src/server/api/endpoints/drive/files/delete.ts21
-rw-r--r--src/server/api/endpoints/drive/files/find.ts20
-rw-r--r--src/server/api/endpoints/drive/files/show.ts54
-rw-r--r--src/server/api/endpoints/drive/files/update.ts70
-rw-r--r--src/server/api/endpoints/drive/files/upload-from-url.ts9
-rw-r--r--src/server/api/endpoints/drive/folders.ts41
-rw-r--r--src/server/api/endpoints/drive/folders/create.ts32
-rw-r--r--src/server/api/endpoints/drive/folders/delete.ts29
-rw-r--r--src/server/api/endpoints/drive/folders/find.ts20
-rw-r--r--src/server/api/endpoints/drive/folders/show.ts20
-rw-r--r--src/server/api/endpoints/drive/folders/update.ts57
-rw-r--r--src/server/api/endpoints/drive/stream.ts44
-rw-r--r--src/server/api/endpoints/federation/instances.ts126
-rw-r--r--src/server/api/endpoints/federation/show-instance.ts4
-rw-r--r--src/server/api/endpoints/following/create.ts20
-rw-r--r--src/server/api/endpoints/following/delete.ts20
-rw-r--r--src/server/api/endpoints/following/requests/accept.ts5
-rw-r--r--src/server/api/endpoints/following/requests/cancel.ts9
-rw-r--r--src/server/api/endpoints/following/requests/list.ts10
-rw-r--r--src/server/api/endpoints/following/requests/reject.ts5
-rw-r--r--src/server/api/endpoints/games/reversi/games.ts45
-rw-r--r--src/server/api/endpoints/games/reversi/games/show.ts20
-rw-r--r--src/server/api/endpoints/games/reversi/games/surrender.ts29
-rw-r--r--src/server/api/endpoints/games/reversi/invitations.ts12
-rw-r--r--src/server/api/endpoints/games/reversi/match.ts65
-rw-r--r--src/server/api/endpoints/games/reversi/match/cancel.ts8
-rw-r--r--src/server/api/endpoints/hashtags/list.ts67
-rw-r--r--src/server/api/endpoints/hashtags/search.ts21
-rw-r--r--src/server/api/endpoints/hashtags/trend.ts133
-rw-r--r--src/server/api/endpoints/hashtags/users.ts55
-rw-r--r--src/server/api/endpoints/i.ts4
-rw-r--r--src/server/api/endpoints/i/2fa/done.ts12
-rw-r--r--src/server/api/endpoints/i/2fa/register.ts8
-rw-r--r--src/server/api/endpoints/i/2fa/unregister.ts10
-rw-r--r--src/server/api/endpoints/i/authorized-apps.ts25
-rw-r--r--src/server/api/endpoints/i/change-password.ts10
-rw-r--r--src/server/api/endpoints/i/clear-follow-request-notification.ts23
-rw-r--r--src/server/api/endpoints/i/delete-account.ts29
-rw-r--r--src/server/api/endpoints/i/export-blocking.ts2
-rw-r--r--src/server/api/endpoints/i/export-following.ts2
-rw-r--r--src/server/api/endpoints/i/export-mute.ts2
-rw-r--r--src/server/api/endpoints/i/export-notes.ts2
-rw-r--r--src/server/api/endpoints/i/export-user-lists.ts2
-rw-r--r--src/server/api/endpoints/i/favorites.ts41
-rw-r--r--src/server/api/endpoints/i/import-following.ts19
-rw-r--r--src/server/api/endpoints/i/import-user-lists.ts19
-rw-r--r--src/server/api/endpoints/i/notifications.ts81
-rw-r--r--src/server/api/endpoints/i/pin.ts9
-rw-r--r--src/server/api/endpoints/i/read-all-messaging-messages.ts25
-rw-r--r--src/server/api/endpoints/i/read-all-unread-notes.ts22
-rw-r--r--src/server/api/endpoints/i/regenerate-token.ts12
-rw-r--r--src/server/api/endpoints/i/signin-history.ts35
-rw-r--r--src/server/api/endpoints/i/unpin.ts9
-rw-r--r--src/server/api/endpoints/i/update-client-setting.ts22
-rw-r--r--src/server/api/endpoints/i/update-email.ts24
-rw-r--r--src/server/api/endpoints/i/update-home.ts33
-rw-r--r--src/server/api/endpoints/i/update-mobile-home.ts32
-rw-r--r--src/server/api/endpoints/i/update-widget.ts88
-rw-r--r--src/server/api/endpoints/i/update.ts138
-rw-r--r--src/server/api/endpoints/messaging/history.ts52
-rw-r--r--src/server/api/endpoints/messaging/messages.ts45
-rw-r--r--src/server/api/endpoints/messaging/messages/create.ts56
-rw-r--r--src/server/api/endpoints/messaging/messages/delete.ts21
-rw-r--r--src/server/api/endpoints/messaging/messages/read.ts15
-rw-r--r--src/server/api/endpoints/meta.ts21
-rw-r--r--src/server/api/endpoints/mute/create.ts33
-rw-r--r--src/server/api/endpoints/mute/delete.ts23
-rw-r--r--src/server/api/endpoints/mute/list.ts39
-rw-r--r--src/server/api/endpoints/my/apps.ts19
-rw-r--r--src/server/api/endpoints/notes.ts58
-rw-r--r--src/server/api/endpoints/notes/children.ts106
-rw-r--r--src/server/api/endpoints/notes/conversation.ts12
-rw-r--r--src/server/api/endpoints/notes/create.ts67
-rw-r--r--src/server/api/endpoints/notes/delete.ts12
-rw-r--r--src/server/api/endpoints/notes/favorites/create.ts23
-rw-r--r--src/server/api/endpoints/notes/favorites/delete.ts19
-rw-r--r--src/server/api/endpoints/notes/featured.ts32
-rw-r--r--src/server/api/endpoints/notes/global-timeline.ts89
-rw-r--r--src/server/api/endpoints/notes/local-timeline.ts111
-rw-r--r--src/server/api/endpoints/notes/mentions.ts110
-rw-r--r--src/server/api/endpoints/notes/polls/recommendation.ts82
-rw-r--r--src/server/api/endpoints/notes/polls/vote.ts98
-rw-r--r--src/server/api/endpoints/notes/reactions.ts35
-rw-r--r--src/server/api/endpoints/notes/reactions/create.ts5
-rw-r--r--src/server/api/endpoints/notes/reactions/delete.ts5
-rw-r--r--src/server/api/endpoints/notes/renotes.ts39
-rw-r--r--src/server/api/endpoints/notes/replies.ts83
-rw-r--r--src/server/api/endpoints/notes/search-by-tag.ts271
-rw-r--r--src/server/api/endpoints/notes/search.ts24
-rw-r--r--src/server/api/endpoints/notes/show.ts7
-rw-r--r--src/server/api/endpoints/notes/social-timeline.ts (renamed from src/server/api/endpoints/notes/hybrid-timeline.ts)165
-rw-r--r--src/server/api/endpoints/notes/state.ts24
-rw-r--r--src/server/api/endpoints/notes/timeline.ts150
-rw-r--r--src/server/api/endpoints/notes/user-list-timeline.ts161
-rw-r--r--src/server/api/endpoints/notes/watching/create.ts7
-rw-r--r--src/server/api/endpoints/notes/watching/delete.ts7
-rw-r--r--src/server/api/endpoints/notifications/mark-all-as-read.ts26
-rw-r--r--src/server/api/endpoints/stats.ts28
-rw-r--r--src/server/api/endpoints/sw/register.ts13
-rw-r--r--src/server/api/endpoints/username/available.ts9
-rw-r--r--src/server/api/endpoints/users.ts80
-rw-r--r--src/server/api/endpoints/users/followers.ts98
-rw-r--r--src/server/api/endpoints/users/following.ts98
-rw-r--r--src/server/api/endpoints/users/get-frequently-replied-users.ts52
-rw-r--r--src/server/api/endpoints/users/lists/create.ts18
-rw-r--r--src/server/api/endpoints/users/lists/delete.ts17
-rw-r--r--src/server/api/endpoints/users/lists/list.ts10
-rw-r--r--src/server/api/endpoints/users/lists/pull.ts23
-rw-r--r--src/server/api/endpoints/users/lists/push.ts21
-rw-r--r--src/server/api/endpoints/users/lists/show.ts15
-rw-r--r--src/server/api/endpoints/users/lists/update.ts23
-rw-r--r--src/server/api/endpoints/users/notes.ts130
-rw-r--r--src/server/api/endpoints/users/recommendation.ts99
-rw-r--r--src/server/api/endpoints/users/relation.ts7
-rw-r--r--src/server/api/endpoints/users/report-abuse.ts24
-rw-r--r--src/server/api/endpoints/users/search.ts40
-rw-r--r--src/server/api/endpoints/users/show.ts29
-rw-r--r--src/server/api/index.ts9
-rw-r--r--src/server/api/limiter.ts8
-rw-r--r--src/server/api/openapi/schemas.ts40
-rw-r--r--src/server/api/private/signin.ts23
-rw-r--r--src/server/api/private/signup.ts99
-rw-r--r--src/server/api/service/discord.ts86
-rw-r--r--src/server/api/service/github.ts55
-rw-r--r--src/server/api/service/twitter.ts58
-rw-r--r--src/server/api/stream/channel.ts8
-rw-r--r--src/server/api/stream/channels/admin.ts2
-rw-r--r--src/server/api/stream/channels/drive.ts2
-rw-r--r--src/server/api/stream/channels/games/reversi-game.ts168
-rw-r--r--src/server/api/stream/channels/games/reversi.ts13
-rw-r--r--src/server/api/stream/channels/global-timeline.ts18
-rw-r--r--src/server/api/stream/channels/hashtag.ts48
-rw-r--r--src/server/api/stream/channels/home-timeline.ts45
-rw-r--r--src/server/api/stream/channels/hybrid-timeline.ts55
-rw-r--r--src/server/api/stream/channels/index.ts4
-rw-r--r--src/server/api/stream/channels/local-timeline.ts45
-rw-r--r--src/server/api/stream/channels/main.ts9
-rw-r--r--src/server/api/stream/channels/messaging-index.ts2
-rw-r--r--src/server/api/stream/channels/messaging.ts4
-rw-r--r--src/server/api/stream/channels/social-timeline.ts64
-rw-r--r--src/server/api/stream/channels/user-list.ts72
-rw-r--r--src/server/api/stream/index.ts64
-rw-r--r--src/server/api/streaming.ts27
-rw-r--r--src/server/file/index.ts4
-rw-r--r--src/server/file/send-drive-file.ts91
-rw-r--r--src/server/index.ts16
-rw-r--r--src/server/nodeinfo.ts40
-rw-r--r--src/server/web/feed.ts35
-rw-r--r--src/server/web/index.ts74
-rw-r--r--src/server/web/manifest.ts3
-rw-r--r--src/server/web/views/info.pug10
-rw-r--r--src/server/well-known.ts22
-rw-r--r--src/services/blocking/create.ts89
-rw-r--r--src/services/blocking/delete.ts18
-rw-r--r--src/services/chart/active-users.ts48
-rw-r--r--src/services/chart/charts/classes/active-users.ts35
-rw-r--r--src/services/chart/charts/classes/drive.ts69
-rw-r--r--src/services/chart/charts/classes/federation.ts51
-rw-r--r--src/services/chart/charts/classes/hashtag.ts35
-rw-r--r--src/services/chart/charts/classes/instance.ts160
-rw-r--r--src/services/chart/charts/classes/network.ts34
-rw-r--r--src/services/chart/charts/classes/notes.ts71
-rw-r--r--src/services/chart/charts/classes/per-user-drive.ts52
-rw-r--r--src/services/chart/charts/classes/per-user-following.ts91
-rw-r--r--src/services/chart/charts/classes/per-user-notes.ts58
-rw-r--r--src/services/chart/charts/classes/per-user-reactions.ts32
-rw-r--r--src/services/chart/charts/classes/test-grouped.ts47
-rw-r--r--src/services/chart/charts/classes/test-unique.ts29
-rw-r--r--src/services/chart/charts/classes/test.ts45
-rw-r--r--src/services/chart/charts/classes/users.ts60
-rw-r--r--src/services/chart/charts/schemas/active-users.ts28
-rw-r--r--src/services/chart/charts/schemas/drive.ts65
-rw-r--r--src/services/chart/charts/schemas/federation.ts27
-rw-r--r--src/services/chart/charts/schemas/hashtag.ts28
-rw-r--r--src/services/chart/charts/schemas/instance.ts124
-rw-r--r--src/services/chart/charts/schemas/network.ts30
-rw-r--r--src/services/chart/charts/schemas/notes.ts52
-rw-r--r--src/services/chart/charts/schemas/per-user-drive.ts54
-rw-r--r--src/services/chart/charts/schemas/per-user-following.ts81
-rw-r--r--src/services/chart/charts/schemas/per-user-notes.ts41
-rw-r--r--src/services/chart/charts/schemas/per-user-reactions.ts28
-rw-r--r--src/services/chart/charts/schemas/test-grouped.ts26
-rw-r--r--src/services/chart/charts/schemas/test-unique.ts11
-rw-r--r--src/services/chart/charts/schemas/test.ts26
-rw-r--r--src/services/chart/charts/schemas/users.ts41
-rw-r--r--src/services/chart/core.ts460
-rw-r--r--src/services/chart/drive.ts150
-rw-r--r--src/services/chart/entities.ts8
-rw-r--r--src/services/chart/federation.ts66
-rw-r--r--src/services/chart/hashtag.ts56
-rw-r--r--src/services/chart/index.ts387
-rw-r--r--src/services/chart/instance.ts302
-rw-r--r--src/services/chart/network.ts64
-rw-r--r--src/services/chart/notes.ts127
-rw-r--r--src/services/chart/per-user-drive.ts122
-rw-r--r--src/services/chart/per-user-following.ts162
-rw-r--r--src/services/chart/per-user-notes.ts100
-rw-r--r--src/services/chart/per-user-reactions.ts45
-rw-r--r--src/services/chart/users.ts94
-rw-r--r--src/services/create-notification.ts78
-rw-r--r--src/services/drive/add-file.ts300
-rw-r--r--src/services/drive/delete-file.ts108
-rw-r--r--src/services/drive/internal-storage.ts27
-rw-r--r--src/services/drive/upload-from-url.ts20
-rw-r--r--src/services/following/create.ts142
-rw-r--r--src/services/following/delete.ts58
-rw-r--r--src/services/following/requests/accept-all.ts18
-rw-r--r--src/services/following/requests/accept.ts18
-rw-r--r--src/services/following/requests/cancel.ts30
-rw-r--r--src/services/following/requests/create.ts65
-rw-r--r--src/services/following/requests/reject.ts30
-rw-r--r--src/services/i/pin.ts95
-rw-r--r--src/services/i/update.ts25
-rw-r--r--src/services/logger.ts13
-rw-r--r--src/services/note/create.ts466
-rw-r--r--src/services/note/delete.ts94
-rw-r--r--src/services/note/polls/update.ts35
-rw-r--r--src/services/note/polls/vote.ts89
-rw-r--r--src/services/note/reaction/create.ts92
-rw-r--r--src/services/note/reaction/delete.ts45
-rw-r--r--src/services/note/read.ts56
-rw-r--r--src/services/note/unread.ts45
-rw-r--r--src/services/note/unwatch.ts11
-rw-r--r--src/services/note/watch.ts31
-rw-r--r--src/services/push-notification.ts17
-rw-r--r--src/services/register-or-fetch-instance-doc.ts14
-rw-r--r--src/services/stream.ts51
-rw-r--r--src/services/update-hashtag.ts109
-rw-r--r--src/services/user-list/push.ts25
-rw-r--r--src/tools/add-emoji.ts6
-rw-r--r--src/tools/clean-remote-files.ts21
-rw-r--r--src/tools/move-drive-files.ts83
-rw-r--r--src/tools/show-signin-history.ts13
-rw-r--r--test/api-visibility.ts271
-rw-r--r--test/api.ts635
-rw-r--r--test/chart.ts323
-rw-r--r--test/mfm.ts4
-rw-r--r--test/mocha.opts3
-rw-r--r--test/mute.ts170
-rw-r--r--test/note.ts361
-rw-r--r--test/reaction-lib.ts4
-rw-r--r--test/streaming.ts913
-rw-r--r--test/user-notes.ts86
-rw-r--r--test/utils.ts99
-rw-r--r--tsconfig.json1
592 files changed, 13435 insertions, 14119 deletions
diff --git a/.config/docker_example.env b/.config/docker_example.env
new file mode 100644
index 0000000000..411d93659b
--- /dev/null
+++ b/.config/docker_example.env
@@ -0,0 +1,5 @@
+# db settings
+POSTGRES_PASSWORD="example-misskey-pass"
+POSTGRES_USER="example-misskey-user"
+POSTGRES_DB="misskey"
+
diff --git a/.config/example.yml b/.config/example.yml
index 70c096baa1..10239f1a76 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -1,8 +1,16 @@
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+# Misskey configuration
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+# ┌─────┐
+#───┘ URL └─────────────────────────────────────────────────────
+
# Final accessible URL seen by a user.
url: https://example.tld/
+# ┌───────────────────────┐
+#───┘ Port and TLS settings └───────────────────────────────────
-### Port and TLS settings ######################################
#
# Misskey supports two deployment options for public.
#
@@ -30,28 +38,51 @@ url: https://example.tld/
# You need to set Certificate in 'https' section.
# To use option 1, uncomment below line.
-# port: 3000 # A port that your Misskey server should listen.
+#port: 3000 # A port that your Misskey server should listen.
# To use option 2, uncomment below lines.
-# port: 443
-#
-# https:
-# # path for certification
-# key: /etc/letsencrypt/live/example.tld/privkey.pem
-# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
+#port: 443
-################################################################
+#https:
+# # path for certification
+# key: /etc/letsencrypt/live/example.tld/privkey.pem
+# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
+# ┌──────────────────────────┐
+#───┘ PostgreSQL configuration └────────────────────────────────
-mongodb:
+db:
host: localhost
- port: 27017
+ port: 5432
+
+ # Database name
db: misskey
+
+ # Auth
user: example-misskey-user
pass: example-misskey-pass
+# ┌─────────────────────┐
+#───┘ Redis configuration └─────────────────────────────────────
+
+#redis:
+# host: localhost
+# port: 6379
+# pass: example-pass
+
+# ┌─────────────────────────────┐
+#───┘ Elasticsearch configuration └─────────────────────────────
+
+#elasticsearch:
+# host: localhost
+# port: 9200
+# pass: null
+
+# ┌────────────────────────────────────┐
+#───┘ File storage (Drive) configuration └──────────────────────
+
drive:
- storage: 'db'
+ storage: 'fs'
# OR
@@ -88,25 +119,43 @@ drive:
# accessKey: XXX
# secretKey: YYY
-# If enabled:
-# The first account created is automatically marked as Admin.
-autoAdmin: true
+# ┌───────────────┐
+#───┘ ID generation └───────────────────────────────────────────
-#
-# Below settings are optional
-#
+# You can select the ID generation method.
+# You don't usually need to change this setting, but you can
+# change it according to your preferences.
-# Redis
-#redis:
-# host: localhost
-# port: 6379
-# pass: example-pass
+# Available methods:
+# aid1 ... Use AID for ID generation (with random 1 char)
+# aid2 ... Use AID for ID generation (with random 2 chars)
+# aid3 ... Use AID for ID generation (with random 3 chars)
+# aid4 ... Use AID for ID generation (with random 4 chars)
+# ulid ... Use ulid for ID generation
+# objectid ... This is left for backward compatibility.
-# Elasticsearch
-#elasticsearch:
-# host: localhost
-# port: 9200
-# pass: null
+# AID(n) is the original ID generation method.
+# The trailing n represents the number of random characters that
+# will be suffixed.
+# The larger n is the safer. If n is small, the possibility of
+# collision at the same time increases, but there are also
+# advantages such as shortening of the URL.
+
+# ULID: Universally Unique Lexicographically Sortable Identifier.
+# for more details: https://github.com/ulid/spec
+# * Normally, AID should be sufficient.
+
+# ObjectID is the method used in previous versions of Misskey.
+# * Choose this if you are migrating from a previous Misskey.
+
+id: 'aid2'
+
+# ┌─────────────────────┐
+#───┘ Other configuration └─────────────────────────────────────
+
+# If enabled:
+# The first account created is automatically marked as Admin.
+autoAdmin: true
# Whether disable HSTS
#disableHsts: true
diff --git a/.config/mongo_initdb_example.js b/.config/mongo_initdb_example.js
deleted file mode 100644
index b7e7321f35..0000000000
--- a/.config/mongo_initdb_example.js
+++ /dev/null
@@ -1,13 +0,0 @@
-var user = {
- user: 'example-misskey-user',
- pwd: 'example-misskey-pass',
- roles: [
- {
- role: 'readWrite',
- db: 'misskey'
- }
- ]
-};
-
-db.createUser(user);
-
diff --git a/.dockerignore b/.dockerignore
index a25d4e5718..324c4bce58 100755..100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -5,8 +5,8 @@
.vscode
Dockerfile
build/
+db/
docker-compose.yml
+elasticsearch/
node_modules/
-mongo/
redis/
-elasticsearch/
diff --git a/.gitignore b/.gitignore
index 98ee82cd7e..650d4f6128 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,14 +8,15 @@
built
/data
/.cache-loader
+/db
+/elasticsearch
npm-debug.log
*.pem
run.bat
api-docs.json
*.log
/redis
-/mongo
-/elasticsearch
*.code-workspace
yarn.lock
.DS_Store
+/files
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c1ad1f8041..a45ed5cb5f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -75,3 +75,61 @@ src ... Source code
test ... Test code
```
+
+## Notes
+### placeholder
+SQLをクエリビルダで組み立てる際、使用するプレースホルダは重複してはならない
+例えば
+``` ts
+query.andWhere(new Brackets(qb => {
+ for (const type of ps.fileType) {
+ qb.orWhere(`:type = ANY(note.attachedFileTypes)`, { type: type });
+ }
+}));
+```
+と書くと、ループ中で`type`というプレースホルダが複数回使われてしまいおかしくなる
+だから次のようにする必要がある
+```ts
+query.andWhere(new Brackets(qb => {
+ for (const type of ps.fileType) {
+ const i = ps.fileType.indexOf(type);
+ qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
+ }
+}));
+```
+
+### `null` in SQL
+SQLを発行する際、パラメータが`null`になる可能性のある場合はSQL文を出し分けなければならない
+例えば
+``` ts
+query.where('file.folderId = :folderId', { folderId: ps.folderId });
+```
+という処理で、`ps.folderId`が`null`だと結果的に`file.folderId = null`のようなクエリが発行されてしまい、これは正しいSQLではないので期待した結果が得られない
+だから次のようにする必要がある
+``` ts
+if (ps.folderId) {
+ query.where('file.folderId = :folderId', { folderId: ps.folderId });
+} else {
+ query.where('file.folderId IS NULL');
+}
+```
+
+### `[]` in SQL
+SQLを発行する際、`IN`のパラメータが`[]`(空の配列)になる可能性のある場合はSQL文を出し分けなければならない
+例えば
+``` ts
+const users = await Users.find({
+ id: In(userIds)
+});
+```
+という処理で、`userIds`が`[]`だと結果的に`user.id IN ()`のようなクエリが発行されてしまい、これは正しいSQLではないので期待した結果が得られない
+だから次のようにする必要がある
+``` ts
+const users = userIds.length > 0 ? await Users.find({
+ id: In(userIds)
+}) : [];
+```
+
+### `undefined`にご用心
+MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。
+MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください
diff --git a/Dockerfile b/Dockerfile
index ad04fb33dc..ec7d8a6a27 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,8 +23,9 @@ RUN apk add --no-cache \
zlib-dev
RUN npm i -g yarn
-COPY . ./
+COPY package.json ./
RUN yarn install
+COPY . ./
RUN yarn build
FROM base AS runner
diff --git a/binding.gyp b/binding.gyp
deleted file mode 100644
index 0349526d52..0000000000
--- a/binding.gyp
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- 'targets': [
- {
- 'target_name': 'crypto_key',
- 'sources': ['src/crypto_key.cc'],
- 'include_dirs': ['<!(node -e "require(\'nan\')")']
- }
- ]
-}
diff --git a/cli/migration/2.0.0.js b/cli/migration/2.0.0.js
deleted file mode 100644
index f7298972e5..0000000000
--- a/cli/migration/2.0.0.js
+++ /dev/null
@@ -1,57 +0,0 @@
-// for Node.js interpret
-
-const chalk = require('chalk');
-const sequential = require('promise-sequential');
-
-const { default: User } = require('../../built/models/user');
-const { default: DriveFile } = require('../../built/models/drive-file');
-
-async function main() {
- const promiseGens = [];
-
- const count = await DriveFile.count({});
-
- let prev;
-
- for (let i = 0; i < count; i++) {
- promiseGens.push(() => {
- const promise = new Promise(async (res, rej) => {
- const file = await DriveFile.findOne(prev ? {
- _id: { $gt: prev._id }
- } : {}, {
- sort: {
- _id: 1
- }
- });
-
- prev = file;
-
- const user = await User.findOne({ _id: file.metadata.userId });
-
- DriveFile.update({
- _id: file._id
- }, {
- $set: {
- 'metadata._user': {
- host: user.host
- }
- }
- }).then(() => {
- res([i, file]);
- }).catch(rej);
- });
-
- promise.then(([i, file]) => {
- console.log(chalk`{gray ${i}} {green done: {bold ${file._id}} ${file.filename}}`);
- });
-
- return promise;
- });
- }
-
- return await sequential(promiseGens);
-}
-
-main().then(() => {
- console.log('ALL DONE');
-}).catch(console.error);
diff --git a/cli/migration/2.4.0.js b/cli/migration/2.4.0.js
deleted file mode 100644
index aa37849aa1..0000000000
--- a/cli/migration/2.4.0.js
+++ /dev/null
@@ -1,71 +0,0 @@
-// for Node.js interpret
-
-const chalk = require('chalk');
-const sequential = require('promise-sequential');
-
-const { default: User } = require('../../built/models/user');
-const { default: DriveFile } = require('../../built/models/drive-file');
-
-async function main() {
- const promiseGens = [];
-
- const count = await User.count({});
-
- let prev;
-
- for (let i = 0; i < count; i++) {
- promiseGens.push(() => {
- const promise = new Promise(async (res, rej) => {
- const user = await User.findOne(prev ? {
- _id: { $gt: prev._id }
- } : {}, {
- sort: {
- _id: 1
- }
- });
-
- prev = user;
-
- const set = {};
-
- if (user.avatarId != null) {
- const file = await DriveFile.findOne({ _id: user.avatarId });
-
- if (file && file.metadata.properties.avgColor) {
- set.avatarColor = file.metadata.properties.avgColor;
- }
- }
-
- if (user.bannerId != null) {
- const file = await DriveFile.findOne({ _id: user.bannerId });
-
- if (file && file.metadata.properties.avgColor) {
- set.bannerColor = file.metadata.properties.avgColor;
- }
- }
-
- if (Object.keys(set).length === 0) return res([i, user]);
-
- User.update({
- _id: user._id
- }, {
- $set: set
- }).then(() => {
- res([i, user]);
- }).catch(rej);
- });
-
- promise.then(([i, user]) => {
- console.log(chalk`{gray ${i}} {green done: {bold ${user._id}} @${user.username}}`);
- });
-
- return promise;
- });
- }
-
- return await sequential(promiseGens);
-}
-
-main().then(() => {
- console.log('ALL DONE');
-}).catch(console.error);
diff --git a/cli/migration/5.0.0.js b/cli/migration/5.0.0.js
deleted file mode 100644
index bef103fe4a..0000000000
--- a/cli/migration/5.0.0.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const { default: DriveFile } = require('../../built/models/drive-file');
-
-DriveFile.update({}, {
- $rename: {
- 'metadata.isMetaOnly': 'metadata.withoutChunks'
- }
-}, {
- multi: true
-});
diff --git a/cli/migration/7.0.0.js b/cli/migration/7.0.0.js
deleted file mode 100644
index fa5e363db8..0000000000
--- a/cli/migration/7.0.0.js
+++ /dev/null
@@ -1,134 +0,0 @@
-const { default: Stats } = require('../../built/models/stats');
-const { default: User } = require('../../built/models/user');
-const { default: Note } = require('../../built/models/note');
-const { default: DriveFile } = require('../../built/models/drive-file');
-
-const now = new Date();
-const y = now.getFullYear();
-const m = now.getMonth();
-const d = now.getDate();
-const today = new Date(y, m, d);
-
-async function main() {
- const localUsersCount = await User.count({
- host: null
- });
-
- const remoteUsersCount = await User.count({
- host: { $ne: null }
- });
-
- const localNotesCount = await Note.count({
- '_user.host': null
- });
-
- const remoteNotesCount = await Note.count({
- '_user.host': { $ne: null }
- });
-
- const localDriveFilesCount = await DriveFile.count({
- 'metadata._user.host': null
- });
-
- const remoteDriveFilesCount = await DriveFile.count({
- 'metadata._user.host': { $ne: null }
- });
-
- const localDriveFilesSize = await DriveFile
- .aggregate([{
- $match: {
- 'metadata._user.host': null,
- 'metadata.deletedAt': { $exists: false }
- }
- }, {
- $project: {
- length: true
- }
- }, {
- $group: {
- _id: null,
- usage: { $sum: '$length' }
- }
- }])
- .then(aggregates => {
- if (aggregates.length > 0) {
- return aggregates[0].usage;
- }
- return 0;
- });
-
- const remoteDriveFilesSize = await DriveFile
- .aggregate([{
- $match: {
- 'metadata._user.host': { $ne: null },
- 'metadata.deletedAt': { $exists: false }
- }
- }, {
- $project: {
- length: true
- }
- }, {
- $group: {
- _id: null,
- usage: { $sum: '$length' }
- }
- }])
- .then(aggregates => {
- if (aggregates.length > 0) {
- return aggregates[0].usage;
- }
- return 0;
- });
-
- await Stats.insert({
- date: today,
- users: {
- local: {
- total: localUsersCount,
- diff: 0
- },
- remote: {
- total: remoteUsersCount,
- diff: 0
- }
- },
- notes: {
- local: {
- total: localNotesCount,
- diff: 0,
- diffs: {
- normal: 0,
- reply: 0,
- renote: 0
- }
- },
- remote: {
- total: remoteNotesCount,
- diff: 0,
- diffs: {
- normal: 0,
- reply: 0,
- renote: 0
- }
- }
- },
- drive: {
- local: {
- totalCount: localDriveFilesCount,
- totalSize: localDriveFilesSize,
- diffCount: 0,
- diffSize: 0
- },
- remote: {
- totalCount: remoteDriveFilesCount,
- totalSize: remoteDriveFilesSize,
- diffCount: 0,
- diffSize: 0
- }
- }
- });
-
- console.log('done');
-}
-
-main();
diff --git a/cli/migration/8.0.0.js b/cli/migration/8.0.0.js
deleted file mode 100644
index fd6cb24525..0000000000
--- a/cli/migration/8.0.0.js
+++ /dev/null
@@ -1,144 +0,0 @@
-const { default: Stats } = require('../../built/models/stats');
-const { default: User } = require('../../built/models/user');
-const { default: Note } = require('../../built/models/note');
-const { default: DriveFile } = require('../../built/models/drive-file');
-
-const now = new Date();
-const y = now.getFullYear();
-const m = now.getMonth();
-const d = now.getDate();
-const h = now.getHours();
-const date = new Date(y, m, d, h);
-
-async function main() {
- await Stats.update({}, {
- $set: {
- span: 'day'
- }
- }, {
- multi: true
- });
-
- const localUsersCount = await User.count({
- host: null
- });
-
- const remoteUsersCount = await User.count({
- host: { $ne: null }
- });
-
- const localNotesCount = await Note.count({
- '_user.host': null
- });
-
- const remoteNotesCount = await Note.count({
- '_user.host': { $ne: null }
- });
-
- const localDriveFilesCount = await DriveFile.count({
- 'metadata._user.host': null
- });
-
- const remoteDriveFilesCount = await DriveFile.count({
- 'metadata._user.host': { $ne: null }
- });
-
- const localDriveFilesSize = await DriveFile
- .aggregate([{
- $match: {
- 'metadata._user.host': null,
- 'metadata.deletedAt': { $exists: false }
- }
- }, {
- $project: {
- length: true
- }
- }, {
- $group: {
- _id: null,
- usage: { $sum: '$length' }
- }
- }])
- .then(aggregates => {
- if (aggregates.length > 0) {
- return aggregates[0].usage;
- }
- return 0;
- });
-
- const remoteDriveFilesSize = await DriveFile
- .aggregate([{
- $match: {
- 'metadata._user.host': { $ne: null },
- 'metadata.deletedAt': { $exists: false }
- }
- }, {
- $project: {
- length: true
- }
- }, {
- $group: {
- _id: null,
- usage: { $sum: '$length' }
- }
- }])
- .then(aggregates => {
- if (aggregates.length > 0) {
- return aggregates[0].usage;
- }
- return 0;
- });
-
- await Stats.insert({
- date: date,
- span: 'hour',
- users: {
- local: {
- total: localUsersCount,
- diff: 0
- },
- remote: {
- total: remoteUsersCount,
- diff: 0
- }
- },
- notes: {
- local: {
- total: localNotesCount,
- diff: 0,
- diffs: {
- normal: 0,
- reply: 0,
- renote: 0
- }
- },
- remote: {
- total: remoteNotesCount,
- diff: 0,
- diffs: {
- normal: 0,
- reply: 0,
- renote: 0
- }
- }
- },
- drive: {
- local: {
- totalCount: localDriveFilesCount,
- totalSize: localDriveFilesSize,
- diffCount: 0,
- diffSize: 0
- },
- remote: {
- totalCount: remoteDriveFilesCount,
- totalSize: remoteDriveFilesSize,
- diffCount: 0,
- diffSize: 0
- }
- }
- });
-
- console.log('done');
-}
-
-main();
diff --git a/docker-compose.yml b/docker-compose.yml
index 7ff8f6a268..184738aa8c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,7 +5,7 @@ services:
build: .
restart: always
links:
- - mongo
+ - db
# - redis
# - es
ports:
@@ -19,21 +19,18 @@ services:
# image: redis:4.0-alpine
# networks:
# - internal_network
-### Uncomment to enable Redis persistance
-## volumes:
-## - ./redis:/data
+# volumes:
+# - ./redis:/data
- mongo:
+ db:
restart: always
- image: mongo:4.1
+ image: postgres:11.2-alpine
networks:
- internal_network
- environment:
- MONGO_INITDB_DATABASE: "misskey"
+ env_file:
+ - .config/docker.env
volumes:
- - ./.config/mongo_initdb.js:/docker-entrypoint-initdb.d/mongo_initdb.js:ro
-### Uncomment to enable MongoDB persistance
-# - ./mongo:/data
+ - ./db:/var/lib/postgresql/data
# es:
# restart: always
@@ -42,9 +39,8 @@ services:
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
# networks:
# - internal_network
-#### Uncomment to enable ES persistence
-## volumes:
-## - ./elasticsearch:/usr/share/elasticsearch/data
+# volumes:
+# - ./elasticsearch:/usr/share/elasticsearch/data
networks:
internal_network:
diff --git a/docs/backup.fr.md b/docs/backup.fr.md
deleted file mode 100644
index 19e99068ce..0000000000
--- a/docs/backup.fr.md
+++ /dev/null
@@ -1,22 +0,0 @@
-Comment faire une sauvegarde de votre Misskey ?
-==========================
-
-Assurez-vous d'avoir installé **mongodb-tools**.
-
----
-
-Dans votre terminal :
-``` shell
-$ mongodump --archive=db-backup -u <VotreNomdUtilisateur> -p <VotreMotDePasse>
-```
-
-Pour plus de détails, merci de consulter [la documentation de mongodump](https://docs.mongodb.com/manual/reference/program/mongodump/).
-
-Restauration
--------
-
-``` shell
-$ mongorestore --archive=db-backup
-```
-
-Pour plus de détails, merci de consulter [la documentation de mongorestore](https://docs.mongodb.com/manual/reference/program/mongorestore/).
diff --git a/docs/backup.md b/docs/backup.md
deleted file mode 100644
index a69af0255b..0000000000
--- a/docs/backup.md
+++ /dev/null
@@ -1,22 +0,0 @@
-How to backup your Misskey
-==========================
-
-Make sure **mongodb-tools** installed.
-
----
-
-In your shell:
-``` shell
-$ mongodump --archive=db-backup -u <YourUserName> -p <YourPassword>
-```
-
-For details, please see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/).
-
-Restore
--------
-
-``` shell
-$ mongorestore --archive=db-backup
-```
-
-For details, please see [mongorestore docs](https://docs.mongodb.com/manual/reference/program/mongorestore/).
diff --git a/docs/docker.en.md b/docs/docker.en.md
index f0fcdb66d5..ee69b6d7ae 100644
--- a/docs/docker.en.md
+++ b/docs/docker.en.md
@@ -15,9 +15,37 @@ This guide describes how to install and setup Misskey with Docker.
*2.* Configure Misskey
----------------------------------------------------------------
-1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
-2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` Copy the `.config/mongo_initdb_example.js` and rename it to `mongo_initdb.js`.
-3. Edit `default.yml` and `mongo_initdb.js`.
+
+Create configuration files with following:
+
+```bash
+cd .config
+cp example.yml default.yml
+cp docker_example.env docker.env
+```
+
+### `default.yml`
+
+Edit this file the same as non-Docker environment.
+However hostname of Postgresql, Redis and Elasticsearch are not `localhost`, they are set in `docker-compose.yml`.
+The following is default hostname:
+
+| Service | Hostname |
+|---------------|----------|
+| Postgresql | `db` |
+| Redis | `redis` |
+| Elasticsearch | `es` |
+
+### `docker.env`
+
+Configure Postgresql in this file.
+The minimum required settings are:
+
+| name | Description |
+|---------------------|---------------|
+| `POSTGRES_PASSWORD` | Password |
+| `POSTGRES_USER` | Username |
+| `POSTGRES_DB` | Database name |
*3.* Configure Docker
----------------------------------------------------------------
diff --git a/docs/docker.ja.md b/docs/docker.ja.md
index 0baf285728..060d4e7bda 100644
--- a/docs/docker.ja.md
+++ b/docs/docker.ja.md
@@ -13,11 +13,39 @@ Dockerを使ったMisskey構築方法
2. `cd misskey` misskeyディレクトリに移動
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
-*2.* 設定ファイルを作成する
+*2.* 設定ファイルの作成と編集
----------------------------------------------------------------
-1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする
-2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` `.config/mongo_initdb_example.js`をコピーし名前を`mongo_initdb.js`にする
-3. `default.yml`と`mongo_initdb.js`を編集する
+
+下記コマンドで設定ファイルを作成してください。
+
+```bash
+cd .config
+cp example.yml default.yml
+cp docker_example.env docker.env
+```
+
+### `default.yml`の編集
+
+非Docker環境と同じ様に編集してください。
+ただし、Postgresql、RedisとElasticsearchのホストは`localhost`ではなく、`docker-compose.yml`で設定されたサービス名になっています。
+標準設定では次の通りです。
+
+| サービス | ホスト名 |
+|---------------|---------|
+| Postgresql |`db` |
+| Redis |`redis` |
+| Elasticsearch |`es` |
+
+### `docker.env`の編集
+
+このファイルはPostgresqlの設定を記述します。
+最低限記述する必要がある設定は次の通りです。
+
+| 設定 | 内容 |
+|---------------------|--------------|
+| `POSTGRES_PASSWORD` | パスワード |
+| `POSTGRES_USER` | ユーザー名 |
+| `POSTGRES_DB` | データベース名 |
*3.* Dockerの設定
----------------------------------------------------------------
diff --git a/docs/setup.en.md b/docs/setup.en.md
index 1125081445..28de1f32f3 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey
Please install and setup these softwares:
#### Dependencies :package:
-* **[Node.js](https://nodejs.org/en/)** >= 10.0.0
-* **[MongoDB](https://www.mongodb.com/)** >= 3.6
+* **[Node.js](https://nodejs.org/en/)** >= 11.7.0
+* **[PostgreSQL](https://www.postgresql.org/)** >= 10
##### Optional
* [Redis](https://redis.io/)
@@ -31,13 +31,9 @@ Please install and setup these softwares:
* [Elasticsearch](https://www.elastic.co/) - required to enable the search feature
* [FFmpeg](https://www.ffmpeg.org/)
-*3.* Setup MongoDB
+*3.* Setup PostgreSQL
----------------------------------------------------------------
-As root:
-1. `mongo` Go to the mongo shell
-2. `use misskey` Use the misskey database
-3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Create the misskey user.
-4. `exit` You're done!
+:)
*4.* Install Misskey
----------------------------------------------------------------
@@ -68,7 +64,13 @@ If you're still encountering errors about some modules, use node-gyp:
3. `node-gyp build`
4. `NODE_ENV=production npm run build`
-*7.* That is it.
+*7.* Init DB
+----------------------------------------------------------------
+``` shell
+npm run init
+```
+
+*8.* That is it.
----------------------------------------------------------------
Well done! Now, you have an environment that run to Misskey.
diff --git a/docs/setup.fr.md b/docs/setup.fr.md
index 959ec3392f..217a4c6a5b 100644
--- a/docs/setup.fr.md
+++ b/docs/setup.fr.md
@@ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey
Installez les paquets suivants :
#### Dépendences :package:
-* **[Node.js](https://nodejs.org/en/)** >= 10.0.0
-* **[MongoDB](https://www.mongodb.com/)** >= 3.6
+* **[Node.js](https://nodejs.org/en/)** >= 11.7.0
+* **[PostgreSQL](https://www.postgresql.org/)** >= 10
##### Optionnels
* [Redis](https://redis.io/)
@@ -31,13 +31,9 @@ Installez les paquets suivants :
* [Elasticsearch](https://www.elastic.co/) - requis pour pouvoir activer la fonctionnalité de recherche
* [FFmpeg](https://www.ffmpeg.org/)
-*3.* Paramètrage de MongoDB
+*3.* Paramètrage de PostgreSQL
----------------------------------------------------------------
-En root :
-1. `mongo` Ouvrez le shell mongo
-2. `use misskey` Utilisez la base de données misskey
-3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Créez l'utilisateur misskey.
-4. `exit` Vous avez terminé !
+:)
*4.* Installation de Misskey
----------------------------------------------------------------
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 8a21e104d6..1543541eee 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey
これらのソフトウェアをインストール・設定してください:
#### 依存関係 :package:
-* **[Node.js](https://nodejs.org/en/)** (10.0.0以上)
-* **[MongoDB](https://www.mongodb.com/)** (3.6以上)
+* **[Node.js](https://nodejs.org/en/)** (11.7.0以上)
+* **[PostgreSQL](https://www.postgresql.org/)** (10以上)
##### オプション
* [Redis](https://redis.io/)
@@ -38,13 +38,9 @@ adduser --disabled-password --disabled-login misskey
* 検索機能を有効にするためにはインストールが必要です。
* [FFmpeg](https://www.ffmpeg.org/)
-*3.* MongoDBの設定
+*3.* PostgreSQLの設定
----------------------------------------------------------------
-ルートで:
-1. `mongo` mongoシェルを起動
-2. `use misskey` misskeyデータベースを使用
-3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` misskeyユーザーを作成
-4. `exit` mongoシェルを終了
+:)
*4.* Misskeyのインストール
----------------------------------------------------------------
@@ -74,7 +70,13 @@ Debianをお使いであれば、`build-essential`パッケージをインスト
3. `node-gyp build`
4. `NODE_ENV=production npm run build`
-*7.* 以上です!
+*7.* データベースを初期化
+----------------------------------------------------------------
+``` shell
+npm run init
+```
+
+*8.* 以上です!
----------------------------------------------------------------
お疲れ様でした。これでMisskeyを動かす準備は整いました。
diff --git a/gulpfile.ts b/gulpfile.ts
index b2956c2403..bf0b87ef20 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -49,7 +49,6 @@ gulp.task('build:copy:views', () =>
gulp.task('build:copy', gulp.parallel('build:copy:views', () =>
gulp.src([
- './build/Release/crypto_key.node',
'./src/const.json',
'./src/server/web/views/**/*',
'./src/**/assets/**/*',
diff --git a/index.js b/index.js
index 5b7d1347aa..bc7e8b2f3a 100644
--- a/index.js
+++ b/index.js
@@ -1 +1 @@
-require('./built');
+require('./built').default();
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index d4457b6594..43d8cb309a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1238,11 +1238,6 @@ admin/views/instance.vue:
save: "保存"
saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
- enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
- external-user-recommendation-engine: "エンジン"
- external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
- external-user-recommendation-timeout: "タイムアウト"
- external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
email-config: "メールサーバーの設定"
email-config-info: "メールアドレス確認やパスワードリセットの際に使われます。"
enable-email: "メール配信を有効にする"
diff --git a/package.json b/package.json
index 39e6aa7deb..a7c12862ff 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
- "version": "10.99.0",
- "codename": "nighthike",
+ "version": "11.0.0",
+ "codename": "daybreak",
"repository": {
"type": "git",
"url": "https://github.com/syuilo/misskey.git"
@@ -11,6 +11,7 @@
"private": true,
"scripts": {
"start": "node ./index.js",
+ "init": "node ./built/init.js",
"debug": "DEBUG=misskey:* node ./index.js",
"build": "webpack && gulp build",
"webpack": "webpack",
@@ -62,10 +63,9 @@
"@types/koa-send": "4.1.1",
"@types/koa-views": "2.0.3",
"@types/koa__cors": "2.2.3",
+ "@types/lolex": "3.1.1",
"@types/minio": "7.0.1",
- "@types/mkdirp": "0.5.2",
- "@types/mocha": "5.2.5",
- "@types/mongodb": "3.1.20",
+ "@types/mocha": "5.2.6",
"@types/node": "11.10.4",
"@types/nodemailer": "4.6.6",
"@types/nprogress": "0.0.29",
@@ -107,6 +107,7 @@
"chai": "4.2.0",
"chai-http": "4.2.1",
"chalk": "2.4.2",
+ "cli-highlight": "2.1.0",
"commander": "2.20.0",
"content-disposition": "0.5.3",
"crc-32": "1.2.0",
@@ -114,12 +115,10 @@
"cssnano": "4.1.10",
"dateformat": "3.0.3",
"deep-equal": "1.0.1",
- "deepcopy": "0.6.3",
"diskusage": "1.0.0",
"double-ended-queue": "2.1.0-0",
"elasticsearch": "15.4.1",
"emojilib": "2.4.0",
- "escape-regexp": "0.0.1",
"eslint": "5.15.1",
"eslint-plugin-vue": "5.2.2",
"eventemitter3": "3.1.0",
@@ -163,23 +162,22 @@
"koa-views": "6.2.0",
"langmap": "0.0.16",
"loader-utils": "1.2.3",
+ "lolex": "3.1.0",
"lookup-dns-cache": "2.1.0",
"minio": "7.0.5",
- "mkdirp": "0.5.1",
- "mocha": "5.2.0",
+ "mocha": "6.0.2",
"moji": "0.5.1",
"moment": "2.24.0",
- "mongodb": "3.2.2",
- "monk": "6.0.6",
"ms": "2.1.1",
- "nan": "2.12.1",
"nested-property": "0.0.7",
+ "node-fetch": "2.3.0",
"nodemailer": "5.1.1",
"nprogress": "0.2.0",
"object-assign-deep": "0.4.0",
"os-utils": "0.0.14",
"parse5": "5.1.0",
"parsimmon": "1.12.0",
+ "pg": "7.9.0",
"portscanner": "2.2.0",
"postcss-loader": "3.0.0",
"prismjs": "1.16.0",
@@ -195,10 +193,12 @@
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "4.1.10",
"redis": "2.8.0",
+ "reflect-metadata": "0.1.13",
"rename": "1.0.4",
"request": "2.88.0",
"request-promise-native": "1.0.7",
"request-stats": "3.0.0",
+ "require-all": "3.0.0",
"rimraf": "2.6.3",
"rndstr": "1.0.0",
"s-age": "1.1.2",
@@ -219,12 +219,14 @@
"tinycolor2": "1.4.1",
"tmp": "0.0.33",
"ts-loader": "5.3.3",
- "ts-node": "8.0.3",
+ "ts-node": "7.0.1",
"tslint": "5.13.1",
"tslint-sonarts": "1.9.0",
+ "typeorm": "0.2.16-rc.1",
"typescript": "3.3.3333",
"typescript-eslint-parser": "22.0.0",
"uglify-es": "3.3.9",
+ "ulid": "2.3.0",
"url-loader": "1.1.2",
"uuid": "3.3.2",
"v-animate-css": "0.0.3",
diff --git a/src/@types/deepcopy.d.ts b/src/@types/deepcopy.d.ts
deleted file mode 100644
index f276b7e678..0000000000
--- a/src/@types/deepcopy.d.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-declare module 'deepcopy' {
- type DeepcopyCustomizerValueType = 'Object';
-
- type DeepcopyCustomizer<T> = (
- value: T,
- valueType: DeepcopyCustomizerValueType) => T;
-
- interface IDeepcopyOptions<T> {
- customizer: DeepcopyCustomizer<T>;
- }
-
- function deepcopy<T>(
- value: T,
- options?: IDeepcopyOptions<T> | DeepcopyCustomizer<T>): T;
-
- namespace deepcopy {} // Hack
-
- export = deepcopy;
-}
diff --git a/src/@types/escape-regexp.d.ts b/src/@types/escape-regexp.d.ts
deleted file mode 100644
index d68e6048a1..0000000000
--- a/src/@types/escape-regexp.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-declare module 'escape-regexp' {
- function escapeRegExp(str: string): string;
-
- namespace escapeRegExp {} // Hack
-
- export = escapeRegExp;
-}
diff --git a/src/argv.ts b/src/argv.ts
index b5540441cc..562852d17b 100644
--- a/src/argv.ts
+++ b/src/argv.ts
@@ -15,5 +15,8 @@ program
.parse(process.argv);
if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true;
+if (process.env.NODE_ENV === 'test') program.disableClustering = true;
+if (process.env.NODE_ENV === 'test') program.quiet = true;
+if (process.env.NODE_ENV === 'test') program.noDaemons = true;
export { program };
diff --git a/src/boot/index.ts b/src/boot/index.ts
new file mode 100644
index 0000000000..2c86d8ed8c
--- /dev/null
+++ b/src/boot/index.ts
@@ -0,0 +1,77 @@
+import * as cluster from 'cluster';
+import chalk from 'chalk';
+import Xev from 'xev';
+
+import Logger from '../services/logger';
+import { program } from '../argv';
+
+// for typeorm
+import 'reflect-metadata';
+import { masterMain } from './master';
+import { workerMain } from './worker';
+
+const logger = new Logger('core', 'cyan');
+const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
+const ev = new Xev();
+
+/**
+ * Init process
+ */
+export default async function() {
+ process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
+
+ if (cluster.isMaster || program.disableClustering) {
+ await masterMain();
+
+ if (cluster.isMaster) {
+ ev.mount();
+ }
+ }
+
+ if (cluster.isWorker || program.disableClustering) {
+ await workerMain();
+ }
+
+ // ユニットテスト時にMisskeyが子プロセスで起動された時のため
+ // それ以外のときは process.send は使えないので弾く
+ if (process.send) {
+ process.send('ok');
+ }
+}
+
+//#region Events
+
+// Listen new workers
+cluster.on('fork', worker => {
+ clusterLogger.debug(`Process forked: [${worker.id}]`);
+});
+
+// Listen online workers
+cluster.on('online', worker => {
+ clusterLogger.debug(`Process is now online: [${worker.id}]`);
+});
+
+// Listen for dying workers
+cluster.on('exit', worker => {
+ // Replace the dead worker,
+ // we're not sentimental
+ clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
+ cluster.fork();
+});
+
+// Display detail of unhandled promise rejection
+if (!program.quiet) {
+ process.on('unhandledRejection', console.dir);
+}
+
+// Display detail of uncaught exception
+process.on('uncaughtException', err => {
+ logger.error(err);
+});
+
+// Dying away...
+process.on('exit', code => {
+ logger.info(`The process is going to exit with code ${code}`);
+});
+
+//#endregion
diff --git a/src/boot/master.ts b/src/boot/master.ts
new file mode 100644
index 0000000000..2d4080fdb0
--- /dev/null
+++ b/src/boot/master.ts
@@ -0,0 +1,176 @@
+import * as os from 'os';
+import * as cluster from 'cluster';
+import chalk from 'chalk';
+import * as portscanner from 'portscanner';
+import * as isRoot from 'is-root';
+
+import Logger from '../services/logger';
+import loadConfig from '../config/load';
+import { Config } from '../config/types';
+import { lessThan } from '../prelude/array';
+import * as pkg from '../../package.json';
+import { program } from '../argv';
+import { showMachineInfo } from '../misc/show-machine-info';
+import { initDb } from '../db/postgre';
+
+const logger = new Logger('core', 'cyan');
+const bootLogger = logger.createSubLogger('boot', 'magenta', false);
+
+function greet() {
+ if (!program.quiet) {
+ //#region Misskey logo
+ const v = `v${pkg.version}`;
+ console.log(' _____ _ _ ');
+ console.log(' | |_|___ ___| |_ ___ _ _ ');
+ console.log(' | | | | |_ -|_ -| \'_| -_| | |');
+ console.log(' |_|_|_|_|___|___|_,_|___|_ |');
+ console.log(' ' + chalk.gray(v) + (' |___|\n'.substr(v.length)));
+ //#endregion
+
+ console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.');
+ console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
+
+ console.log('');
+ console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`);
+ }
+
+ bootLogger.info('Welcome to Misskey!');
+ bootLogger.info(`Misskey v${pkg.version}`, null, true);
+}
+
+/**
+ * Init master process
+ */
+export async function masterMain() {
+ greet();
+
+ let config: Config;
+
+ try {
+ // initialize app
+ config = await init();
+
+ if (config.port == null) {
+ bootLogger.error('The port is not configured. Please configure port.', null, true);
+ process.exit(1);
+ }
+
+ if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
+ bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
+ process.exit(1);
+ }
+
+ if (!await isPortAvailable(config.port)) {
+ bootLogger.error(`Port ${config.port} is already in use`, null, true);
+ process.exit(1);
+ }
+ } catch (e) {
+ bootLogger.error('Fatal error occurred during initialization', null, true);
+ process.exit(1);
+ }
+
+ bootLogger.succ('Misskey initialized');
+
+ if (!program.disableClustering) {
+ await spawnWorkers(config.clusterLimit);
+ }
+
+ if (!program.noDaemons) {
+ require('../daemons/server-stats').default();
+ require('../daemons/notes-stats').default();
+ require('../daemons/queue-stats').default();
+ }
+
+ bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
+}
+
+const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));
+const requiredNodejsVersion = [11, 7, 0];
+const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion);
+
+function isWellKnownPort(port: number): boolean {
+ return port < 1024;
+}
+
+async function isPortAvailable(port: number): Promise<boolean> {
+ return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
+}
+
+function showEnvironment(): void {
+ const env = process.env.NODE_ENV;
+ const logger = bootLogger.createSubLogger('env');
+ logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
+
+ if (env !== 'production') {
+ logger.warn('The environment is not in production mode.');
+ logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
+ }
+
+ logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
+}
+
+/**
+ * Init app
+ */
+async function init(): Promise<Config> {
+ showEnvironment();
+
+ const nodejsLogger = bootLogger.createSubLogger('nodejs');
+
+ nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`);
+
+ if (!satisfyNodejsVersion) {
+ nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true);
+ process.exit(1);
+ }
+
+ await showMachineInfo(bootLogger);
+
+ const configLogger = bootLogger.createSubLogger('config');
+ let config;
+
+ try {
+ config = loadConfig();
+ } catch (exception) {
+ if (typeof exception === 'string') {
+ configLogger.error(exception);
+ process.exit(1);
+ }
+ if (exception.code === 'ENOENT') {
+ configLogger.error('Configuration file not found', null, true);
+ process.exit(1);
+ }
+ throw exception;
+ }
+
+ configLogger.succ('Loaded');
+
+ // Try to connect to DB
+ try {
+ bootLogger.info('Connecting database...');
+ await initDb();
+ } catch (e) {
+ bootLogger.error('Cannot connect to database', null, true);
+ bootLogger.error(e);
+ process.exit(1);
+ }
+
+ return config;
+}
+
+async function spawnWorkers(limit: number = Infinity) {
+ const workers = Math.min(limit, os.cpus().length);
+ bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
+ await Promise.all([...Array(workers)].map(spawnWorker));
+ bootLogger.succ('All workers started');
+}
+
+function spawnWorker(): Promise<void> {
+ return new Promise(res => {
+ const worker = cluster.fork();
+ worker.on('message', message => {
+ if (message !== 'ready') return;
+ res();
+ });
+ });
+}
diff --git a/src/boot/worker.ts b/src/boot/worker.ts
new file mode 100644
index 0000000000..ca3716972a
--- /dev/null
+++ b/src/boot/worker.ts
@@ -0,0 +1,20 @@
+import * as cluster from 'cluster';
+import { initDb } from '../db/postgre';
+
+/**
+ * Init worker process
+ */
+export async function workerMain() {
+ await initDb();
+
+ // start server
+ await require('../server').default();
+
+ // start job queue
+ require('../queue').default();
+
+ if (cluster.isWorker) {
+ // Send a 'ready' message to parent process
+ process.send('ready');
+ }
+}
diff --git a/src/client/app/admin/views/drive.vue b/src/client/app/admin/views/drive.vue
index 7812aadaaf..491050b1f7 100644
--- a/src/client/app/admin/views/drive.vue
+++ b/src/client/app/admin/views/drive.vue
@@ -48,7 +48,7 @@
<div>
<div>
<span style="margin-right:16px;">{{ file.type }}</span>
- <span>{{ file.datasize | bytes }}</span>
+ <span>{{ file.size | bytes }}</span>
</div>
<div><mk-time :time="file.createdAt" mode="detail"/></div>
</div>
diff --git a/src/client/app/admin/views/hashtags.vue b/src/client/app/admin/views/hashtags.vue
index b3190c29c4..e1cc4b494d 100644
--- a/src/client/app/admin/views/hashtags.vue
+++ b/src/client/app/admin/views/hashtags.vue
@@ -3,7 +3,7 @@
<ui-card>
<template #title>{{ $t('hided-tags') }}</template>
<section>
- <textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hidedTags"></textarea>
+ <textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hiddenTags"></textarea>
<ui-button @click="save">{{ $t('save') }}</ui-button>
</section>
</ui-card>
@@ -18,18 +18,18 @@ export default Vue.extend({
i18n: i18n('admin/views/hashtags.vue'),
data() {
return {
- hidedTags: '',
+ hiddenTags: '',
};
},
created() {
this.$root.getMeta().then(meta => {
- this.hidedTags = meta.hidedTags.join('\n');
+ this.hiddenTags = meta.hiddenTags.join('\n');
});
},
methods: {
save() {
this.$root.api('admin/update-meta', {
- hidedTags: this.hidedTags.split('\n')
+ hiddenTags: this.hiddenTags.split('\n')
}).then(() => {
//this.$root.os.apis.dialog({ text: `Saved` });
}).catch(e => {
diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue
index 2d2a07784b..bc2a5fba85 100644
--- a/src/client/app/admin/views/instance.vue
+++ b/src/client/app/admin/views/instance.vue
@@ -78,12 +78,6 @@
<ui-input v-model="summalyProxy">URL</ui-input>
</section>
<section>
- <header><fa :icon="faUserPlus"/> {{ $t('user-recommendation-config') }}</header>
- <ui-switch v-model="enableExternalUserRecommendation">{{ $t('enable-external-user-recommendation') }}</ui-switch>
- <ui-input v-model="externalUserRecommendationEngine" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-engine') }}<template #desc>{{ $t('external-user-recommendation-engine-desc') }}</template></ui-input>
- <ui-input v-model="externalUserRecommendationTimeout" type="number" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-timeout') }}<template #suffix>ms</template><template #desc>{{ $t('external-user-recommendation-timeout-desc') }}</template></ui-input>
- </section>
- <section>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
</section>
</ui-card>
@@ -184,9 +178,6 @@ export default Vue.extend({
discordClientSecret: null,
proxyAccount: null,
inviteCode: null,
- enableExternalUserRecommendation: false,
- externalUserRecommendationEngine: null,
- externalUserRecommendationTimeout: null,
summalyProxy: null,
enableEmail: false,
email: null,
@@ -205,8 +196,8 @@ export default Vue.extend({
created() {
this.$root.getMeta().then(meta => {
- this.maintainerName = meta.maintainer.name;
- this.maintainerEmail = meta.maintainer.email;
+ this.maintainerName = meta.maintainerName;
+ this.maintainerEmail = meta.maintainerEmail;
this.disableRegistration = meta.disableRegistration;
this.disableLocalTimeline = meta.disableLocalTimeline;
this.disableGlobalTimeline = meta.disableGlobalTimeline;
@@ -236,9 +227,6 @@ export default Vue.extend({
this.enableDiscordIntegration = meta.enableDiscordIntegration;
this.discordClientId = meta.discordClientId;
this.discordClientSecret = meta.discordClientSecret;
- this.enableExternalUserRecommendation = meta.enableExternalUserRecommendation;
- this.externalUserRecommendationEngine = meta.externalUserRecommendationEngine;
- this.externalUserRecommendationTimeout = meta.externalUserRecommendationTimeout;
this.summalyProxy = meta.summalyProxy;
this.enableEmail = meta.enableEmail;
this.email = meta.email;
@@ -299,9 +287,6 @@ export default Vue.extend({
enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret,
- enableExternalUserRecommendation: this.enableExternalUserRecommendation,
- externalUserRecommendationEngine: this.externalUserRecommendationEngine,
- externalUserRecommendationTimeout: parseInt(this.externalUserRecommendationTimeout, 10),
summalyProxy: this.summalyProxy,
enableEmail: this.enableEmail,
email: this.email,
diff --git a/src/client/app/admin/views/logs.vue b/src/client/app/admin/views/logs.vue
index 4a2d957ed7..5c2cfdb396 100644
--- a/src/client/app/admin/views/logs.vue
+++ b/src/client/app/admin/views/logs.vue
@@ -19,7 +19,7 @@
</ui-horizon-group>
<div class="nqjzuvev">
- <code v-for="log in logs" :key="log._id" :class="log.level">
+ <code v-for="log in logs" :key="log.id" :class="log.level">
<details>
<summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>
diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue
index ff485cec86..0f46b564a9 100644
--- a/src/client/app/admin/views/users.vue
+++ b/src/client/app/admin/views/users.vue
@@ -165,7 +165,7 @@ export default Vue.extend({
/** 処理対象ユーザーの情報を更新する */
async refreshUser() {
- this.$root.api('admin/show-user', { userId: this.user._id }).then(info => {
+ this.$root.api('admin/show-user', { userId: this.user.id }).then(info => {
this.user = info;
});
},
@@ -173,7 +173,7 @@ export default Vue.extend({
async resetPassword() {
if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return;
- this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => {
+ this.$root.api('admin/reset-password', { userId: this.user.id }).then(res => {
this.$root.dialog({
type: 'success',
text: this.$t('password-updated', { password: res.password })
@@ -187,7 +187,7 @@ export default Vue.extend({
this.verifying = true;
const process = async () => {
- await this.$root.api('admin/verify-user', { userId: this.user._id });
+ await this.$root.api('admin/verify-user', { userId: this.user.id });
this.$root.dialog({
type: 'success',
text: this.$t('verified')
@@ -212,7 +212,7 @@ export default Vue.extend({
this.unverifying = true;
const process = async () => {
- await this.$root.api('admin/unverify-user', { userId: this.user._id });
+ await this.$root.api('admin/unverify-user', { userId: this.user.id });
this.$root.dialog({
type: 'success',
text: this.$t('unverified')
@@ -233,7 +233,7 @@ export default Vue.extend({
async silenceUser() {
const process = async () => {
- await this.$root.api('admin/silence-user', { userId: this.user._id });
+ await this.$root.api('admin/silence-user', { userId: this.user.id });
this.$root.dialog({
type: 'success',
splash: true
@@ -252,7 +252,7 @@ export default Vue.extend({
async unsilenceUser() {
const process = async () => {
- await this.$root.api('admin/unsilence-user', { userId: this.user._id });
+ await this.$root.api('admin/unsilence-user', { userId: this.user.id });
this.$root.dialog({
type: 'success',
splash: true
@@ -275,7 +275,7 @@ export default Vue.extend({
this.suspending = true;
const process = async () => {
- await this.$root.api('admin/suspend-user', { userId: this.user._id });
+ await this.$root.api('admin/suspend-user', { userId: this.user.id });
this.$root.dialog({
type: 'success',
text: this.$t('suspended')
@@ -300,7 +300,7 @@ export default Vue.extend({
this.unsuspending = true;
const process = async () => {
- await this.$root.api('admin/unsuspend-user', { userId: this.user._id });
+ await this.$root.api('admin/unsuspend-user', { userId: this.user.id });
this.$root.dialog({
type: 'success',
text: this.$t('unsuspended')
@@ -320,7 +320,7 @@ export default Vue.extend({
},
async updateRemoteUser() {
- this.$root.api('admin/update-remote-user', { userId: this.user._id }).then(res => {
+ this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => {
this.$root.dialog({
type: 'success',
text: this.$t('remote-user-updated')
diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue
index 105af375b6..d5d6b25f00 100644
--- a/src/client/app/auth/views/form.vue
+++ b/src/client/app/auth/views/form.vue
@@ -14,15 +14,15 @@
<h2>{{ $t('permission-ask') }}</h2>
<ul>
<template v-for="p in app.permission">
- <li v-if="p == 'account-read'">{{ $t('account-read') }}</li>
- <li v-if="p == 'account-write'">{{ $t('account-write') }}</li>
- <li v-if="p == 'note-write'">{{ $t('note-write') }}</li>
+ <li v-if="p == 'read:account'">{{ $t('read:account') }}</li>
+ <li v-if="p == 'write:account'">{{ $t('write:account') }}</li>
+ <li v-if="p == 'write:notes'">{{ $t('write:notes') }}</li>
<li v-if="p == 'like-write'">{{ $t('like-write') }}</li>
- <li v-if="p == 'following-write'">{{ $t('following-write') }}</li>
- <li v-if="p == 'drive-read'">{{ $t('drive-read') }}</li>
- <li v-if="p == 'drive-write'">{{ $t('drive-write') }}</li>
- <li v-if="p == 'notification-read'">{{ $t('notification-read') }}</li>
- <li v-if="p == 'notification-write'">{{ $t('notification-write') }}</li>
+ <li v-if="p == 'write:following'">{{ $t('write:following') }}</li>
+ <li v-if="p == 'read:drive'">{{ $t('read:drive') }}</li>
+ <li v-if="p == 'write:drive'">{{ $t('write:drive') }}</li>
+ <li v-if="p == 'read:notifications'">{{ $t('read:notifications') }}</li>
+ <li v-if="p == 'write:notifications'">{{ $t('write:notifications') }}</li>
</template>
</ul>
</section>
diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
index 1efdbb1880..632ddf2ed6 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/app/common/define-widget.ts
@@ -45,15 +45,9 @@ export default function <T extends object>(data: {
this.$watch('props', () => {
this.mergeProps();
});
-
- this.bakeProps();
},
methods: {
- bakeProps() {
- this.bakedOldProps = JSON.stringify(this.props);
- },
-
mergeProps() {
if (data.props) {
const defaultProps = data.props();
@@ -65,17 +59,10 @@ export default function <T extends object>(data: {
},
save() {
- if (this.bakedOldProps == JSON.stringify(this.props)) return;
-
- this.bakeProps();
-
if (this.platform == 'deck') {
this.$store.commit('device/updateDeckColumn', this.column);
} else {
- this.$root.api('i/update_widget', {
- id: this.id,
- data: this.props
- });
+ this.$store.commit('device/updateWidget', this.widget);
}
}
}
diff --git a/src/client/app/common/scripts/note-mixin.ts b/src/client/app/common/scripts/note-mixin.ts
index 5707d1bb41..67bbe8c0ae 100644
--- a/src/client/app/common/scripts/note-mixin.ts
+++ b/src/client/app/common/scripts/note-mixin.ts
@@ -70,8 +70,8 @@ export default (opts: Opts = {}) => ({
},
reactionsCount(): number {
- return this.appearNote.reactionCounts
- ? sum(Object.values(this.appearNote.reactionCounts))
+ return this.appearNote.reactions
+ ? sum(Object.values(this.appearNote.reactions))
: 0;
},
diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts
index c2b4dd6df9..02d810ded9 100644
--- a/src/client/app/common/scripts/note-subscriber.ts
+++ b/src/client/app/common/scripts/note-subscriber.ts
@@ -87,16 +87,16 @@ export default prop => ({
case 'reacted': {
const reaction = body.reaction;
- if (this.$_ns_target.reactionCounts == null) {
- Vue.set(this.$_ns_target, 'reactionCounts', {});
+ if (this.$_ns_target.reactions == null) {
+ Vue.set(this.$_ns_target, 'reactions', {});
}
- if (this.$_ns_target.reactionCounts[reaction] == null) {
- Vue.set(this.$_ns_target.reactionCounts, reaction, 0);
+ if (this.$_ns_target.reactions[reaction] == null) {
+ Vue.set(this.$_ns_target.reactions, reaction, 0);
}
// Increment the count
- this.$_ns_target.reactionCounts[reaction]++;
+ this.$_ns_target.reactions[reaction]++;
if (body.userId == this.$store.state.i.id) {
Vue.set(this.$_ns_target, 'myReaction', reaction);
@@ -107,16 +107,16 @@ export default prop => ({
case 'unreacted': {
const reaction = body.reaction;
- if (this.$_ns_target.reactionCounts == null) {
+ if (this.$_ns_target.reactions == null) {
return;
}
- if (this.$_ns_target.reactionCounts[reaction] == null) {
+ if (this.$_ns_target.reactions[reaction] == null) {
return;
}
// Decrement the count
- if (this.$_ns_target.reactionCounts[reaction] > 0) this.$_ns_target.reactionCounts[reaction]--;
+ if (this.$_ns_target.reactions[reaction] > 0) this.$_ns_target.reactions[reaction]--;
if (body.userId == this.$store.state.i.id) {
Vue.set(this.$_ns_target, 'myReaction', null);
@@ -125,9 +125,11 @@ export default prop => ({
}
case 'pollVoted': {
- if (body.userId == this.$store.state.i.id) return;
const choice = body.choice;
- this.$_ns_target.poll.choices.find(c => c.id === choice).votes++;
+ this.$_ns_target.poll.choices[choice].votes++;
+ if (body.userId == this.$store.state.i.id) {
+ Vue.set(this.$_ns_target.poll.choices[choice], 'isVoted', true);
+ }
break;
}
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
index dce594e702..c074fb600f 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -55,11 +55,12 @@ export default Vue.extend({
},
icon(): any {
return {
- backgroundColor: this.lightmode
- ? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`
- : this.user.avatarColor && this.user.avatarColor.length == 3
- ? `rgb(${this.user.avatarColor.join(',')})`
- : null,
+ backgroundColor: this.user.avatarColor ? this.lightmode
+ ? this.user.avatarColor
+ : this.user.avatarColor.startsWith('rgb(')
+ ? this.user.avatarColor
+ : null
+ : null,
backgroundImage: this.lightmode ? null : `url(${this.url})`,
borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
};
@@ -67,7 +68,7 @@ export default Vue.extend({
},
mounted() {
if (this.user.avatarColor) {
- this.$el.style.color = `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`;
+ this.$el.style.color = this.user.avatarColor;
}
},
methods: {
diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue
index c6fc36db33..bd0401f785 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.game.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue
@@ -24,11 +24,11 @@
<div class="board">
<div class="labels-x" v-if="this.$store.state.settings.games.reversi.showBoardLabels">
- <span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
+ <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
</div>
<div class="flex">
<div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels">
- <div v-for="i in game.settings.map.length">{{ i }}</div>
+ <div v-for="i in game.map.length">{{ i }}</div>
</div>
<div class="cells" :style="cellsStyle">
<div v-for="(stone, i) in o.board"
@@ -46,11 +46,11 @@
</div>
</div>
<div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels">
- <div v-for="i in game.settings.map.length">{{ i }}</div>
+ <div v-for="i in game.map.length">{{ i }}</div>
</div>
</div>
<div class="labels-x" v-if="this.$store.state.settings.games.reversi.showBoardLabels">
- <span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
+ <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
</div>
</div>
@@ -71,9 +71,9 @@
</div>
<div class="info">
- <p v-if="game.settings.isLlotheo">{{ $t('is-llotheo') }}</p>
- <p v-if="game.settings.loopedBoard">{{ $t('looped-map') }}</p>
- <p v-if="game.settings.canPutEverywhere">{{ $t('can-put-everywhere') }}</p>
+ <p v-if="game.isLlotheo">{{ $t('is-llotheo') }}</p>
+ <p v-if="game.loopedBoard">{{ $t('looped-map') }}</p>
+ <p v-if="game.canPutEverywhere">{{ $t('can-put-everywhere') }}</p>
</div>
</div>
</template>
@@ -160,8 +160,8 @@ export default Vue.extend({
cellsStyle(): any {
return {
- 'grid-template-rows': `repeat(${this.game.settings.map.length}, 1fr)`,
- 'grid-template-columns': `repeat(${this.game.settings.map[0].length}, 1fr)`
+ 'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`,
+ 'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)`
};
}
},
@@ -169,10 +169,10 @@ export default Vue.extend({
watch: {
logPos(v) {
if (!this.game.isEnded) return;
- this.o = new Reversi(this.game.settings.map, {
- isLlotheo: this.game.settings.isLlotheo,
- canPutEverywhere: this.game.settings.canPutEverywhere,
- loopedBoard: this.game.settings.loopedBoard
+ this.o = new Reversi(this.game.map, {
+ isLlotheo: this.game.isLlotheo,
+ canPutEverywhere: this.game.canPutEverywhere,
+ loopedBoard: this.game.loopedBoard
});
for (const log of this.logs.slice(0, v)) {
this.o.put(log.color, log.pos);
@@ -184,10 +184,10 @@ export default Vue.extend({
created() {
this.game = this.initGame;
- this.o = new Reversi(this.game.settings.map, {
- isLlotheo: this.game.settings.isLlotheo,
- canPutEverywhere: this.game.settings.canPutEverywhere,
- loopedBoard: this.game.settings.loopedBoard
+ this.o = new Reversi(this.game.map, {
+ isLlotheo: this.game.isLlotheo,
+ canPutEverywhere: this.game.canPutEverywhere,
+ loopedBoard: this.game.loopedBoard
});
for (const log of this.game.logs) {
@@ -286,10 +286,10 @@ export default Vue.extend({
onRescue(game) {
this.game = game;
- this.o = new Reversi(this.game.settings.map, {
- isLlotheo: this.game.settings.isLlotheo,
- canPutEverywhere: this.game.settings.canPutEverywhere,
- loopedBoard: this.game.settings.loopedBoard
+ this.o = new Reversi(this.game.map, {
+ isLlotheo: this.game.isLlotheo,
+ canPutEverywhere: this.game.canPutEverywhere,
+ loopedBoard: this.game.loopedBoard
});
for (const log of this.game.logs) {
diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue
index d5d148790c..9ee1a78b86 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.room.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue
@@ -17,9 +17,9 @@
</header>
<div>
- <div class="random" v-if="game.settings.map == null"><fa icon="dice"/></div>
- <div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
- <div v-for="(x, i) in game.settings.map.join('')"
+ <div class="random" v-if="game.map == null"><fa icon="dice"/></div>
+ <div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
+ <div v-for="(x, i) in game.map.join('')"
:data-none="x == ' '"
@click="onPixelClick(i, x)">
<fa v-if="x == 'b'" :icon="fasCircle"/>
@@ -35,9 +35,9 @@
</header>
<div>
- <form-radio v-model="game.settings.bw" value="random" @change="updateSettings">{{ $t('random') }}</form-radio>
- <form-radio v-model="game.settings.bw" :value="1" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
- <form-radio v-model="game.settings.bw" :value="2" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
+ <form-radio v-model="game.bw" value="random" @change="updateSettings('bw')">{{ $t('random') }}</form-radio>
+ <form-radio v-model="game.bw" :value="1" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
+ <form-radio v-model="game.bw" :value="2" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
</div>
</div>
@@ -47,9 +47,9 @@
</header>
<div>
- <ui-switch v-model="game.settings.isLlotheo" @change="updateSettings">{{ $t('is-llotheo') }}</ui-switch>
- <ui-switch v-model="game.settings.loopedBoard" @change="updateSettings">{{ $t('looped-map') }}</ui-switch>
- <ui-switch v-model="game.settings.canPutEverywhere" @change="updateSettings">{{ $t('can-put-everywhere') }}</ui-switch>
+ <ui-switch v-model="game.isLlotheo" @change="updateSettings('isLlotheo')">{{ $t('is-llotheo') }}</ui-switch>
+ <ui-switch v-model="game.loopedBoard" @change="updateSettings('loopedBoard')">{{ $t('looped-map') }}</ui-switch>
+ <ui-switch v-model="game.canPutEverywhere" @change="updateSettings('canPutEverywhere')">{{ $t('can-put-everywhere') }}</ui-switch>
</div>
</div>
@@ -159,8 +159,8 @@ export default Vue.extend({
this.connection.on('initForm', this.onInitForm);
this.connection.on('message', this.onMessage);
- if (this.game.user1Id != this.$store.state.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
- if (this.game.user2Id != this.$store.state.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
+ if (this.game.user1Id != this.$store.state.i.id && this.game.form1) this.form = this.game.form1;
+ if (this.game.user2Id != this.$store.state.i.id && this.game.form2) this.form = this.game.form2;
},
beforeDestroy() {
@@ -189,18 +189,19 @@ export default Vue.extend({
this.$forceUpdate();
},
- updateSettings() {
+ updateSettings(key: string) {
this.connection.send('updateSettings', {
- settings: this.game.settings
+ key: key,
+ value: this.game[key]
});
},
- onUpdateSettings(settings) {
- this.game.settings = settings;
- if (this.game.settings.map == null) {
+ onUpdateSettings({ key, value }) {
+ this.game[key] = value;
+ if (this.game.map == null) {
this.mapName = null;
} else {
- const found = Object.values(maps).find(x => x.data.join('') == this.game.settings.map.join(''));
+ const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join(''));
this.mapName = found ? found.name : '-Custom-';
}
},
@@ -224,27 +225,27 @@ export default Vue.extend({
onMapChange() {
if (this.mapName == null) {
- this.game.settings.map = null;
+ this.game.map = null;
} else {
- this.game.settings.map = Object.values(maps).find(x => x.name == this.mapName).data;
+ this.game.map = Object.values(maps).find(x => x.name == this.mapName).data;
}
this.$forceUpdate();
this.updateSettings();
},
onPixelClick(pos, pixel) {
- const x = pos % this.game.settings.map[0].length;
- const y = Math.floor(pos / this.game.settings.map[0].length);
+ const x = pos % this.game.map[0].length;
+ const y = Math.floor(pos / this.game.map[0].length);
const newPixel =
pixel == ' ' ? '-' :
pixel == '-' ? 'b' :
pixel == 'b' ? 'w' :
' ';
- const line = this.game.settings.map[y].split('');
+ const line = this.game.map[y].split('');
line[x] = newPixel;
- this.$set(this.game.settings.map, y, line.join(''));
+ this.$set(this.game.map, y, line.join(''));
this.$forceUpdate();
- this.updateSettings();
+ this.updateSettings('map');
}
}
});
diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue
index b6803cd7f7..d33471a049 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.vue
@@ -106,7 +106,7 @@ export default Vue.extend({
async nav(game, actualNav = true) {
if (this.selfNav) {
// 受け取ったゲーム情報が省略されたものなら完全な情報を取得する
- if (game != null && (game.settings == null || game.settings.map == null)) {
+ if (game != null && game.map == null) {
game = await this.$root.api('games/reversi/games/show', {
gameId: game.id
});
diff --git a/src/client/app/common/views/components/instance.vue b/src/client/app/common/views/components/instance.vue
index 7b8d4f8e0b..497e4976f5 100644
--- a/src/client/app/common/views/components/instance.vue
+++ b/src/client/app/common/views/components/instance.vue
@@ -2,7 +2,7 @@
<div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta">
<div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div>
- <h1>{{ meta.name }}</h1>
+ <h1>{{ meta.name || 'Misskey' }}</h1>
<p v-html="meta.description || this.$t('@.about')"></p>
<router-link to="/">{{ $t('start') }}</router-link>
</div>
diff --git a/src/client/app/common/views/components/mention.vue b/src/client/app/common/views/components/mention.vue
index 11dddbd52a..e1f67282b6 100644
--- a/src/client/app/common/views/components/mention.vue
+++ b/src/client/app/common/views/components/mention.vue
@@ -33,7 +33,7 @@ export default Vue.extend({
},
computed: {
canonical(): string {
- return `@${this.username}@${toUnicode(this.host)}`;
+ return this.host === localHost ? `@${this.username}` : `@${this.username}@${toUnicode(this.host)}`;
},
isMe(): boolean {
return this.$store.getters.isSignedIn && this.canonical.toLowerCase() === `@${this.$store.state.i.username}@${toUnicode(localHost)}`.toLowerCase();
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index ba14ba3a44..dc3aaa34f3 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -1,7 +1,7 @@
<template>
<div class="mk-poll" :data-done="closed || isVoted">
<ul>
- <li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''">
+ <li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''">
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span>
<template v-if="choice.isVoted"><fa icon="check"/></template>
@@ -82,12 +82,6 @@ export default Vue.extend({
noteId: this.note.id,
choice: id
}).then(() => {
- for (const c of this.poll.choices) {
- if (c.id == id) {
- c.votes++;
- Vue.set(c, 'isVoted', true);
- }
- }
if (!this.showResult) this.showResult = !this.poll.multiple;
});
}
diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue
index cf7f88b2f5..46668054b8 100644
--- a/src/client/app/common/views/components/reactions-viewer.vue
+++ b/src/client/app/common/views/components/reactions-viewer.vue
@@ -20,7 +20,7 @@ export default Vue.extend({
},
computed: {
reactions(): any {
- return this.note.reactionCounts;
+ return this.note.reactions;
},
isMe(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId;
diff --git a/src/client/app/common/views/components/settings/notification.vue b/src/client/app/common/views/components/settings/notification.vue
index b689544d69..2554fe6331 100644
--- a/src/client/app/common/views/components/settings/notification.vue
+++ b/src/client/app/common/views/components/settings/notification.vue
@@ -2,7 +2,7 @@
<ui-card>
<template #title><fa :icon="['far', 'bell']"/> {{ $t('title') }}</template>
<section>
- <ui-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch">
+ <ui-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
{{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template>
</ui-switch>
<section>
diff --git a/src/client/app/common/views/components/settings/profile.vue b/src/client/app/common/views/components/settings/profile.vue
index b9837a6966..acfc1875a6 100644
--- a/src/client/app/common/views/components/settings/profile.vue
+++ b/src/client/app/common/views/components/settings/profile.vue
@@ -158,14 +158,14 @@ export default Vue.extend({
computed: {
alwaysMarkNsfw: {
- get() { return this.$store.state.i.settings.alwaysMarkNsfw; },
+ get() { return this.$store.state.i.alwaysMarkNsfw; },
set(value) { this.$root.api('i/update', { alwaysMarkNsfw: value }); }
},
bannerStyle(): any {
if (this.$store.state.i.bannerUrl == null) return {};
return {
- backgroundColor: this.$store.state.i.bannerColor && this.$store.state.i.bannerColor.length == 3 ? `rgb(${ this.$store.state.i.bannerColor.join(',') })` : null,
+ backgroundColor: this.$store.state.i.bannerColor ? this.$store.state.i.bannerColor : null,
backgroundImage: `url(${ this.$store.state.i.bannerUrl })`
};
},
@@ -178,10 +178,10 @@ export default Vue.extend({
this.email = this.$store.state.i.email;
this.name = this.$store.state.i.name;
this.username = this.$store.state.i.username;
- this.location = this.$store.state.i.profile.location;
+ this.location = this.$store.state.i.location;
this.description = this.$store.state.i.description;
this.lang = this.$store.state.i.lang;
- this.birthday = this.$store.state.i.profile.birthday;
+ this.birthday = this.$store.state.i.birthday;
this.avatarId = this.$store.state.i.avatarId;
this.bannerId = this.$store.state.i.bannerId;
this.isCat = this.$store.state.i.isCat;
diff --git a/src/client/app/common/views/components/settings/theme.vue b/src/client/app/common/views/components/settings/theme.vue
index 1dff61e459..3440aacb28 100644
--- a/src/client/app/common/views/components/settings/theme.vue
+++ b/src/client/app/common/views/components/settings/theme.vue
@@ -130,20 +130,6 @@ import * as tinycolor from 'tinycolor2';
import * as JSON5 from 'json5';
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
-// 後方互換性のため
-function convertOldThemedefinition(t) {
- const t2 = {
- id: t.meta.id,
- name: t.meta.name,
- author: t.meta.author,
- base: t.meta.base,
- vars: t.meta.vars,
- props: t
- };
- delete t2.props.meta;
- return t2;
-}
-
export default Vue.extend({
i18n: i18n('common/views/components/theme.vue'),
components: {
@@ -231,20 +217,6 @@ export default Vue.extend({
}
},
- beforeCreate() {
- // migrate old theme definitions
- // 後方互換性のため
- this.$store.commit('device/set', {
- key: 'themes', value: this.$store.state.device.themes.map(t => {
- if (t.id == null) {
- return convertOldThemedefinition(t);
- } else {
- return t;
- }
- })
- });
- },
-
methods: {
install(code) {
let theme;
@@ -259,11 +231,6 @@ export default Vue.extend({
return;
}
- // 後方互換性のため
- if (theme.id == null && theme.meta != null) {
- theme = convertOldThemedefinition(theme);
- }
-
if (theme.id == null) {
this.$root.dialog({
type: 'error',
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 16e1afaa94..45c2eabd45 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -4,7 +4,7 @@
<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill">
<span>{{ $t('invitation-code') }}</span>
<template #prefix><fa icon="id-card-alt"/></template>
- <template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainer.email)"></template>
+ <template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainerEmail)"></template>
</ui-input>
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill">
<span>{{ $t('username') }}</span>
diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue
index 536d55247c..cd67cc0092 100644
--- a/src/client/app/common/views/components/trends.vue
+++ b/src/client/app/common/views/components/trends.vue
@@ -4,9 +4,9 @@
<p class="empty" v-else-if="stats.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p>
<!-- トランジションを有効にするとなぜかメモリリークする -->
<transition-group v-else tag="div" name="chart">
- <div v-for="stat in stats" :key="stat.tag">
+ <div v-for="stat in stats" :key="stat.name">
<div class="tag">
- <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
+ <router-link :to="`/tags/${ encodeURIComponent(stat.name) }`" :title="stat.name">#{{ stat.name }}</router-link>
<p>{{ $t('count').replace('{}', stat.usersCount) }}</p>
</div>
<x-chart class="chart" :src="stat.chart"/>
diff --git a/src/client/app/common/views/components/user-list-editor.vue b/src/client/app/common/views/components/user-list-editor.vue
index 53c945ca0a..8d2e04d045 100644
--- a/src/client/app/common/views/components/user-list-editor.vue
+++ b/src/client/app/common/views/components/user-list-editor.vue
@@ -1,7 +1,7 @@
<template>
<div class="cudqjmnl">
<ui-card>
- <template #title><fa :icon="faList"/> {{ list.title }}</template>
+ <template #title><fa :icon="faList"/> {{ list.name }}</template>
<section>
<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button>
@@ -75,7 +75,7 @@ export default Vue.extend({
this.$root.dialog({
title: this.$t('rename'),
input: {
- default: this.list.title
+ default: this.list.name
}
}).then(({ canceled, result: title }) => {
if (canceled) return;
@@ -89,7 +89,7 @@ export default Vue.extend({
del() {
this.$root.dialog({
type: 'warning',
- text: this.$t('delete-are-you-sure').replace('$1', this.list.title),
+ text: this.$t('delete-are-you-sure').replace('$1', this.list.name),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue
index 93fd759fd9..a95f7a9225 100644
--- a/src/client/app/common/views/components/user-menu.vue
+++ b/src/client/app/common/views/components/user-menu.vue
@@ -73,7 +73,7 @@ export default Vue.extend({
title: t,
select: {
items: lists.map(list => ({
- value: list.id, text: list.title
+ value: list.id, text: list.name
}))
},
showCancelButton: true
diff --git a/src/client/app/common/views/deck/deck.column-core.vue b/src/client/app/common/views/deck/deck.column-core.vue
index 974c58235d..e3f92dea16 100644
--- a/src/client/app/common/views/deck/deck.column-core.vue
+++ b/src/client/app/common/views/deck/deck.column-core.vue
@@ -3,7 +3,7 @@
<x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
+<x-tl-column v-else-if="column.type == 'social'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
diff --git a/src/client/app/common/views/deck/deck.hashtag-tl.vue b/src/client/app/common/views/deck/deck.hashtag-tl.vue
index 07d96f82c4..b94267b74b 100644
--- a/src/client/app/common/views/deck/deck.hashtag-tl.vue
+++ b/src/client/app/common/views/deck/deck.hashtag-tl.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
data() {
return {
connection: null,
- makePromise: cursor => this.$root.api('notes/search_by_tag', {
+ makePromise: cursor => this.$root.api('notes/search-by-tag', {
limit: fetchLimit + 1,
untilId: cursor ? cursor : undefined,
withFiles: this.mediaOnly,
diff --git a/src/client/app/common/views/deck/deck.notification.vue b/src/client/app/common/views/deck/deck.notification.vue
index 6a116260e5..3ced7b7e23 100644
--- a/src/client/app/common/views/deck/deck.notification.vue
+++ b/src/client/app/common/views/deck/deck.notification.vue
@@ -62,7 +62,7 @@
</div>
</div>
- <div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
+ <div class="notification pollVote" v-if="notification.type == 'pollVote'">
<mk-avatar class="avatar" :user="notification.user"/>
<div>
<header>
diff --git a/src/client/app/common/views/deck/deck.tl-column.vue b/src/client/app/common/views/deck/deck.tl-column.vue
index d53aabaea5..f6a9ee5286 100644
--- a/src/client/app/common/views/deck/deck.tl-column.vue
+++ b/src/client/app/common/views/deck/deck.tl-column.vue
@@ -3,7 +3,7 @@
<template #header>
<fa v-if="column.type == 'home'" icon="home"/>
<fa v-if="column.type == 'local'" :icon="['far', 'comments']"/>
- <fa v-if="column.type == 'hybrid'" icon="share-alt"/>
+ <fa v-if="column.type == 'social'" icon="share-alt"/>
<fa v-if="column.type == 'global'" icon="globe"/>
<fa v-if="column.type == 'list'" icon="list"/>
<fa v-if="column.type == 'hashtag'" icon="hashtag"/>
@@ -80,9 +80,9 @@ export default Vue.extend({
switch (this.column.type) {
case 'home': return this.$t('@deck.home');
case 'local': return this.$t('@deck.local');
- case 'hybrid': return this.$t('@deck.hybrid');
+ case 'social': return this.$t('@deck.social');
case 'global': return this.$t('@deck.global');
- case 'list': return this.column.list.title;
+ case 'list': return this.column.list.name;
case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
}
}
diff --git a/src/client/app/common/views/deck/deck.tl.vue b/src/client/app/common/views/deck/deck.tl.vue
index 35cdfa704f..5381cfbd5e 100644
--- a/src/client/app/common/views/deck/deck.tl.vue
+++ b/src/client/app/common/views/deck/deck.tl.vue
@@ -51,7 +51,7 @@ export default Vue.extend({
switch (this.src) {
case 'home': return this.$root.stream.useSharedConnection('homeTimeline');
case 'local': return this.$root.stream.useSharedConnection('localTimeline');
- case 'hybrid': return this.$root.stream.useSharedConnection('hybridTimeline');
+ case 'social': return this.$root.stream.useSharedConnection('socialTimeline');
case 'global': return this.$root.stream.useSharedConnection('globalTimeline');
}
},
@@ -60,7 +60,7 @@ export default Vue.extend({
switch (this.src) {
case 'home': return 'notes/timeline';
case 'local': return 'notes/local-timeline';
- case 'hybrid': return 'notes/hybrid-timeline';
+ case 'social': return 'notes/social-timeline';
case 'global': return 'notes/global-timeline';
}
},
@@ -107,7 +107,7 @@ export default Vue.extend({
this.$root.getMeta().then(meta => {
this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && (
- meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) ||
+ meta.disableLocalTimeline && ['local', 'social'].includes(this.src) ||
meta.disableGlobalTimeline && ['global'].includes(this.src));
});
},
diff --git a/src/client/app/common/views/deck/deck.vue b/src/client/app/common/views/deck/deck.vue
index 8ffb3223f9..a1bef84008 100644
--- a/src/client/app/common/views/deck/deck.vue
+++ b/src/client/app/common/views/deck/deck.vue
@@ -106,16 +106,6 @@ export default Vue.extend({
value: deck
});
}
-
- // 互換性のため
- if (this.$store.state.device.deck != null && this.$store.state.device.deck.layout == null) {
- this.$store.commit('device/set', {
- key: 'deck',
- value: Object.assign({}, this.$store.state.device.deck, {
- layout: this.$store.state.device.deck.columns.map(c => [c.id])
- })
- });
- }
},
mounted() {
@@ -155,11 +145,11 @@ export default Vue.extend({
}
}, {
icon: 'share-alt',
- text: this.$t('@deck.hybrid'),
+ text: this.$t('@deck.social'),
action: () => {
this.$store.commit('device/addDeckColumn', {
id: uuid(),
- type: 'hybrid'
+ type: 'social'
});
}
}, {
@@ -199,7 +189,7 @@ export default Vue.extend({
title: this.$t('@deck.select-list'),
select: {
items: lists.map(list => ({
- value: list.id, text: list.title
+ value: list.id, text: list.name
}))
},
showCancelButton: true
@@ -312,7 +302,7 @@ export default Vue.extend({
isTlColumn(id) {
const column = this.columns.find(c => c.id === id);
- return ['home', 'local', 'hybrid', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type);
+ return ['home', 'local', 'social', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type);
}
}
});
diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue
index 098bf1f4c4..67e92af445 100644
--- a/src/client/app/common/views/pages/explore.vue
+++ b/src/client/app/common/views/pages/explore.vue
@@ -3,7 +3,7 @@
<ui-container :show-header="false" v-if="meta && stats">
<div class="kpdsmpnk" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
<div>
- <router-link to="/explore" class="title">{{ $t('explore', { host: meta.name }) }}</router-link>
+ <router-link to="/explore" class="title">{{ $t('explore', { host: meta.name || 'Misskey' }) }}</router-link>
<span>{{ $t('users-info', { users: num(stats.originalUsersCount) }) }}</span>
</div>
</div>
@@ -13,8 +13,8 @@
<template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popular-tags') }}</template>
<div class="vxjfqztj">
- <router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link>
- <router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link>
+ <router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.name}`" :key="'local:' + tag.name" class="local">{{ tag.name }}</router-link>
+ <router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.name}`" :key="'remote:' + tag.name">{{ tag.name }}</router-link>
</div>
</ui-container>
diff --git a/src/client/app/common/views/pages/followers.vue b/src/client/app/common/views/pages/followers.vue
index 94d9c9b13c..67cfb8512f 100644
--- a/src/client/app/common/views/pages/followers.vue
+++ b/src/client/app/common/views/pages/followers.vue
@@ -9,20 +9,30 @@ import Vue from 'vue';
import parseAcct from '../../../../../misc/acct/parse';
import i18n from '../../../i18n';
+const fetchLimit = 30;
+
export default Vue.extend({
- i18n: i18n(''),
+ i18n: i18n(),
data() {
return {
makePromise: cursor => this.$root.api('users/followers', {
...parseAcct(this.$route.params.user),
- limit: 30,
- cursor: cursor ? cursor : undefined
- }).then(x => {
- return {
- users: x.users,
- cursor: x.next
- };
+ limit: fetchLimit + 1,
+ untilId: cursor ? cursor : undefined,
+ }).then(followings => {
+ if (followings.length == fetchLimit + 1) {
+ followings.pop();
+ return {
+ users: followings.map(following => following.follower),
+ cursor: followings[followings.length - 1].id
+ };
+ } else {
+ return {
+ users: followings.map(following => following.follower),
+ cursor: null
+ };
+ }
}),
};
},
diff --git a/src/client/app/common/views/pages/following.vue b/src/client/app/common/views/pages/following.vue
index 39739fa3da..518a63ac1b 100644
--- a/src/client/app/common/views/pages/following.vue
+++ b/src/client/app/common/views/pages/following.vue
@@ -7,19 +7,32 @@
<script lang="ts">
import Vue from 'vue';
import parseAcct from '../../../../../misc/acct/parse';
+import i18n from '../../../i18n';
+
+const fetchLimit = 30;
export default Vue.extend({
+ i18n: i18n(),
+
data() {
return {
makePromise: cursor => this.$root.api('users/following', {
...parseAcct(this.$route.params.user),
- limit: 30,
- cursor: cursor ? cursor : undefined
- }).then(x => {
- return {
- users: x.users,
- cursor: x.next
- };
+ limit: fetchLimit + 1,
+ untilId: cursor ? cursor : undefined,
+ }).then(followings => {
+ if (followings.length == fetchLimit + 1) {
+ followings.pop();
+ return {
+ users: followings.map(following => following.followee),
+ cursor: followings[followings.length - 1].id
+ };
+ } else {
+ return {
+ users: followings.map(following => following.followee),
+ cursor: null
+ };
+ }
}),
};
},
diff --git a/src/client/app/common/views/pages/share.vue b/src/client/app/common/views/pages/share.vue
index 760350b921..0452b25dfc 100644
--- a/src/client/app/common/views/pages/share.vue
+++ b/src/client/app/common/views/pages/share.vue
@@ -42,7 +42,7 @@ export default Vue.extend({
},
mounted() {
this.$root.getMeta().then(meta => {
- this.name = meta.name;
+ this.name = meta.name || 'Misskey';
});
}
});
diff --git a/src/client/app/common/views/widgets/server.info.vue b/src/client/app/common/views/widgets/server.info.vue
index f7efb6fa2a..a97b4ec496 100644
--- a/src/client/app/common/views/widgets/server.info.vue
+++ b/src/client/app/common/views/widgets/server.info.vue
@@ -1,6 +1,6 @@
<template>
<div class="info">
- <p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+ <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p>
<p>Machine: {{ meta.machine }}</p>
<p>Node: {{ meta.node }}</p>
<p>Version: {{ meta.version }} </p>
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index c560e6d97e..5b9ff81c0d 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -60,7 +60,7 @@ export default Vue.extend({
return this.browser.selectedFiles.some(f => f.id == this.file.id);
},
title(): string {
- return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
+ return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`;
}
},
methods: {
diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue
index 9b36716e83..585294fc89 100644
--- a/src/client/app/desktop/views/components/note.vue
+++ b/src/client/app/desktop/views/components/note.vue
@@ -54,11 +54,11 @@
</button>
<button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton button" @click="react()" ref="reactButton" :title="$t('add-reaction')">
<fa icon="plus"/>
- <p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p>
+ <p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p>
</button>
<button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted button" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')">
<fa icon="minus"/>
- <p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p>
+ <p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p>
</button>
<button @click="menu()" ref="menuButton" class="button">
<fa icon="ellipsis-h"/>
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 24b6fc3eba..0bf0132926 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -110,7 +110,7 @@
</div>
</template>
- <template v-if="notification.type == 'poll_vote'">
+ <template v-if="notification.type == 'pollVote'">
<mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
<p><fa icon="chart-pie"/><a :href="notification.user | userPage" v-user-preview="notification.user.id">
diff --git a/src/client/app/desktop/views/components/user-list-window.vue b/src/client/app/desktop/views/components/user-list-window.vue
index afece9fe86..6764579b20 100644
--- a/src/client/app/desktop/views/components/user-list-window.vue
+++ b/src/client/app/desktop/views/components/user-list-window.vue
@@ -1,6 +1,6 @@
<template>
<mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
- <template #header><fa icon="list"/> {{ list.title }}</template>
+ <template #header><fa icon="list"/> {{ list.name }}</template>
<x-editor :list="list"/>
</mk-window>
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index 4f0af4a278..7afcd6aa3b 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -4,7 +4,7 @@
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
<button class="ui" @click="add">{{ $t('create-list') }}</button>
- <a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
+ <a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a>
</div>
</mk-window>
</template>
diff --git a/src/client/app/desktop/views/home/home.vue b/src/client/app/desktop/views/home/home.vue
index fb7af5a9ad..d0b2fc10bc 100644
--- a/src/client/app/desktop/views/home/home.vue
+++ b/src/client/app/desktop/views/home/home.vue
@@ -101,7 +101,7 @@ export default Vue.extend({
computed: {
home(): any[] {
if (this.$store.getters.isSignedIn) {
- return this.$store.state.settings.home || [];
+ return this.$store.state.device.home || [];
} else {
return [{
name: 'instance',
@@ -182,12 +182,8 @@ export default Vue.extend({
}
//#endregion
- if (this.$store.state.settings.home == null) {
- this.$root.api('i/update_home', {
- home: _defaultDesktopHomeWidgets
- }).then(() => {
- this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets);
- });
+ if (this.$store.state.device.home == null) {
+ this.$store.commit('device/setHome', _defaultDesktopHomeWidgets);
}
}
},
@@ -226,7 +222,7 @@ export default Vue.extend({
},
addWidget() {
- this.$store.dispatch('settings/addHomeWidget', {
+ this.$store.commit('device/addHomeWidget', {
name: this.widgetAdderSelected,
id: uuid(),
place: 'left',
@@ -237,12 +233,9 @@ export default Vue.extend({
saveHome() {
const left = this.widgets.left;
const right = this.widgets.right;
- this.$store.commit('settings/setHome', left.concat(right));
+ this.$store.commit('device/setHome', left.concat(right));
for (const w of left) w.place = 'left';
for (const w of right) w.place = 'right';
- this.$root.api('i/update_home', {
- home: this.home
- });
},
done() {
diff --git a/src/client/app/desktop/views/home/tag.vue b/src/client/app/desktop/views/home/tag.vue
index 4f9bc66e7b..98d89955b3 100644
--- a/src/client/app/desktop/views/home/tag.vue
+++ b/src/client/app/desktop/views/home/tag.vue
@@ -21,7 +21,7 @@ export default Vue.extend({
i18n: i18n('desktop/views/pages/tag.vue'),
data() {
return {
- makePromise: cursor => this.$root.api('notes/search_by_tag', {
+ makePromise: cursor => this.$root.api('notes/search-by-tag', {
limit: limit + 1,
offset: cursor ? cursor : undefined,
tag: this.$route.params.tag
diff --git a/src/client/app/desktop/views/home/timeline.core.vue b/src/client/app/desktop/views/home/timeline.core.vue
index e306ac873c..bf07b69dbf 100644
--- a/src/client/app/desktop/views/home/timeline.core.vue
+++ b/src/client/app/desktop/views/home/timeline.core.vue
@@ -58,7 +58,7 @@ export default Vue.extend({
};
if (this.src == 'tag') {
- this.endpoint = 'notes/search_by_tag';
+ this.endpoint = 'notes/search-by-tag';
this.query = {
query: this.tagTl.query
};
@@ -77,9 +77,9 @@ export default Vue.extend({
this.endpoint = 'notes/local-timeline';
this.connection = this.$root.stream.useSharedConnection('localTimeline');
this.connection.on('note', prepend);
- } else if (this.src == 'hybrid') {
- this.endpoint = 'notes/hybrid-timeline';
- this.connection = this.$root.stream.useSharedConnection('hybridTimeline');
+ } else if (this.src == 'social') {
+ this.endpoint = 'notes/social-timeline';
+ this.connection = this.$root.stream.useSharedConnection('socialTimeline');
this.connection.on('note', prepend);
} else if (this.src == 'global') {
this.endpoint = 'notes/global-timeline';
diff --git a/src/client/app/desktop/views/home/timeline.vue b/src/client/app/desktop/views/home/timeline.vue
index 0b8ced4795..ccd55d1d7a 100644
--- a/src/client/app/desktop/views/home/timeline.vue
+++ b/src/client/app/desktop/views/home/timeline.vue
@@ -6,10 +6,10 @@
<header class="zahtxcqi">
<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
- <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span>
+ <span :data-active="src == 'social'" @click="src = 'social'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('social') }}</span>
<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span>
<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span>
- <span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span>
+ <span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</span>
<div class="buttons">
<button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button>
<button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button>
@@ -78,7 +78,7 @@ export default Vue.extend({
) && this.src === 'global') this.src = 'local';
if (!(
this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
- ) && ['local', 'hybrid'].includes(this.src)) this.src = 'home';
+ ) && ['local', 'social'].includes(this.src)) this.src = 'home';
});
if (this.$store.state.device.tl) {
@@ -89,7 +89,7 @@ export default Vue.extend({
this.tagTl = this.$store.state.device.tl.arg;
}
} else if (this.$store.state.i.followingCount == 0) {
- this.src = 'hybrid';
+ this.src = 'social';
}
},
@@ -143,7 +143,7 @@ export default Vue.extend({
menu = menu.concat(lists.map(list => ({
icon: 'list',
- text: list.title,
+ text: list.name,
action: () => {
this.list = list;
this.src = 'list';
diff --git a/src/client/app/desktop/views/home/user/user.header.vue b/src/client/app/desktop/views/home/user/user.header.vue
index 85dcd3ddae..61c3839c14 100644
--- a/src/client/app/desktop/views/home/user/user.header.vue
+++ b/src/client/app/desktop/views/home/user/user.header.vue
@@ -36,8 +36,8 @@
</dl>
</div>
<div class="info">
- <span class="location" v-if="user.host === null && user.profile.location"><fa icon="map-marker"/> {{ user.profile.location }}</span>
- <span class="birthday" v-if="user.host === null && user.profile.birthday"><fa icon="birthday-cake"/> {{ user.profile.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span>
+ <span class="location" v-if="user.host === null && user.location"><fa icon="map-marker"/> {{ user.location }}</span>
+ <span class="birthday" v-if="user.host === null && user.birthday"><fa icon="birthday-cake"/> {{ user.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span>
</div>
<div class="status">
<router-link :to="user | userPage()" class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</router-link>
@@ -71,7 +71,7 @@ export default Vue.extend({
},
age(): number {
- return age(this.user.profile.birthday);
+ return age(this.user.birthday);
}
},
mounted() {
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index ddffeae408..5a5cd9c8e6 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -13,8 +13,8 @@
<div class="body">
<div class="main block">
<div>
- <h1 v-if="name != 'Misskey'">{{ name }}</h1>
- <h1 v-else><img svg-inline src="../../../../assets/title.svg" :alt="name"></h1>
+ <h1 v-if="name != null">{{ name }}</h1>
+ <h1 v-else><img svg-inline src="../../../../assets/title.svg" alt="Misskey"></h1>
<div class="info">
<span><b>{{ host }}</b> - <span v-html="$t('powered-by-misskey')"></span></span>
@@ -87,7 +87,7 @@
<div>
<div v-if="meta" class="body">
<p>Version: <b>{{ meta.version }}</b></p>
- <p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+ <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p>
</div>
</div>
</div>
@@ -162,7 +162,7 @@ export default Vue.extend({
banner: null,
copyright,
host: toUnicode(host),
- name: 'Misskey',
+ name: null,
description: '',
announcements: [],
photos: []
diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue
index d8c128904a..00f2ed60d9 100644
--- a/src/client/app/dev/views/new-app.vue
+++ b/src/client/app/dev/views/new-app.vue
@@ -15,15 +15,21 @@
<b-form-group :description="$t('description')">
<b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert>
<b-form-checkbox-group v-model="permission" stacked>
- <b-form-checkbox value="account-read">{{ $t('account-read') }}</b-form-checkbox>
- <b-form-checkbox value="account-write">{{ $t('account-write') }}</b-form-checkbox>
- <b-form-checkbox value="note-write">{{ $t('note-write') }}</b-form-checkbox>
- <b-form-checkbox value="reaction-write">{{ $t('reaction-write') }}</b-form-checkbox>
- <b-form-checkbox value="following-write">{{ $t('following-write') }}</b-form-checkbox>
- <b-form-checkbox value="drive-read">{{ $t('drive-read') }}</b-form-checkbox>
- <b-form-checkbox value="drive-write">{{ $t('drive-write') }}</b-form-checkbox>
- <b-form-checkbox value="notification-read">{{ $t('notification-read') }}</b-form-checkbox>
- <b-form-checkbox value="notification-write">{{ $t('notification-write') }}</b-form-checkbox>
+ <b-form-checkbox value="read:account">{{ $t('read:account') }}</b-form-checkbox>
+ <b-form-checkbox value="write:account">{{ $t('write:account') }}</b-form-checkbox>
+ <b-form-checkbox value="write:notes">{{ $t('write:notes') }}</b-form-checkbox>
+ <b-form-checkbox value="read:reactions">{{ $t('read:reactions') }}</b-form-checkbox>
+ <b-form-checkbox value="write:reactions">{{ $t('write:reactions') }}</b-form-checkbox>
+ <b-form-checkbox value="read:following">{{ $t('read:following') }}</b-form-checkbox>
+ <b-form-checkbox value="write:following">{{ $t('write:following') }}</b-form-checkbox>
+ <b-form-checkbox value="read:mutes">{{ $t('read:mutes') }}</b-form-checkbox>
+ <b-form-checkbox value="write:mutes">{{ $t('write:mutes') }}</b-form-checkbox>
+ <b-form-checkbox value="read:blocks">{{ $t('read:blocks') }}</b-form-checkbox>
+ <b-form-checkbox value="write:blocks">{{ $t('write:blocks') }}</b-form-checkbox>
+ <b-form-checkbox value="read:drive">{{ $t('read:drive') }}</b-form-checkbox>
+ <b-form-checkbox value="write:drive">{{ $t('write:drive') }}</b-form-checkbox>
+ <b-form-checkbox value="read:notifications">{{ $t('read:notifications') }}</b-form-checkbox>
+ <b-form-checkbox value="write:notifications">{{ $t('write:notifications') }}</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
</b-card>
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index 9e191bf43c..8f4b243623 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -278,21 +278,6 @@ export default class MiOS extends EventEmitter {
});
});
- main.on('homeUpdated', x => {
- this.store.commit('settings/setHome', x);
- });
-
- main.on('mobileHomeUpdated', x => {
- this.store.commit('settings/setMobileHome', x);
- });
-
- main.on('widgetUpdated', x => {
- this.store.commit('settings/updateWidget', {
- id: x.id,
- data: x.data
- });
- });
-
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
index 92f5c1fd19..8f724b0f8e 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -22,7 +22,7 @@
<div>
<span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span>
<span class="separator"></span>
- <span class="data-size">{{ file.datasize | bytes }}</span>
+ <span class="data-size">{{ file.size | bytes }}</span>
<span class="separator"></span>
<span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span>
<template v-if="file.isSensitive">
diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
index feca266ede..ed95537f9c 100644
--- a/src/client/app/mobile/views/components/drive.file.vue
+++ b/src/client/app/mobile/views/components/drive.file.vue
@@ -10,7 +10,7 @@
<footer>
<span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span>
<span class="separator"></span>
- <span class="data-size">{{ file.datasize | bytes }}</span>
+ <span class="data-size">{{ file.size | bytes }}</span>
<span class="separator"></span>
<span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span>
<template v-if="file.isSensitive">
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index 1b8eceaa6c..8422c73420 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -54,7 +54,7 @@
</div>
</template>
- <template v-if="notification.type == 'poll_vote'">
+ <template v-if="notification.type == 'pollVote'">
<mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
<p><fa icon="chart-pie"/><mk-user-name :user="notification.user"/></p>
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 5308d96533..1128a76000 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -54,7 +54,7 @@
</div>
</div>
- <div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
+ <div class="notification pollVote" v-if="notification.type == 'pollVote'">
<mk-avatar class="avatar" :user="notification.user"/>
<div>
<header>
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
index 4f9f5119ab..1eb7399979 100644
--- a/src/client/app/mobile/views/pages/home.timeline.vue
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -59,7 +59,7 @@ export default Vue.extend({
};
if (this.src == 'tag') {
- this.endpoint = 'notes/search_by_tag';
+ this.endpoint = 'notes/search-by-tag';
this.query = {
query: this.tagTl.query
};
@@ -78,9 +78,9 @@ export default Vue.extend({
this.endpoint = 'notes/local-timeline';
this.connection = this.$root.stream.useSharedConnection('localTimeline');
this.connection.on('note', prepend);
- } else if (this.src == 'hybrid') {
- this.endpoint = 'notes/hybrid-timeline';
- this.connection = this.$root.stream.useSharedConnection('hybridTimeline');
+ } else if (this.src == 'social') {
+ this.endpoint = 'notes/social-timeline';
+ this.connection = this.$root.stream.useSharedConnection('socialTimeline');
this.connection.on('note', prepend);
} else if (this.src == 'global') {
this.endpoint = 'notes/global-timeline';
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index 59fae2340b..7e39441996 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -5,11 +5,11 @@
<span :class="$style.title">
<span v-if="src == 'home'"><fa icon="home"/>{{ $t('home') }}</span>
<span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>{{ $t('local') }}</span>
- <span v-if="src == 'hybrid'"><fa icon="share-alt"/>{{ $t('hybrid') }}</span>
+ <span v-if="src == 'social'"><fa icon="share-alt"/>{{ $t('social') }}</span>
<span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span>
<span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span>
<span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span>
- <span v-if="src == 'list'"><fa icon="list"/>{{ list.title }}</span>
+ <span v-if="src == 'list'"><fa icon="list"/>{{ list.name }}</span>
<span v-if="src == 'tag'"><fa icon="hashtag"/>{{ tagTl.title }}</span>
</span>
<span style="margin-left:8px">
@@ -32,7 +32,7 @@
<div>
<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
- <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span>
+ <span :data-active="src == 'social'" @click="src = 'social'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('social') }}</span>
<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span>
<div class="hr"></div>
<span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> {{ $t('mentions') }}<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span>
@@ -50,7 +50,7 @@
<div class="tl">
<x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/>
<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
- <x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
+ <x-tl v-if="src == 'social'" ref="tl" key="social" src="social"/>
<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
<x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
@@ -120,7 +120,7 @@ export default Vue.extend({
) && this.src === 'global') this.src = 'local';
if (!(
this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
- ) && ['local', 'hybrid'].includes(this.src)) this.src = 'home';
+ ) && ['local', 'social'].includes(this.src)) this.src = 'home';
});
if (this.$store.state.device.tl) {
@@ -131,7 +131,7 @@ export default Vue.extend({
this.tagTl = this.$store.state.device.tl.arg;
}
} else if (this.$store.state.i.followingCount == 0) {
- this.src = 'hybrid';
+ this.src = 'social';
}
},
diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue
index 318e63a473..7a7b90dad0 100644
--- a/src/client/app/mobile/views/pages/tag.vue
+++ b/src/client/app/mobile/views/pages/tag.vue
@@ -19,7 +19,7 @@ export default Vue.extend({
i18n: i18n('mobile/views/pages/tag.vue'),
data() {
return {
- makePromise: cursor => this.$root.api('notes/search_by_tag', {
+ makePromise: cursor => this.$root.api('notes/search-by-tag', {
limit: limit + 1,
offset: cursor ? cursor : undefined,
tag: this.$route.params.tag
diff --git a/src/client/app/mobile/views/pages/user-list.vue b/src/client/app/mobile/views/pages/user-list.vue
index 874bae5d18..68fd0358c4 100644
--- a/src/client/app/mobile/views/pages/user-list.vue
+++ b/src/client/app/mobile/views/pages/user-list.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui>
- <template #header v-if="!fetching"><fa icon="list"/>{{ list.title }}</template>
+ <template #header v-if="!fetching"><fa icon="list"/>{{ list.name }}</template>
<main v-if="!fetching">
<x-editor :list="list"/>
diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue
index fd129339fd..49006f41f6 100644
--- a/src/client/app/mobile/views/pages/user-lists.vue
+++ b/src/client/app/mobile/views/pages/user-lists.vue
@@ -5,7 +5,7 @@
<main>
<ul>
- <li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.title }}</router-link></li>
+ <li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link></li>
</ul>
</main>
</mk-ui>
diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue
index fe5ef057e7..72f2998dba 100644
--- a/src/client/app/mobile/views/pages/user/index.vue
+++ b/src/client/app/mobile/views/pages/user/index.vue
@@ -36,11 +36,11 @@
</dl>
</div>
<div class="info">
- <p class="location" v-if="user.host === null && user.profile.location">
- <fa icon="map-marker"/>{{ user.profile.location }}
+ <p class="location" v-if="user.host === null && user.location">
+ <fa icon="map-marker"/>{{ user.location }}
</p>
- <p class="birthday" v-if="user.host === null && user.profile.birthday">
- <fa icon="birthday-cake"/>{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }})
+ <p class="birthday" v-if="user.host === null && user.birthday">
+ <fa icon="birthday-cake"/>{{ user.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }})
</p>
</div>
<div class="status">
@@ -104,7 +104,7 @@ export default Vue.extend({
},
computed: {
age(): number {
- return age(this.user.profile.birthday);
+ return age(this.user.birthday);
},
avator(): string {
return this.$store.state.device.disableShowingAnimatedImages
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 1a2b0b6c12..dd71a918db 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -3,10 +3,10 @@
<div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div>
<div>
- <img svg-inline src="../../../../assets/title.svg" :alt="name">
+ <img svg-inline src="../../../../assets/title.svg" alt="Misskey">
<p class="host">{{ host }}</p>
<div class="about">
- <h2>{{ name }}</h2>
+ <h2>{{ name || 'Misskey' }}</h2>
<p v-html="description || this.$t('@.about')"></p>
<router-link class="signup" to="/signup">{{ $t('@.signup') }}</router-link>
</div>
@@ -62,7 +62,7 @@
</article>
<div class="info" v-if="meta">
<p>Version: <b>{{ meta.version }}</b></p>
- <p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+ <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p>
</div>
<footer>
<small>{{ copyright }}</small>
@@ -87,7 +87,7 @@ export default Vue.extend({
stats: null,
banner: null,
host: toUnicode(host),
- name: 'Misskey',
+ name: null,
description: '',
photos: [],
announcements: []
diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue
index 96dcb977fa..2647f96de2 100644
--- a/src/client/app/mobile/views/pages/widgets.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -119,7 +119,7 @@ export default Vue.extend({
},
addWidget() {
- this.$store.dispatch('settings/addMobileHomeWidget', {
+ this.$store.commit('settings/addMobileHomeWidget', {
name: this.widgetAdderSelected,
id: uuid(),
data: {}
@@ -127,14 +127,11 @@ export default Vue.extend({
},
removeWidget(widget) {
- this.$store.dispatch('settings/removeMobileHomeWidget', widget);
+ this.$store.commit('settings/removeMobileHomeWidget', widget);
},
saveHome() {
this.$store.commit('settings/setMobileHome', this.widgets);
- this.$root.api('i/update_mobile_home', {
- home: this.widgets
- });
}
}
});
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index e49934fc16..c82981ad24 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -7,8 +7,6 @@ import { erase } from '../../prelude/array';
import getNoteSummary from '../../misc/get-note-summary';
const defaultSettings = {
- home: null,
- mobileHome: [],
keepCw: false,
tagTimelines: [],
fetchOnScroll: true,
@@ -41,6 +39,8 @@ const defaultSettings = {
};
const defaultDeviceSettings = {
+ home: null,
+ mobileHome: [],
deck: null,
deckMode: false,
deckColumnAlign: 'center',
@@ -120,7 +120,7 @@ export default (os: MiOS) => new Vuex.Store({
actions: {
login(ctx, i) {
ctx.commit('updateI', i);
- ctx.dispatch('settings/merge', i.clientSettings);
+ ctx.dispatch('settings/merge', i.clientData);
},
logout(ctx) {
@@ -134,8 +134,8 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('updateIKeyValue', { key, value });
}
- if (me.clientSettings) {
- ctx.dispatch('settings/merge', me.clientSettings);
+ if (me.clientData) {
+ ctx.dispatch('settings/merge', me.clientData);
}
},
},
@@ -162,6 +162,48 @@ export default (os: MiOS) => new Vuex.Store({
state.visibility = visibility;
},
+ setHome(state, data) {
+ state.home = data;
+ },
+
+ addHomeWidget(state, widget) {
+ state.home.unshift(widget);
+ },
+
+ setMobileHome(state, data) {
+ state.mobileHome = data;
+ },
+
+ updateWidget(state, x) {
+ let w;
+
+ //#region Desktop home
+ if (state.home) {
+ w = state.home.find(w => w.id == x.id);
+ if (w) {
+ w.data = x.data;
+ }
+ }
+ //#endregion
+
+ //#region Mobile home
+ if (state.mobileHome) {
+ w = state.mobileHome.find(w => w.id == x.id);
+ if (w) {
+ w.data = x.data;
+ }
+ }
+ //#endregion
+ },
+
+ addMobileHomeWidget(state, widget) {
+ state.mobileHome.unshift(widget);
+ },
+
+ removeMobileHomeWidget(state, widget) {
+ state.mobileHome = state.mobileHome.filter(w => w.id != widget.id);
+ },
+
addDeckColumn(state, column) {
if (column.name == undefined) column.name = null;
state.deck.columns.push(column);
@@ -301,48 +343,6 @@ export default (os: MiOS) => new Vuex.Store({
set(state, x: { key: string; value: any }) {
nestedProperty.set(state, x.key, x.value);
},
-
- setHome(state, data) {
- state.home = data;
- },
-
- addHomeWidget(state, widget) {
- state.home.unshift(widget);
- },
-
- setMobileHome(state, data) {
- state.mobileHome = data;
- },
-
- updateWidget(state, x) {
- let w;
-
- //#region Desktop home
- if (state.home) {
- w = state.home.find(w => w.id == x.id);
- if (w) {
- w.data = x.data;
- }
- }
- //#endregion
-
- //#region Mobile home
- if (state.mobileHome) {
- w = state.mobileHome.find(w => w.id == x.id);
- if (w) {
- w.data = x.data;
- }
- }
- //#endregion
- },
-
- addMobileHomeWidget(state, widget) {
- state.mobileHome.unshift(widget);
- },
-
- removeMobileHomeWidget(state, widget) {
- state.mobileHome = state.mobileHome.filter(w => w.id != widget.id);
- },
},
actions: {
@@ -363,30 +363,6 @@ export default (os: MiOS) => new Vuex.Store({
});
}
},
-
- addHomeWidget(ctx, widget) {
- ctx.commit('addHomeWidget', widget);
-
- os.api('i/update_home', {
- home: ctx.state.home
- });
- },
-
- addMobileHomeWidget(ctx, widget) {
- ctx.commit('addMobileHomeWidget', widget);
-
- os.api('i/update_mobile_home', {
- home: ctx.state.mobileHome
- });
- },
-
- removeMobileHomeWidget(ctx, widget) {
- ctx.commit('removeMobileHomeWidget', widget);
-
- os.api('i/update_mobile_home', {
- home: ctx.state.mobileHome.filter(w => w.id != widget.id)
- });
- }
}
}
}
diff --git a/src/config/types.ts b/src/config/types.ts
index 5f30d410c9..d1749c52f7 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -8,7 +8,7 @@ export type Source = {
port: number;
https?: { [x: string]: string };
disableHsts?: boolean;
- mongodb: {
+ db: {
host: string;
port: number;
db: string;
@@ -42,6 +42,8 @@ export type Source = {
accesslog?: string;
clusterLimit?: number;
+
+ id: string;
};
/**
diff --git a/src/crypto_key.cc b/src/crypto_key.cc
deleted file mode 100644
index 658586baed..0000000000
--- a/src/crypto_key.cc
+++ /dev/null
@@ -1,111 +0,0 @@
-#include <nan.h>
-#include <openssl/bio.h>
-#include <openssl/buffer.h>
-#include <openssl/crypto.h>
-#include <openssl/pem.h>
-#include <openssl/rsa.h>
-#include <openssl/x509.h>
-
-NAN_METHOD(extractPublic)
-{
- const auto sourceString = info[0]->ToString(Nan::GetCurrentContext()).ToLocalChecked();
- if (!sourceString->IsOneByte()) {
- Nan::ThrowError("Malformed character found");
- return;
- }
-
- size_t sourceLength = sourceString->Length();
- const auto sourceBuf = new char[sourceLength];
-
- Nan::DecodeWrite(sourceBuf, sourceLength, sourceString);
-
- const auto source = BIO_new_mem_buf(sourceBuf, sourceLength);
- if (source == nullptr) {
- Nan::ThrowError("Memory allocation failed");
- delete[] sourceBuf;
- return;
- }
-
- const auto rsa = PEM_read_bio_RSAPrivateKey(source, nullptr, nullptr, nullptr);
-
- BIO_free(source);
- delete[] sourceBuf;
-
- if (rsa == nullptr) {
- Nan::ThrowError("Decode failed");
- return;
- }
-
- const auto destination = BIO_new(BIO_s_mem());
- if (destination == nullptr) {
- Nan::ThrowError("Memory allocation failed");
- return;
- }
-
- const auto result = PEM_write_bio_RSAPublicKey(destination, rsa);
-
- RSA_free(rsa);
-
- if (result != 1) {
- Nan::ThrowError("Public key extraction failed");
- BIO_free(destination);
- return;
- }
-
- char *pem;
- const auto pemLength = BIO_get_mem_data(destination, &pem);
-
- info.GetReturnValue().Set(Nan::Encode(pem, pemLength));
- BIO_free(destination);
-}
-
-NAN_METHOD(generate)
-{
- const auto exponent = BN_new();
- const auto mem = BIO_new(BIO_s_mem());
- const auto rsa = RSA_new();
- char *data;
- long result;
-
- if (exponent == nullptr || mem == nullptr || rsa == nullptr) {
- Nan::ThrowError("Memory allocation failed");
- goto done;
- }
-
- result = BN_set_word(exponent, 65537);
- if (result != 1) {
- Nan::ThrowError("Exponent setting failed");
- goto done;
- }
-
- result = RSA_generate_key_ex(rsa, 2048, exponent, nullptr);
- if (result != 1) {
- Nan::ThrowError("Key generation failed");
- goto done;
- }
-
- result = PEM_write_bio_RSAPrivateKey(mem, rsa, NULL, NULL, 0, NULL, NULL);
- if (result != 1) {
- Nan::ThrowError("Key export failed");
- goto done;
- }
-
- result = BIO_get_mem_data(mem, &data);
- info.GetReturnValue().Set(Nan::Encode(data, result));
-
-done:
- RSA_free(rsa);
- BIO_free(mem);
- BN_free(exponent);
-}
-
-NAN_MODULE_INIT(InitAll)
-{
- Nan::Set(target, Nan::New<v8::String>("extractPublic").ToLocalChecked(),
- Nan::GetFunction(Nan::New<v8::FunctionTemplate>(extractPublic)).ToLocalChecked());
-
- Nan::Set(target, Nan::New<v8::String>("generate").ToLocalChecked(),
- Nan::GetFunction(Nan::New<v8::FunctionTemplate>(generate)).ToLocalChecked());
-}
-
-NODE_MODULE(crypto_key, InitAll);
diff --git a/src/crypto_key.d.ts b/src/crypto_key.d.ts
deleted file mode 100644
index 9aa81a687c..0000000000
--- a/src/crypto_key.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export function extractPublic(keypair: string): string;
-export function generate(): string;
diff --git a/src/daemons/notes-stats-child.ts b/src/daemons/notes-stats-child.ts
index 7f54a36bff..c491aed4cd 100644
--- a/src/daemons/notes-stats-child.ts
+++ b/src/daemons/notes-stats-child.ts
@@ -1,26 +1,28 @@
-import Note from '../models/note';
+import { MoreThanOrEqual, getRepository } from 'typeorm';
+import { Note } from '../models/entities/note';
+import { initDb } from '../db/postgre';
const interval = 5000;
-async function tick() {
- const [all, local] = await Promise.all([Note.count({
- createdAt: {
- $gte: new Date(Date.now() - interval)
- }
- }), Note.count({
- createdAt: {
- $gte: new Date(Date.now() - interval)
- },
- '_user.host': null
- })]);
+initDb().then(() => {
+ const Notes = getRepository(Note);
- const stats = {
- all, local
- };
+ async function tick() {
+ const [all, local] = await Promise.all([Notes.count({
+ createdAt: MoreThanOrEqual(new Date(Date.now() - interval))
+ }), Notes.count({
+ createdAt: MoreThanOrEqual(new Date(Date.now() - interval)),
+ userHost: null
+ })]);
- process.send(stats);
-}
+ const stats = {
+ all, local
+ };
-tick();
+ process.send(stats);
+ }
-setInterval(tick, interval);
+ tick();
+
+ setInterval(tick, interval);
+});
diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
deleted file mode 100644
index f82ced1765..0000000000
--- a/src/db/mongodb.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import config from '../config';
-
-const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
-const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
-
-const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
-
-/**
- * monk
- */
-import mongo from 'monk';
-
-const db = mongo(uri);
-
-export default db;
-
-/**
- * MongoDB native module (officialy)
- */
-import * as mongodb from 'mongodb';
-
-let mdb: mongodb.Db;
-
-const nativeDbConn = async (): Promise<mongodb.Db> => {
- if (mdb) return mdb;
-
- const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => {
- mongodb.MongoClient.connect(uri, { useNewUrlParser: true }, (e: Error, client: any) => {
- if (e) return reject(e);
- resolve(client.db(config.mongodb.db));
- });
- }))();
-
- mdb = db;
-
- return db;
-};
-
-export { nativeDbConn };
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
new file mode 100644
index 0000000000..bc5ee4ce8c
--- /dev/null
+++ b/src/db/postgre.ts
@@ -0,0 +1,137 @@
+import { createConnection, Logger, getConnection } from 'typeorm';
+import config from '../config';
+import { entities as charts } from '../services/chart/entities';
+import { dbLogger } from './logger';
+import * as highlight from 'cli-highlight';
+
+import { Log } from '../models/entities/log';
+import { User } from '../models/entities/user';
+import { DriveFile } from '../models/entities/drive-file';
+import { DriveFolder } from '../models/entities/drive-folder';
+import { AccessToken } from '../models/entities/access-token';
+import { App } from '../models/entities/app';
+import { PollVote } from '../models/entities/poll-vote';
+import { Note } from '../models/entities/note';
+import { NoteReaction } from '../models/entities/note-reaction';
+import { NoteWatching } from '../models/entities/note-watching';
+import { NoteUnread } from '../models/entities/note-unread';
+import { Notification } from '../models/entities/notification';
+import { Meta } from '../models/entities/meta';
+import { Following } from '../models/entities/following';
+import { Instance } from '../models/entities/instance';
+import { Muting } from '../models/entities/muting';
+import { SwSubscription } from '../models/entities/sw-subscription';
+import { Blocking } from '../models/entities/blocking';
+import { UserList } from '../models/entities/user-list';
+import { UserListJoining } from '../models/entities/user-list-joining';
+import { Hashtag } from '../models/entities/hashtag';
+import { NoteFavorite } from '../models/entities/note-favorite';
+import { AbuseUserReport } from '../models/entities/abuse-user-report';
+import { RegistrationTicket } from '../models/entities/registration-tickets';
+import { MessagingMessage } from '../models/entities/messaging-message';
+import { Signin } from '../models/entities/signin';
+import { AuthSession } from '../models/entities/auth-session';
+import { FollowRequest } from '../models/entities/follow-request';
+import { Emoji } from '../models/entities/emoji';
+import { ReversiGame } from '../models/entities/games/reversi/game';
+import { ReversiMatching } from '../models/entities/games/reversi/matching';
+import { UserNotePining } from '../models/entities/user-note-pinings';
+import { UserServiceLinking } from '../models/entities/user-service-linking';
+import { Poll } from '../models/entities/poll';
+import { UserKeypair } from '../models/entities/user-keypair';
+import { UserPublickey } from '../models/entities/user-publickey';
+
+const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
+
+class MyCustomLogger implements Logger {
+ private highlight(sql: string) {
+ return highlight.highlight(sql, {
+ language: 'sql', ignoreIllegals: true,
+ });
+ }
+
+ public logQuery(query: string, parameters?: any[]) {
+ sqlLogger.info(this.highlight(query));
+ }
+
+ public logQueryError(error: string, query: string, parameters?: any[]) {
+ sqlLogger.error(this.highlight(query));
+ }
+
+ public logQuerySlow(time: number, query: string, parameters?: any[]) {
+ sqlLogger.warn(this.highlight(query));
+ }
+
+ public logSchemaBuild(message: string) {
+ sqlLogger.info(message);
+ }
+
+ public log(message: string) {
+ sqlLogger.info(message);
+ }
+
+ public logMigration(message: string) {
+ sqlLogger.info(message);
+ }
+}
+
+export function initDb(justBorrow = false, sync = false, log = false) {
+ const enableLogging = log || !['production', 'test'].includes(process.env.NODE_ENV);
+
+ try {
+ const conn = getConnection();
+ return Promise.resolve(conn);
+ } catch (e) {}
+
+ return createConnection({
+ type: 'postgres',
+ host: config.db.host,
+ port: config.db.port,
+ username: config.db.user,
+ password: config.db.pass,
+ database: config.db.db,
+ synchronize: process.env.NODE_ENV === 'test' || sync,
+ dropSchema: process.env.NODE_ENV === 'test' && !justBorrow,
+ logging: enableLogging,
+ logger: enableLogging ? new MyCustomLogger() : null,
+ entities: [
+ Meta,
+ Instance,
+ App,
+ AuthSession,
+ AccessToken,
+ User,
+ UserKeypair,
+ UserPublickey,
+ UserList,
+ UserListJoining,
+ UserNotePining,
+ UserServiceLinking,
+ Following,
+ FollowRequest,
+ Muting,
+ Blocking,
+ Note,
+ NoteFavorite,
+ NoteReaction,
+ NoteWatching,
+ NoteUnread,
+ Log,
+ DriveFile,
+ DriveFolder,
+ Poll,
+ PollVote,
+ Notification,
+ Emoji,
+ Hashtag,
+ SwSubscription,
+ AbuseUserReport,
+ RegistrationTicket,
+ MessagingMessage,
+ Signin,
+ ReversiGame,
+ ReversiMatching,
+ ...charts as any
+ ]
+ });
+}
diff --git a/src/docs/reversi-bot.ja-JP.md b/src/docs/reversi-bot.ja-JP.md
index a389ead571..b1f759ade8 100644
--- a/src/docs/reversi-bot.ja-JP.md
+++ b/src/docs/reversi-bot.ja-JP.md
@@ -42,9 +42,9 @@ Misskeyのリバーシ機能に対応したBotの開発方法をここに記し
```
pos = x + (y * mapWidth)
```
-`mapWidth`は、ゲーム情報の`settings.map`から、次のようにして計算できます:
+`mapWidth`は、ゲーム情報の`map`から、次のようにして計算できます:
```
-mapWidth = settings.map[0].length
+mapWidth = map[0].length
```
### Pos から X,Y座標 に変換する
@@ -54,7 +54,7 @@ y = Math.floor(pos / mapWidth)
```
## マップ情報
-マップ情報は、ゲーム情報の`settings.map`に入っています。
+マップ情報は、ゲーム情報の`map`に入っています。
文字列の配列になっており、ひとつひとつの文字がマス情報を表しています。
それをもとにマップのデザインを知る事が出来ます:
* `(スペース)` ... マス無し
diff --git a/src/docs/stream.ja-JP.md b/src/docs/stream.ja-JP.md
index 0e9afa7332..8a6bf63464 100644
--- a/src/docs/stream.ja-JP.md
+++ b/src/docs/stream.ja-JP.md
@@ -339,7 +339,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
#### `note`
ローカルタイムラインに新しい投稿が流れてきたときに発生するイベントです。
-## `hybridTimeline`
+## `socialTimeline`
ソーシャルタイムラインの投稿情報が流れてきます。このチャンネルにパラメータはありません。
### 流れてくるイベント一覧
diff --git a/src/index.ts b/src/index.ts
index e55ba5115d..c4a1088c2e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,281 +6,8 @@ Error.stackTraceLimit = Infinity;
require('events').EventEmitter.defaultMaxListeners = 128;
-import * as os from 'os';
-import * as cluster from 'cluster';
-import chalk from 'chalk';
-import * as portscanner from 'portscanner';
-import * as isRoot from 'is-root';
-import Xev from 'xev';
+import boot from './boot';
-import Logger from './services/logger';
-import serverStats from './daemons/server-stats';
-import notesStats from './daemons/notes-stats';
-import queueStats from './daemons/queue-stats';
-import loadConfig from './config/load';
-import { Config } from './config/types';
-import { lessThan } from './prelude/array';
-import * as pkg from '../package.json';
-import { program } from './argv';
-import { checkMongoDB } from './misc/check-mongodb';
-import { showMachineInfo } from './misc/show-machine-info';
-
-const logger = new Logger('core', 'cyan');
-const bootLogger = logger.createSubLogger('boot', 'magenta', false);
-const clusterLogger = logger.createSubLogger('cluster', 'orange');
-const ev = new Xev();
-
-/**
- * Init process
- */
-function main() {
- process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
-
- if (program.onlyQueue) {
- queueMain();
- return;
- }
-
- if (cluster.isMaster || program.disableClustering) {
- masterMain();
-
- if (cluster.isMaster) {
- ev.mount();
- }
-
- if (program.daemons) {
- serverStats();
- notesStats();
- queueStats();
- }
- }
-
- if (cluster.isWorker || program.disableClustering) {
- workerMain();
- }
-}
-
-function greet() {
- if (!program.quiet) {
- //#region Misskey logo
- const v = `v${pkg.version}`;
- console.log(' _____ _ _ ');
- console.log(' | |_|___ ___| |_ ___ _ _ ');
- console.log(' | | | | |_ -|_ -| \'_| -_| | |');
- console.log(' |_|_|_|_|___|___|_,_|___|_ |');
- console.log(' ' + chalk.gray(v) + (' |___|\n'.substr(v.length)));
- //#endregion
-
- console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.');
- console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
-
- console.log('');
- console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`);
- }
-
- bootLogger.info('Welcome to Misskey!');
- bootLogger.info(`Misskey v${pkg.version}`, null, true);
-}
-
-/**
- * Init master process
- */
-async function masterMain() {
- greet();
-
- let config: Config;
-
- try {
- // initialize app
- config = await init();
-
- if (config.port == null) {
- bootLogger.error('The port is not configured. Please configure port.', null, true);
- process.exit(1);
- }
-
- if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
- bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
- process.exit(1);
- }
-
- if (!await isPortAvailable(config.port)) {
- bootLogger.error(`Port ${config.port} is already in use`, null, true);
- process.exit(1);
- }
- } catch (e) {
- bootLogger.error('Fatal error occurred during initialization', null, true);
- process.exit(1);
- }
-
- bootLogger.succ('Misskey initialized');
-
- if (!program.disableClustering) {
- await spawnWorkers(config.clusterLimit);
- }
-
- bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
-}
-
-/**
- * Init worker process
- */
-async function workerMain() {
- // start server
- await require('./server').default();
-
- // start job queue
- require('./queue').default();
-
- if (cluster.isWorker) {
- // Send a 'ready' message to parent process
- process.send('ready');
- }
-}
-
-async function queueMain() {
- greet();
-
- try {
- // initialize app
- await init();
- } catch (e) {
- bootLogger.error('Fatal error occurred during initialization', null, true);
- process.exit(1);
- }
-
- bootLogger.succ('Misskey initialized');
-
- // start processor
- require('./queue').default();
-
- bootLogger.succ('Queue started', null, true);
-}
-
-const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));
-const requiredNodejsVersion = [10, 0, 0];
-const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion);
-
-function isWellKnownPort(port: number): boolean {
- return port < 1024;
-}
-
-async function isPortAvailable(port: number): Promise<boolean> {
- return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
-}
-
-function showEnvironment(): void {
- const env = process.env.NODE_ENV;
- const logger = bootLogger.createSubLogger('env');
- logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
-
- if (env !== 'production') {
- logger.warn('The environment is not in production mode.');
- logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
- }
-
- logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
+export default function() {
+ return boot();
}
-
-/**
- * Init app
- */
-async function init(): Promise<Config> {
- showEnvironment();
-
- const nodejsLogger = bootLogger.createSubLogger('nodejs');
-
- nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`);
-
- if (!satisfyNodejsVersion) {
- nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true);
- process.exit(1);
- }
-
- await showMachineInfo(bootLogger);
-
- const configLogger = bootLogger.createSubLogger('config');
- let config;
-
- try {
- config = loadConfig();
- } catch (exception) {
- if (typeof exception === 'string') {
- configLogger.error(exception);
- process.exit(1);
- }
- if (exception.code === 'ENOENT') {
- configLogger.error('Configuration file not found', null, true);
- process.exit(1);
- }
- throw exception;
- }
-
- configLogger.succ('Loaded');
-
- // Try to connect to MongoDB
- try {
- await checkMongoDB(config, bootLogger);
- } catch (e) {
- bootLogger.error('Cannot connect to database', null, true);
- process.exit(1);
- }
-
- return config;
-}
-
-async function spawnWorkers(limit: number = Infinity) {
- const workers = Math.min(limit, os.cpus().length);
- bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
- await Promise.all([...Array(workers)].map(spawnWorker));
- bootLogger.succ('All workers started');
-}
-
-function spawnWorker(): Promise<void> {
- return new Promise(res => {
- const worker = cluster.fork();
- worker.on('message', message => {
- if (message !== 'ready') return;
- res();
- });
- });
-}
-
-//#region Events
-
-// Listen new workers
-cluster.on('fork', worker => {
- clusterLogger.debug(`Process forked: [${worker.id}]`);
-});
-
-// Listen online workers
-cluster.on('online', worker => {
- clusterLogger.debug(`Process is now online: [${worker.id}]`);
-});
-
-// Listen for dying workers
-cluster.on('exit', worker => {
- // Replace the dead worker,
- // we're not sentimental
- clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
- cluster.fork();
-});
-
-// Display detail of unhandled promise rejection
-if (!program.quiet) {
- process.on('unhandledRejection', console.dir);
-}
-
-// Display detail of uncaught exception
-process.on('uncaughtException', err => {
- logger.error(err);
-});
-
-// Dying away...
-process.on('exit', code => {
- logger.info(`The process is going to exit with code ${code}`);
-});
-
-//#endregion
-
-main();
diff --git a/src/init.ts b/src/init.ts
new file mode 100644
index 0000000000..69c117c140
--- /dev/null
+++ b/src/init.ts
@@ -0,0 +1,16 @@
+import { initDb } from './db/postgre';
+
+async function main() {
+ try {
+ console.log('Connecting database...');
+ await initDb(false, true, true);
+ } catch (e) {
+ console.error('Cannot connect to database', null, true);
+ console.error(e);
+ process.exit(1);
+ }
+
+ console.log('Done :)');
+}
+
+main();
diff --git a/src/mfm/toHtml.ts b/src/mfm/toHtml.ts
index c676ae6ffc..3cd7987624 100644
--- a/src/mfm/toHtml.ts
+++ b/src/mfm/toHtml.ts
@@ -1,10 +1,10 @@
import { JSDOM } from 'jsdom';
import config from '../config';
-import { INote } from '../models/note';
import { intersperse } from '../prelude/array';
import { MfmForest, MfmTree } from './prelude';
+import { IMentionedRemoteUsers } from '../models/entities/note';
-export function toHtml(tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) {
+export function toHtml(tokens: MfmForest, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
if (tokens == null) {
return null;
}
diff --git a/src/misc/aid.ts b/src/misc/aid.ts
new file mode 100644
index 0000000000..aba53ed5fb
--- /dev/null
+++ b/src/misc/aid.ts
@@ -0,0 +1,26 @@
+// AID
+// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さnの[ランダムな文字列]
+
+const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
+const TIME2000 = 946684800000;
+
+function getTime(time: number) {
+ time = time - TIME2000;
+ if (time < 0) time = 0;
+
+ return time.toString(36);
+}
+
+function getRandom(length: number) {
+ let str = '';
+
+ for (let i = 0; i < length; i++) {
+ str += CHARS[Math.floor(Math.random() * CHARS.length)];
+ }
+
+ return str;
+}
+
+export function genAid(date: Date, rand: number): string {
+ return getTime(date.getTime()).padStart(8, CHARS[0]) + getRandom(rand);
+}
diff --git a/src/misc/aidc.ts b/src/misc/aidc.ts
new file mode 100644
index 0000000000..75168ac307
--- /dev/null
+++ b/src/misc/aidc.ts
@@ -0,0 +1,26 @@
+// AID(Cheep)
+// 長さ6の[2000年1月1日からの経過秒をbase36でエンコードしたもの] + 長さ3の[ランダムな文字列]
+
+const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
+const TIME2000 = 946684800000;
+
+function getTime(time: number) {
+ time = time - TIME2000;
+ if (time < 0) time = 0;
+ time = Math.floor(time / 1000);
+ return time.toString(36);
+}
+
+function getRandom() {
+ let str = '';
+
+ for (let i = 0; i < 3; i++) {
+ str += CHARS[Math.floor(Math.random() * CHARS.length)];
+ }
+
+ return str;
+}
+
+export function genAidc(date: Date): string {
+ return getTime(date.getTime()).padStart(6, CHARS[0]) + getRandom();
+}
diff --git a/src/misc/cafy-id.ts b/src/misc/cafy-id.ts
index bc8fe4ea2b..39886611e1 100644
--- a/src/misc/cafy-id.ts
+++ b/src/misc/cafy-id.ts
@@ -1,38 +1,13 @@
-import * as mongo from 'mongodb';
import { Context } from 'cafy';
-import isObjectId from './is-objectid';
-export const isAnId = (x: any) => mongo.ObjectID.isValid(x);
-export const isNotAnId = (x: any) => !isAnId(x);
-export const transform = (x: string | mongo.ObjectID): mongo.ObjectID => {
- if (x === undefined) return undefined;
- if (x === null) return null;
-
- if (isAnId(x) && !isObjectId(x)) {
- return new mongo.ObjectID(x);
- } else {
- return x as mongo.ObjectID;
- }
-};
-export const transformMany = (xs: (string | mongo.ObjectID)[]): mongo.ObjectID[] => {
- if (xs == null) return null;
-
- return xs.map(x => transform(x));
-};
-
-export type ObjectId = mongo.ObjectID;
-
-/**
- * ID
- */
-export default class ID<Maybe = string> extends Context<string | Maybe> {
+export class ID<Maybe = string> extends Context<string | (Maybe extends {} ? string : Maybe)> {
public readonly name = 'ID';
constructor(optional = false, nullable = false) {
super(optional, nullable);
this.push((v: any) => {
- if (!isObjectId(v) && isNotAnId(v)) {
+ if (typeof v !== 'string') {
return new Error('must-be-an-id');
}
return true;
diff --git a/src/misc/check-mongodb.ts b/src/misc/check-mongodb.ts
deleted file mode 100644
index 8e03db5d42..0000000000
--- a/src/misc/check-mongodb.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { nativeDbConn } from '../db/mongodb';
-import { Config } from '../config/types';
-import Logger from '../services/logger';
-import { lessThan } from '../prelude/array';
-
-const requiredMongoDBVersion = [3, 6];
-
-export function checkMongoDB(config: Config, logger: Logger) {
- return new Promise((res, rej) => {
- const mongoDBLogger = logger.createSubLogger('db');
- const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
- const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
- const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
- mongoDBLogger.info(`Connecting to ${uri} ...`);
-
- nativeDbConn().then(db => {
- mongoDBLogger.succ('Connectivity confirmed');
-
- db.admin().serverInfo().then(x => {
- const version = x.version as string;
- mongoDBLogger.info(`Version: ${version}`);
- if (lessThan(version.split('.').map(x => parseInt(x, 10)), requiredMongoDBVersion)) {
- mongoDBLogger.error(`MongoDB version is less than ${requiredMongoDBVersion.join('.')}. Please upgrade it.`);
- rej('outdated version');
- } else {
- res();
- }
- }).catch(err => {
- mongoDBLogger.error(`Failed to fetch server info: ${err.message}`);
- rej(err);
- });
- }).catch(err => {
- mongoDBLogger.error(err.message);
- rej(err);
- });
- });
-}
diff --git a/src/misc/fetch-meta.ts b/src/misc/fetch-meta.ts
index 3584a819bf..d1483e9edb 100644
--- a/src/misc/fetch-meta.ts
+++ b/src/misc/fetch-meta.ts
@@ -1,32 +1,15 @@
-import Meta, { IMeta } from '../models/meta';
+import { Meta } from '../models/entities/meta';
+import { Metas } from '../models';
+import { genId } from './gen-id';
-const defaultMeta: any = {
- name: 'Misskey',
- maintainer: {},
- langs: [],
- cacheRemoteFiles: true,
- localDriveCapacityMb: 256,
- remoteDriveCapacityMb: 8,
- hidedTags: [],
- stats: {
- originalNotesCount: 0,
- originalUsersCount: 0
- },
- maxNoteTextLength: 1000,
- enableEmojiReaction: true,
- enableTwitterIntegration: false,
- enableGithubIntegration: false,
- enableDiscordIntegration: false,
- enableExternalUserRecommendation: false,
- externalUserRecommendationEngine: 'https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}',
- externalUserRecommendationTimeout: 300000,
- mascotImageUrl: '/assets/ai.png',
- errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png',
- enableServiceWorker: false
-};
-
-export default async function(): Promise<IMeta> {
- const meta = await Meta.findOne({});
-
- return Object.assign({}, defaultMeta, meta);
+export default async function(): Promise<Meta> {
+ const meta = await Metas.findOne();
+ if (meta) {
+ return meta;
+ } else {
+ return Metas.save({
+ id: genId(),
+ hiddenTags: []
+ } as Meta);
+ }
}
diff --git a/src/misc/fetch-proxy-account.ts b/src/misc/fetch-proxy-account.ts
new file mode 100644
index 0000000000..d60fa9b313
--- /dev/null
+++ b/src/misc/fetch-proxy-account.ts
@@ -0,0 +1,8 @@
+import fetchMeta from './fetch-meta';
+import { ILocalUser } from '../models/entities/user';
+import { Users } from '../models';
+
+export async function fetchProxyAccount(): Promise<ILocalUser> {
+ const meta = await fetchMeta();
+ return await Users.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser;
+}
diff --git a/src/misc/gen-id.ts b/src/misc/gen-id.ts
new file mode 100644
index 0000000000..fe901b1fe7
--- /dev/null
+++ b/src/misc/gen-id.ts
@@ -0,0 +1,22 @@
+import { ulid } from 'ulid';
+import { genAid } from './aid';
+import { genAidc } from './aidc';
+import { genObjectId } from './object-id';
+import config from '../config';
+
+const metohd = config.id.toLowerCase();
+
+export function genId(date?: Date): string {
+ if (!date || (date > new Date())) date = new Date();
+
+ switch (metohd) {
+ case 'aidc': return genAidc(date);
+ case 'aid1': return genAid(date, 1);
+ case 'aid2': return genAid(date, 2);
+ case 'aid3': return genAid(date, 3);
+ case 'aid4': return genAid(date, 4);
+ case 'ulid': return ulid(date.getTime());
+ case 'objectid': return genObjectId(date);
+ default: throw 'unknown id generation method';
+ }
+}
diff --git a/src/misc/get-drive-file-url.ts b/src/misc/get-drive-file-url.ts
deleted file mode 100644
index 067db8a5d0..0000000000
--- a/src/misc/get-drive-file-url.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { IDriveFile } from '../models/drive-file';
-import config from '../config';
-
-export default function(file: IDriveFile, thumbnail = false): string {
- if (file == null) return null;
-
- const isImage = file.contentType && file.contentType.startsWith('image/');
-
- if (file.metadata.withoutChunks) {
- if (thumbnail) {
- return file.metadata.thumbnailUrl || file.metadata.webpublicUrl || (isImage ? file.metadata.url : null);
- } else {
- return file.metadata.webpublicUrl || file.metadata.url;
- }
- } else {
- if (thumbnail) {
- return `${config.driveUrl}/${file._id}?thumbnail`;
- } else {
- return `${config.driveUrl}/${file._id}?web`;
- }
- }
-}
-
-export function getOriginalUrl(file: IDriveFile) {
- if (file.metadata && file.metadata.url) {
- return file.metadata.url;
- }
-
- const accessKey = file.metadata ? file.metadata.accessKey : null;
- return `${config.driveUrl}/${file._id}${accessKey ? '?original=' + accessKey : ''}`;
-}
diff --git a/src/misc/get-notification-summary.ts b/src/misc/get-notification-summary.ts
index 71d4973ce9..b20711c605 100644
--- a/src/misc/get-notification-summary.ts
+++ b/src/misc/get-notification-summary.ts
@@ -20,7 +20,7 @@ export default function(notification: any): string {
return `引用されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
case 'reaction':
return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getNoteSummary(notification.note)}」`;
- case 'poll_vote':
+ case 'pollVote':
return `投票されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
default:
return `<不明な通知タイプ: ${notification.type}>`;
diff --git a/src/misc/get-user-name.ts b/src/misc/get-user-name.ts
index eab9f87ef0..b6b45118b0 100644
--- a/src/misc/get-user-name.ts
+++ b/src/misc/get-user-name.ts
@@ -1,5 +1,5 @@
-import { IUser } from '../models/user';
+import { User } from '../models/entities/user';
-export default function(user: IUser): string {
+export default function(user: User): string {
return user.name || user.username;
}
diff --git a/src/misc/get-user-summary.ts b/src/misc/get-user-summary.ts
index 09cf5ebadc..9cb06f43ce 100644
--- a/src/misc/get-user-summary.ts
+++ b/src/misc/get-user-summary.ts
@@ -1,17 +1,18 @@
-import { IUser, isLocalUser } from '../models/user';
import getAcct from './acct/render';
import getUserName from './get-user-name';
+import { User } from '../models/entities/user';
+import { Users } from '../models';
/**
* ユーザーを表す文字列を取得します。
* @param user ユーザー
*/
-export default function(user: IUser): string {
+export default function(user: User): string {
let string = `${getUserName(user)} (@${getAcct(user)})\n` +
`${user.notesCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
- if (isLocalUser(user)) {
- string += `場所: ${user.profile.location}、誕生日: ${user.profile.birthday}\n`;
+ if (Users.isLocalUser(user)) {
+ string += `場所: ${user.location}、誕生日: ${user.birthday}\n`;
}
return string + `「${user.description}」`;
diff --git a/src/misc/is-duplicate-key-value-error.ts b/src/misc/is-duplicate-key-value-error.ts
new file mode 100644
index 0000000000..23d8ceb1b7
--- /dev/null
+++ b/src/misc/is-duplicate-key-value-error.ts
@@ -0,0 +1,3 @@
+export function isDuplicateKeyValueError(e: Error): boolean {
+ return e.message.startsWith('duplicate key value');
+}
diff --git a/src/misc/is-objectid.ts b/src/misc/is-objectid.ts
deleted file mode 100644
index a77c4ee2d5..0000000000
--- a/src/misc/is-objectid.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { ObjectID } from 'mongodb';
-
-export default function(x: any): x is ObjectID {
- return x && typeof x === 'object' && (x.hasOwnProperty('toHexString') || x.hasOwnProperty('_bsontype'));
-}
diff --git a/src/misc/is-quote.ts b/src/misc/is-quote.ts
index a99b8f6434..0a2a72f4a0 100644
--- a/src/misc/is-quote.ts
+++ b/src/misc/is-quote.ts
@@ -1,5 +1,5 @@
-import { INote } from '../models/note';
+import { Note } from '../models/entities/note';
-export default function(note: INote): boolean {
- return note.renoteId != null && (note.text != null || note.poll != null || (note.fileIds != null && note.fileIds.length > 0));
+export default function(note: Note): boolean {
+ return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
}
diff --git a/src/misc/nyaize.ts b/src/misc/nyaize.ts
new file mode 100644
index 0000000000..8b06300eab
--- /dev/null
+++ b/src/misc/nyaize.ts
@@ -0,0 +1,9 @@
+export function nyaize(text: string): string {
+ return text
+ // ja-JP
+ .replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
+ // ko-KR
+ .replace(/[나-낳]/g, (match: string) => String.fromCharCode(
+ match.codePointAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
+ ));
+}
diff --git a/src/misc/object-id.ts b/src/misc/object-id.ts
new file mode 100644
index 0000000000..6f6422e5e4
--- /dev/null
+++ b/src/misc/object-id.ts
@@ -0,0 +1,26 @@
+const CHARS = '0123456789abcdef';
+
+function getTime(time: number) {
+ if (time < 0) time = 0;
+ if (time === 0) {
+ return CHARS[0];
+ }
+
+ time = Math.floor(time / 1000);
+
+ return time.toString(16);
+}
+
+function getRandom() {
+ let str = '';
+
+ for (let i = 0; i < 16; i++) {
+ str += CHARS[Math.floor(Math.random() * CHARS.length)];
+ }
+
+ return str;
+}
+
+export function genObjectId(date: Date): string {
+ return getTime(date.getTime()) + getRandom();
+}
diff --git a/src/misc/reaction-lib.ts b/src/misc/reaction-lib.ts
index 7e5a1b0bc0..20051f0280 100644
--- a/src/misc/reaction-lib.ts
+++ b/src/misc/reaction-lib.ts
@@ -1,6 +1,6 @@
-import Emoji from '../models/emoji';
import { emojiRegex } from './emoji-regex';
import fetchMeta from './fetch-meta';
+import { Emojis } from '../models';
const basic10: Record<string, string> = {
'👍': 'like',
@@ -49,7 +49,7 @@ export async function toDbReaction(reaction: string, enableEmoji = true): Promis
const custom = reaction.match(/^:([\w+-]+):$/);
if (custom) {
- const emoji = await Emoji.findOne({
+ const emoji = await Emojis.findOne({
host: null,
name: custom[1],
});
diff --git a/src/misc/should-mute-this-note.ts b/src/misc/should-mute-this-note.ts
index b1d29c6a28..8f606a2943 100644
--- a/src/misc/should-mute-this-note.ts
+++ b/src/misc/should-mute-this-note.ts
@@ -1,20 +1,13 @@
-import * as mongo from 'mongodb';
-import isObjectId from './is-objectid';
-
-function toString(id: any) {
- return isObjectId(id) ? (id as mongo.ObjectID).toHexString() : id;
-}
-
export default function(note: any, mutedUserIds: string[]): boolean {
- if (mutedUserIds.includes(toString(note.userId))) {
+ if (mutedUserIds.includes(note.userId)) {
return true;
}
- if (note.reply != null && mutedUserIds.includes(toString(note.reply.userId))) {
+ if (note.reply != null && mutedUserIds.includes(note.reply.userId)) {
return true;
}
- if (note.renote != null && mutedUserIds.includes(toString(note.renote.userId))) {
+ if (note.renote != null && mutedUserIds.includes(note.renote.userId)) {
return true;
}
diff --git a/src/models/abuse-user-report.ts b/src/models/abuse-user-report.ts
deleted file mode 100644
index f3900d348d..0000000000
--- a/src/models/abuse-user-report.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packUser } from './user';
-
-const AbuseUserReport = db.get<IAbuseUserReport>('abuseUserReports');
-AbuseUserReport.createIndex('userId');
-AbuseUserReport.createIndex('reporterId');
-AbuseUserReport.createIndex(['userId', 'reporterId'], { unique: true });
-export default AbuseUserReport;
-
-export interface IAbuseUserReport {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID;
- reporterId: mongo.ObjectID;
- comment: string;
-}
-
-export const packMany = (
- reports: (string | mongo.ObjectID | IAbuseUserReport)[]
-) => {
- return Promise.all(reports.map(x => pack(x)));
-};
-
-export const pack = (
- report: any
-) => new Promise<any>(async (resolve, reject) => {
- let _report: any;
-
- if (isObjectId(report)) {
- _report = await AbuseUserReport.findOne({
- _id: report
- });
- } else if (typeof report === 'string') {
- _report = await AbuseUserReport.findOne({
- _id: new mongo.ObjectID(report)
- });
- } else {
- _report = deepcopy(report);
- }
-
- // Rename _id to id
- _report.id = _report._id;
- delete _report._id;
-
- _report.reporter = await packUser(_report.reporterId, null, { detail: true });
- _report.user = await packUser(_report.userId, null, { detail: true });
-
- resolve(_report);
-});
diff --git a/src/models/access-token.ts b/src/models/access-token.ts
deleted file mode 100644
index 66c5c91c0b..0000000000
--- a/src/models/access-token.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const AccessToken = db.get<IAccessToken>('accessTokens');
-AccessToken.createIndex('token');
-AccessToken.createIndex('hash');
-export default AccessToken;
-
-export type IAccessToken = {
- _id: mongo.ObjectID;
- createdAt: Date;
- appId: mongo.ObjectID;
- userId: mongo.ObjectID;
- token: string;
- hash: string;
-};
diff --git a/src/models/app.ts b/src/models/app.ts
deleted file mode 100644
index 45d50bccda..0000000000
--- a/src/models/app.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import AccessToken from './access-token';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import config from '../config';
-import { dbLogger } from '../db/logger';
-
-const App = db.get<IApp>('apps');
-App.createIndex('secret');
-export default App;
-
-export type IApp = {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID | null;
- secret: string;
- name: string;
- description: string;
- permission: string[];
- callbackUrl: string;
-};
-
-/**
- * Pack an app for API response
- */
-export const pack = (
- app: any,
- me?: any,
- options?: {
- detail?: boolean,
- includeSecret?: boolean,
- includeProfileImageIds?: boolean
- }
-) => new Promise<any>(async (resolve, reject) => {
- const opts = Object.assign({
- detail: false,
- includeSecret: false,
- includeProfileImageIds: false
- }, options);
-
- let _app: any;
-
- const fields = opts.detail ? {} : {
- name: true
- };
-
- // Populate the app if 'app' is ID
- if (isObjectId(app)) {
- _app = await App.findOne({
- _id: app
- });
- } else if (typeof app === 'string') {
- _app = await App.findOne({
- _id: new mongo.ObjectID(app)
- }, { fields });
- } else {
- _app = deepcopy(app);
- }
-
- // Me
- if (me && !isObjectId(me)) {
- if (typeof me === 'string') {
- me = new mongo.ObjectID(me);
- } else {
- me = me._id;
- }
- }
-
- // (データベースの欠損などで)アプリがデータベース上に見つからなかったとき
- if (_app == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: app :: ${app}`);
- return null;
- }
-
- // Rename _id to id
- _app.id = _app._id;
- delete _app._id;
-
- // Visible by only owner
- if (!opts.includeSecret) {
- delete _app.secret;
- }
-
- _app.iconUrl = _app.icon != null
- ? `${config.driveUrl}/${_app.icon}`
- : `${config.driveUrl}/app-default.jpg`;
-
- if (me) {
- // 既に連携しているか
- const exist = await AccessToken.count({
- appId: _app.id,
- userId: me,
- }, {
- limit: 1
- });
-
- _app.isAuthorized = exist === 1;
- }
-
- resolve(_app);
-});
diff --git a/src/models/auth-session.ts b/src/models/auth-session.ts
deleted file mode 100644
index 428c707470..0000000000
--- a/src/models/auth-session.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packApp } from './app';
-
-const AuthSession = db.get<IAuthSession>('authSessions');
-export default AuthSession;
-
-export interface IAuthSession {
- _id: mongo.ObjectID;
- createdAt: Date;
- appId: mongo.ObjectID;
- userId: mongo.ObjectID;
- token: string;
-}
-
-/**
- * Pack an auth session for API response
- *
- * @param {any} session
- * @param {any} me?
- * @return {Promise<any>}
- */
-export const pack = (
- session: any,
- me?: any
-) => new Promise<any>(async (resolve, reject) => {
- let _session: any;
-
- // TODO: Populate session if it ID
- _session = deepcopy(session);
-
- // Me
- if (me && !isObjectId(me)) {
- if (typeof me === 'string') {
- me = new mongo.ObjectID(me);
- } else {
- me = me._id;
- }
- }
-
- delete _session._id;
-
- // Populate app
- _session.app = await packApp(_session.appId, me);
-
- resolve(_session);
-});
diff --git a/src/models/blocking.ts b/src/models/blocking.ts
deleted file mode 100644
index 4bdaa741e9..0000000000
--- a/src/models/blocking.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import * as deepcopy from 'deepcopy';
-import { pack as packUser, IUser } from './user';
-
-const Blocking = db.get<IBlocking>('blocking');
-Blocking.createIndex('blockerId');
-Blocking.createIndex('blockeeId');
-Blocking.createIndex(['blockerId', 'blockeeId'], { unique: true });
-export default Blocking;
-
-export type IBlocking = {
- _id: mongo.ObjectID;
- createdAt: Date;
- blockeeId: mongo.ObjectID;
- blockerId: mongo.ObjectID;
-};
-
-export const packMany = (
- blockings: (string | mongo.ObjectID | IBlocking)[],
- me?: string | mongo.ObjectID | IUser
-) => {
- return Promise.all(blockings.map(x => pack(x, me)));
-};
-
-export const pack = (
- blocking: any,
- me?: any
-) => new Promise<any>(async (resolve, reject) => {
- let _blocking: any;
-
- // Populate the blocking if 'blocking' is ID
- if (isObjectId(blocking)) {
- _blocking = await Blocking.findOne({
- _id: blocking
- });
- } else if (typeof blocking === 'string') {
- _blocking = await Blocking.findOne({
- _id: new mongo.ObjectID(blocking)
- });
- } else {
- _blocking = deepcopy(blocking);
- }
-
- // Rename _id to id
- _blocking.id = _blocking._id;
- delete _blocking._id;
-
- // Populate blockee
- _blocking.blockee = await packUser(_blocking.blockeeId, me, {
- detail: true
- });
-
- resolve(_blocking);
-});
diff --git a/src/models/drive-file-thumbnail.ts b/src/models/drive-file-thumbnail.ts
deleted file mode 100644
index bdb3d010e6..0000000000
--- a/src/models/drive-file-thumbnail.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as mongo from 'mongodb';
-import monkDb, { nativeDbConn } from '../db/mongodb';
-
-const DriveFileThumbnail = monkDb.get<IDriveFileThumbnail>('driveFileThumbnails.files');
-DriveFileThumbnail.createIndex('metadata.originalId', { sparse: true, unique: true });
-export default DriveFileThumbnail;
-
-export const DriveFileThumbnailChunk = monkDb.get('driveFileThumbnails.chunks');
-
-export const getDriveFileThumbnailBucket = async (): Promise<mongo.GridFSBucket> => {
- const db = await nativeDbConn();
- const bucket = new mongo.GridFSBucket(db, {
- bucketName: 'driveFileThumbnails'
- });
- return bucket;
-};
-
-export type IMetadata = {
- originalId: mongo.ObjectID;
-};
-
-export type IDriveFileThumbnail = {
- _id: mongo.ObjectID;
- uploadDate: Date;
- md5: string;
- filename: string;
- contentType: string;
- metadata: IMetadata;
-};
diff --git a/src/models/drive-file-webpublic.ts b/src/models/drive-file-webpublic.ts
deleted file mode 100644
index d087c355d3..0000000000
--- a/src/models/drive-file-webpublic.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as mongo from 'mongodb';
-import monkDb, { nativeDbConn } from '../db/mongodb';
-
-const DriveFileWebpublic = monkDb.get<IDriveFileWebpublic>('driveFileWebpublics.files');
-DriveFileWebpublic.createIndex('metadata.originalId', { sparse: true, unique: true });
-export default DriveFileWebpublic;
-
-export const DriveFileWebpublicChunk = monkDb.get('driveFileWebpublics.chunks');
-
-export const getDriveFileWebpublicBucket = async (): Promise<mongo.GridFSBucket> => {
- const db = await nativeDbConn();
- const bucket = new mongo.GridFSBucket(db, {
- bucketName: 'driveFileWebpublics'
- });
- return bucket;
-};
-
-export type IMetadata = {
- originalId: mongo.ObjectID;
-};
-
-export type IDriveFileWebpublic = {
- _id: mongo.ObjectID;
- uploadDate: Date;
- md5: string;
- filename: string;
- contentType: string;
- metadata: IMetadata;
-};
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
deleted file mode 100644
index c31e9a709f..0000000000
--- a/src/models/drive-file.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import { pack as packFolder } from './drive-folder';
-import { pack as packUser } from './user';
-import monkDb, { nativeDbConn } from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import getDriveFileUrl, { getOriginalUrl } from '../misc/get-drive-file-url';
-import { dbLogger } from '../db/logger';
-
-const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
-DriveFile.createIndex('md5');
-DriveFile.createIndex('metadata.uri');
-DriveFile.createIndex('metadata.userId');
-DriveFile.createIndex('metadata.folderId');
-DriveFile.createIndex('metadata._user.host');
-export default DriveFile;
-
-export const DriveFileChunk = monkDb.get('driveFiles.chunks');
-
-export const getDriveFileBucket = async (): Promise<mongo.GridFSBucket> => {
- const db = await nativeDbConn();
- const bucket = new mongo.GridFSBucket(db, {
- bucketName: 'driveFiles'
- });
- return bucket;
-};
-
-export type IMetadata = {
- properties: any;
- userId: mongo.ObjectID;
- _user: any;
- folderId: mongo.ObjectID;
- comment: string;
-
- /**
- * リモートインスタンスから取得した場合の元URL
- */
- uri?: string;
-
- /**
- * URL for web(生成されている場合) or original
- * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
- */
- url?: string;
-
- /**
- * URL for thumbnail (thumbnailがなければなし)
- * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
- */
- thumbnailUrl?: string;
-
- /**
- * URL for original (web用が生成されてない場合はurlがoriginalを指す)
- * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
- */
- webpublicUrl?: string;
-
- accessKey?: string;
-
- src?: string;
- deletedAt?: Date;
-
- /**
- * このファイルの中身データがMongoDB内に保存されていないか否か
- * オブジェクトストレージを利用している or リモートサーバーへの直リンクである
- * な場合は true になります
- */
- withoutChunks?: boolean;
-
- storage?: string;
-
- /***
- * ObjectStorage の格納先の情報
- */
- storageProps?: IStorageProps;
- isSensitive?: boolean;
-
- /**
- * このファイルが添付された投稿のID一覧
- */
- attachedNoteIds?: mongo.ObjectID[];
-
- /**
- * 外部の(信頼されていない)URLへの直リンクか否か
- */
- isRemote?: boolean;
-};
-
-export type IStorageProps = {
- /**
- * ObjectStorage key for original
- */
- key: string;
-
- /***
- * ObjectStorage key for thumbnail (thumbnailがなければなし)
- */
- thumbnailKey?: string;
-
- /***
- * ObjectStorage key for webpublic (webpublicがなければなし)
- */
- webpublicKey?: string;
-
- id?: string;
-};
-
-export type IDriveFile = {
- _id: mongo.ObjectID;
- uploadDate: Date;
- md5: string;
- filename: string;
- contentType: string;
- metadata: IMetadata;
-
- /**
- * ファイルサイズ
- */
- length: number;
-};
-
-export function validateFileName(name: string): boolean {
- return (
- (name.trim().length > 0) &&
- (name.length <= 200) &&
- (name.indexOf('\\') === -1) &&
- (name.indexOf('/') === -1) &&
- (name.indexOf('..') === -1)
- );
-}
-
-export const packMany = (
- files: any[],
- options?: {
- detail?: boolean
- self?: boolean,
- withUser?: boolean,
- }
-) => {
- return Promise.all(files.map(f => pack(f, options)));
-};
-
-/**
- * Pack a drive file for API response
- */
-export const pack = (
- file: any,
- options?: {
- detail?: boolean,
- self?: boolean,
- withUser?: boolean,
- }
-) => new Promise<any>(async (resolve, reject) => {
- const opts = Object.assign({
- detail: false,
- self: false
- }, options);
-
- let _file: any;
-
- // Populate the file if 'file' is ID
- if (isObjectId(file)) {
- _file = await DriveFile.findOne({
- _id: file
- });
- } else if (typeof file === 'string') {
- _file = await DriveFile.findOne({
- _id: new mongo.ObjectID(file)
- });
- } else {
- _file = deepcopy(file);
- }
-
- // (データベースの欠損などで)ファイルがデータベース上に見つからなかったとき
- if (_file == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: driveFile :: ${file}`);
- return resolve(null);
- }
-
- // rendered target
- let _target: any = {};
-
- _target.id = _file._id;
- _target.createdAt = _file.uploadDate;
- _target.name = _file.filename;
- _target.type = _file.contentType;
- _target.datasize = _file.length;
- _target.md5 = _file.md5;
-
- _target = Object.assign(_target, _file.metadata);
-
- _target.url = getDriveFileUrl(_file);
- _target.thumbnailUrl = getDriveFileUrl(_file, true);
- _target.isRemote = _file.metadata.isRemote;
-
- if (_target.properties == null) _target.properties = {};
-
- if (opts.detail) {
- if (_target.folderId) {
- // Populate folder
- _target.folder = await packFolder(_target.folderId, {
- detail: true
- });
- }
-
- /*
- if (_target.tags) {
- // Populate tags
- _target.tags = await _target.tags.map(async (tag: any) =>
- await serializeDriveTag(tag)
- );
- }
- */
- }
-
- if (opts.withUser) {
- // Populate user
- _target.user = await packUser(_file.metadata.userId);
- }
-
- delete _target.withoutChunks;
- delete _target.storage;
- delete _target.storageProps;
- delete _target.isRemote;
- delete _target._user;
-
- if (opts.self) {
- _target.url = getOriginalUrl(_file);
- }
-
- resolve(_target);
-});
diff --git a/src/models/drive-folder.ts b/src/models/drive-folder.ts
deleted file mode 100644
index b0f6e4273e..0000000000
--- a/src/models/drive-folder.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import DriveFile from './drive-file';
-
-const DriveFolder = db.get<IDriveFolder>('driveFolders');
-DriveFolder.createIndex('userId');
-export default DriveFolder;
-
-export type IDriveFolder = {
- _id: mongo.ObjectID;
- createdAt: Date;
- name: string;
- userId: mongo.ObjectID;
- parentId: mongo.ObjectID;
-};
-
-export function isValidFolderName(name: string): boolean {
- return (
- (name.trim().length > 0) &&
- (name.length <= 200)
- );
-}
-
-/**
- * Pack a drive folder for API response
- */
-export const pack = (
- folder: any,
- options?: {
- detail: boolean
- }
-) => new Promise<any>(async (resolve, reject) => {
- const opts = Object.assign({
- detail: false
- }, options);
-
- let _folder: any;
-
- // Populate the folder if 'folder' is ID
- if (isObjectId(folder)) {
- _folder = await DriveFolder.findOne({ _id: folder });
- } else if (typeof folder === 'string') {
- _folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) });
- } else {
- _folder = deepcopy(folder);
- }
-
- // Rename _id to id
- _folder.id = _folder._id;
- delete _folder._id;
-
- if (opts.detail) {
- const childFoldersCount = await DriveFolder.count({
- parentId: _folder.id
- });
-
- const childFilesCount = await DriveFile.count({
- 'metadata.folderId': _folder.id
- });
-
- _folder.foldersCount = childFoldersCount;
- _folder.filesCount = childFilesCount;
- }
-
- if (opts.detail && _folder.parentId) {
- // Populate parent folder
- _folder.parent = await pack(_folder.parentId, {
- detail: true
- });
- }
-
- resolve(_folder);
-});
diff --git a/src/models/emoji.ts b/src/models/emoji.ts
deleted file mode 100644
index cbf939222e..0000000000
--- a/src/models/emoji.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Emoji = db.get<IEmoji>('emoji');
-Emoji.createIndex('name');
-Emoji.createIndex('host');
-Emoji.createIndex(['name', 'host'], { unique: true });
-
-export default Emoji;
-
-export type IEmoji = {
- _id: mongo.ObjectID;
- name: string;
- host: string;
- url: string;
- aliases?: string[];
- updatedAt?: Date;
- /** AP object id */
- uri?: string;
- type?: string;
-};
diff --git a/src/models/entities/abuse-user-report.ts b/src/models/entities/abuse-user-report.ts
new file mode 100644
index 0000000000..43ab56023a
--- /dev/null
+++ b/src/models/entities/abuse-user-report.ts
@@ -0,0 +1,41 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'reporterId'], { unique: true })
+export class AbuseUserReport {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the AbuseUserReport.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column(id())
+ public reporterId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public reporter: User | null;
+
+ @Column('varchar', {
+ length: 512,
+ })
+ public comment: string;
+}
diff --git a/src/models/entities/access-token.ts b/src/models/entities/access-token.ts
new file mode 100644
index 0000000000..d08930cf5a
--- /dev/null
+++ b/src/models/entities/access-token.ts
@@ -0,0 +1,45 @@
+import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn, RelationId } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { id } from '../id';
+
+@Entity()
+export class AccessToken {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the AccessToken.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column('varchar', {
+ length: 128
+ })
+ public token: string;
+
+ @Index()
+ @Column('varchar', {
+ length: 128
+ })
+ public hash: string;
+
+ @RelationId((self: AccessToken) => self.user)
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column(id())
+ public appId: App['id'];
+
+ @ManyToOne(type => App, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public app: App | null;
+}
diff --git a/src/models/entities/app.ts b/src/models/entities/app.ts
new file mode 100644
index 0000000000..d0c89000fc
--- /dev/null
+++ b/src/models/entities/app.ts
@@ -0,0 +1,60 @@
+import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class App {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the App.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The owner ID.'
+ })
+ public userId: User['id'] | null;
+
+ @ManyToOne(type => User, {
+ onDelete: 'SET NULL',
+ nullable: true,
+ })
+ public user: User | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 64,
+ comment: 'The secret key of the App.'
+ })
+ public secret: string;
+
+ @Column('varchar', {
+ length: 128,
+ comment: 'The name of the App.'
+ })
+ public name: string;
+
+ @Column('varchar', {
+ length: 512,
+ comment: 'The description of the App.'
+ })
+ public description: string;
+
+ @Column('varchar', {
+ length: 64, array: true,
+ comment: 'The permission of the App.'
+ })
+ public permission: string[];
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: 'The callbackUrl of the App.'
+ })
+ public callbackUrl: string | null;
+}
diff --git a/src/models/entities/auth-session.ts b/src/models/entities/auth-session.ts
new file mode 100644
index 0000000000..83f8365630
--- /dev/null
+++ b/src/models/entities/auth-session.ts
@@ -0,0 +1,39 @@
+import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { id } from '../id';
+
+@Entity()
+export class AuthSession {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the AuthSession.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column('varchar', {
+ length: 128
+ })
+ public token: string;
+
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column(id())
+ public appId: App['id'];
+
+ @ManyToOne(type => App, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public app: App | null;
+}
diff --git a/src/models/entities/blocking.ts b/src/models/entities/blocking.ts
new file mode 100644
index 0000000000..48487cb086
--- /dev/null
+++ b/src/models/entities/blocking.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['blockerId', 'blockeeId'], { unique: true })
+export class Blocking {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Blocking.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The blockee user ID.'
+ })
+ public blockeeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public blockee: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The blocker user ID.'
+ })
+ public blockerId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public blocker: User | null;
+}
diff --git a/src/models/entities/drive-file.ts b/src/models/entities/drive-file.ts
new file mode 100644
index 0000000000..a8f8c69e56
--- /dev/null
+++ b/src/models/entities/drive-file.ts
@@ -0,0 +1,154 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { DriveFolder } from './drive-folder';
+import { id } from '../id';
+
+@Entity()
+export class DriveFile {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the DriveFile.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The owner ID.'
+ })
+ public userId: User['id'] | null;
+
+ @ManyToOne(type => User, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The host of owner. It will be null if the user in local.'
+ })
+ public userHost: string | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 32,
+ comment: 'The MD5 hash of the DriveFile.'
+ })
+ public md5: string;
+
+ @Column('varchar', {
+ length: 256,
+ comment: 'The file name of the DriveFile.'
+ })
+ public name: string;
+
+ @Index()
+ @Column('varchar', {
+ length: 128,
+ comment: 'The content type (MIME) of the DriveFile.'
+ })
+ public type: string;
+
+ @Column('integer', {
+ comment: 'The file size (bytes) of the DriveFile.'
+ })
+ public size: number;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The comment of the DriveFile.'
+ })
+ public comment: string | null;
+
+ @Column('jsonb', {
+ default: {},
+ comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
+ })
+ public properties: Record<string, any>;
+
+ @Column('boolean')
+ public storedInternal: boolean;
+
+ @Column('varchar', {
+ length: 512,
+ comment: 'The URL of the DriveFile.'
+ })
+ public url: string;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The URL of the thumbnail of the DriveFile.'
+ })
+ public thumbnailUrl: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The URL of the webpublic of the DriveFile.'
+ })
+ public webpublicUrl: string | null;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 256,
+ })
+ public accessKey: string;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public thumbnailAccessKey: string | null;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public webpublicAccessKey: string | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.'
+ })
+ public uri: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ })
+ public src: string | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The parent folder ID. If null, it means the DriveFile is located in root.'
+ })
+ public folderId: DriveFolder['id'] | null;
+
+ @ManyToOne(type => DriveFolder, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public folder: DriveFolder | null;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the DriveFile is NSFW.'
+ })
+ public isSensitive: boolean;
+
+ /**
+ * 外部の(信頼されていない)URLへの直リンクか否か
+ */
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the DriveFile is direct link to remote server.'
+ })
+ public isRemote: boolean;
+}
diff --git a/src/models/entities/drive-folder.ts b/src/models/entities/drive-folder.ts
new file mode 100644
index 0000000000..a80d075855
--- /dev/null
+++ b/src/models/entities/drive-folder.ts
@@ -0,0 +1,49 @@
+import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class DriveFolder {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the DriveFolder.'
+ })
+ public createdAt: Date;
+
+ @Column('varchar', {
+ length: 128,
+ comment: 'The name of the DriveFolder.'
+ })
+ public name: string;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The owner ID.'
+ })
+ public userId: User['id'] | null;
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.'
+ })
+ public parentId: DriveFolder['id'] | null;
+
+ @ManyToOne(type => DriveFolder, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public parent: DriveFolder | null;
+}
diff --git a/src/models/entities/emoji.ts b/src/models/entities/emoji.ts
new file mode 100644
index 0000000000..da04da897e
--- /dev/null
+++ b/src/models/entities/emoji.ts
@@ -0,0 +1,46 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+@Index(['name', 'host'], { unique: true })
+export class Emoji {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ nullable: true
+ })
+ public updatedAt: Date | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 128
+ })
+ public name: string;
+
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true
+ })
+ public host: string | null;
+
+ @Column('varchar', {
+ length: 256,
+ })
+ public url: string;
+
+ @Column('varchar', {
+ length: 256, nullable: true
+ })
+ public uri: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true
+ })
+ public type: string | null;
+
+ @Column('varchar', {
+ array: true, length: 128, default: '{}'
+ })
+ public aliases: string[];
+}
diff --git a/src/models/entities/follow-request.ts b/src/models/entities/follow-request.ts
new file mode 100644
index 0000000000..80a71fe482
--- /dev/null
+++ b/src/models/entities/follow-request.ts
@@ -0,0 +1,85 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['followerId', 'followeeId'], { unique: true })
+export class FollowRequest {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the FollowRequest.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The followee user ID.'
+ })
+ public followeeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public followee: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The follower user ID.'
+ })
+ public followerId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public follower: User | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'id of Follow Activity.'
+ })
+ public requestId: string | null;
+
+ //#region Denormalized fields
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerHost: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerInbox: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerSharedInbox: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeHost: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeInbox: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeSharedInbox: string | null;
+ //#endregion
+}
diff --git a/src/models/entities/following.ts b/src/models/entities/following.ts
new file mode 100644
index 0000000000..963873d112
--- /dev/null
+++ b/src/models/entities/following.ts
@@ -0,0 +1,80 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['followerId', 'followeeId'], { unique: true })
+export class Following {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Following.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The followee user ID.'
+ })
+ public followeeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public followee: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The follower user ID.'
+ })
+ public followerId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public follower: User | null;
+
+ //#region Denormalized fields
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerHost: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerInbox: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerSharedInbox: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeHost: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeInbox: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeSharedInbox: string | null;
+ //#endregion
+}
diff --git a/src/models/entities/games/reversi/game.ts b/src/models/entities/games/reversi/game.ts
new file mode 100644
index 0000000000..9deacaf5c6
--- /dev/null
+++ b/src/models/entities/games/reversi/game.ts
@@ -0,0 +1,133 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from '../../user';
+import { id } from '../../../id';
+
+@Entity()
+export class ReversiGame {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the ReversiGame.'
+ })
+ public createdAt: Date;
+
+ @Column('timestamp with time zone', {
+ nullable: true,
+ comment: 'The started date of the ReversiGame.'
+ })
+ public startedAt: Date | null;
+
+ @Column(id())
+ public user1Id: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user1: User | null;
+
+ @Column(id())
+ public user2Id: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user2: User | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public user1Accepted: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public user2Accepted: boolean;
+
+ /**
+ * どちらのプレイヤーが先行(黒)か
+ * 1 ... user1
+ * 2 ... user2
+ */
+ @Column('integer', {
+ nullable: true,
+ })
+ public black: number | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isStarted: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isEnded: boolean;
+
+ @Column({
+ ...id(),
+ nullable: true
+ })
+ public winnerId: User['id'] | null;
+
+ @Column({
+ ...id(),
+ nullable: true
+ })
+ public surrendered: User['id'] | null;
+
+ @Column('jsonb', {
+ default: [],
+ })
+ public logs: {
+ at: Date;
+ color: boolean;
+ pos: number;
+ }[];
+
+ @Column('varchar', {
+ array: true, length: 64,
+ })
+ public map: string[];
+
+ @Column('varchar', {
+ length: 32
+ })
+ public bw: string;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isLlotheo: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public canPutEverywhere: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public loopedBoard: boolean;
+
+ @Column('jsonb', {
+ nullable: true, default: null,
+ })
+ public form1: any | null;
+
+ @Column('jsonb', {
+ nullable: true, default: null,
+ })
+ public form2: any | null;
+
+ /**
+ * ログのposを文字列としてすべて連結したもののCRC32値
+ */
+ @Column('varchar', {
+ length: 32, nullable: true
+ })
+ public crc32: string | null;
+}
diff --git a/src/models/entities/games/reversi/matching.ts b/src/models/entities/games/reversi/matching.ts
new file mode 100644
index 0000000000..477a29316e
--- /dev/null
+++ b/src/models/entities/games/reversi/matching.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from '../../user';
+import { id } from '../../../id';
+
+@Entity()
+export class ReversiMatching {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the ReversiMatching.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public parentId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public parent: User | null;
+
+ @Index()
+ @Column(id())
+ public childId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public child: User | null;
+}
diff --git a/src/models/entities/hashtag.ts b/src/models/entities/hashtag.ts
new file mode 100644
index 0000000000..842cdaa562
--- /dev/null
+++ b/src/models/entities/hashtag.ts
@@ -0,0 +1,87 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class Hashtag {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 128
+ })
+ public name: string;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public mentionedUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public mentionedUsersCount: number;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public mentionedLocalUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public mentionedLocalUsersCount: number;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public mentionedRemoteUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public mentionedRemoteUsersCount: number;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public attachedUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public attachedUsersCount: number;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public attachedLocalUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public attachedLocalUsersCount: number;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public attachedRemoteUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public attachedRemoteUsersCount: number;
+}
diff --git a/src/models/entities/instance.ts b/src/models/entities/instance.ts
new file mode 100644
index 0000000000..977054263c
--- /dev/null
+++ b/src/models/entities/instance.ts
@@ -0,0 +1,132 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Instance {
+ @PrimaryColumn(id())
+ public id: string;
+
+ /**
+ * このインスタンスを捕捉した日時
+ */
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The caught date of the Instance.'
+ })
+ public caughtAt: Date;
+
+ /**
+ * ホスト
+ */
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 128,
+ comment: 'The host of the Instance.'
+ })
+ public host: string;
+
+ /**
+ * インスタンスのシステム (MastodonとかMisskeyとかPleromaとか)
+ */
+ @Column('varchar', {
+ length: 64, nullable: true,
+ comment: 'The system of the Instance.'
+ })
+ public system: string | null;
+
+ /**
+ * インスタンスのユーザー数
+ */
+ @Column('integer', {
+ default: 0,
+ comment: 'The count of the users of the Instance.'
+ })
+ public usersCount: number;
+
+ /**
+ * インスタンスの投稿数
+ */
+ @Column('integer', {
+ default: 0,
+ comment: 'The count of the notes of the Instance.'
+ })
+ public notesCount: number;
+
+ /**
+ * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
+ */
+ @Column('integer', {
+ default: 0,
+ })
+ public followingCount: number;
+
+ /**
+ * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
+ */
+ @Column('integer', {
+ default: 0,
+ })
+ public followersCount: number;
+
+ /**
+ * ドライブ使用量
+ */
+ @Column('integer', {
+ default: 0,
+ })
+ public driveUsage: number;
+
+ /**
+ * ドライブのファイル数
+ */
+ @Column('integer', {
+ default: 0,
+ })
+ public driveFiles: number;
+
+ /**
+ * 直近のリクエスト送信日時
+ */
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public latestRequestSentAt: Date | null;
+
+ /**
+ * 直近のリクエスト送信時のHTTPステータスコード
+ */
+ @Column('integer', {
+ nullable: true,
+ })
+ public latestStatus: number | null;
+
+ /**
+ * 直近のリクエスト受信日時
+ */
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public latestRequestReceivedAt: Date | null;
+
+ /**
+ * このインスタンスと最後にやり取りした日時
+ */
+ @Column('timestamp with time zone')
+ public lastCommunicatedAt: Date;
+
+ /**
+ * このインスタンスと不通かどうか
+ */
+ @Column('boolean', {
+ default: false
+ })
+ public isNotResponding: boolean;
+
+ /**
+ * このインスタンスが閉鎖済みとしてマークされているか
+ */
+ @Column('boolean', {
+ default: false
+ })
+ public isMarkedAsClosed: boolean;
+}
diff --git a/src/models/entities/log.ts b/src/models/entities/log.ts
new file mode 100644
index 0000000000..99e1e8947e
--- /dev/null
+++ b/src/models/entities/log.ts
@@ -0,0 +1,46 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Log {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Log.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column('varchar', {
+ length: 64, array: true, default: '{}'
+ })
+ public domain: string[];
+
+ @Index()
+ @Column('enum', {
+ enum: ['error', 'warning', 'info', 'success', 'debug']
+ })
+ public level: string;
+
+ @Column('varchar', {
+ length: 8
+ })
+ public worker: string;
+
+ @Column('varchar', {
+ length: 128
+ })
+ public machine: string;
+
+ @Column('varchar', {
+ length: 1024
+ })
+ public message: string;
+
+ @Column('jsonb', {
+ default: {}
+ })
+ public data: Record<string, any>;
+}
diff --git a/src/models/entities/messaging-message.ts b/src/models/entities/messaging-message.ts
new file mode 100644
index 0000000000..d3c3eab3a2
--- /dev/null
+++ b/src/models/entities/messaging-message.ts
@@ -0,0 +1,64 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+
+@Entity()
+export class MessagingMessage {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the MessagingMessage.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The sender user ID.'
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The recipient user ID.'
+ })
+ public recipientId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public recipient: User | null;
+
+ @Column('varchar', {
+ length: 4096, nullable: true
+ })
+ public text: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isRead: boolean;
+
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public fileId: DriveFile['id'] | null;
+
+ @ManyToOne(type => DriveFile, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public file: DriveFile | null;
+}
diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts
new file mode 100644
index 0000000000..c34f5b6904
--- /dev/null
+++ b/src/models/entities/meta.ts
@@ -0,0 +1,264 @@
+import { Entity, Column, PrimaryColumn } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Meta {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('varchar', {
+ length: 128, nullable: true
+ })
+ public name: string | null;
+
+ @Column('varchar', {
+ length: 1024, nullable: true
+ })
+ public description: string | null;
+
+ /**
+ * メンテナの名前
+ */
+ @Column('varchar', {
+ length: 128, nullable: true
+ })
+ public maintainerName: string | null;
+
+ /**
+ * メンテナの連絡先
+ */
+ @Column('varchar', {
+ length: 128, nullable: true
+ })
+ public maintainerEmail: string | null;
+
+ @Column('jsonb', {
+ default: [],
+ })
+ public announcements: Record<string, any>[];
+
+ @Column('boolean', {
+ default: false,
+ })
+ public disableRegistration: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public disableLocalTimeline: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public disableGlobalTimeline: boolean;
+
+ @Column('boolean', {
+ default: true,
+ })
+ public enableEmojiReaction: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public useStarForReactionFallback: boolean;
+
+ @Column('varchar', {
+ length: 64, array: true, default: '{}'
+ })
+ public langs: string[];
+
+ @Column('varchar', {
+ length: 256, array: true, default: '{}'
+ })
+ public hiddenTags: string[];
+
+ @Column('varchar', {
+ length: 256, array: true, default: '{}'
+ })
+ public blockedHosts: string[];
+
+ @Column('varchar', {
+ length: 256,
+ nullable: true,
+ default: '/assets/ai.png'
+ })
+ public mascotImageUrl: string | null;
+
+ @Column('varchar', {
+ length: 256,
+ nullable: true
+ })
+ public bannerUrl: string | null;
+
+ @Column('varchar', {
+ length: 256,
+ nullable: true,
+ default: 'https://ai.misskey.xyz/aiart/yubitun.png'
+ })
+ public errorImageUrl: string | null;
+
+ @Column('varchar', {
+ length: 256,
+ nullable: true
+ })
+ public iconUrl: string | null;
+
+ @Column('boolean', {
+ default: true,
+ })
+ public cacheRemoteFiles: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public proxyAccount: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableRecaptcha: boolean;
+
+ @Column('varchar', {
+ length: 64,
+ nullable: true
+ })
+ public recaptchaSiteKey: string | null;
+
+ @Column('varchar', {
+ length: 64,
+ nullable: true
+ })
+ public recaptchaSecretKey: string | null;
+
+ @Column('integer', {
+ default: 1024,
+ comment: 'Drive capacity of a local user (MB)'
+ })
+ public localDriveCapacityMb: number;
+
+ @Column('integer', {
+ default: 32,
+ comment: 'Drive capacity of a remote user (MB)'
+ })
+ public remoteDriveCapacityMb: number;
+
+ @Column('integer', {
+ default: 500,
+ comment: 'Max allowed note text length in characters'
+ })
+ public maxNoteTextLength: number;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public summalyProxy: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableEmail: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public email: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public smtpSecure: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public smtpHost: string | null;
+
+ @Column('integer', {
+ nullable: true
+ })
+ public smtpPort: number | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public smtpUser: string | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public smtpPass: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableServiceWorker: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public swPublicKey: string | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public swPrivateKey: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableTwitterIntegration: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public twitterConsumerKey: string | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public twitterConsumerSecret: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableGithubIntegration: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public githubClientId: string | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public githubClientSecret: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableDiscordIntegration: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public discordClientId: string | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public discordClientSecret: string | null;
+}
diff --git a/src/models/entities/muting.ts b/src/models/entities/muting.ts
new file mode 100644
index 0000000000..0084213bcc
--- /dev/null
+++ b/src/models/entities/muting.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['muterId', 'muteeId'], { unique: true })
+export class Muting {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Muting.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The mutee user ID.'
+ })
+ public muteeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public mutee: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The muter user ID.'
+ })
+ public muterId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public muter: User | null;
+}
diff --git a/src/models/entities/note-favorite.ts b/src/models/entities/note-favorite.ts
new file mode 100644
index 0000000000..0713c3ae56
--- /dev/null
+++ b/src/models/entities/note-favorite.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteFavorite {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the NoteFavorite.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column(id())
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+}
diff --git a/src/models/entities/note-reaction.ts b/src/models/entities/note-reaction.ts
new file mode 100644
index 0000000000..1ce5d841fb
--- /dev/null
+++ b/src/models/entities/note-reaction.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteReaction {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the NoteReaction.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column(id())
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ @Column('varchar', {
+ length: 32
+ })
+ public reaction: string;
+}
diff --git a/src/models/entities/note-unread.ts b/src/models/entities/note-unread.ts
new file mode 100644
index 0000000000..2d18728256
--- /dev/null
+++ b/src/models/entities/note-unread.ts
@@ -0,0 +1,43 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteUnread {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column(id())
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ @Column({
+ ...id(),
+ comment: '[Denormalized]'
+ })
+ public noteUserId: User['id'];
+
+ /**
+ * ダイレクト投稿か
+ */
+ @Column('boolean')
+ public isSpecified: boolean;
+}
diff --git a/src/models/entities/note-watching.ts b/src/models/entities/note-watching.ts
new file mode 100644
index 0000000000..741a1c0c8b
--- /dev/null
+++ b/src/models/entities/note-watching.ts
@@ -0,0 +1,52 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteWatching {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the NoteWatching.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The watcher ID.'
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The target Note ID.'
+ })
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ //#region Denormalized fields
+ @Index()
+ @Column({
+ ...id(),
+ comment: '[Denormalized]'
+ })
+ public noteUserId: Note['userId'];
+ //#endregion
+}
diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts
new file mode 100644
index 0000000000..0bcb9b4a44
--- /dev/null
+++ b/src/models/entities/note.ts
@@ -0,0 +1,236 @@
+import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+
+@Entity()
+export class Note {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Note.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ nullable: true,
+ comment: 'The updated date of the Note.'
+ })
+ public updatedAt: Date | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The ID of reply target.'
+ })
+ public replyId: Note['id'] | null;
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public reply: Note | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The ID of renote target.'
+ })
+ public renoteId: Note['id'] | null;
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public renote: Note | null;
+
+ @Column({
+ type: 'text', nullable: true
+ })
+ public text: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true
+ })
+ public name: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true
+ })
+ public cw: string | null;
+
+ @Column({
+ ...id(),
+ nullable: true
+ })
+ public appId: App['id'] | null;
+
+ @ManyToOne(type => App, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public app: App | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The ID of author.'
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('boolean', {
+ default: false
+ })
+ public viaMobile: boolean;
+
+ @Column('boolean', {
+ default: false
+ })
+ public localOnly: boolean;
+
+ @Column('integer', {
+ default: 0
+ })
+ public renoteCount: number;
+
+ @Column('integer', {
+ default: 0
+ })
+ public repliesCount: number;
+
+ @Column('jsonb', {
+ default: {}
+ })
+ public reactions: Record<string, number>;
+
+ /**
+ * public ... 公開
+ * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
+ * followers ... フォロワーのみ
+ * specified ... visibleUserIds で指定したユーザーのみ
+ */
+ @Column('enum', { enum: ['public', 'home', 'followers', 'specified'] })
+ public visibility: 'public' | 'home' | 'followers' | 'specified';
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: 'The URI of a note. it will be null when the note is local.'
+ })
+ public uri: string | null;
+
+ @Column('integer', {
+ default: 0
+ })
+ public score: number;
+
+ @Column({
+ ...id(),
+ array: true, default: '{}'
+ })
+ public fileIds: DriveFile['id'][];
+
+ @Column('varchar', {
+ length: 256, array: true, default: '{}'
+ })
+ public attachedFileTypes: string[];
+
+ @Index()
+ @Column({
+ ...id(),
+ array: true, default: '{}'
+ })
+ public visibleUserIds: User['id'][];
+
+ @Index()
+ @Column({
+ ...id(),
+ array: true, default: '{}'
+ })
+ public mentions: User['id'][];
+
+ @Column('text', {
+ default: '[]'
+ })
+ public mentionedRemoteUsers: string;
+
+ @Column('varchar', {
+ length: 128, array: true, default: '{}'
+ })
+ public emojis: string[];
+
+ @Index()
+ @Column('varchar', {
+ length: 128, array: true, default: '{}'
+ })
+ public tags: string[];
+
+ @Column('boolean', {
+ default: false
+ })
+ public hasPoll: boolean;
+
+ @Column('jsonb', {
+ nullable: true, default: {}
+ })
+ public geo: any | null;
+
+ //#region Denormalized fields
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public userHost: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public userInbox: string | null;
+
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: '[Denormalized]'
+ })
+ public replyUserId: User['id'] | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public replyUserHost: string | null;
+
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: '[Denormalized]'
+ })
+ public renoteUserId: User['id'] | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public renoteUserHost: string | null;
+ //#endregion
+}
+
+export type IMentionedRemoteUsers = {
+ uri: string;
+ username: string;
+ host: string;
+}[];
diff --git a/src/models/entities/notification.ts b/src/models/entities/notification.ts
new file mode 100644
index 0000000000..627a57bece
--- /dev/null
+++ b/src/models/entities/notification.ts
@@ -0,0 +1,94 @@
+import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { Note } from './note';
+
+@Entity()
+export class Notification {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Notification.'
+ })
+ public createdAt: Date;
+
+ /**
+ * 通知の受信者
+ */
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The ID of recipient user of the Notification.'
+ })
+ public notifieeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public notifiee: User | null;
+
+ /**
+ * 通知の送信者(initiator)
+ */
+ @Column({
+ ...id(),
+ comment: 'The ID of sender user of the Notification.'
+ })
+ public notifierId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public notifier: User | null;
+
+ /**
+ * 通知の種類。
+ * follow - フォローされた
+ * mention - 投稿で自分が言及された
+ * reply - (自分または自分がWatchしている)投稿が返信された
+ * renote - (自分または自分がWatchしている)投稿がRenoteされた
+ * quote - (自分または自分がWatchしている)投稿が引用Renoteされた
+ * reaction - (自分または自分がWatchしている)投稿にリアクションされた
+ * pollVote - (自分または自分がWatchしている)投稿の投票に投票された
+ */
+ @Column('varchar', {
+ length: 32,
+ comment: 'The type of the Notification.'
+ })
+ public type: string;
+
+ /**
+ * 通知が読まれたかどうか
+ */
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the Notification is read.'
+ })
+ public isRead: boolean;
+
+ @Column({
+ ...id(),
+ nullable: true
+ })
+ public noteId: Note['id'] | null;
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true
+ })
+ public reaction: string;
+
+ @Column('integer', {
+ nullable: true
+ })
+ public choice: number;
+}
diff --git a/src/models/entities/poll-vote.ts b/src/models/entities/poll-vote.ts
new file mode 100644
index 0000000000..709376f909
--- /dev/null
+++ b/src/models/entities/poll-vote.ts
@@ -0,0 +1,40 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId', 'choice'], { unique: true })
+export class PollVote {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the PollVote.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column(id())
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ @Column('integer')
+ public choice: number;
+}
diff --git a/src/models/entities/poll.ts b/src/models/entities/poll.ts
new file mode 100644
index 0000000000..204f102f51
--- /dev/null
+++ b/src/models/entities/poll.ts
@@ -0,0 +1,67 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { id } from '../id';
+import { Note } from './note';
+import { User } from './user';
+
+@Entity()
+export class Poll {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index({ unique: true })
+ @Column(id())
+ public noteId: Note['id'];
+
+ @OneToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ @Column('timestamp with time zone', {
+ nullable: true
+ })
+ public expiresAt: Date | null;
+
+ @Column('boolean')
+ public multiple: boolean;
+
+ @Column('varchar', {
+ length: 128, array: true, default: '{}'
+ })
+ public choices: string[];
+
+ @Column('integer', {
+ array: true,
+ })
+ public votes: number[];
+
+ //#region Denormalized fields
+ @Column('enum', {
+ enum: ['public', 'home', 'followers', 'specified'],
+ comment: '[Denormalized]'
+ })
+ public noteVisibility: 'public' | 'home' | 'followers' | 'specified';
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: '[Denormalized]'
+ })
+ public userId: User['id'];
+
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public userHost: string | null;
+ //#endregion
+}
+
+export type IPoll = {
+ choices: string[];
+ votes?: number[];
+ multiple: boolean;
+ expiresAt: Date;
+};
diff --git a/src/models/entities/registration-tickets.ts b/src/models/entities/registration-tickets.ts
new file mode 100644
index 0000000000..d962f78a78
--- /dev/null
+++ b/src/models/entities/registration-tickets.ts
@@ -0,0 +1,17 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class RegistrationTicket {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone')
+ public createdAt: Date;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 64,
+ })
+ public code: string;
+}
diff --git a/src/models/entities/signin.ts b/src/models/entities/signin.ts
new file mode 100644
index 0000000000..7e047084b1
--- /dev/null
+++ b/src/models/entities/signin.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class Signin {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Signin.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('varchar', {
+ length: 128,
+ })
+ public ip: string;
+
+ @Column('jsonb')
+ public headers: Record<string, any>;
+
+ @Column('boolean')
+ public success: boolean;
+}
diff --git a/src/models/entities/sw-subscription.ts b/src/models/entities/sw-subscription.ts
new file mode 100644
index 0000000000..f0f2a69f1b
--- /dev/null
+++ b/src/models/entities/sw-subscription.ts
@@ -0,0 +1,37 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class SwSubscription {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone')
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('varchar', {
+ length: 256,
+ })
+ public endpoint: string;
+
+ @Column('varchar', {
+ length: 256,
+ })
+ public auth: string;
+
+ @Column('varchar', {
+ length: 128,
+ })
+ public publickey: string;
+}
diff --git a/src/models/entities/user-keypair.ts b/src/models/entities/user-keypair.ts
new file mode 100644
index 0000000000..06b98d2536
--- /dev/null
+++ b/src/models/entities/user-keypair.ts
@@ -0,0 +1,24 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserKeypair {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index({ unique: true })
+ @Column(id())
+ public userId: User['id'];
+
+ @OneToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('varchar', {
+ length: 4096,
+ })
+ public keyPem: string;
+}
diff --git a/src/models/entities/user-list-joining.ts b/src/models/entities/user-list-joining.ts
new file mode 100644
index 0000000000..8af4efb6a7
--- /dev/null
+++ b/src/models/entities/user-list-joining.ts
@@ -0,0 +1,41 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { UserList } from './user-list';
+import { id } from '../id';
+
+@Entity()
+export class UserListJoining {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the UserListJoining.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The user ID.'
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The list ID.'
+ })
+ public userListId: UserList['id'];
+
+ @ManyToOne(type => UserList, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public userList: UserList | null;
+}
diff --git a/src/models/entities/user-list.ts b/src/models/entities/user-list.ts
new file mode 100644
index 0000000000..35a83ef8c3
--- /dev/null
+++ b/src/models/entities/user-list.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserList {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the UserList.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The owner ID.'
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('varchar', {
+ length: 128,
+ comment: 'The name of the UserList.'
+ })
+ public name: string;
+}
diff --git a/src/models/entities/user-note-pinings.ts b/src/models/entities/user-note-pinings.ts
new file mode 100644
index 0000000000..04a6f8f645
--- /dev/null
+++ b/src/models/entities/user-note-pinings.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class UserNotePining {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the UserNotePinings.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column(id())
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+}
diff --git a/src/models/entities/user-publickey.ts b/src/models/entities/user-publickey.ts
new file mode 100644
index 0000000000..6c019f3313
--- /dev/null
+++ b/src/models/entities/user-publickey.ts
@@ -0,0 +1,30 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserPublickey {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index({ unique: true })
+ @Column(id())
+ public userId: User['id'];
+
+ @OneToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 256,
+ })
+ public keyId: string;
+
+ @Column('varchar', {
+ length: 4096,
+ })
+ public keyPem: string;
+}
diff --git a/src/models/entities/user-service-linking.ts b/src/models/entities/user-service-linking.ts
new file mode 100644
index 0000000000..3d99554e1e
--- /dev/null
+++ b/src/models/entities/user-service-linking.ts
@@ -0,0 +1,108 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserServiceLinking {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index({ unique: true })
+ @Column(id())
+ public userId: User['id'];
+
+ @OneToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public twitter: boolean;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public twitterAccessToken: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public twitterAccessTokenSecret: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public twitterUserId: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public twitterScreenName: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public github: boolean;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public githubAccessToken: string | null;
+
+ @Column('integer', {
+ nullable: true, default: null,
+ })
+ public githubId: number | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public githubLogin: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public discord: boolean;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public discordAccessToken: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public discordRefreshToken: string | null;
+
+ @Column('integer', {
+ nullable: true, default: null,
+ })
+ public discordExpiresDate: number | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public discordId: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public discordUsername: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public discordDiscriminator: string | null;
+
+ //#region Denormalized fields
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public userHost: string | null;
+ //#endregion
+}
diff --git a/src/models/entities/user.ts b/src/models/entities/user.ts
new file mode 100644
index 0000000000..1ef98cadc2
--- /dev/null
+++ b/src/models/entities/user.ts
@@ -0,0 +1,297 @@
+import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+
+@Entity()
+@Index(['usernameLower', 'host'], { unique: true })
+export class User {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the User.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ nullable: true,
+ comment: 'The updated date of the User.'
+ })
+ public updatedAt: Date | null;
+
+ @Column('timestamp with time zone', {
+ nullable: true
+ })
+ public lastFetchedAt: Date | null;
+
+ @Column('varchar', {
+ length: 128,
+ comment: 'The username of the User.'
+ })
+ public username: string;
+
+ @Index()
+ @Column('varchar', {
+ length: 128, select: false,
+ comment: 'The username (lowercased) of the User.'
+ })
+ public usernameLower: string;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The name of the User.'
+ })
+ public name: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The location of the User.'
+ })
+ public location: string | null;
+
+ @Column('char', {
+ length: 10, nullable: true,
+ comment: 'The birthday (YYYY-MM-DD) of the User.'
+ })
+ public birthday: string | null;
+
+ @Column('integer', {
+ default: 0,
+ comment: 'The count of followers.'
+ })
+ public followersCount: number;
+
+ @Column('integer', {
+ default: 0,
+ comment: 'The count of following.'
+ })
+ public followingCount: number;
+
+ @Column('integer', {
+ default: 0,
+ comment: 'The count of notes.'
+ })
+ public notesCount: number;
+
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The ID of avatar DriveFile.'
+ })
+ public avatarId: DriveFile['id'] | null;
+
+ @OneToOne(type => DriveFile, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public avatar: DriveFile | null;
+
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The ID of banner DriveFile.'
+ })
+ public bannerId: DriveFile['id'] | null;
+
+ @OneToOne(type => DriveFile, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public banner: DriveFile | null;
+
+ @Column('varchar', {
+ length: 1024, nullable: true,
+ comment: 'The description (bio) of the User.'
+ })
+ public description: string | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 128, array: true, default: '{}'
+ })
+ public tags: string[];
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The email address of the User.'
+ })
+ public email: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public emailVerifyCode: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public emailVerified: boolean;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public twoFactorTempSecret: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public twoFactorSecret: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public avatarUrl: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public bannerUrl: string | null;
+
+ @Column('varchar', {
+ length: 32, nullable: true,
+ })
+ public avatarColor: string | null;
+
+ @Column('varchar', {
+ length: 32, nullable: true,
+ })
+ public bannerColor: string | null;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is suspended.'
+ })
+ public isSuspended: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is silenced.'
+ })
+ public isSilenced: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is locked.'
+ })
+ public isLocked: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is a bot.'
+ })
+ public isBot: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is a cat.'
+ })
+ public isCat: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is the admin.'
+ })
+ public isAdmin: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is a moderator.'
+ })
+ public isModerator: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isVerified: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public twoFactorEnabled: boolean;
+
+ @Column('varchar', {
+ length: 128, array: true, default: '{}'
+ })
+ public emojis: string[];
+
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The host of the User. It will be null if the origin of the user is local.'
+ })
+ public host: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: 'The inbox of the User. It will be null if the origin of the user is local.'
+ })
+ public inbox: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: 'The sharedInbox of the User. It will be null if the origin of the user is local.'
+ })
+ public sharedInbox: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: 'The featured of the User. It will be null if the origin of the user is local.'
+ })
+ public featured: string | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 256, nullable: true,
+ comment: 'The URI of the User. It will be null if the origin of the user is local.'
+ })
+ public uri: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The password hash of the User. It will be null if the origin of the user is local.'
+ })
+ public password: string | null;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 32, nullable: true, unique: true,
+ comment: 'The native access token of the User. It will be null if the origin of the user is local.'
+ })
+ public token: string | null;
+
+ @Column('jsonb', {
+ default: {},
+ comment: 'The client-specific data of the User.'
+ })
+ public clientData: Record<string, any>;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public autoWatch: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public autoAcceptFollowed: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public alwaysMarkNsfw: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public carefulBot: boolean;
+}
+
+export interface ILocalUser extends User {
+ host: null;
+}
+
+export interface IRemoteUser extends User {
+ host: string;
+}
diff --git a/src/models/favorite.ts b/src/models/favorite.ts
deleted file mode 100644
index 2008edbfaf..0000000000
--- a/src/models/favorite.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packNote } from './note';
-import { dbLogger } from '../db/logger';
-
-const Favorite = db.get<IFavorite>('favorites');
-Favorite.createIndex('userId');
-Favorite.createIndex(['userId', 'noteId'], { unique: true });
-export default Favorite;
-
-export type IFavorite = {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID;
- noteId: mongo.ObjectID;
-};
-
-export const packMany = (
- favorites: any[],
- me: any
-) => {
- return Promise.all(favorites.map(f => pack(f, me)));
-};
-
-/**
- * Pack a favorite for API response
- */
-export const pack = (
- favorite: any,
- me: any
-) => new Promise<any>(async (resolve, reject) => {
- let _favorite: any;
-
- // Populate the favorite if 'favorite' is ID
- if (isObjectId(favorite)) {
- _favorite = await Favorite.findOne({
- _id: favorite
- });
- } else if (typeof favorite === 'string') {
- _favorite = await Favorite.findOne({
- _id: new mongo.ObjectID(favorite)
- });
- } else {
- _favorite = deepcopy(favorite);
- }
-
- // Rename _id to id
- _favorite.id = _favorite._id;
- delete _favorite._id;
-
- // Populate note
- _favorite.note = await packNote(_favorite.noteId, me, {
- detail: true
- });
-
- // (データベースの不具合などで)投稿が見つからなかったら
- if (_favorite.note == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: favorite -> note :: ${_favorite.id} (note ${_favorite.noteId})`);
- return resolve(null);
- }
-
- resolve(_favorite);
-});
diff --git a/src/models/follow-request.ts b/src/models/follow-request.ts
deleted file mode 100644
index 4f75c63a32..0000000000
--- a/src/models/follow-request.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packUser } from './user';
-
-const FollowRequest = db.get<IFollowRequest>('followRequests');
-FollowRequest.createIndex('followerId');
-FollowRequest.createIndex('followeeId');
-FollowRequest.createIndex(['followerId', 'followeeId'], { unique: true });
-export default FollowRequest;
-
-export type IFollowRequest = {
- _id: mongo.ObjectID;
- createdAt: Date;
- followeeId: mongo.ObjectID;
- followerId: mongo.ObjectID;
- requestId?: string; // id of Follow Activity
-
- // 非正規化
- _followee: {
- host: string;
- inbox?: string;
- sharedInbox?: string;
- },
- _follower: {
- host: string;
- inbox?: string;
- sharedInbox?: string;
- }
-};
-
-/**
- * Pack a request for API response
- */
-export const pack = (
- request: any,
- me?: any
-) => new Promise<any>(async (resolve, reject) => {
- let _request: any;
-
- // Populate the request if 'request' is ID
- if (isObjectId(request)) {
- _request = await FollowRequest.findOne({
- _id: request
- });
- } else if (typeof request === 'string') {
- _request = await FollowRequest.findOne({
- _id: new mongo.ObjectID(request)
- });
- } else {
- _request = deepcopy(request);
- }
-
- // Rename _id to id
- _request.id = _request._id;
- delete _request._id;
-
- // Populate follower
- _request.follower = await packUser(_request.followerId, me);
-
- // Populate followee
- _request.followee = await packUser(_request.followeeId, me);
-
- resolve(_request);
-});
diff --git a/src/models/following.ts b/src/models/following.ts
deleted file mode 100644
index 12cc27211b..0000000000
--- a/src/models/following.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Following = db.get<IFollowing>('following');
-Following.createIndex('followerId');
-Following.createIndex('followeeId');
-Following.createIndex(['followerId', 'followeeId'], { unique: true });
-export default Following;
-
-export type IFollowing = {
- _id: mongo.ObjectID;
- createdAt: Date;
- followeeId: mongo.ObjectID;
- followerId: mongo.ObjectID;
-
- // 非正規化
- _followee: {
- host: string;
- inbox?: string;
- sharedInbox?: string;
- },
- _follower: {
- host: string;
- inbox?: string;
- sharedInbox?: string;
- }
-};
diff --git a/src/models/games/reversi/game.ts b/src/models/games/reversi/game.ts
deleted file mode 100644
index 57c493cff5..0000000000
--- a/src/models/games/reversi/game.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../../../db/mongodb';
-import isObjectId from '../../../misc/is-objectid';
-import { IUser, pack as packUser } from '../../user';
-
-const ReversiGame = db.get<IReversiGame>('reversiGames');
-export default ReversiGame;
-
-export interface IReversiGame {
- _id: mongo.ObjectID;
- createdAt: Date;
- startedAt: Date;
- user1Id: mongo.ObjectID;
- user2Id: mongo.ObjectID;
- user1Accepted: boolean;
- user2Accepted: boolean;
-
- /**
- * どちらのプレイヤーが先行(黒)か
- * 1 ... user1
- * 2 ... user2
- */
- black: number;
-
- isStarted: boolean;
- isEnded: boolean;
- winnerId: mongo.ObjectID;
- surrendered: mongo.ObjectID;
- logs: {
- at: Date;
- color: boolean;
- pos: number;
- }[];
- settings: {
- map: string[];
- bw: string | number;
- isLlotheo: boolean;
- canPutEverywhere: boolean;
- loopedBoard: boolean;
- };
- form1: any;
- form2: any;
-
- // ログのposを文字列としてすべて連結したもののCRC32値
- crc32: string;
-}
-
-/**
- * Pack an reversi game for API response
- */
-export const pack = (
- game: any,
- me?: string | mongo.ObjectID | IUser,
- options?: {
- detail?: boolean
- }
-) => new Promise<any>(async (resolve, reject) => {
- const opts = Object.assign({
- detail: true
- }, options);
-
- let _game: any;
-
- // Populate the game if 'game' is ID
- if (isObjectId(game)) {
- _game = await ReversiGame.findOne({
- _id: game
- });
- } else if (typeof game === 'string') {
- _game = await ReversiGame.findOne({
- _id: new mongo.ObjectID(game)
- });
- } else {
- _game = deepcopy(game);
- }
-
- // Me
- const meId: mongo.ObjectID = me
- ? isObjectId(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- // Rename _id to id
- _game.id = _game._id;
- delete _game._id;
-
- if (opts.detail === false) {
- delete _game.logs;
- delete _game.settings.map;
- } else {
- // 互換性のため
- if (_game.settings.map.hasOwnProperty('size')) {
- _game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g'));
- }
- }
-
- // Populate user
- _game.user1 = await packUser(_game.user1Id, meId);
- _game.user2 = await packUser(_game.user2Id, meId);
- if (_game.winnerId) {
- _game.winner = await packUser(_game.winnerId, meId);
- } else {
- _game.winner = null;
- }
-
- resolve(_game);
-});
diff --git a/src/models/games/reversi/matching.ts b/src/models/games/reversi/matching.ts
deleted file mode 100644
index ba2ac1bc05..0000000000
--- a/src/models/games/reversi/matching.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../../../db/mongodb';
-import isObjectId from '../../../misc/is-objectid';
-import { IUser, pack as packUser } from '../../user';
-
-const Matching = db.get<IMatching>('reversiMatchings');
-export default Matching;
-
-export interface IMatching {
- _id: mongo.ObjectID;
- createdAt: Date;
- parentId: mongo.ObjectID;
- childId: mongo.ObjectID;
-}
-
-/**
- * Pack an reversi matching for API response
- */
-export const pack = (
- matching: any,
- me?: string | mongo.ObjectID | IUser
-) => new Promise<any>(async (resolve, reject) => {
-
- // Me
- const meId: mongo.ObjectID = me
- ? isObjectId(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- const _matching = deepcopy(matching);
-
- // Rename _id to id
- _matching.id = _matching._id;
- delete _matching._id;
-
- // Populate user
- _matching.parent = await packUser(_matching.parentId, meId);
- _matching.child = await packUser(_matching.childId, meId);
-
- resolve(_matching);
-});
diff --git a/src/models/hashtag.ts b/src/models/hashtag.ts
deleted file mode 100644
index c1de42086e..0000000000
--- a/src/models/hashtag.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Hashtag = db.get<IHashtags>('hashtags');
-Hashtag.createIndex('tag', { unique: true });
-Hashtag.createIndex('mentionedUsersCount');
-Hashtag.createIndex('mentionedLocalUsersCount');
-Hashtag.createIndex('mentionedRemoteUsersCount');
-Hashtag.createIndex('attachedUsersCount');
-Hashtag.createIndex('attachedLocalUsersCount');
-Hashtag.createIndex('attachedRemoteUsersCount');
-export default Hashtag;
-
-// 後方互換性のため
-Hashtag.findOne({ attachedUserIds: { $exists: false }}).then(h => {
- if (h != null) {
- Hashtag.update({}, {
- $rename: {
- mentionedUserIdsCount: 'mentionedUsersCount'
- },
- $set: {
- mentionedLocalUserIds: [],
- mentionedLocalUsersCount: 0,
- attachedUserIds: [],
- attachedUsersCount: 0,
- attachedLocalUserIds: [],
- attachedLocalUsersCount: 0,
- }
- }, {
- multi: true
- });
- }
-});
-Hashtag.findOne({ attachedRemoteUserIds: { $exists: false }}).then(h => {
- if (h != null) {
- Hashtag.update({}, {
- $set: {
- mentionedRemoteUserIds: [],
- mentionedRemoteUsersCount: 0,
- attachedRemoteUserIds: [],
- attachedRemoteUsersCount: 0,
- }
- }, {
- multi: true
- });
- }
-});
-
-export interface IHashtags {
- tag: string;
- mentionedUserIds: mongo.ObjectID[];
- mentionedUsersCount: number;
- mentionedLocalUserIds: mongo.ObjectID[];
- mentionedLocalUsersCount: number;
- mentionedRemoteUserIds: mongo.ObjectID[];
- mentionedRemoteUsersCount: number;
- attachedUserIds: mongo.ObjectID[];
- attachedUsersCount: number;
- attachedLocalUserIds: mongo.ObjectID[];
- attachedLocalUsersCount: number;
- attachedRemoteUserIds: mongo.ObjectID[];
- attachedRemoteUsersCount: number;
-}
diff --git a/src/models/id.ts b/src/models/id.ts
new file mode 100644
index 0000000000..be2cccfe3b
--- /dev/null
+++ b/src/models/id.ts
@@ -0,0 +1,4 @@
+export const id = () => ({
+ type: 'varchar' as 'varchar',
+ length: 32
+});
diff --git a/src/models/index.ts b/src/models/index.ts
new file mode 100644
index 0000000000..f88bb8d636
--- /dev/null
+++ b/src/models/index.ts
@@ -0,0 +1,74 @@
+import { getRepository, getCustomRepository } from 'typeorm';
+import { Instance } from './entities/instance';
+import { Emoji } from './entities/emoji';
+import { Poll } from './entities/poll';
+import { PollVote } from './entities/poll-vote';
+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';
+import { NoteRepository } from './repositories/note';
+import { DriveFileRepository } from './repositories/drive-file';
+import { DriveFolderRepository } from './repositories/drive-folder';
+import { Log } from './entities/log';
+import { AccessToken } from './entities/access-token';
+import { UserNotePining } from './entities/user-note-pinings';
+import { SigninRepository } from './repositories/signin';
+import { MessagingMessageRepository } from './repositories/messaging-message';
+import { ReversiGameRepository } from './repositories/games/reversi/game';
+import { UserListRepository } from './repositories/user-list';
+import { FollowRequestRepository } from './repositories/follow-request';
+import { MutingRepository } from './repositories/muting';
+import { BlockingRepository } from './repositories/blocking';
+import { NoteReactionRepository } from './repositories/note-reaction';
+import { UserServiceLinking } from './entities/user-service-linking';
+import { NotificationRepository } from './repositories/notification';
+import { NoteFavoriteRepository } from './repositories/note-favorite';
+import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
+import { UserPublickey } from './entities/user-publickey';
+import { UserKeypair } from './entities/user-keypair';
+import { AppRepository } from './repositories/app';
+import { FollowingRepository } from './repositories/following';
+import { AbuseUserReportRepository } from './repositories/abuse-user-report';
+import { AuthSessionRepository } from './repositories/auth-session';
+
+export const Apps = getCustomRepository(AppRepository);
+export const Notes = getCustomRepository(NoteRepository);
+export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
+export const NoteWatchings = getRepository(NoteWatching);
+export const NoteReactions = getCustomRepository(NoteReactionRepository);
+export const NoteUnreads = getRepository(NoteUnread);
+export const Polls = getRepository(Poll);
+export const PollVotes = getRepository(PollVote);
+export const Users = getCustomRepository(UserRepository);
+export const UserKeypairs = getRepository(UserKeypair);
+export const UserPublickeys = getRepository(UserPublickey);
+export const UserLists = getCustomRepository(UserListRepository);
+export const UserListJoinings = getRepository(UserListJoining);
+export const UserNotePinings = getRepository(UserNotePining);
+export const UserServiceLinkings = getRepository(UserServiceLinking);
+export const Followings = getCustomRepository(FollowingRepository);
+export const FollowRequests = getCustomRepository(FollowRequestRepository);
+export const Instances = getRepository(Instance);
+export const Emojis = getRepository(Emoji);
+export const DriveFiles = getCustomRepository(DriveFileRepository);
+export const DriveFolders = getCustomRepository(DriveFolderRepository);
+export const Notifications = getCustomRepository(NotificationRepository);
+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 AbuseUserReports = getCustomRepository(AbuseUserReportRepository);
+export const RegistrationTickets = getRepository(RegistrationTicket);
+export const AuthSessions = getCustomRepository(AuthSessionRepository);
+export const AccessTokens = getRepository(AccessToken);
+export const Signins = getCustomRepository(SigninRepository);
+export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
+export const ReversiGames = getCustomRepository(ReversiGameRepository);
+export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
+export const Logs = getRepository(Log);
diff --git a/src/models/instance.ts b/src/models/instance.ts
deleted file mode 100644
index cdce570a4b..0000000000
--- a/src/models/instance.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Instance = db.get<IInstance>('instances');
-Instance.createIndex('host', { unique: true });
-export default Instance;
-
-export interface IInstance {
- _id: mongo.ObjectID;
-
- /**
- * ホスト
- */
- host: string;
-
- /**
- * このインスタンスを捕捉した日時
- */
- caughtAt: Date;
-
- /**
- * このインスタンスのシステム (MastodonとかMisskeyとかPleromaとか)
- */
- system: string;
-
- /**
- * このインスタンスのユーザー数
- */
- usersCount: number;
-
- /**
- * このインスタンスから受け取った投稿数
- */
- notesCount: number;
-
- /**
- * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
- */
- followingCount: number;
-
- /**
- * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
- */
- followersCount: number;
-
- /**
- * ドライブ使用量
- */
- driveUsage: number;
-
- /**
- * ドライブのファイル数
- */
- driveFiles: number;
-
- /**
- * 直近のリクエスト送信日時
- */
- latestRequestSentAt?: Date;
-
- /**
- * 直近のリクエスト送信時のHTTPステータスコード
- */
- latestStatus?: number;
-
- /**
- * 直近のリクエスト受信日時
- */
- latestRequestReceivedAt?: Date;
-
- /**
- * このインスタンスと不通かどうか
- */
- isNotResponding: boolean;
-
- /**
- * このインスタンスと最後にやり取りした日時
- */
- lastCommunicatedAt: Date;
-
- /**
- * このインスタンスをブロックしているか
- */
- isBlocked: boolean;
-
- /**
- * このインスタンスが閉鎖済みとしてマークされているか
- */
- isMarkedAsClosed: boolean;
-}
diff --git a/src/models/log.ts b/src/models/log.ts
deleted file mode 100644
index 6f79e83c78..0000000000
--- a/src/models/log.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Log = db.get<ILog>('logs');
-Log.createIndex('createdAt', { expireAfterSeconds: 3600 * 24 * 3 });
-Log.createIndex('level');
-Log.createIndex('domain');
-export default Log;
-
-export interface ILog {
- _id: mongo.ObjectID;
- createdAt: Date;
- machine: string;
- worker: string;
- domain: string[];
- level: string;
- message: string;
- data: any;
-}
diff --git a/src/models/messaging-message.ts b/src/models/messaging-message.ts
deleted file mode 100644
index 67abb4d111..0000000000
--- a/src/models/messaging-message.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import { pack as packUser } from './user';
-import { pack as packFile } from './drive-file';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { length } from 'stringz';
-
-const MessagingMessage = db.get<IMessagingMessage>('messagingMessages');
-MessagingMessage.createIndex('userId');
-MessagingMessage.createIndex('recipientId');
-export default MessagingMessage;
-
-export interface IMessagingMessage {
- _id: mongo.ObjectID;
- createdAt: Date;
- text: string;
- userId: mongo.ObjectID;
- recipientId: mongo.ObjectID;
- isRead: boolean;
- fileId: mongo.ObjectID;
-}
-
-export function isValidText(text: string): boolean {
- return length(text.trim()) <= 1000 && text.trim() != '';
-}
-
-/**
- * Pack a messaging message for API response
- */
-export const pack = (
- message: any,
- me?: any,
- options?: {
- populateRecipient: boolean
- }
-) => new Promise<any>(async (resolve, reject) => {
- const opts = options || {
- populateRecipient: true
- };
-
- let _message: any;
-
- // Populate the message if 'message' is ID
- if (isObjectId(message)) {
- _message = await MessagingMessage.findOne({
- _id: message
- });
- } else if (typeof message === 'string') {
- _message = await MessagingMessage.findOne({
- _id: new mongo.ObjectID(message)
- });
- } else {
- _message = deepcopy(message);
- }
-
- // Rename _id to id
- _message.id = _message._id;
- delete _message._id;
-
- // Populate user
- _message.user = await packUser(_message.userId, me);
-
- if (_message.fileId) {
- // Populate file
- _message.file = await packFile(_message.fileId);
- }
-
- if (opts.populateRecipient) {
- // Populate recipient
- _message.recipient = await packUser(_message.recipientId, me);
- }
-
- resolve(_message);
-});
diff --git a/src/models/meta.ts b/src/models/meta.ts
deleted file mode 100644
index 5ca0f01236..0000000000
--- a/src/models/meta.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-import db from '../db/mongodb';
-import config from '../config';
-import User from './user';
-import { transform } from '../misc/cafy-id';
-
-const Meta = db.get<IMeta>('meta');
-export default Meta;
-
-// 後方互換性のため。
-// 過去のMisskeyではインスタンス名や紹介を設定ファイルに記述していたのでそれを移行
-if ((config as any).name) {
- Meta.findOne({}).then(m => {
- if (m != null && m.name == null) {
- Meta.update({}, {
- $set: {
- name: (config as any).name
- }
- });
- }
- });
-}
-if ((config as any).description) {
- Meta.findOne({}).then(m => {
- if (m != null && m.description == null) {
- Meta.update({}, {
- $set: {
- description: (config as any).description
- }
- });
- }
- });
-}
-if ((config as any).localDriveCapacityMb) {
- Meta.findOne({}).then(m => {
- if (m != null && m.localDriveCapacityMb == null) {
- Meta.update({}, {
- $set: {
- localDriveCapacityMb: (config as any).localDriveCapacityMb
- }
- });
- }
- });
-}
-if ((config as any).remoteDriveCapacityMb) {
- Meta.findOne({}).then(m => {
- if (m != null && m.remoteDriveCapacityMb == null) {
- Meta.update({}, {
- $set: {
- remoteDriveCapacityMb: (config as any).remoteDriveCapacityMb
- }
- });
- }
- });
-}
-if ((config as any).preventCacheRemoteFiles) {
- Meta.findOne({}).then(m => {
- if (m != null && m.cacheRemoteFiles == null) {
- Meta.update({}, {
- $set: {
- cacheRemoteFiles: !(config as any).preventCacheRemoteFiles
- }
- });
- }
- });
-}
-if ((config as any).recaptcha) {
- Meta.findOne({}).then(m => {
- if (m != null && m.enableRecaptcha == null) {
- Meta.update({}, {
- $set: {
- enableRecaptcha: (config as any).recaptcha != null,
- recaptchaSiteKey: (config as any).recaptcha.site_key,
- recaptchaSecretKey: (config as any).recaptcha.secret_key,
- }
- });
- }
- });
-}
-if ((config as any).ghost) {
- Meta.findOne({}).then(async m => {
- if (m != null && m.proxyAccount == null) {
- const account = await User.findOne({ _id: transform((config as any).ghost) });
- Meta.update({}, {
- $set: {
- proxyAccount: account.username
- }
- });
- }
- });
-}
-if ((config as any).maintainer) {
- Meta.findOne({}).then(m => {
- if (m != null && m.maintainer == null) {
- Meta.update({}, {
- $set: {
- maintainer: (config as any).maintainer
- }
- });
- }
- });
-}
-if ((config as any).twitter) {
- Meta.findOne({}).then(m => {
- if (m != null && m.enableTwitterIntegration == null) {
- Meta.update({}, {
- $set: {
- enableTwitterIntegration: true,
- twitterConsumerKey: (config as any).twitter.consumer_key,
- twitterConsumerSecret: (config as any).twitter.consumer_secret
- }
- });
- }
- });
-}
-if ((config as any).github) {
- Meta.findOne({}).then(m => {
- if (m != null && m.enableGithubIntegration == null) {
- Meta.update({}, {
- $set: {
- enableGithubIntegration: true,
- githubClientId: (config as any).github.client_id,
- githubClientSecret: (config as any).github.client_secret
- }
- });
- }
- });
-}
-if ((config as any).user_recommendation) {
- Meta.findOne({}).then(m => {
- if (m != null && m.enableExternalUserRecommendation == null) {
- Meta.update({}, {
- $set: {
- enableExternalUserRecommendation: true,
- externalUserRecommendationEngine: (config as any).user_recommendation.engine,
- externalUserRecommendationTimeout: (config as any).user_recommendation.timeout
- }
- });
- }
- });
-}
-if ((config as any).sw) {
- Meta.findOne({}).then(m => {
- if (m != null && m.enableServiceWorker == null) {
- Meta.update({}, {
- $set: {
- enableServiceWorker: true,
- swPublicKey: (config as any).sw.public_key,
- swPrivateKey: (config as any).sw.private_key
- }
- });
- }
- });
-}
-Meta.findOne({}).then(m => {
- if (m != null && (m as any).broadcasts != null) {
- Meta.update({}, {
- $rename: {
- broadcasts: 'announcements'
- }
- });
- }
-});
-
-export type IMeta = {
- name?: string;
- description?: string;
-
- /**
- * メンテナ情報
- */
- maintainer: {
- /**
- * メンテナの名前
- */
- name: string;
-
- /**
- * メンテナの連絡先
- */
- email?: string;
- };
-
- langs?: string[];
-
- announcements?: any[];
-
- stats?: {
- notesCount: number;
- originalNotesCount: number;
- usersCount: number;
- originalUsersCount: number;
- };
-
- disableRegistration?: boolean;
- disableLocalTimeline?: boolean;
- disableGlobalTimeline?: boolean;
- enableEmojiReaction?: boolean;
- useStarForReactionFallback?: boolean;
- hidedTags?: string[];
- mascotImageUrl?: string;
- bannerUrl?: string;
- errorImageUrl?: string;
- iconUrl?: string;
-
- cacheRemoteFiles?: boolean;
-
- proxyAccount?: string;
-
- enableRecaptcha?: boolean;
- recaptchaSiteKey?: string;
- recaptchaSecretKey?: string;
-
- /**
- * Drive capacity of a local user (MB)
- */
- localDriveCapacityMb?: number;
-
- /**
- * Drive capacity of a remote user (MB)
- */
- remoteDriveCapacityMb?: number;
-
- /**
- * Max allowed note text length in characters
- */
- maxNoteTextLength?: number;
-
- summalyProxy?: string;
-
- enableTwitterIntegration?: boolean;
- twitterConsumerKey?: string;
- twitterConsumerSecret?: string;
-
- enableGithubIntegration?: boolean;
- githubClientId?: string;
- githubClientSecret?: string;
-
- enableDiscordIntegration?: boolean;
- discordClientId?: string;
- discordClientSecret?: string;
-
- enableExternalUserRecommendation?: boolean;
- externalUserRecommendationEngine?: string;
- externalUserRecommendationTimeout?: number;
-
- enableEmail?: boolean;
- email?: string;
- smtpSecure?: boolean;
- smtpHost?: string;
- smtpPort?: number;
- smtpUser?: string;
- smtpPass?: string;
-
- enableServiceWorker?: boolean;
- swPublicKey?: string;
- swPrivateKey?: string;
-};
diff --git a/src/models/mute.ts b/src/models/mute.ts
deleted file mode 100644
index 52775e13ca..0000000000
--- a/src/models/mute.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import * as deepcopy from 'deepcopy';
-import { pack as packUser, IUser } from './user';
-
-const Mute = db.get<IMute>('mute');
-Mute.createIndex('muterId');
-Mute.createIndex('muteeId');
-Mute.createIndex(['muterId', 'muteeId'], { unique: true });
-export default Mute;
-
-export interface IMute {
- _id: mongo.ObjectID;
- createdAt: Date;
- muterId: mongo.ObjectID;
- muteeId: mongo.ObjectID;
-}
-
-export const packMany = (
- mutes: (string | mongo.ObjectID | IMute)[],
- me?: string | mongo.ObjectID | IUser
-) => {
- return Promise.all(mutes.map(x => pack(x, me)));
-};
-
-export const pack = (
- mute: any,
- me?: any
-) => new Promise<any>(async (resolve, reject) => {
- let _mute: any;
-
- // Populate the mute if 'mute' is ID
- if (isObjectId(mute)) {
- _mute = await Mute.findOne({
- _id: mute
- });
- } else if (typeof mute === 'string') {
- _mute = await Mute.findOne({
- _id: new mongo.ObjectID(mute)
- });
- } else {
- _mute = deepcopy(mute);
- }
-
- // Rename _id to id
- _mute.id = _mute._id;
- delete _mute._id;
-
- // Populate mutee
- _mute.mutee = await packUser(_mute.muteeId, me, {
- detail: true
- });
-
- resolve(_mute);
-});
diff --git a/src/models/note-reaction.ts b/src/models/note-reaction.ts
deleted file mode 100644
index 89b7529350..0000000000
--- a/src/models/note-reaction.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packUser } from './user';
-
-const NoteReaction = db.get<INoteReaction>('noteReactions');
-NoteReaction.createIndex('noteId');
-NoteReaction.createIndex('userId');
-NoteReaction.createIndex(['userId', 'noteId'], { unique: true });
-export default NoteReaction;
-
-export interface INoteReaction {
- _id: mongo.ObjectID;
- createdAt: Date;
- noteId: mongo.ObjectID;
- userId: mongo.ObjectID;
- reaction: string;
-}
-
-/**
- * Pack a reaction for API response
- */
-export const pack = (
- reaction: any,
- me?: any
-) => new Promise<any>(async (resolve, reject) => {
- let _reaction: any;
-
- // Populate the reaction if 'reaction' is ID
- if (isObjectId(reaction)) {
- _reaction = await NoteReaction.findOne({
- _id: reaction
- });
- } else if (typeof reaction === 'string') {
- _reaction = await NoteReaction.findOne({
- _id: new mongo.ObjectID(reaction)
- });
- } else {
- _reaction = deepcopy(reaction);
- }
-
- // Rename _id to id
- _reaction.id = _reaction._id;
- delete _reaction._id;
-
- // Populate user
- _reaction.user = await packUser(_reaction.userId, me);
-
- resolve(_reaction);
-});
diff --git a/src/models/note-unread.ts b/src/models/note-unread.ts
deleted file mode 100644
index dd08640d85..0000000000
--- a/src/models/note-unread.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const NoteUnread = db.get<INoteUnread>('noteUnreads');
-NoteUnread.createIndex('userId');
-NoteUnread.createIndex('noteId');
-NoteUnread.createIndex(['userId', 'noteId'], { unique: true });
-export default NoteUnread;
-
-export interface INoteUnread {
- _id: mongo.ObjectID;
- noteId: mongo.ObjectID;
- userId: mongo.ObjectID;
- isSpecified: boolean;
-
- _note: {
- userId: mongo.ObjectID;
- };
-}
diff --git a/src/models/note-watching.ts b/src/models/note-watching.ts
deleted file mode 100644
index 83aaf8ad06..0000000000
--- a/src/models/note-watching.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const NoteWatching = db.get<INoteWatching>('noteWatching');
-NoteWatching.createIndex('userId');
-NoteWatching.createIndex('noteId');
-NoteWatching.createIndex(['userId', 'noteId'], { unique: true });
-export default NoteWatching;
-
-export interface INoteWatching {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID;
- noteId: mongo.ObjectID;
-}
diff --git a/src/models/note.ts b/src/models/note.ts
deleted file mode 100644
index 8c71c1940c..0000000000
--- a/src/models/note.ts
+++ /dev/null
@@ -1,418 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import rap from '@prezzemolo/rap';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { length } from 'stringz';
-import { IUser, pack as packUser } from './user';
-import { pack as packApp } from './app';
-import PollVote from './poll-vote';
-import NoteReaction from './note-reaction';
-import { packMany as packFileMany, IDriveFile } from './drive-file';
-import Following from './following';
-import Emoji from './emoji';
-import { dbLogger } from '../db/logger';
-import { unique, concat } from '../prelude/array';
-
-const Note = db.get<INote>('notes');
-Note.createIndex('uri', { sparse: true, unique: true });
-Note.createIndex('userId');
-Note.createIndex('mentions');
-Note.createIndex('visibleUserIds');
-Note.createIndex('replyId');
-Note.createIndex('renoteId');
-Note.createIndex('tagsLower');
-Note.createIndex('_user.host');
-Note.createIndex('_files._id');
-Note.createIndex('_files.contentType');
-Note.createIndex({ createdAt: -1 });
-Note.createIndex({ score: -1 }, { sparse: true });
-export default Note;
-
-export function isValidCw(text: string): boolean {
- return length(text.trim()) <= 100;
-}
-
-export type INote = {
- _id: mongo.ObjectID;
- createdAt: Date;
- deletedAt: Date;
- updatedAt?: Date;
- fileIds: mongo.ObjectID[];
- replyId: mongo.ObjectID;
- renoteId: mongo.ObjectID;
- poll: IPoll;
- name?: string;
- text: string;
- tags: string[];
- tagsLower: string[];
- emojis: string[];
- cw: string;
- userId: mongo.ObjectID;
- appId: mongo.ObjectID;
- viaMobile: boolean;
- localOnly: boolean;
- renoteCount: number;
- repliesCount: number;
- reactionCounts: Record<string, number>;
- mentions: mongo.ObjectID[];
- mentionedRemoteUsers: {
- uri: string;
- username: string;
- host: string;
- }[];
-
- /**
- * public ... 公開
- * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
- * followers ... フォロワーのみ
- * specified ... visibleUserIds で指定したユーザーのみ
- */
- visibility: 'public' | 'home' | 'followers' | 'specified';
-
- visibleUserIds: mongo.ObjectID[];
-
- geo: {
- coordinates: number[];
- altitude: number;
- accuracy: number;
- altitudeAccuracy: number;
- heading: number;
- speed: number;
- };
-
- uri: string;
-
- /**
- * 人気の投稿度合いを表すスコア
- */
- score: number;
-
- // 非正規化
- _reply?: {
- userId: mongo.ObjectID;
- };
- _renote?: {
- userId: mongo.ObjectID;
- };
- _user: {
- host: string;
- inbox?: string;
- };
- _files?: IDriveFile[];
-};
-
-export type IPoll = {
- choices: IChoice[];
- multiple?: boolean;
- expiresAt?: Date;
-};
-
-export type IChoice = {
- id: number;
- text: string;
- votes: number;
-};
-
-export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
- let hide = false;
-
- // visibility が private かつ投稿者のIDが自分のIDではなかったら非表示(後方互換性のため)
- if (packedNote.visibility == 'private' && (meId == null || !meId.equals(packedNote.userId))) {
- hide = true;
- }
-
- // visibility が specified かつ自分が指定されていなかったら非表示
- if (packedNote.visibility == 'specified') {
- if (meId == null) {
- hide = true;
- } else if (meId.equals(packedNote.userId)) {
- hide = false;
- } else {
- // 指定されているかどうか
- const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id));
-
- if (specified) {
- hide = false;
- } else {
- hide = true;
- }
- }
- }
-
- // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
- if (packedNote.visibility == 'followers') {
- if (meId == null) {
- hide = true;
- } else if (meId.equals(packedNote.userId)) {
- hide = false;
- } else if (packedNote.reply && meId.equals(packedNote.reply.userId)) {
- // 自分の投稿に対するリプライ
- hide = false;
- } else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId.equals(id))) {
- // 自分へのメンション
- hide = false;
- } else {
- // フォロワーかどうか
- const following = await Following.findOne({
- followeeId: packedNote.userId,
- followerId: meId
- });
-
- if (following == null) {
- hide = true;
- } else {
- hide = false;
- }
- }
- }
-
- if (hide) {
- packedNote.fileIds = [];
- packedNote.files = [];
- packedNote.text = null;
- packedNote.poll = null;
- packedNote.cw = null;
- packedNote.tags = [];
- packedNote.geo = null;
- packedNote.isHidden = true;
- }
-};
-
-export const packMany = (
- notes: (string | mongo.ObjectID | INote)[],
- me?: string | mongo.ObjectID | IUser,
- options?: {
- detail?: boolean;
- skipHide?: boolean;
- }
-) => {
- return Promise.all(notes.map(n => pack(n, me, options)));
-};
-
-/**
- * Pack a note for API response
- *
- * @param note target
- * @param me? serializee
- * @param options? serialize options
- * @return response
- */
-export const pack = async (
- note: string | mongo.ObjectID | INote,
- me?: string | mongo.ObjectID | IUser,
- options?: {
- detail?: boolean;
- skipHide?: boolean;
- }
-) => {
- const opts = Object.assign({
- detail: true,
- skipHide: false
- }, options);
-
- // Me
- const meId: mongo.ObjectID = me
- ? isObjectId(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- let _note: any;
-
- // Populate the note if 'note' is ID
- if (isObjectId(note)) {
- _note = await Note.findOne({
- _id: note
- });
- } else if (typeof note === 'string') {
- _note = await Note.findOne({
- _id: new mongo.ObjectID(note)
- });
- } else {
- _note = deepcopy(note);
- }
-
- // (データベースの欠損などで)投稿がデータベース上に見つからなかったとき
- if (_note == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: note :: ${note}`);
- return null;
- }
-
- const id = _note._id;
-
- // Some counts
- _note.renoteCount = _note.renoteCount || 0;
- _note.repliesCount = _note.repliesCount || 0;
- _note.reactionCounts = _note.reactionCounts || {};
-
- // _note._userを消す前か、_note.userを解決した後でないとホストがわからない
- if (_note._user) {
- const host = _note._user.host;
- // 互換性のため。(古いMisskeyではNoteにemojisが無い)
- if (_note.emojis == null) {
- _note.emojis = Emoji.find({
- host: host
- }, {
- fields: { _id: false }
- });
- } else {
- _note.emojis = unique(concat([_note.emojis, Object.keys(_note.reactionCounts).map(x => x.replace(/:/g, ''))]));
-
- _note.emojis = Emoji.find({
- name: { $in: _note.emojis },
- host: host
- }, {
- fields: { _id: false }
- });
- }
- }
-
- // Rename _id to id
- _note.id = _note._id;
- delete _note._id;
-
- delete _note.prev;
- delete _note.next;
- delete _note.tagsLower;
- delete _note.score;
- delete _note._user;
- delete _note._reply;
- delete _note._renote;
- delete _note._files;
- delete _note._replyIds;
- delete _note.mentionedRemoteUsers;
-
- if (_note.geo) delete _note.geo.type;
-
- // Populate user
- _note.user = packUser(_note.userId, meId);
-
- // Populate app
- if (_note.appId) {
- _note.app = packApp(_note.appId);
- }
-
- // Populate files
- _note.files = packFileMany(_note.fileIds || []);
-
- // 後方互換性のため
- _note.mediaIds = _note.fileIds;
- _note.media = _note.files;
-
- // When requested a detailed note data
- if (opts.detail) {
- if (_note.replyId) {
- // Populate reply to note
- _note.reply = pack(_note.replyId, meId, {
- detail: false
- });
- }
-
- if (_note.renoteId) {
- // Populate renote
- _note.renote = pack(_note.renoteId, meId, {
- detail: _note.text == null
- });
- }
-
- // Poll
- if (meId && _note.poll) {
- _note.poll = (async poll => {
- if (poll.multiple) {
- const votes = await PollVote.find({
- userId: meId,
- noteId: id
- });
-
- const myChoices = (poll.choices as IChoice[]).filter(x => votes.some(y => x.id == y.choice));
- for (const myChoice of myChoices) {
- (myChoice as any).isVoted = true;
- }
-
- return poll;
- } else {
- poll.multiple = false;
- }
-
- const vote = await PollVote
- .findOne({
- userId: meId,
- noteId: id
- });
-
- if (vote) {
- const myChoice = (poll.choices as IChoice[])
- .filter(x => x.id == vote.choice)[0] as any;
-
- myChoice.isVoted = true;
- }
-
- return poll;
- })(_note.poll);
- }
-
- if (meId) {
- // Fetch my reaction
- _note.myReaction = (async () => {
- const reaction = await NoteReaction
- .findOne({
- userId: meId,
- noteId: id,
- deletedAt: { $exists: false }
- });
-
- if (reaction) {
- return reaction.reaction;
- }
-
- return null;
- })();
- }
- }
-
- // resolve promises in _note object
- _note = await rap(_note);
-
- //#region (データベースの欠損などで)参照しているデータがデータベース上に見つからなかったとき
- if (_note.user == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> user :: ${_note.id} (user ${_note.userId})`);
- return null;
- }
-
- if (opts.detail) {
- if (_note.replyId != null && _note.reply == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> reply :: ${_note.id} (reply ${_note.replyId})`);
- return null;
- }
-
- if (_note.renoteId != null && _note.renote == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> renote :: ${_note.id} (renote ${_note.renoteId})`);
- return null;
- }
- }
- //#endregion
-
- if (_note.name) {
- _note.text = `【${_note.name}】\n${_note.text}`;
- }
-
- if (_note.user.isCat && _note.text) {
- _note.text = (_note.text
- // ja-JP
- .replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
- // ko-KR
- .replace(/[나-낳]/g, (match: string) => String.fromCharCode(
- match.codePointAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
- ))
- );
- }
-
- if (!opts.skipHide) {
- await hideNote(_note, meId);
- }
-
- return _note;
-};
diff --git a/src/models/notification.ts b/src/models/notification.ts
deleted file mode 100644
index 75456af57b..0000000000
--- a/src/models/notification.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { IUser, pack as packUser } from './user';
-import { pack as packNote } from './note';
-import { dbLogger } from '../db/logger';
-
-const Notification = db.get<INotification>('notifications');
-Notification.createIndex('notifieeId');
-export default Notification;
-
-export interface INotification {
- _id: mongo.ObjectID;
- createdAt: Date;
-
- /**
- * 通知の受信者
- */
- notifiee?: IUser;
-
- /**
- * 通知の受信者
- */
- notifieeId: mongo.ObjectID;
-
- /**
- * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
- */
- notifier?: IUser;
-
- /**
- * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
- */
- notifierId: mongo.ObjectID;
-
- /**
- * 通知の種類。
- * follow - フォローされた
- * mention - 投稿で自分が言及された
- * reply - (自分または自分がWatchしている)投稿が返信された
- * renote - (自分または自分がWatchしている)投稿がRenoteされた
- * quote - (自分または自分がWatchしている)投稿が引用Renoteされた
- * reaction - (自分または自分がWatchしている)投稿にリアクションされた
- * poll_vote - (自分または自分がWatchしている)投稿の投票に投票された
- */
- type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'poll_vote';
-
- /**
- * 通知が読まれたかどうか
- */
- isRead: boolean;
-}
-
-export const packMany = (
- notifications: any[]
-) => {
- return Promise.all(notifications.map(n => pack(n)));
-};
-
-/**
- * Pack a notification for API response
- */
-export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => {
- let _notification: any;
-
- // Populate the notification if 'notification' is ID
- if (isObjectId(notification)) {
- _notification = await Notification.findOne({
- _id: notification
- });
- } else if (typeof notification === 'string') {
- _notification = await Notification.findOne({
- _id: new mongo.ObjectID(notification)
- });
- } else {
- _notification = deepcopy(notification);
- }
-
- // Rename _id to id
- _notification.id = _notification._id;
- delete _notification._id;
-
- // Rename notifierId to userId
- _notification.userId = _notification.notifierId;
- delete _notification.notifierId;
-
- const me = _notification.notifieeId;
- delete _notification.notifieeId;
-
- // Populate notifier
- _notification.user = await packUser(_notification.userId, me);
-
- switch (_notification.type) {
- case 'follow':
- case 'receiveFollowRequest':
- // nope
- break;
- case 'mention':
- case 'reply':
- case 'renote':
- case 'quote':
- case 'reaction':
- case 'poll_vote':
- // Populate note
- _notification.note = await packNote(_notification.noteId, me);
-
- // (データベースの不具合などで)投稿が見つからなかったら
- if (_notification.note == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: notification -> note :: ${_notification.id} (note ${_notification.noteId})`);
- return resolve(null);
- }
- break;
- default:
- dbLogger.error(`Unknown type: ${_notification.type}`);
- break;
- }
-
- resolve(_notification);
-});
diff --git a/src/models/poll-vote.ts b/src/models/poll-vote.ts
deleted file mode 100644
index e6178cbc26..0000000000
--- a/src/models/poll-vote.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const PollVote = db.get<IPollVote>('pollVotes');
-PollVote.dropIndex(['userId', 'noteId'], { unique: true }).catch(() => {});
-PollVote.createIndex('userId');
-PollVote.createIndex('noteId');
-PollVote.createIndex(['userId', 'noteId', 'choice'], { unique: true });
-export default PollVote;
-
-export interface IPollVote {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID;
- noteId: mongo.ObjectID;
- choice: number;
-}
diff --git a/src/models/registration-tickets.ts b/src/models/registration-tickets.ts
deleted file mode 100644
index 846acefedf..0000000000
--- a/src/models/registration-tickets.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const RegistrationTicket = db.get<IRegistrationTicket>('registrationTickets');
-RegistrationTicket.createIndex('code', { unique: true });
-export default RegistrationTicket;
-
-export interface IRegistrationTicket {
- _id: mongo.ObjectID;
- createdAt: Date;
- code: string;
-}
diff --git a/src/models/repositories/abuse-user-report.ts b/src/models/repositories/abuse-user-report.ts
new file mode 100644
index 0000000000..c72a582c04
--- /dev/null
+++ b/src/models/repositories/abuse-user-report.ts
@@ -0,0 +1,32 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { AbuseUserReport } from '../entities/abuse-user-report';
+
+@EntityRepository(AbuseUserReport)
+export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
+ public packMany(
+ reports: any[],
+ ) {
+ return Promise.all(reports.map(x => this.pack(x)));
+ }
+
+ public async pack(
+ src: AbuseUserReport['id'] | AbuseUserReport,
+ ) {
+ const report = typeof src === 'object' ? src : await this.findOne(src);
+
+ return await rap({
+ id: report.id,
+ createdAt: report.createdAt,
+ reporterId: report.reporterId,
+ userId: report.userId,
+ reporter: Users.pack(report.reporter || report.reporterId, null, {
+ detail: true
+ }),
+ user: Users.pack(report.user || report.userId, null, {
+ detail: true
+ }),
+ });
+ }
+}
diff --git a/src/models/repositories/app.ts b/src/models/repositories/app.ts
new file mode 100644
index 0000000000..2e3323baf8
--- /dev/null
+++ b/src/models/repositories/app.ts
@@ -0,0 +1,36 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { App } from '../entities/app';
+import { AccessTokens } from '..';
+
+@EntityRepository(App)
+export class AppRepository extends Repository<App> {
+ public async pack(
+ src: App['id'] | App,
+ me?: any,
+ options?: {
+ detail?: boolean,
+ includeSecret?: boolean,
+ includeProfileImageIds?: boolean
+ }
+ ) {
+ const opts = Object.assign({
+ detail: false,
+ includeSecret: false,
+ includeProfileImageIds: false
+ }, options);
+
+ const app = typeof src === 'object' ? src : await this.findOne(src);
+
+ return {
+ id: app.id,
+ name: app.name,
+ ...(opts.includeSecret ? { secret: app.secret } : {}),
+ ...(me ? {
+ isAuthorized: await AccessTokens.count({
+ appId: app.id,
+ userId: me,
+ }).then(count => count > 0)
+ } : {})
+ };
+ }
+}
diff --git a/src/models/repositories/auth-session.ts b/src/models/repositories/auth-session.ts
new file mode 100644
index 0000000000..76e3ddf9ab
--- /dev/null
+++ b/src/models/repositories/auth-session.ts
@@ -0,0 +1,19 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Apps } from '..';
+import rap from '@prezzemolo/rap';
+import { AuthSession } from '../entities/auth-session';
+
+@EntityRepository(AuthSession)
+export class AuthSessionRepository extends Repository<AuthSession> {
+ public async pack(
+ src: AuthSession['id'] | AuthSession,
+ me?: any
+ ) {
+ const session = typeof src === 'object' ? src : await this.findOne(src);
+
+ return await rap({
+ id: session.id,
+ app: Apps.pack(session.appId, me)
+ });
+ }
+}
diff --git a/src/models/repositories/blocking.ts b/src/models/repositories/blocking.ts
new file mode 100644
index 0000000000..81f3866131
--- /dev/null
+++ b/src/models/repositories/blocking.ts
@@ -0,0 +1,28 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { Blocking } from '../entities/blocking';
+
+@EntityRepository(Blocking)
+export class BlockingRepository extends Repository<Blocking> {
+ 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
+ ) {
+ const blocking = typeof src === 'object' ? src : await this.findOne(src);
+
+ return await rap({
+ id: blocking.id,
+ blockee: Users.pack(blocking.blockeeId, me, {
+ detail: true
+ })
+ });
+ }
+}
diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts
new file mode 100644
index 0000000000..fe0ca72bfb
--- /dev/null
+++ b/src/models/repositories/drive-file.ts
@@ -0,0 +1,113 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { DriveFile } from '../entities/drive-file';
+import { Users, DriveFolders } from '..';
+import rap from '@prezzemolo/rap';
+import { User } from '../entities/user';
+
+@EntityRepository(DriveFile)
+export class DriveFileRepository extends Repository<DriveFile> {
+ public validateFileName(name: string): boolean {
+ return (
+ (name.trim().length > 0) &&
+ (name.length <= 200) &&
+ (name.indexOf('\\') === -1) &&
+ (name.indexOf('/') === -1) &&
+ (name.indexOf('..') === -1)
+ );
+ }
+
+ public getPublicUrl(file: DriveFile, thumbnail = false): string {
+ if (thumbnail) {
+ return file.thumbnailUrl || file.webpublicUrl || file.url;
+ } else {
+ return file.webpublicUrl || file.thumbnailUrl || file.url;
+ }
+ }
+
+ public async clacDriveUsageOf(user: User['id'] | User): Promise<number> {
+ const id = typeof user === 'object' ? user.id : user;
+
+ const { sum } = await this
+ .createQueryBuilder('file')
+ .where('file.userId = :id', { id: id })
+ .select('SUM(file.size)', 'sum')
+ .getRawOne();
+
+ return parseInt(sum, 10) || 0;
+ }
+
+ public async clacDriveUsageOfHost(host: string): Promise<number> {
+ const { sum } = await this
+ .createQueryBuilder('file')
+ .where('file.userHost = :host', { host: host })
+ .select('SUM(file.size)', 'sum')
+ .getRawOne();
+
+ return parseInt(sum, 10) || 0;
+ }
+
+ public async clacDriveUsageOfLocal(): Promise<number> {
+ const { sum } = await this
+ .createQueryBuilder('file')
+ .where('file.userHost IS NULL')
+ .select('SUM(file.size)', 'sum')
+ .getRawOne();
+
+ return parseInt(sum, 10) || 0;
+ }
+
+ public async clacDriveUsageOfRemote(): Promise<number> {
+ const { sum } = await this
+ .createQueryBuilder('file')
+ .where('file.userHost IS NOT NULL')
+ .select('SUM(file.size)', 'sum')
+ .getRawOne();
+
+ 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?: {
+ detail?: boolean,
+ self?: boolean,
+ withUser?: boolean,
+ }
+ ) {
+ const opts = Object.assign({
+ detail: false,
+ self: false
+ }, options);
+
+ const file = typeof src === 'object' ? src : await this.findOne(src);
+
+ return await rap({
+ id: file.id,
+ createdAt: file.createdAt,
+ name: file.name,
+ type: file.type,
+ md5: file.md5,
+ size: file.size,
+ isSensitive: file.isSensitive,
+ properties: file.properties,
+ url: opts.self ? file.url : this.getPublicUrl(file, false),
+ thumbnailUrl: this.getPublicUrl(file, true),
+ folderId: file.folderId,
+ folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
+ detail: true
+ }) : null,
+ user: opts.withUser ? Users.pack(file.userId) : null
+ });
+ }
+}
diff --git a/src/models/repositories/drive-folder.ts b/src/models/repositories/drive-folder.ts
new file mode 100644
index 0000000000..faf0f353aa
--- /dev/null
+++ b/src/models/repositories/drive-folder.ts
@@ -0,0 +1,49 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { DriveFolders, DriveFiles } from '..';
+import rap from '@prezzemolo/rap';
+import { DriveFolder } from '../entities/drive-folder';
+
+@EntityRepository(DriveFolder)
+export class DriveFolderRepository extends Repository<DriveFolder> {
+ public validateFolderName(name: string): boolean {
+ return (
+ (name.trim().length > 0) &&
+ (name.length <= 200)
+ );
+ }
+
+ public async pack(
+ src: DriveFolder['id'] | DriveFolder,
+ options?: {
+ detail: boolean
+ }
+ ): Promise<Record<string, any>> {
+ const opts = Object.assign({
+ detail: false
+ }, options);
+
+ const folder = typeof src === 'object' ? src : await this.findOne(src);
+
+ return await rap({
+ id: folder.id,
+ createdAt: folder.createdAt,
+ name: folder.name,
+ parentId: folder.parentId,
+
+ ...(opts.detail ? {
+ foldersCount: DriveFolders.count({
+ parentId: folder.id
+ }),
+ filesCount: DriveFiles.count({
+ folderId: folder.id
+ }),
+
+ ...(folder.parentId ? {
+ parent: this.pack(folder.parentId, {
+ detail: true
+ })
+ } : {})
+ } : {})
+ });
+ }
+}
diff --git a/src/models/repositories/follow-request.ts b/src/models/repositories/follow-request.ts
new file mode 100644
index 0000000000..bead093b21
--- /dev/null
+++ b/src/models/repositories/follow-request.ts
@@ -0,0 +1,19 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { FollowRequest } from '../entities/follow-request';
+import { Users } from '..';
+
+@EntityRepository(FollowRequest)
+export class FollowRequestRepository extends Repository<FollowRequest> {
+ public async pack(
+ src: FollowRequest['id'] | FollowRequest,
+ me?: any
+ ) {
+ const request = typeof src === 'object' ? src : await this.findOne(src);
+
+ return {
+ id: request.id,
+ follower: await Users.pack(request.followerId, me),
+ followee: await Users.pack(request.followeeId, me),
+ };
+ }
+}
diff --git a/src/models/repositories/following.ts b/src/models/repositories/following.ts
new file mode 100644
index 0000000000..02253d272d
--- /dev/null
+++ b/src/models/repositories/following.ts
@@ -0,0 +1,44 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { Following } from '../entities/following';
+
+@EntityRepository(Following)
+export class FollowingRepository extends Repository<Following> {
+ 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,
+ opts?: {
+ populateFollowee?: boolean;
+ populateFollower?: boolean;
+ }
+ ) {
+ const following = typeof src === 'object' ? src : await this.findOne(src);
+
+ if (opts == null) opts = {};
+
+ return await rap({
+ id: following.id,
+ createdAt: following.createdAt,
+ followeeId: following.followeeId,
+ followerId: following.followerId,
+ followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, {
+ detail: true
+ }) : null,
+ follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, {
+ detail: true
+ }) : null,
+ });
+ }
+}
diff --git a/src/models/repositories/games/reversi/game.ts b/src/models/repositories/games/reversi/game.ts
new file mode 100644
index 0000000000..f0cb6ff905
--- /dev/null
+++ b/src/models/repositories/games/reversi/game.ts
@@ -0,0 +1,49 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '../../..';
+import { ReversiGame } from '../../../entities/games/reversi/game';
+
+@EntityRepository(ReversiGame)
+export class ReversiGameRepository extends Repository<ReversiGame> {
+ public async pack(
+ src: ReversiGame['id'] | ReversiGame,
+ me?: any,
+ options?: {
+ detail?: boolean
+ }
+ ) {
+ const opts = Object.assign({
+ detail: true
+ }, options);
+
+ const game = typeof src === 'object' ? src : await this.findOne(src);
+ const meId = me ? typeof me === 'string' ? me : me.id : null;
+
+ return {
+ id: game.id,
+ createdAt: game.createdAt,
+ startedAt: game.startedAt,
+ isStarted: game.isStarted,
+ isEnded: game.isEnded,
+ form1: game.form1,
+ form2: game.form2,
+ user1Accepted: game.user1Accepted,
+ user2Accepted: game.user2Accepted,
+ user1Id: game.user1Id,
+ user2Id: game.user2Id,
+ user1: await Users.pack(game.user1Id, meId),
+ user2: await Users.pack(game.user2Id, meId),
+ winnerId: game.winnerId,
+ winner: game.winnerId ? await Users.pack(game.winnerId, meId) : null,
+ surrendered: game.surrendered,
+ black: game.black,
+ bw: game.bw,
+ isLlotheo: game.isLlotheo,
+ canPutEverywhere: game.canPutEverywhere,
+ loopedBoard: game.loopedBoard,
+ ...(opts.detail ? {
+ logs: game.logs,
+ map: game.map,
+ } : {})
+ };
+ }
+}
diff --git a/src/models/repositories/games/reversi/matching.ts b/src/models/repositories/games/reversi/matching.ts
new file mode 100644
index 0000000000..3612ac5c47
--- /dev/null
+++ b/src/models/repositories/games/reversi/matching.ts
@@ -0,0 +1,27 @@
+import { EntityRepository, Repository } from 'typeorm';
+import rap from '@prezzemolo/rap';
+import { ReversiMatching } from '../../../entities/games/reversi/matching';
+import { Users } from '../../..';
+
+@EntityRepository(ReversiMatching)
+export class ReversiMatchingRepository extends Repository<ReversiMatching> {
+ public async pack(
+ src: ReversiMatching['id'] | ReversiMatching,
+ me: any
+ ) {
+ const matching = typeof src === 'object' ? src : await this.findOne(src);
+
+ return await rap({
+ id: matching.id,
+ createdAt: matching.createdAt,
+ parentId: matching.parentId,
+ parent: Users.pack(matching.parentId, me, {
+ detail: true
+ }),
+ childId: matching.childId,
+ child: Users.pack(matching.childId, me, {
+ detail: true
+ })
+ });
+ }
+}
diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts
new file mode 100644
index 0000000000..b87b30388a
--- /dev/null
+++ b/src/models/repositories/messaging-message.ts
@@ -0,0 +1,37 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { MessagingMessage } from '../entities/messaging-message';
+import { Users, DriveFiles } from '..';
+
+@EntityRepository(MessagingMessage)
+export class MessagingMessageRepository extends Repository<MessagingMessage> {
+ public isValidText(text: string): boolean {
+ return text.trim().length <= 1000 && text.trim() != '';
+ }
+
+ public async pack(
+ src: MessagingMessage['id'] | MessagingMessage,
+ me?: any,
+ options?: {
+ populateRecipient: boolean
+ }
+ ) {
+ const opts = options || {
+ populateRecipient: true
+ };
+
+ const message = typeof src === 'object' ? src : await this.findOne(src);
+
+ return {
+ id: message.id,
+ createdAt: message.createdAt,
+ text: message.text,
+ userId: message.userId,
+ user: await Users.pack(message.user || message.userId, me),
+ recipientId: message.recipientId,
+ recipient: opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : null,
+ fileId: message.fileId,
+ file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
+ isRead: message.isRead
+ };
+ }
+}
diff --git a/src/models/repositories/muting.ts b/src/models/repositories/muting.ts
new file mode 100644
index 0000000000..cd98cb4fec
--- /dev/null
+++ b/src/models/repositories/muting.ts
@@ -0,0 +1,28 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { Muting } from '../entities/muting';
+
+@EntityRepository(Muting)
+export class MutingRepository extends Repository<Muting> {
+ 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
+ ) {
+ const muting = typeof src === 'object' ? src : await this.findOne(src);
+
+ return await rap({
+ id: muting.id,
+ mutee: Users.pack(muting.muteeId, me, {
+ detail: true
+ })
+ });
+ }
+}
diff --git a/src/models/repositories/note-favorite.ts b/src/models/repositories/note-favorite.ts
new file mode 100644
index 0000000000..4526461e69
--- /dev/null
+++ b/src/models/repositories/note-favorite.ts
@@ -0,0 +1,25 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { NoteFavorite } from '../entities/note-favorite';
+import { Notes } from '..';
+
+@EntityRepository(NoteFavorite)
+export class NoteFavoriteRepository extends Repository<NoteFavorite> {
+ 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
+ ) {
+ const favorite = typeof src === 'object' ? src : await this.findOne(src);
+
+ return {
+ id: favorite.id,
+ note: await Notes.pack(favorite.note || favorite.noteId, me),
+ };
+ }
+}
diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts
new file mode 100644
index 0000000000..7189da8e20
--- /dev/null
+++ b/src/models/repositories/note-reaction.ts
@@ -0,0 +1,18 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { NoteReaction } from '../entities/note-reaction';
+import { Users } from '..';
+
+@EntityRepository(NoteReaction)
+export class NoteReactionRepository extends Repository<NoteReaction> {
+ public async pack(
+ src: NoteReaction['id'] | NoteReaction,
+ me?: any
+ ) {
+ const reaction = typeof src === 'object' ? src : await this.findOne(src);
+
+ return {
+ id: reaction.id,
+ user: await Users.pack(reaction.userId, me),
+ };
+ }
+}
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
new file mode 100644
index 0000000000..4df0135115
--- /dev/null
+++ b/src/models/repositories/note.ts
@@ -0,0 +1,210 @@
+import { EntityRepository, Repository, In } from 'typeorm';
+import { Note } from '../entities/note';
+import { User } from '../entities/user';
+import { unique, concat } from '../../prelude/array';
+import { nyaize } from '../../misc/nyaize';
+import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
+import rap from '@prezzemolo/rap';
+
+@EntityRepository(Note)
+export class NoteRepository extends Repository<Note> {
+ public validateCw(x: string) {
+ return x.trim().length <= 100;
+ }
+
+ private async hideNote(packedNote: any, meId: User['id']) {
+ let hide = false;
+
+ // visibility が specified かつ自分が指定されていなかったら非表示
+ if (packedNote.visibility == 'specified') {
+ if (meId == null) {
+ hide = true;
+ } else if (meId === packedNote.userId) {
+ hide = false;
+ } else {
+ // 指定されているかどうか
+ const specified = packedNote.visibleUserIds.some((id: any) => meId === id);
+
+ if (specified) {
+ hide = false;
+ } else {
+ hide = true;
+ }
+ }
+ }
+
+ // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
+ if (packedNote.visibility == 'followers') {
+ if (meId == null) {
+ hide = true;
+ } else if (meId === packedNote.userId) {
+ hide = false;
+ } else if (packedNote.reply && (meId === packedNote.reply.userId)) {
+ // 自分の投稿に対するリプライ
+ hide = false;
+ } else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId === id)) {
+ // 自分へのメンション
+ hide = false;
+ } else {
+ // フォロワーかどうか
+ const following = await Followings.findOne({
+ followeeId: packedNote.userId,
+ followerId: meId
+ });
+
+ if (following == null) {
+ hide = true;
+ } else {
+ hide = false;
+ }
+ }
+ }
+
+ if (hide) {
+ packedNote.visibleUserIds = null;
+ packedNote.fileIds = [];
+ packedNote.files = [];
+ packedNote.text = null;
+ packedNote.poll = null;
+ packedNote.cw = null;
+ packedNote.tags = [];
+ packedNote.geo = null;
+ packedNote.isHidden = true;
+ }
+ }
+
+ public packMany(
+ notes: (Note['id'] | Note)[],
+ me?: User['id'] | User,
+ 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,
+ options?: {
+ detail?: boolean;
+ skipHide?: boolean;
+ }
+ ): Promise<Record<string, any>> {
+ const opts = Object.assign({
+ detail: true,
+ skipHide: false
+ }, options);
+
+ const meId = me ? typeof me === 'string' ? me : me.id : null;
+ const note = typeof src === 'object' ? src : await this.findOne(src);
+ const host = note.userHost;
+
+ async function populatePoll() {
+ const poll = await Polls.findOne({ noteId: note.id });
+ const choices = poll.choices.map(c => ({
+ text: c,
+ votes: poll.votes[poll.choices.indexOf(c)],
+ isVoted: false
+ }));
+
+ if (poll.multiple) {
+ const votes = await PollVotes.find({
+ userId: meId,
+ noteId: note.id
+ });
+
+ const myChoices = votes.map(v => v.choice);
+ for (const myChoice of myChoices) {
+ choices[myChoice].isVoted = true;
+ }
+ } else {
+ const vote = await PollVotes.findOne({
+ userId: meId,
+ noteId: note.id
+ });
+
+ if (vote) {
+ choices[vote.choice].isVoted = true;
+ }
+ }
+
+ return {
+ multiple: poll.multiple,
+ expiresAt: poll.expiresAt,
+ choices
+ };
+ }
+
+ async function populateMyReaction() {
+ const reaction = await NoteReactions.findOne({
+ userId: meId,
+ noteId: note.id,
+ });
+
+ if (reaction) {
+ return reaction.reaction;
+ }
+
+ return null;
+ }
+
+ let text = note.text;
+
+ if (note.name) {
+ text = `【${note.name}】\n${note.text}`;
+ }
+
+ const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)]));
+
+ const packed = await rap({
+ id: note.id,
+ createdAt: note.createdAt,
+ app: note.appId ? Apps.pack(note.appId) : null,
+ userId: note.userId,
+ user: Users.pack(note.user || note.userId, meId),
+ text: text,
+ cw: note.cw,
+ visibility: note.visibility,
+ visibleUserIds: note.visibleUserIds,
+ viaMobile: note.viaMobile,
+ reactions: note.reactions,
+ emojis: reactionEmojis.length > 0 ? Emojis.find({
+ name: In(reactionEmojis),
+ host: host
+ }) : [],
+ tags: note.tags,
+ fileIds: note.fileIds,
+ files: DriveFiles.packMany(note.fileIds),
+ replyId: note.replyId,
+ renoteId: note.renoteId,
+
+ ...(opts.detail ? {
+ reply: note.replyId ? this.pack(note.replyId, meId, {
+ detail: false
+ }) : null,
+
+ renote: note.renoteId ? this.pack(note.renoteId, meId, {
+ detail: false
+ }) : null,
+
+ poll: note.hasPoll ? populatePoll() : null,
+
+ ...(meId ? {
+ myReaction: populateMyReaction()
+ } : {})
+ } : {})
+ });
+
+ if (packed.user.isCat && packed.text) {
+ packed.text = nyaize(packed.text);
+ }
+
+ if (!opts.skipHide) {
+ await this.hideNote(packed, meId);
+ }
+
+ return packed;
+ }
+}
diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts
new file mode 100644
index 0000000000..9bc569cd3f
--- /dev/null
+++ b/src/models/repositories/notification.ts
@@ -0,0 +1,47 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users, Notes } from '..';
+import rap from '@prezzemolo/rap';
+import { Notification } from '../entities/notification';
+
+@EntityRepository(Notification)
+export class NotificationRepository extends Repository<Notification> {
+ public packMany(
+ notifications: any[],
+ ) {
+ return Promise.all(notifications.map(x => this.pack(x)));
+ }
+
+ public async pack(
+ src: Notification['id'] | Notification,
+ ) {
+ const notification = typeof src === 'object' ? src : await this.findOne(src);
+
+ return await rap({
+ id: notification.id,
+ createdAt: notification.createdAt,
+ type: notification.type,
+ userId: notification.notifierId,
+ user: Users.pack(notification.notifier || notification.notifierId),
+ ...(notification.type === 'mention' ? {
+ note: Notes.pack(notification.note || notification.noteId),
+ } : {}),
+ ...(notification.type === 'reply' ? {
+ note: Notes.pack(notification.note || notification.noteId),
+ } : {}),
+ ...(notification.type === 'renote' ? {
+ note: Notes.pack(notification.note || notification.noteId),
+ } : {}),
+ ...(notification.type === 'quote' ? {
+ note: Notes.pack(notification.note || notification.noteId),
+ } : {}),
+ ...(notification.type === 'reaction' ? {
+ note: Notes.pack(notification.note || notification.noteId),
+ reaction: notification.reaction
+ } : {}),
+ ...(notification.type === 'pollVote' ? {
+ note: Notes.pack(notification.note || notification.noteId),
+ choice: notification.choice
+ } : {})
+ });
+ }
+}
diff --git a/src/models/repositories/signin.ts b/src/models/repositories/signin.ts
new file mode 100644
index 0000000000..f5b90c0e9e
--- /dev/null
+++ b/src/models/repositories/signin.ts
@@ -0,0 +1,11 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Signin } from '../entities/signin';
+
+@EntityRepository(Signin)
+export class SigninRepository extends Repository<Signin> {
+ public async pack(
+ src: any,
+ ) {
+ return src;
+ }
+}
diff --git a/src/models/repositories/user-list.ts b/src/models/repositories/user-list.ts
new file mode 100644
index 0000000000..921c18ca7a
--- /dev/null
+++ b/src/models/repositories/user-list.ts
@@ -0,0 +1,16 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { UserList } from '../entities/user-list';
+
+@EntityRepository(UserList)
+export class UserListRepository extends Repository<UserList> {
+ public async pack(
+ src: any,
+ ) {
+ const userList = typeof src === 'object' ? src : await this.findOne(src);
+
+ return {
+ id: userList.id,
+ name: userList.name
+ };
+ }
+}
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
new file mode 100644
index 0000000000..7c4cc545cf
--- /dev/null
+++ b/src/models/repositories/user.ts
@@ -0,0 +1,198 @@
+import { EntityRepository, Repository, In } from 'typeorm';
+import { User, ILocalUser, IRemoteUser } from '../entities/user';
+import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings } from '..';
+import rap from '@prezzemolo/rap';
+
+@EntityRepository(User)
+export class UserRepository extends Repository<User> {
+ public async getRelation(me: User['id'], target: User['id']) {
+ const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
+ Followings.findOne({
+ followerId: me,
+ followeeId: target
+ }),
+ Followings.findOne({
+ followerId: target,
+ followeeId: me
+ }),
+ FollowRequests.findOne({
+ followerId: me,
+ followeeId: target
+ }),
+ FollowRequests.findOne({
+ followerId: target,
+ followeeId: me
+ }),
+ Blockings.findOne({
+ blockerId: me,
+ blockeeId: target
+ }),
+ Blockings.findOne({
+ blockerId: target,
+ blockeeId: me
+ }),
+ Mutings.findOne({
+ muterId: me,
+ muteeId: target
+ })
+ ]);
+
+ return {
+ id: target,
+ isFollowing: following1 != null,
+ hasPendingFollowRequestFromYou: followReq1 != null,
+ hasPendingFollowRequestToYou: followReq2 != null,
+ isFollowed: following2 != null,
+ isBlocking: toBlocking != null,
+ isBlocked: fromBlocked != null,
+ isMuted: mute != null
+ };
+ }
+
+ public packMany(
+ users: (User['id'] | User)[],
+ me?: User['id'] | User,
+ 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,
+ options?: {
+ detail?: boolean,
+ includeSecrets?: boolean,
+ includeHasUnreadNotes?: boolean
+ }
+ ): Promise<Record<string, any>> {
+ const opts = Object.assign({
+ detail: false,
+ includeSecrets: false
+ }, options);
+
+ const user = typeof src === 'object' ? src : await this.findOne(src);
+ const meId = me ? typeof me === 'string' ? me : me.id : null;
+
+ const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
+ const pins = opts.detail ? await UserNotePinings.find({ userId: user.id }) : [];
+
+ return await rap({
+ id: user.id,
+ name: user.name,
+ username: user.username,
+ host: user.host,
+ avatarUrl: user.avatarUrl,
+ bannerUrl: user.bannerUrl,
+ avatarColor: user.avatarColor,
+ bannerColor: user.bannerColor,
+ isAdmin: user.isAdmin,
+
+ // カスタム絵文字添付
+ emojis: user.emojis.length > 0 ? Emojis.find({
+ where: {
+ name: In(user.emojis),
+ host: user.host
+ },
+ select: ['name', 'host', 'url', 'aliases']
+ }) : [],
+
+ ...(opts.includeHasUnreadNotes ? {
+ hasUnreadSpecifiedNotes: NoteUnreads.count({
+ where: { userId: user.id, isSpecified: true },
+ take: 1
+ }).then(count => count > 0),
+ hasUnreadMentions: NoteUnreads.count({
+ where: { userId: user.id },
+ take: 1
+ }).then(count => count > 0),
+ } : {}),
+
+ ...(opts.detail ? {
+ description: user.description,
+ location: user.location,
+ birthday: user.birthday,
+ followersCount: user.followersCount,
+ followingCount: user.followingCount,
+ notesCount: user.notesCount,
+ pinnedNoteIds: pins.map(pin => pin.noteId),
+ pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, {
+ detail: true
+ }),
+ } : {}),
+
+ ...(opts.detail && meId === user.id ? {
+ avatarId: user.avatarId,
+ bannerId: user.bannerId,
+ autoWatch: user.autoWatch,
+ alwaysMarkNsfw: user.alwaysMarkNsfw,
+ carefulBot: user.carefulBot,
+ hasUnreadMessagingMessage: MessagingMessages.count({
+ where: {
+ recipientId: user.id,
+ isRead: false
+ },
+ take: 1
+ }).then(count => count > 0),
+ hasUnreadNotification: Notifications.count({
+ where: {
+ userId: user.id,
+ isRead: false
+ },
+ take: 1
+ }).then(count => count > 0),
+ pendingReceivedFollowRequestsCount: FollowRequests.count({
+ followeeId: user.id
+ }),
+ } : {}),
+
+ ...(relation ? {
+ isFollowing: relation.isFollowing,
+ isFollowed: relation.isFollowed,
+ hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
+ hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
+ isBlocking: relation.isBlocking,
+ isBlocked: relation.isBlocked,
+ isMuted: relation.isMuted,
+ } : {})
+ });
+ }
+
+ public isLocalUser(user: User): user is ILocalUser {
+ return user.host === null;
+ }
+
+ public isRemoteUser(user: User): user is IRemoteUser {
+ return !this.isLocalUser(user);
+ }
+
+ //#region Validators
+ public validateUsername(username: string, remote = false): boolean {
+ return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
+ }
+
+ public validatePassword(password: string): boolean {
+ return typeof password == 'string' && password != '';
+ }
+
+ public isValidName(name?: string): boolean {
+ return name === null || (typeof name == 'string' && name.length < 50 && name.trim() != '');
+ }
+
+ public isValidDescription(description: string): boolean {
+ return typeof description == 'string' && description.length < 500 && description.trim() != '';
+ }
+
+ public isValidLocation(location: string): boolean {
+ return typeof location == 'string' && location.length < 50 && location.trim() != '';
+ }
+
+ public isValidBirthday(birthday: string): boolean {
+ return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
+ }
+ //#endregion
+}
diff --git a/src/models/signin.ts b/src/models/signin.ts
deleted file mode 100644
index d8b05c0e30..0000000000
--- a/src/models/signin.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-
-const Signin = db.get<ISignin>('signin');
-export default Signin;
-
-export interface ISignin {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID;
- ip: string;
- headers: any;
- success: boolean;
-}
-
-/**
- * Pack a signin record for API response
- *
- * @param {any} record
- * @return {Promise<any>}
- */
-export const pack = (
- record: any
-) => new Promise<any>(async (resolve, reject) => {
-
- const _record = deepcopy(record);
-
- // Rename _id to id
- _record.id = _record._id;
- delete _record._id;
-
- resolve(_record);
-});
diff --git a/src/models/sw-subscription.ts b/src/models/sw-subscription.ts
deleted file mode 100644
index 743d0d2dd9..0000000000
--- a/src/models/sw-subscription.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const SwSubscription = db.get<ISwSubscription>('swSubscriptions');
-export default SwSubscription;
-
-export interface ISwSubscription {
- _id: mongo.ObjectID;
- userId: mongo.ObjectID;
- endpoint: string;
- auth: string;
- publickey: string;
-}
diff --git a/src/models/user-list.ts b/src/models/user-list.ts
deleted file mode 100644
index e7dd74bdd1..0000000000
--- a/src/models/user-list.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-
-const UserList = db.get<IUserList>('userList');
-export default UserList;
-
-export interface IUserList {
- _id: mongo.ObjectID;
- createdAt: Date;
- title: string;
- userId: mongo.ObjectID;
- userIds: mongo.ObjectID[];
-}
-
-export const pack = (
- userList: string | mongo.ObjectID | IUserList
-) => new Promise<any>(async (resolve, reject) => {
- let _userList: any;
-
- if (isObjectId(userList)) {
- _userList = await UserList.findOne({
- _id: userList
- });
- } else if (typeof userList === 'string') {
- _userList = await UserList.findOne({
- _id: new mongo.ObjectID(userList)
- });
- } else {
- _userList = deepcopy(userList);
- }
-
- if (!_userList) throw `invalid userList arg ${userList}`;
-
- // Rename _id to id
- _userList.id = _userList._id;
- delete _userList._id;
-
- resolve(_userList);
-});
diff --git a/src/models/user.ts b/src/models/user.ts
deleted file mode 100644
index 0c3f7b5508..0000000000
--- a/src/models/user.ts
+++ /dev/null
@@ -1,438 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import rap from '@prezzemolo/rap';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { packMany as packNoteMany } from './note';
-import Following from './following';
-import Blocking from './blocking';
-import Mute from './mute';
-import { getFriendIds } from '../server/api/common/get-friends';
-import config from '../config';
-import FollowRequest from './follow-request';
-import fetchMeta from '../misc/fetch-meta';
-import Emoji from './emoji';
-import { dbLogger } from '../db/logger';
-
-const User = db.get<IUser>('users');
-
-User.createIndex('createdAt');
-User.createIndex('updatedAt');
-User.createIndex('followersCount');
-User.createIndex('tags');
-User.createIndex('isSuspended');
-User.createIndex('username');
-User.createIndex('usernameLower');
-User.createIndex('host');
-User.createIndex(['username', 'host'], { unique: true });
-User.createIndex(['usernameLower', 'host'], { unique: true });
-User.createIndex('token', { sparse: true, unique: true });
-User.createIndex('uri', { sparse: true, unique: true });
-
-export default User;
-
-type IUserBase = {
- _id: mongo.ObjectID;
- createdAt: Date;
- updatedAt?: Date;
- deletedAt?: Date;
- followersCount: number;
- followingCount: number;
- name?: string;
- notesCount: number;
- username: string;
- usernameLower: string;
- avatarId: mongo.ObjectID;
- bannerId: mongo.ObjectID;
- avatarUrl?: string;
- bannerUrl?: string;
- avatarColor?: any;
- bannerColor?: any;
- wallpaperId: mongo.ObjectID;
- wallpaperUrl?: string;
- data: any;
- description: string;
- lang?: string;
- pinnedNoteIds: mongo.ObjectID[];
- emojis?: string[];
- tags?: string[];
-
- isDeleted: boolean;
-
- /**
- * 凍結されているか否か
- */
- isSuspended: boolean;
-
- /**
- * サイレンスされているか否か
- */
- isSilenced: boolean;
-
- /**
- * 鍵アカウントか否か
- */
- isLocked: boolean;
-
- /**
- * Botか否か
- */
- isBot: boolean;
-
- /**
- * Botからのフォローを承認制にするか
- */
- carefulBot: boolean;
-
- /**
- * フォローしているユーザーからのフォローリクエストを自動承認するか
- */
- autoAcceptFollowed: boolean;
-
- /**
- * このアカウントに届いているフォローリクエストの数
- */
- pendingReceivedFollowRequestsCount: number;
-
- host: string;
-};
-
-export interface ILocalUser extends IUserBase {
- host: null;
- keypair: string;
- email: string;
- emailVerified?: boolean;
- emailVerifyCode?: string;
- password: string;
- token: string;
- twitter: {
- accessToken: string;
- accessTokenSecret: string;
- userId: string;
- screenName: string;
- };
- github: {
- accessToken: string;
- id: string;
- login: string;
- };
- discord: {
- accessToken: string;
- refreshToken: string;
- expiresDate: number;
- id: string;
- username: string;
- discriminator: string;
- };
- profile: {
- location: string;
- birthday: string; // 'YYYY-MM-DD'
- tags: string[];
- };
- fields?: {
- name: string;
- value: string;
- }[];
- isCat: boolean;
- isAdmin?: boolean;
- isModerator?: boolean;
- isVerified?: boolean;
- twoFactorSecret: string;
- twoFactorEnabled: boolean;
- twoFactorTempSecret?: string;
- clientSettings: any;
- settings: {
- autoWatch: boolean;
- alwaysMarkNsfw?: boolean;
- };
- hasUnreadNotification: boolean;
- hasUnreadMessagingMessage: boolean;
-}
-
-export interface IRemoteUser extends IUserBase {
- inbox: string;
- sharedInbox?: string;
- featured?: string;
- endpoints: string[];
- uri: string;
- url?: string;
- publicKey: {
- id: string;
- publicKeyPem: string;
- };
- lastFetchedAt: Date;
- isAdmin: false;
- isModerator: false;
-}
-
-export type IUser = ILocalUser | IRemoteUser;
-
-export const isLocalUser = (user: any): user is ILocalUser =>
- user.host === null;
-
-export const isRemoteUser = (user: any): user is IRemoteUser =>
- !isLocalUser(user);
-
-//#region Validators
-export function validateUsername(username: string, remote = false): boolean {
- return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
-}
-
-export function validatePassword(password: string): boolean {
- return typeof password == 'string' && password != '';
-}
-
-export function isValidName(name?: string): boolean {
- return name === null || (typeof name == 'string' && name.length < 50 && name.trim() != '');
-}
-
-export function isValidDescription(description: string): boolean {
- return typeof description == 'string' && description.length < 500 && description.trim() != '';
-}
-
-export function isValidLocation(location: string): boolean {
- return typeof location == 'string' && location.length < 50 && location.trim() != '';
-}
-
-export function isValidBirthday(birthday: string): boolean {
- return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
-}
-//#endregion
-
-export async function getRelation(me: mongo.ObjectId, target: mongo.ObjectId) {
- const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
- Following.findOne({
- followerId: me,
- followeeId: target
- }),
- Following.findOne({
- followerId: target,
- followeeId: me
- }),
- FollowRequest.findOne({
- followerId: me,
- followeeId: target
- }),
- FollowRequest.findOne({
- followerId: target,
- followeeId: me
- }),
- Blocking.findOne({
- blockerId: me,
- blockeeId: target
- }),
- Blocking.findOne({
- blockerId: target,
- blockeeId: me
- }),
- Mute.findOne({
- muterId: me,
- muteeId: target
- })
- ]);
-
- return {
- id: target,
- isFollowing: following1 !== null,
- hasPendingFollowRequestFromYou: followReq1 !== null,
- hasPendingFollowRequestToYou: followReq2 !== null,
- isFollowed: following2 !== null,
- isBlocking: toBlocking !== null,
- isBlocked: fromBlocked !== null,
- isMuted: mute !== null
- };
-}
-
-/**
- * Pack a user for API response
- *
- * @param user target
- * @param me? serializee
- * @param options? serialize options
- * @return Packed user
- */
-export const pack = (
- user: string | mongo.ObjectID | IUser,
- me?: string | mongo.ObjectID | IUser,
- options?: {
- detail?: boolean,
- includeSecrets?: boolean,
- includeHasUnreadNotes?: boolean
- }
-) => new Promise<any>(async (resolve, reject) => {
- const opts = Object.assign({
- detail: false,
- includeSecrets: false
- }, options);
-
- let _user: any;
-
- const fields = opts.detail ? {} : {
- name: true,
- username: true,
- host: true,
- avatarColor: true,
- avatarUrl: true,
- emojis: true,
- isCat: true,
- isBot: true,
- isAdmin: true,
- isVerified: true
- };
-
- // Populate the user if 'user' is ID
- if (isObjectId(user)) {
- _user = await User.findOne({
- _id: user
- }, { fields });
- } else if (typeof user === 'string') {
- _user = await User.findOne({
- _id: new mongo.ObjectID(user)
- }, { fields });
- } else {
- _user = deepcopy(user);
- }
-
- // (データベースの欠損などで)ユーザーがデータベース上に見つからなかったとき
- if (_user == null) {
- dbLogger.warn(`user not found on database: ${user}`);
- return resolve(null);
- }
-
- // Me
- const meId: mongo.ObjectID = me
- ? isObjectId(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- // Rename _id to id
- _user.id = _user._id;
- delete _user._id;
-
- delete _user.usernameLower;
- delete _user.emailVerifyCode;
-
- if (_user.host == null) {
- // Remove private properties
- delete _user.keypair;
- delete _user.password;
- delete _user.token;
- delete _user.twoFactorTempSecret;
- delete _user.two_factor_temp_secret; // 後方互換性のため
- delete _user.twoFactorSecret;
- if (_user.twitter) {
- delete _user.twitter.accessToken;
- delete _user.twitter.accessTokenSecret;
- }
- if (_user.github) {
- delete _user.github.accessToken;
- }
- if (_user.discord) {
- delete _user.discord.accessToken;
- delete _user.discord.refreshToken;
- delete _user.discord.expiresDate;
- }
-
- // Visible via only the official client
- if (!opts.includeSecrets) {
- delete _user.email;
- delete _user.emailVerified;
- delete _user.settings;
- delete _user.clientSettings;
- }
-
- if (!opts.detail) {
- delete _user.twoFactorEnabled;
- }
- } else {
- delete _user.publicKey;
- }
-
- if (_user.avatarUrl == null) {
- _user.avatarUrl = `${config.driveUrl}/default-avatar.jpg`;
- }
-
- if (!meId || !meId.equals(_user.id) || !opts.detail) {
- delete _user.avatarId;
- delete _user.bannerId;
- delete _user.hasUnreadMessagingMessage;
- delete _user.hasUnreadNotification;
- }
-
- if (meId && !meId.equals(_user.id) && opts.detail) {
- const relation = await getRelation(meId, _user.id);
-
- _user.isFollowing = relation.isFollowing;
- _user.isFollowed = relation.isFollowed;
- _user.hasPendingFollowRequestFromYou = relation.hasPendingFollowRequestFromYou;
- _user.hasPendingFollowRequestToYou = relation.hasPendingFollowRequestToYou;
- _user.isBlocking = relation.isBlocking;
- _user.isBlocked = relation.isBlocked;
- _user.isMuted = relation.isMuted;
- }
-
- if (opts.detail) {
- if (_user.pinnedNoteIds) {
- // Populate pinned notes
- _user.pinnedNotes = packNoteMany(_user.pinnedNoteIds, meId, {
- detail: true
- });
- }
-
- if (meId && !meId.equals(_user.id)) {
- const myFollowingIds = await getFriendIds(meId);
-
- // Get following you know count
- _user.followingYouKnowCount = Following.count({
- followeeId: { $in: myFollowingIds },
- followerId: _user.id
- });
-
- // Get followers you know count
- _user.followersYouKnowCount = Following.count({
- followeeId: _user.id,
- followerId: { $in: myFollowingIds }
- });
- }
- }
-
- if (!opts.includeHasUnreadNotes) {
- delete _user.hasUnreadSpecifiedNotes;
- delete _user.hasUnreadMentions;
- }
-
- // カスタム絵文字添付
- if (_user.emojis) {
- _user.emojis = Emoji.find({
- name: { $in: _user.emojis },
- host: _user.host
- }, {
- fields: { _id: false }
- });
- }
-
- // resolve promises in _user object
- _user = await rap(_user);
-
- resolve(_user);
-});
-
-/*
-function img(url) {
- return {
- thumbnail: {
- large: `${url}`,
- medium: '',
- small: ''
- }
- };
-}
-*/
-
-export async function fetchProxyAccount(): Promise<ILocalUser> {
- const meta = await fetchMeta();
- return await User.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser;
-}
diff --git a/src/queue/index.ts b/src/queue/index.ts
index d8328a1d57..728c43c6ac 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -2,14 +2,14 @@ import * as Queue from 'bull';
import * as httpSignature from 'http-signature';
import config from '../config';
-import { ILocalUser } from '../models/user';
+import { ILocalUser } from '../models/entities/user';
import { program } from '../argv';
import processDeliver from './processors/deliver';
import processInbox from './processors/inbox';
import processDb from './processors/db';
import { queueLogger } from './logger';
-import { IDriveFile } from '../models/drive-file';
+import { DriveFile } from '../models/entities/drive-file';
function initializeQueue(name: string) {
return new Queue(name, config.redis != null ? {
@@ -83,15 +83,6 @@ export function inbox(activity: any, signature: httpSignature.IParsedSignature)
});
}
-export function createDeleteNotesJob(user: ILocalUser) {
- return dbQueue.add('deleteNotes', {
- user: user
- }, {
- removeOnComplete: true,
- removeOnFail: true
- });
-}
-
export function createDeleteDriveFilesJob(user: ILocalUser) {
return dbQueue.add('deleteDriveFiles', {
user: user
@@ -146,7 +137,7 @@ export function createExportUserListsJob(user: ILocalUser) {
});
}
-export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_id']) {
+export function createImportFollowingJob(user: ILocalUser, fileId: DriveFile['id']) {
return dbQueue.add('importFollowing', {
user: user,
fileId: fileId
@@ -156,7 +147,7 @@ export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_
});
}
-export function createImportUserListsJob(user: ILocalUser, fileId: IDriveFile['_id']) {
+export function createImportUserListsJob(user: ILocalUser, fileId: DriveFile['id']) {
return dbQueue.add('importUserLists', {
user: user,
fileId: fileId
diff --git a/src/queue/processors/db/delete-drive-files.ts b/src/queue/processors/db/delete-drive-files.ts
index 3de960a25e..5f347fb588 100644
--- a/src/queue/processors/db/delete-drive-files.ts
+++ b/src/queue/processors/db/delete-drive-files.ts
@@ -1,18 +1,17 @@
import * as Bull from 'bull';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
-import User from '../../../models/user';
-import DriveFile from '../../../models/drive-file';
import deleteFile from '../../../services/drive/delete-file';
+import { Users, DriveFiles } from '../../../models';
+import { MoreThan } from 'typeorm';
const logger = queueLogger.createSubLogger('delete-drive-files');
export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void> {
- logger.info(`Deleting drive files of ${job.data.user._id} ...`);
+ logger.info(`Deleting drive files of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
+ const user = await Users.findOne({
+ id: job.data.user.id
});
let deletedCount = 0;
@@ -20,13 +19,14 @@ export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void>
let cursor: any = null;
while (!ended) {
- const files = await DriveFile.find({
- userId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
+ const files = await DriveFiles.find({
+ where: {
+ userId: user.id,
+ ...(cursor ? { id: MoreThan(cursor) } : {})
+ },
+ take: 100,
+ order: {
+ id: 1
}
});
@@ -36,20 +36,20 @@ export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void>
break;
}
- cursor = files[files.length - 1]._id;
+ cursor = files[files.length - 1].id;
for (const file of files) {
await deleteFile(file);
deletedCount++;
}
- const total = await DriveFile.count({
- userId: user._id,
+ const total = await DriveFiles.count({
+ userId: user.id,
});
job.progress(deletedCount / total);
}
- logger.succ(`All drive files (${deletedCount}) of ${user._id} has been deleted.`);
+ logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`);
done();
}
diff --git a/src/queue/processors/db/delete-notes.ts b/src/queue/processors/db/delete-notes.ts
deleted file mode 100644
index 021db8062e..0000000000
--- a/src/queue/processors/db/delete-notes.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import * as Bull from 'bull';
-import * as mongo from 'mongodb';
-
-import { queueLogger } from '../../logger';
-import Note from '../../../models/note';
-import deleteNote from '../../../services/note/delete';
-import User from '../../../models/user';
-
-const logger = queueLogger.createSubLogger('delete-notes');
-
-export async function deleteNotes(job: Bull.Job, done: any): Promise<void> {
- logger.info(`Deleting notes of ${job.data.user._id} ...`);
-
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
- });
-
- let deletedCount = 0;
- let ended = false;
- let cursor: any = null;
-
- while (!ended) {
- const notes = await Note.find({
- userId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
- }
- });
-
- if (notes.length === 0) {
- ended = true;
- job.progress(100);
- break;
- }
-
- cursor = notes[notes.length - 1]._id;
-
- for (const note of notes) {
- await deleteNote(user, note, true);
- deletedCount++;
- }
-
- const total = await Note.count({
- userId: user._id,
- });
-
- job.progress(deletedCount / total);
- }
-
- logger.succ(`All notes (${deletedCount}) of ${user._id} has been deleted.`);
- done();
-}
diff --git a/src/queue/processors/db/export-blocking.ts b/src/queue/processors/db/export-blocking.ts
index 7f32c06472..c12aa4fca3 100644
--- a/src/queue/processors/db/export-blocking.ts
+++ b/src/queue/processors/db/export-blocking.ts
@@ -1,22 +1,21 @@
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
import dateFormat = require('dateformat');
-import Blocking from '../../../models/blocking';
import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, Blockings } from '../../../models';
+import { MoreThan } from 'typeorm';
const logger = queueLogger.createSubLogger('export-blocking');
export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
- logger.info(`Exporting blocking of ${job.data.user._id} ...`);
+ logger.info(`Exporting blocking of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
+ const user = await Users.findOne({
+ id: job.data.user.id
});
// Create temp file
@@ -36,13 +35,14 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
let cursor: any = null;
while (!ended) {
- const blockings = await Blocking.find({
- blockerId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
+ const blockings = await Blockings.find({
+ where: {
+ blockerId: user.id,
+ ...(cursor ? { id: MoreThan(cursor) } : {})
+ },
+ take: 100,
+ order: {
+ id: 1
}
});
@@ -52,10 +52,10 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
break;
}
- cursor = blockings[blockings.length - 1]._id;
+ cursor = blockings[blockings.length - 1].id;
for (const block of blockings) {
- const u = await User.findOne({ _id: block.blockeeId }, { fields: { username: true, host: true } });
+ const u = await Users.findOne({ id: block.blockeeId });
const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
@@ -70,8 +70,8 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
exportedCount++;
}
- const total = await Blocking.count({
- blockerId: user._id,
+ const total = await Blockings.count({
+ blockerId: user.id,
});
job.progress(exportedCount / total);
@@ -83,7 +83,7 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
- logger.succ(`Exported to: ${driveFile._id}`);
+ logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done();
}
diff --git a/src/queue/processors/db/export-following.ts b/src/queue/processors/db/export-following.ts
index 019414072a..fb30df79fe 100644
--- a/src/queue/processors/db/export-following.ts
+++ b/src/queue/processors/db/export-following.ts
@@ -1,22 +1,21 @@
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
import dateFormat = require('dateformat');
-import Following from '../../../models/following';
import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, Followings } from '../../../models';
+import { MoreThan } from 'typeorm';
const logger = queueLogger.createSubLogger('export-following');
export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
- logger.info(`Exporting following of ${job.data.user._id} ...`);
+ logger.info(`Exporting following of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
+ const user = await Users.findOne({
+ id: job.data.user.id
});
// Create temp file
@@ -36,13 +35,14 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
let cursor: any = null;
while (!ended) {
- const followings = await Following.find({
- followerId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
+ const followings = await Followings.find({
+ where: {
+ followerId: user.id,
+ ...(cursor ? { id: MoreThan(cursor) } : {})
+ },
+ take: 100,
+ order: {
+ id: 1
}
});
@@ -52,10 +52,10 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
break;
}
- cursor = followings[followings.length - 1]._id;
+ cursor = followings[followings.length - 1].id;
for (const following of followings) {
- const u = await User.findOne({ _id: following.followeeId }, { fields: { username: true, host: true } });
+ const u = await Users.findOne({ id: following.followeeId });
const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
@@ -70,8 +70,8 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
exportedCount++;
}
- const total = await Following.count({
- followerId: user._id,
+ const total = await Followings.count({
+ followerId: user.id,
});
job.progress(exportedCount / total);
@@ -83,7 +83,7 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
- logger.succ(`Exported to: ${driveFile._id}`);
+ logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done();
}
diff --git a/src/queue/processors/db/export-mute.ts b/src/queue/processors/db/export-mute.ts
index 5ded7cf651..3aed526dc5 100644
--- a/src/queue/processors/db/export-mute.ts
+++ b/src/queue/processors/db/export-mute.ts
@@ -1,22 +1,21 @@
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
import dateFormat = require('dateformat');
-import Mute from '../../../models/mute';
import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, Mutings } from '../../../models';
+import { MoreThan } from 'typeorm';
const logger = queueLogger.createSubLogger('export-mute');
export async function exportMute(job: Bull.Job, done: any): Promise<void> {
- logger.info(`Exporting mute of ${job.data.user._id} ...`);
+ logger.info(`Exporting mute of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
+ const user = await Users.findOne({
+ id: job.data.user.id
});
// Create temp file
@@ -36,13 +35,14 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
let cursor: any = null;
while (!ended) {
- const mutes = await Mute.find({
- muterId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
+ const mutes = await Mutings.find({
+ where: {
+ muterId: user.id,
+ ...(cursor ? { id: MoreThan(cursor) } : {})
+ },
+ take: 100,
+ order: {
+ id: 1
}
});
@@ -52,10 +52,10 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
break;
}
- cursor = mutes[mutes.length - 1]._id;
+ cursor = mutes[mutes.length - 1].id;
for (const mute of mutes) {
- const u = await User.findOne({ _id: mute.muteeId }, { fields: { username: true, host: true } });
+ const u = await Users.findOne({ id: mute.muteeId });
const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
@@ -70,8 +70,8 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
exportedCount++;
}
- const total = await Mute.count({
- muterId: user._id,
+ const total = await Mutings.count({
+ muterId: user.id,
});
job.progress(exportedCount / total);
@@ -83,7 +83,7 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
- logger.succ(`Exported to: ${driveFile._id}`);
+ logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done();
}
diff --git a/src/queue/processors/db/export-notes.ts b/src/queue/processors/db/export-notes.ts
index 8f3cdc5b99..92867ad82e 100644
--- a/src/queue/processors/db/export-notes.ts
+++ b/src/queue/processors/db/export-notes.ts
@@ -1,21 +1,22 @@
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
-import Note, { INote } from '../../../models/note';
import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
import dateFormat = require('dateformat');
+import { Users, Notes, Polls } from '../../../models';
+import { MoreThan } from 'typeorm';
+import { Note } from '../../../models/entities/note';
+import { Poll } from '../../../models/entities/poll';
const logger = queueLogger.createSubLogger('export-notes');
export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
- logger.info(`Exporting notes of ${job.data.user._id} ...`);
+ logger.info(`Exporting notes of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
+ const user = await Users.findOne({
+ id: job.data.user.id
});
// Create temp file
@@ -46,13 +47,14 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
let cursor: any = null;
while (!ended) {
- const notes = await Note.find({
- userId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
+ const notes = await Notes.find({
+ where: {
+ userId: user.id,
+ ...(cursor ? { id: MoreThan(cursor) } : {})
+ },
+ take: 100,
+ order: {
+ id: 1
}
});
@@ -62,10 +64,14 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
break;
}
- cursor = notes[notes.length - 1]._id;
+ cursor = notes[notes.length - 1].id;
for (const note of notes) {
- const content = JSON.stringify(serialize(note));
+ let poll: Poll;
+ if (note.hasPoll) {
+ poll = await Polls.findOne({ noteId: note.id });
+ }
+ const content = JSON.stringify(serialize(note, poll));
await new Promise((res, rej) => {
stream.write(exportedNotesCount === 0 ? content : ',\n' + content, err => {
if (err) {
@@ -79,8 +85,8 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
exportedNotesCount++;
}
- const total = await Note.count({
- userId: user._id,
+ const total = await Notes.count({
+ userId: user.id,
});
job.progress(exportedNotesCount / total);
@@ -103,20 +109,20 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
const driveFile = await addFile(user, path, fileName);
- logger.succ(`Exported to: ${driveFile._id}`);
+ logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done();
}
-function serialize(note: INote): any {
+function serialize(note: Note, poll: Poll): any {
return {
- id: note._id,
+ id: note.id,
text: note.text,
createdAt: note.createdAt,
fileIds: note.fileIds,
replyId: note.replyId,
renoteId: note.renoteId,
- poll: note.poll,
+ poll: poll,
cw: note.cw,
viaMobile: note.viaMobile,
visibility: note.visibility,
diff --git a/src/queue/processors/db/export-user-lists.ts b/src/queue/processors/db/export-user-lists.ts
index dfbf152ec0..f3987cb0d2 100644
--- a/src/queue/processors/db/export-user-lists.ts
+++ b/src/queue/processors/db/export-user-lists.ts
@@ -1,26 +1,25 @@
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
import dateFormat = require('dateformat');
-import UserList from '../../../models/user-list';
import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, UserLists, UserListJoinings } from '../../../models';
+import { In } from 'typeorm';
const logger = queueLogger.createSubLogger('export-user-lists');
export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
- logger.info(`Exporting user lists of ${job.data.user._id} ...`);
+ logger.info(`Exporting user lists of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
+ const user = await Users.findOne({
+ id: job.data.user.id
});
- const lists = await UserList.find({
- userId: user._id
+ const lists = await UserLists.find({
+ userId: user.id
});
// Create temp file
@@ -36,18 +35,14 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
const stream = fs.createWriteStream(path, { flags: 'a' });
for (const list of lists) {
- const users = await User.find({
- _id: { $in: list.userIds }
- }, {
- fields: {
- username: true,
- host: true
- }
+ const joinings = await UserListJoinings.find({ userListId: list.id });
+ const users = await Users.find({
+ id: In(joinings.map(j => j.userId))
});
for (const u of users) {
const acct = getFullApAccount(u.username, u.host);
- const content = `${list.title},${acct}`;
+ const content = `${list.name},${acct}`;
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
@@ -67,7 +62,7 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
- logger.succ(`Exported to: ${driveFile._id}`);
+ logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done();
}
diff --git a/src/queue/processors/db/import-following.ts b/src/queue/processors/db/import-following.ts
index 069afa74c4..2e646c1869 100644
--- a/src/queue/processors/db/import-following.ts
+++ b/src/queue/processors/db/import-following.ts
@@ -1,32 +1,27 @@
import * as Bull from 'bull';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
-import User from '../../../models/user';
import follow from '../../../services/following/create';
-import DriveFile from '../../../models/drive-file';
-import { getOriginalUrl } from '../../../misc/get-drive-file-url';
import parseAcct from '../../../misc/acct/parse';
import resolveUser from '../../../remote/resolve-user';
import { downloadTextFile } from '../../../misc/download-text-file';
import { isSelfHost, toDbHost } from '../../../misc/convert-host';
+import { Users, DriveFiles } from '../../../models';
const logger = queueLogger.createSubLogger('import-following');
export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
- logger.info(`Importing following of ${job.data.user._id} ...`);
+ logger.info(`Importing following of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
+ const user = await Users.findOne({
+ id: job.data.user.id
});
- const file = await DriveFile.findOne({
- _id: new mongo.ObjectID(job.data.fileId.toString())
+ const file = await DriveFiles.findOne({
+ id: job.data.fileId
});
- const url = getOriginalUrl(file);
-
- const csv = await downloadTextFile(url);
+ const csv = await downloadTextFile(file.url);
let linenum = 0;
@@ -36,10 +31,10 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
try {
const { username, host } = parseAcct(line.trim());
- let target = isSelfHost(host) ? await User.findOne({
+ let target = isSelfHost(host) ? await Users.findOne({
host: null,
usernameLower: username.toLowerCase()
- }) : await User.findOne({
+ }) : await Users.findOne({
host: toDbHost(host),
usernameLower: username.toLowerCase()
});
@@ -55,9 +50,9 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
}
// skip myself
- if (target._id.equals(job.data.user._id)) continue;
+ if (target.id === job.data.user.id) continue;
- logger.info(`Follow[${linenum}] ${target._id} ...`);
+ logger.info(`Follow[${linenum}] ${target.id} ...`);
follow(user, target);
} catch (e) {
diff --git a/src/queue/processors/db/import-user-lists.ts b/src/queue/processors/db/import-user-lists.ts
index 50d3c649d4..8be5785896 100644
--- a/src/queue/processors/db/import-user-lists.ts
+++ b/src/queue/processors/db/import-user-lists.ts
@@ -1,62 +1,59 @@
import * as Bull from 'bull';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
-import User from '../../../models/user';
-import UserList from '../../../models/user-list';
-import DriveFile from '../../../models/drive-file';
-import { getOriginalUrl } from '../../../misc/get-drive-file-url';
import parseAcct from '../../../misc/acct/parse';
import resolveUser from '../../../remote/resolve-user';
import { pushUserToUserList } from '../../../services/user-list/push';
import { downloadTextFile } from '../../../misc/download-text-file';
import { isSelfHost, toDbHost } from '../../../misc/convert-host';
+import { DriveFiles, Users, UserLists, UserListJoinings } from '../../../models';
+import { genId } from '../../../misc/gen-id';
const logger = queueLogger.createSubLogger('import-user-lists');
export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
- logger.info(`Importing user lists of ${job.data.user._id} ...`);
+ logger.info(`Importing user lists of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
+ const user = await Users.findOne({
+ id: job.data.user.id
});
- const file = await DriveFile.findOne({
- _id: new mongo.ObjectID(job.data.fileId.toString())
+ const file = await DriveFiles.findOne({
+ id: job.data.fileId
});
- const url = getOriginalUrl(file);
-
- const csv = await downloadTextFile(url);
+ const csv = await downloadTextFile(file.url);
for (const line of csv.trim().split('\n')) {
const listName = line.split(',')[0].trim();
const { username, host } = parseAcct(line.split(',')[1].trim());
- let list = await UserList.findOne({
- userId: user._id,
- title: listName
+ let list = await UserLists.findOne({
+ userId: user.id,
+ name: listName
});
if (list == null) {
- list = await UserList.insert({
+ list = await UserLists.save({
+ id: genId(),
createdAt: new Date(),
- userId: user._id,
- title: listName,
+ userId: user.id,
+ name: listName,
userIds: []
});
}
- let target = isSelfHost(host) ? await User.findOne({
+ let target = isSelfHost(host) ? await Users.findOne({
host: null,
usernameLower: username.toLowerCase()
- }) : await User.findOne({
+ }) : await Users.findOne({
host: toDbHost(host),
usernameLower: username.toLowerCase()
});
if (host == null && target == null) continue;
- if (list.userIds.some(id => id.equals(target._id))) continue;
+
+ if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue;
if (target == null) {
target = await resolveUser(username, host);
diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts
index 1bc9a9af7c..921cdf7ab1 100644
--- a/src/queue/processors/db/index.ts
+++ b/src/queue/processors/db/index.ts
@@ -1,5 +1,4 @@
import * as Bull from 'bull';
-import { deleteNotes } from './delete-notes';
import { deleteDriveFiles } from './delete-drive-files';
import { exportNotes } from './export-notes';
import { exportFollowing } from './export-following';
@@ -10,7 +9,6 @@ import { importFollowing } from './import-following';
import { importUserLists } from './import-user-lists';
const jobs = {
- deleteNotes,
deleteDriveFiles,
exportNotes,
exportFollowing,
diff --git a/src/queue/processors/deliver.ts b/src/queue/processors/deliver.ts
index 28d3a17f6b..b9701c0c65 100644
--- a/src/queue/processors/deliver.ts
+++ b/src/queue/processors/deliver.ts
@@ -1,9 +1,9 @@
import * as Bull from 'bull';
import request from '../../remote/activitypub/request';
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
import Logger from '../../services/logger';
+import { Instances } from '../../models';
+import { instanceChart } from '../../services/chart';
const logger = new Logger('deliver');
@@ -21,13 +21,11 @@ export default async (job: Bull.Job) => {
// Update stats
registerOrFetchInstanceDoc(host).then(i => {
- Instance.update({ _id: i._id }, {
- $set: {
- latestRequestSentAt: new Date(),
- latestStatus: 200,
- lastCommunicatedAt: new Date(),
- isNotResponding: false
- }
+ Instances.update(i.id, {
+ latestRequestSentAt: new Date(),
+ latestStatus: 200,
+ lastCommunicatedAt: new Date(),
+ isNotResponding: false
});
instanceChart.requestSent(i.host, true);
@@ -37,12 +35,10 @@ export default async (job: Bull.Job) => {
} catch (res) {
// Update stats
registerOrFetchInstanceDoc(host).then(i => {
- Instance.update({ _id: i._id }, {
- $set: {
- latestRequestSentAt: new Date(),
- latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null,
- isNotResponding: true
- }
+ Instances.update(i.id, {
+ latestRequestSentAt: new Date(),
+ latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null,
+ isNotResponding: true
});
instanceChart.requestSent(i.host, false);
diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts
index 436f3335c8..16badabcf7 100644
--- a/src/queue/processors/inbox.ts
+++ b/src/queue/processors/inbox.ts
@@ -1,7 +1,7 @@
import * as Bull from 'bull';
import * as httpSignature from 'http-signature';
import parseAcct from '../../misc/acct/parse';
-import User, { IRemoteUser } from '../../models/user';
+import { IRemoteUser } from '../../models/entities/user';
import perform from '../../remote/activitypub/perform';
import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
import { toUnicode } from 'punycode';
@@ -9,8 +9,10 @@ import { URL } from 'url';
import { publishApLogStream } from '../../services/stream';
import Logger from '../../services/logger';
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
+import { Instances, Users, UserPublickeys } from '../../models';
+import { instanceChart } from '../../services/chart';
+import { UserPublickey } from '../../models/entities/user-publickey';
+import fetchMeta from '../../misc/fetch-meta';
const logger = new Logger('inbox');
@@ -28,6 +30,7 @@ export default async (job: Bull.Job): Promise<void> => {
const keyIdLower = signature.keyId.toLowerCase();
let user: IRemoteUser;
+ let key: UserPublickey;
if (keyIdLower.startsWith('acct:')) {
const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
@@ -46,13 +49,17 @@ export default async (job: Bull.Job): Promise<void> => {
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
- const instance = await Instance.findOne({ host: host.toLowerCase() });
- if (instance && instance.isBlocked) {
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(host.toLowerCase())) {
logger.info(`Blocked request: ${host}`);
return;
}
- user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser;
+ user = await Users.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser;
+
+ key = await UserPublickeys.findOne({
+ userId: user.id
+ });
} else {
// アクティビティ内のホストの検証
const host = toUnicode(new URL(signature.keyId).hostname.toLowerCase());
@@ -65,16 +72,17 @@ export default async (job: Bull.Job): Promise<void> => {
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
- const instance = await Instance.findOne({ host: host.toLowerCase() });
- if (instance && instance.isBlocked) {
- logger.warn(`Blocked request: ${host}`);
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(host.toLowerCase())) {
+ logger.info(`Blocked request: ${host}`);
return;
}
- user = await User.findOne({
- host: { $ne: null },
- 'publicKey.id': signature.keyId
- }) as IRemoteUser;
+ key = await UserPublickeys.findOne({
+ keyId: signature.keyId
+ });
+
+ user = await Users.findOne(key.userId) as IRemoteUser;
}
// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
@@ -82,7 +90,7 @@ export default async (job: Bull.Job): Promise<void> => {
if (activity.object && activity.object.type === 'Person') {
if (user == null) {
logger.warn('Update activity received, but user not registed.');
- } else if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
+ } else if (!httpSignature.verifySignature(signature, key.keyPem)) {
logger.warn('Update activity received, but signature verification failed.');
} else {
updatePerson(activity.actor, null, activity.object);
@@ -92,15 +100,15 @@ export default async (job: Bull.Job): Promise<void> => {
}
// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
- if (user === null) {
+ if (user == null) {
user = await resolvePerson(activity.actor) as IRemoteUser;
}
- if (user === null) {
+ if (user == null) {
throw new Error('failed to resolve user');
}
- if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
+ if (!httpSignature.verifySignature(signature, key.keyPem)) {
logger.error('signature verification failed');
return;
}
@@ -116,12 +124,10 @@ export default async (job: Bull.Job): Promise<void> => {
// Update stats
registerOrFetchInstanceDoc(user.host).then(i => {
- Instance.update({ _id: i._id }, {
- $set: {
- latestRequestReceivedAt: new Date(),
- lastCommunicatedAt: new Date(),
- isNotResponding: false
- }
+ Instances.update(i.id, {
+ latestRequestReceivedAt: new Date(),
+ lastCommunicatedAt: new Date(),
+ isNotResponding: false
});
instanceChart.requestReceived(i.host);
diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts
index 07c820c28a..816fcbadbf 100644
--- a/src/remote/activitypub/kernel/accept/follow.ts
+++ b/src/remote/activitypub/kernel/accept/follow.ts
@@ -1,8 +1,8 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import config from '../../../../config';
import accept from '../../../../services/following/requests/accept';
import { IFollow } from '../../type';
+import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
@@ -11,11 +11,11 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
return null;
}
- const follower = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
+ const follower = await Users.findOne({
+ id: id.split('/').pop()
});
- if (follower === null) {
+ if (follower == null) {
throw new Error('follower not found');
}
diff --git a/src/remote/activitypub/kernel/accept/index.ts b/src/remote/activitypub/kernel/accept/index.ts
index 443c1935d6..5a27ce1d4d 100644
--- a/src/remote/activitypub/kernel/accept/index.ts
+++ b/src/remote/activitypub/kernel/accept/index.ts
@@ -1,5 +1,5 @@
import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import acceptFollow from './follow';
import { IAccept, IFollow } from '../../type';
import { apLogger } from '../../logger';
diff --git a/src/remote/activitypub/kernel/add/index.ts b/src/remote/activitypub/kernel/add/index.ts
index eb2dba5b21..d16f0a4a0d 100644
--- a/src/remote/activitypub/kernel/add/index.ts
+++ b/src/remote/activitypub/kernel/add/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { IAdd } from '../../type';
import { resolveNote } from '../../models/note';
import { addPinned } from '../../../../services/i/pin';
@@ -14,7 +14,7 @@ export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
if (activity.target === actor.featured) {
const note = await resolveNote(activity.object);
- await addPinned(actor, note._id);
+ await addPinned(actor, note.id);
return;
}
diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts
index 5f738da6c7..ebd5a27b92 100644
--- a/src/remote/activitypub/kernel/announce/index.ts
+++ b/src/remote/activitypub/kernel/announce/index.ts
@@ -1,5 +1,5 @@
import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import announceNote from './note';
import { IAnnounce, INote } from '../../type';
import { apLogger } from '../../logger';
diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
index 912936bef8..403fc66bed 100644
--- a/src/remote/activitypub/kernel/announce/note.ts
+++ b/src/remote/activitypub/kernel/announce/note.ts
@@ -1,12 +1,12 @@
import Resolver from '../../resolver';
import post from '../../../../services/note/create';
-import { IRemoteUser, IUser } from '../../../../models/user';
+import { IRemoteUser, User } from '../../../../models/entities/user';
import { IAnnounce, INote } from '../../type';
import { fetchNote, resolveNote } from '../../models/note';
import { resolvePerson } from '../../models/person';
import { apLogger } from '../../logger';
import { extractDbHost } from '../../../../misc/convert-host';
-import Instance from '../../../../models/instance';
+import fetchMeta from '../../../../misc/fetch-meta';
const logger = apLogger;
@@ -27,8 +27,8 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
// アナウンス先をブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
- const instance = await Instance.findOne({ host: extractDbHost(uri) });
- if (instance && instance.isBlocked) return;
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(extractDbHost(uri))) return;
// 既に同じURIを持つものが登録されていないかチェック
const exist = await fetchNote(uri);
@@ -55,7 +55,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
//#region Visibility
const visibility = getVisibility(activity.to, activity.cc, actor);
- let visibleUsers: IUser[] = [];
+ let visibleUsers: User[] = [];
if (visibility == 'specified') {
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
}
diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts
index a10163016c..48e251dd9b 100644
--- a/src/remote/activitypub/kernel/block/index.ts
+++ b/src/remote/activitypub/kernel/block/index.ts
@@ -1,9 +1,9 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import { IBlock } from '../../type';
import block from '../../../../services/blocking/create';
import { apLogger } from '../../logger';
+import { Users } from '../../../../models';
+import { IRemoteUser } from '../../../../models/entities/user';
const logger = apLogger;
@@ -18,11 +18,9 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
return null;
}
- const blockee = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
- });
+ const blockee = await Users.findOne(id.split('/').pop());
- if (blockee === null) {
+ if (blockee == null) {
throw new Error('blockee not found');
}
diff --git a/src/remote/activitypub/kernel/create/image.ts b/src/remote/activitypub/kernel/create/image.ts
index 9c19abbcc4..7720e8f1bd 100644
--- a/src/remote/activitypub/kernel/create/image.ts
+++ b/src/remote/activitypub/kernel/create/image.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { createImage } from '../../models/image';
export default async function(actor: IRemoteUser, image: any): Promise<void> {
diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts
index 6e314d0b82..0326b591f8 100644
--- a/src/remote/activitypub/kernel/create/index.ts
+++ b/src/remote/activitypub/kernel/create/index.ts
@@ -1,5 +1,5 @@
import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import createImage from './image';
import createNote from './note';
import { ICreate } from '../../type';
diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts
index 0f874b9fbf..70e61bdf1b 100644
--- a/src/remote/activitypub/kernel/create/note.ts
+++ b/src/remote/activitypub/kernel/create/note.ts
@@ -1,5 +1,5 @@
import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { createNote, fetchNote } from '../../models/note';
/**
diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts
index c9c385b1fa..fab5e7ab64 100644
--- a/src/remote/activitypub/kernel/delete/index.ts
+++ b/src/remote/activitypub/kernel/delete/index.ts
@@ -1,9 +1,9 @@
import Resolver from '../../resolver';
import deleteNote from './note';
-import Note from '../../../../models/note';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { IDelete } from '../../type';
import { apLogger } from '../../logger';
+import { Notes } from '../../../../models';
/**
* 削除アクティビティを捌きます
@@ -27,7 +27,7 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
break;
case 'Tombstone':
- const note = await Note.findOne({ uri });
+ const note = await Notes.findOne({ uri });
if (note != null) {
deleteNote(actor, uri);
}
diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts
index f67919c56b..b146e68a07 100644
--- a/src/remote/activitypub/kernel/delete/note.ts
+++ b/src/remote/activitypub/kernel/delete/note.ts
@@ -1,20 +1,20 @@
-import Note from '../../../../models/note';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import deleteNode from '../../../../services/note/delete';
import { apLogger } from '../../logger';
+import { Notes } from '../../../../models';
const logger = apLogger;
export default async function(actor: IRemoteUser, uri: string): Promise<void> {
logger.info(`Deleting the Note: ${uri}`);
- const note = await Note.findOne({ uri });
+ const note = await Notes.findOne({ uri });
if (note == null) {
throw new Error('note not found');
}
- if (!note.userId.equals(actor._id)) {
+ if (note.userId !== actor.id) {
throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません');
}
diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts
index e2db70b20d..e6c8833f3a 100644
--- a/src/remote/activitypub/kernel/follow.ts
+++ b/src/remote/activitypub/kernel/follow.ts
@@ -1,8 +1,8 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../models/user';
+import { IRemoteUser } from '../../../models/entities/user';
import config from '../../../config';
import follow from '../../../services/following/create';
import { IFollow } from '../type';
+import { Users } from '../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@@ -11,11 +11,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
return null;
}
- const followee = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
- });
+ const followee = await Users.findOne(id.split('/').pop());
- if (followee === null) {
+ if (followee == null) {
throw new Error('followee not found');
}
diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts
index 4f7a5c91fd..4a57d0675e 100644
--- a/src/remote/activitypub/kernel/index.ts
+++ b/src/remote/activitypub/kernel/index.ts
@@ -1,5 +1,5 @@
import { Object } from '../type';
-import { IRemoteUser } from '../../../models/user';
+import { IRemoteUser } from '../../../models/entities/user';
import create from './create';
import performDeleteActivity from './delete';
import performUpdateActivity from './update';
diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts
index ed35da8133..86dd8fb33d 100644
--- a/src/remote/activitypub/kernel/like.ts
+++ b/src/remote/activitypub/kernel/like.ts
@@ -1,8 +1,7 @@
-import * as mongo from 'mongodb';
-import Note from '../../../models/note';
-import { IRemoteUser } from '../../../models/user';
+import { IRemoteUser } from '../../../models/entities/user';
import { ILike } from '../type';
import create from '../../../services/note/reaction/create';
+import { Notes } from '../../../models';
export default async (actor: IRemoteUser, activity: ILike) => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@@ -10,10 +9,10 @@ export default async (actor: IRemoteUser, activity: ILike) => {
// Transform:
// https://misskey.ex/notes/xxxx to
// xxxx
- const noteId = new mongo.ObjectID(id.split('/').pop());
+ const noteId = id.split('/').pop();
- const note = await Note.findOne({ _id: noteId });
- if (note === null) {
+ const note = await Notes.findOne(noteId);
+ if (note == null) {
throw new Error();
}
diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts
index 35cd2ec0c9..b06ae6fb96 100644
--- a/src/remote/activitypub/kernel/reject/follow.ts
+++ b/src/remote/activitypub/kernel/reject/follow.ts
@@ -1,8 +1,8 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import config from '../../../../config';
import reject from '../../../../services/following/requests/reject';
import { IFollow } from '../../type';
+import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
@@ -11,11 +11,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
return null;
}
- const follower = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
- });
+ const follower = await Users.findOne(id.split('/').pop());
- if (follower === null) {
+ if (follower == null) {
throw new Error('follower not found');
}
diff --git a/src/remote/activitypub/kernel/reject/index.ts b/src/remote/activitypub/kernel/reject/index.ts
index c3585abbb6..8ece5cf174 100644
--- a/src/remote/activitypub/kernel/reject/index.ts
+++ b/src/remote/activitypub/kernel/reject/index.ts
@@ -1,5 +1,5 @@
import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import rejectFollow from './follow';
import { IReject, IFollow } from '../../type';
import { apLogger } from '../../logger';
diff --git a/src/remote/activitypub/kernel/remove/index.ts b/src/remote/activitypub/kernel/remove/index.ts
index 91b207c80d..ae33be59dc 100644
--- a/src/remote/activitypub/kernel/remove/index.ts
+++ b/src/remote/activitypub/kernel/remove/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { IRemove } from '../../type';
import { resolveNote } from '../../models/note';
import { removePinned } from '../../../../services/i/pin';
@@ -14,7 +14,7 @@ export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => {
if (activity.target === actor.featured) {
const note = await resolveNote(activity.object);
- await removePinned(actor, note._id);
+ await removePinned(actor, note.id);
return;
}
diff --git a/src/remote/activitypub/kernel/undo/block.ts b/src/remote/activitypub/kernel/undo/block.ts
index 4a22ac7924..c916a00737 100644
--- a/src/remote/activitypub/kernel/undo/block.ts
+++ b/src/remote/activitypub/kernel/undo/block.ts
@@ -1,9 +1,9 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import { IBlock } from '../../type';
import unblock from '../../../../services/blocking/delete';
import { apLogger } from '../../logger';
+import { IRemoteUser } from '../../../../models/entities/user';
+import { Users } from '../../../../models';
const logger = apLogger;
@@ -18,11 +18,9 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
return null;
}
- const blockee = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
- });
+ const blockee = await Users.findOne(id.split('/').pop());
- if (blockee === null) {
+ if (blockee == null) {
throw new Error('blockee not found');
}
diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts
index af06aa5b31..cc63a740b1 100644
--- a/src/remote/activitypub/kernel/undo/follow.ts
+++ b/src/remote/activitypub/kernel/undo/follow.ts
@@ -1,11 +1,9 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import unfollow from '../../../../services/following/delete';
import cancelRequest from '../../../../services/following/requests/cancel';
import { IFollow } from '../../type';
-import FollowRequest from '../../../../models/follow-request';
-import Following from '../../../../models/following';
+import { IRemoteUser } from '../../../../models/entities/user';
+import { Users, FollowRequests, Followings } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@@ -14,11 +12,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
return null;
}
- const followee = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
- });
+ const followee = await Users.findOne(id.split('/').pop());
- if (followee === null) {
+ if (followee == null) {
throw new Error('followee not found');
}
@@ -26,14 +22,14 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません');
}
- const req = await FollowRequest.findOne({
- followerId: actor._id,
- followeeId: followee._id
+ const req = await FollowRequests.findOne({
+ followerId: actor.id,
+ followeeId: followee.id
});
- const following = await Following.findOne({
- followerId: actor._id,
- followeeId: followee._id
+ const following = await Followings.findOne({
+ followerId: actor.id,
+ followeeId: followee.id
});
if (req) {
diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts
index 80b44fae04..6376ab93a8 100644
--- a/src/remote/activitypub/kernel/undo/index.ts
+++ b/src/remote/activitypub/kernel/undo/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { IUndo, IFollow, IBlock, ILike } from '../../type';
import unfollow from './follow';
import unblock from './block';
diff --git a/src/remote/activitypub/kernel/undo/like.ts b/src/remote/activitypub/kernel/undo/like.ts
index b324ec854c..f337a0173e 100644
--- a/src/remote/activitypub/kernel/undo/like.ts
+++ b/src/remote/activitypub/kernel/undo/like.ts
@@ -1,8 +1,7 @@
-import * as mongo from 'mongodb';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { ILike } from '../../type';
-import Note from '../../../../models/note';
import deleteReaction from '../../../../services/note/reaction/delete';
+import { Notes } from '../../../../models';
/**
* Process Undo.Like activity
@@ -10,10 +9,10 @@ import deleteReaction from '../../../../services/note/reaction/delete';
export default async (actor: IRemoteUser, activity: ILike): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
- const noteId = new mongo.ObjectID(id.split('/').pop());
+ const noteId = id.split('/').pop();
- const note = await Note.findOne({ _id: noteId });
- if (note === null) {
+ const note = await Notes.findOne(noteId);
+ if (note == null) {
throw 'note not found';
}
diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts
index 49b730391a..b8dff73395 100644
--- a/src/remote/activitypub/kernel/update/index.ts
+++ b/src/remote/activitypub/kernel/update/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { IUpdate, IObject } from '../../type';
import { apLogger } from '../../logger';
import { updateQuestion } from '../../models/question';
diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts
index 967ee65544..dba915fee9 100644
--- a/src/remote/activitypub/misc/get-note-html.ts
+++ b/src/remote/activitypub/misc/get-note-html.ts
@@ -1,9 +1,9 @@
-import { INote } from '../../../models/note';
+import { Note } from '../../../models/entities/note';
import { toHtml } from '../../../mfm/toHtml';
import { parse } from '../../../mfm/parse';
-export default function(note: INote) {
- let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
+export default function(note: Note) {
+ let html = toHtml(parse(note.text), JSON.parse(note.mentionedRemoteUsers));
if (html == null) html = '<p>.</p>';
return html;
diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts
index bd97d13d27..87095acd88 100644
--- a/src/remote/activitypub/models/image.ts
+++ b/src/remote/activitypub/models/image.ts
@@ -1,16 +1,17 @@
import uploadFromUrl from '../../../services/drive/upload-from-url';
-import { IRemoteUser } from '../../../models/user';
-import DriveFile, { IDriveFile } from '../../../models/drive-file';
+import { IRemoteUser } from '../../../models/entities/user';
import Resolver from '../resolver';
import fetchMeta from '../../../misc/fetch-meta';
import { apLogger } from '../logger';
+import { DriveFile } from '../../../models/entities/drive-file';
+import { DriveFiles } from '../../../models';
const logger = apLogger;
/**
* Imageを作成します。
*/
-export async function createImage(actor: IRemoteUser, value: any): Promise<IDriveFile> {
+export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
return null;
@@ -39,18 +40,16 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
throw e;
}
- if (file.metadata.isRemote) {
+ if (file.isRemote) {
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
// URLを更新する
- if (file.metadata.url !== image.url) {
- file = await DriveFile.findOneAndUpdate({ _id: file._id }, {
- $set: {
- 'metadata.url': image.url,
- 'metadata.uri': image.url
- }
- }, {
- returnNewDocument: true
+ if (file.url !== image.url) {
+ await DriveFiles.update({ id: file.id }, {
+ url: image.url,
+ uri: image.url
});
+
+ file = DriveFiles.findOne(file.id);
}
}
@@ -63,7 +62,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
* Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
-export async function resolveImage(actor: IRemoteUser, value: any): Promise<IDriveFile> {
+export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
// TODO
// リモートサーバーからフェッチしてきて登録
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index 6251621527..cd587c51cf 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -1,26 +1,27 @@
-import * as mongo from 'mongodb';
import * as promiseLimit from 'promise-limit';
import config from '../../../config';
import Resolver from '../resolver';
-import Note, { INote } from '../../../models/note';
import post from '../../../services/note/create';
-import { INote as INoteActivityStreamsObject, IObject } from '../type';
import { resolvePerson, updatePerson } from './person';
import { resolveImage } from './image';
-import { IRemoteUser, IUser } from '../../../models/user';
+import { IRemoteUser, User } from '../../../models/entities/user';
import { fromHtml } from '../../../mfm/fromHtml';
-import Emoji, { IEmoji } from '../../../models/emoji';
import { ITag, extractHashtags } from './tag';
import { toUnicode } from 'punycode';
import { unique, concat, difference } from '../../../prelude/array';
import { extractPollFromQuestion } from './question';
import vote from '../../../services/note/polls/vote';
import { apLogger } from '../logger';
-import { IDriveFile } from '../../../models/drive-file';
+import { DriveFile } from '../../../models/entities/drive-file';
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
-import Instance from '../../../models/instance';
import { extractDbHost } from '../../../misc/convert-host';
+import { Notes, Emojis, Polls } from '../../../models';
+import { Note } from '../../../models/entities/note';
+import { IObject, INote } from '../type';
+import { Emoji } from '../../../models/entities/emoji';
+import { genId } from '../../../misc/gen-id';
+import fetchMeta from '../../../misc/fetch-meta';
const logger = apLogger;
@@ -29,17 +30,17 @@ const logger = apLogger;
*
* Misskeyに対象のNoteが登録されていればそれを返します。
*/
-export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<INote> {
+export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note> {
const uri = typeof value == 'string' ? value : value.id;
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) {
- const id = new mongo.ObjectID(uri.split('/').pop());
- return await Note.findOne({ _id: id });
+ const id = uri.split('/').pop();
+ return await Notes.findOne(id);
}
//#region このサーバーに既に登録されていたらそれを返す
- const exist = await Note.findOne({ uri });
+ const exist = await Notes.findOne({ uri });
if (exist) {
return exist;
@@ -52,7 +53,7 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
/**
* Noteを作成します。
*/
-export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> {
+export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note> {
if (resolver == null) resolver = new Resolver();
const object: any = await resolver.resolve(value);
@@ -68,7 +69,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
return null;
}
- const note: INoteActivityStreamsObject = object;
+ const note: INote = object;
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
@@ -87,7 +88,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc;
let visibility = 'public';
- let visibleUsers: IUser[] = [];
+ let visibleUsers: User[] = [];
if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
visibility = 'home';
@@ -113,12 +114,12 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
const files = note.attachment
.map(attach => attach.sensitive = note.sensitive)
- ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>)))
+ ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<DriveFile>)))
.filter(image => image != null)
: [];
// リプライ
- const reply: INote = note.inReplyTo
+ const reply: Note = note.inReplyTo
? await resolveNote(note.inReplyTo, resolver).catch(e => {
// 4xxの場合はリプライしてないことにする
if (e.statusCode >= 400 && e.statusCode < 500) {
@@ -131,7 +132,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
: null;
// 引用
- let quote: INote;
+ let quote: Note;
if (note._misskey_quote && typeof note._misskey_quote == 'string') {
quote = await resolveNote(note._misskey_quote).catch(e => {
@@ -151,22 +152,23 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const text = note._misskey_content || fromHtml(note.content);
// vote
- if (reply && reply.poll) {
+ if (reply && reply.hasPoll) {
+ const poll = await Polls.findOne({ noteId: reply.id });
const tryCreateVote = async (name: string, index: number): Promise<null> => {
- if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) {
+ if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
} else if (index >= 0) {
logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
await vote(actor, reply, index);
// リモートフォロワーにUpdate配信
- deliverQuestionUpdate(reply._id);
+ deliverQuestionUpdate(reply.id);
}
return null;
};
if (note.name) {
- return await tryCreateVote(note.name, reply.poll.choices.findIndex(x => x.text === note.name));
+ return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
}
// 後方互換性のため
@@ -181,7 +183,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const emojis = await extractEmojis(note.tag, actor.host).catch(e => {
logger.info(`extractEmojis: ${e}`);
- return [] as IEmoji[];
+ return [] as Emoji[];
});
const apEmojis = emojis.map(emoji => emoji.name);
@@ -222,13 +224,13 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
* Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
-export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> {
+export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note> {
const uri = typeof value == 'string' ? value : value.id;
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
- const instance = await Instance.findOne({ host: extractDbHost(uri) });
- if (instance && instance.isBlocked) throw { statusCode: 451 };
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 };
//#region このサーバーに既に登録されていたらそれを返す
const exist = await fetchNote(uri);
@@ -255,7 +257,7 @@ export async function extractEmojis(tags: ITag[], host_: string) {
eomjiTags.map(async tag => {
const name = tag.name.replace(/^:/, '').replace(/:$/, '');
- const exists = await Emoji.findOne({
+ const exists = await Emojis.findOne({
host,
name
});
@@ -263,31 +265,37 @@ export async function extractEmojis(tags: ITag[], host_: string) {
if (exists) {
if ((tag.updated != null && exists.updatedAt == null)
|| (tag.id != null && exists.uri == null)
- || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)) {
- return await Emoji.findOneAndUpdate({
- host,
- name,
- }, {
- $set: {
- uri: tag.id,
- url: tag.icon.url,
- updatedAt: new Date(tag.updated),
- }
- });
+ || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
+ ) {
+ await Emojis.update({
+ host,
+ name,
+ }, {
+ uri: tag.id,
+ url: tag.icon.url,
+ updatedAt: new Date(tag.updated),
+ });
+
+ return await Emojis.findOne({
+ host,
+ name
+ });
}
+
return exists;
}
logger.info(`register emoji host=${host}, name=${name}`);
- return await Emoji.insert({
+ return await Emojis.save({
+ id: genId(),
host,
name,
uri: tag.id,
url: tag.icon.url,
updatedAt: tag.updated ? new Date(tag.updated) : undefined,
aliases: []
- });
+ } as Emoji);
})
);
}
@@ -298,7 +306,7 @@ async function extractMentionedUsers(actor: IRemoteUser, to: string[], cc: strin
const limit = promiseLimit(2);
const users = await Promise.all(
- uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<IUser>)
+ uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<User>)
);
return users.filter(x => x != null);
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index d27c937988..51a9efa10b 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -1,29 +1,29 @@
-import * as mongo from 'mongodb';
import * as promiseLimit from 'promise-limit';
import { toUnicode } from 'punycode';
import config from '../../../config';
-import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user';
import Resolver from '../resolver';
import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type';
-import { IDriveFile } from '../../../models/drive-file';
-import Meta from '../../../models/meta';
+import { DriveFile } from '../../../models/entities/drive-file';
import { fromHtml } from '../../../mfm/fromHtml';
-import usersChart from '../../../services/chart/users';
-import instanceChart from '../../../services/chart/instance';
import { URL } from 'url';
import { resolveNote, extractEmojis } from './note';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
-import Instance from '../../../models/instance';
-import getDriveFileUrl from '../../../misc/get-drive-file-url';
-import { IEmoji } from '../../../models/emoji';
import { ITag, extractHashtags } from './tag';
-import Following from '../../../models/following';
import { IIdentifier } from './identifier';
import { apLogger } from '../logger';
-import { INote } from '../../../models/note';
+import { Note } from '../../../models/entities/note';
import { updateHashtag } from '../../../services/update-hashtag';
+import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserServiceLinkings, UserPublickeys } from '../../../models';
+import { User, IRemoteUser } from '../../../models/entities/user';
+import { Emoji } from '../../../models/entities/emoji';
+import { UserNotePining } from '../../../models/entities/user-note-pinings';
+import { genId } from '../../../misc/gen-id';
+import { UserServiceLinking } from '../../../models/entities/user-service-linking';
+import { instanceChart, usersChart } from '../../../services/chart';
+import { UserPublickey } from '../../../models/entities/user-publickey';
+import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error';
const logger = apLogger;
/**
@@ -50,11 +50,11 @@ function validatePerson(x: any, uri: string) {
return new Error('invalid person: inbox is not a string');
}
- if (!validateUsername(x.preferredUsername, true)) {
+ if (!Users.validateUsername(x.preferredUsername, true)) {
return new Error('invalid person: invalid username');
}
- if (!isValidName(x.name == '' ? null : x.name)) {
+ if (!Users.isValidName(x.name == '' ? null : x.name)) {
return new Error('invalid person: invalid name');
}
@@ -84,17 +84,17 @@ function validatePerson(x: any, uri: string) {
*
* Misskeyに対象のPersonが登録されていればそれを返します。
*/
-export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> {
+export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User> {
if (typeof uri !== 'string') throw 'uri is not string';
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) {
- const id = new mongo.ObjectID(uri.split('/').pop());
- return await User.findOne({ _id: id });
+ const id = uri.split('/').pop();
+ return await Users.findOne(id);
}
//#region このサーバーに既に登録されていたらそれを返す
- const exist = await User.findOne({ uri });
+ const exist = await Users.findOne({ uri });
if (exist) {
return exist;
@@ -107,7 +107,7 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUs
/**
* Personを作成します。
*/
-export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> {
+export async function createPerson(uri: string, resolver?: Resolver): Promise<User> {
if (typeof uri !== 'string') throw 'uri is not string';
if (resolver == null) resolver = new Resolver();
@@ -124,21 +124,6 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
logger.info(`Creating the Person: ${person.id}`);
- const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([
- resolver.resolve(person.followers).then(
- resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
- () => undefined
- ),
- resolver.resolve(person.following).then(
- resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
- () => undefined
- ),
- resolver.resolve(person.outbox).then(
- resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
- () => undefined
- )
- ]);
-
const host = toUnicode(new URL(object.id).hostname.toLowerCase());
const { fields, services } = analyzeAttachments(person.attachment);
@@ -150,24 +135,18 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
// Create user
let user: IRemoteUser;
try {
- user = await User.insert({
+ user = await Users.save({
+ id: genId(),
avatarId: null,
bannerId: null,
- createdAt: Date.parse(person.published) || null,
+ createdAt: Date.parse(person.published) || new Date(),
lastFetchedAt: new Date(),
description: fromHtml(person.summary),
- followersCount,
- followingCount,
- notesCount,
name: person.name,
isLocked: person.manuallyApprovesFollowers,
username: person.preferredUsername,
usernameLower: person.preferredUsername.toLowerCase(),
host,
- publicKey: {
- id: person.publicKey.id,
- publicKeyPem: person.publicKey.publicKeyPem
- },
inbox: person.inbox,
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
featured: person.featured,
@@ -179,10 +158,22 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
tags,
isBot,
isCat: (person as any).isCat === true
- }) as IRemoteUser;
+ } as Partial<User>) as IRemoteUser;
+
+ await UserPublickeys.save({
+ id: genId(),
+ userId: user.id,
+ keyId: person.publicKey.id,
+ keyPem: person.publicKey.publicKeyPem
+ } as UserPublickey);
+
+ await UserServiceLinkings.save({
+ id: genId(),
+ userId: user.id,
+ } as UserServiceLinking);
} catch (e) {
// duplicate key error
- if (e.code === 11000) {
+ if (isDuplicateKeyValueError(e)) {
throw new Error('already registered');
}
@@ -190,33 +181,25 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
throw e;
}
+ await UserServiceLinkings.save({
+ id: genId(),
+ userId: user.id
+ } as UserServiceLinking);
+
// Register host
registerOrFetchInstanceDoc(host).then(i => {
- Instance.update({ _id: i._id }, {
- $inc: {
- usersCount: 1
- }
- });
-
+ Instances.increment({ id: i.id }, 'usersCount', 1);
instanceChart.newUser(i.host);
});
- //#region Increment users count
- Meta.update({}, {
- $inc: {
- 'stats.usersCount': 1
- }
- }, { upsert: true });
-
usersChart.update(user, true);
- //#endregion
// ハッシュタグ更新
for (const tag of tags) updateHashtag(user, tag, true, true);
for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
//#region アイコンとヘッダー画像をフェッチ
- const [avatar, banner] = (await Promise.all<IDriveFile>([
+ const [avatar, banner] = (await Promise.all<DriveFile>([
person.icon,
person.image
].map(img =>
@@ -225,22 +208,20 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
: resolveImage(user, img).catch(() => null)
)));
- const avatarId = avatar ? avatar._id : null;
- const bannerId = banner ? banner._id : null;
- const avatarUrl = getDriveFileUrl(avatar, true);
- const bannerUrl = getDriveFileUrl(banner, false);
- const avatarColor = avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null;
- const bannerColor = banner && avatar.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null;
+ const avatarId = avatar ? avatar.id : null;
+ const bannerId = banner ? banner.id : null;
+ const avatarUrl = DriveFiles.getPublicUrl(avatar);
+ const bannerUrl = DriveFiles.getPublicUrl(banner);
+ const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
+ const bannerColor = banner && avatar.properties.avgColor ? banner.properties.avgColor : null;
- await User.update({ _id: user._id }, {
- $set: {
- avatarId,
- bannerId,
- avatarUrl,
- bannerUrl,
- avatarColor,
- bannerColor
- }
+ await Users.update(user.id, {
+ avatarId,
+ bannerId,
+ avatarUrl,
+ bannerUrl,
+ avatarColor,
+ bannerColor
});
user.avatarId = avatarId;
@@ -254,19 +235,17 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
//#region カスタム絵文字取得
const emojis = await extractEmojis(person.tag, host).catch(e => {
logger.info(`extractEmojis: ${e}`);
- return [] as IEmoji[];
+ return [] as Emoji[];
});
const emojiNames = emojis.map(emoji => emoji.name);
- await User.update({ _id: user._id }, {
- $set: {
- emojis: emojiNames
- }
+ await Users.update(user.id, {
+ emojis: emojiNames
});
//#endregion
- await updateFeatured(user._id).catch(err => logger.error(err));
+ await updateFeatured(user.id).catch(err => logger.error(err));
return user;
}
@@ -287,7 +266,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
}
//#region このサーバーに既に登録されているか
- const exist = await User.findOne({ uri }) as IRemoteUser;
+ const exist = await Users.findOne({ uri }) as IRemoteUser;
if (exist == null) {
return;
@@ -295,10 +274,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
//#endregion
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
- await User.update({ _id: exist._id }, {
- $set: {
- lastFetchedAt: new Date(),
- },
+ await Users.update(exist.id, {
+ lastFetchedAt: new Date(),
});
if (resolver == null) resolver = new Resolver();
@@ -315,23 +292,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
logger.info(`Updating the Person: ${person.id}`);
- const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([
- resolver.resolve(person.followers).then(
- resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
- () => undefined
- ),
- resolver.resolve(person.following).then(
- resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
- () => undefined
- ),
- resolver.resolve(person.outbox).then(
- resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
- () => undefined
- )
- ]);
-
// アイコンとヘッダー画像をフェッチ
- const [avatar, banner] = (await Promise.all<IDriveFile>([
+ const [avatar, banner] = (await Promise.all<DriveFile>([
person.icon,
person.image
].map(img =>
@@ -343,7 +305,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
// カスタム絵文字取得
const emojis = await extractEmojis(person.tag, exist.host).catch(e => {
logger.info(`extractEmojis: ${e}`);
- return [] as IEmoji[];
+ return [] as Emoji[];
});
const emojiNames = emojis.map(emoji => emoji.name);
@@ -359,40 +321,45 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
featured: person.featured,
emojis: emojiNames,
description: fromHtml(person.summary),
- followersCount,
- followingCount,
- notesCount,
name: person.name,
url: person.url,
endpoints: person.endpoints,
fields,
- ...services,
tags,
isBot: object.type == 'Service',
isCat: (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers,
- createdAt: Date.parse(person.published) || null,
- publicKey: {
- id: person.publicKey.id,
- publicKeyPem: person.publicKey.publicKeyPem
- },
- } as any;
+ createdAt: new Date(Date.parse(person.published)) || null,
+ } as Partial<User>;
if (avatar) {
- updates.avatarId = avatar._id;
- updates.avatarUrl = getDriveFileUrl(avatar, true);
- updates.avatarColor = avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null;
+ updates.avatarId = avatar.id;
+ updates.avatarUrl = DriveFiles.getPublicUrl(avatar);
+ updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null;
}
if (banner) {
- updates.bannerId = banner._id;
- updates.bannerUrl = getDriveFileUrl(banner, true);
- updates.bannerColor = banner.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null;
+ updates.bannerId = banner.id;
+ updates.bannerUrl = DriveFiles.getPublicUrl(banner);
+ updates.bannerColor = banner.properties.avgColor ? banner.properties.avgColor : null;
}
// Update user
- await User.update({ _id: exist._id }, {
- $set: updates
+ await Users.update(exist.id, updates);
+
+ await UserPublickeys.update({ userId: exist.id }, {
+ keyId: person.publicKey.id,
+ keyPem: person.publicKey.publicKeyPem
+ });
+
+ await UserServiceLinkings.update({ userId: exist.id }, {
+ twitterUserId: services.twitter.userId,
+ twitterScreenName: services.twitter.screenName,
+ githubId: services.github.id,
+ githubLogin: services.github.login,
+ discordId: services.discord.id,
+ discordUsername: services.discord.username,
+ discordDiscriminator: services.discord.discriminator,
});
// ハッシュタグ更新
@@ -400,17 +367,13 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
for (const tag of (exist.tags || []).filter(x => !tags.includes(x))) updateHashtag(exist, tag, true, false);
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
- await Following.update({
- followerId: exist._id
+ await Followings.update({
+ followerId: exist.id
}, {
- $set: {
- '_follower.sharedInbox': person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
- }
- }, {
- multi: true
+ followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
});
- await updateFeatured(exist._id).catch(err => logger.error(err));
+ await updateFeatured(exist.id).catch(err => logger.error(err));
}
/**
@@ -419,7 +382,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
* Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
-export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> {
+export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<User> {
if (typeof uri !== 'string') throw 'uri is not string';
//#region このサーバーに既に登録されていたらそれを返す
@@ -492,9 +455,9 @@ export function analyzeAttachments(attachments: ITag[]) {
return { fields, services };
}
-export async function updateFeatured(userId: mongo.ObjectID) {
- const user = await User.findOne({ _id: userId });
- if (!isRemoteUser(user)) return;
+export async function updateFeatured(userId: User['id']) {
+ const user = await Users.findOne(userId);
+ if (!Users.isRemoteUser(user)) return;
if (!user.featured) return;
logger.info(`Updating the featured: ${user.uri}`);
@@ -515,11 +478,14 @@ export async function updateFeatured(userId: mongo.ObjectID) {
const featuredNotes = await Promise.all(items
.filter(item => item.type === 'Note')
.slice(0, 5)
- .map(item => limit(() => resolveNote(item, resolver)) as Promise<INote>));
+ .map(item => limit(() => resolveNote(item, resolver)) as Promise<Note>));
- await User.update({ _id: user._id }, {
- $set: {
- pinnedNoteIds: featuredNotes.filter(note => note != null).map(note => note._id)
- }
- });
+ for (const note of featuredNotes.filter(note => note != null)) {
+ UserNotePinings.save({
+ id: genId(),
+ createdAt: new Date(),
+ userId: user.id,
+ noteId: note.id
+ } as UserNotePining);
+ }
}
diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts
index c073684349..a5091a6d96 100644
--- a/src/remote/activitypub/models/question.ts
+++ b/src/remote/activitypub/models/question.ts
@@ -1,8 +1,9 @@
import config from '../../../config';
-import Note, { IChoice, IPoll } from '../../../models/note';
import Resolver from '../resolver';
import { IQuestion } from '../type';
import { apLogger } from '../logger';
+import { Notes, Polls } from '../../../models';
+import { IPoll } from '../../../models/entities/poll';
export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> {
const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source;
@@ -14,14 +15,14 @@ export async function extractPollFromQuestion(source: string | IQuestion): Promi
}
const choices = question[multiple ? 'anyOf' : 'oneOf']
- .map((x, i) => ({
- id: i,
- text: x.name,
- votes: x.replies && x.replies.totalItems || x._misskey_votes || 0,
- } as IChoice));
+ .map((x, i) => x.name);
+
+ const votes = question[multiple ? 'anyOf' : 'oneOf']
+ .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
return {
choices,
+ votes,
multiple,
expiresAt
};
@@ -39,9 +40,11 @@ export async function updateQuestion(value: any) {
if (uri.startsWith(config.url + '/')) throw 'uri points local';
//#region このサーバーに既に登録されているか
- const note = await Note.findOne({ uri });
-
+ const note = await Notes.findOne({ uri });
if (note == null) throw 'Question is not registed';
+
+ const poll = await Polls.findOne({ noteId: note.id });
+ if (poll == null) throw 'Question is not registed';
//#endregion
// resolve new Question object
@@ -52,27 +55,25 @@ export async function updateQuestion(value: any) {
if (question.type !== 'Question') throw 'object is not a Question';
const apChoices = question.oneOf || question.anyOf;
- const dbChoices = note.poll.choices;
let changed = false;
- for (const db of dbChoices) {
- const oldCount = db.votes;
- const newCount = apChoices.filter(ap => ap.name === db.text)[0].replies.totalItems;
+ for (const choice of poll.choices) {
+ const oldCount = poll.votes[poll.choices.indexOf(choice)];
+ const newCount = apChoices.filter(ap => ap.name === choice)[0].replies.totalItems;
if (oldCount != newCount) {
changed = true;
- db.votes = newCount;
+ poll.votes[poll.choices.indexOf(choice)] = newCount;
}
}
- await Note.update({
- _id: note._id
- }, {
- $set: {
- 'poll.choices': dbChoices,
- updatedAt: new Date(),
- }
+ await Notes.update(note.id, {
+ updatedAt: new Date(),
+ });
+
+ await Polls.update(poll.id, {
+ votes: poll.votes
});
return changed;
diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts
index 2e4f53adf5..425adaec96 100644
--- a/src/remote/activitypub/perform.ts
+++ b/src/remote/activitypub/perform.ts
@@ -1,5 +1,5 @@
import { Object } from './type';
-import { IRemoteUser } from '../../models/user';
+import { IRemoteUser } from '../../models/entities/user';
import kernel from './kernel';
export default async (actor: IRemoteUser, activity: Object): Promise<void> => {
diff --git a/src/remote/activitypub/renderer/accept.ts b/src/remote/activitypub/renderer/accept.ts
index fdbdff3f12..21b4629074 100644
--- a/src/remote/activitypub/renderer/accept.ts
+++ b/src/remote/activitypub/renderer/accept.ts
@@ -1,8 +1,8 @@
import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
export default (object: any, user: ILocalUser) => ({
type: 'Accept',
- actor: `${config.url}/users/${user._id}`,
+ actor: `${config.url}/users/${user.id}`,
object
});
diff --git a/src/remote/activitypub/renderer/add.ts b/src/remote/activitypub/renderer/add.ts
index 4d6fe392aa..46f937f61d 100644
--- a/src/remote/activitypub/renderer/add.ts
+++ b/src/remote/activitypub/renderer/add.ts
@@ -1,9 +1,9 @@
import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
export default (user: ILocalUser, target: any, object: any) => ({
type: 'Add',
- actor: `${config.url}/users/${user._id}`,
+ actor: `${config.url}/users/${user.id}`,
target,
object
});
diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts
index f6f2f9bdcd..11e7be449b 100644
--- a/src/remote/activitypub/renderer/announce.ts
+++ b/src/remote/activitypub/renderer/announce.ts
@@ -1,7 +1,7 @@
import config from '../../../config';
-import { INote } from '../../../models/note';
+import { Note } from '../../../models/entities/note';
-export default (object: any, note: INote) => {
+export default (object: any, note: Note) => {
const attributedTo = `${config.url}/users/${note.userId}`;
let to: string[] = [];
@@ -18,7 +18,7 @@ export default (object: any, note: INote) => {
}
return {
- id: `${config.url}/notes/${note._id}/activity`,
+ id: `${config.url}/notes/${note.id}/activity`,
actor: `${config.url}/users/${note.userId}`,
type: 'Announce',
published: note.createdAt.toISOString(),
diff --git a/src/remote/activitypub/renderer/block.ts b/src/remote/activitypub/renderer/block.ts
index 694f3a1418..946c45a813 100644
--- a/src/remote/activitypub/renderer/block.ts
+++ b/src/remote/activitypub/renderer/block.ts
@@ -1,8 +1,8 @@
import config from '../../../config';
-import { ILocalUser, IRemoteUser } from '../../../models/user';
+import { ILocalUser, IRemoteUser } from '../../../models/entities/user';
export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({
type: 'Block',
- actor: `${config.url}/users/${blocker._id}`,
+ actor: `${config.url}/users/${blocker.id}`,
object: blockee.uri
});
diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts
index 1ee1418fce..e1fc0515c8 100644
--- a/src/remote/activitypub/renderer/create.ts
+++ b/src/remote/activitypub/renderer/create.ts
@@ -1,9 +1,9 @@
import config from '../../../config';
-import { INote } from '../../../models/note';
+import { Note } from '../../../models/entities/note';
-export default (object: any, note: INote) => {
+export default (object: any, note: Note) => {
const activity = {
- id: `${config.url}/notes/${note._id}/activity`,
+ id: `${config.url}/notes/${note.id}/activity`,
actor: `${config.url}/users/${note.userId}`,
type: 'Create',
published: note.createdAt.toISOString(),
diff --git a/src/remote/activitypub/renderer/delete.ts b/src/remote/activitypub/renderer/delete.ts
index e090e1c886..a98c97e6e9 100644
--- a/src/remote/activitypub/renderer/delete.ts
+++ b/src/remote/activitypub/renderer/delete.ts
@@ -1,8 +1,8 @@
import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
export default (object: any, user: ILocalUser) => ({
type: 'Delete',
- actor: `${config.url}/users/${user._id}`,
+ actor: `${config.url}/users/${user.id}`,
object
});
diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts
index 17721e9417..4f6ea8c4ee 100644
--- a/src/remote/activitypub/renderer/document.ts
+++ b/src/remote/activitypub/renderer/document.ts
@@ -1,8 +1,8 @@
-import { IDriveFile } from '../../../models/drive-file';
-import getDriveFileUrl from '../../../misc/get-drive-file-url';
+import { DriveFile } from '../../../models/entities/drive-file';
+import { DriveFiles } from '../../../models';
-export default (file: IDriveFile) => ({
+export default (file: DriveFile) => ({
type: 'Document',
- mediaType: file.contentType,
- url: getDriveFileUrl(file)
+ mediaType: file.type,
+ url: DriveFiles.getPublicUrl(file)
});
diff --git a/src/remote/activitypub/renderer/emoji.ts b/src/remote/activitypub/renderer/emoji.ts
index 1a05b4e89e..947a96df37 100644
--- a/src/remote/activitypub/renderer/emoji.ts
+++ b/src/remote/activitypub/renderer/emoji.ts
@@ -1,7 +1,7 @@
-import { IEmoji } from '../../../models/emoji';
import config from '../../../config';
+import { Emoji } from '../../../models/entities/emoji';
-export default (emoji: IEmoji) => ({
+export default (emoji: Emoji) => ({
id: `${config.url}/emojis/${emoji.name}`,
type: 'Emoji',
name: `:${emoji.name}:`,
diff --git a/src/remote/activitypub/renderer/follow-user.ts b/src/remote/activitypub/renderer/follow-user.ts
index 9a488d392b..9446be3c86 100644
--- a/src/remote/activitypub/renderer/follow-user.ts
+++ b/src/remote/activitypub/renderer/follow-user.ts
@@ -1,16 +1,12 @@
import config from '../../../config';
-import * as mongo from 'mongodb';
-import User, { isLocalUser } from '../../../models/user';
+import { Users } from '../../../models';
+import { User } from '../../../models/entities/user';
/**
* Convert (local|remote)(Follower|Followee)ID to URL
* @param id Follower|Followee ID
*/
-export default async function renderFollowUser(id: mongo.ObjectID): Promise<any> {
-
- const user = await User.findOne({
- _id: id
- });
-
- return isLocalUser(user) ? `${config.url}/users/${user._id}` : user.uri;
+export default async function renderFollowUser(id: User['id']): Promise<any> {
+ const user = await Users.findOne(id);
+ return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri;
}
diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts
index 98d4cdd020..400b15ec7b 100644
--- a/src/remote/activitypub/renderer/follow.ts
+++ b/src/remote/activitypub/renderer/follow.ts
@@ -1,11 +1,12 @@
import config from '../../../config';
-import { IUser, isLocalUser } from '../../../models/user';
+import { User } from '../../../models/entities/user';
+import { Users } from '../../../models';
-export default (follower: IUser, followee: IUser, requestId?: string) => {
+export default (follower: User, followee: User, requestId?: string) => {
const follow = {
type: 'Follow',
- actor: isLocalUser(follower) ? `${config.url}/users/${follower._id}` : follower.uri,
- object: isLocalUser(followee) ? `${config.url}/users/${followee._id}` : followee.uri
+ actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
+ object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri
} as any;
if (requestId) follow.id = requestId;
diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts
index ec637b9521..ce98f98c62 100644
--- a/src/remote/activitypub/renderer/image.ts
+++ b/src/remote/activitypub/renderer/image.ts
@@ -1,8 +1,8 @@
-import { IDriveFile } from '../../../models/drive-file';
-import getDriveFileUrl from '../../../misc/get-drive-file-url';
+import { DriveFile } from '../../../models/entities/drive-file';
+import { DriveFiles } from '../../../models';
-export default (file: IDriveFile) => ({
+export default (file: DriveFile) => ({
type: 'Image',
- url: getDriveFileUrl(file),
- sensitive: file.metadata.isSensitive
+ url: DriveFiles.getPublicUrl(file),
+ sensitive: file.isSensitive
});
diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts
index 0d5e52557c..fb5975a6c4 100644
--- a/src/remote/activitypub/renderer/key.ts
+++ b/src/remote/activitypub/renderer/key.ts
@@ -1,10 +1,11 @@
+import { createPublicKey } from 'crypto';
import config from '../../../config';
-import { extractPublic } from '../../../crypto_key';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
+import { UserKeypair } from '../../../models/entities/user-keypair';
-export default (user: ILocalUser) => ({
- id: `${config.url}/users/${user._id}/publickey`,
+export default (user: ILocalUser, key: UserKeypair) => ({
+ id: `${config.url}/users/${user.id}/publickey`,
type: 'Key',
- owner: `${config.url}/users/${user._id}`,
- publicKeyPem: extractPublic(user.keypair)
+ owner: `${config.url}/users/${user.id}`,
+ publicKeyPem: createPublicKey(key.keyPem)
});
diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts
index 523cb4f1ad..01f10ec0a9 100644
--- a/src/remote/activitypub/renderer/like.ts
+++ b/src/remote/activitypub/renderer/like.ts
@@ -1,10 +1,10 @@
import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
-import { INote } from '../../../models/note';
+import { ILocalUser } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
-export default (user: ILocalUser, note: INote, reaction: string) => ({
+export default (user: ILocalUser, note: Note, reaction: string) => ({
type: 'Like',
- actor: `${config.url}/users/${user._id}`,
- object: note.uri ? note.uri : `${config.url}/notes/${note._id}`,
+ actor: `${config.url}/users/${user.id}`,
+ object: note.uri ? note.uri : `${config.url}/notes/${note.id}`,
_misskey_reaction: reaction
});
diff --git a/src/remote/activitypub/renderer/mention.ts b/src/remote/activitypub/renderer/mention.ts
index 8d12e6d8bf..889be5d85d 100644
--- a/src/remote/activitypub/renderer/mention.ts
+++ b/src/remote/activitypub/renderer/mention.ts
@@ -1,8 +1,9 @@
-import { IUser, isRemoteUser } from '../../../models/user';
import config from '../../../config';
+import { User, ILocalUser } from '../../../models/entities/user';
+import { Users } from '../../../models';
-export default (mention: IUser) => ({
+export default (mention: User) => ({
type: 'Mention',
- href: isRemoteUser(mention) ? mention.uri : `${config.url}/@${mention.username}`,
- name: isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${mention.username}`,
+ href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/@${(mention as ILocalUser).username}`,
+ name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`,
});
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 8b349526e1..5b36366b28 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -3,29 +3,27 @@ import renderHashtag from './hashtag';
import renderMention from './mention';
import renderEmoji from './emoji';
import config from '../../../config';
-import DriveFile, { IDriveFile } from '../../../models/drive-file';
-import Note, { INote } from '../../../models/note';
-import User from '../../../models/user';
import toHtml from '../misc/get-note-html';
-import Emoji, { IEmoji } from '../../../models/emoji';
+import { Note, IMentionedRemoteUsers } from '../../../models/entities/note';
+import { DriveFile } from '../../../models/entities/drive-file';
+import { DriveFiles, Notes, Users, Emojis, Polls } from '../../../models';
+import { In } from 'typeorm';
+import { Emoji } from '../../../models/entities/emoji';
+import { Poll } from '../../../models/entities/poll';
-export default async function renderNote(note: INote, dive = true): Promise<any> {
- const promisedFiles: Promise<IDriveFile[]> = note.fileIds
- ? DriveFile.find({ _id: { $in: note.fileIds } })
+export default async function renderNote(note: Note, dive = true): Promise<any> {
+ const promisedFiles: Promise<DriveFile[]> = note.fileIds.length > 1
+ ? DriveFiles.find({ id: In(note.fileIds) })
: Promise.resolve([]);
let inReplyTo;
- let inReplyToNote: INote;
+ let inReplyToNote: Note;
if (note.replyId) {
- inReplyToNote = await Note.findOne({
- _id: note.replyId,
- });
+ inReplyToNote = await Notes.findOne(note.replyId);
if (inReplyToNote !== null) {
- const inReplyToUser = await User.findOne({
- _id: inReplyToNote.userId,
- });
+ const inReplyToUser = await Users.findOne(inReplyToNote.userId);
if (inReplyToUser !== null) {
if (inReplyToNote.uri) {
@@ -34,7 +32,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
if (dive) {
inReplyTo = await renderNote(inReplyToNote, false);
} else {
- inReplyTo = `${config.url}/notes/${inReplyToNote._id}`;
+ inReplyTo = `${config.url}/notes/${inReplyToNote.id}`;
}
}
}
@@ -46,24 +44,20 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
let quote;
if (note.renoteId) {
- const renote = await Note.findOne({
- _id: note.renoteId,
- });
+ const renote = await Notes.findOne(note.renoteId);
if (renote) {
- quote = renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`;
+ quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`;
}
}
- const user = await User.findOne({
- _id: note.userId
+ const user = await Users.findOne({
+ id: note.userId
});
- const attributedTo = `${config.url}/users/${user._id}`;
+ const attributedTo = `${config.url}/users/${user.id}`;
- const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0
- ? note.mentionedRemoteUsers.map(x => x.uri)
- : [];
+ const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
let to: string[] = [];
let cc: string[] = [];
@@ -81,10 +75,8 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
to = mentions;
}
- const mentionedUsers = note.mentions ? await User.find({
- _id: {
- $in: note.mentions
- }
+ const mentionedUsers = note.mentions.length > 0 ? await Users.find({
+ id: In(note.mentions)
}) : [];
const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag));
@@ -93,23 +85,28 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
const files = await promisedFiles;
let text = note.text;
+ let poll: Poll;
+
+ if (note.hasPoll) {
+ poll = await Polls.findOne({ noteId: note.id });
+ }
let question: string;
- if (note.poll != null) {
+ if (poll) {
if (text == null) text = '';
- const url = `${config.url}/notes/${note._id}`;
+ const url = `${config.url}/notes/${note.id}`;
// TODO: i18n
text += `\n[リモートで結果を表示](${url})`;
- question = `${config.url}/questions/${note._id}`;
+ question = `${config.url}/questions/${note.id}`;
}
let apText = text;
if (apText == null) apText = '';
// Provides choices as text for AP
- if (note.poll != null) {
- const cs = note.poll.choices.map(c => `${c.id}: ${c.text}`);
+ if (poll) {
+ const cs = poll.choices.map((c, i) => `${i}: ${c}`);
apText += '\n----------------------------------------\n';
apText += cs.join('\n');
apText += '\n----------------------------------------\n';
@@ -135,31 +132,25 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
...apemojis,
];
- const {
- choices = [],
- expiresAt = null,
- multiple = false
- } = note.poll || {};
-
- const asPoll = note.poll ? {
+ const asPoll = poll ? {
type: 'Question',
content: toHtml(Object.assign({}, note, {
text: text
})),
_misskey_fallback_content: content,
- [expiresAt && expiresAt < new Date() ? 'closed' : 'endTime']: expiresAt,
- [multiple ? 'anyOf' : 'oneOf']: choices.map(({ text, votes }) => ({
+ [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
+ [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
type: 'Note',
name: text,
replies: {
type: 'Collection',
- totalItems: votes
+ totalItems: poll.votes[i]
}
}))
} : {};
return {
- id: `${config.url}/notes/${note._id}`,
+ id: `${config.url}/notes/${note.id}`,
type: 'Note',
attributedTo,
summary,
@@ -172,17 +163,17 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
cc,
inReplyTo,
attachment: files.map(renderDocument),
- sensitive: files.some(file => file.metadata.isSensitive),
+ sensitive: files.some(file => file.isSensitive),
tag,
...asPoll
};
}
-export async function getEmojis(names: string[]): Promise<IEmoji[]> {
- if (names == null || names.length < 1) return [];
+export async function getEmojis(names: string[]): Promise<Emoji[]> {
+ if (names == null || names.length === 0) return [];
const emojis = await Promise.all(
- names.map(name => Emoji.findOne({
+ names.map(name => Emojis.findOne({
name,
host: null
}))
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index 77e60cd61a..4c6b518eb6 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -1,21 +1,22 @@
import renderImage from './image';
import renderKey from './key';
import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
import { toHtml } from '../../../mfm/toHtml';
import { parse } from '../../../mfm/parse';
-import DriveFile from '../../../models/drive-file';
import { getEmojis } from './note';
import renderEmoji from './emoji';
import { IIdentifier } from '../models/identifier';
import renderHashtag from './hashtag';
+import { DriveFiles, UserServiceLinkings, UserKeypairs } from '../../../models';
-export default async (user: ILocalUser) => {
- const id = `${config.url}/users/${user._id}`;
+export async function renderPerson(user: ILocalUser) {
+ const id = `${config.url}/users/${user.id}`;
- const [avatar, banner] = await Promise.all([
- DriveFile.findOne({ _id: user.avatarId }),
- DriveFile.findOne({ _id: user.bannerId })
+ const [avatar, banner, links] = await Promise.all([
+ DriveFiles.findOne(user.avatarId),
+ DriveFiles.findOne(user.bannerId),
+ UserServiceLinkings.findOne({ userId: user.id })
]);
const attachment: {
@@ -26,41 +27,41 @@ export default async (user: ILocalUser) => {
identifier?: IIdentifier
}[] = [];
- if (user.twitter) {
+ if (links.twitter) {
attachment.push({
type: 'PropertyValue',
name: 'Twitter',
- value: `<a href="https://twitter.com/intent/user?user_id=${user.twitter.userId}" rel="me nofollow noopener" target="_blank"><span>@${user.twitter.screenName}</span></a>`,
+ value: `<a href="https://twitter.com/intent/user?user_id=${links.twitterUserId}" rel="me nofollow noopener" target="_blank"><span>@${links.twitterScreenName}</span></a>`,
identifier: {
type: 'PropertyValue',
name: 'misskey:authentication:twitter',
- value: `${user.twitter.userId}@${user.twitter.screenName}`
+ value: `${links.twitterUserId}@${links.twitterScreenName}`
}
});
}
- if (user.github) {
+ if (links.github) {
attachment.push({
type: 'PropertyValue',
name: 'GitHub',
- value: `<a href="https://github.com/${user.github.login}" rel="me nofollow noopener" target="_blank"><span>@${user.github.login}</span></a>`,
+ value: `<a href="https://github.com/${links.githubLogin}" rel="me nofollow noopener" target="_blank"><span>@${links.githubLogin}</span></a>`,
identifier: {
type: 'PropertyValue',
name: 'misskey:authentication:github',
- value: `${user.github.id}@${user.github.login}`
+ value: `${links.githubId}@${links.githubLogin}`
}
});
}
- if (user.discord) {
+ if (links.discord) {
attachment.push({
type: 'PropertyValue',
name: 'Discord',
- value: `<a href="https://discordapp.com/users/${user.discord.id}" rel="me nofollow noopener" target="_blank"><span>${user.discord.username}#${user.discord.discriminator}</span></a>`,
+ value: `<a href="https://discordapp.com/users/${links.discordId}" rel="me nofollow noopener" target="_blank"><span>${links.discordUsername}#${links.discordDiscriminator}</span></a>`,
identifier: {
type: 'PropertyValue',
name: 'misskey:authentication:discord',
- value: `${user.discord.id}@${user.discord.username}#${user.discord.discriminator}`
+ value: `${links.discordId}@${links.discordUsername}#${links.discordDiscriminator}`
}
});
}
@@ -75,6 +76,10 @@ export default async (user: ILocalUser) => {
...hashtagTags,
];
+ const keypair = await UserKeypairs.findOne({
+ userId: user.id
+ });
+
return {
type: user.isBot ? 'Service' : 'Person',
id,
@@ -93,8 +98,8 @@ export default async (user: ILocalUser) => {
image: user.bannerId && renderImage(banner),
tag,
manuallyApprovesFollowers: user.isLocked,
- publicKey: renderKey(user),
+ publicKey: renderKey(user, keypair),
isCat: user.isCat,
attachment: attachment.length ? attachment : undefined
};
-};
+}
diff --git a/src/remote/activitypub/renderer/question.ts b/src/remote/activitypub/renderer/question.ts
index cf0bf387c8..6ade10d1bf 100644
--- a/src/remote/activitypub/renderer/question.ts
+++ b/src/remote/activitypub/renderer/question.ts
@@ -1,19 +1,20 @@
import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
-import { INote } from '../../../models/note';
+import { ILocalUser } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
+import { Poll } from '../../../models/entities/poll';
-export default async function renderQuestion(user: ILocalUser, note: INote) {
+export default async function renderQuestion(user: ILocalUser, note: Note, poll: Poll) {
const question = {
type: 'Question',
- id: `${config.url}/questions/${note._id}`,
- actor: `${config.url}/users/${user._id}`,
+ id: `${config.url}/questions/${note.id}`,
+ actor: `${config.url}/users/${user.id}`,
content: note.text || '',
- [note.poll.multiple ? 'anyOf' : 'oneOf']: note.poll.choices.map(c => ({
- name: c.text,
- _misskey_votes: c.votes,
+ [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
+ name: text,
+ _misskey_votes: poll.votes[i],
replies: {
type: 'Collection',
- totalItems: c.votes
+ totalItems: poll.votes[i]
}
}))
};
diff --git a/src/remote/activitypub/renderer/reject.ts b/src/remote/activitypub/renderer/reject.ts
index 6d7d23708a..c4e0ba0d0a 100644
--- a/src/remote/activitypub/renderer/reject.ts
+++ b/src/remote/activitypub/renderer/reject.ts
@@ -1,8 +1,8 @@
import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
export default (object: any, user: ILocalUser) => ({
type: 'Reject',
- actor: `${config.url}/users/${user._id}`,
+ actor: `${config.url}/users/${user.id}`,
object
});
diff --git a/src/remote/activitypub/renderer/remove.ts b/src/remote/activitypub/renderer/remove.ts
index ed840be751..1b9a6b8c05 100644
--- a/src/remote/activitypub/renderer/remove.ts
+++ b/src/remote/activitypub/renderer/remove.ts
@@ -1,9 +1,9 @@
import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
export default (user: ILocalUser, target: any, object: any) => ({
type: 'Remove',
- actor: `${config.url}/users/${user._id}`,
+ actor: `${config.url}/users/${user.id}`,
target,
object
});
diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts
index dbcf5732be..2ff6b61b90 100644
--- a/src/remote/activitypub/renderer/undo.ts
+++ b/src/remote/activitypub/renderer/undo.ts
@@ -1,8 +1,8 @@
import config from '../../../config';
-import { ILocalUser, IUser } from '../../../models/user';
+import { ILocalUser, User } from '../../../models/entities/user';
-export default (object: any, user: ILocalUser | IUser) => ({
+export default (object: any, user: ILocalUser | User) => ({
type: 'Undo',
- actor: `${config.url}/users/${user._id}`,
+ actor: `${config.url}/users/${user.id}`,
object
});
diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts
index cf9acc9acb..c1d5ba29b2 100644
--- a/src/remote/activitypub/renderer/update.ts
+++ b/src/remote/activitypub/renderer/update.ts
@@ -1,10 +1,10 @@
import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
export default (object: any, user: ILocalUser) => {
const activity = {
- id: `${config.url}/users/${user._id}#updates/${new Date().getTime()}`,
- actor: `${config.url}/users/${user._id}`,
+ id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`,
+ actor: `${config.url}/users/${user.id}`,
type: 'Update',
to: [ 'https://www.w3.org/ns/activitystreams#Public' ],
object
diff --git a/src/remote/activitypub/renderer/vote.ts b/src/remote/activitypub/renderer/vote.ts
index 014b76765b..8929c03460 100644
--- a/src/remote/activitypub/renderer/vote.ts
+++ b/src/remote/activitypub/renderer/vote.ts
@@ -1,22 +1,23 @@
import config from '../../../config';
-import { INote } from '../../../models/note';
-import { IRemoteUser, ILocalUser } from '../../../models/user';
-import { IPollVote } from '../../../models/poll-vote';
+import { Note } from '../../../models/entities/note';
+import { IRemoteUser, ILocalUser } from '../../../models/entities/user';
+import { PollVote } from '../../../models/entities/poll-vote';
+import { Poll } from '../../../models/entities/poll';
-export default async function renderVote(user: ILocalUser, vote: IPollVote, pollNote: INote, pollOwner: IRemoteUser): Promise<any> {
+export default async function renderVote(user: ILocalUser, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser): Promise<any> {
return {
- id: `${config.url}/users/${user._id}#votes/${vote._id}/activity`,
- actor: `${config.url}/users/${user._id}`,
+ id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`,
+ actor: `${config.url}/users/${user.id}`,
type: 'Create',
to: [pollOwner.uri],
published: new Date().toISOString(),
object: {
- id: `${config.url}/users/${user._id}#votes/${vote._id}`,
+ id: `${config.url}/users/${user.id}#votes/${vote.id}`,
type: 'Note',
- attributedTo: `${config.url}/users/${user._id}`,
+ attributedTo: `${config.url}/users/${user.id}`,
to: [pollOwner.uri],
- inReplyTo: pollNote.uri,
- name: pollNote.poll.choices.find(x => x.id === vote.choice).text
+ inReplyTo: note.uri,
+ name: poll.choices[vote.choice]
}
};
}
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
index 08dd7a6ba9..a089ed371c 100644
--- a/src/remote/activitypub/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -7,10 +7,11 @@ import * as promiseAny from 'promise-any';
import { toUnicode } from 'punycode';
import config from '../../config';
-import { ILocalUser } from '../../models/user';
+import { ILocalUser } from '../../models/entities/user';
import { publishApLogStream } from '../../services/stream';
import { apLogger } from './logger';
-import Instance from '../../models/instance';
+import { UserKeypairs } from '../../models';
+import fetchMeta from '../../misc/fetch-meta';
export const logger = apLogger.createSubLogger('deliver');
@@ -23,8 +24,8 @@ export default async (user: ILocalUser, url: string, object: any) => {
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
- const instance = await Instance.findOne({ host: toUnicode(host) });
- if (instance && instance.isBlocked) return;
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(toUnicode(host))) return;
const data = JSON.stringify(object);
@@ -35,6 +36,10 @@ export default async (user: ILocalUser, url: string, object: any) => {
const addr = await resolveAddr(hostname);
if (!addr) return;
+ const keypair = await UserKeypairs.findOne({
+ userId: user.id
+ });
+
const _ = new Promise((resolve, reject) => {
const req = request({
protocol,
@@ -62,8 +67,8 @@ export default async (user: ILocalUser, url: string, object: any) => {
sign(req, {
authorizationHeaderName: 'Signature',
- key: user.keypair,
- keyId: `${config.url}/users/${user._id}/publickey`,
+ key: keypair.keyPem,
+ keyId: `${config.url}/users/${user.id}/publickey`,
headers: ['date', 'host', 'digest']
});
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index 05152993e4..e8d0be638a 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -64,7 +64,7 @@ export default class Resolver {
json: true
});
- if (object === null || (
+ if (object == null || (
Array.isArray(object['@context']) ?
!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
index 400293da89..be846ab279 100644
--- a/src/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -1,20 +1,21 @@
import { toUnicode, toASCII } from 'punycode';
-import User, { IUser, IRemoteUser } from '../models/user';
import webFinger from './webfinger';
import config from '../config';
import { createPerson, updatePerson } from './activitypub/models/person';
import { URL } from 'url';
import { remoteLogger } from './logger';
import chalk from 'chalk';
+import { User, IRemoteUser } from '../models/entities/user';
+import { Users } from '../models';
const logger = remoteLogger.createSubLogger('resolve-user');
-export default async (username: string, _host: string, option?: any, resync?: boolean): Promise<IUser> => {
+export default async (username: string, _host: string, option?: any, resync = false): Promise<User> => {
const usernameLower = username.toLowerCase();
if (_host == null) {
logger.info(`return local user: ${usernameLower}`);
- return await User.findOne({ usernameLower, host: null });
+ return await Users.findOne({ usernameLower, host: null });
}
const configHostAscii = toASCII(config.host).toLowerCase();
@@ -25,14 +26,14 @@ export default async (username: string, _host: string, option?: any, resync?: bo
if (configHost == host) {
logger.info(`return local user: ${usernameLower}`);
- return await User.findOne({ usernameLower, host: null });
+ return await Users.findOne({ usernameLower, host: null });
}
- const user = await User.findOne({ usernameLower, host }, option);
+ const user = await Users.findOne({ usernameLower, host }, option);
const acctLower = `${usernameLower}@${hostAscii}`;
- if (user === null) {
+ if (user == null) {
const self = await resolveSelf(acctLower);
logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
@@ -54,13 +55,11 @@ export default async (username: string, _host: string, option?: any, resync?: bo
throw new Error(`Invalied uri`);
}
- await User.update({
+ await Users.update({
usernameLower,
host: host
}, {
- $set: {
- uri: self.href
- }
+ uri: self.href
});
} else {
logger.info(`uri is fine: ${acctLower}`);
@@ -69,7 +68,7 @@ export default async (username: string, _host: string, option?: any, resync?: bo
await updatePerson(self.href);
logger.info(`return resynced remote user: ${acctLower}`);
- return await User.findOne({ uri: self.href });
+ return await Users.findOne({ uri: self.href });
}
logger.info(`return existing remote user: ${acctLower}`);
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index df5f5b141d..3b39977d47 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -1,15 +1,11 @@
-import { ObjectID } from 'mongodb';
import * as Router from 'koa-router';
import * as json from 'koa-json-body';
import * as httpSignature from 'http-signature';
import { renderActivity } from '../remote/activitypub/renderer';
-import Note from '../models/note';
-import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
-import Emoji from '../models/emoji';
import renderNote from '../remote/activitypub/renderer/note';
import renderKey from '../remote/activitypub/renderer/key';
-import renderPerson from '../remote/activitypub/renderer/person';
+import { renderPerson } from '../remote/activitypub/renderer/person';
import renderEmoji from '../remote/activitypub/renderer/emoji';
import Outbox, { packActivity } from './activitypub/outbox';
import Followers from './activitypub/followers';
@@ -18,6 +14,9 @@ import Featured from './activitypub/featured';
import renderQuestion from '../remote/activitypub/renderer/question';
import { inbox as processInbox } from '../queue';
import { isSelfHost } from '../misc/convert-host';
+import { Notes, Users, Emojis, UserKeypairs, Polls } from '../models';
+import { ILocalUser, User } from '../models/entities/user';
+import { In } from 'typeorm';
// Init router
const router = new Router();
@@ -64,25 +63,20 @@ router.post('/users/:user/inbox', json(), inbox);
router.get('/notes/:note', async (ctx, next) => {
if (!isActivityPubReq(ctx)) return await next();
- if (!ObjectID.isValid(ctx.params.note)) {
- ctx.status = 404;
- return;
- }
-
- const note = await Note.findOne({
- _id: new ObjectID(ctx.params.note),
- visibility: { $in: ['public', 'home'] },
- localOnly: { $ne: true }
+ const note = await Notes.findOne({
+ id: ctx.params.note,
+ visibility: In(['public', 'home']),
+ localOnly: false
});
- if (note === null) {
+ if (note == null) {
ctx.status = 404;
return;
}
// リモートだったらリダイレクト
- if (note._user.host != null) {
- if (note.uri == null || isSelfHost(note._user.host)) {
+ if (note.userHost != null) {
+ if (note.uri == null || isSelfHost(note.userHost)) {
ctx.status = 500;
return;
}
@@ -97,19 +91,14 @@ router.get('/notes/:note', async (ctx, next) => {
// note activity
router.get('/notes/:note/activity', async ctx => {
- if (!ObjectID.isValid(ctx.params.note)) {
- ctx.status = 404;
- return;
- }
-
- const note = await Note.findOne({
- _id: new ObjectID(ctx.params.note),
- '_user.host': null,
- visibility: { $in: ['public', 'home'] },
- localOnly: { $ne: true }
+ const note = await Notes.findOne({
+ id: ctx.params.note,
+ userHost: null,
+ visibility: In(['public', 'home']),
+ localOnly: false
});
- if (note === null) {
+ if (note == null) {
ctx.status = 404;
return;
}
@@ -121,32 +110,23 @@ router.get('/notes/:note/activity', async ctx => {
// question
router.get('/questions/:question', async (ctx, next) => {
- if (!ObjectID.isValid(ctx.params.question)) {
- ctx.status = 404;
- return;
- }
-
- const poll = await Note.findOne({
- _id: new ObjectID(ctx.params.question),
- '_user.host': null,
- visibility: { $in: ['public', 'home'] },
- localOnly: { $ne: true },
- poll: {
- $exists: true,
- $ne: null
- },
+ const pollNote = await Notes.findOne({
+ id: ctx.params.question,
+ userHost: null,
+ visibility: In(['public', 'home']),
+ localOnly: false,
+ hasPoll: true
});
- if (poll === null) {
+ if (pollNote == null) {
ctx.status = 404;
return;
}
- const user = await User.findOne({
- _id: poll.userId
- });
+ const user = await Users.findOne(pollNote.userId);
+ const poll = await Polls.findOne({ noteId: pollNote.id });
- ctx.body = renderActivity(await renderQuestion(user as ILocalUser, poll));
+ ctx.body = renderActivity(await renderQuestion(user as ILocalUser, pollNote, poll));
setResponseType(ctx);
});
@@ -164,25 +144,24 @@ router.get('/users/:user/collections/featured', Featured);
// publickey
router.get('/users/:user/publickey', async ctx => {
- if (!ObjectID.isValid(ctx.params.user)) {
- ctx.status = 404;
- return;
- }
-
- const userId = new ObjectID(ctx.params.user);
+ const userId = ctx.params.user;
- const user = await User.findOne({
- _id: userId,
+ const user = await Users.findOne({
+ id: userId,
host: null
});
- if (user === null) {
+ if (user == null) {
ctx.status = 404;
return;
}
- if (isLocalUser(user)) {
- ctx.body = renderActivity(renderKey(user));
+ const keypair = await UserKeypairs.findOne({
+ userId: user.id
+ });
+
+ if (Users.isLocalUser(user)) {
+ ctx.body = renderActivity(renderKey(user, keypair));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
} else {
@@ -191,8 +170,8 @@ router.get('/users/:user/publickey', async ctx => {
});
// user
-async function userInfo(ctx: Router.IRouterContext, user: IUser) {
- if (user === null) {
+async function userInfo(ctx: Router.IRouterContext, user: User) {
+ if (user == null) {
ctx.status = 404;
return;
}
@@ -205,15 +184,10 @@ async function userInfo(ctx: Router.IRouterContext, user: IUser) {
router.get('/users/:user', async (ctx, next) => {
if (!isActivityPubReq(ctx)) return await next();
- if (!ObjectID.isValid(ctx.params.user)) {
- ctx.status = 404;
- return;
- }
-
- const userId = new ObjectID(ctx.params.user);
+ const userId = ctx.params.user;
- const user = await User.findOne({
- _id: userId,
+ const user = await Users.findOne({
+ id: userId,
host: null
});
@@ -223,7 +197,7 @@ router.get('/users/:user', async (ctx, next) => {
router.get('/@:user', async (ctx, next) => {
if (!isActivityPubReq(ctx)) return await next();
- const user = await User.findOne({
+ const user = await Users.findOne({
usernameLower: ctx.params.user.toLowerCase(),
host: null
});
@@ -234,12 +208,12 @@ router.get('/@:user', async (ctx, next) => {
// emoji
router.get('/emojis/:emoji', async ctx => {
- const emoji = await Emoji.findOne({
+ const emoji = await Emojis.findOne({
host: null,
name: ctx.params.emoji
});
- if (emoji === null) {
+ if (emoji == null) {
ctx.status = 404;
return;
}
diff --git a/src/server/activitypub/featured.ts b/src/server/activitypub/featured.ts
index fc6150902b..f43312d79a 100644
--- a/src/server/activitypub/featured.ts
+++ b/src/server/activitypub/featured.ts
@@ -1,35 +1,28 @@
-import { ObjectID } from 'mongodb';
import * as Router from 'koa-router';
import config from '../../config';
-import User from '../../models/user';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import { setResponseType } from '../activitypub';
-import Note from '../../models/note';
import renderNote from '../../remote/activitypub/renderer/note';
+import { Users, Notes, UserNotePinings } from '../../models';
export default async (ctx: Router.IRouterContext) => {
- if (!ObjectID.isValid(ctx.params.user)) {
- ctx.status = 404;
- return;
- }
-
- const userId = new ObjectID(ctx.params.user);
+ const userId = ctx.params.user;
// Verify user
- const user = await User.findOne({
- _id: userId,
+ const user = await Users.findOne({
+ id: userId,
host: null
});
- if (user === null) {
+ if (user == null) {
ctx.status = 404;
return;
}
- const pinnedNoteIds = user.pinnedNoteIds || [];
+ const pinings = await UserNotePinings.find({ userId: user.id });
- const pinnedNotes = await Promise.all(pinnedNoteIds.filter(ObjectID.isValid).map(id => Note.findOne({ _id: id })));
+ const pinnedNotes = await Promise.all(pinings.map(pining => Notes.findOne(pining.noteId)));
const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note)));
diff --git a/src/server/activitypub/followers.ts b/src/server/activitypub/followers.ts
index 002576b2e7..62c54399ed 100644
--- a/src/server/activitypub/followers.ts
+++ b/src/server/activitypub/followers.ts
@@ -1,24 +1,18 @@
-import { ObjectID } from 'mongodb';
import * as Router from 'koa-router';
import config from '../../config';
import $ from 'cafy';
-import ID, { transform } from '../../misc/cafy-id';
-import User from '../../models/user';
-import Following from '../../models/following';
+import { ID } from '../../misc/cafy-id';
import * as url from '../../prelude/url';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
import renderFollowUser from '../../remote/activitypub/renderer/follow-user';
import { setResponseType } from '../activitypub';
+import { Users, Followings } from '../../models';
+import { LessThan } from 'typeorm';
export default async (ctx: Router.IRouterContext) => {
- if (!ObjectID.isValid(ctx.params.user)) {
- ctx.status = 404;
- return;
- }
-
- const userId = new ObjectID(ctx.params.user);
+ const userId = ctx.params.user;
// Get 'cursor' parameter
const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor);
@@ -34,12 +28,12 @@ export default async (ctx: Router.IRouterContext) => {
}
// Verify user
- const user = await User.findOne({
- _id: userId,
+ const user = await Users.findOne({
+ id: userId,
host: null
});
- if (user === null) {
+ if (user == null) {
ctx.status = 404;
return;
}
@@ -49,22 +43,20 @@ export default async (ctx: Router.IRouterContext) => {
if (page) {
const query = {
- followeeId: user._id
+ followeeId: user.id
} as any;
// カーソルが指定されている場合
if (cursor) {
- query._id = {
- $lt: transform(cursor)
- };
+ query.id = LessThan(cursor);
}
// Get followers
- const followings = await Following
- .find(query, {
- limit: limit + 1,
- sort: { _id: -1 }
- });
+ const followings = await Followings.find({
+ where: query,
+ take: limit + 1,
+ order: { id: -1 }
+ });
// 「次のページ」があるかどうか
const inStock = followings.length === limit + 1;
@@ -80,7 +72,7 @@ export default async (ctx: Router.IRouterContext) => {
null,
inStock ? `${partOf}?${url.query({
page: 'true',
- cursor: followings[followings.length - 1]._id.toHexString()
+ cursor: followings[followings.length - 1].id
})}` : null
);
diff --git a/src/server/activitypub/following.ts b/src/server/activitypub/following.ts
index 0d7486f68a..4894aac1f8 100644
--- a/src/server/activitypub/following.ts
+++ b/src/server/activitypub/following.ts
@@ -1,24 +1,19 @@
-import { ObjectID } from 'mongodb';
import * as Router from 'koa-router';
import config from '../../config';
import $ from 'cafy';
-import ID, { transform } from '../../misc/cafy-id';
-import User from '../../models/user';
-import Following from '../../models/following';
+import { ID } from '../../misc/cafy-id';
import * as url from '../../prelude/url';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
import renderFollowUser from '../../remote/activitypub/renderer/follow-user';
import { setResponseType } from '../activitypub';
+import { Users, Followings } from '../../models';
+import { LessThan, FindConditions } from 'typeorm';
+import { Following } from '../../models/entities/following';
export default async (ctx: Router.IRouterContext) => {
- if (!ObjectID.isValid(ctx.params.user)) {
- ctx.status = 404;
- return;
- }
-
- const userId = new ObjectID(ctx.params.user);
+ const userId = ctx.params.user;
// Get 'cursor' parameter
const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor);
@@ -34,12 +29,12 @@ export default async (ctx: Router.IRouterContext) => {
}
// Verify user
- const user = await User.findOne({
- _id: userId,
+ const user = await Users.findOne({
+ id: userId,
host: null
});
- if (user === null) {
+ if (user == null) {
ctx.status = 404;
return;
}
@@ -49,22 +44,20 @@ export default async (ctx: Router.IRouterContext) => {
if (page) {
const query = {
- followerId: user._id
- } as any;
+ followerId: user.id
+ } as FindConditions<Following>;
// カーソルが指定されている場合
if (cursor) {
- query._id = {
- $lt: transform(cursor)
- };
+ query.id = LessThan(cursor);
}
// Get followings
- const followings = await Following
- .find(query, {
- limit: limit + 1,
- sort: { _id: -1 }
- });
+ const followings = await Followings.find({
+ where: query,
+ take: limit + 1,
+ order: { id: -1 }
+ });
// 「次のページ」があるかどうか
const inStock = followings.length === limit + 1;
@@ -80,7 +73,7 @@ export default async (ctx: Router.IRouterContext) => {
null,
inStock ? `${partOf}?${url.query({
page: 'true',
- cursor: followings[followings.length - 1]._id.toHexString()
+ cursor: followings[followings.length - 1].id
})}` : null
);
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index ff8f884b19..377f43c986 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -1,28 +1,23 @@
-import { ObjectID } from 'mongodb';
import * as Router from 'koa-router';
import config from '../../config';
import $ from 'cafy';
-import ID, { transform } from '../../misc/cafy-id';
-import User from '../../models/user';
+import { ID } from '../../misc/cafy-id';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
import { setResponseType } from '../activitypub';
-
-import Note, { INote } from '../../models/note';
import renderNote from '../../remote/activitypub/renderer/note';
import renderCreate from '../../remote/activitypub/renderer/create';
import renderAnnounce from '../../remote/activitypub/renderer/announce';
import { countIf } from '../../prelude/array';
import * as url from '../../prelude/url';
+import { Users, Notes } from '../../models';
+import { makePaginationQuery } from '../api/common/make-pagination-query';
+import { Brackets } from 'typeorm';
+import { Note } from '../../models/entities/note';
export default async (ctx: Router.IRouterContext) => {
- if (!ObjectID.isValid(ctx.params.user)) {
- ctx.status = 404;
- return;
- }
-
- const userId = new ObjectID(ctx.params.user);
+ const userId = ctx.params.user;
// Get 'sinceId' parameter
const [sinceId, sinceIdErr] = $.optional.type(ID).get(ctx.request.query.since_id);
@@ -41,12 +36,12 @@ export default async (ctx: Router.IRouterContext) => {
}
// Verify user
- const user = await User.findOne({
- _id: userId,
+ const user = await Users.findOne({
+ id: userId,
host: null
});
- if (user === null) {
+ if (user == null) {
ctx.status = 404;
return;
}
@@ -55,34 +50,15 @@ export default async (ctx: Router.IRouterContext) => {
const partOf = `${config.url}/users/${userId}/outbox`;
if (page) {
- //#region Construct query
- const sort = {
- _id: -1
- };
-
- const query = {
- userId: user._id,
- visibility: { $in: ['public', 'home'] },
- localOnly: { $ne: true }
- } as any;
-
- if (sinceId) {
- sort._id = 1;
- query._id = {
- $gt: transform(sinceId)
- };
- } else if (untilId) {
- query._id = {
- $lt: transform(untilId)
- };
- }
- //#endregion
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId)
+ .andWhere('note.userId = :userId', { userId: user.id })
+ .andWhere(new Brackets(qb => { qb
+ .where(`note.visibility = 'public'`)
+ .orWhere(`note.visibility = 'home'`);
+ }))
+ .andWhere('note.localOnly = FALSE');
- const notes = await Note
- .find(query, {
- limit: limit,
- sort: sort
- });
+ const notes = await query.take(limit).getMany();
if (sinceId) notes.reverse();
@@ -96,11 +72,11 @@ export default async (ctx: Router.IRouterContext) => {
user.notesCount, activities, partOf,
notes.length ? `${partOf}?${url.query({
page: 'true',
- since_id: notes[0]._id.toHexString()
+ since_id: notes[0].id
})}` : null,
notes.length ? `${partOf}?${url.query({
page: 'true',
- until_id: notes[notes.length - 1]._id.toHexString()
+ until_id: notes[notes.length - 1].id
})}` : null
);
@@ -123,10 +99,10 @@ export default async (ctx: Router.IRouterContext) => {
* Pack Create<Note> or Announce Activity
* @param note Note
*/
-export async function packActivity(note: INote): Promise<object> {
- if (note.renoteId && note.text == null && note.poll == null && (note.fileIds == null || note.fileIds.length == 0)) {
- const renote = await Note.findOne(note.renoteId);
- return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`, note);
+export async function packActivity(note: Note): Promise<object> {
+ if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) {
+ const renote = await Notes.findOne(note.renoteId);
+ return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note);
}
return renderCreate(await renderNote(note, false), note);
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts
index 7781b87c88..e293e3fed0 100644
--- a/src/server/api/authenticate.ts
+++ b/src/server/api/authenticate.ts
@@ -1,37 +1,37 @@
-import App, { IApp } from '../../models/app';
-import { default as User, IUser } from '../../models/user';
-import AccessToken from '../../models/access-token';
import isNativeToken from './common/is-native-token';
+import { User } from '../../models/entities/user';
+import { App } from '../../models/entities/app';
+import { Users, AccessTokens, Apps } from '../../models';
-export default async (token: string): Promise<[IUser, IApp]> => {
+export default async (token: string): Promise<[User, App]> => {
if (token == null) {
return [null, null];
}
if (isNativeToken(token)) {
// Fetch user
- const user: IUser = await User
+ const user = await Users
.findOne({ token });
- if (user === null) {
+ if (user == null) {
throw 'user not found';
}
return [user, null];
} else {
- const accessToken = await AccessToken.findOne({
+ const accessToken = await AccessTokens.findOne({
hash: token.toLowerCase()
});
- if (accessToken === null) {
+ if (accessToken == null) {
throw 'invalid signature';
}
- const app = await App
- .findOne({ _id: accessToken.appId });
+ const app = await Apps
+ .findOne(accessToken.appId);
- const user = await User
- .findOne({ _id: accessToken.userId });
+ const user = await Users
+ .findOne(accessToken.userId);
return [user, app];
}
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
index 89a44b3c65..885c622667 100644
--- a/src/server/api/call.ts
+++ b/src/server/api/call.ts
@@ -1,10 +1,10 @@
import { performance } from 'perf_hooks';
import limiter from './limiter';
-import { IUser } from '../../models/user';
-import { IApp } from '../../models/app';
+import { User } from '../../models/entities/user';
import endpoints from './endpoints';
import { ApiError } from './error';
import { apiLogger } from './logger';
+import { App } from '../../models/entities/app';
const accessDenied = {
message: 'Access denied.',
@@ -12,7 +12,7 @@ const accessDenied = {
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e'
};
-export default async (endpoint: string, user: IUser, app: IApp, data: any, file?: any) => {
+export default async (endpoint: string, user: User, app: App, data: any, file?: any) => {
const isSecure = user != null && app == null;
const ep = endpoints.find(e => e.name === endpoint);
diff --git a/src/server/api/common/generate-mute-query.ts b/src/server/api/common/generate-mute-query.ts
new file mode 100644
index 0000000000..090c14eb83
--- /dev/null
+++ b/src/server/api/common/generate-mute-query.ts
@@ -0,0 +1,36 @@
+import { User } from '../../../models/entities/user';
+import { Mutings } from '../../../models';
+import { SelectQueryBuilder, Brackets } from 'typeorm';
+
+export function generateMuteQuery(q: SelectQueryBuilder<any>, me: User) {
+ const mutingQuery = Mutings.createQueryBuilder('muting')
+ .select('muting.muteeId')
+ .where('muting.muterId = :muterId', { muterId: me.id });
+
+ // 投稿の作者をミュートしていない かつ
+ // 投稿の返信先の作者をミュートしていない かつ
+ // 投稿の引用元の作者をミュートしていない
+ q
+ .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
+ .andWhere(new Brackets(qb => { qb
+ .where(`note.replyUserId IS NULL`)
+ .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
+ }))
+ .andWhere(new Brackets(qb => { qb
+ .where(`note.renoteUserId IS NULL`)
+ .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
+ }));
+
+ q.setParameters(mutingQuery.getParameters());
+}
+
+export function generateMuteQueryForUsers(q: SelectQueryBuilder<any>, me: User) {
+ const mutingQuery = Mutings.createQueryBuilder('muting')
+ .select('muting.muteeId')
+ .where('muting.muterId = :muterId', { muterId: me.id });
+
+ q
+ .andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
+
+ q.setParameters(mutingQuery.getParameters());
+}
diff --git a/src/server/api/common/generate-native-user-token.ts b/src/server/api/common/generate-native-user-token.ts
index 2082b89a5a..92f8a3a0e8 100644
--- a/src/server/api/common/generate-native-user-token.ts
+++ b/src/server/api/common/generate-native-user-token.ts
@@ -1,3 +1,3 @@
import rndstr from 'rndstr';
-export default () => `!${rndstr('a-zA-Z0-9', 32)}`;
+export default () => `!${rndstr('a-zA-Z0-9', 31)}`;
diff --git a/src/server/api/common/generate-visibility-query.ts b/src/server/api/common/generate-visibility-query.ts
new file mode 100644
index 0000000000..2807dc99dc
--- /dev/null
+++ b/src/server/api/common/generate-visibility-query.ts
@@ -0,0 +1,40 @@
+import { User } from '../../../models/entities/user';
+import { Followings } from '../../../models';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+
+export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: User) {
+ if (me == null) {
+ q.andWhere(new Brackets(qb => { qb
+ .where(`note.visibility = 'public'`)
+ .orWhere(`note.visibility = 'home'`);
+ }));
+ } else {
+ const followingQuery = Followings.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
+
+ q.andWhere(new Brackets(qb => { qb
+ // 公開投稿である
+ .where(new Brackets(qb => { qb
+ .where(`note.visibility = 'public'`)
+ .orWhere(`note.visibility = 'home'`);
+ }))
+ // または 自分自身
+ .orWhere('note.userId = :userId1', { userId1: me.id })
+ // または 自分宛て
+ .orWhere(':userId2 = ANY(note.visibleUserIds)', { userId2: me.id })
+ .orWhere(new Brackets(qb => { qb
+ // または フォロワー宛ての投稿であり、
+ .where('note.visibility = \'followers\'')
+ .andWhere(new Brackets(qb => { qb
+ // 自分がフォロワーである
+ .where(`note.userId IN (${ followingQuery.getQuery() })`)
+ // または 自分の投稿へのリプライ
+ .orWhere('note.replyUserId = :userId3', { userId3: me.id });
+ }));
+ }));
+ }));
+
+ q.setParameters(followingQuery.getParameters());
+ }
+}
diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts
deleted file mode 100644
index 876aa399f7..0000000000
--- a/src/server/api/common/get-friends.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as mongodb from 'mongodb';
-import Following from '../../../models/following';
-
-export const getFriendIds = async (me: mongodb.ObjectID, includeMe = true) => {
- // Fetch relation to other users who the I follows
- // SELECT followee
- const followings = await Following
- .find({
- followerId: me
- }, {
- fields: {
- followeeId: true
- }
- });
-
- // ID list of other users who the I follows
- const myfollowingIds = followings.map(following => following.followeeId);
-
- if (includeMe) {
- myfollowingIds.push(me);
- }
-
- return myfollowingIds;
-};
-
-export const getFriends = async (me: mongodb.ObjectID, includeMe = true, remoteOnly = false) => {
- const q: any = remoteOnly ? {
- followerId: me,
- '_followee.host': { $ne: null }
- } : {
- followerId: me
- };
- // Fetch relation to other users who the I follows
- const followings = await Following
- .find(q);
-
- // ID list of other users who the I follows
- const myfollowings = followings.map(following => ({
- id: following.followeeId
- }));
-
- if (includeMe) {
- myfollowings.push({
- id: me
- });
- }
-
- return myfollowings;
-};
diff --git a/src/server/api/common/get-hide-users.ts b/src/server/api/common/get-hide-users.ts
deleted file mode 100644
index 3cdf806751..0000000000
--- a/src/server/api/common/get-hide-users.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as mongo from 'mongodb';
-import Mute from '../../../models/mute';
-import User, { IUser } from '../../../models/user';
-import { unique } from '../../../prelude/array';
-
-export async function getHideUserIds(me: IUser) {
- return await getHideUserIdsById(me ? me._id : null);
-}
-
-export async function getHideUserIdsById(meId?: mongo.ObjectID) {
- const [suspended, muted] = await Promise.all([
- User.find({
- isSuspended: true
- }, {
- fields: {
- _id: true
- }
- }),
- meId ? Mute.find({
- muterId: meId
- }) : Promise.resolve([])
- ]);
-
- return unique(suspended.map(user => user._id).concat(muted.map(mute => mute.muteeId)));
-}
diff --git a/src/server/api/common/getters.ts b/src/server/api/common/getters.ts
index 7a72e6489a..b720840ebb 100644
--- a/src/server/api/common/getters.ts
+++ b/src/server/api/common/getters.ts
@@ -1,18 +1,15 @@
-import * as mongo from 'mongodb';
-import Note from '../../../models/note';
-import User, { isRemoteUser, isLocalUser } from '../../../models/user';
import { IdentifiableError } from '../../../misc/identifiable-error';
+import { User } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
+import { Notes, Users } from '../../../models';
/**
* Get note for API processing
*/
-export async function getNote(noteId: mongo.ObjectID) {
- const note = await Note.findOne({
- _id: noteId,
- deletedAt: { $exists: false }
- });
+export async function getNote(noteId: Note['id']) {
+ const note = await Notes.findOne(noteId);
- if (note === null) {
+ if (note == null) {
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
}
@@ -22,23 +19,10 @@ export async function getNote(noteId: mongo.ObjectID) {
/**
* Get user for API processing
*/
-export async function getUser(userId: mongo.ObjectID) {
- const user = await User.findOne({
- _id: userId,
- $or: [{
- isDeleted: { $exists: false }
- }, {
- isDeleted: false
- }]
- }, {
- fields: {
- data: false,
- profile: false,
- clientSettings: false
- }
- });
+export async function getUser(userId: User['id']) {
+ const user = await Users.findOne(userId);
- if (user === null) {
+ if (user == null) {
throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.');
}
@@ -48,10 +32,10 @@ export async function getUser(userId: mongo.ObjectID) {
/**
* Get remote user for API processing
*/
-export async function getRemoteUser(userId: mongo.ObjectID) {
+export async function getRemoteUser(userId: User['id']) {
const user = await getUser(userId);
- if (!isRemoteUser(user)) {
+ if (!Users.isRemoteUser(user)) {
throw 'user is not a remote user';
}
@@ -61,10 +45,10 @@ export async function getRemoteUser(userId: mongo.ObjectID) {
/**
* Get local user for API processing
*/
-export async function getLocalUser(userId: mongo.ObjectID) {
+export async function getLocalUser(userId: User['id']) {
const user = await getUser(userId);
- if (!isLocalUser(user)) {
+ if (!Users.isLocalUser(user)) {
throw 'user is not a local user';
}
diff --git a/src/server/api/common/make-pagination-query.ts b/src/server/api/common/make-pagination-query.ts
new file mode 100644
index 0000000000..0c859a4f8d
--- /dev/null
+++ b/src/server/api/common/make-pagination-query.ts
@@ -0,0 +1,28 @@
+import { SelectQueryBuilder } from 'typeorm';
+
+export function makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId: string, untilId: string, sinceDate?: number, untilDate?: number) {
+ if (sinceId && untilId) {
+ q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
+ q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
+ q.orderBy(`${q.alias}.id`, 'DESC');
+ } else if (sinceId) {
+ q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
+ q.orderBy(`${q.alias}.id`, 'ASC');
+ } else if (untilId) {
+ q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
+ q.orderBy(`${q.alias}.id`, 'DESC');
+ } else if (sinceDate && untilDate) {
+ q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) });
+ q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) });
+ q.orderBy(`${q.alias}.createdAt`, 'DESC');
+ } else if (sinceDate) {
+ q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) });
+ q.orderBy(`${q.alias}.createdAt`, 'ASC');
+ } else if (untilDate) {
+ q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) });
+ q.orderBy(`${q.alias}.createdAt`, 'DESC');
+ } else {
+ q.orderBy(`${q.alias}.id`, 'DESC');
+ }
+ return q;
+}
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
index 9f1e7e6ab4..2cb5a1f87f 100644
--- a/src/server/api/common/read-messaging-message.ts
+++ b/src/server/api/common/read-messaging-message.ts
@@ -1,77 +1,43 @@
-import * as mongo from 'mongodb';
-import isObjectId from '../../../misc/is-objectid';
-import Message from '../../../models/messaging-message';
-import { IMessagingMessage as IMessage } from '../../../models/messaging-message';
import { publishMainStream } from '../../../services/stream';
import { publishMessagingStream } from '../../../services/stream';
import { publishMessagingIndexStream } from '../../../services/stream';
-import User from '../../../models/user';
+import { User } from '../../../models/entities/user';
+import { MessagingMessage } from '../../../models/entities/messaging-message';
+import { MessagingMessages } from '../../../models';
+import { In } from 'typeorm';
/**
* Mark messages as read
*/
-export default (
- user: string | mongo.ObjectID,
- otherparty: string | mongo.ObjectID,
- message: string | string[] | IMessage | IMessage[] | mongo.ObjectID | mongo.ObjectID[]
-) => new Promise<any>(async (resolve, reject) => {
-
- const userId = isObjectId(user)
- ? user
- : new mongo.ObjectID(user);
-
- const otherpartyId = isObjectId(otherparty)
- ? otherparty
- : new mongo.ObjectID(otherparty);
-
- const ids: mongo.ObjectID[] = Array.isArray(message)
- ? isObjectId(message[0])
- ? (message as mongo.ObjectID[])
- : typeof message[0] === 'string'
- ? (message as string[]).map(m => new mongo.ObjectID(m))
- : (message as IMessage[]).map(m => m._id)
- : isObjectId(message)
- ? [(message as mongo.ObjectID)]
- : typeof message === 'string'
- ? [new mongo.ObjectID(message)]
- : [(message as IMessage)._id];
+export default async (
+ userId: User['id'],
+ otherpartyId: User['id'],
+ messageIds: MessagingMessage['id'][]
+) => {
+ if (messageIds.length === 0) return;
// Update documents
- await Message.update({
- _id: { $in: ids },
+ await MessagingMessages.update({
+ id: In(messageIds),
userId: otherpartyId,
recipientId: userId,
isRead: false
}, {
- $set: {
- isRead: true
- }
- }, {
- multi: true
- });
+ isRead: true
+ });
// Publish event
- publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString()));
- publishMessagingIndexStream(userId, 'read', ids.map(id => id.toString()));
+ publishMessagingStream(otherpartyId, userId, 'read', messageIds);
+ publishMessagingIndexStream(userId, 'read', messageIds);
// Calc count of my unread messages
- const count = await Message
- .count({
- recipientId: userId,
- isRead: false
- }, {
- limit: 1
- });
+ const count = await MessagingMessages.count({
+ recipientId: userId,
+ isRead: false
+ });
if (count == 0) {
- // Update flag
- User.update({ _id: userId }, {
- $set: {
- hasUnreadMessagingMessage: false
- }
- });
-
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, 'readAllMessagingMessages');
}
-});
+};
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index 4361305119..c8d43ba286 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -1,72 +1,38 @@
-import * as mongo from 'mongodb';
-import isObjectId from '../../../misc/is-objectid';
-import { default as Notification, INotification } from '../../../models/notification';
import { publishMainStream } from '../../../services/stream';
-import Mute from '../../../models/mute';
-import User from '../../../models/user';
+import { User } from '../../../models/entities/user';
+import { Notification } from '../../../models/entities/notification';
+import { Mutings, Notifications } from '../../../models';
+import { In, Not } from 'typeorm';
/**
* Mark notifications as read
*/
-export default (
- user: string | mongo.ObjectID,
- message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[]
-) => new Promise<any>(async (resolve, reject) => {
-
- const userId = isObjectId(user)
- ? user
- : new mongo.ObjectID(user);
-
- const ids: mongo.ObjectID[] = Array.isArray(message)
- ? isObjectId(message[0])
- ? (message as mongo.ObjectID[])
- : typeof message[0] === 'string'
- ? (message as string[]).map(m => new mongo.ObjectID(m))
- : (message as INotification[]).map(m => m._id)
- : isObjectId(message)
- ? [(message as mongo.ObjectID)]
- : typeof message === 'string'
- ? [new mongo.ObjectID(message)]
- : [(message as INotification)._id];
-
- const mute = await Mute.find({
+export async function readNotification(
+ userId: User['id'],
+ notificationIds: Notification['id'][]
+) {
+ const mute = await Mutings.find({
muterId: userId
});
const mutedUserIds = mute.map(m => m.muteeId);
// Update documents
- await Notification.update({
- _id: { $in: ids },
+ await Notifications.update({
+ id: In(notificationIds),
isRead: false
}, {
- $set: {
- isRead: true
- }
- }, {
- multi: true
- });
+ isRead: true
+ });
// Calc count of my unread notifications
- const count = await Notification
- .count({
- notifieeId: userId,
- notifierId: {
- $nin: mutedUserIds
- },
- isRead: false
- }, {
- limit: 1
- });
-
- if (count == 0) {
- // Update flag
- User.update({ _id: userId }, {
- $set: {
- hasUnreadNotification: false
- }
- });
+ const count = await Notifications.count({
+ notifieeId: userId,
+ ...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
+ isRead: false
+ });
+ if (count === 0) {
// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
publishMainStream(userId, 'readAllNotifications');
}
-});
+}
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
index 84cad3a935..0f4ee4ca11 100644
--- a/src/server/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -1,7 +1,7 @@
import * as Koa from 'koa';
import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
export default function(ctx: Koa.BaseContext, user: ILocalUser, redirect = false) {
if (redirect) {
diff --git a/src/server/api/define.ts b/src/server/api/define.ts
index f2fababc32..a18419bcf6 100644
--- a/src/server/api/define.ts
+++ b/src/server/api/define.ts
@@ -1,8 +1,8 @@
import * as fs from 'fs';
-import { ILocalUser } from '../../models/user';
-import { IApp } from '../../models/app';
+import { ILocalUser } from '../../models/entities/user';
import { IEndpointMeta } from './endpoints';
import { ApiError } from './error';
+import { App } from '../../models/entities/app';
type Params<T extends IEndpointMeta> = {
[P in keyof T['params']]: T['params'][P]['transform'] extends Function
@@ -12,8 +12,8 @@ type Params<T extends IEndpointMeta> = {
export type Response = Record<string, any> | void;
-export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T>, user: ILocalUser, app: IApp, file?: any, cleanup?: Function) => Promise<Response>): (params: any, user: ILocalUser, app: IApp, file?: any) => Promise<any> {
- return (params: any, user: ILocalUser, app: IApp, file?: any) => {
+export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T>, user: ILocalUser, app: App, file?: any, cleanup?: Function) => Promise<Response>): (params: any, user: ILocalUser, app: App, file?: any) => Promise<any> {
+ return (params: any, user: ILocalUser, app: App, file?: any) => {
function cleanup() {
fs.unlink(file.path, () => {});
}
diff --git a/src/server/api/endpoints/admin/abuse-user-reports.ts b/src/server/api/endpoints/admin/abuse-user-reports.ts
index d9fe3429ce..5c5a734c1d 100644
--- a/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Report, { packMany } from '../../../../models/abuse-user-report';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
+import { AbuseUserReports } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
tags: ['admin'],
@@ -17,37 +18,18 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
}
};
export default define(meta, async (ps) => {
- const sort = {
- _id: -1
- };
- const query = {} as any;
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- }
+ const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId);
- const reports = await Report
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const reports = await query.take(ps.limit).getMany();
- return await packMany(reports);
+ return await AbuseUserReports.packMany(reports);
});
diff --git a/src/server/api/endpoints/admin/drive/files.ts b/src/server/api/endpoints/admin/drive/files.ts
index 8ed417a429..1ccabc92d9 100644
--- a/src/server/api/endpoints/admin/drive/files.ts
+++ b/src/server/api/endpoints/admin/drive/files.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import File, { packMany } from '../../../../../models/drive-file';
import define from '../../../define';
import { fallback } from '../../../../../prelude/symbol';
+import { DriveFiles } from '../../../../../models';
export const meta = {
tags: ['admin'],
@@ -41,27 +41,25 @@ export const meta = {
};
const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
- '+createdAt': { uploadDate: -1 },
- '-createdAt': { uploadDate: 1 },
- '+size': { length: -1 },
- '-size': { length: 1 },
- [fallback]: { _id: -1 }
+ '+createdAt': { createdAt: -1 },
+ '-createdAt': { createdAt: 1 },
+ '+size': { size: -1 },
+ '-size': { size: 1 },
+ [fallback]: { id: -1 }
};
export default define(meta, async (ps, me) => {
- const q = {
- 'metadata.deletedAt': { $exists: false },
- } as any;
+ const q = {} as any;
- if (ps.origin == 'local') q['metadata._user.host'] = null;
- if (ps.origin == 'remote') q['metadata._user.host'] = { $ne: null };
+ if (ps.origin == 'local') q['userHost'] = null;
+ if (ps.origin == 'remote') q['userHost'] = { $ne: null };
- const files = await File
- .find(q, {
- limit: ps.limit,
- sort: sort[ps.sort] || sort[fallback],
- skip: ps.offset
- });
+ const files = await DriveFiles.find({
+ where: q,
+ take: ps.limit,
+ order: sort[ps.sort] || sort[fallback],
+ skip: ps.offset
+ });
- return await packMany(files, { detail: true, withUser: true, self: true });
+ return await DriveFiles.packMany(files, { detail: true, withUser: true, self: true });
});
diff --git a/src/server/api/endpoints/admin/drive/show-file.ts b/src/server/api/endpoints/admin/drive/show-file.ts
index 405b6d44ce..a2b6c158f0 100644
--- a/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/src/server/api/endpoints/admin/drive/show-file.ts
@@ -1,8 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
-import DriveFile from '../../../../../models/drive-file';
import { ApiError } from '../../../error';
+import { DriveFiles } from '../../../../../models';
export const meta = {
tags: ['admin'],
@@ -13,7 +13,6 @@ export const meta = {
params: {
fileId: {
validator: $.type(ID),
- transform: transform,
},
},
@@ -27,9 +26,7 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const file = await DriveFile.findOne({
- _id: ps.fileId
- });
+ const file = await DriveFiles.findOne(ps.fileId);
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts
index c126c8380f..c26e8dd04d 100644
--- a/src/server/api/endpoints/admin/emoji/add.ts
+++ b/src/server/api/endpoints/admin/emoji/add.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import Emoji from '../../../../../models/emoji';
import define from '../../../define';
import { detectUrlMine } from '../../../../../misc/detect-url-mine';
+import { Emojis } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
export const meta = {
desc: {
@@ -32,7 +33,8 @@ export const meta = {
export default define(meta, async (ps) => {
const type = await detectUrlMine(ps.url);
- const emoji = await Emoji.insert({
+ const emoji = await Emojis.save({
+ id: genId(),
updatedAt: new Date(),
name: ps.name,
host: null,
@@ -42,6 +44,6 @@ export default define(meta, async (ps) => {
});
return {
- id: emoji._id
+ id: emoji.id
};
});
diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts
index 954f8f96c6..07174723b9 100644
--- a/src/server/api/endpoints/admin/emoji/list.ts
+++ b/src/server/api/endpoints/admin/emoji/list.ts
@@ -1,6 +1,6 @@
import $ from 'cafy';
-import Emoji from '../../../../../models/emoji';
import define from '../../../define';
+import { Emojis } from '../../../../../models';
export const meta = {
desc: {
@@ -21,12 +21,12 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const emojis = await Emoji.find({
+ const emojis = await Emojis.find({
host: ps.host
});
return emojis.map(e => ({
- id: e._id,
+ id: e.id,
name: e.name,
aliases: e.aliases,
host: e.host,
diff --git a/src/server/api/endpoints/admin/emoji/remove.ts b/src/server/api/endpoints/admin/emoji/remove.ts
index 4c69dffbae..316834b884 100644
--- a/src/server/api/endpoints/admin/emoji/remove.ts
+++ b/src/server/api/endpoints/admin/emoji/remove.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import Emoji from '../../../../../models/emoji';
import define from '../../../define';
-import ID from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
+import { Emojis } from '../../../../../models';
export const meta = {
desc: {
@@ -21,13 +21,9 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const emoji = await Emoji.findOne({
- _id: ps.id
- });
+ const emoji = await Emojis.findOne(ps.id);
if (emoji == null) throw new Error('emoji not found');
- await Emoji.remove({ _id: emoji._id });
-
- return;
+ await Emojis.delete(emoji.id);
});
diff --git a/src/server/api/endpoints/admin/emoji/update.ts b/src/server/api/endpoints/admin/emoji/update.ts
index 8b1c07be9e..48b4a4ee23 100644
--- a/src/server/api/endpoints/admin/emoji/update.ts
+++ b/src/server/api/endpoints/admin/emoji/update.ts
@@ -1,8 +1,8 @@
import $ from 'cafy';
-import Emoji from '../../../../../models/emoji';
import define from '../../../define';
-import ID from '../../../../../misc/cafy-id';
import { detectUrlMine } from '../../../../../misc/detect-url-mine';
+import { ID } from '../../../../../misc/cafy-id';
+import { Emojis } from '../../../../../models';
export const meta = {
desc: {
@@ -34,23 +34,17 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const emoji = await Emoji.findOne({
- _id: ps.id
- });
+ const emoji = await Emojis.findOne(ps.id);
if (emoji == null) throw new Error('emoji not found');
const type = await detectUrlMine(ps.url);
- await Emoji.update({ _id: emoji._id }, {
- $set: {
- updatedAt: new Date(),
- name: ps.name,
- aliases: ps.aliases,
- url: ps.url,
- type,
- }
+ await Emojis.update(emoji.id, {
+ updatedAt: new Date(),
+ name: ps.name,
+ aliases: ps.aliases,
+ url: ps.url,
+ type,
});
-
- return;
});
diff --git a/src/server/api/endpoints/admin/federation/remove-all-following.ts b/src/server/api/endpoints/admin/federation/remove-all-following.ts
index 98afdfc2a5..fca76e7086 100644
--- a/src/server/api/endpoints/admin/federation/remove-all-following.ts
+++ b/src/server/api/endpoints/admin/federation/remove-all-following.ts
@@ -1,8 +1,7 @@
import $ from 'cafy';
import define from '../../../define';
-import Following from '../../../../../models/following';
-import User from '../../../../../models/user';
import deleteFollowing from '../../../../../services/following/delete';
+import { Followings, Users } from '../../../../../models';
export const meta = {
tags: ['admin'],
@@ -18,13 +17,13 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const followings = await Following.find({
- '_follower.host': ps.host
+ const followings = await Followings.find({
+ followerHost: ps.host
});
const pairs = await Promise.all(followings.map(f => Promise.all([
- User.findOne({ _id: f.followerId }),
- User.findOne({ _id: f.followeeId })
+ Users.findOne(f.followerId),
+ Users.findOne(f.followeeId)
])));
for (const pair of pairs) {
diff --git a/src/server/api/endpoints/admin/federation/update-instance.ts b/src/server/api/endpoints/admin/federation/update-instance.ts
index 0d127b53b3..d1abe95a5b 100644
--- a/src/server/api/endpoints/admin/federation/update-instance.ts
+++ b/src/server/api/endpoints/admin/federation/update-instance.ts
@@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../../define';
-import Instance from '../../../../../models/instance';
+import { Instances } from '../../../../../models';
export const meta = {
tags: ['admin'],
@@ -13,10 +13,6 @@ export const meta = {
validator: $.str
},
- isBlocked: {
- validator: $.bool
- },
-
isClosed: {
validator: $.bool
},
@@ -24,18 +20,13 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const instance = await Instance.findOne({ host: ps.host });
+ const instance = await Instances.findOne({ host: ps.host });
if (instance == null) {
throw new Error('instance not found');
}
- Instance.update({ host: ps.host }, {
- $set: {
- isBlocked: ps.isBlocked,
- isMarkedAsClosed: ps.isClosed
- }
+ Instances.update({ host: ps.host }, {
+ isMarkedAsClosed: ps.isClosed
});
-
- return;
});
diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts
index 28aa301957..4e264feef6 100644
--- a/src/server/api/endpoints/admin/invite.ts
+++ b/src/server/api/endpoints/admin/invite.ts
@@ -1,6 +1,7 @@
import rndstr from 'rndstr';
-import RegistrationTicket from '../../../../models/registration-tickets';
import define from '../../define';
+import { RegistrationTickets } from '../../../../models';
+import { genId } from '../../../../misc/gen-id';
export const meta = {
desc: {
@@ -18,7 +19,8 @@ export const meta = {
export default define(meta, async (ps) => {
const code = rndstr({ length: 5, chars: '0-9' });
- await RegistrationTicket.insert({
+ await RegistrationTickets.save({
+ id: genId(),
createdAt: new Date(),
code: code
});
diff --git a/src/server/api/endpoints/admin/logs.ts b/src/server/api/endpoints/admin/logs.ts
index 805a42b9e0..eee56a3939 100644
--- a/src/server/api/endpoints/admin/logs.ts
+++ b/src/server/api/endpoints/admin/logs.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
-import Log from '../../../../models/log';
+import { Logs } from '../../../../models';
+import { Brackets } from 'typeorm';
export const meta = {
tags: ['admin'],
@@ -27,41 +28,44 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const sort = {
- _id: -1
- };
- const query = {} as any;
+ const query = Logs.createQueryBuilder('log');
+
+ if (ps.level) query.andWhere('log.level = :level', { level: ps.level });
- if (ps.level) query.level = ps.level;
if (ps.domain) {
- for (const d of ps.domain.split(' ')) {
- const qs: any[] = [];
- let i = 0;
- for (const sd of (d.startsWith('-') ? d.substr(1) : d).split('.')) {
- qs.push({
- [`domain.${i}`]: d.startsWith('-') ? { $ne: sd } : sd
- });
- i++;
- }
- if (d.startsWith('-')) {
- if (query['$and'] == null) query['$and'] = [];
- query['$and'].push({
- $and: qs
- });
- } else {
- if (query['$or'] == null) query['$or'] = [];
- query['$or'].push({
- $and: qs
- });
- }
+ const whiteDomains = ps.domain.split(' ').filter(x => !x.startsWith('-'));
+ const blackDomains = ps.domain.split(' ').filter(x => x.startsWith('-'));
+
+ if (whiteDomains.length > 0) {
+ query.andWhere(new Brackets(qb => {
+ for (const whiteDomain of whiteDomains) {
+ let i = 0;
+ for (const subDomain of whiteDomain.split('.')) {
+ const p = `whiteSubDomain_${subDomain}_${i}`;
+ // SQL is 1 based, so we need '+ 1'
+ qb.orWhere(`log.domain[${i + 1}] = :${p}`, { [p]: subDomain });
+ i++;
+ }
+ }
+ }));
+ }
+
+ if (blackDomains.length > 0) {
+ query.andWhere(new Brackets(qb => {
+ for (const blackDomain of blackDomains) {
+ let i = 0;
+ for (const subDomain of blackDomain.split('.')) {
+ const p = `blackSubDomain_${subDomain}_${i}`;
+ // SQL is 1 based, so we need '+ 1'
+ qb.andWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain });
+ i++;
+ }
+ }
+ }));
}
}
- const logs = await Log
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const logs = await query.take(ps.limit).getMany();
return logs;
});
diff --git a/src/server/api/endpoints/admin/moderators/add.ts b/src/server/api/endpoints/admin/moderators/add.ts
index 2271bcd1a9..a15f0a17a2 100644
--- a/src/server/api/endpoints/admin/moderators/add.ts
+++ b/src/server/api/endpoints/admin/moderators/add.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
-import User from '../../../../../models/user';
+import { Users } from '../../../../../models';
export const meta = {
desc: {
@@ -17,7 +17,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID'
@@ -27,21 +26,13 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const user = await User.findOne({
- _id: ps.userId
- });
+ const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
}
- await User.update({
- _id: user._id
- }, {
- $set: {
- isModerator: true
- }
+ await Users.update(user.id, {
+ isModerator: true
});
-
- return;
});
diff --git a/src/server/api/endpoints/admin/moderators/remove.ts b/src/server/api/endpoints/admin/moderators/remove.ts
index 84143d3e35..209cf0814f 100644
--- a/src/server/api/endpoints/admin/moderators/remove.ts
+++ b/src/server/api/endpoints/admin/moderators/remove.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
-import User from '../../../../../models/user';
+import { Users } from '../../../../../models';
export const meta = {
desc: {
@@ -17,7 +17,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID'
@@ -27,21 +26,13 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const user = await User.findOne({
- _id: ps.userId
- });
+ const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
}
- await User.update({
- _id: user._id
- }, {
- $set: {
- isModerator: false
- }
+ await Users.update(user.id, {
+ isModerator: false
});
-
- return;
});
diff --git a/src/server/api/endpoints/admin/remove-abuse-user-report.ts b/src/server/api/endpoints/admin/remove-abuse-user-report.ts
index fa17e2c937..f293c00718 100644
--- a/src/server/api/endpoints/admin/remove-abuse-user-report.ts
+++ b/src/server/api/endpoints/admin/remove-abuse-user-report.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import AbuseUserReport from '../../../../models/abuse-user-report';
+import { AbuseUserReports } from '../../../../models';
export const meta = {
tags: ['admin'],
@@ -12,23 +12,16 @@ export const meta = {
params: {
reportId: {
validator: $.type(ID),
- transform: transform
},
}
};
export default define(meta, async (ps) => {
- const report = await AbuseUserReport.findOne({
- _id: ps.reportId
- });
+ const report = await AbuseUserReports.findOne(ps.reportId);
if (report == null) {
throw new Error('report not found');
}
- await AbuseUserReport.remove({
- _id: report._id
- });
-
- return;
+ await AbuseUserReports.delete(report.id);
});
diff --git a/src/server/api/endpoints/admin/reset-password.ts b/src/server/api/endpoints/admin/reset-password.ts
index 73901d8358..07b8b6d938 100644
--- a/src/server/api/endpoints/admin/reset-password.ts
+++ b/src/server/api/endpoints/admin/reset-password.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import User from '../../../../models/user';
import * as bcrypt from 'bcryptjs';
import rndstr from 'rndstr';
+import { Users } from '../../../../models';
export const meta = {
desc: {
@@ -18,7 +18,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to suspend'
@@ -28,9 +27,7 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const user = await User.findOne({
- _id: ps.userId
- });
+ const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
@@ -45,12 +42,8 @@ export default define(meta, async (ps) => {
// Generate hash of password
const hash = bcrypt.hashSync(passwd);
- await User.findOneAndUpdate({
- _id: user._id
- }, {
- $set: {
- password: hash
- }
+ await Users.update(user.id, {
+ password: hash
});
return {
diff --git a/src/server/api/endpoints/admin/show-user.ts b/src/server/api/endpoints/admin/show-user.ts
index 985f71a873..452125dea0 100644
--- a/src/server/api/endpoints/admin/show-user.ts
+++ b/src/server/api/endpoints/admin/show-user.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
export const meta = {
desc: {
@@ -16,7 +16,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to suspend'
@@ -26,9 +25,7 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const user = await User.findOne({
- _id: ps.userId
- });
+ const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
diff --git a/src/server/api/endpoints/admin/show-users.ts b/src/server/api/endpoints/admin/show-users.ts
index 5feb1b4fd8..73976b9872 100644
--- a/src/server/api/endpoints/admin/show-users.ts
+++ b/src/server/api/endpoints/admin/show-users.ts
@@ -1,7 +1,6 @@
import $ from 'cafy';
-import User, { pack } from '../../../../models/user';
import define from '../../define';
-import { fallback } from '../../../../prelude/symbol';
+import { Users } from '../../../../models';
export const meta = {
tags: ['admin'],
@@ -55,51 +54,38 @@ export const meta = {
}
};
-const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
- '+follower': { followersCount: -1 },
- '-follower': { followersCount: 1 },
- '+createdAt': { createdAt: -1 },
- '-createdAt': { createdAt: 1 },
- '+updatedAt': { updatedAt: -1 },
- '-updatedAt': { updatedAt: 1 },
- [fallback]: { _id: -1 }
-};
-
export default define(meta, async (ps, me) => {
- const q = {
- $and: []
- } as any;
+ const query = Users.createQueryBuilder('user');
+
+ switch (ps.state) {
+ case 'admin': query.where('user.isAdmin = TRUE'); break;
+ case 'moderator': query.where('user.isModerator = TRUE'); break;
+ case 'adminOrModerator': query.where('user.isAdmin = TRUE OR isModerator = TRUE'); break;
+ case 'verified': query.where('user.isVerified = TRUE'); break;
+ case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
+ case 'silenced': query.where('user.isSilenced = TRUE'); break;
+ case 'suspended': query.where('user.isSuspended = TRUE'); break;
+ }
- // state
- q.$and.push(
- ps.state == 'admin' ? { isAdmin: true } :
- ps.state == 'moderator' ? { isModerator: true } :
- ps.state == 'adminOrModerator' ? {
- $or: [{
- isAdmin: true
- }, {
- isModerator: true
- }]
- } :
- ps.state == 'verified' ? { isVerified: true } :
- ps.state == 'silenced' ? { isSilenced: true } :
- ps.state == 'suspended' ? { isSuspended: true } :
- {}
- );
+ switch (ps.origin) {
+ case 'local': query.andWhere('user.host IS NULL'); break;
+ case 'remote': query.andWhere('user.host IS NOT NULL'); break;
+ }
+
+ switch (ps.sort) {
+ case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
+ case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
+ case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
+ case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
+ case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break;
+ case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
+ default: query.orderBy('user.id', 'ASC'); break;
+ }
- // origin
- q.$and.push(
- ps.origin == 'local' ? { host: null } :
- ps.origin == 'remote' ? { host: { $ne: null } } :
- {}
- );
+ query.take(ps.limit);
+ query.skip(ps.offset);
- const users = await User
- .find(q, {
- limit: ps.limit,
- sort: sort[ps.sort] || sort[fallback],
- skip: ps.offset
- });
+ const users = await query.getMany();
- return await Promise.all(users.map(user => pack(user, me, { detail: true })));
+ return await Users.packMany(users, me, { detail: true });
});
diff --git a/src/server/api/endpoints/admin/silence-user.ts b/src/server/api/endpoints/admin/silence-user.ts
index 2557d8de6a..83aa88012a 100644
--- a/src/server/api/endpoints/admin/silence-user.ts
+++ b/src/server/api/endpoints/admin/silence-user.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
export const meta = {
desc: {
@@ -17,7 +17,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to make silence'
@@ -27,9 +26,7 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const user = await User.findOne({
- _id: ps.userId
- });
+ const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
@@ -39,13 +36,7 @@ export default define(meta, async (ps) => {
throw new Error('cannot silence admin');
}
- await User.findOneAndUpdate({
- _id: user._id
- }, {
- $set: {
- isSilenced: true
- }
+ await Users.update(user.id, {
+ isSilenced: true
});
-
- return;
});
diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts
index 0a2d309530..fa4d378708 100644
--- a/src/server/api/endpoints/admin/suspend-user.ts
+++ b/src/server/api/endpoints/admin/suspend-user.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import User, { IUser } from '../../../../models/user';
-import Following from '../../../../models/following';
import deleteFollowing from '../../../../services/following/delete';
+import { Users, Followings } from '../../../../models';
+import { User } from '../../../../models/entities/user';
export const meta = {
desc: {
@@ -19,7 +19,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to suspend'
@@ -29,9 +28,7 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const user = await User.findOne({
- _id: ps.userId
- });
+ const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
@@ -45,27 +42,21 @@ export default define(meta, async (ps) => {
throw new Error('cannot suspend moderator');
}
- await User.findOneAndUpdate({
- _id: user._id
- }, {
- $set: {
- isSuspended: true
- }
+ await Users.update(user.id, {
+ isSuspended: true
});
unFollowAll(user);
-
- return;
});
-async function unFollowAll(follower: IUser) {
- const followings = await Following.find({
- followerId: follower._id
+async function unFollowAll(follower: User) {
+ const followings = await Followings.find({
+ followerId: follower.id
});
for (const following of followings) {
- const followee = await User.findOne({
- _id: following.followeeId
+ const followee = await Users.findOne({
+ id: following.followeeId
});
if (followee == null) {
diff --git a/src/server/api/endpoints/admin/unsilence-user.ts b/src/server/api/endpoints/admin/unsilence-user.ts
index 01bf41aaef..f9b173366b 100644
--- a/src/server/api/endpoints/admin/unsilence-user.ts
+++ b/src/server/api/endpoints/admin/unsilence-user.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
export const meta = {
desc: {
@@ -17,7 +17,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to unsilence'
@@ -27,21 +26,13 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const user = await User.findOne({
- _id: ps.userId
- });
+ const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
}
- await User.findOneAndUpdate({
- _id: user._id
- }, {
- $set: {
- isSilenced: false
- }
+ await Users.update(user.id, {
+ isSilenced: false
});
-
- return;
});
diff --git a/src/server/api/endpoints/admin/unsuspend-user.ts b/src/server/api/endpoints/admin/unsuspend-user.ts
index 5da35f28e6..08dae034d3 100644
--- a/src/server/api/endpoints/admin/unsuspend-user.ts
+++ b/src/server/api/endpoints/admin/unsuspend-user.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
export const meta = {
desc: {
@@ -17,7 +17,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to unsuspend'
@@ -27,21 +26,13 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const user = await User.findOne({
- _id: ps.userId
- });
+ const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
}
- await User.findOneAndUpdate({
- _id: user._id
- }, {
- $set: {
- isSuspended: false
- }
+ await Users.update(user.id, {
+ isSuspended: false
});
-
- return;
});
diff --git a/src/server/api/endpoints/admin/unverify-user.ts b/src/server/api/endpoints/admin/unverify-user.ts
index d3ca05cb39..b215dbf10d 100644
--- a/src/server/api/endpoints/admin/unverify-user.ts
+++ b/src/server/api/endpoints/admin/unverify-user.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
export const meta = {
desc: {
@@ -17,7 +17,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to unverify'
@@ -27,21 +26,13 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const user = await User.findOne({
- _id: ps.userId
- });
+ const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
}
- await User.findOneAndUpdate({
- _id: user._id
- }, {
- $set: {
- isVerified: false
- }
+ await Users.update(user.id, {
+ isVerified: false
});
-
- return;
});
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index f8f7cb5d9a..e242ac71a1 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
-import Meta from '../../../../models/meta';
import define from '../../define';
+import { Metas } from '../../../../models';
+import { Meta } from '../../../../models/entities/meta';
export const meta = {
desc: {
@@ -55,7 +56,7 @@ export const meta = {
}
},
- hidedTags: {
+ hiddenTags: {
validator: $.optional.nullable.arr($.str),
desc: {
'ja-JP': '統計などで無視するハッシュタグ'
@@ -253,27 +254,6 @@ export const meta = {
}
},
- enableExternalUserRecommendation: {
- validator: $.optional.bool,
- desc: {
- 'ja-JP': '外部ユーザーレコメンデーションを有効にする'
- }
- },
-
- externalUserRecommendationEngine: {
- validator: $.optional.nullable.str,
- desc: {
- 'ja-JP': '外部ユーザーレコメンデーションのサードパーティエンジン'
- }
- },
-
- externalUserRecommendationTimeout: {
- validator: $.optional.nullable.num.min(0),
- desc: {
- 'ja-JP': '外部ユーザーレコメンデーションのタイムアウト (ミリ秒)'
- }
- },
-
enableEmail: {
validator: $.optional.bool,
desc: {
@@ -347,7 +327,7 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const set = {} as any;
+ const set = {} as Partial<Meta>;
if (ps.announcements) {
set.announcements = ps.announcements;
@@ -373,8 +353,8 @@ export default define(meta, async (ps) => {
set.useStarForReactionFallback = ps.useStarForReactionFallback;
}
- if (Array.isArray(ps.hidedTags)) {
- set.hidedTags = ps.hidedTags;
+ if (Array.isArray(ps.hiddenTags)) {
+ set.hiddenTags = ps.hiddenTags;
}
if (ps.mascotImageUrl !== undefined) {
@@ -430,11 +410,11 @@ export default define(meta, async (ps) => {
}
if (ps.maintainerName !== undefined) {
- set['maintainer.name'] = ps.maintainerName;
+ set.maintainerName = ps.maintainerName;
}
if (ps.maintainerEmail !== undefined) {
- set['maintainer.email'] = ps.maintainerEmail;
+ set.maintainerEmail = ps.maintainerEmail;
}
if (ps.langs !== undefined) {
@@ -481,18 +461,6 @@ export default define(meta, async (ps) => {
set.discordClientSecret = ps.discordClientSecret;
}
- if (ps.enableExternalUserRecommendation !== undefined) {
- set.enableExternalUserRecommendation = ps.enableExternalUserRecommendation;
- }
-
- if (ps.externalUserRecommendationEngine !== undefined) {
- set.externalUserRecommendationEngine = ps.externalUserRecommendationEngine;
- }
-
- if (ps.externalUserRecommendationTimeout !== undefined) {
- set.externalUserRecommendationTimeout = ps.externalUserRecommendationTimeout;
- }
-
if (ps.enableEmail !== undefined) {
set.enableEmail = ps.enableEmail;
}
@@ -537,9 +505,11 @@ export default define(meta, async (ps) => {
set.swPrivateKey = ps.swPrivateKey;
}
- await Meta.update({}, {
- $set: set
- }, { upsert: true });
+ const meta = await Metas.findOne();
- return;
+ if (meta) {
+ await Metas.update(meta.id, set);
+ } else {
+ await Metas.save(set);
+ }
});
diff --git a/src/server/api/endpoints/admin/update-remote-user.ts b/src/server/api/endpoints/admin/update-remote-user.ts
index a74685912c..0be9047d5a 100644
--- a/src/server/api/endpoints/admin/update-remote-user.ts
+++ b/src/server/api/endpoints/admin/update-remote-user.ts
@@ -1,6 +1,5 @@
-import * as mongo from 'mongodb';
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getRemoteUser } from '../../common/getters';
import { updatePerson } from '../../../../remote/activitypub/models/person';
@@ -19,7 +18,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to update'
@@ -29,11 +27,6 @@ export const meta = {
};
export default define(meta, async (ps) => {
- await updatePersonById(ps.userId);
- return;
-});
-
-async function updatePersonById(userId: mongo.ObjectID) {
- const user = await getRemoteUser(userId);
+ const user = await getRemoteUser(ps.userId);
await updatePerson(user.uri);
-}
+});
diff --git a/src/server/api/endpoints/admin/verify-user.ts b/src/server/api/endpoints/admin/verify-user.ts
index f67b6c3bf0..c1b447a92b 100644
--- a/src/server/api/endpoints/admin/verify-user.ts
+++ b/src/server/api/endpoints/admin/verify-user.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
export const meta = {
desc: {
@@ -17,7 +17,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to verify'
@@ -27,21 +26,13 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const user = await User.findOne({
- _id: ps.userId
- });
+ const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
}
- await User.findOneAndUpdate({
- _id: user._id
- }, {
- $set: {
- isVerified: true
- }
+ await Users.update(user.id, {
+ isVerified: true
});
-
- return;
});
diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts
deleted file mode 100644
index 978e9f64b7..0000000000
--- a/src/server/api/endpoints/aggregation/hashtags.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import Note from '../../../../models/note';
-import define from '../../define';
-import fetchMeta from '../../../../misc/fetch-meta';
-
-export const meta = {
- tags: ['hashtags'],
-
- requireCredential: false,
-};
-
-export default define(meta, async (ps) => {
- const instance = await fetchMeta();
- const hidedTags = instance.hidedTags.map(t => t.toLowerCase());
-
- // 重い
- //const span = 1000 * 60 * 60 * 24 * 7; // 1週間
- const span = 1000 * 60 * 60 * 24; // 1日
-
- //#region 1. 指定期間の内に投稿されたハッシュタグ(とユーザーのペア)を集計
- const data = await Note.aggregate([{
- $match: {
- createdAt: {
- $gt: new Date(Date.now() - span)
- },
- tagsLower: {
- $exists: true,
- $ne: []
- }
- }
- }, {
- $unwind: '$tagsLower'
- }, {
- $group: {
- _id: { tag: '$tagsLower', userId: '$userId' }
- }
- }]) as {
- _id: {
- tag: string;
- userId: any;
- }
- }[];
- //#endregion
-
- if (data.length == 0) {
- return [];
- }
-
- let tags: {
- name: string;
- count: number;
- }[] = [];
-
- // カウント
- for (const x of data.map(x => x._id).filter(x => !hidedTags.includes(x.tag))) {
- const i = tags.findIndex(tag => tag.name == x.tag);
- if (i != -1) {
- tags[i].count++;
- } else {
- tags.push({
- name: x.tag,
- count: 1
- });
- }
- }
-
- // タグを人気順に並べ替え
- tags.sort((a, b) => b.count - a.count);
-
- tags = tags.slice(0, 30);
-
- return tags;
-});
diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts
index 7f4afa1f6e..5b2aaeadbb 100644
--- a/src/server/api/endpoints/ap/show.ts
+++ b/src/server/api/endpoints/ap/show.ts
@@ -1,15 +1,15 @@
import $ from 'cafy';
import define from '../../define';
import config from '../../../../config';
-import * as mongo from 'mongodb';
-import User, { pack as packUser, IUser } from '../../../../models/user';
import { createPerson } from '../../../../remote/activitypub/models/person';
-import Note, { pack as packNote, INote } from '../../../../models/note';
import { createNote } from '../../../../remote/activitypub/models/note';
import Resolver from '../../../../remote/activitypub/resolver';
import { ApiError } from '../../error';
-import Instance from '../../../../models/instance';
import { extractDbHost } from '../../../../misc/convert-host';
+import { Users, Notes } from '../../../../models';
+import { Note } from '../../../../models/entities/note';
+import { User } from '../../../../models/entities/user';
+import fetchMeta from '../../../../misc/fetch-meta';
export const meta = {
tags: ['federation'],
@@ -53,25 +53,40 @@ export default define(meta, async (ps) => {
async function fetchAny(uri: string) {
// URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ
if (uri.startsWith(config.url + '/')) {
- const id = new mongo.ObjectID(uri.split('/').pop());
- const [user, note] = await Promise.all([
- User.findOne({ _id: id }),
- Note.findOne({ _id: id })
- ]);
+ const parts = uri.split('/');
+ const id = parts.pop();
+ const type = parts.pop();
- const packed = await mergePack(user, note);
- if (packed !== null) return packed;
+ if (type === 'notes') {
+ const note = await Notes.findOne(id);
+
+ if (note) {
+ return {
+ type: 'Note',
+ object: await Notes.pack(note, null, { detail: true })
+ };
+ }
+ } else if (type === 'users') {
+ const user = await Users.findOne(id);
+
+ if (user) {
+ return {
+ type: 'User',
+ object: await Users.pack(user, null, { detail: true })
+ };
+ }
+ }
}
// ブロックしてたら中断
- const instance = await Instance.findOne({ host: extractDbHost(uri) });
- if (instance && instance.isBlocked) return null;
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(extractDbHost(uri))) return null;
// URI(AP Object id)としてDB検索
{
const [user, note] = await Promise.all([
- User.findOne({ uri: uri }),
- Note.findOne({ uri: uri })
+ Users.findOne({ uri: uri }),
+ Notes.findOne({ uri: uri })
]);
const packed = await mergePack(user, note);
@@ -86,8 +101,8 @@ async function fetchAny(uri: string) {
// これはDBに存在する可能性があるため再度DB検索
if (uri !== object.id) {
const [user, note] = await Promise.all([
- User.findOne({ uri: object.id }),
- Note.findOne({ uri: object.id })
+ Users.findOne({ uri: object.id }),
+ Notes.findOne({ uri: object.id })
]);
const packed = await mergePack(user, note);
@@ -99,7 +114,7 @@ async function fetchAny(uri: string) {
const user = await createPerson(object.id);
return {
type: 'User',
- object: await packUser(user, null, { detail: true })
+ object: await Users.pack(user, null, { detail: true })
};
}
@@ -107,25 +122,25 @@ async function fetchAny(uri: string) {
const note = await createNote(object.id);
return {
type: 'Note',
- object: await packNote(note, null, { detail: true })
+ object: await Notes.pack(note, null, { detail: true })
};
}
return null;
}
-async function mergePack(user: IUser, note: INote) {
+async function mergePack(user: User, note: Note) {
if (user !== null) {
return {
type: 'User',
- object: await packUser(user, null, { detail: true })
+ object: await Users.pack(user, null, { detail: true })
};
}
if (note !== null) {
return {
type: 'Note',
- object: await packNote(note, null, { detail: true })
+ object: await Notes.pack(note, null, { detail: true })
};
}
diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts
index 67b1b8150a..c7e7e516ad 100644
--- a/src/server/api/endpoints/app/create.ts
+++ b/src/server/api/endpoints/app/create.ts
@@ -1,7 +1,8 @@
import rndstr from 'rndstr';
import $ from 'cafy';
-import App, { pack } from '../../../../models/app';
import define from '../../define';
+import { Apps } from '../../../../models';
+import { genId } from '../../../../misc/gen-id';
export const meta = {
tags: ['app'],
@@ -34,9 +35,10 @@ export default define(meta, async (ps, user) => {
const secret = rndstr('a-zA-Z0-9', 32);
// Create account
- const app = await App.insert({
+ const app = await Apps.save({
+ id: genId(),
createdAt: new Date(),
- userId: user && user._id,
+ userId: user && user.id,
name: ps.name,
description: ps.description,
permission: ps.permission,
@@ -44,7 +46,7 @@ export default define(meta, async (ps, user) => {
secret: secret
});
- return await pack(app, null, {
+ return await Apps.pack(app, null, {
detail: true,
includeSecret: true
});
diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
index f3f5b843b3..ce9baed2ae 100644
--- a/src/server/api/endpoints/app/show.ts
+++ b/src/server/api/endpoints/app/show.ts
@@ -1,8 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import App, { pack } from '../../../../models/app';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
+import { Apps } from '../../../../models';
export const meta = {
tags: ['app'],
@@ -10,7 +10,6 @@ export const meta = {
params: {
appId: {
validator: $.type(ID),
- transform: transform
},
},
@@ -27,14 +26,14 @@ export default define(meta, async (ps, user, app) => {
const isSecure = user != null && app == null;
// Lookup app
- const ap = await App.findOne({ _id: ps.appId });
+ const ap = await Apps.findOne(ps.appId);
- if (ap === null) {
+ if (ap == null) {
throw new ApiError(meta.errors.noSuchApp);
}
- return await pack(ap, user, {
+ return await Apps.pack(ap, user, {
detail: true,
- includeSecret: isSecure && ap.userId.equals(user._id)
+ includeSecret: isSecure && (ap.userId === user.id)
});
});
diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts
index cedf7821fe..21a78011dc 100644
--- a/src/server/api/endpoints/auth/accept.ts
+++ b/src/server/api/endpoints/auth/accept.ts
@@ -1,11 +1,10 @@
import rndstr from 'rndstr';
import * as crypto from 'crypto';
import $ from 'cafy';
-import App from '../../../../models/app';
-import AuthSess from '../../../../models/auth-session';
-import AccessToken from '../../../../models/access-token';
import define from '../../define';
import { ApiError } from '../../error';
+import { AuthSessions, AccessTokens, Apps } from '../../../../models';
+import { genId } from '../../../../misc/gen-id';
export const meta = {
tags: ['auth'],
@@ -31,10 +30,10 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Fetch token
- const session = await AuthSess
+ const session = await AuthSessions
.findOne({ token: ps.token });
- if (session === null) {
+ if (session == null) {
throw new ApiError(meta.errors.noSuchSession);
}
@@ -42,16 +41,14 @@ export default define(meta, async (ps, user) => {
const accessToken = rndstr('a-zA-Z0-9', 32);
// Fetch exist access token
- const exist = await AccessToken.findOne({
+ const exist = await AccessTokens.findOne({
appId: session.appId,
- userId: user._id,
+ userId: user.id,
});
- if (exist === null) {
+ if (exist == null) {
// Lookup app
- const app = await App.findOne({
- _id: session.appId
- });
+ const app = await Apps.findOne(session.appId);
// Generate Hash
const sha256 = crypto.createHash('sha256');
@@ -59,20 +56,19 @@ export default define(meta, async (ps, user) => {
const hash = sha256.digest('hex');
// Insert access token doc
- await AccessToken.insert({
+ await AccessTokens.save({
+ id: genId(),
createdAt: new Date(),
appId: session.appId,
- userId: user._id,
+ userId: user.id,
token: accessToken,
hash: hash
});
}
// Update session
- await AuthSess.update(session._id, {
- $set: {
- userId: user._id
- }
+ await AuthSessions.update(session.id, {
+ userId: user.id
});
return;
diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts
index e12bea7681..5a9bfe6451 100644
--- a/src/server/api/endpoints/auth/session/generate.ts
+++ b/src/server/api/endpoints/auth/session/generate.ts
@@ -1,10 +1,10 @@
import * as uuid from 'uuid';
import $ from 'cafy';
-import App from '../../../../../models/app';
-import AuthSess from '../../../../../models/auth-session';
import config from '../../../../../config';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { Apps, AuthSessions } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
export const meta = {
tags: ['auth'],
@@ -46,7 +46,7 @@ export const meta = {
export default define(meta, async (ps) => {
// Lookup app
- const app = await App.findOne({
+ const app = await Apps.findOne({
secret: ps.appSecret
});
@@ -58,9 +58,10 @@ export default define(meta, async (ps) => {
const token = uuid.v4();
// Create session token document
- const doc = await AuthSess.insert({
+ const doc = await AuthSessions.save({
+ id: genId(),
createdAt: new Date(),
- appId: app._id,
+ appId: app.id,
token: token
});
diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts
index 992e0a499e..e6ecd8b839 100644
--- a/src/server/api/endpoints/auth/session/show.ts
+++ b/src/server/api/endpoints/auth/session/show.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import AuthSess, { pack } from '../../../../../models/auth-session';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { AuthSessions } from '../../../../../models';
export const meta = {
tags: ['auth'],
@@ -29,7 +29,7 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Lookup session
- const session = await AuthSess.findOne({
+ const session = await AuthSessions.findOne({
token: ps.token
});
@@ -37,5 +37,5 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.noSuchSession);
}
- return await pack(session, user);
+ return await AuthSessions.pack(session, user);
});
diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts
index e09e16e658..8524b96f94 100644
--- a/src/server/api/endpoints/auth/session/userkey.ts
+++ b/src/server/api/endpoints/auth/session/userkey.ts
@@ -1,10 +1,7 @@
import $ from 'cafy';
-import App from '../../../../../models/app';
-import AuthSess from '../../../../../models/auth-session';
-import AccessToken from '../../../../../models/access-token';
-import { pack } from '../../../../../models/user';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { Apps, AuthSessions, AccessTokens, Users } from '../../../../../models';
export const meta = {
tags: ['auth'],
@@ -67,7 +64,7 @@ export const meta = {
export default define(meta, async (ps) => {
// Lookup app
- const app = await App.findOne({
+ const app = await Apps.findOne({
secret: ps.appSecret
});
@@ -76,13 +73,12 @@ export default define(meta, async (ps) => {
}
// Fetch token
- const session = await AuthSess
- .findOne({
- token: ps.token,
- appId: app._id
- });
+ const session = await AuthSessions.findOne({
+ token: ps.token,
+ appId: app.id
+ });
- if (session === null) {
+ if (session == null) {
throw new ApiError(meta.errors.noSuchSession);
}
@@ -91,25 +87,17 @@ export default define(meta, async (ps) => {
}
// Lookup access token
- const accessToken = await AccessToken.findOne({
- appId: app._id,
+ const accessToken = await AccessTokens.findOne({
+ appId: app.id,
userId: session.userId
});
// Delete session
-
- /* https://github.com/Automattic/monk/issues/178
- AuthSess.deleteOne({
- _id: session._id
- });
- */
- AuthSess.remove({
- _id: session._id
- });
+ AuthSessions.delete(session.id);
return {
accessToken: accessToken.token,
- user: await pack(session.userId, null, {
+ user: await Users.pack(session.userId, null, {
detail: true
})
};
diff --git a/src/server/api/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts
index e723cb0386..0d6626b2d5 100644
--- a/src/server/api/endpoints/blocking/create.ts
+++ b/src/server/api/endpoints/blocking/create.ts
@@ -1,12 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import * as ms from 'ms';
-import { pack } from '../../../../models/user';
-import Blocking from '../../../../models/blocking';
import create from '../../../../services/blocking/create';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
+import { Blockings, NoteWatchings } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -25,12 +24,11 @@ export const meta = {
requireCredential: true,
- kind: 'following-write',
+ kind: 'write:blocks',
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => {
const blocker = user;
// 自分自身
- if (user._id.equals(ps.userId)) {
+ if (user.id === ps.userId) {
throw new ApiError(meta.errors.blockeeIsYourself);
}
@@ -74,19 +72,22 @@ export default define(meta, async (ps, user) => {
});
// Check if already blocking
- const exist = await Blocking.findOne({
- blockerId: blocker._id,
- blockeeId: blockee._id
+ const exist = await Blockings.findOne({
+ blockerId: blocker.id,
+ blockeeId: blockee.id
});
- if (exist !== null) {
+ if (exist != null) {
throw new ApiError(meta.errors.alreadyBlocking);
}
// Create blocking
await create(blocker, blockee);
- return await pack(blockee._id, user, {
- detail: true
+ NoteWatchings.delete({
+ userId: blocker.id,
+ noteUserId: blockee.id
});
+
+ return await Blockings.pack(blockee.id, user);
});
diff --git a/src/server/api/endpoints/blocking/delete.ts b/src/server/api/endpoints/blocking/delete.ts
index 2a9fdc5e24..e304dca811 100644
--- a/src/server/api/endpoints/blocking/delete.ts
+++ b/src/server/api/endpoints/blocking/delete.ts
@@ -1,12 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import * as ms from 'ms';
-import { pack } from '../../../../models/user';
-import Blocking from '../../../../models/blocking';
import deleteBlocking from '../../../../services/blocking/delete';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
+import { Blockings } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -25,12 +24,11 @@ export const meta = {
requireCredential: true,
- kind: 'following-write',
+ kind: 'write:blocks',
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => {
const blocker = user;
// Check if the blockee is yourself
- if (user._id.equals(ps.userId)) {
+ if (user.id === ps.userId) {
throw new ApiError(meta.errors.blockeeIsYourself);
}
@@ -74,19 +72,17 @@ export default define(meta, async (ps, user) => {
});
// Check not blocking
- const exist = await Blocking.findOne({
- blockerId: blocker._id,
- blockeeId: blockee._id
+ const exist = await Blockings.findOne({
+ blockerId: blocker.id,
+ blockeeId: blockee.id
});
- if (exist === null) {
+ if (exist == null) {
throw new ApiError(meta.errors.notBlocking);
}
// Delete blocking
await deleteBlocking(blocker, blockee);
- return await pack(blockee._id, user, {
- detail: true
- });
+ return await Blockings.pack(blockee.id, user);
});
diff --git a/src/server/api/endpoints/blocking/list.ts b/src/server/api/endpoints/blocking/list.ts
index b9ad6e8a3f..a078891ab0 100644
--- a/src/server/api/endpoints/blocking/list.ts
+++ b/src/server/api/endpoints/blocking/list.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Blocking, { packMany } from '../../../../models/blocking';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
+import { Blockings } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
desc: {
@@ -13,7 +14,7 @@ export const meta = {
requireCredential: true,
- kind: 'following-read',
+ kind: 'read:blocks',
params: {
limit: {
@@ -23,12 +24,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
},
@@ -41,30 +40,12 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const query = {
- blockerId: me._id
- } as any;
+ const query = makePaginationQuery(Blockings.createQueryBuilder('blocking'), ps.sinceId, ps.untilId)
+ .andWhere(`blocking.blockerId = :meId`, { meId: me.id });
- const sort = {
- _id: -1
- };
+ const blockings = await query
+ .take(ps.limit)
+ .getMany();
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- }
-
- const blockings = await Blocking
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
-
- return await packMany(blockings, me);
+ return await Blockings.packMany(blockings, me);
});
diff --git a/src/server/api/endpoints/charts/active-users.ts b/src/server/api/endpoints/charts/active-users.ts
index 9dad942e06..60fa72c5c7 100644
--- a/src/server/api/endpoints/charts/active-users.ts
+++ b/src/server/api/endpoints/charts/active-users.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
-import activeUsersChart from '../../../../services/chart/active-users';
+import { convertLog } from '../../../../services/chart/core';
+import { activeUsersChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -28,12 +29,7 @@ export const meta = {
},
},
- res: {
- type: 'array',
- items: {
- type: 'object',
- },
- },
+ res: convertLog(activeUsersChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/drive.ts b/src/server/api/endpoints/charts/drive.ts
index 6bbb266f96..a9676e1586 100644
--- a/src/server/api/endpoints/charts/drive.ts
+++ b/src/server/api/endpoints/charts/drive.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
import define from '../../define';
-import driveChart, { driveLogSchema } from '../../../../services/chart/drive';
-import { convertLog } from '../../../../services/chart';
+import { convertLog } from '../../../../services/chart/core';
+import { driveChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -29,7 +29,7 @@ export const meta = {
},
},
- res: convertLog(driveLogSchema),
+ res: convertLog(driveChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/federation.ts b/src/server/api/endpoints/charts/federation.ts
index c7b34f1015..95f16c76a7 100644
--- a/src/server/api/endpoints/charts/federation.ts
+++ b/src/server/api/endpoints/charts/federation.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
-import federationChart from '../../../../services/chart/federation';
+import { convertLog } from '../../../../services/chart/core';
+import { federationChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -28,12 +29,7 @@ export const meta = {
},
},
- res: {
- type: 'array',
- items: {
- type: 'object',
- },
- },
+ res: convertLog(federationChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/hashtag.ts b/src/server/api/endpoints/charts/hashtag.ts
index 4db6e62408..a7ec12707e 100644
--- a/src/server/api/endpoints/charts/hashtag.ts
+++ b/src/server/api/endpoints/charts/hashtag.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
-import hashtagChart from '../../../../services/chart/hashtag';
+import { convertLog } from '../../../../services/chart/core';
+import { hashtagChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -35,12 +36,7 @@ export const meta = {
},
},
- res: {
- type: 'array',
- items: {
- type: 'object',
- },
- },
+ res: convertLog(hashtagChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/instance.ts b/src/server/api/endpoints/charts/instance.ts
index 3fe85f086a..cf3094f7e1 100644
--- a/src/server/api/endpoints/charts/instance.ts
+++ b/src/server/api/endpoints/charts/instance.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
-import instanceChart from '../../../../services/chart/instance';
+import { convertLog } from '../../../../services/chart/core';
+import { instanceChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -36,12 +37,7 @@ export const meta = {
}
},
- res: {
- type: 'array',
- items: {
- type: 'object',
- },
- },
+ res: convertLog(instanceChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/network.ts b/src/server/api/endpoints/charts/network.ts
index 48b1d0f66f..c0fcd95fe9 100644
--- a/src/server/api/endpoints/charts/network.ts
+++ b/src/server/api/endpoints/charts/network.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
-import networkChart from '../../../../services/chart/network';
+import { convertLog } from '../../../../services/chart/core';
+import { networkChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -28,12 +29,7 @@ export const meta = {
},
},
- res: {
- type: 'array',
- items: {
- type: 'object',
- },
- },
+ res: convertLog(networkChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts
index cc0ca8bef7..86f30e4b89 100644
--- a/src/server/api/endpoints/charts/notes.ts
+++ b/src/server/api/endpoints/charts/notes.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
import define from '../../define';
-import notesChart, { notesLogSchema } from '../../../../services/chart/notes';
-import { convertLog } from '../../../../services/chart';
+import { convertLog } from '../../../../services/chart/core';
+import { notesChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -29,7 +29,7 @@ export const meta = {
},
},
- res: convertLog(notesLogSchema),
+ res: convertLog(notesChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/user/drive.ts b/src/server/api/endpoints/charts/user/drive.ts
index 064c7c7b72..e3696dfda1 100644
--- a/src/server/api/endpoints/charts/user/drive.ts
+++ b/src/server/api/endpoints/charts/user/drive.ts
@@ -1,8 +1,8 @@
import $ from 'cafy';
import define from '../../../define';
-import perUserDriveChart, { perUserDriveLogSchema } from '../../../../../services/chart/per-user-drive';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import { convertLog } from '../../../../../services/chart';
+import { ID } from '../../../../../misc/cafy-id';
+import { convertLog } from '../../../../../services/chart/core';
+import { perUserDriveChart } from '../../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -31,7 +31,6 @@ export const meta = {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -39,7 +38,7 @@ export const meta = {
}
},
- res: convertLog(perUserDriveLogSchema),
+ res: convertLog(perUserDriveChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/user/following.ts b/src/server/api/endpoints/charts/user/following.ts
index f5b1355038..8feba0bd16 100644
--- a/src/server/api/endpoints/charts/user/following.ts
+++ b/src/server/api/endpoints/charts/user/following.ts
@@ -1,8 +1,8 @@
import $ from 'cafy';
import define from '../../../define';
-import perUserFollowingChart, { perUserFollowingLogSchema } from '../../../../../services/chart/per-user-following';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import { convertLog } from '../../../../../services/chart';
+import { ID } from '../../../../../misc/cafy-id';
+import { convertLog } from '../../../../../services/chart/core';
+import { perUserFollowingChart } from '../../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -31,7 +31,6 @@ export const meta = {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -39,9 +38,9 @@ export const meta = {
}
},
- res: convertLog(perUserFollowingLogSchema),
+ res: convertLog(perUserFollowingChart.schema),
};
export default define(meta, async (ps) => {
- return await perUserFollowingChart.getChart(ps.span as any, ps.limit, ps.userId);
+ return await perUserFollowingChart.getChart(ps.span as any, ps.limit, ps.userId);
});
diff --git a/src/server/api/endpoints/charts/user/notes.ts b/src/server/api/endpoints/charts/user/notes.ts
index 7e31978bf3..8c1db54f76 100644
--- a/src/server/api/endpoints/charts/user/notes.ts
+++ b/src/server/api/endpoints/charts/user/notes.ts
@@ -1,8 +1,8 @@
import $ from 'cafy';
import define from '../../../define';
-import perUserNotesChart, { perUserNotesLogSchema } from '../../../../../services/chart/per-user-notes';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import { convertLog } from '../../../../../services/chart';
+import { ID } from '../../../../../misc/cafy-id';
+import { convertLog } from '../../../../../services/chart/core';
+import { perUserNotesChart } from '../../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -31,7 +31,6 @@ export const meta = {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -39,7 +38,7 @@ export const meta = {
}
},
- res: convertLog(perUserNotesLogSchema),
+ res: convertLog(perUserNotesChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/user/reactions.ts b/src/server/api/endpoints/charts/user/reactions.ts
index 51ff83f20e..7c9b2508ae 100644
--- a/src/server/api/endpoints/charts/user/reactions.ts
+++ b/src/server/api/endpoints/charts/user/reactions.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
import define from '../../../define';
-import perUserReactionsChart from '../../../../../services/chart/per-user-reactions';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
+import { convertLog } from '../../../../../services/chart/core';
+import { perUserReactionsChart } from '../../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -30,7 +31,6 @@ export const meta = {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -38,12 +38,7 @@ export const meta = {
}
},
- res: {
- type: 'array',
- items: {
- type: 'object',
- },
- },
+ res: convertLog(perUserReactionsChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/users.ts b/src/server/api/endpoints/charts/users.ts
index 9de54a630e..3ed5e09349 100644
--- a/src/server/api/endpoints/charts/users.ts
+++ b/src/server/api/endpoints/charts/users.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
import define from '../../define';
-import usersChart, { usersLogSchema } from '../../../../services/chart/users';
-import { convertLog } from '../../../../services/chart';
+import { convertLog } from '../../../../services/chart/core';
+import { usersChart } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@@ -29,7 +29,7 @@ export const meta = {
},
},
- res: convertLog(usersLogSchema),
+ res: convertLog(usersChart.schema),
};
export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts
index 138adffad2..adf780301b 100644
--- a/src/server/api/endpoints/drive.ts
+++ b/src/server/api/endpoints/drive.ts
@@ -1,6 +1,6 @@
-import DriveFile from '../../../models/drive-file';
import define from '../define';
import fetchMeta from '../../../misc/fetch-meta';
+import { DriveFiles } from '../../../models';
export const meta = {
desc: {
@@ -12,7 +12,7 @@ export const meta = {
requireCredential: true,
- kind: 'drive-read',
+ kind: 'read:drive',
res: {
type: 'object',
@@ -31,27 +31,7 @@ export default define(meta, async (ps, user) => {
const instance = await fetchMeta();
// Calculate drive usage
- const usage = await DriveFile.aggregate([{
- $match: {
- 'metadata.userId': user._id,
- 'metadata.deletedAt': { $exists: false }
- }
- }, {
- $project: {
- length: true
- }
- }, {
- $group: {
- _id: null,
- usage: { $sum: '$length' }
- }
- }])
- .then((aggregates: any[]) => {
- if (aggregates.length > 0) {
- return aggregates[0].usage;
- }
- return 0;
- });
+ const usage = await DriveFiles.clacDriveUsageOf(user);
return {
capacity: 1024 * 1024 * instance.localDriveCapacityMb,
diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts
index f108e820e7..400b73d3b7 100644
--- a/src/server/api/endpoints/drive/files.ts
+++ b/src/server/api/endpoints/drive/files.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import DriveFile, { packMany } from '../../../../models/drive-file';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
+import { DriveFiles } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
desc: {
@@ -13,7 +14,7 @@ export const meta = {
requireCredential: true,
- kind: 'drive-read',
+ kind: 'read:drive',
params: {
limit: {
@@ -23,18 +24,15 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
folderId: {
validator: $.optional.nullable.type(ID),
default: null as any,
- transform: transform,
},
type: {
@@ -51,36 +49,24 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const sort = {
- _id: -1
- };
+ const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId)
+ .andWhere('file.userId = :userId', { userId: user.id });
- const query = {
- 'metadata.userId': user._id,
- 'metadata.folderId': ps.folderId,
- 'metadata.deletedAt': { $exists: false }
- } as any;
-
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
+ if (ps.folderId) {
+ query.andWhere('file.folderId = :folderId', { folderId: ps.folderId });
+ } else {
+ query.andWhere('file.folderId IS NULL');
}
if (ps.type) {
- query.contentType = new RegExp(`^${ps.type.replace(/\*/g, '.+?')}$`);
+ if (ps.type.endsWith('/*')) {
+ query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
+ } else {
+ query.andWhere('file.type = :type', { type: ps.type });
+ }
}
- const files = await DriveFile
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const files = await query.take(ps.limit).getMany();
- return await packMany(files, { detail: false, self: true });
+ return await DriveFiles.packMany(files, { detail: false, self: true });
});
diff --git a/src/server/api/endpoints/drive/files/attached-notes.ts b/src/server/api/endpoints/drive/files/attached-notes.ts
index c9eeab58c5..7214463dde 100644
--- a/src/server/api/endpoints/drive/files/attached-notes.ts
+++ b/src/server/api/endpoints/drive/files/attached-notes.ts
@@ -1,9 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFile from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
-import { packMany } from '../../../../../models/note';
import { ApiError } from '../../../error';
+import { DriveFiles } from '../../../../../models';
export const meta = {
stability: 'stable',
@@ -17,12 +16,11 @@ export const meta = {
requireCredential: true,
- kind: 'drive-read',
+ kind: 'read:drive',
params: {
fileId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のファイルID',
'en-US': 'Target file ID'
@@ -48,18 +46,17 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Fetch file
- const file = await DriveFile
- .findOne({
- _id: ps.fileId,
- 'metadata.userId': user._id,
- 'metadata.deletedAt': { $exists: false }
- });
+ const file = await DriveFiles.findOne({
+ id: ps.fileId,
+ userId: user.id,
+ });
- if (file === null) {
+ if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
+ /* v11 TODO
return await packMany(file.metadata.attachedNoteIds || [], user, {
detail: true
- });
+ });*/
});
diff --git a/src/server/api/endpoints/drive/files/check-existence.ts b/src/server/api/endpoints/drive/files/check-existence.ts
index 926411c83a..3a87a9497f 100644
--- a/src/server/api/endpoints/drive/files/check-existence.ts
+++ b/src/server/api/endpoints/drive/files/check-existence.ts
@@ -1,6 +1,6 @@
import $ from 'cafy';
-import DriveFile, { pack } from '../../../../../models/drive-file';
import define from '../../../define';
+import { DriveFiles } from '../../../../../models';
export const meta = {
desc: {
@@ -12,7 +12,7 @@ export const meta = {
requireCredential: true,
- kind: 'drive-read',
+ kind: 'read:drive',
params: {
md5: {
@@ -29,11 +29,12 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const file = await DriveFile.findOne({
+ const file = await DriveFiles.findOne({
md5: ps.md5,
- 'metadata.userId': user._id,
- 'metadata.deletedAt': { $exists: false }
+ userId: user.id,
});
- return { file: file ? await pack(file, { self: true }) : null };
+ return {
+ file: file ? await DriveFiles.pack(file, { self: true }) : null
+ };
});
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index b2979c4888..5702c70fc0 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -1,11 +1,11 @@
import * as ms from 'ms';
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import { validateFileName, pack } from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
import create from '../../../../../services/drive/add-file';
import define from '../../../define';
import { apiLogger } from '../../../logger';
import { ApiError } from '../../../error';
+import { DriveFiles } from '../../../../../models';
export const meta = {
desc: {
@@ -24,12 +24,11 @@ export const meta = {
requireFile: true,
- kind: 'drive-write',
+ kind: 'write:drive',
params: {
folderId: {
validator: $.optional.nullable.type(ID),
- transform: transform,
default: null as any,
desc: {
'ja-JP': 'フォルダID'
@@ -78,7 +77,7 @@ export default define(meta, async (ps, user, app, file, cleanup) => {
name = null;
} else if (name === 'blob') {
name = null;
- } else if (!validateFileName(name)) {
+ } else if (!DriveFiles.validateFileName(name)) {
throw new ApiError(meta.errors.invalidFileName);
}
} else {
@@ -88,7 +87,7 @@ export default define(meta, async (ps, user, app, file, cleanup) => {
try {
// Create file
const driveFile = await create(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive);
- return pack(driveFile, { self: true });
+ return DriveFiles.pack(driveFile, { self: true });
} catch (e) {
apiLogger.error(e);
throw new ApiError();
diff --git a/src/server/api/endpoints/drive/files/delete.ts b/src/server/api/endpoints/drive/files/delete.ts
index dd4e187fcd..d8cc5ec0a1 100644
--- a/src/server/api/endpoints/drive/files/delete.ts
+++ b/src/server/api/endpoints/drive/files/delete.ts
@@ -1,10 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFile from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
import del from '../../../../../services/drive/delete-file';
import { publishDriveStream } from '../../../../../services/stream';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { DriveFiles } from '../../../../../models';
export const meta = {
stability: 'stable',
@@ -18,12 +18,11 @@ export const meta = {
requireCredential: true,
- kind: 'drive-write',
+ kind: 'write:drive',
params: {
fileId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のファイルID',
'en-US': 'Target file ID'
@@ -47,17 +46,13 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- // Fetch file
- const file = await DriveFile
- .findOne({
- _id: ps.fileId
- });
+ const file = await DriveFiles.findOne(ps.fileId);
- if (file === null) {
+ if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
- if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) {
+ if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}
@@ -65,7 +60,5 @@ export default define(meta, async (ps, user) => {
await del(file);
// Publish fileDeleted event
- publishDriveStream(user._id, 'fileDeleted', file._id);
-
- return;
+ publishDriveStream(user.id, 'fileDeleted', file.id);
});
diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts
index 0d4102a48f..265850f84c 100644
--- a/src/server/api/endpoints/drive/files/find.ts
+++ b/src/server/api/endpoints/drive/files/find.ts
@@ -1,14 +1,14 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFile, { pack } from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
+import { DriveFiles } from '../../../../../models';
export const meta = {
requireCredential: true,
tags: ['drive'],
- kind: 'drive-read',
+ kind: 'read:drive',
params: {
name: {
@@ -17,7 +17,6 @@ export const meta = {
folderId: {
validator: $.optional.nullable.type(ID),
- transform: transform,
default: null as any,
desc: {
'ja-JP': 'フォルダID'
@@ -27,12 +26,11 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const files = await DriveFile
- .find({
- filename: ps.name,
- 'metadata.userId': user._id,
- 'metadata.folderId': ps.folderId
- });
+ const files = await DriveFiles.find({
+ name: ps.name,
+ userId: user.id,
+ folderId: ps.folderId
+ });
- return await Promise.all(files.map(file => pack(file, { self: true })));
+ return await Promise.all(files.map(file => DriveFiles.pack(file, { self: true })));
});
diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts
index 6d63a8605c..b516ec2df6 100644
--- a/src/server/api/endpoints/drive/files/show.ts
+++ b/src/server/api/endpoints/drive/files/show.ts
@@ -1,10 +1,9 @@
import $ from 'cafy';
-import * as mongo from 'mongodb';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFile, { pack, IDriveFile } from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
-import config from '../../../../../config';
import { ApiError } from '../../../error';
+import { DriveFile } from '../../../../../models/entities/drive-file';
+import { DriveFiles } from '../../../../../models';
export const meta = {
stability: 'stable',
@@ -18,12 +17,11 @@ export const meta = {
requireCredential: true,
- kind: 'drive-read',
+ kind: 'read:drive',
params: {
fileId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のファイルID',
'en-US': 'Target file ID'
@@ -65,49 +63,33 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- let file: IDriveFile;
+ let file: DriveFile;
if (ps.fileId) {
- file = await DriveFile.findOne({
- _id: ps.fileId,
- 'metadata.deletedAt': { $exists: false }
- });
+ file = await DriveFiles.findOne(ps.fileId);
} else if (ps.url) {
- const isInternalStorageUrl = ps.url.startsWith(config.driveUrl);
- if (isInternalStorageUrl) {
- // Extract file ID from url
- // e.g.
- // http://misskey.local/files/foo?original=bar --> foo
- const fileId = new mongo.ObjectID(ps.url.replace(config.driveUrl, '').replace(/\?(.*)$/, '').replace(/\//g, ''));
- file = await DriveFile.findOne({
- _id: fileId,
- 'metadata.deletedAt': { $exists: false }
- });
- } else {
- file = await DriveFile.findOne({
- $or: [{
- 'metadata.url': ps.url
- }, {
- 'metadata.webpublicUrl': ps.url
- }, {
- 'metadata.thumbnailUrl': ps.url
- }],
- 'metadata.deletedAt': { $exists: false }
- });
- }
+ file = await DriveFiles.findOne({
+ where: [{
+ url: ps.url
+ }, {
+ webpublicUrl: ps.url
+ }, {
+ thumbnailUrl: ps.url
+ }],
+ });
} else {
throw new ApiError(meta.errors.fileIdOrUrlRequired);
}
- if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) {
+ if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}
- if (file === null) {
+ if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
- return await pack(file, {
+ return await DriveFiles.pack(file, {
detail: true,
self: true
});
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index c8803bec3a..81e86a2734 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -1,11 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder from '../../../../../models/drive-folder';
-import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
import { publishDriveStream } from '../../../../../services/stream';
import define from '../../../define';
-import Note from '../../../../../models/note';
import { ApiError } from '../../../error';
+import { DriveFiles, DriveFolders } from '../../../../../models';
export const meta = {
desc: {
@@ -17,12 +15,11 @@ export const meta = {
requireCredential: true,
- kind: 'drive-write',
+ kind: 'write:drive',
params: {
fileId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のファイルID'
}
@@ -30,7 +27,6 @@ export const meta = {
folderId: {
validator: $.optional.nullable.type(ID),
- transform: transform,
default: undefined as any,
desc: {
'ja-JP': 'フォルダID'
@@ -38,7 +34,7 @@ export const meta = {
},
name: {
- validator: $.optional.str.pipe(validateFileName),
+ validator: $.optional.str.pipe(DriveFiles.validateFileName),
default: undefined as any,
desc: {
'ja-JP': 'ファイル名',
@@ -78,69 +74,47 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- // Fetch file
- const file = await DriveFile
- .findOne({
- _id: ps.fileId
- });
+ const file = await DriveFiles.findOne(ps.fileId);
- if (file === null) {
+ if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
- if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) {
+ if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}
- if (ps.name) file.filename = ps.name;
+ if (ps.name) file.name = ps.name;
- if (ps.isSensitive !== undefined) file.metadata.isSensitive = ps.isSensitive;
+ if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
if (ps.folderId !== undefined) {
if (ps.folderId === null) {
- file.metadata.folderId = null;
+ file.folderId = null;
} else {
- // Fetch folder
- const folder = await DriveFolder
- .findOne({
- _id: ps.folderId,
- userId: user._id
- });
+ const folder = await DriveFolders.findOne({
+ id: ps.folderId,
+ userId: user.id
+ });
- if (folder === null) {
+ if (folder == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
- file.metadata.folderId = folder._id;
+ file.folderId = folder.id;
}
}
- await DriveFile.update(file._id, {
- $set: {
- filename: file.filename,
- 'metadata.folderId': file.metadata.folderId,
- 'metadata.isSensitive': file.metadata.isSensitive
- }
- });
-
- // ドライブのファイルが非正規化されているドキュメントも更新
- Note.find({
- '_files._id': file._id
- }).then(notes => {
- for (const note of notes) {
- note._files[note._files.findIndex(f => f._id.equals(file._id))] = file;
- Note.update({ _id: note._id }, {
- $set: {
- _files: note._files
- }
- });
- }
+ await DriveFiles.update(file.id, {
+ name: file.name,
+ folderId: file.folderId,
+ isSensitive: file.isSensitive
});
- const fileObj = await pack(file, { self: true });
+ const fileObj = await DriveFiles.pack(file, { self: true });
// Publish fileUpdated event
- publishDriveStream(user._id, 'fileUpdated', fileObj);
+ publishDriveStream(user.id, 'fileUpdated', fileObj);
return fileObj;
});
diff --git a/src/server/api/endpoints/drive/files/upload-from-url.ts b/src/server/api/endpoints/drive/files/upload-from-url.ts
index 93a9fa62fa..034ab10f19 100644
--- a/src/server/api/endpoints/drive/files/upload-from-url.ts
+++ b/src/server/api/endpoints/drive/files/upload-from-url.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import * as ms from 'ms';
-import { pack } from '../../../../../models/drive-file';
import uploadFromUrl from '../../../../../services/drive/upload-from-url';
import define from '../../../define';
+import { DriveFiles } from '../../../../../models';
export const meta = {
desc: {
@@ -19,7 +19,7 @@ export const meta = {
requireCredential: true,
- kind: 'drive-write',
+ kind: 'write:drive',
params: {
url: {
@@ -30,7 +30,6 @@ export const meta = {
folderId: {
validator: $.optional.nullable.type(ID),
default: null as any,
- transform: transform
},
isSensitive: {
@@ -53,5 +52,5 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- return await pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true });
+ return await DriveFiles.pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true });
});
diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts
index 73c179f7be..f5c3816407 100644
--- a/src/server/api/endpoints/drive/folders.ts
+++ b/src/server/api/endpoints/drive/folders.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import DriveFolder, { pack } from '../../../../models/drive-folder';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
+import { DriveFolders } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
desc: {
@@ -13,7 +14,7 @@ export const meta = {
requireCredential: true,
- kind: 'drive-read',
+ kind: 'read:drive',
params: {
limit: {
@@ -23,18 +24,15 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
folderId: {
validator: $.optional.nullable.type(ID),
default: null as any,
- transform: transform,
}
},
@@ -47,29 +45,16 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const sort = {
- _id: -1
- };
- const query = {
- userId: user._id,
- parentId: ps.folderId
- } as any;
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
+ const query = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId)
+ .andWhere('folder.userId = :userId', { userId: user.id });
+
+ if (ps.folderId) {
+ query.andWhere('folder.parentId = :parentId', { parentId: ps.folderId });
+ } else {
+ query.andWhere('folder.parentId IS NULL');
}
- const folders = await DriveFolder
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const folders = await query.take(ps.limit).getMany();
- return await Promise.all(folders.map(folder => pack(folder)));
+ return await Promise.all(folders.map(folder => DriveFolders.pack(folder)));
});
diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
index 5fab0b91a1..5530abf9dc 100644
--- a/src/server/api/endpoints/drive/folders/create.ts
+++ b/src/server/api/endpoints/drive/folders/create.ts
@@ -1,9 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
+import { ID } from '../../../../../misc/cafy-id';
import { publishDriveStream } from '../../../../../services/stream';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { DriveFolders } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
export const meta = {
stability: 'stable',
@@ -17,11 +18,11 @@ export const meta = {
requireCredential: true,
- kind: 'drive-write',
+ kind: 'write:drive',
params: {
name: {
- validator: $.optional.str.pipe(isValidFolderName),
+ validator: $.optional.str.pipe(DriveFolders.validateFolderName),
default: 'Untitled',
desc: {
'ja-JP': 'フォルダ名',
@@ -31,7 +32,6 @@ export const meta = {
parentId: {
validator: $.optional.nullable.type(ID),
- transform: transform,
desc: {
'ja-JP': '親フォルダID',
'en-US': 'Parent folder ID'
@@ -53,29 +53,29 @@ export default define(meta, async (ps, user) => {
let parent = null;
if (ps.parentId) {
// Fetch parent folder
- parent = await DriveFolder
- .findOne({
- _id: ps.parentId,
- userId: user._id
- });
+ parent = await DriveFolders.findOne({
+ id: ps.parentId,
+ userId: user.id
+ });
- if (parent === null) {
+ if (parent == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
}
// Create folder
- const folder = await DriveFolder.insert({
+ const folder = await DriveFolders.save({
+ id: genId(),
createdAt: new Date(),
name: ps.name,
- parentId: parent !== null ? parent._id : null,
- userId: user._id
+ parentId: parent !== null ? parent.id : null,
+ userId: user.id
});
- const folderObj = await pack(folder);
+ const folderObj = await DriveFolders.pack(folder);
// Publish folderCreated event
- publishDriveStream(user._id, 'folderCreated', folderObj);
+ publishDriveStream(user.id, 'folderCreated', folderObj);
return folderObj;
});
diff --git a/src/server/api/endpoints/drive/folders/delete.ts b/src/server/api/endpoints/drive/folders/delete.ts
index 9f22bf9ea7..fe6c05ad07 100644
--- a/src/server/api/endpoints/drive/folders/delete.ts
+++ b/src/server/api/endpoints/drive/folders/delete.ts
@@ -1,10 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder from '../../../../../models/drive-folder';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { publishDriveStream } from '../../../../../services/stream';
-import DriveFile from '../../../../../models/drive-file';
import { ApiError } from '../../../error';
+import { DriveFolders, DriveFiles } from '../../../../../models';
export const meta = {
stability: 'stable',
@@ -18,12 +17,11 @@ export const meta = {
requireCredential: true,
- kind: 'drive-write',
+ kind: 'write:drive',
params: {
folderId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のフォルダID',
'en-US': 'Target folder ID'
@@ -48,29 +46,26 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Get folder
- const folder = await DriveFolder
- .findOne({
- _id: ps.folderId,
- userId: user._id
- });
+ const folder = await DriveFolders.findOne({
+ id: ps.folderId,
+ userId: user.id
+ });
- if (folder === null) {
+ if (folder == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
const [childFoldersCount, childFilesCount] = await Promise.all([
- DriveFolder.count({ parentId: folder._id }),
- DriveFile.count({ 'metadata.folderId': folder._id })
+ DriveFolders.count({ parentId: folder.id }),
+ DriveFiles.count({ folderId: folder.id })
]);
if (childFoldersCount !== 0 || childFilesCount !== 0) {
throw new ApiError(meta.errors.hasChildFilesOrFolders);
}
- await DriveFolder.remove({ _id: folder._id });
+ await DriveFolders.delete(folder.id);
// Publish folderCreated event
- publishDriveStream(user._id, 'folderDeleted', folder._id);
-
- return;
+ publishDriveStream(user.id, 'folderDeleted', folder.id);
});
diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts
index 16b6c10633..f0989ec5ae 100644
--- a/src/server/api/endpoints/drive/folders/find.ts
+++ b/src/server/api/endpoints/drive/folders/find.ts
@@ -1,14 +1,14 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder, { pack } from '../../../../../models/drive-folder';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
+import { DriveFolders } from '../../../../../models';
export const meta = {
tags: ['drive'],
requireCredential: true,
- kind: 'drive-read',
+ kind: 'read:drive',
params: {
name: {
@@ -17,7 +17,6 @@ export const meta = {
parentId: {
validator: $.optional.nullable.type(ID),
- transform: transform,
default: null as any,
desc: {
'ja-JP': 'フォルダID'
@@ -34,12 +33,11 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const folders = await DriveFolder
- .find({
- name: ps.name,
- userId: user._id,
- parentId: ps.parentId
- });
+ const folders = await DriveFolders.find({
+ name: ps.name,
+ userId: user.id,
+ parentId: ps.parentId
+ });
- return await Promise.all(folders.map(folder => pack(folder)));
+ return await Promise.all(folders.map(folder => DriveFolders.pack(folder)));
});
diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts
index bbfcbed51f..60507e7d7f 100644
--- a/src/server/api/endpoints/drive/folders/show.ts
+++ b/src/server/api/endpoints/drive/folders/show.ts
@@ -1,8 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder, { pack } from '../../../../../models/drive-folder';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { DriveFolders } from '../../../../../models';
export const meta = {
stability: 'stable',
@@ -16,12 +16,11 @@ export const meta = {
requireCredential: true,
- kind: 'drive-read',
+ kind: 'read:drive',
params: {
folderId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のフォルダID',
'en-US': 'Target folder ID'
@@ -44,17 +43,16 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Get folder
- const folder = await DriveFolder
- .findOne({
- _id: ps.folderId,
- userId: user._id
- });
+ const folder = await DriveFolders.findOne({
+ id: ps.folderId,
+ userId: user.id
+ });
- if (folder === null) {
+ if (folder == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
- return await pack(folder, {
+ return await DriveFolders.pack(folder, {
detail: true
});
});
diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
index a1ee2669f0..90129bed63 100644
--- a/src/server/api/endpoints/drive/folders/update.ts
+++ b/src/server/api/endpoints/drive/folders/update.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
+import { ID } from '../../../../../misc/cafy-id';
import { publishDriveStream } from '../../../../../services/stream';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { DriveFolders } from '../../../../../models';
export const meta = {
stability: 'stable',
@@ -17,12 +17,11 @@ export const meta = {
requireCredential: true,
- kind: 'drive-write',
+ kind: 'write:drive',
params: {
folderId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のフォルダID',
'en-US': 'Target folder ID'
@@ -30,7 +29,7 @@ export const meta = {
},
name: {
- validator: $.optional.str.pipe(isValidFolderName),
+ validator: $.optional.str.pipe(DriveFolders.validateFolderName),
desc: {
'ja-JP': 'フォルダ名',
'en-US': 'Folder name'
@@ -39,7 +38,6 @@ export const meta = {
parentId: {
validator: $.optional.nullable.type(ID),
- transform: transform,
desc: {
'ja-JP': '親フォルダID',
'en-US': 'Parent folder ID'
@@ -70,46 +68,41 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Fetch folder
- const folder = await DriveFolder
- .findOne({
- _id: ps.folderId,
- userId: user._id
- });
+ const folder = await DriveFolders.findOne({
+ id: ps.folderId,
+ userId: user.id
+ });
- if (folder === null) {
+ if (folder == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
if (ps.name) folder.name = ps.name;
if (ps.parentId !== undefined) {
- if (ps.parentId.equals(folder._id)) {
+ if (ps.parentId === folder.id) {
throw new ApiError(meta.errors.recursiveNesting);
} else if (ps.parentId === null) {
folder.parentId = null;
} else {
// Get parent folder
- const parent = await DriveFolder
- .findOne({
- _id: ps.parentId,
- userId: user._id
- });
+ const parent = await DriveFolders.findOne({
+ id: ps.parentId,
+ userId: user.id
+ });
- if (parent === null) {
+ if (parent == null) {
throw new ApiError(meta.errors.noSuchParentFolder);
}
// Check if the circular reference will occur
async function checkCircle(folderId: any): Promise<boolean> {
// Fetch folder
- const folder2 = await DriveFolder.findOne({
- _id: folderId
- }, {
- _id: true,
- parentId: true
+ const folder2 = await DriveFolders.findOne({
+ id: folderId
});
- if (folder2._id.equals(folder._id)) {
+ if (folder2.id === folder.id) {
return true;
} else if (folder2.parentId) {
return await checkCircle(folder2.parentId);
@@ -124,22 +117,20 @@ export default define(meta, async (ps, user) => {
}
}
- folder.parentId = parent._id;
+ folder.parentId = parent.id;
}
}
// Update
- DriveFolder.update(folder._id, {
- $set: {
- name: folder.name,
- parentId: folder.parentId
- }
+ DriveFolders.update(folder.id, {
+ name: folder.name,
+ parentId: folder.parentId
});
- const folderObj = await pack(folder);
+ const folderObj = await DriveFolders.pack(folder);
// Publish folderUpdated event
- publishDriveStream(user._id, 'folderUpdated', folderObj);
+ publishDriveStream(user.id, 'folderUpdated', folderObj);
return folderObj;
});
diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts
index 916482be4d..9a84627767 100644
--- a/src/server/api/endpoints/drive/stream.ts
+++ b/src/server/api/endpoints/drive/stream.ts
@@ -1,14 +1,15 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import DriveFile, { packMany } from '../../../../models/drive-file';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
+import { DriveFiles } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
tags: ['drive'],
requireCredential: true,
- kind: 'drive-read',
+ kind: 'read:drive',
params: {
limit: {
@@ -18,12 +19,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
type: {
@@ -40,35 +39,18 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const sort = {
- _id: -1
- };
-
- const query = {
- 'metadata.userId': user._id,
- 'metadata.deletedAt': { $exists: false }
- } as any;
-
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- }
+ const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId)
+ .andWhere('file.userId = :userId', { userId: user.id });
if (ps.type) {
- query.contentType = new RegExp(`^${ps.type.replace(/\*/g, '.+?')}$`);
+ if (ps.type.endsWith('/*')) {
+ query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
+ } else {
+ query.andWhere('file.type = :type', { type: ps.type });
+ }
}
- const files = await DriveFile
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const files = await query.take(ps.limit).getMany();
- return await packMany(files, { self: true });
+ return await DriveFiles.packMany(files, { detail: false, self: true });
});
diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts
index f81f81822e..1946d26dec 100644
--- a/src/server/api/endpoints/federation/instances.ts
+++ b/src/server/api/endpoints/federation/instances.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
-import Instance from '../../../../models/instance';
+import { Instances } from '../../../../models';
+import fetchMeta from '../../../../misc/fetch-meta';
export const meta = {
tags: ['federation'],
@@ -37,92 +38,55 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- let sort;
+ const query = Instances.createQueryBuilder('instance');
- if (ps.sort) {
- if (ps.sort == '+notes') {
- sort = {
- notesCount: -1
- };
- } else if (ps.sort == '-notes') {
- sort = {
- notesCount: 1
- };
- } else if (ps.sort == '+users') {
- sort = {
- usersCount: -1
- };
- } else if (ps.sort == '-users') {
- sort = {
- usersCount: 1
- };
- } else if (ps.sort == '+following') {
- sort = {
- followingCount: -1
- };
- } else if (ps.sort == '-following') {
- sort = {
- followingCount: 1
- };
- } else if (ps.sort == '+followers') {
- sort = {
- followersCount: -1
- };
- } else if (ps.sort == '-followers') {
- sort = {
- followersCount: 1
- };
- } else if (ps.sort == '+caughtAt') {
- sort = {
- caughtAt: -1
- };
- } else if (ps.sort == '-caughtAt') {
- sort = {
- caughtAt: 1
- };
- } else if (ps.sort == '+lastCommunicatedAt') {
- sort = {
- lastCommunicatedAt: -1
- };
- } else if (ps.sort == '-lastCommunicatedAt') {
- sort = {
- lastCommunicatedAt: 1
- };
- } else if (ps.sort == '+driveUsage') {
- sort = {
- driveUsage: -1
- };
- } else if (ps.sort == '-driveUsage') {
- sort = {
- driveUsage: 1
- };
- } else if (ps.sort == '+driveFiles') {
- sort = {
- driveFiles: -1
- };
- } else if (ps.sort == '-driveFiles') {
- sort = {
- driveFiles: 1
- };
+ switch (ps.sort) {
+ case '+notes': query.orderBy('instance.notesCount', 'DESC'); break;
+ case '-notes': query.orderBy('instance.notesCount', 'ASC'); break;
+ case '+usersCount': query.orderBy('instance.usersCount', 'DESC'); break;
+ case '-usersCount': query.orderBy('instance.usersCount', 'ASC'); break;
+ case '+followingCount': query.orderBy('instance.followingCount', 'DESC'); break;
+ case '-followingCount': query.orderBy('instance.followingCount', 'ASC'); break;
+ case '+followersCount': query.orderBy('instance.followersCount', 'DESC'); break;
+ case '-followersCount': query.orderBy('instance.followersCount', 'ASC'); break;
+ case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
+ case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
+ case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break;
+ case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break;
+ case '+driveUsage': query.orderBy('instance.driveUsage', 'DESC'); break;
+ case '-driveUsage': query.orderBy('instance.driveUsage', 'ASC'); break;
+ case '+driveFiles': query.orderBy('instance.driveFiles', 'DESC'); break;
+ case '-driveFiles': query.orderBy('instance.driveFiles', 'ASC'); break;
+
+ default: query.orderBy('instance.id', 'DESC'); break;
+ }
+
+ if (typeof ps.blocked === 'boolean') {
+ const meta = await fetchMeta();
+ if (ps.blocked) {
+ query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts });
+ } else {
+ query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts });
}
- } else {
- sort = {
- _id: -1
- };
}
- const q = {} as any;
+ if (typeof ps.notResponding === 'boolean') {
+ if (ps.notResponding) {
+ query.andWhere('instance.isNotResponding = TRUE');
+ } else {
+ query.andWhere('instance.isNotResponding = FALSE');
+ }
+ }
- if (typeof ps.blocked === 'boolean') q.isBlocked = ps.blocked;
- if (typeof ps.notResponding === 'boolean') q.isNotResponding = ps.notResponding;
- if (typeof ps.markedAsClosed === 'boolean') q.isMarkedAsClosed = ps.markedAsClosed;
+ if (typeof ps.markedAsClosed === 'boolean') {
+ if (ps.markedAsClosed) {
+ query.andWhere('instance.isMarkedAsClosed = TRUE');
+ } else {
+ query.andWhere('instance.isMarkedAsClosed = FALSE');
+ }
+ }
- const instances = await Instance
- .find(q, {
- limit: ps.limit,
- sort: sort,
- skip: ps.offset
- });
+ const instances = await query.take(ps.limit).skip(ps.offset).getMany();
return instances;
});
diff --git a/src/server/api/endpoints/federation/show-instance.ts b/src/server/api/endpoints/federation/show-instance.ts
index e7f68620af..875afa05b2 100644
--- a/src/server/api/endpoints/federation/show-instance.ts
+++ b/src/server/api/endpoints/federation/show-instance.ts
@@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../define';
-import Instance from '../../../../models/instance';
+import { Instances } from '../../../../models';
export const meta = {
tags: ['federation'],
@@ -15,7 +15,7 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const instance = await Instance
+ const instance = await Instances
.findOne({ host: ps.host });
return instance;
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 81b2399551..5b43815a5e 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -1,12 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import * as ms from 'ms';
-import { pack } from '../../../../models/user';
-import Following from '../../../../models/following';
import create from '../../../../services/following/create';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
+import { Followings, Users } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -25,12 +24,11 @@ export const meta = {
requireCredential: true,
- kind: 'following-write',
+ kind: 'write:following',
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -75,7 +73,7 @@ export default define(meta, async (ps, user) => {
const follower = user;
// 自分自身
- if (user._id.equals(ps.userId)) {
+ if (user.id === ps.userId) {
throw new ApiError(meta.errors.followeeIsYourself);
}
@@ -86,12 +84,12 @@ export default define(meta, async (ps, user) => {
});
// Check if already following
- const exist = await Following.findOne({
- followerId: follower._id,
- followeeId: followee._id
+ const exist = await Followings.findOne({
+ followerId: follower.id,
+ followeeId: followee.id
});
- if (exist !== null) {
+ if (exist != null) {
throw new ApiError(meta.errors.alreadyFollowing);
}
@@ -103,5 +101,5 @@ export default define(meta, async (ps, user) => {
throw e;
}
- return await pack(followee._id, user);
+ return await Users.pack(followee.id, user);
});
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 8f8249b1e8..240a037c9e 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -1,12 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import * as ms from 'ms';
-import { pack } from '../../../../models/user';
-import Following from '../../../../models/following';
import deleteFollowing from '../../../../services/following/delete';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
+import { Followings, Users } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -25,12 +24,11 @@ export const meta = {
requireCredential: true,
- kind: 'following-write',
+ kind: 'write:following',
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => {
const follower = user;
// Check if the followee is yourself
- if (user._id.equals(ps.userId)) {
+ if (user.id === ps.userId) {
throw new ApiError(meta.errors.followeeIsYourself);
}
@@ -74,16 +72,16 @@ export default define(meta, async (ps, user) => {
});
// Check not following
- const exist = await Following.findOne({
- followerId: follower._id,
- followeeId: followee._id
+ const exist = await Followings.findOne({
+ followerId: follower.id,
+ followeeId: followee.id
});
- if (exist === null) {
+ if (exist == null) {
throw new ApiError(meta.errors.notFollowing);
}
await deleteFollowing(follower, followee);
- return await pack(followee._id, user);
+ return await Users.pack(followee.id, user);
});
diff --git a/src/server/api/endpoints/following/requests/accept.ts b/src/server/api/endpoints/following/requests/accept.ts
index 0975990c02..65c24f7be9 100644
--- a/src/server/api/endpoints/following/requests/accept.ts
+++ b/src/server/api/endpoints/following/requests/accept.ts
@@ -1,5 +1,5 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import acceptFollowRequest from '../../../../../services/following/requests/accept';
import define from '../../../define';
import { ApiError } from '../../../error';
@@ -15,12 +15,11 @@ export const meta = {
requireCredential: true,
- kind: 'following-write',
+ kind: 'write:following',
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
diff --git a/src/server/api/endpoints/following/requests/cancel.ts b/src/server/api/endpoints/following/requests/cancel.ts
index 371f9f0ed3..79cdb776f2 100644
--- a/src/server/api/endpoints/following/requests/cancel.ts
+++ b/src/server/api/endpoints/following/requests/cancel.ts
@@ -1,10 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import cancelFollowRequest from '../../../../../services/following/requests/cancel';
-import { pack } from '../../../../../models/user';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters';
+import { Users } from '../../../../../models';
export const meta = {
desc: {
@@ -16,12 +16,11 @@ export const meta = {
requireCredential: true,
- kind: 'following-write',
+ kind: 'write:following',
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -58,5 +57,5 @@ export default define(meta, async (ps, user) => {
throw e;
}
- return await pack(followee._id, user);
+ return await Users.pack(followee.id, user);
});
diff --git a/src/server/api/endpoints/following/requests/list.ts b/src/server/api/endpoints/following/requests/list.ts
index c9bcedf929..13e4a39388 100644
--- a/src/server/api/endpoints/following/requests/list.ts
+++ b/src/server/api/endpoints/following/requests/list.ts
@@ -1,5 +1,5 @@
-import FollowRequest, { pack } from '../../../../../models/follow-request';
import define from '../../../define';
+import { FollowRequests } from '../../../../../models';
export const meta = {
desc: {
@@ -11,13 +11,13 @@ export const meta = {
requireCredential: true,
- kind: 'following-read'
+ kind: 'read:following'
};
export default define(meta, async (ps, user) => {
- const reqs = await FollowRequest.find({
- followeeId: user._id
+ const reqs = await FollowRequests.find({
+ followeeId: user.id
});
- return await Promise.all(reqs.map(req => pack(req)));
+ return await Promise.all(reqs.map(req => FollowRequests.pack(req)));
});
diff --git a/src/server/api/endpoints/following/requests/reject.ts b/src/server/api/endpoints/following/requests/reject.ts
index 5e59d4bc97..cccb60b243 100644
--- a/src/server/api/endpoints/following/requests/reject.ts
+++ b/src/server/api/endpoints/following/requests/reject.ts
@@ -1,5 +1,5 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import rejectFollowRequest from '../../../../../services/following/requests/reject';
import define from '../../../define';
import { ApiError } from '../../../error';
@@ -15,12 +15,11 @@ export const meta = {
requireCredential: true,
- kind: 'following-write',
+ kind: 'write:following',
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
diff --git a/src/server/api/endpoints/games/reversi/games.ts b/src/server/api/endpoints/games/reversi/games.ts
index e3c22c7611..07736e0424 100644
--- a/src/server/api/endpoints/games/reversi/games.ts
+++ b/src/server/api/endpoints/games/reversi/games.ts
@@ -1,7 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import ReversiGame, { pack } from '../../../../../models/games/reversi/game';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
+import { ReversiGames } from '../../../../../models';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+import { Brackets } from 'typeorm';
export const meta = {
tags: ['games'],
@@ -14,12 +16,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
my: {
@@ -30,39 +30,20 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const q: any = ps.my ? {
- isStarted: true,
- $or: [{
- user1Id: user._id
- }, {
- user2Id: user._id
- }]
- } : {
- isStarted: true
- };
+ const query = makePaginationQuery(ReversiGames.createQueryBuilder('game'), ps.sinceId, ps.untilId)
+ .andWhere('game.isStarted = TRUE');
- const sort = {
- _id: -1
- };
-
- if (ps.sinceId) {
- sort._id = 1;
- q._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- q._id = {
- $lt: ps.untilId
- };
+ if (ps.my) {
+ query.andWhere(new Brackets(qb => { qb
+ .where('game.user1Id = :userId', { userId: user.id })
+ .orWhere('game.user2Id = :userId', { userId: user.id });
+ }));
}
// Fetch games
- const games = await ReversiGame.find(q, {
- sort: sort,
- limit: ps.limit
- });
+ const games = await query.take(ps.limit).getMany();
- return await Promise.all(games.map((g) => pack(g, user, {
+ return await Promise.all(games.map((g) => ReversiGames.pack(g, user, {
detail: false
})));
});
diff --git a/src/server/api/endpoints/games/reversi/games/show.ts b/src/server/api/endpoints/games/reversi/games/show.ts
index 766ca90119..ea2776b16f 100644
--- a/src/server/api/endpoints/games/reversi/games/show.ts
+++ b/src/server/api/endpoints/games/reversi/games/show.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../../misc/cafy-id';
-import ReversiGame, { pack } from '../../../../../../models/games/reversi/game';
+import { ID } from '../../../../../../misc/cafy-id';
import Reversi from '../../../../../../games/reversi/core';
import define from '../../../../define';
import { ApiError } from '../../../../error';
+import { ReversiGames } from '../../../../../../models';
export const meta = {
tags: ['games'],
@@ -11,7 +11,6 @@ export const meta = {
params: {
gameId: {
validator: $.type(ID),
- transform: transform,
},
},
@@ -25,22 +24,23 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const game = await ReversiGame.findOne({ _id: ps.gameId });
+ const game = await ReversiGames.findOne(ps.gameId);
if (game == null) {
throw new ApiError(meta.errors.noSuchGame);
}
- const o = new Reversi(game.settings.map, {
- isLlotheo: game.settings.isLlotheo,
- canPutEverywhere: game.settings.canPutEverywhere,
- loopedBoard: game.settings.loopedBoard
+ const o = new Reversi(game.map, {
+ isLlotheo: game.isLlotheo,
+ canPutEverywhere: game.canPutEverywhere,
+ loopedBoard: game.loopedBoard
});
- for (const log of game.logs)
+ for (const log of game.logs) {
o.put(log.color, log.pos);
+ }
- const packed = await pack(game, user);
+ const packed = await ReversiGames.pack(game, user);
return Object.assign({
board: o.board,
diff --git a/src/server/api/endpoints/games/reversi/games/surrender.ts b/src/server/api/endpoints/games/reversi/games/surrender.ts
index 446210894d..56d66fb205 100644
--- a/src/server/api/endpoints/games/reversi/games/surrender.ts
+++ b/src/server/api/endpoints/games/reversi/games/surrender.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../../misc/cafy-id';
-import ReversiGame, { pack } from '../../../../../../models/games/reversi/game';
+import { ID } from '../../../../../../misc/cafy-id';
import { publishReversiGameStream } from '../../../../../../services/stream';
import define from '../../../../define';
import { ApiError } from '../../../../error';
+import { ReversiGames } from '../../../../../../models';
export const meta = {
tags: ['games'],
@@ -17,7 +17,6 @@ export const meta = {
params: {
gameId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '投了したい対局'
}
@@ -46,7 +45,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const game = await ReversiGame.findOne({ _id: ps.gameId });
+ const game = await ReversiGames.findOne(ps.gameId);
if (game == null) {
throw new ApiError(meta.errors.noSuchGame);
@@ -56,26 +55,20 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.alreadyEnded);
}
- if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) {
+ if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}
- const winnerId = game.user1Id.equals(user._id) ? game.user2Id : game.user1Id;
+ const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
- await ReversiGame.update({
- _id: game._id
- }, {
- $set: {
- surrendered: user._id,
- isEnded: true,
- winnerId: winnerId
- }
+ await ReversiGames.update(game.id, {
+ surrendered: user.id,
+ isEnded: true,
+ winnerId: winnerId
});
- publishReversiGameStream(game._id, 'ended', {
+ publishReversiGameStream(game.id, 'ended', {
winnerId: winnerId,
- game: await pack(game._id, user)
+ game: await ReversiGames.pack(game.id, user)
});
-
- return;
});
diff --git a/src/server/api/endpoints/games/reversi/invitations.ts b/src/server/api/endpoints/games/reversi/invitations.ts
index c204770578..71f5aca1d1 100644
--- a/src/server/api/endpoints/games/reversi/invitations.ts
+++ b/src/server/api/endpoints/games/reversi/invitations.ts
@@ -1,5 +1,5 @@
-import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching';
import define from '../../../define';
+import { ReversiMatchings } from '../../../../../models';
export const meta = {
tags: ['games'],
@@ -9,13 +9,9 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Find session
- const invitations = await Matching.find({
- childId: user._id
- }, {
- sort: {
- _id: -1
- }
+ const invitations = await ReversiMatchings.find({
+ childId: user.id
});
- return await Promise.all(invitations.map((i) => packMatching(i, user)));
+ return await Promise.all(invitations.map((i) => ReversiMatchings.pack(i, user)));
});
diff --git a/src/server/api/endpoints/games/reversi/match.ts b/src/server/api/endpoints/games/reversi/match.ts
index e66765944d..e34d3c67f4 100644
--- a/src/server/api/endpoints/games/reversi/match.ts
+++ b/src/server/api/endpoints/games/reversi/match.ts
@@ -1,12 +1,14 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching';
-import ReversiGame, { pack as packGame } from '../../../../../models/games/reversi/game';
+import { ID } from '../../../../../misc/cafy-id';
import { publishMainStream, publishReversiStream } from '../../../../../services/stream';
import { eighteight } from '../../../../../games/reversi/maps';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters';
+import { genId } from '../../../../../misc/gen-id';
+import { ReversiMatchings, ReversiGames } from '../../../../../models';
+import { ReversiGame } from '../../../../../models/entities/games/reversi/game';
+import { ReversiMatching } from '../../../../../models/entities/games/reversi/matching';
export const meta = {
tags: ['games'],
@@ -16,7 +18,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -41,50 +42,47 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Myself
- if (ps.userId.equals(user._id)) {
+ if (ps.userId === user.id) {
throw new ApiError(meta.errors.isYourself);
}
// Find session
- const exist = await Matching.findOne({
+ const exist = await ReversiMatchings.findOne({
parentId: ps.userId,
- childId: user._id
+ childId: user.id
});
if (exist) {
// Destroy session
- Matching.remove({
- _id: exist._id
- });
+ ReversiMatchings.delete(exist.id);
// Create game
- const game = await ReversiGame.insert({
+ const game = await ReversiGames.save({
+ id: genId(),
createdAt: new Date(),
user1Id: exist.parentId,
- user2Id: user._id,
+ user2Id: user.id,
user1Accepted: false,
user2Accepted: false,
isStarted: false,
isEnded: false,
logs: [],
- settings: {
- map: eighteight.data,
- bw: 'random',
- isLlotheo: false
- }
- });
+ map: eighteight.data,
+ bw: 'random',
+ isLlotheo: false
+ } as ReversiGame);
- publishReversiStream(exist.parentId, 'matched', await packGame(game, exist.parentId));
+ publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, exist.parentId));
- const other = await Matching.count({
- childId: user._id
+ const other = await ReversiMatchings.count({
+ childId: user.id
});
if (other == 0) {
- publishMainStream(user._id, 'reversiNoInvites');
+ publishMainStream(user.id, 'reversiNoInvites');
}
- return await packGame(game, user);
+ return await ReversiGames.pack(game, user);
} else {
// Fetch child
const child = await getUser(ps.userId).catch(e => {
@@ -93,21 +91,22 @@ export default define(meta, async (ps, user) => {
});
// 以前のセッションはすべて削除しておく
- await Matching.remove({
- parentId: user._id
+ await ReversiMatchings.delete({
+ parentId: user.id
});
// セッションを作成
- const matching = await Matching.insert({
+ const matching = await ReversiMatchings.save({
+ id: genId(),
createdAt: new Date(),
- parentId: user._id,
- childId: child._id
- });
+ parentId: user.id,
+ childId: child.id
+ } as ReversiMatching);
- const packed = await packMatching(matching, child);
- publishReversiStream(child._id, 'invited', packed);
- publishMainStream(child._id, 'reversiInvited', packed);
+ const packed = await ReversiMatchings.pack(matching, child);
+ publishReversiStream(child.id, 'invited', packed);
+ publishMainStream(child.id, 'reversiInvited', packed);
- return;
+ return null;
}
});
diff --git a/src/server/api/endpoints/games/reversi/match/cancel.ts b/src/server/api/endpoints/games/reversi/match/cancel.ts
index fb230032d8..71aaae5ee1 100644
--- a/src/server/api/endpoints/games/reversi/match/cancel.ts
+++ b/src/server/api/endpoints/games/reversi/match/cancel.ts
@@ -1,5 +1,5 @@
-import Matching from '../../../../../../models/games/reversi/matching';
import define from '../../../../define';
+import { ReversiMatchings } from '../../../../../../models';
export const meta = {
tags: ['games'],
@@ -8,9 +8,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- await Matching.remove({
- parentId: user._id
+ await ReversiMatchings.delete({
+ parentId: user.id
});
-
- return;
});
diff --git a/src/server/api/endpoints/hashtags/list.ts b/src/server/api/endpoints/hashtags/list.ts
index f454d47fed..7996c81669 100644
--- a/src/server/api/endpoints/hashtags/list.ts
+++ b/src/server/api/endpoints/hashtags/list.ts
@@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../define';
-import Hashtag from '../../../../models/hashtag';
+import { Hashtags } from '../../../../models';
export const meta = {
tags: ['hashtags'],
@@ -54,40 +54,39 @@ export const meta = {
},
};
-const sort: any = {
- '+mentionedUsers': { mentionedUsersCount: -1 },
- '-mentionedUsers': { mentionedUsersCount: 1 },
- '+mentionedLocalUsers': { mentionedLocalUsersCount: -1 },
- '-mentionedLocalUsers': { mentionedLocalUsersCount: 1 },
- '+mentionedRemoteUsers': { mentionedRemoteUsersCount: -1 },
- '-mentionedRemoteUsers': { mentionedRemoteUsersCount: 1 },
- '+attachedUsers': { attachedUsersCount: -1 },
- '-attachedUsers': { attachedUsersCount: 1 },
- '+attachedLocalUsers': { attachedLocalUsersCount: -1 },
- '-attachedLocalUsers': { attachedLocalUsersCount: 1 },
- '+attachedRemoteUsers': { attachedRemoteUsersCount: -1 },
- '-attachedRemoteUsers': { attachedRemoteUsersCount: 1 },
-};
-
export default define(meta, async (ps, me) => {
- const q = {} as any;
- if (ps.attachedToUserOnly) q.attachedUsersCount = { $ne: 0 };
- if (ps.attachedToLocalUserOnly) q.attachedLocalUsersCount = { $ne: 0 };
- if (ps.attachedToRemoteUserOnly) q.attachedRemoteUsersCount = { $ne: 0 };
- const tags = await Hashtag
- .find(q, {
- limit: ps.limit,
- sort: sort[ps.sort],
- fields: {
- tag: true,
- mentionedUsersCount: true,
- mentionedLocalUsersCount: true,
- mentionedRemoteUsersCount: true,
- attachedUsersCount: true,
- attachedLocalUsersCount: true,
- attachedRemoteUsersCount: true
- }
- });
+ const query = Hashtags.createQueryBuilder('tag');
+
+ if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0');
+ if (ps.attachedToLocalUserOnly) query.andWhere('tag.attachedLocalUsersCount != 0');
+ if (ps.attachedToRemoteUserOnly) query.andWhere('tag.attachedRemoteUsersCount != 0');
+
+ switch (ps.sort) {
+ case '+mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'DESC'); break;
+ case '-mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'ASC'); break;
+ case '+mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'DESC'); break;
+ case '-mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'ASC'); break;
+ case '+mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'DESC'); break;
+ case '-mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'ASC'); break;
+ case '+attachedUsers': query.orderBy('tag.attachedUsersCount', 'DESC'); break;
+ case '-attachedUsers': query.orderBy('tag.attachedUsersCount', 'ASC'); break;
+ case '+attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'DESC'); break;
+ case '-attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'ASC'); break;
+ case '+attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'DESC'); break;
+ case '-attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'ASC'); break;
+ }
+
+ query.select([
+ 'tag.name',
+ 'tag.mentionedUsersCount',
+ 'tag.mentionedLocalUsersCount',
+ 'tag.mentionedRemoteUsersCount',
+ 'tag.attachedUsersCount',
+ 'tag.attachedLocalUsersCount',
+ 'tag.attachedRemoteUsersCount',
+ ]);
+
+ const tags = await query.take(ps.limit).getMany();
return tags;
});
diff --git a/src/server/api/endpoints/hashtags/search.ts b/src/server/api/endpoints/hashtags/search.ts
index 19b2975adf..fd91a2ebcc 100644
--- a/src/server/api/endpoints/hashtags/search.ts
+++ b/src/server/api/endpoints/hashtags/search.ts
@@ -1,7 +1,6 @@
import $ from 'cafy';
-import Hashtag from '../../../../models/hashtag';
import define from '../../define';
-import * as escapeRegexp from 'escape-regexp';
+import { Hashtags } from '../../../../models';
export const meta = {
desc: {
@@ -46,16 +45,12 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const hashtags = await Hashtag
- .find({
- tag: new RegExp('^' + escapeRegexp(ps.query.toLowerCase()))
- }, {
- sort: {
- count: -1
- },
- limit: ps.limit,
- skip: ps.offset
- });
+ const hashtags = await Hashtags.createQueryBuilder('tag')
+ .where('tag.name like :q', { q: ps.query.toLowerCase() + '%' })
+ .orderBy('tag.count', 'DESC')
+ .take(ps.limit)
+ .skip(ps.offset)
+ .getMany();
- return hashtags.map(tag => tag.tag);
+ return hashtags.map(tag => tag.name);
});
diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts
index 8b8dd70245..c750e72a15 100644
--- a/src/server/api/endpoints/hashtags/trend.ts
+++ b/src/server/api/endpoints/hashtags/trend.ts
@@ -1,17 +1,19 @@
-import Note from '../../../../models/note';
-import { erase } from '../../../../prelude/array';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
+import { Notes } from '../../../../models';
+import { Note } from '../../../../models/entities/note';
/*
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる
+
+..が理想だけどPostgreSQLでどうするのか分からないので単に「直近Aの内に投稿されたユニーク投稿数が多いハッシュタグ」で妥協する
*/
const rangeA = 1000 * 60 * 30; // 30分
-const rangeB = 1000 * 60 * 120; // 2時間
-const coefficient = 1.25; // 「n倍」の部分
-const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
+//const rangeB = 1000 * 60 * 120; // 2時間
+//const coefficient = 1.25; // 「n倍」の部分
+//const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
const max = 5;
@@ -23,92 +25,47 @@ export const meta = {
export default define(meta, async () => {
const instance = await fetchMeta();
- const hidedTags = instance.hidedTags.map(t => t.toLowerCase());
+ const hiddenTags = instance.hiddenTags.map(t => t.toLowerCase());
- //#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計
- const data = await Note.aggregate([{
- $match: {
- createdAt: {
- $gt: new Date(Date.now() - rangeA)
- },
- tagsLower: {
- $exists: true,
- $ne: []
- }
- }
- }, {
- $unwind: '$tagsLower'
- }, {
- $group: {
- _id: { tag: '$tagsLower', userId: '$userId' }
- }
- }]) as {
- _id: {
- tag: string;
- userId: any;
- }
- }[];
- //#endregion
+ const tagNotes = await Notes.createQueryBuilder('note')
+ .where(`note.createdAt > :date`, { date: new Date(Date.now() - rangeA) })
+ .andWhere(`note.tags != '{}'`)
+ .select(['note.tags', 'note.userId'])
+ .getMany();
- if (data.length == 0) {
+ if (tagNotes.length === 0) {
return [];
}
const tags: {
name: string;
- count: number;
+ users: Note['userId'][];
}[] = [];
- // カウント
- for (const x of data.map(x => x._id).filter(x => !hidedTags.includes(x.tag))) {
- const i = tags.findIndex(tag => tag.name == x.tag);
- if (i != -1) {
- tags[i].count++;
- } else {
- tags.push({
- name: x.tag,
- count: 1
- });
- }
- }
-
- // 最低要求投稿者数を下回るならカットする
- const limitedTags = tags.filter(tag => tag.count >= requiredUsers);
+ for (const note of tagNotes) {
+ for (const tag of note.tags) {
+ if (hiddenTags.includes(tag)) continue;
- //#region 2. 1で取得したそれぞれのタグについて、「直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上」かどうかを判定する
- const hotsPromises = limitedTags.map(async tag => {
- const passedCount = (await Note.distinct('userId', {
- tagsLower: tag.name,
- createdAt: {
- $lt: new Date(Date.now() - rangeA),
- $gt: new Date(Date.now() - rangeB)
+ const x = tags.find(x => x.name === tag);
+ if (x) {
+ if (!x.users.includes(note.userId)) {
+ x.users.push(note.userId);
+ }
+ } else {
+ tags.push({
+ name: tag,
+ users: [note.userId]
+ });
}
- }) as any).length;
-
- if (tag.count >= (passedCount * coefficient)) {
- return tag;
- } else {
- return null;
}
- });
- //#endregion
+ }
// タグを人気順に並べ替え
- let hots = erase(null, await Promise.all(hotsPromises))
- .sort((a, b) => b.count - a.count)
+ const hots = tags
+ .sort((a, b) => b.users.length - a.users.length)
.map(tag => tag.name)
.slice(0, max);
- //#region 3. もし上記の方法でのトレンド抽出の結果、求められているタグ数に達しなければ「ただ単に現在投稿数が多いハッシュタグ」に切り替える
- if (hots.length < max) {
- hots = hots.concat(tags
- .filter(tag => hots.indexOf(tag.name) == -1)
- .sort((a, b) => b.count - a.count)
- .map(tag => tag.name)
- .slice(0, max - hots.length));
- }
- //#endregion
-
//#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する
const countPromises: Promise<any[]>[] = [];
@@ -118,23 +75,25 @@ export default define(meta, async () => {
const interval = 1000 * 60 * 10;
for (let i = 0; i < range; i++) {
- countPromises.push(Promise.all(hots.map(tag => Note.distinct('userId', {
- tagsLower: tag,
- createdAt: {
- $lt: new Date(Date.now() - (interval * i)),
- $gt: new Date(Date.now() - (interval * (i + 1)))
- }
- }))));
+ countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
+ .select('count(distinct note.userId)')
+ .where(':tag = ANY(note.tags)', { tag: tag })
+ .andWhere('note.createdAt < :lt', { lt: new Date(Date.now() - (interval * i)) })
+ .andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * (i + 1))) })
+ .getRawOne()
+ .then(x => parseInt(x.count, 10))
+ )));
}
const countsLog = await Promise.all(countPromises);
- const totalCounts: any = await Promise.all(hots.map(tag => Note.distinct('userId', {
- tagsLower: tag,
- createdAt: {
- $gt: new Date(Date.now() - (interval * range))
- }
- })));
+ const totalCounts: any = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
+ .select('count(distinct note.userId)')
+ .where(':tag = ANY(note.tags)', { tag: tag })
+ .andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * range)) })
+ .getRawOne()
+ .then(x => parseInt(x.count, 10))
+ ));
//#endregion
const stats = hots.map((tag, i) => ({
diff --git a/src/server/api/endpoints/hashtags/users.ts b/src/server/api/endpoints/hashtags/users.ts
index 4b047aee95..20cba96d0e 100644
--- a/src/server/api/endpoints/hashtags/users.ts
+++ b/src/server/api/endpoints/hashtags/users.ts
@@ -1,6 +1,6 @@
import $ from 'cafy';
-import User, { pack } from '../../../../models/user';
import define from '../../define';
+import { Users } from '../../../../models';
export const meta = {
requireCredential: false,
@@ -54,39 +54,32 @@ export const meta = {
},
};
-const sort: any = {
- '+follower': { followersCount: -1 },
- '-follower': { followersCount: 1 },
- '+createdAt': { createdAt: -1 },
- '-createdAt': { createdAt: 1 },
- '+updatedAt': { updatedAt: -1 },
- '-updatedAt': { updatedAt: 1 },
-};
-
export default define(meta, async (ps, me) => {
- const q = {
- tags: ps.tag,
- $and: []
- } as any;
+ const query = Users.createQueryBuilder('user')
+ .where(':tag = ANY(user.tags)', { tag: ps.tag });
+
+ const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5));
+
+ if (ps.state === 'alive') {
+ query.andWhere('user.updatedAt > :date', { date: recent });
+ }
- // state
- q.$and.push(
- ps.state == 'alive' ? { updatedAt: { $gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)) } } :
- {}
- );
+ if (ps.origin === 'local') {
+ query.andWhere('user.host IS NULL');
+ } else if (ps.origin === 'remote') {
+ query.andWhere('user.host IS NOT NULL');
+ }
- // origin
- q.$and.push(
- ps.origin == 'local' ? { host: null } :
- ps.origin == 'remote' ? { host: { $ne: null } } :
- {}
- );
+ switch (ps.sort) {
+ case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
+ case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
+ case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
+ case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
+ case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break;
+ case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
+ }
- const users = await User
- .find(q, {
- limit: ps.limit,
- sort: sort[ps.sort],
- });
+ const users = await query.take(ps.limit).getMany();
- return await Promise.all(users.map(user => pack(user, me, { detail: true })));
+ return await Users.packMany(users, me, { detail: true });
});
diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts
index 7b50cc76c2..afad38c469 100644
--- a/src/server/api/endpoints/i.ts
+++ b/src/server/api/endpoints/i.ts
@@ -1,5 +1,5 @@
-import { pack } from '../../../models/user';
import define from '../define';
+import { Users } from '../../../models';
export const meta = {
stability: 'stable',
@@ -22,7 +22,7 @@ export const meta = {
export default define(meta, async (ps, user, app) => {
const isSecure = user != null && app == null;
- return await pack(user, user, {
+ return await Users.pack(user, user, {
detail: true,
includeHasUnreadNotes: true,
includeSecrets: isSecure
diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts
index 556354c386..8ccb09b8b7 100644
--- a/src/server/api/endpoints/i/2fa/done.ts
+++ b/src/server/api/endpoints/i/2fa/done.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
import * as speakeasy from 'speakeasy';
-import User from '../../../../../models/user';
import define from '../../../define';
+import { Users } from '../../../../../models';
export const meta = {
requireCredential: true,
@@ -32,12 +32,8 @@ export default define(meta, async (ps, user) => {
throw new Error('not verified');
}
- await User.update(user._id, {
- $set: {
- 'twoFactorSecret': user.twoFactorTempSecret,
- 'twoFactorEnabled': true
- }
+ await Users.update(user.id, {
+ twoFactorSecret: user.twoFactorTempSecret,
+ twoFactorEnabled: true
});
-
- return;
});
diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts
index 302b51ec0b..5efe77900a 100644
--- a/src/server/api/endpoints/i/2fa/register.ts
+++ b/src/server/api/endpoints/i/2fa/register.ts
@@ -2,9 +2,9 @@ import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
import * as speakeasy from 'speakeasy';
import * as QRCode from 'qrcode';
-import User from '../../../../../models/user';
import config from '../../../../../config';
import define from '../../../define';
+import { Users } from '../../../../../models';
export const meta = {
requireCredential: true,
@@ -31,10 +31,8 @@ export default define(meta, async (ps, user) => {
length: 32
});
- await User.update(user._id, {
- $set: {
- twoFactorTempSecret: secret.base32
- }
+ await Users.update(user.id, {
+ twoFactorTempSecret: secret.base32
});
// Get the data URL of the authenticator URL
diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts
index 37b2639198..fb3ecd4043 100644
--- a/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/src/server/api/endpoints/i/2fa/unregister.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
-import User from '../../../../../models/user';
import define from '../../../define';
+import { Users } from '../../../../../models';
export const meta = {
requireCredential: true,
@@ -23,11 +23,9 @@ export default define(meta, async (ps, user) => {
throw new Error('incorrect password');
}
- await User.update(user._id, {
- $set: {
- 'twoFactorSecret': null,
- 'twoFactorEnabled': false
- }
+ await Users.update(user.id, {
+ twoFactorSecret: null,
+ twoFactorEnabled: false
});
return;
diff --git a/src/server/api/endpoints/i/authorized-apps.ts b/src/server/api/endpoints/i/authorized-apps.ts
index cb8be9ed97..ebf04fcb58 100644
--- a/src/server/api/endpoints/i/authorized-apps.ts
+++ b/src/server/api/endpoints/i/authorized-apps.ts
@@ -1,7 +1,6 @@
import $ from 'cafy';
-import AccessToken from '../../../../models/access-token';
-import { pack } from '../../../../models/app';
import define from '../../define';
+import { AccessTokens, Apps } from '../../../../models';
export const meta = {
requireCredential: true,
@@ -28,18 +27,18 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Get tokens
- const tokens = await AccessToken
- .find({
- userId: user._id
- }, {
- limit: ps.limit,
- skip: ps.offset,
- sort: {
- _id: ps.sort == 'asc' ? 1 : -1
- }
- });
+ const tokens = await AccessTokens.find({
+ where: {
+ userId: user.id
+ },
+ take: ps.limit,
+ skip: ps.offset,
+ order: {
+ id: ps.sort == 'asc' ? 1 : -1
+ }
+ });
- return await Promise.all(tokens.map(token => pack(token.appId, user, {
+ return await Promise.all(tokens.map(token => Apps.pack(token.appId, user, {
detail: true
})));
});
diff --git a/src/server/api/endpoints/i/change-password.ts b/src/server/api/endpoints/i/change-password.ts
index 8ab286b4bf..f8f977200f 100644
--- a/src/server/api/endpoints/i/change-password.ts
+++ b/src/server/api/endpoints/i/change-password.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
-import User from '../../../../models/user';
import define from '../../define';
+import { Users } from '../../../../models';
export const meta = {
requireCredential: true,
@@ -31,11 +31,7 @@ export default define(meta, async (ps, user) => {
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(ps.newPassword, salt);
- await User.update(user._id, {
- $set: {
- 'password': hash
- }
+ await Users.update(user.id, {
+ password: hash
});
-
- return;
});
diff --git a/src/server/api/endpoints/i/clear-follow-request-notification.ts b/src/server/api/endpoints/i/clear-follow-request-notification.ts
deleted file mode 100644
index 38c6ec1cef..0000000000
--- a/src/server/api/endpoints/i/clear-follow-request-notification.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import User from '../../../../models/user';
-import define from '../../define';
-
-export const meta = {
- tags: ['account', 'following'],
-
- requireCredential: true,
-
- kind: 'account-write',
-
- params: {
- }
-};
-
-export default define(meta, async (ps, user) => {
- await User.update({ _id: user._id }, {
- $set: {
- pendingReceivedFollowRequestsCount: 0
- }
- });
-
- return;
-});
diff --git a/src/server/api/endpoints/i/delete-account.ts b/src/server/api/endpoints/i/delete-account.ts
index fed38eab5a..5aff74e0cc 100644
--- a/src/server/api/endpoints/i/delete-account.ts
+++ b/src/server/api/endpoints/i/delete-account.ts
@@ -1,10 +1,7 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
-import User from '../../../../models/user';
import define from '../../define';
-import { createDeleteNotesJob, createDeleteDriveFilesJob } from '../../../../queue';
-import Message from '../../../../models/messaging-message';
-import Signin from '../../../../models/signin';
+import { Users } from '../../../../models';
export const meta = {
requireCredential: true,
@@ -26,27 +23,5 @@ export default define(meta, async (ps, user) => {
throw new Error('incorrect password');
}
- await User.update({ _id: user._id }, {
- $set: {
- isDeleted: true,
- name: null,
- description: null,
- pinnedNoteIds: [],
- password: null,
- email: null,
- twitter: null,
- github: null,
- discord: null,
- profile: {},
- fields: [],
- clientSettings: {},
- }
- });
-
- Message.remove({ userId: user._id });
- Signin.remove({ userId: user._id });
- createDeleteNotesJob(user);
- createDeleteDriveFilesJob(user);
-
- return;
+ await Users.delete(user.id);
});
diff --git a/src/server/api/endpoints/i/export-blocking.ts b/src/server/api/endpoints/i/export-blocking.ts
index 346b29c79d..14d49487e8 100644
--- a/src/server/api/endpoints/i/export-blocking.ts
+++ b/src/server/api/endpoints/i/export-blocking.ts
@@ -13,6 +13,4 @@ export const meta = {
export default define(meta, async (ps, user) => {
createExportBlockingJob(user);
-
- return;
});
diff --git a/src/server/api/endpoints/i/export-following.ts b/src/server/api/endpoints/i/export-following.ts
index 5977b03105..50dd28837f 100644
--- a/src/server/api/endpoints/i/export-following.ts
+++ b/src/server/api/endpoints/i/export-following.ts
@@ -13,6 +13,4 @@ export const meta = {
export default define(meta, async (ps, user) => {
createExportFollowingJob(user);
-
- return;
});
diff --git a/src/server/api/endpoints/i/export-mute.ts b/src/server/api/endpoints/i/export-mute.ts
index 22ceff3631..1eb51cd77e 100644
--- a/src/server/api/endpoints/i/export-mute.ts
+++ b/src/server/api/endpoints/i/export-mute.ts
@@ -13,6 +13,4 @@ export const meta = {
export default define(meta, async (ps, user) => {
createExportMuteJob(user);
-
- return;
});
diff --git a/src/server/api/endpoints/i/export-notes.ts b/src/server/api/endpoints/i/export-notes.ts
index 2881aa2697..dd32c18d11 100644
--- a/src/server/api/endpoints/i/export-notes.ts
+++ b/src/server/api/endpoints/i/export-notes.ts
@@ -13,6 +13,4 @@ export const meta = {
export default define(meta, async (ps, user) => {
createExportNotesJob(user);
-
- return;
});
diff --git a/src/server/api/endpoints/i/export-user-lists.ts b/src/server/api/endpoints/i/export-user-lists.ts
index 9d7424ad89..7650ca7210 100644
--- a/src/server/api/endpoints/i/export-user-lists.ts
+++ b/src/server/api/endpoints/i/export-user-lists.ts
@@ -13,6 +13,4 @@ export const meta = {
export default define(meta, async (ps, user) => {
createExportUserListsJob(user);
-
- return;
});
diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts
index 7ea6f7b966..d2d149b2d1 100644
--- a/src/server/api/endpoints/i/favorites.ts
+++ b/src/server/api/endpoints/i/favorites.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Favorite, { packMany } from '../../../../models/favorite';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
+import { NoteFavorites } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
desc: {
@@ -23,42 +24,22 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
- }
+ },
}
};
export default define(meta, async (ps, user) => {
- const query = {
- userId: user._id
- } as any;
-
- const sort = {
- _id: -1
- };
-
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- }
+ const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId)
+ .andWhere(`favorite.userId = :meId`, { meId: user.id })
+ .leftJoinAndSelect('favorite.note', 'note');
- // Get favorites
- const favorites = await Favorite
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const favorites = await query
+ .take(ps.limit)
+ .getMany();
- return await packMany(favorites, user);
+ return await NoteFavorites.packMany(favorites, user);
});
diff --git a/src/server/api/endpoints/i/import-following.ts b/src/server/api/endpoints/i/import-following.ts
index f188291bc2..deafec18ec 100644
--- a/src/server/api/endpoints/i/import-following.ts
+++ b/src/server/api/endpoints/i/import-following.ts
@@ -1,10 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { createImportFollowingJob } from '../../../../queue';
import ms = require('ms');
-import DriveFile from '../../../../models/drive-file';
import { ApiError } from '../../error';
+import { DriveFiles } from '../../../../models';
export const meta = {
secure: true,
@@ -17,7 +17,6 @@ export const meta = {
params: {
fileId: {
validator: $.type(ID),
- transform: transform,
}
},
@@ -49,16 +48,12 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const file = await DriveFile.findOne({
- _id: ps.fileId
- });
+ const file = await DriveFiles.findOne(ps.fileId);
if (file == null) throw new ApiError(meta.errors.noSuchFile);
- //if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
- if (file.length > 50000) throw new ApiError(meta.errors.tooBigFile);
- if (file.length === 0) throw new ApiError(meta.errors.emptyFile);
+ //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+ if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
+ if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
- createImportFollowingJob(user, file._id);
-
- return;
+ createImportFollowingJob(user, file.id);
});
diff --git a/src/server/api/endpoints/i/import-user-lists.ts b/src/server/api/endpoints/i/import-user-lists.ts
index ed3085e5f8..b7d9d029b7 100644
--- a/src/server/api/endpoints/i/import-user-lists.ts
+++ b/src/server/api/endpoints/i/import-user-lists.ts
@@ -1,10 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { createImportUserListsJob } from '../../../../queue';
import ms = require('ms');
-import DriveFile from '../../../../models/drive-file';
import { ApiError } from '../../error';
+import { DriveFiles } from '../../../../models';
export const meta = {
secure: true,
@@ -17,7 +17,6 @@ export const meta = {
params: {
fileId: {
validator: $.type(ID),
- transform: transform,
}
},
@@ -49,16 +48,12 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const file = await DriveFile.findOne({
- _id: ps.fileId
- });
+ const file = await DriveFiles.findOne(ps.fileId);
if (file == null) throw new ApiError(meta.errors.noSuchFile);
- //if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
- if (file.length > 30000) throw new ApiError(meta.errors.tooBigFile);
- if (file.length === 0) throw new ApiError(meta.errors.emptyFile);
+ //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+ if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile);
+ if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
- createImportUserListsJob(user, file._id);
-
- return;
+ createImportUserListsJob(user, file.id);
});
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index d3e3064abd..9b016e0a2d 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -1,11 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Notification from '../../../../models/notification';
-import { packMany } from '../../../../models/notification';
-import { getFriendIds } from '../../common/get-friends';
-import read from '../../common/read-notification';
+import { ID } from '../../../../misc/cafy-id';
+import { readNotification } from '../../common/read-notification';
import define from '../../define';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notifications, Followings, Mutings } from '../../../../models';
export const meta = {
desc: {
@@ -17,7 +15,7 @@ export const meta = {
requireCredential: true,
- kind: 'account-read',
+ kind: 'read:notifications',
params: {
limit: {
@@ -27,12 +25,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
following: {
@@ -46,12 +42,12 @@ export const meta = {
},
includeTypes: {
- validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote', 'receiveFollowRequest'])),
+ validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])),
default: [] as string[]
},
excludeTypes: {
- validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote', 'receiveFollowRequest'])),
+ validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])),
default: [] as string[]
}
},
@@ -65,63 +61,38 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const hideUserIds = await getHideUserIds(user);
+ const followingQuery = Followings.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: user.id });
- const query = {
- notifieeId: user._id,
- $and: [{
- notifierId: {
- $nin: hideUserIds
- }
- }]
- } as any;
+ const mutingQuery = Mutings.createQueryBuilder('muting')
+ .select('muting.muteeId')
+ .where('muting.muterId = :muterId', { muterId: user.id });
- const sort = {
- _id: -1
- };
+ const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
+ .andWhere(`notification.notifieeId = :meId`, { meId: user.id })
+ .leftJoinAndSelect('notification.notifier', 'notifier');
- if (ps.following) {
- // ID list of the user itself and other users who the user follows
- const followingIds = await getFriendIds(user._id);
-
- query.$and.push({
- notifierId: {
- $in: followingIds
- }
- });
- }
+ query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`);
+ query.setParameters(mutingQuery.getParameters());
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
+ if (ps.following) {
+ query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id });
+ query.setParameters(followingQuery.getParameters());
}
if (ps.includeTypes.length > 0) {
- query.type = {
- $in: ps.includeTypes
- };
+ query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes });
} else if (ps.excludeTypes.length > 0) {
- query.type = {
- $nin: ps.excludeTypes
- };
+ query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes });
}
- const notifications = await Notification
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const notifications = await query.take(ps.limit).getMany();
// Mark all as read
if (notifications.length > 0 && ps.markAsRead) {
- read(user._id, notifications);
+ readNotification(user.id, notifications.map(x => x.id));
}
- return await packMany(notifications);
+ return await Notifications.packMany(notifications);
});
diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts
index 8d853d45c8..ac104b19f9 100644
--- a/src/server/api/endpoints/i/pin.ts
+++ b/src/server/api/endpoints/i/pin.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import { pack } from '../../../../models/user';
+import { ID } from '../../../../misc/cafy-id';
import { addPinned } from '../../../../services/i/pin';
import define from '../../define';
import { ApiError } from '../../error';
+import { Users } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -16,12 +16,11 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
@@ -58,7 +57,7 @@ export default define(meta, async (ps, user) => {
throw e;
});
- return await pack(user, user, {
+ return await Users.pack(user, user, {
detail: true
});
});
diff --git a/src/server/api/endpoints/i/read-all-messaging-messages.ts b/src/server/api/endpoints/i/read-all-messaging-messages.ts
index bbbfa0d7b3..e8ada277e9 100644
--- a/src/server/api/endpoints/i/read-all-messaging-messages.ts
+++ b/src/server/api/endpoints/i/read-all-messaging-messages.ts
@@ -1,7 +1,6 @@
-import User from '../../../../models/user';
import { publishMainStream } from '../../../../services/stream';
-import Message from '../../../../models/messaging-message';
import define from '../../define';
+import { MessagingMessages } from '../../../../models';
export const meta = {
desc: {
@@ -13,7 +12,7 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
}
@@ -21,24 +20,12 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Update documents
- await Message.update({
- recipientId: user._id,
+ await MessagingMessages.update({
+ recipientId: user.id,
isRead: false
}, {
- $set: {
- isRead: true
- }
- }, {
- multi: true
- });
-
- User.update({ _id: user._id }, {
- $set: {
- hasUnreadMessagingMessage: false
- }
+ isRead: true
});
- publishMainStream(user._id, 'readAllMessagingMessages');
-
- return;
+ publishMainStream(user.id, 'readAllMessagingMessages');
});
diff --git a/src/server/api/endpoints/i/read-all-unread-notes.ts b/src/server/api/endpoints/i/read-all-unread-notes.ts
index 742c2d9908..cc8ebf58ec 100644
--- a/src/server/api/endpoints/i/read-all-unread-notes.ts
+++ b/src/server/api/endpoints/i/read-all-unread-notes.ts
@@ -1,7 +1,6 @@
-import User from '../../../../models/user';
import { publishMainStream } from '../../../../services/stream';
-import NoteUnread from '../../../../models/note-unread';
import define from '../../define';
+import { NoteUnreads } from '../../../../models';
export const meta = {
desc: {
@@ -13,7 +12,7 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
}
@@ -21,20 +20,11 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Remove documents
- await NoteUnread.remove({
- userId: user._id
- });
-
- User.update({ _id: user._id }, {
- $set: {
- hasUnreadMentions: false,
- hasUnreadSpecifiedNotes: false
- }
+ await NoteUnreads.delete({
+ userId: user.id
});
// 全て既読になったイベントを発行
- publishMainStream(user._id, 'readAllUnreadMentions');
- publishMainStream(user._id, 'readAllUnreadSpecifiedNotes');
-
- return;
+ publishMainStream(user.id, 'readAllUnreadMentions');
+ publishMainStream(user.id, 'readAllUnreadSpecifiedNotes');
});
diff --git a/src/server/api/endpoints/i/regenerate-token.ts b/src/server/api/endpoints/i/regenerate-token.ts
index ad10b99b36..729c1a300a 100644
--- a/src/server/api/endpoints/i/regenerate-token.ts
+++ b/src/server/api/endpoints/i/regenerate-token.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
-import User from '../../../../models/user';
import { publishMainStream } from '../../../../services/stream';
import generateUserToken from '../../common/generate-native-user-token';
import define from '../../define';
+import { Users } from '../../../../models';
export const meta = {
requireCredential: true,
@@ -28,14 +28,10 @@ export default define(meta, async (ps, user) => {
// Generate secret
const secret = generateUserToken();
- await User.update(user._id, {
- $set: {
- 'token': secret
- }
+ await Users.update(user.id, {
+ token: secret
});
// Publish event
- publishMainStream(user._id, 'myTokenRegenerated');
-
- return;
+ publishMainStream(user.id, 'myTokenRegenerated');
});
diff --git a/src/server/api/endpoints/i/signin-history.ts b/src/server/api/endpoints/i/signin-history.ts
index 87160a9f91..e9ae19d734 100644
--- a/src/server/api/endpoints/i/signin-history.ts
+++ b/src/server/api/endpoints/i/signin-history.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Signin, { pack } from '../../../../models/signin';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
+import { Signins } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
requireCredential: true,
@@ -16,41 +17,19 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
}
}
};
export default define(meta, async (ps, user) => {
- const query = {
- userId: user._id
- } as any;
+ const query = makePaginationQuery(Signins.createQueryBuilder('signin'), ps.sinceId, ps.untilId)
+ .andWhere(`signin.userId = :meId`, { meId: user.id });
- const sort = {
- _id: -1
- };
+ const history = await query.take(ps.limit).getMany();
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- }
-
- const history = await Signin
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
-
- return await Promise.all(history.map(record => pack(record)));
+ return await Promise.all(history.map(record => Signins.pack(record)));
});
diff --git a/src/server/api/endpoints/i/unpin.ts b/src/server/api/endpoints/i/unpin.ts
index 184d46f2c3..4688533578 100644
--- a/src/server/api/endpoints/i/unpin.ts
+++ b/src/server/api/endpoints/i/unpin.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import { pack } from '../../../../models/user';
+import { ID } from '../../../../misc/cafy-id';
import { removePinned } from '../../../../services/i/pin';
import define from '../../define';
import { ApiError } from '../../error';
+import { Users } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -16,12 +16,11 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
@@ -44,7 +43,7 @@ export default define(meta, async (ps, user) => {
throw e;
});
- return await pack(user, user, {
+ return await Users.pack(user, user, {
detail: true
});
});
diff --git a/src/server/api/endpoints/i/update-client-setting.ts b/src/server/api/endpoints/i/update-client-setting.ts
index 79cd04e169..edbfe28f35 100644
--- a/src/server/api/endpoints/i/update-client-setting.ts
+++ b/src/server/api/endpoints/i/update-client-setting.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import User from '../../../../models/user';
import { publishMainStream } from '../../../../services/stream';
import define from '../../define';
+import { Users } from '../../../../models';
export const meta = {
requireCredential: true,
@@ -10,7 +10,7 @@ export const meta = {
params: {
name: {
- validator: $.str
+ validator: $.str.match(/^[a-zA-Z]+$/)
},
value: {
@@ -20,18 +20,18 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const x: any = {};
- x[`clientSettings.${ps.name}`] = ps.value;
-
- await User.update(user._id, {
- $set: x
- });
+ await Users.createQueryBuilder().update()
+ .set({
+ clientData: {
+ [ps.name]: ps.value
+ },
+ })
+ .where('id = :id', { id: user.id })
+ .execute();
// Publish event
- publishMainStream(user._id, 'clientSettingUpdated', {
+ publishMainStream(user.id, 'clientSettingUpdated', {
key: ps.name,
value: ps.value
});
-
- return;
});
diff --git a/src/server/api/endpoints/i/update-email.ts b/src/server/api/endpoints/i/update-email.ts
index c90462d850..253017535f 100644
--- a/src/server/api/endpoints/i/update-email.ts
+++ b/src/server/api/endpoints/i/update-email.ts
@@ -1,5 +1,4 @@
import $ from 'cafy';
-import User, { pack } from '../../../../models/user';
import { publishMainStream } from '../../../../services/stream';
import define from '../../define';
import * as nodemailer from 'nodemailer';
@@ -9,6 +8,7 @@ import config from '../../../../config';
import * as ms from 'ms';
import * as bcrypt from 'bcryptjs';
import { apiLogger } from '../../logger';
+import { Users } from '../../../../models';
export const meta = {
requireCredential: true,
@@ -39,29 +39,25 @@ export default define(meta, async (ps, user) => {
throw new Error('incorrect password');
}
- await User.update(user._id, {
- $set: {
- email: ps.email,
- emailVerified: false,
- emailVerifyCode: null
- }
+ await Users.update(user.id, {
+ email: ps.email,
+ emailVerified: false,
+ emailVerifyCode: null
});
- const iObj = await pack(user._id, user, {
+ const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
});
// Publish meUpdated event
- publishMainStream(user._id, 'meUpdated', iObj);
+ publishMainStream(user.id, 'meUpdated', iObj);
if (ps.email != null) {
const code = rndstr('a-z0-9', 16);
- await User.update(user._id, {
- $set: {
- emailVerifyCode: code
- }
+ await Users.update(user.id, {
+ emailVerifyCode: code
});
const meta = await fetchMeta();
@@ -84,7 +80,7 @@ export default define(meta, async (ps, user) => {
transporter.sendMail({
from: meta.email,
to: ps.email,
- subject: meta.name,
+ subject: meta.name || 'Misskey',
text: `To verify email, please click this link: ${link}`
}, (error, info) => {
if (error) {
diff --git a/src/server/api/endpoints/i/update-home.ts b/src/server/api/endpoints/i/update-home.ts
deleted file mode 100644
index e2c319887f..0000000000
--- a/src/server/api/endpoints/i/update-home.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import $ from 'cafy';
-import User from '../../../../models/user';
-import { publishMainStream } from '../../../../services/stream';
-import define from '../../define';
-
-export const meta = {
- requireCredential: true,
-
- secure: true,
-
- params: {
- home: {
- validator: $.arr($.obj({
- name: $.str,
- id: $.str,
- place: $.str,
- data: $.obj()
- }).strict())
- }
- }
-};
-
-export default define(meta, async (ps, user) => {
- await User.update(user._id, {
- $set: {
- 'clientSettings.home': ps.home
- }
- });
-
- publishMainStream(user._id, 'homeUpdated', ps.home);
-
- return;
-});
diff --git a/src/server/api/endpoints/i/update-mobile-home.ts b/src/server/api/endpoints/i/update-mobile-home.ts
deleted file mode 100644
index 642e2b3e09..0000000000
--- a/src/server/api/endpoints/i/update-mobile-home.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import $ from 'cafy';
-import User from '../../../../models/user';
-import { publishMainStream } from '../../../../services/stream';
-import define from '../../define';
-
-export const meta = {
- requireCredential: true,
-
- secure: true,
-
- params: {
- home: {
- validator: $.arr($.obj({
- name: $.str,
- id: $.str,
- data: $.obj()
- }).strict())
- }
- }
-};
-
-export default define(meta, async (ps, user) => {
- await User.update(user._id, {
- $set: {
- 'clientSettings.mobileHome': ps.home
- }
- });
-
- publishMainStream(user._id, 'mobileHomeUpdated', ps.home);
-
- return;
-});
diff --git a/src/server/api/endpoints/i/update-widget.ts b/src/server/api/endpoints/i/update-widget.ts
deleted file mode 100644
index 67d342278d..0000000000
--- a/src/server/api/endpoints/i/update-widget.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import $ from 'cafy';
-import User from '../../../../models/user';
-import { publishMainStream } from '../../../../services/stream';
-import define from '../../define';
-
-export const meta = {
- requireCredential: true,
-
- secure: true,
-
- params: {
- id: {
- validator: $.str
- },
-
- data: {
- validator: $.obj()
- }
- }
-};
-
-export default define(meta, async (ps, user) => {
- if (ps.id == null && ps.data == null) throw new Error('you need to set id and data params if home param unset');
-
- let widget;
-
- //#region Desktop home
- if (widget == null && user.clientSettings.home) {
- const desktopHome = user.clientSettings.home;
- widget = desktopHome.find((w: any) => w.id == ps.id);
- if (widget) {
- widget.data = ps.data;
-
- await User.update(user._id, {
- $set: {
- 'clientSettings.home': desktopHome
- }
- });
- }
- }
- //#endregion
-
- //#region Mobile home
- if (widget == null && user.clientSettings.mobileHome) {
- const mobileHome = user.clientSettings.mobileHome;
- widget = mobileHome.find((w: any) => w.id == ps.id);
- if (widget) {
- widget.data = ps.data;
-
- await User.update(user._id, {
- $set: {
- 'clientSettings.mobileHome': mobileHome
- }
- });
- }
- }
- //#endregion
-
- //#region Deck
- if (widget == null && user.clientSettings.deck && user.clientSettings.deck.columns) {
- const deck = user.clientSettings.deck;
- for (const c of deck.columns.filter((c: any) => c.type == 'widgets')) {
- for (const w of c.widgets.filter((w: any) => w.id == ps.id)) {
- widget = w;
- }
- }
- if (widget) {
- widget.data = ps.data;
-
- await User.update(user._id, {
- $set: {
- 'clientSettings.deck': deck
- }
- });
- }
- }
- //#endregion
-
- if (widget) {
- publishMainStream(user._id, 'widgetUpdated', {
- id: ps.id, data: ps.data
- });
-
- return;
- } else {
- throw new Error('widget not found');
- }
-});
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 099ef33990..f3e5d41021 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -1,18 +1,16 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
+import { ID } from '../../../../misc/cafy-id';
import { publishMainStream } from '../../../../services/stream';
-import DriveFile from '../../../../models/drive-file';
import acceptAllFollowRequests from '../../../../services/following/requests/accept-all';
import { publishToFollowers } from '../../../../services/i/update';
import define from '../../define';
-import getDriveFileUrl from '../../../../misc/get-drive-file-url';
import { parse, parsePlain } from '../../../../mfm/parse';
import extractEmojis from '../../../../misc/extract-emojis';
import extractHashtags from '../../../../misc/extract-hashtags';
import * as langmap from 'langmap';
import { updateHashtag } from '../../../../services/update-hashtag';
import { ApiError } from '../../error';
+import { Users, DriveFiles } from '../../../../models';
export const meta = {
desc: {
@@ -24,18 +22,18 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
name: {
- validator: $.optional.nullable.str.pipe(isValidName),
+ validator: $.optional.nullable.str.pipe(Users.isValidName),
desc: {
'ja-JP': '名前(ハンドルネームやニックネーム)'
}
},
description: {
- validator: $.optional.nullable.str.pipe(isValidDescription),
+ validator: $.optional.nullable.str.pipe(Users.isValidDescription),
desc: {
'ja-JP': 'アカウントの説明や自己紹介'
}
@@ -49,14 +47,14 @@ export const meta = {
},
location: {
- validator: $.optional.nullable.str.pipe(isValidLocation),
+ validator: $.optional.nullable.str.pipe(Users.isValidLocation),
desc: {
'ja-JP': '住んでいる地域、所在'
}
},
birthday: {
- validator: $.optional.nullable.str.pipe(isValidBirthday),
+ validator: $.optional.nullable.str.pipe(Users.isValidBirthday),
desc: {
'ja-JP': '誕生日 (YYYY-MM-DD形式)'
}
@@ -64,7 +62,6 @@ export const meta = {
avatarId: {
validator: $.optional.nullable.type(ID),
- transform: transform,
desc: {
'ja-JP': 'アイコンに設定する画像のドライブファイルID'
}
@@ -72,20 +69,11 @@ export const meta = {
bannerId: {
validator: $.optional.nullable.type(ID),
- transform: transform,
desc: {
'ja-JP': 'バナーに設定する画像のドライブファイルID'
}
},
- wallpaperId: {
- validator: $.optional.nullable.type(ID),
- transform: transform,
- desc: {
- 'ja-JP': '壁紙に設定する画像のドライブファイルID'
- }
- },
-
isLocked: {
validator: $.optional.bool,
desc: {
@@ -171,116 +159,76 @@ export default define(meta, async (ps, user, app) => {
if (ps.name !== undefined) updates.name = ps.name;
if (ps.description !== undefined) updates.description = ps.description;
if (ps.lang !== undefined) updates.lang = ps.lang;
- if (ps.location !== undefined) updates['profile.location'] = ps.location;
- if (ps.birthday !== undefined) updates['profile.birthday'] = ps.birthday;
+ if (ps.location !== undefined) updates.location = ps.location;
+ if (ps.birthday !== undefined) updates.birthday = ps.birthday;
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
- if (ps.wallpaperId !== undefined) updates.wallpaperId = ps.wallpaperId;
if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot == 'boolean') updates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed == 'boolean') updates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat;
- if (typeof ps.autoWatch == 'boolean') updates['settings.autoWatch'] = ps.autoWatch;
- if (typeof ps.alwaysMarkNsfw == 'boolean') updates['settings.alwaysMarkNsfw'] = ps.alwaysMarkNsfw;
+ if (typeof ps.autoWatch == 'boolean') updates.autoWatch = ps.autoWatch;
+ if (typeof ps.alwaysMarkNsfw == 'boolean') updates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
if (ps.avatarId) {
- const avatar = await DriveFile.findOne({
- _id: ps.avatarId
- });
+ const avatar = await DriveFiles.findOne(ps.avatarId);
- if (avatar == null) throw new ApiError(meta.errors.noSuchAvatar);
- if (!avatar.contentType.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
+ if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
+ if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
- if (avatar.metadata.deletedAt) {
- updates.avatarUrl = null;
- } else {
- updates.avatarUrl = getDriveFileUrl(avatar, true);
+ updates.avatarUrl = avatar.thumbnailUrl;
- if (avatar.metadata.properties.avgColor) {
- updates.avatarColor = avatar.metadata.properties.avgColor;
- }
+ if (avatar.properties.avgColor) {
+ updates.avatarColor = avatar.properties.avgColor;
}
}
if (ps.bannerId) {
- const banner = await DriveFile.findOne({
- _id: ps.bannerId
- });
-
- if (banner == null) throw new ApiError(meta.errors.noSuchBanner);
- if (!banner.contentType.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
-
- if (banner.metadata.deletedAt) {
- updates.bannerUrl = null;
- } else {
- updates.bannerUrl = getDriveFileUrl(banner, false);
-
- if (banner.metadata.properties.avgColor) {
- updates.bannerColor = banner.metadata.properties.avgColor;
- }
- }
- }
-
- if (ps.wallpaperId !== undefined) {
- if (ps.wallpaperId === null) {
- updates.wallpaperUrl = null;
- updates.wallpaperColor = null;
- } else {
- const wallpaper = await DriveFile.findOne({
- _id: ps.wallpaperId
- });
+ const banner = await DriveFiles.findOne(ps.bannerId);
- if (wallpaper == null) throw new Error('wallpaper not found');
+ if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
+ if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
- if (wallpaper.metadata.deletedAt) {
- updates.wallpaperUrl = null;
- } else {
- updates.wallpaperUrl = getDriveFileUrl(wallpaper);
+ updates.bannerUrl = banner.webpublicUrl;
- if (wallpaper.metadata.properties.avgColor) {
- updates.wallpaperColor = wallpaper.metadata.properties.avgColor;
- }
- }
+ if (banner.properties.avgColor) {
+ updates.bannerColor = banner.properties.avgColor;
}
}
//#region emojis/tags
- if (updates.name != null || updates.description != null) {
- let emojis = [] as string[];
- let tags = [] as string[];
+ let emojis = [] as string[];
+ let tags = [] as string[];
- if (updates.name != null) {
- const tokens = parsePlain(updates.name);
- emojis = emojis.concat(extractEmojis(tokens));
- }
+ if (updates.name != null) {
+ const tokens = parsePlain(updates.name);
+ emojis = emojis.concat(extractEmojis(tokens));
+ }
- if (updates.description != null) {
- const tokens = parse(updates.description);
- emojis = emojis.concat(extractEmojis(tokens));
- tags = extractHashtags(tokens).map(tag => tag.toLowerCase());
- }
+ if (updates.description != null) {
+ const tokens = parse(updates.description);
+ emojis = emojis.concat(extractEmojis(tokens));
+ tags = extractHashtags(tokens).map(tag => tag.toLowerCase());
+ }
- updates.emojis = emojis;
- updates.tags = tags;
+ updates.emojis = emojis;
+ updates.tags = tags;
- // ハッシュタグ更新
- for (const tag of tags) updateHashtag(user, tag, true, true);
- for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
- }
+ // ハッシュタグ更新
+ for (const tag of tags) updateHashtag(user, tag, true, true);
+ for (const tag of user.tags.filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
//#endregion
- await User.update(user._id, {
- $set: updates
- });
+ await Users.update(user.id, updates);
- const iObj = await pack(user._id, user, {
+ const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: isSecure
});
// Publish meUpdated event
- publishMainStream(user._id, 'meUpdated', iObj);
+ publishMainStream(user.id, 'meUpdated', iObj);
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
if (user.isLocked && ps.isLocked === false) {
@@ -288,7 +236,7 @@ export default define(meta, async (ps, user, app) => {
}
// フォロワーにUpdateを配信
- publishToFollowers(user._id);
+ publishToFollowers(user.id);
return iObj;
});
diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts
index 699dc7c253..c0aec61212 100644
--- a/src/server/api/endpoints/messaging/history.ts
+++ b/src/server/api/endpoints/messaging/history.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import Mute from '../../../../models/mute';
-import Message, { pack, IMessagingMessage } from '../../../../models/messaging-message';
import define from '../../define';
+import { MessagingMessage } from '../../../../models/entities/messaging-message';
+import { MessagingMessages, Mutings } from '../../../../models';
+import { Brackets } from 'typeorm';
export const meta = {
desc: {
@@ -31,34 +32,33 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const mute = await Mute.find({
- muterId: user._id,
- deletedAt: { $exists: false }
+ const mute = await Mutings.find({
+ muterId: user.id,
});
- const history: IMessagingMessage[] = [];
+ const history: MessagingMessage[] = [];
for (let i = 0; i < ps.limit; i++) {
- const found = history.map(m => m.userId.equals(user._id) ? m.recipientId : m.userId);
+ const found = history.map(m => (m.userId === user.id) ? m.recipientId : m.userId);
- const message = await Message.findOne({
- $or: [{
- userId: user._id
- }, {
- recipientId: user._id
- }],
- $and: [{
- userId: { $nin: found },
- recipientId: { $nin: found }
- }, {
- userId: { $nin: mute.map(m => m.muteeId) },
- recipientId: { $nin: mute.map(m => m.muteeId) }
- }]
- }, {
- sort: {
- createdAt: -1
- }
- });
+ const query = MessagingMessages.createQueryBuilder('message')
+ .where(new Brackets(qb => { qb
+ .where(`message.userId = :userId`, { userId: user.id })
+ .orWhere(`message.recipientId = :userId`, { userId: user.id });
+ }))
+ .orderBy('message.createdAt', 'DESC');
+
+ if (found.length > 0) {
+ query.andWhere(`message.userId NOT IN (:...found)`, { found: found });
+ query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found });
+ }
+
+ if (mute.length > 0) {
+ query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
+ query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
+ }
+
+ const message = await query.getOne();
if (message) {
history.push(message);
@@ -67,5 +67,5 @@ export default define(meta, async (ps, user) => {
}
}
- return await Promise.all(history.map(h => pack(h._id, user)));
+ return await Promise.all(history.map(h => MessagingMessages.pack(h.id, user)));
});
diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts
index c19db45f1f..02c57b8d03 100644
--- a/src/server/api/endpoints/messaging/messages.ts
+++ b/src/server/api/endpoints/messaging/messages.ts
@@ -1,11 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Message from '../../../../models/messaging-message';
-import { pack } from '../../../../models/messaging-message';
+import { ID } from '../../../../misc/cafy-id';
import read from '../../common/read-messaging-message';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
+import { MessagingMessages } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
desc: {
@@ -22,7 +22,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -36,12 +35,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
markAsRead: {
@@ -73,43 +70,17 @@ export default define(meta, async (ps, user) => {
throw e;
});
- const query = {
- $or: [{
- userId: user._id,
- recipientId: recipient._id
- }, {
- userId: recipient._id,
- recipientId: user._id
- }]
- } as any;
+ const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
+ .andWhere(`(message.userId = :meId AND message.recipientId = :recipientId) OR (message.userId = :recipientId AND message.recipientId = :meId)`, { meId: user.id, recipientId: recipient.id });
- const sort = {
- _id: -1
- };
-
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- }
-
- const messages = await Message
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const messages = await query.getMany();
// Mark all as read
if (ps.markAsRead) {
- read(user._id, recipient._id, messages);
+ read(user.id, recipient.id, messages.map(x => x.id));
}
- return await Promise.all(messages.map(message => pack(message, user, {
+ return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
populateRecipient: false
})));
});
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index fc048e6edd..2c7e5ad2d9 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -1,17 +1,14 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Message from '../../../../../models/messaging-message';
-import { isValidText } from '../../../../../models/messaging-message';
-import User from '../../../../../models/user';
-import Mute from '../../../../../models/mute';
-import DriveFile from '../../../../../models/drive-file';
-import { pack } from '../../../../../models/messaging-message';
+import { ID } from '../../../../../misc/cafy-id';
import { publishMainStream } from '../../../../../services/stream';
import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../services/stream';
import pushSw from '../../../../../services/push-notification';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters';
+import { MessagingMessages, DriveFiles, Mutings } from '../../../../../models';
+import { MessagingMessage } from '../../../../../models/entities/messaging-message';
+import { genId } from '../../../../../misc/gen-id';
export const meta = {
desc: {
@@ -28,7 +25,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -36,12 +32,11 @@ export const meta = {
},
text: {
- validator: $.optional.str.pipe(isValidText)
+ validator: $.optional.str.pipe(MessagingMessages.isValidText)
},
fileId: {
validator: $.optional.type(ID),
- transform: transform,
}
},
@@ -78,7 +73,7 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Myself
- if (ps.userId.equals(user._id)) {
+ if (ps.userId === user.id) {
throw new ApiError(meta.errors.recipientIsYourself);
}
@@ -90,12 +85,12 @@ export default define(meta, async (ps, user) => {
let file = null;
if (ps.fileId != null) {
- file = await DriveFile.findOne({
- _id: ps.fileId,
- 'metadata.userId': user._id
+ file = await DriveFiles.findOne({
+ id: ps.fileId,
+ userId: user.id
});
- if (file === null) {
+ if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
}
@@ -105,16 +100,17 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.contentRequired);
}
- const message = await Message.insert({
+ const message = await MessagingMessages.save({
+ id: genId(),
createdAt: new Date(),
- fileId: file ? file._id : undefined,
- recipientId: recipient._id,
- text: ps.text ? ps.text.trim() : undefined,
- userId: user._id,
+ fileId: file ? file.id : null,
+ recipientId: recipient.id,
+ text: ps.text ? ps.text.trim() : null,
+ userId: user.id,
isRead: false
- });
+ } as MessagingMessage);
- const messageObj = await pack(message);
+ const messageObj = await MessagingMessages.pack(message);
// 自分のストリーム
publishMessagingStream(message.userId, message.recipientId, 'message', messageObj);
@@ -126,25 +122,17 @@ export default define(meta, async (ps, user) => {
publishMessagingIndexStream(message.recipientId, 'message', messageObj);
publishMainStream(message.recipientId, 'messagingMessage', messageObj);
- // Update flag
- User.update({ _id: recipient._id }, {
- $set: {
- hasUnreadMessagingMessage: true
- }
- });
-
// 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
setTimeout(async () => {
- const freshMessage = await Message.findOne({ _id: message._id }, { isRead: true });
+ const freshMessage = await MessagingMessages.findOne({ id: message.id });
if (freshMessage == null) return; // メッセージが削除されている場合もある
if (!freshMessage.isRead) {
//#region ただしミュートされているなら発行しない
- const mute = await Mute.find({
- muterId: recipient._id,
- deletedAt: { $exists: false }
+ const mute = await Mutings.find({
+ muterId: recipient.id,
});
const mutedUserIds = mute.map(m => m.muteeId.toString());
- if (mutedUserIds.indexOf(user._id.toString()) != -1) {
+ if (mutedUserIds.indexOf(user.id) != -1) {
return;
}
//#endregion
diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts
index 0ca12846c1..9f55caba62 100644
--- a/src/server/api/endpoints/messaging/messages/delete.ts
+++ b/src/server/api/endpoints/messaging/messages/delete.ts
@@ -1,10 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Message from '../../../../../models/messaging-message';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { publishMessagingStream } from '../../../../../services/stream';
import * as ms from 'ms';
import { ApiError } from '../../../error';
+import { MessagingMessages } from '../../../../../models';
export const meta = {
stability: 'stable',
@@ -29,7 +29,6 @@ export const meta = {
params: {
messageId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のメッセージのID',
'en-US': 'Target message ID.'
@@ -47,19 +46,17 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const message = await Message.findOne({
- _id: ps.messageId,
- userId: user._id
+ const message = await MessagingMessages.findOne({
+ id: ps.messageId,
+ userId: user.id
});
- if (message === null) {
+ if (message == null) {
throw new ApiError(meta.errors.noSuchMessage);
}
- await Message.remove({ _id: message._id });
+ await MessagingMessages.delete(message.id);
- publishMessagingStream(message.userId, message.recipientId, 'deleted', message._id);
- publishMessagingStream(message.recipientId, message.userId, 'deleted', message._id);
-
- return;
+ publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id);
+ publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id);
});
diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts
index aa8ecdc4ff..24a28285bf 100644
--- a/src/server/api/endpoints/messaging/messages/read.ts
+++ b/src/server/api/endpoints/messaging/messages/read.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Message from '../../../../../models/messaging-message';
+import { ID } from '../../../../../misc/cafy-id';
import read from '../../../common/read-messaging-message';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { MessagingMessages } from '../../../../../models';
export const meta = {
desc: {
@@ -20,7 +20,6 @@ export const meta = {
params: {
messageId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '既読にするメッセージのID',
'en-US': 'The ID of a message that you want to mark as read'
@@ -38,16 +37,14 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const message = await Message.findOne({
- _id: ps.messageId,
- recipientId: user._id
+ const message = await MessagingMessages.findOne({
+ id: ps.messageId,
+ recipientId: user.id
});
if (message == null) {
throw new ApiError(meta.errors.noSuchMessage);
}
- read(user._id, message.userId, message);
-
- return;
+ read(user.id, message.userId, [message.id]);
});
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index a297f47e0e..785f21f22b 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -1,10 +1,10 @@
import $ from 'cafy';
import * as os from 'os';
import config from '../../../config';
-import Emoji from '../../../models/emoji';
import define from '../define';
import fetchMeta from '../../../misc/fetch-meta';
import * as pkg from '../../../../package.json';
+import { Emojis } from '../../../models';
export const meta = {
stability: 'stable',
@@ -81,14 +81,11 @@ export const meta = {
export default define(meta, async (ps, me) => {
const instance = await fetchMeta();
- const emojis = await Emoji.find({ host: null }, {
- fields: {
- _id: false
- }
- });
+ const emojis = await Emojis.find({ host: null });
const response: any = {
- maintainer: instance.maintainer,
+ maintainerName: instance.maintainerName,
+ maintainerEmail: instance.maintainerEmail,
version: pkg.version,
@@ -145,17 +142,12 @@ export default define(meta, async (ps, me) => {
github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration,
serviceWorker: instance.enableServiceWorker,
- userRecommendation: {
- external: instance.enableExternalUserRecommendation,
- engine: instance.externalUserRecommendationEngine,
- timeout: instance.externalUserRecommendationTimeout
- }
};
}
if (me && (me.isAdmin || me.isModerator)) {
response.useStarForReactionFallback = instance.useStarForReactionFallback;
- response.hidedTags = instance.hidedTags;
+ response.hiddenTags = instance.hiddenTags;
response.recaptchaSecretKey = instance.recaptchaSecretKey;
response.proxyAccount = instance.proxyAccount;
response.twitterConsumerKey = instance.twitterConsumerKey;
@@ -164,9 +156,6 @@ export default define(meta, async (ps, me) => {
response.githubClientSecret = instance.githubClientSecret;
response.discordClientId = instance.discordClientId;
response.discordClientSecret = instance.discordClientSecret;
- response.enableExternalUserRecommendation = instance.enableExternalUserRecommendation;
- response.externalUserRecommendationEngine = instance.externalUserRecommendationEngine;
- response.externalUserRecommendationTimeout = instance.externalUserRecommendationTimeout;
response.summalyProxy = instance.summalyProxy;
response.email = instance.email;
response.smtpSecure = instance.smtpSecure;
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
index 7eaee90a05..d13c546fdc 100644
--- a/src/server/api/endpoints/mute/create.ts
+++ b/src/server/api/endpoints/mute/create.ts
@@ -1,9 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Mute from '../../../../models/mute';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
+import { genId } from '../../../../misc/gen-id';
+import { Mutings, NoteWatchings } from '../../../../models';
+import { Muting } from '../../../../models/entities/muting';
export const meta = {
desc: {
@@ -15,12 +17,11 @@ export const meta = {
requireCredential: true,
- kind: 'account/write',
+ kind: 'write:mutes',
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -53,7 +54,7 @@ export default define(meta, async (ps, user) => {
const muter = user;
// 自分自身
- if (user._id.equals(ps.userId)) {
+ if (user.id === ps.userId) {
throw new ApiError(meta.errors.muteeIsYourself);
}
@@ -64,21 +65,25 @@ export default define(meta, async (ps, user) => {
});
// Check if already muting
- const exist = await Mute.findOne({
- muterId: muter._id,
- muteeId: mutee._id
+ const exist = await Mutings.findOne({
+ muterId: muter.id,
+ muteeId: mutee.id
});
- if (exist !== null) {
+ if (exist != null) {
throw new ApiError(meta.errors.alreadyMuting);
}
// Create mute
- await Mute.insert({
+ await Mutings.save({
+ id: genId(),
createdAt: new Date(),
- muterId: muter._id,
- muteeId: mutee._id,
- });
+ muterId: muter.id,
+ muteeId: mutee.id,
+ } as Muting);
- return;
+ NoteWatchings.delete({
+ userId: muter.id,
+ noteUserId: mutee.id
+ });
});
diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
index 1a03f6371b..1aae15af91 100644
--- a/src/server/api/endpoints/mute/delete.ts
+++ b/src/server/api/endpoints/mute/delete.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Mute from '../../../../models/mute';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
+import { Mutings } from '../../../../models';
export const meta = {
desc: {
@@ -15,12 +15,11 @@ export const meta = {
requireCredential: true,
- kind: 'account/write',
+ kind: 'write:mutes',
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -53,7 +52,7 @@ export default define(meta, async (ps, user) => {
const muter = user;
// Check if the mutee is yourself
- if (user._id.equals(ps.userId)) {
+ if (user.id === ps.userId) {
throw new ApiError(meta.errors.muteeIsYourself);
}
@@ -64,19 +63,17 @@ export default define(meta, async (ps, user) => {
});
// Check not muting
- const exist = await Mute.findOne({
- muterId: muter._id,
- muteeId: mutee._id
+ const exist = await Mutings.findOne({
+ muterId: muter.id,
+ muteeId: mutee.id
});
- if (exist === null) {
+ if (exist == null) {
throw new ApiError(meta.errors.notMuting);
}
// Delete mute
- await Mute.remove({
- _id: exist._id
+ await Mutings.delete({
+ id: exist.id
});
-
- return;
});
diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts
index 1b8f759496..5f2d184472 100644
--- a/src/server/api/endpoints/mute/list.ts
+++ b/src/server/api/endpoints/mute/list.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Mute, { packMany } from '../../../../models/mute';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Mutings } from '../../../../models';
export const meta = {
desc: {
@@ -13,7 +14,7 @@ export const meta = {
requireCredential: true,
- kind: 'account/read',
+ kind: 'read:mutes',
params: {
limit: {
@@ -23,12 +24,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
},
@@ -41,30 +40,12 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const query = {
- muterId: me._id
- } as any;
+ const query = makePaginationQuery(Mutings.createQueryBuilder('muting'), ps.sinceId, ps.untilId)
+ .andWhere(`muting.muterId = :meId`, { meId: me.id });
- const sort = {
- _id: -1
- };
+ const mutings = await query
+ .take(ps.limit)
+ .getMany();
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- }
-
- const mutes = await Mute
- .find(query, {
- limit: ps.limit,
- sort: sort
- });
-
- return await packMany(mutes, me);
+ return await Mutings.packMany(mutings, me);
});
diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts
index 1a936c918b..d205d1674c 100644
--- a/src/server/api/endpoints/my/apps.ts
+++ b/src/server/api/endpoints/my/apps.ts
@@ -1,6 +1,6 @@
import $ from 'cafy';
-import App, { pack } from '../../../../models/app';
import define from '../../define';
+import { Apps } from '../../../../models';
export const meta = {
tags: ['account', 'app'],
@@ -27,19 +27,16 @@ export const meta = {
export default define(meta, async (ps, user) => {
const query = {
- userId: user._id
+ userId: user.id
};
- const apps = await App
- .find(query, {
- limit: ps.limit,
- skip: ps.offset,
- sort: {
- _id: -1
- }
- });
+ const apps = await Apps.find({
+ where: query,
+ take: ps.limit,
+ skip: ps.offset,
+ });
- return await Promise.all(apps.map(app => pack(app, user, {
+ return await Promise.all(apps.map(app => Apps.pack(app, user, {
detail: true
})));
});
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index 835c515cfe..10f6e39845 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../misc/cafy-id';
-import Note, { packMany } from '../../../models/note';
+import { ID } from '../../../misc/cafy-id';
import define from '../define';
+import { makePaginationQuery } from '../common/make-pagination-query';
+import { Notes } from '../../../models';
export const meta = {
desc: {
@@ -39,14 +40,6 @@ export const meta = {
}
},
- media: {
- validator: $.optional.bool,
- deprecated: true,
- desc: {
- 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
- }
- },
-
poll: {
validator: $.optional.bool,
desc: {
@@ -61,12 +54,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
},
@@ -79,43 +70,29 @@ export const meta = {
};
export default define(meta, async (ps) => {
- const sort = {
- _id: -1
- };
- const query = {
- deletedAt: null,
- visibility: 'public',
- localOnly: { $ne: true },
- } as any;
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- }
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere(`note.visibility = 'public'`)
+ .andWhere(`note.localOnly = FALSE`)
+ .leftJoinAndSelect('note.user', 'user');
if (ps.local) {
- query['_user.host'] = null;
+ query.andWhere('note.userHost IS NULL');
}
if (ps.reply != undefined) {
- query.replyId = ps.reply ? { $exists: true, $ne: null } : null;
+ query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL');
}
if (ps.renote != undefined) {
- query.renoteId = ps.renote ? { $exists: true, $ne: null } : null;
+ query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL');
}
- const withFiles = ps.withFiles != undefined ? ps.withFiles : ps.media;
-
- if (withFiles) query.fileIds = { $exists: true, $ne: null };
+ if (ps.withFiles != undefined) {
+ query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`);
+ }
if (ps.poll != undefined) {
- query.poll = ps.poll ? { $exists: true, $ne: null } : null;
+ query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE');
}
// TODO
@@ -123,10 +100,7 @@ export default define(meta, async (ps) => {
// query.isBot = bot;
//}
- const notes = await Note.find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const notes = await query.take(ps.limit).getMany();
- return await packMany(notes);
+ return await Notes.packMany(notes);
});
diff --git a/src/server/api/endpoints/notes/children.ts b/src/server/api/endpoints/notes/children.ts
index 3738459b71..72f2c39d6a 100644
--- a/src/server/api/endpoints/notes/children.ts
+++ b/src/server/api/endpoints/notes/children.ts
@@ -1,9 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note, { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import { getFriends } from '../../common/get-friends';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { Brackets } from 'typeorm';
+import { Notes } from '../../../../models';
export const meta = {
desc: {
@@ -18,7 +20,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
@@ -32,12 +33,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
},
@@ -50,83 +49,24 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const [followings, hideUserIds] = await Promise.all([
- // フォローを取得
- // Fetch following
- user ? getFriends(user._id) : [],
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere(new Brackets(qb => { qb
+ .where(`note.replyId = :noteId`, { noteId: ps.noteId })
+ .orWhere(new Brackets(qb => { qb
+ .where(`note.renoteId = :noteId`, { noteId: ps.noteId })
+ .andWhere(new Brackets(qb => { qb
+ .where(`note.text IS NOT NULL`)
+ .orWhere(`note.fileIds != '{}'`)
+ .orWhere(`note.hasPoll = TRUE`);
+ }));
+ }));
+ }))
+ .leftJoinAndSelect('note.user', 'user');
- // 隠すユーザーを取得
- getHideUserIds(user)
- ]);
+ if (user) generateVisibilityQuery(query, user);
+ if (user) generateMuteQuery(query, user);
- const visibleQuery = user == null ? [{
- visibility: { $in: [ 'public', 'home' ] }
- }] : [{
- visibility: { $in: [ 'public', 'home' ] }
- }, {
- // myself (for followers/specified/private)
- userId: user._id
- }, {
- // to me (for specified)
- visibleUserIds: { $in: [ user._id ] }
- }, {
- visibility: 'followers',
- $or: [{
- // フォロワーの投稿
- userId: { $in: followings.map(f => f.id) },
- }, {
- // 自分の投稿へのリプライ
- '_reply.userId': user._id,
- }, {
- // 自分へのメンションが含まれている
- mentions: { $in: [ user._id ] }
- }]
- }];
+ const notes = await query.take(ps.limit).getMany();
- const q = {
- $and: [{
- $or: [{
- replyId: ps.noteId,
- }, {
- renoteId: ps.noteId,
- $or: [{
- text: { $ne: null }
- }, {
- fileIds: { $ne: [] }
- }, {
- poll: { $ne: null }
- }]
- }]
- }, {
- $or: visibleQuery
- }]
- } as any;
-
- if (hideUserIds && hideUserIds.length > 0) {
- q['userId'] = {
- $nin: hideUserIds
- };
- }
-
- const sort = {
- _id: -1
- };
-
- if (ps.sinceId) {
- sort._id = 1;
- q._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- q._id = {
- $lt: ps.untilId
- };
- }
-
- const notes = await Note.find(q, {
- limit: ps.limit,
- sort: sort
- });
-
- return await packMany(notes, user);
+ return await Notes.packMany(notes, user);
});
diff --git a/src/server/api/endpoints/notes/conversation.ts b/src/server/api/endpoints/notes/conversation.ts
index 702d8dc430..6defd79042 100644
--- a/src/server/api/endpoints/notes/conversation.ts
+++ b/src/server/api/endpoints/notes/conversation.ts
@@ -1,9 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note, { packMany, INote } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
import { getNote } from '../../common/getters';
+import { Note } from '../../../../models/entities/note';
+import { Notes } from '../../../../models';
export const meta = {
desc: {
@@ -18,7 +19,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
@@ -58,12 +58,12 @@ export default define(meta, async (ps, user) => {
throw e;
});
- const conversation: INote[] = [];
+ const conversation: Note[] = [];
let i = 0;
async function get(id: any) {
i++;
- const p = await Note.findOne({ _id: id });
+ const p = await Notes.findOne(id);
if (i > ps.offset) {
conversation.push(p);
@@ -82,5 +82,5 @@ export default define(meta, async (ps, user) => {
await get(note.replyId);
}
- return await packMany(conversation, user);
+ return await Notes.packMany(conversation, user);
});
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index 8cc5e4b815..138f05fb3b 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -1,14 +1,15 @@
import $ from 'cafy';
-import ID, { transform, transformMany } from '../../../../misc/cafy-id';
import * as ms from 'ms';
import { length } from 'stringz';
-import Note, { INote, isValidCw, pack } from '../../../../models/note';
-import User, { IUser } from '../../../../models/user';
-import DriveFile, { IDriveFile } from '../../../../models/drive-file';
import create from '../../../../services/note/create';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
import { ApiError } from '../../error';
+import { ID } from '../../../../misc/cafy-id';
+import { User } from '../../../../models/entities/user';
+import { Users, DriveFiles, Notes } from '../../../../models';
+import { DriveFile } from '../../../../models/entities/drive-file';
+import { Note } from '../../../../models/entities/note';
let maxNoteTextLength = 1000;
@@ -34,7 +35,7 @@ export const meta = {
max: 300
},
- kind: 'note-write',
+ kind: 'write:notes',
params: {
visibility: {
@@ -47,7 +48,6 @@ export const meta = {
visibleUserIds: {
validator: $.optional.arr($.type(ID)).unique().min(0),
- transform: transformMany,
desc: {
'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー'
}
@@ -64,7 +64,7 @@ export const meta = {
},
cw: {
- validator: $.optional.nullable.str.pipe(isValidCw),
+ validator: $.optional.nullable.str.pipe(Notes.validateCw),
desc: {
'ja-JP': 'コンテンツの警告。このパラメータを指定すると設定したテキストで投稿のコンテンツを隠す事が出来ます。'
}
@@ -129,7 +129,6 @@ export const meta = {
fileIds: {
validator: $.optional.arr($.type(ID)).unique().range(1, 4),
- transform: transformMany,
desc: {
'ja-JP': '添付するファイル'
}
@@ -137,7 +136,6 @@ export const meta = {
mediaIds: {
validator: $.optional.arr($.type(ID)).unique().range(1, 4),
- transform: transformMany,
deprecated: true,
desc: {
'ja-JP': '添付するファイル (このパラメータは廃止予定です。代わりに fileIds を使ってください。)'
@@ -146,7 +144,6 @@ export const meta = {
replyId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
'ja-JP': '返信対象'
}
@@ -154,7 +151,6 @@ export const meta = {
renoteId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
'ja-JP': 'Renote対象'
}
@@ -227,32 +223,28 @@ export const meta = {
};
export default define(meta, async (ps, user, app) => {
- let visibleUsers: IUser[] = [];
+ let visibleUsers: User[] = [];
if (ps.visibleUserIds) {
- visibleUsers = await Promise.all(ps.visibleUserIds.map(id => User.findOne({
- _id: id
- })));
+ visibleUsers = await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id)));
}
- let files: IDriveFile[] = [];
+ let files: DriveFile[] = [];
const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
if (fileIds != null) {
files = await Promise.all(fileIds.map(fileId => {
- return DriveFile.findOne({
- _id: fileId,
- 'metadata.userId': user._id
+ return DriveFiles.findOne({
+ id: fileId,
+ userId: user.id
});
}));
files = files.filter(file => file != null);
}
- let renote: INote = null;
+ let renote: Note = null;
if (ps.renoteId != null) {
// Fetch renote to note
- renote = await Note.findOne({
- _id: ps.renoteId
- });
+ renote = await Notes.findOne(ps.renoteId);
if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget);
@@ -261,14 +253,12 @@ export default define(meta, async (ps, user, app) => {
}
}
- let reply: INote = null;
+ let reply: Note = null;
if (ps.replyId != null) {
// Fetch reply
- reply = await Note.findOne({
- _id: ps.replyId
- });
+ reply = await Notes.findOne(ps.replyId);
- if (reply === null) {
+ if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget);
}
@@ -279,12 +269,6 @@ export default define(meta, async (ps, user, app) => {
}
if (ps.poll) {
- (ps.poll as any).choices = (ps.poll as any).choices.map((choice: string, i: number) => ({
- id: i, // IDを付与
- text: choice.trim(),
- votes: 0
- }));
-
if (typeof ps.poll.expiresAt === 'number') {
if (ps.poll.expiresAt < Date.now())
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
@@ -298,11 +282,6 @@ export default define(meta, async (ps, user, app) => {
throw new ApiError(meta.errors.contentRequired);
}
- // 後方互換性のため
- if (ps.visibility == 'private') {
- ps.visibility = 'specified';
- }
-
// 投稿を作成
const note = await create(user, {
createdAt: new Date(),
@@ -311,7 +290,7 @@ export default define(meta, async (ps, user, app) => {
choices: ps.poll.choices,
multiple: ps.poll.multiple || false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null
- } : undefined,
+ } : null,
text: ps.text,
reply,
renote,
@@ -321,13 +300,13 @@ export default define(meta, async (ps, user, app) => {
localOnly: ps.localOnly,
visibility: ps.visibility,
visibleUsers,
- apMentions: ps.noExtractMentions ? [] : undefined,
- apHashtags: ps.noExtractHashtags ? [] : undefined,
- apEmojis: ps.noExtractEmojis ? [] : undefined,
+ apMentions: ps.noExtractMentions ? [] : null,
+ apHashtags: ps.noExtractHashtags ? [] : null,
+ apEmojis: ps.noExtractEmojis ? [] : null,
geo: ps.geo
});
return {
- createdNote: await pack(note, user)
+ createdNote: await Notes.pack(note, user)
};
});
diff --git a/src/server/api/endpoints/notes/delete.ts b/src/server/api/endpoints/notes/delete.ts
index 399f9288d6..dbaf91bca3 100644
--- a/src/server/api/endpoints/notes/delete.ts
+++ b/src/server/api/endpoints/notes/delete.ts
@@ -1,11 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import deleteNote from '../../../../services/note/delete';
-import User from '../../../../models/user';
import define from '../../define';
import * as ms from 'ms';
import { getNote } from '../../common/getters';
import { ApiError } from '../../error';
+import { Users } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -19,7 +19,7 @@ export const meta = {
requireCredential: true,
- kind: 'note-write',
+ kind: 'write:notes',
limit: {
duration: ms('1hour'),
@@ -30,7 +30,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -59,9 +58,10 @@ export default define(meta, async (ps, user) => {
throw e;
});
- if (!user.isAdmin && !user.isModerator && !note.userId.equals(user._id)) {
+ if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}
- await deleteNote(await User.findOne({ _id: note.userId }), note);
+ // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
+ await deleteNote(await Users.findOne(note.userId), note);
});
diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts
index 9cde1a7dcf..7e04637758 100644
--- a/src/server/api/endpoints/notes/favorites/create.ts
+++ b/src/server/api/endpoints/notes/favorites/create.ts
@@ -1,9 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Favorite from '../../../../../models/favorite';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getNote } from '../../../common/getters';
+import { NoteFavorites } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
export const meta = {
stability: 'stable',
@@ -22,7 +23,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -53,21 +53,20 @@ export default define(meta, async (ps, user) => {
});
// if already favorited
- const exist = await Favorite.findOne({
- noteId: note._id,
- userId: user._id
+ const exist = await NoteFavorites.findOne({
+ noteId: note.id,
+ userId: user.id
});
- if (exist !== null) {
+ if (exist != null) {
throw new ApiError(meta.errors.alreadyFavorited);
}
// Create favorite
- await Favorite.insert({
+ await NoteFavorites.save({
+ id: genId(),
createdAt: new Date(),
- noteId: note._id,
- userId: user._id
+ noteId: note.id,
+ userId: user.id
});
-
- return;
});
diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts
index e2c787f3b5..a889c84d4d 100644
--- a/src/server/api/endpoints/notes/favorites/delete.ts
+++ b/src/server/api/endpoints/notes/favorites/delete.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Favorite from '../../../../../models/favorite';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getNote } from '../../../common/getters';
+import { NoteFavorites } from '../../../../../models';
export const meta = {
stability: 'stable',
@@ -22,7 +22,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -53,19 +52,15 @@ export default define(meta, async (ps, user) => {
});
// if already favorited
- const exist = await Favorite.findOne({
- noteId: note._id,
- userId: user._id
+ const exist = await NoteFavorites.findOne({
+ noteId: note.id,
+ userId: user.id
});
- if (exist === null) {
+ if (exist == null) {
throw new ApiError(meta.errors.notFavorited);
}
// Delete favorite
- await Favorite.remove({
- _id: exist._id
- });
-
- return;
+ await NoteFavorites.delete(exist.id);
});
diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts
index 3648b307d7..c44a5275bb 100644
--- a/src/server/api/endpoints/notes/featured.ts
+++ b/src/server/api/endpoints/notes/featured.ts
@@ -1,8 +1,7 @@
import $ from 'cafy';
-import Note from '../../../../models/note';
-import { packMany } from '../../../../models/note';
import define from '../../define';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { Notes } from '../../../../models';
export const meta = {
desc: {
@@ -35,25 +34,14 @@ export const meta = {
export default define(meta, async (ps, user) => {
const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
- const hideUserIds = await getHideUserIds(user);
+ const query = Notes.createQueryBuilder('note')
+ .andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
+ .andWhere(`note.visibility = 'public'`)
+ .leftJoinAndSelect('note.user', 'user');
- const notes = await Note.find({
- createdAt: {
- $gt: new Date(Date.now() - day)
- },
- deletedAt: null,
- visibility: 'public',
- '_user.host': null,
- ...(hideUserIds && hideUserIds.length > 0 ? { userId: { $nin: hideUserIds } } : {})
- }, {
- limit: ps.limit,
- sort: {
- score: -1
- },
- hint: {
- score: -1
- }
- });
+ if (user) generateMuteQuery(query, user);
+
+ const notes = await query.orderBy('note.score', 'DESC').take(ps.limit).getMany();
- return await packMany(notes, user);
+ return await Notes.packMany(notes, user);
});
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 0eb761cdb6..7bf62f366b 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -1,11 +1,12 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
-import { getHideUserIds } from '../../common/get-hide-users';
import { ApiError } from '../../error';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes } from '../../../../models';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { activeUsersChart } from '../../../../services/chart';
export const meta = {
desc: {
@@ -22,14 +23,6 @@ export const meta = {
}
},
- mediaOnly: {
- validator: $.optional.bool,
- deprecated: true,
- desc: {
- 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
- }
- },
-
limit: {
validator: $.optional.num.range(1, 100),
default: 10
@@ -37,12 +30,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
sinceDate: {
@@ -71,6 +62,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
+ // TODO どっかにキャッシュ
const m = await fetchMeta();
if (m.disableGlobalTimeline) {
if (user == null || (!user.isAdmin && !user.isModerator)) {
@@ -78,68 +70,25 @@ export default define(meta, async (ps, user) => {
}
}
- // 隠すユーザーを取得
- const hideUserIds = await getHideUserIds(user);
-
//#region Construct query
- const sort = {
- _id: -1
- };
-
- const query = {
- deletedAt: null,
-
- // public only
- visibility: 'public',
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere('note.visibility = \'public\'')
+ .andWhere('note.replyId IS NULL')
+ .leftJoinAndSelect('note.user', 'user');
- replyId: null
- } as any;
+ if (user) generateMuteQuery(query, user);
- if (hideUserIds && hideUserIds.length > 0) {
- query.userId = {
- $nin: hideUserIds
- };
-
- query['_reply.userId'] = {
- $nin: hideUserIds
- };
-
- query['_renote.userId'] = {
- $nin: hideUserIds
- };
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
}
+ //#endregion
- const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+ const timeline = await query.take(ps.limit).getMany();
- if (withFiles) {
- query.fileIds = { $exists: true, $ne: [] };
+ if (user) {
+ activeUsersChart.update(user);
}
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- } else if (ps.sinceDate) {
- sort._id = 1;
- query.createdAt = {
- $gt: new Date(ps.sinceDate)
- };
- } else if (ps.untilDate) {
- query.createdAt = {
- $lt: new Date(ps.untilDate)
- };
- }
- //#endregion
-
- const timeline = await Note.find(query, {
- limit: ps.limit,
- sort: sort
- });
-
- return await packMany(timeline, 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 57ef4c3e15..cd07341342 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -1,12 +1,14 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
-import activeUsersChart from '../../../../services/chart/active-users';
-import { getHideUserIds } from '../../common/get-hide-users';
import { ApiError } from '../../error';
+import { Notes } from '../../../../models';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { activeUsersChart } from '../../../../services/chart';
+import { Brackets } from 'typeorm';
export const meta = {
desc: {
@@ -23,14 +25,6 @@ export const meta = {
}
},
- mediaOnly: {
- validator: $.optional.bool,
- deprecated: true,
- desc: {
- 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
- }
- },
-
fileType: {
validator: $.optional.arr($.str),
desc: {
@@ -53,12 +47,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
sinceDate: {
@@ -87,6 +79,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
+ // TODO どっかにキャッシュ
const m = await fetchMeta();
if (m.disableLocalTimeline) {
if (user == null || (!user.isAdmin && !user.isModerator)) {
@@ -94,90 +87,44 @@ export default define(meta, async (ps, user) => {
}
}
- // 隠すユーザーを取得
- const hideUserIds = await getHideUserIds(user);
-
//#region Construct query
- const sort = {
- _id: -1
- };
-
- const query = {
- deletedAt: null,
-
- // public only
- visibility: 'public',
-
- // リプライでない
- //replyId: null,
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
+ .leftJoinAndSelect('note.user', 'user');
- // local
- '_user.host': null
- } as any;
+ if (user) generateVisibilityQuery(query, user);
+ if (user) generateMuteQuery(query, user);
- if (hideUserIds && hideUserIds.length > 0) {
- query.userId = {
- $nin: hideUserIds
- };
-
- query['_reply.userId'] = {
- $nin: hideUserIds
- };
-
- query['_renote.userId'] = {
- $nin: hideUserIds
- };
- }
-
- const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
-
- if (withFiles) {
- query.fileIds = { $exists: true, $ne: [] };
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
}
if (ps.fileType) {
- query.fileIds = { $exists: true, $ne: [] };
-
- query['_files.contentType'] = {
- $in: ps.fileType
- };
+ query.andWhere('note.fileIds != \'{}\'');
+ query.andWhere(new Brackets(qb => {
+ for (const type of ps.fileType) {
+ const i = ps.fileType.indexOf(type);
+ qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
+ }
+ }));
if (ps.excludeNsfw) {
- query['_files.metadata.isSensitive'] = {
+ // v11 TODO
+ /*
+ query['_files.isSensitive'] = {
$ne: true
};
+ */
}
}
-
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- } else if (ps.sinceDate) {
- sort._id = 1;
- query.createdAt = {
- $gt: new Date(ps.sinceDate)
- };
- } else if (ps.untilDate) {
- query.createdAt = {
- $lt: new Date(ps.untilDate)
- };
- }
//#endregion
- const timeline = await Note.find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const timeline = await query.take(ps.limit).getMany();
if (user) {
activeUsersChart.update(user);
}
- return await packMany(timeline, user);
+ return await Notes.packMany(timeline, user);
});
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index 91333174ed..0bbe7d3327 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -1,11 +1,12 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { getFriendIds, getFriends } from '../../common/get-friends';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import read from '../../../../services/note/read';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { Notes, Followings } from '../../../../models';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Brackets } from 'typeorm';
export const meta = {
desc: {
@@ -30,12 +31,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
visibility: {
@@ -52,97 +51,34 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- // フォローを取得
- const followings = await getFriends(user._id);
+ const followingQuery = Followings.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: user.id });
- const visibleQuery = [{
- visibility: { $in: [ 'public', 'home' ] }
- }, {
- // myself (for followers/specified/private)
- userId: user._id
- }, {
- // to me (for specified)
- visibleUserIds: { $in: [ user._id ] }
- }, {
- visibility: 'followers',
- $or: [{
- // フォロワーの投稿
- userId: { $in: followings.map(f => f.id) },
- }, {
- // 自分の投稿へのリプライ
- '_reply.userId': user._id,
- }, {
- // 自分へのメンションが含まれている
- mentions: { $in: [ user._id ] }
- }]
- }];
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere(new Brackets(qb => { qb
+ .where(`:meId = ANY(note.mentions)`, { meId: user.id })
+ .orWhere(`:meId = ANY(note.visibleUserIds)`, { meId: user.id });
+ }))
+ .leftJoinAndSelect('note.user', 'user');
- const query = {
- $and: [{
- deletedAt: null,
- }, {
- $or: visibleQuery,
- }],
-
- $or: [{
- mentions: user._id
- }, {
- visibleUserIds: user._id
- }]
- } as any;
-
- // 隠すユーザーを取得
- const hideUserIds = await getHideUserIds(user);
-
- if (hideUserIds && hideUserIds.length > 0) {
- query.userId = {
- $nin: hideUserIds
- };
-
- query['_reply.userId'] = {
- $nin: hideUserIds
- };
-
- query['_renote.userId'] = {
- $nin: hideUserIds
- };
- }
-
- const sort = {
- _id: -1
- };
+ generateVisibilityQuery(query, user);
+ generateMuteQuery(query, user);
if (ps.visibility) {
- query.visibility = ps.visibility;
+ query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
}
if (ps.following) {
- const followingIds = await getFriendIds(user._id);
-
- query.userId = {
- $in: followingIds
- };
- }
-
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
+ query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id });
+ query.setParameters(followingQuery.getParameters());
}
- const mentions = await Note.find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const mentions = await query.take(ps.limit).getMany();
for (const note of mentions) {
- read(user._id, note._id);
+ read(user.id, note.id);
}
- return await packMany(mentions, user);
+ return await Notes.packMany(mentions, user);
});
diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts
index 9adabdf0e9..ff838d4f4f 100644
--- a/src/server/api/endpoints/notes/polls/recommendation.ts
+++ b/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -1,8 +1,7 @@
import $ from 'cafy';
-import Vote from '../../../../../models/poll-vote';
-import Note, { pack } from '../../../../../models/note';
import define from '../../../define';
-import { getHideUserIds } from '../../../common/get-hide-users';
+import { Polls, Mutings, Notes, PollVotes } from '../../../../../models';
+import { Brackets, In } from 'typeorm';
export const meta = {
desc: {
@@ -28,51 +27,46 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- // Get votes
- const votes = await Vote.find({
- userId: user._id
- }, {
- fields: {
- _id: false,
- noteId: true
- }
- });
+ const query = Polls.createQueryBuilder('poll')
+ .where('poll.userHost IS NULL')
+ .andWhere(`poll.userId != :meId`, { meId: user.id })
+ .andWhere(`poll.noteVisibility = 'public'`)
+ .andWhere(new Brackets(qb => { qb
+ .where('poll.expiresAt IS NULL')
+ .orWhere('poll.expiresAt > :now', { now: new Date() });
+ }));
- const nin = votes && votes.length != 0 ? votes.map(v => v.noteId) : [];
+ //#region exclude arleady voted polls
+ const votedQuery = PollVotes.createQueryBuilder('vote')
+ .select('vote.noteId')
+ .where('vote.userId = :meId', { meId: user.id });
- // 隠すユーザーを取得
- const hideUserIds = await getHideUserIds(user);
+ query
+ .andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`);
- const notes = await Note.find({
- '_user.host': null,
- _id: {
- $nin: nin
- },
- userId: {
- $ne: user._id,
- $nin: hideUserIds
- },
- visibility: 'public',
- poll: {
- $exists: true,
- $ne: null
- },
- $or: [{
- 'poll.expiresAt': null
- }, {
- 'poll.expiresAt': {
- $gt: new Date()
- }
- }],
- }, {
- limit: ps.limit,
- skip: ps.offset,
- sort: {
- _id: -1
- }
+ query.setParameters(votedQuery.getParameters());
+ //#endregion
+
+ //#region mute
+ const mutingQuery = Mutings.createQueryBuilder('muting')
+ .select('muting.muteeId')
+ .where('muting.muterId = :muterId', { muterId: user.id });
+
+ query
+ .andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`);
+
+ query.setParameters(mutingQuery.getParameters());
+ //#endregion
+
+ const polls = await query.take(ps.limit).skip(ps.offset).getMany();
+
+ if (polls.length === 0) return [];
+
+ const notes = await Notes.find({
+ id: In(polls.map(poll => poll.noteId))
});
- return await Promise.all(notes.map(note => pack(note, user, {
+ return await Notes.packMany(notes, user, {
detail: true
- })));
+ });
});
diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
index ed20e0221f..7d0ed6e4f9 100644
--- a/src/server/api/endpoints/notes/polls/vote.ts
+++ b/src/server/api/endpoints/notes/polls/vote.ts
@@ -1,19 +1,19 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Vote from '../../../../../models/poll-vote';
-import Note from '../../../../../models/note';
-import Watching from '../../../../../models/note-watching';
+import { ID } from '../../../../../misc/cafy-id';
import watch from '../../../../../services/note/watch';
import { publishNoteStream } from '../../../../../services/stream';
-import notify from '../../../../../services/create-notification';
+import { createNotification } from '../../../../../services/create-notification';
import define from '../../../define';
-import User, { IRemoteUser } from '../../../../../models/user';
import { ApiError } from '../../../error';
import { getNote } from '../../../common/getters';
import { deliver } from '../../../../../queue';
import { renderActivity } from '../../../../../remote/activitypub/renderer';
import renderVote from '../../../../../remote/activitypub/renderer/vote';
import { deliverQuestionUpdate } from '../../../../../services/note/polls/update';
+import { PollVotes, NoteWatchings, Users, Polls } from '../../../../../models';
+import { Not } from 'typeorm';
+import { IRemoteUser } from '../../../../../models/entities/user';
+import { genId } from '../../../../../misc/gen-id';
export const meta = {
desc: {
@@ -30,7 +30,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
@@ -84,26 +83,28 @@ export default define(meta, async (ps, user) => {
throw e;
});
- if (note.poll == null) {
+ if (!note.hasPoll) {
throw new ApiError(meta.errors.noPoll);
}
- if (note.poll.expiresAt && note.poll.expiresAt < createdAt) {
+ const poll = await Polls.findOne({ noteId: note.id });
+
+ if (poll.expiresAt && poll.expiresAt < createdAt) {
throw new ApiError(meta.errors.alreadyExpired);
}
- if (!note.poll.choices.some(x => x.id == ps.choice)) {
+ if (poll.choices[ps.choice] == null) {
throw new ApiError(meta.errors.invalidChoice);
}
// if already voted
- const exist = await Vote.find({
- noteId: note._id,
- userId: user._id
+ const exist = await PollVotes.find({
+ noteId: note.id,
+ userId: user.id
});
if (exist.length) {
- if (note.poll.multiple) {
+ if (poll.multiple) {
if (exist.some(x => x.choice == ps.choice))
throw new ApiError(meta.errors.alreadyVoted);
} else {
@@ -112,69 +113,54 @@ export default define(meta, async (ps, user) => {
}
// Create vote
- const vote = await Vote.insert({
+ const vote = await PollVotes.save({
+ id: genId(),
createdAt,
- noteId: note._id,
- userId: user._id,
+ noteId: note.id,
+ userId: user.id,
choice: ps.choice
});
- const inc: any = {};
- inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == ps.choice)}.votes`] = 1;
-
// Increment votes count
- await Note.update({ _id: note._id }, {
- $inc: inc
- });
+ const index = ps.choice + 1; // In SQL, array index is 1 based
+ await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE id = '${poll.id}'`);
- publishNoteStream(note._id, 'pollVoted', {
+ publishNoteStream(note.id, 'pollVoted', {
choice: ps.choice,
- userId: user._id.toHexString()
+ userId: user.id
});
// Notify
- notify(note.userId, user._id, 'poll_vote', {
- noteId: note._id,
+ createNotification(note.userId, user.id, 'pollVote', {
+ noteId: note.id,
choice: ps.choice
});
// Fetch watchers
- Watching
- .find({
- noteId: note._id,
- userId: { $ne: user._id },
- // 削除されたドキュメントは除く
- deletedAt: { $exists: false }
- }, {
- fields: {
- userId: true
- }
- })
- .then(watchers => {
- for (const watcher of watchers) {
- notify(watcher.userId, user._id, 'poll_vote', {
- noteId: note._id,
- choice: ps.choice
- });
- }
- });
+ NoteWatchings.find({
+ noteId: note.id,
+ userId: Not(user.id),
+ }).then(watchers => {
+ for (const watcher of watchers) {
+ createNotification(watcher.userId, user.id, 'pollVote', {
+ noteId: note.id,
+ choice: ps.choice
+ });
+ }
+ });
// この投稿をWatchする
- if (user.settings.autoWatch !== false) {
- watch(user._id, note);
+ if (user.autoWatch !== false) {
+ watch(user.id, note);
}
// リモート投票の場合リプライ送信
- if (note._user.host != null) {
- const pollOwner: IRemoteUser = await User.findOne({
- _id: note.userId
- });
+ if (note.userHost != null) {
+ const pollOwner: IRemoteUser = await Users.findOne(note.userId);
- deliver(user, renderActivity(await renderVote(user, vote, note, pollOwner)), pollOwner.inbox);
+ deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
}
// リモートフォロワーにUpdate配信
- deliverQuestionUpdate(note._id);
-
- return;
+ deliverQuestionUpdate(note.id);
});
diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts
index 7d977154f2..b1b5ca9d33 100644
--- a/src/server/api/endpoints/notes/reactions.ts
+++ b/src/server/api/endpoints/notes/reactions.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import NoteReaction, { pack } from '../../../../models/note-reaction';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getNote } from '../../common/getters';
import { ApiError } from '../../error';
+import { NoteReactions } from '../../../../models';
export const meta = {
desc: {
@@ -18,7 +18,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'The ID of the target note'
@@ -37,12 +36,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
},
},
@@ -69,29 +66,17 @@ export default define(meta, async (ps, user) => {
});
const query = {
- noteId: note._id
- } as any;
-
- const sort = {
- _id: -1
+ noteId: note.id
};
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- }
-
- const reactions = await NoteReaction.find(query, {
- limit: ps.limit,
+ const reactions = await NoteReactions.find({
+ where: query,
+ take: ps.limit,
skip: ps.offset,
- sort: sort
+ order: {
+ id: -1
+ }
});
- return await Promise.all(reactions.map(reaction => pack(reaction, user)));
+ return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user)));
});
diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts
index 299ed30278..b6aa4c58f3 100644
--- a/src/server/api/endpoints/notes/reactions/create.ts
+++ b/src/server/api/endpoints/notes/reactions/create.ts
@@ -1,5 +1,5 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import createReaction from '../../../../../services/note/reaction/create';
import define from '../../../define';
import { getNote } from '../../../common/getters';
@@ -17,12 +17,11 @@ export const meta = {
requireCredential: true,
- kind: 'reaction-write',
+ kind: 'write:reactions',
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿'
}
diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts
index 08442226c5..0bdea58027 100644
--- a/src/server/api/endpoints/notes/reactions/delete.ts
+++ b/src/server/api/endpoints/notes/reactions/delete.ts
@@ -1,5 +1,5 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import * as ms from 'ms';
import deleteReaction from '../../../../../services/note/reaction/delete';
@@ -16,7 +16,7 @@ export const meta = {
requireCredential: true,
- kind: 'reaction-write',
+ kind: 'write:reactions',
limit: {
duration: ms('1hour'),
@@ -27,7 +27,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
diff --git a/src/server/api/endpoints/notes/renotes.ts b/src/server/api/endpoints/notes/renotes.ts
index 15dcf55dce..81b899836d 100644
--- a/src/server/api/endpoints/notes/renotes.ts
+++ b/src/server/api/endpoints/notes/renotes.ts
@@ -1,9 +1,12 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note, { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getNote } from '../../common/getters';
import { ApiError } from '../../error';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes } from '../../../../models';
export const meta = {
desc: {
@@ -18,7 +21,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
@@ -32,12 +34,10 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
}
},
@@ -63,29 +63,14 @@ export default define(meta, async (ps, user) => {
throw e;
});
- const sort = {
- _id: -1
- };
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
+ .leftJoinAndSelect('note.user', 'user');
- const query = {
- renoteId: note._id
- } as any;
+ if (user) generateVisibilityQuery(query, user);
+ if (user) generateMuteQuery(query, user);
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- }
-
- const renotes = await Note.find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const renotes = await query.take(ps.limit).getMany();
- return await packMany(renotes, user);
+ return await Notes.packMany(renotes, user);
});
diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts
index c80fd73205..09b0f17164 100644
--- a/src/server/api/endpoints/notes/replies.ts
+++ b/src/server/api/endpoints/notes/replies.ts
@@ -1,9 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note, { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import { getFriends } from '../../common/get-friends';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { Notes } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
export const meta = {
desc: {
@@ -18,22 +19,30 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
}
},
+ sinceId: {
+ validator: $.optional.type(ID),
+ desc: {
+ 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
+ }
+ },
+
+ untilId: {
+ validator: $.optional.type(ID),
+ desc: {
+ 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
+ }
+ },
+
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
-
- offset: {
- validator: $.optional.num.min(0),
- default: 0
- },
},
res: {
@@ -45,54 +54,14 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const [followings, hideUserIds] = await Promise.all([
- // フォローを取得
- // Fetch following
- user ? getFriends(user._id) : [],
-
- // 隠すユーザーを取得
- getHideUserIds(user)
- ]);
-
- const visibleQuery = user == null ? [{
- visibility: { $in: [ 'public', 'home' ] }
- }] : [{
- visibility: { $in: [ 'public', 'home' ] }
- }, {
- // myself (for followers/specified/private)
- userId: user._id
- }, {
- // to me (for specified)
- visibleUserIds: { $in: [ user._id ] }
- }, {
- visibility: 'followers',
- $or: [{
- // フォロワーの投稿
- userId: { $in: followings.map(f => f.id) },
- }, {
- // 自分の投稿へのリプライ
- '_reply.userId': user._id,
- }, {
- // 自分へのメンションが含まれている
- mentions: { $in: [ user._id ] }
- }]
- }];
-
- const q = {
- replyId: ps.noteId,
- $or: visibleQuery
- } as any;
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere('note.replyId = :replyId', { replyId: ps.noteId })
+ .leftJoinAndSelect('note.user', 'user');
- if (hideUserIds && hideUserIds.length > 0) {
- q['userId'] = {
- $nin: hideUserIds
- };
- }
+ if (user) generateVisibilityQuery(query, user);
+ if (user) generateMuteQuery(query, user);
- const notes = await Note.find(q, {
- limit: ps.limit,
- skip: ps.offset
- });
+ const timeline = await query.take(ps.limit).getMany();
- return await packMany(notes, user);
+ return await Notes.packMany(timeline, user);
});
diff --git a/src/server/api/endpoints/notes/search-by-tag.ts b/src/server/api/endpoints/notes/search-by-tag.ts
index b33c884049..48de88d36e 100644
--- a/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/src/server/api/endpoints/notes/search-by-tag.ts
@@ -1,10 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { getFriendIds } from '../../common/get-friends';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes } from '../../../../models';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { Brackets } from 'typeorm';
export const meta = {
desc: {
@@ -28,16 +29,6 @@ export const meta = {
}
},
- following: {
- validator: $.optional.nullable.bool,
- default: null as any
- },
-
- mute: {
- validator: $.optional.str,
- default: 'mute_all'
- },
-
reply: {
validator: $.optional.nullable.bool,
default: null as any,
@@ -61,44 +52,28 @@ export const meta = {
}
},
- media: {
+ poll: {
validator: $.optional.nullable.bool,
default: null as any,
- deprecated: true,
desc: {
- 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ 'ja-JP': 'アンケートが添付された投稿に限定するか否か'
}
},
- poll: {
- validator: $.optional.nullable.bool,
- default: null as any,
+ sinceId: {
+ validator: $.optional.type(ID),
desc: {
- 'ja-JP': 'アンケートが添付された投稿に限定するか否か'
+ 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
}
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
- 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+ 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
}
},
- sinceDate: {
- validator: $.optional.num,
- },
-
- untilDate: {
- validator: $.optional.num,
- },
-
- offset: {
- validator: $.optional.num.min(0),
- default: 0
- },
-
limit: {
validator: $.optional.num.range(1, 30),
default: 10
@@ -114,226 +89,58 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const visibleQuery = me == null ? [{
- visibility: { $in: [ 'public', 'home' ] }
- }] : [{
- visibility: { $in: [ 'public', 'home' ] }
- }, {
- // myself (for specified/private)
- userId: me._id
- }, {
- // to me (for specified)
- visibleUserIds: { $in: [ me._id ] }
- }];
-
- const q: any = {
- $and: [ps.tag ? {
- tagsLower: ps.tag.toLowerCase()
- } : {
- $or: ps.query.map(tags => ({
- $and: tags.map(t => ({
- tagsLower: t.toLowerCase()
- }))
- }))
- }],
- deletedAt: { $exists: false },
- $or: visibleQuery
- };
-
- const push = (x: any) => q.$and.push(x);
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .leftJoinAndSelect('note.user', 'user');
- if (ps.following != null && me != null) {
- const ids = await getFriendIds(me._id, false);
- push({
- userId: ps.following ? {
- $in: ids
- } : {
- $nin: ids.concat(me._id)
- }
- });
- }
+ if (me) generateVisibilityQuery(query, me);
+ if (me) generateMuteQuery(query, me);
- if (me != null) {
- const hideUserIds = await getHideUserIds(me);
-
- switch (ps.mute) {
- case 'mute_all':
- push({
- userId: {
- $nin: hideUserIds
- },
- '_reply.userId': {
- $nin: hideUserIds
- },
- '_renote.userId': {
- $nin: hideUserIds
- }
- });
- break;
- case 'mute_related':
- push({
- '_reply.userId': {
- $nin: hideUserIds
- },
- '_renote.userId': {
- $nin: hideUserIds
- }
- });
- break;
- case 'mute_direct':
- push({
- userId: {
- $nin: hideUserIds
+ if (ps.tag) {
+ query.andWhere(':tag = ANY(note.tags)', { tag: ps.tag });
+ } else {
+ let i = 0;
+ query.andWhere(new Brackets(qb => {
+ for (const tags of ps.query) {
+ qb.orWhere(new Brackets(qb => {
+ for (const tag of tags) {
+ qb.andWhere(`:tag${i} = ANY(note.tags)`, { [`tag${i}`]: tag });
+ i++;
}
- });
- break;
- case 'direct_only':
- push({
- userId: {
- $in: hideUserIds
- }
- });
- break;
- case 'related_only':
- push({
- $or: [{
- '_reply.userId': {
- $in: hideUserIds
- }
- }, {
- '_renote.userId': {
- $in: hideUserIds
- }
- }]
- });
- break;
- case 'all_only':
- push({
- $or: [{
- userId: {
- $in: hideUserIds
- }
- }, {
- '_reply.userId': {
- $in: hideUserIds
- }
- }, {
- '_renote.userId': {
- $in: hideUserIds
- }
- }]
- });
- break;
- }
+ }));
+ }
+ }));
}
if (ps.reply != null) {
if (ps.reply) {
- push({
- replyId: {
- $exists: true,
- $ne: null
- }
- });
+ query.andWhere('note.replyId IS NOT NULL');
} else {
- push({
- $or: [{
- replyId: {
- $exists: false
- }
- }, {
- replyId: null
- }]
- });
+ query.andWhere('note.replyId IS NULL');
}
}
if (ps.renote != null) {
if (ps.renote) {
- push({
- renoteId: {
- $exists: true,
- $ne: null
- }
- });
+ query.andWhere('note.renoteId IS NOT NULL');
} else {
- push({
- $or: [{
- renoteId: {
- $exists: false
- }
- }, {
- renoteId: null
- }]
- });
+ query.andWhere('note.renoteId IS NULL');
}
}
- const withFiles = ps.withFiles != null ? ps.withFiles : ps.media;
-
- if (withFiles) {
- push({
- fileIds: { $exists: true, $ne: [] }
- });
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
}
if (ps.poll != null) {
if (ps.poll) {
- push({
- poll: {
- $exists: true,
- $ne: null
- }
- });
+ query.andWhere('note.hasPoll = TRUE');
} else {
- push({
- $or: [{
- poll: {
- $exists: false
- }
- }, {
- poll: null
- }]
- });
+ query.andWhere('note.hasPoll = FALSE');
}
}
- if (ps.untilId) {
- push({
- _id: {
- $lt: ps.untilId
- }
- });
- }
-
- if (ps.sinceDate) {
- push({
- createdAt: {
- $gt: new Date(ps.sinceDate)
- }
- });
- }
-
- if (ps.untilDate) {
- push({
- createdAt: {
- $lt: new Date(ps.untilDate)
- }
- });
- }
-
- if (q.$and.length == 0) {
- delete q.$and;
- }
-
// Search notes
- const notes = await Note.find(q, {
- sort: {
- _id: -1
- },
- limit: ps.limit,
- skip: ps.offset
- });
+ const notes = await query.take(ps.limit).getMany();
- return await packMany(notes, me);
+ return await Notes.packMany(notes, me);
});
diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts
index edc8a14560..cc88fb9380 100644
--- a/src/server/api/endpoints/notes/search.ts
+++ b/src/server/api/endpoints/notes/search.ts
@@ -1,10 +1,9 @@
import $ from 'cafy';
-import * as mongo from 'mongodb';
-import Note from '../../../../models/note';
-import { packMany } from '../../../../models/note';
import es from '../../../../db/elasticsearch';
import define from '../../define';
import { ApiError } from '../../error';
+import { Notes } from '../../../../models';
+import { In } from 'typeorm';
export const meta = {
desc: {
@@ -74,18 +73,19 @@ export default define(meta, async (ps, me) => {
return [];
}
- const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id));
+ const hits = response.hits.hits.map((hit: any) => hit.id);
+
+ if (hits.length === 0) return [];
// Fetch found notes
- const notes = await Note.find({
- _id: {
- $in: hits
- }
- }, {
- sort: {
- _id: -1
+ const notes = await Notes.find({
+ where: {
+ id: In(hits)
+ },
+ order: {
+ id: -1
}
});
- return await packMany(notes, me);
+ return await Notes.packMany(notes, me);
});
diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts
index 6d8dc73ff2..d41dc20c54 100644
--- a/src/server/api/endpoints/notes/show.ts
+++ b/src/server/api/endpoints/notes/show.ts
@@ -1,9 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import { pack } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { getNote } from '../../common/getters';
import { ApiError } from '../../error';
+import { Notes } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -20,7 +20,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -47,7 +46,7 @@ export default define(meta, async (ps, user) => {
throw e;
});
- return await pack(note, user, {
+ return await Notes.pack(note, user, {
detail: true
});
});
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/social-timeline.ts
index 9695547f04..10e215d6c4 100644
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/src/server/api/endpoints/notes/social-timeline.ts
@@ -1,17 +1,18 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { getFriends } from '../../common/get-friends';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
-import activeUsersChart from '../../../../services/chart/active-users';
-import { getHideUserIds } from '../../common/get-hide-users';
import { ApiError } from '../../error';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Followings, Notes } from '../../../../models';
+import { Brackets } from 'typeorm';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { activeUsersChart } from '../../../../services/chart';
export const meta = {
desc: {
- 'ja-JP': 'ハイブリッドタイムラインを取得します。'
+ 'ja-JP': 'ソーシャルタイムラインを取得します。'
},
tags: ['notes'],
@@ -27,17 +28,15 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
- 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
+ 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
}
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
- 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+ 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
}
},
@@ -85,14 +84,6 @@ export const meta = {
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
}
},
-
- mediaOnly: {
- validator: $.optional.bool,
- deprecated: true,
- desc: {
- 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
- }
- },
},
res: {
@@ -112,94 +103,30 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
+ // TODO どっかにキャッシュ
const m = await fetchMeta();
if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) {
throw new ApiError(meta.errors.stlDisabled);
}
- const [followings, hideUserIds] = await Promise.all([
- // フォローを取得
- // Fetch following
- getFriends(user._id, true, false),
-
- // 隠すユーザーを取得
- getHideUserIds(user)
- ]);
-
//#region Construct query
- const sort = {
- _id: -1
- };
-
- const followQuery = followings.map(f => ({
- userId: f.id,
+ const followingQuery = Followings.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: user.id });
- /*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める)
- $or: [{
- // リプライでない
- replyId: null
- }, { // または
- // リプライだが返信先が投稿者自身の投稿
- $expr: {
- $eq: ['$_reply.userId', '$userId']
- }
- }, { // または
- // リプライだが返信先が自分(フォロワー)の投稿
- '_reply.userId': user._id
- }, { // または
- // 自分(フォロワー)が送信したリプライ
- userId: user._id
- }]*/
- }));
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere(new Brackets(qb => {
+ qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id })
+ .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
+ }))
+ .leftJoinAndSelect('note.user', 'user')
+ .setParameters(followingQuery.getParameters());
- const visibleQuery = user == null ? [{
- visibility: { $in: ['public', 'home'] }
- }] : [{
- visibility: { $in: ['public', 'home', 'followers'] }
- }, {
- // myself (for specified/private)
- userId: user._id
- }, {
- // to me (for specified)
- visibleUserIds: { $in: [ user._id ] }
- }];
-
- const query = {
- $and: [{
- deletedAt: null,
-
- $or: [{
- $and: [{
- // フォローしている人の投稿
- $or: followQuery
- }, {
- // visible for me
- $or: visibleQuery
- }]
- }, {
- // public only
- visibility: 'public',
-
- // リプライでない
- //replyId: null,
-
- // local
- '_user.host': null
- }],
-
- // hide
- userId: {
- $nin: hideUserIds
- },
- '_reply.userId': {
- $nin: hideUserIds
- },
- '_renote.userId': {
- $nin: hideUserIds
- },
- }]
- } as any;
+ generateVisibilityQuery(query, user);
+ generateMuteQuery(query, user);
+ /* TODO
// MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
@@ -207,7 +134,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeMyRenotes === false) {
query.$and.push({
$or: [{
- userId: { $ne: user._id }
+ userId: { $ne: user.id }
}, {
renoteId: null
}, {
@@ -223,7 +150,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeRenotedMyNotes === false) {
query.$and.push({
$or: [{
- '_renote.userId': { $ne: user._id }
+ '_renote.userId': { $ne: user.id }
}, {
renoteId: null
}, {
@@ -251,40 +178,18 @@ export default define(meta, async (ps, user) => {
}]
});
}
+ */
- if (ps.withFiles || ps.mediaOnly) {
- query.$and.push({
- fileIds: { $exists: true, $ne: [] }
- });
- }
-
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- } else if (ps.sinceDate) {
- sort._id = 1;
- query.createdAt = {
- $gt: new Date(ps.sinceDate)
- };
- } else if (ps.untilDate) {
- query.createdAt = {
- $lt: new Date(ps.untilDate)
- };
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
- const timeline = await Note.find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const timeline = await query.take(ps.limit).getMany();
- activeUsersChart.update(user);
+ if (user) {
+ activeUsersChart.update(user);
+ }
- return await packMany(timeline, user);
+ return await Notes.packMany(timeline, user);
});
diff --git a/src/server/api/endpoints/notes/state.ts b/src/server/api/endpoints/notes/state.ts
index 4944802849..df1d9d9fb0 100644
--- a/src/server/api/endpoints/notes/state.ts
+++ b/src/server/api/endpoints/notes/state.ts
@@ -1,8 +1,7 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import Favorite from '../../../../models/favorite';
-import NoteWatching from '../../../../models/note-watching';
+import { NoteFavorites, NoteWatchings } from '../../../../models';
export const meta = {
stability: 'stable',
@@ -19,7 +18,6 @@ export const meta = {
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -30,17 +28,19 @@ export const meta = {
export default define(meta, async (ps, user) => {
const [favorite, watching] = await Promise.all([
- Favorite.count({
- userId: user._id,
+ NoteFavorites.count({
+ where: {
+ userId: user.id,
noteId: ps.noteId
- }, {
- limit: 1
+ },
+ take: 1
}),
- NoteWatching.count({
- userId: user._id,
+ NoteWatchings.count({
+ where: {
+ userId: user.id,
noteId: ps.noteId
- }, {
- limit: 1
+ },
+ take: 1
})
]);
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index 6ff7690c74..e22db4d1b0 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -1,11 +1,12 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { getFriends } from '../../common/get-friends';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import activeUsersChart from '../../../../services/chart/active-users';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes, Followings } from '../../../../models';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { activeUsersChart } from '../../../../services/chart';
+import { Brackets } from 'typeorm';
export const meta = {
desc: {
@@ -28,17 +29,15 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
- 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
+ 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
}
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
- 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+ 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
}
},
@@ -86,14 +85,6 @@ export const meta = {
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
}
},
-
- mediaOnly: {
- validator: $.optional.bool,
- deprecated: true,
- desc: {
- 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
- }
- },
},
res: {
@@ -105,78 +96,24 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const [followings, hideUserIds] = await Promise.all([
- // フォローを取得
- // Fetch following
- getFriends(user._id),
-
- // 隠すユーザーを取得
- getHideUserIds(user)
- ]);
-
//#region Construct query
- const sort = {
- _id: -1
- };
-
- const followQuery = followings.map(f => ({
- userId: f.id,
-
- /*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める)
- $or: [{
- // リプライでない
- replyId: null
- }, { // または
- // リプライだが返信先が投稿者自身の投稿
- $expr: {
- $eq: ['$_reply.userId', '$userId']
- }
- }, { // または
- // リプライだが返信先が自分(フォロワー)の投稿
- '_reply.userId': user._id
- }, { // または
- // 自分(フォロワー)が送信したリプライ
- userId: user._id
- }]*/
- }));
+ const followingQuery = Followings.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: user.id });
- const visibleQuery = user == null ? [{
- visibility: { $in: [ 'public', 'home' ] }
- }] : [{
- visibility: { $in: [ 'public', 'home', 'followers' ] }
- }, {
- // myself (for specified/private)
- userId: user._id
- }, {
- // to me (for specified)
- visibleUserIds: { $in: [ user._id ] }
- }];
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere(new Brackets(qb => { qb
+ .where(`note.userId IN (${ followingQuery.getQuery() })`)
+ .orWhere('note.userId = :meId', { meId: user.id });
+ }))
+ .leftJoinAndSelect('note.user', 'user')
+ .setParameters(followingQuery.getParameters());
- const query = {
- $and: [{
- deletedAt: null,
-
- $and: [{
- // フォローしている人の投稿
- $or: followQuery
- }, {
- // visible for me
- $or: visibleQuery
- }],
-
- // mute
- userId: {
- $nin: hideUserIds
- },
- '_reply.userId': {
- $nin: hideUserIds
- },
- '_renote.userId': {
- $nin: hideUserIds
- },
- }]
- } as any;
+ generateVisibilityQuery(query, user);
+ generateMuteQuery(query, user);
+ /* v11 TODO
// MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
@@ -184,7 +121,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeMyRenotes === false) {
query.$and.push({
$or: [{
- userId: { $ne: user._id }
+ userId: { $ne: user.id }
}, {
renoteId: null
}, {
@@ -200,7 +137,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeRenotedMyNotes === false) {
query.$and.push({
$or: [{
- '_renote.userId': { $ne: user._id }
+ '_renote.userId': { $ne: user.id }
}, {
renoteId: null
}, {
@@ -227,43 +164,16 @@ export default define(meta, async (ps, user) => {
poll: { $ne: null }
}]
});
- }
-
- const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
-
- if (withFiles) {
- query.$and.push({
- fileIds: { $exists: true, $ne: [] }
- });
- }
+ }*/
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- } else if (ps.sinceDate) {
- sort._id = 1;
- query.createdAt = {
- $gt: new Date(ps.sinceDate)
- };
- } else if (ps.untilDate) {
- query.createdAt = {
- $lt: new Date(ps.untilDate)
- };
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
- const timeline = await Note.find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const timeline = await query.take(ps.limit).getMany();
activeUsersChart.update(user);
- return await packMany(timeline, user);
+ return await Notes.packMany(timeline, user);
});
diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts
index 17c24ab119..deda04acb4 100644
--- a/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -1,12 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { packMany } from '../../../../models/note';
-import UserList from '../../../../models/user-list';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import { getFriends } from '../../common/get-friends';
-import { getHideUserIds } from '../../common/get-hide-users';
import { ApiError } from '../../error';
+import { UserLists, UserListJoinings, Notes } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { activeUsersChart } from '../../../../services/chart';
export const meta = {
desc: {
@@ -21,7 +20,6 @@ export const meta = {
params: {
listId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': 'リストのID'
}
@@ -37,17 +35,15 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
- 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
+ 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
}
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
- 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+ 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
}
},
@@ -95,14 +91,6 @@ export const meta = {
'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
}
},
-
- mediaOnly: {
- validator: $.optional.bool,
- deprecated: true,
- desc: {
- 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
- }
- },
},
res: {
@@ -122,94 +110,28 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const [list, followings, hideUserIds] = await Promise.all([
- // リストを取得
- // Fetch the list
- UserList.findOne({
- _id: ps.listId,
- userId: user._id
- }),
-
- // フォローを取得
- // Fetch following
- getFriends(user._id, true, false),
-
- // 隠すユーザーを取得
- getHideUserIds(user)
- ]);
+ const list = await UserLists.findOne({
+ id: ps.listId,
+ userId: user.id
+ });
if (list == null) {
throw new ApiError(meta.errors.noSuchList);
}
- if (list.userIds.length == 0) {
- return [];
- }
-
//#region Construct query
- const sort = {
- _id: -1
- };
-
- const listQuery = list.userIds.map(u => ({
- userId: u,
-
- /*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める)
- $or: [{
- // リプライでない
- replyId: null
- }, { // または
- // リプライだが返信先が投稿者自身の投稿
- $expr: {
- $eq: ['$_reply.userId', '$userId']
- }
- }, { // または
- // リプライだが返信先が自分(フォロワー)の投稿
- '_reply.userId': user._id
- }, { // または
- // 自分(フォロワー)が送信したリプライ
- userId: user._id
- }]*/
- }));
-
- const visibleQuery = [{
- visibility: { $in: ['public', 'home'] }
- }, {
- // myself (for specified/private)
- userId: user._id
- }, {
- // to me (for specified)
- visibleUserIds: { $in: [user._id] }
- }, {
- visibility: 'followers',
- userId: { $in: followings.map(f => f.id) }
- }];
-
- const query = {
- $and: [{
- deletedAt: null,
+ const listQuery = UserListJoinings.createQueryBuilder('joining')
+ .select('joining.userId')
+ .where('joining.userListId = :userListId', { userListId: list.id });
- $and: [{
- // リストに入っている人のタイムラインへの投稿
- $or: listQuery
- }, {
- // visible for me
- $or: visibleQuery
- }],
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere(`note.userId IN (${ listQuery.getQuery() })`)
+ .leftJoinAndSelect('note.user', 'user')
+ .setParameters(listQuery.getParameters());
- // mute
- userId: {
- $nin: hideUserIds
- },
- '_reply.userId': {
- $nin: hideUserIds
- },
- '_renote.userId': {
- $nin: hideUserIds
- },
- }]
- } as any;
+ generateVisibilityQuery(query, user);
+ /* TODO
// MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
@@ -217,7 +139,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeMyRenotes === false) {
query.$and.push({
$or: [{
- userId: { $ne: user._id }
+ userId: { $ne: user.id }
}, {
renoteId: null
}, {
@@ -233,7 +155,7 @@ export default define(meta, async (ps, user) => {
if (ps.includeRenotedMyNotes === false) {
query.$and.push({
$or: [{
- '_renote.userId': { $ne: user._id }
+ '_renote.userId': { $ne: user.id }
}, {
renoteId: null
}, {
@@ -260,41 +182,16 @@ export default define(meta, async (ps, user) => {
poll: { $ne: null }
}]
});
- }
-
- const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
-
- if (withFiles) {
- query.$and.push({
- fileIds: { $exists: true, $ne: [] }
- });
- }
+ }*/
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- query._id = {
- $lt: ps.untilId
- };
- } else if (ps.sinceDate) {
- sort._id = 1;
- query.createdAt = {
- $gt: new Date(ps.sinceDate)
- };
- } else if (ps.untilDate) {
- query.createdAt = {
- $lt: new Date(ps.untilDate)
- };
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
- const timeline = await Note.find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const timeline = await query.take(ps.limit).getMany();
+
+ activeUsersChart.update(user);
- return await packMany(timeline, user);
+ return await Notes.packMany(timeline, user);
});
diff --git a/src/server/api/endpoints/notes/watching/create.ts b/src/server/api/endpoints/notes/watching/create.ts
index 2b2de1bd3b..b4045fe93c 100644
--- a/src/server/api/endpoints/notes/watching/create.ts
+++ b/src/server/api/endpoints/notes/watching/create.ts
@@ -1,5 +1,5 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import watch from '../../../../../services/note/watch';
import { getNote } from '../../../common/getters';
@@ -17,12 +17,11 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -45,5 +44,5 @@ export default define(meta, async (ps, user) => {
throw e;
});
- await watch(user._id, note);
+ await watch(user.id, note);
});
diff --git a/src/server/api/endpoints/notes/watching/delete.ts b/src/server/api/endpoints/notes/watching/delete.ts
index 512db793ea..a272ecc37d 100644
--- a/src/server/api/endpoints/notes/watching/delete.ts
+++ b/src/server/api/endpoints/notes/watching/delete.ts
@@ -1,5 +1,5 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import unwatch from '../../../../../services/note/unwatch';
import { getNote } from '../../../common/getters';
@@ -17,12 +17,11 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
noteId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID.'
@@ -45,5 +44,5 @@ export default define(meta, async (ps, user) => {
throw e;
});
- await unwatch(user._id, note);
+ await unwatch(user.id, note);
});
diff --git a/src/server/api/endpoints/notifications/mark-all-as-read.ts b/src/server/api/endpoints/notifications/mark-all-as-read.ts
index e5df648285..9f34a32e80 100644
--- a/src/server/api/endpoints/notifications/mark-all-as-read.ts
+++ b/src/server/api/endpoints/notifications/mark-all-as-read.ts
@@ -1,7 +1,6 @@
-import Notification from '../../../../models/notification';
import { publishMainStream } from '../../../../services/stream';
-import User from '../../../../models/user';
import define from '../../define';
+import { Notifications } from '../../../../models';
export const meta = {
desc: {
@@ -13,29 +12,18 @@ export const meta = {
requireCredential: true,
- kind: 'notification-write'
+ kind: 'write:notifications'
};
export default define(meta, async (ps, user) => {
// Update documents
- await Notification.update({
- notifieeId: user._id,
- isRead: false
+ await Notifications.update({
+ notifieeId: user.id,
+ isRead: false,
}, {
- $set: {
- isRead: true
- }
- }, {
- multi: true
- });
-
- // Update flag
- User.update({ _id: user._id }, {
- $set: {
- hasUnreadNotification: false
- }
+ isRead: true
});
// 全ての通知を読みましたよというイベントを発行
- publishMainStream(user._id, 'readAllNotifications');
+ publishMainStream(user.id, 'readAllNotifications');
});
diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts
index 30c49cdd86..f3ebaa16ad 100644
--- a/src/server/api/endpoints/stats.ts
+++ b/src/server/api/endpoints/stats.ts
@@ -1,7 +1,6 @@
import define from '../define';
-import driveChart from '../../../services/chart/drive';
-import federationChart from '../../../services/chart/federation';
-import fetchMeta from '../../../misc/fetch-meta';
+import { Notes, Users } from '../../../models';
+import { federationChart, driveChart } from '../../../services/chart';
export const meta = {
requireCredential: false,
@@ -43,16 +42,17 @@ export const meta = {
};
export default define(meta, async () => {
- const instance = await fetchMeta();
+ const [notesCount, originalNotesCount, usersCount, originalUsersCount, instances, driveUsageLocal, driveUsageRemote] = await Promise.all([
+ Notes.count(),
+ Notes.count({ userHost: null }),
+ Users.count(),
+ Users.count({ host: null }),
+ federationChart.getChart('hour', 1).then(chart => chart.instance.total[0]),
+ driveChart.getChart('hour', 1).then(chart => chart.local.totalSize[0]),
+ driveChart.getChart('hour', 1).then(chart => chart.remote.totalSize[0]),
+ ]);
- const stats: any = instance.stats;
-
- const driveStats = await driveChart.getChart('hour', 1);
- stats.driveUsageLocal = driveStats.local.totalSize[0];
- stats.driveUsageRemote = driveStats.remote.totalSize[0];
-
- const federationStats = await federationChart.getChart('hour', 1);
- stats.instances = federationStats.instance.total[0];
-
- return stats;
+ return {
+ notesCount, originalNotesCount, usersCount, originalUsersCount, instances, driveUsageLocal, driveUsageRemote
+ };
});
diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts
index 0b81b06abe..cb0572aa90 100644
--- a/src/server/api/endpoints/sw/register.ts
+++ b/src/server/api/endpoints/sw/register.ts
@@ -1,7 +1,8 @@
import $ from 'cafy';
-import Subscription from '../../../../models/sw-subscription';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
+import { genId } from '../../../../misc/gen-id';
+import { SwSubscriptions } from '../../../../models';
export const meta = {
tags: ['account'],
@@ -25,12 +26,11 @@ export const meta = {
export default define(meta, async (ps, user) => {
// if already subscribed
- const exist = await Subscription.findOne({
- userId: user._id,
+ const exist = await SwSubscriptions.findOne({
+ userId: user.id,
endpoint: ps.endpoint,
auth: ps.auth,
publickey: ps.publickey,
- deletedAt: { $exists: false }
});
const instance = await fetchMeta();
@@ -42,8 +42,9 @@ export default define(meta, async (ps, user) => {
};
}
- await Subscription.insert({
- userId: user._id,
+ await SwSubscriptions.save({
+ id: genId(),
+ userId: user.id,
endpoint: ps.endpoint,
auth: ps.auth,
publickey: ps.publickey
diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts
index 1d098eb399..42ab176652 100644
--- a/src/server/api/endpoints/username/available.ts
+++ b/src/server/api/endpoints/username/available.ts
@@ -1,7 +1,6 @@
import $ from 'cafy';
-import User from '../../../../models/user';
-import { validateUsername } from '../../../../models/user';
import define from '../../define';
+import { Users } from '../../../../models';
export const meta = {
tags: ['users'],
@@ -10,18 +9,16 @@ export const meta = {
params: {
username: {
- validator: $.str.pipe(validateUsername)
+ validator: $.str.pipe(Users.validateUsername)
}
}
};
export default define(meta, async (ps) => {
// Get exist
- const exist = await User.count({
+ const exist = await Users.count({
host: null,
usernameLower: ps.username.toLowerCase()
- }, {
- limit: 1
});
return {
diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts
index be83dcd9cc..f99165f3d5 100644
--- a/src/server/api/endpoints/users.ts
+++ b/src/server/api/endpoints/users.ts
@@ -1,10 +1,7 @@
import $ from 'cafy';
-import User, { pack } from '../../../models/user';
import define from '../define';
-import { fallback } from '../../../prelude/symbol';
-import { getHideUserIds } from '../common/get-hide-users';
-
-const nonnull = { $ne: null as any };
+import { Users } from '../../../models';
+import { generateMuteQueryForUsers } from '../common/generate-mute-query';
export const meta = {
tags: ['users'],
@@ -63,53 +60,38 @@ export const meta = {
},
};
-const state: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
- 'admin': { isAdmin: true },
- 'moderator': { isModerator: true },
- 'adminOrModerator': {
- $or: [
- { isAdmin: true },
- { isModerator: true }
- ]
- },
- 'verified': { isVerified: true },
- 'alive': {
- updatedAt: { $gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }
- },
- [fallback]: {}
-};
+export default define(meta, async (ps, me) => {
+ const query = Users.createQueryBuilder('user');
-const origin: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
- 'local': { host: null },
- 'remote': { host: nonnull },
- [fallback]: {}
-};
+ switch (ps.state) {
+ case 'admin': query.where('user.isAdmin = TRUE'); break;
+ case 'moderator': query.where('user.isModerator = TRUE'); break;
+ case 'adminOrModerator': query.where('user.isAdmin = TRUE OR isModerator = TRUE'); break;
+ case 'verified': query.where('user.isVerified = TRUE'); break;
+ case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
+ }
-const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
- '+follower': { followersCount: -1 },
- '-follower': { followersCount: 1 },
- '+createdAt': { createdAt: -1 },
- '-createdAt': { createdAt: 1 },
- '+updatedAt': { updatedAt: -1 },
- '-updatedAt': { updatedAt: 1 },
- [fallback]: { _id: -1 }
-};
+ switch (ps.origin) {
+ case 'local': query.andWhere('user.host IS NULL'); break;
+ case 'remote': query.andWhere('user.host IS NOT NULL'); break;
+ }
-export default define(meta, async (ps, me) => {
- const hideUserIds = await getHideUserIds(me);
+ switch (ps.sort) {
+ case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
+ case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
+ case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
+ case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
+ case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break;
+ case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
+ default: query.orderBy('user.id', 'ASC'); break;
+ }
+
+ if (me) generateMuteQueryForUsers(query, me);
+
+ query.take(ps.limit);
+ query.skip(ps.offset);
- const users = await User
- .find({
- $and: [
- state[ps.state] || state[fallback],
- origin[ps.origin] || origin[fallback]
- ],
- ...(hideUserIds && hideUserIds.length > 0 ? { _id: { $nin: hideUserIds } } : {})
- }, {
- limit: ps.limit,
- sort: sort[ps.sort] || sort[fallback],
- skip: ps.offset
- });
+ const users = await query.getMany();
- return await Promise.all(users.map(user => pack(user, me, { detail: true })));
+ return await Users.packMany(users, me, { detail: true });
});
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
index 3c8290a8b1..51b007ddaa 100644
--- a/src/server/api/endpoints/users/followers.ts
+++ b/src/server/api/endpoints/users/followers.ts
@@ -1,11 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import User from '../../../../models/user';
-import Following from '../../../../models/following';
-import { pack } from '../../../../models/user';
-import { getFriendIds } from '../../common/get-friends';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
+import { Users, Followings } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
desc: {
@@ -20,7 +18,6 @@ export const meta = {
params: {
userId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -35,38 +32,25 @@ export const meta = {
validator: $.optional.nullable.str
},
- limit: {
- validator: $.optional.num.range(1, 100),
- default: 10
+ sinceId: {
+ validator: $.optional.type(ID),
},
- cursor: {
+ untilId: {
validator: $.optional.type(ID),
- default: null as any,
- transform: transform,
},
- iknow: {
- validator: $.optional.bool,
- default: false,
- }
+ limit: {
+ validator: $.optional.num.range(1, 100),
+ default: 10
+ },
},
res: {
- type: 'object',
- properties: {
- users: {
- type: 'array',
- items: {
- type: 'User',
- }
- },
- next: {
- type: 'string',
- format: 'id',
- nullable: true
- }
- }
+ type: 'array',
+ items: {
+ type: 'Following',
+ },
},
errors: {
@@ -79,54 +63,20 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const q: any = ps.userId != null
- ? { _id: ps.userId }
- : { usernameLower: ps.username.toLowerCase(), host: ps.host };
-
- const user = await User.findOne(q);
+ const user = await Users.findOne(ps.userId != null
+ ? { id: ps.userId }
+ : { usernameLower: ps.username.toLowerCase(), host: ps.host });
- if (user === null) {
+ if (user == null) {
throw new ApiError(meta.errors.noSuchUser);
}
- const query = {
- followeeId: user._id
- } as any;
-
- // ログインしていてかつ iknow フラグがあるとき
- if (me && ps.iknow) {
- // Get my friends
- const myFriends = await getFriendIds(me._id);
-
- query.followerId = {
- $in: myFriends
- };
- }
-
- // カーソルが指定されている場合
- if (ps.cursor) {
- query._id = {
- $lt: ps.cursor
- };
- }
-
- // Get followers
- const following = await Following
- .find(query, {
- limit: ps.limit + 1,
- sort: { _id: -1 }
- });
-
- // 「次のページ」があるかどうか
- const inStock = following.length === ps.limit + 1;
- if (inStock) {
- following.pop();
- }
+ const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
+ .andWhere(`following.followeeId = :userId`, { userId: user.id });
- const users = await Promise.all(following.map(f => pack(f.followerId, me, { detail: true })));
+ const followings = await query
+ .take(ps.limit)
+ .getMany();
- return {
- users: users,
- next: inStock ? following[following.length - 1]._id : null,
- };
+ return await Followings.packMany(followings, me, { populateFollower: true });
});
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
index 4bc740cad9..46550f0f77 100644
--- a/src/server/api/endpoints/users/following.ts
+++ b/src/server/api/endpoints/users/following.ts
@@ -1,11 +1,9 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import User from '../../../../models/user';
-import Following from '../../../../models/following';
-import { pack } from '../../../../models/user';
-import { getFriendIds } from '../../common/get-friends';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
+import { Users, Followings } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
desc: {
@@ -20,7 +18,6 @@ export const meta = {
params: {
userId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -35,38 +32,25 @@ export const meta = {
validator: $.optional.nullable.str
},
- limit: {
- validator: $.optional.num.range(1, 100),
- default: 10
+ sinceId: {
+ validator: $.optional.type(ID),
},
- cursor: {
+ untilId: {
validator: $.optional.type(ID),
- default: null as any,
- transform: transform,
},
- iknow: {
- validator: $.optional.bool,
- default: false,
- }
+ limit: {
+ validator: $.optional.num.range(1, 100),
+ default: 10
+ },
},
res: {
- type: 'object',
- properties: {
- users: {
- type: 'array',
- items: {
- type: 'User',
- }
- },
- next: {
- type: 'string',
- format: 'id',
- nullable: true
- }
- }
+ type: 'array',
+ items: {
+ type: 'Following',
+ },
},
errors: {
@@ -79,54 +63,20 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const q: any = ps.userId != null
- ? { _id: ps.userId }
- : { usernameLower: ps.username.toLowerCase(), host: ps.host };
-
- const user = await User.findOne(q);
+ const user = await Users.findOne(ps.userId != null
+ ? { id: ps.userId }
+ : { usernameLower: ps.username.toLowerCase(), host: ps.host });
- if (user === null) {
+ if (user == null) {
throw new ApiError(meta.errors.noSuchUser);
}
- const query = {
- followerId: user._id
- } as any;
-
- // ログインしていてかつ iknow フラグがあるとき
- if (me && ps.iknow) {
- // Get my friends
- const myFriends = await getFriendIds(me._id);
-
- query.followeeId = {
- $in: myFriends
- };
- }
-
- // カーソルが指定されている場合
- if (ps.cursor) {
- query._id = {
- $lt: ps.cursor
- };
- }
-
- // Get followers
- const following = await Following
- .find(query, {
- limit: ps.limit + 1,
- sort: { _id: -1 }
- });
-
- // 「次のページ」があるかどうか
- const inStock = following.length === ps.limit + 1;
- if (inStock) {
- following.pop();
- }
+ const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
+ .andWhere(`following.followerId = :userId`, { userId: user.id });
- const users = await Promise.all(following.map(f => pack(f.followeeId, me, { detail: true })));
+ const followings = await query
+ .take(ps.limit)
+ .getMany();
- return {
- users: users,
- next: inStock ? following[following.length - 1]._id : null,
- };
+ return await Followings.packMany(followings, me, { populateFollowee: true });
});
diff --git a/src/server/api/endpoints/users/get-frequently-replied-users.ts b/src/server/api/endpoints/users/get-frequently-replied-users.ts
index 46c7fba2f6..f82f437629 100644
--- a/src/server/api/endpoints/users/get-frequently-replied-users.ts
+++ b/src/server/api/endpoints/users/get-frequently-replied-users.ts
@@ -1,12 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { pack } from '../../../../models/user';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { maximum } from '../../../../prelude/array';
-import { getHideUserIds } from '../../common/get-hide-users';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
+import { Not, In } from 'typeorm';
+import { Notes, Users } from '../../../../models';
export const meta = {
tags: ['users'],
@@ -16,7 +15,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -53,21 +51,16 @@ export default define(meta, async (ps, me) => {
});
// Fetch recent notes
- const recentNotes = await Note.find({
- userId: user._id,
- replyId: {
- $exists: true,
- $ne: null
- }
- }, {
- sort: {
- _id: -1
+ const recentNotes = await Notes.find({
+ where: {
+ userId: user.id,
+ replyId: Not(null)
},
- limit: 1000,
- fields: {
- _id: false,
- replyId: true
- }
+ order: {
+ id: -1
+ },
+ take: 1000,
+ select: ['replyId']
});
// 投稿が少なかったら中断
@@ -75,21 +68,12 @@ export default define(meta, async (ps, me) => {
return [];
}
- const hideUserIds = await getHideUserIds(me);
- hideUserIds.push(user._id);
-
- const replyTargetNotes = await Note.find({
- _id: {
- $in: recentNotes.map(p => p.replyId)
+ // TODO ミュートを考慮
+ const replyTargetNotes = await Notes.find({
+ where: {
+ id: In(recentNotes.map(p => p.replyId)),
},
- userId: {
- $nin: hideUserIds
- }
- }, {
- fields: {
- _id: false,
- userId: true
- }
+ select: ['userId']
});
const repliedUsers: any = {};
@@ -114,7 +98,7 @@ export default define(meta, async (ps, me) => {
// Make replies object (includes weights)
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
- user: await pack(user, me, { detail: true }),
+ user: await Users.pack(user, me, { detail: true }),
weight: repliedUsers[user] / peak
})));
diff --git a/src/server/api/endpoints/users/lists/create.ts b/src/server/api/endpoints/users/lists/create.ts
index 00d2538c9f..21dc6d331d 100644
--- a/src/server/api/endpoints/users/lists/create.ts
+++ b/src/server/api/endpoints/users/lists/create.ts
@@ -1,6 +1,8 @@
import $ from 'cafy';
-import UserList, { pack } from '../../../../../models/user-list';
import define from '../../../define';
+import { UserLists } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
+import { UserList } from '../../../../../models/entities/user-list';
export const meta = {
desc: {
@@ -12,7 +14,7 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
title: {
@@ -22,12 +24,12 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const userList = await UserList.insert({
+ const userList = await UserLists.save({
+ id: genId(),
createdAt: new Date(),
- userId: user._id,
- title: ps.title,
- userIds: []
- });
+ userId: user.id,
+ name: ps.title,
+ } as UserList);
- return await pack(userList);
+ return await UserLists.pack(userList);
});
diff --git a/src/server/api/endpoints/users/lists/delete.ts b/src/server/api/endpoints/users/lists/delete.ts
index d8faaa928c..0634bca4e3 100644
--- a/src/server/api/endpoints/users/lists/delete.ts
+++ b/src/server/api/endpoints/users/lists/delete.ts
@@ -1,8 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import UserList from '../../../../../models/user-list';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { UserLists } from '../../../../../models';
export const meta = {
desc: {
@@ -14,12 +14,11 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
listId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象となるユーザーリストのID',
'en-US': 'ID of target user list'
@@ -37,16 +36,14 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
- const userList = await UserList.findOne({
- _id: ps.listId,
- userId: user._id
+ const userList = await UserLists.findOne({
+ id: ps.listId,
+ userId: user.id
});
if (userList == null) {
throw new ApiError(meta.errors.noSuchList);
}
- await UserList.remove({
- _id: userList._id
- });
+ await UserLists.delete(userList.id);
});
diff --git a/src/server/api/endpoints/users/lists/list.ts b/src/server/api/endpoints/users/lists/list.ts
index ece2af5603..b05fc45527 100644
--- a/src/server/api/endpoints/users/lists/list.ts
+++ b/src/server/api/endpoints/users/lists/list.ts
@@ -1,5 +1,5 @@
-import UserList, { pack } from '../../../../../models/user-list';
import define from '../../../define';
+import { UserLists } from '../../../../../models';
export const meta = {
desc: {
@@ -10,7 +10,7 @@ export const meta = {
requireCredential: true,
- kind: 'account-read',
+ kind: 'read:account',
res: {
type: 'array',
@@ -21,9 +21,9 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const userLists = await UserList.find({
- userId: me._id,
+ const userLists = await UserLists.find({
+ userId: me.id,
});
- return await Promise.all(userLists.map(x => pack(x)));
+ return await Promise.all(userLists.map(x => UserLists.pack(x)));
});
diff --git a/src/server/api/endpoints/users/lists/pull.ts b/src/server/api/endpoints/users/lists/pull.ts
index 0eee1975db..524670b341 100644
--- a/src/server/api/endpoints/users/lists/pull.ts
+++ b/src/server/api/endpoints/users/lists/pull.ts
@@ -1,11 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import UserList from '../../../../../models/user-list';
-import { pack as packUser } from '../../../../../models/user';
+import { ID } from '../../../../../misc/cafy-id';
import { publishUserListStream } from '../../../../../services/stream';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters';
+import { UserLists, UserListJoinings, Users } from '../../../../../models';
export const meta = {
desc: {
@@ -17,17 +16,15 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
listId: {
validator: $.type(ID),
- transform: transform,
},
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -52,9 +49,9 @@ export const meta = {
export default define(meta, async (ps, me) => {
// Fetch the list
- const userList = await UserList.findOne({
- _id: ps.listId,
- userId: me._id,
+ const userList = await UserLists.findOne({
+ id: ps.listId,
+ userId: me.id,
});
if (userList == null) {
@@ -68,11 +65,7 @@ export default define(meta, async (ps, me) => {
});
// Pull the user
- await UserList.update({ _id: userList._id }, {
- $pull: {
- userIds: user._id
- }
- });
+ await UserListJoinings.delete({ userId: user.id });
- publishUserListStream(userList._id, 'userRemoved', await packUser(user));
+ publishUserListStream(userList.id, 'userRemoved', await Users.pack(user));
});
diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts
index eea2f39a8c..2763b3a19c 100644
--- a/src/server/api/endpoints/users/lists/push.ts
+++ b/src/server/api/endpoints/users/lists/push.ts
@@ -1,10 +1,10 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import UserList from '../../../../../models/user-list';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters';
import { pushUserToUserList } from '../../../../../services/user-list/push';
+import { UserLists, UserListJoinings } from '../../../../../models';
export const meta = {
desc: {
@@ -16,17 +16,15 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
listId: {
validator: $.type(ID),
- transform: transform,
},
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -57,9 +55,9 @@ export const meta = {
export default define(meta, async (ps, me) => {
// Fetch the list
- const userList = await UserList.findOne({
- _id: ps.listId,
- userId: me._id,
+ const userList = await UserLists.findOne({
+ id: ps.listId,
+ userId: me.id,
});
if (userList == null) {
@@ -72,7 +70,12 @@ export default define(meta, async (ps, me) => {
throw e;
});
- if (userList.userIds.map(id => id.toHexString()).includes(user._id.toHexString())) {
+ const exist = await UserListJoinings.findOne({
+ userListId: userList.id,
+ userId: user.id
+ });
+
+ if (exist) {
throw new ApiError(meta.errors.alreadyAdded);
}
diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts
index 0fab2fa499..1a997ec7c5 100644
--- a/src/server/api/endpoints/users/lists/show.ts
+++ b/src/server/api/endpoints/users/lists/show.ts
@@ -1,8 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import UserList, { pack } from '../../../../../models/user-list';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { UserLists } from '../../../../../models';
export const meta = {
desc: {
@@ -14,12 +14,11 @@ export const meta = {
requireCredential: true,
- kind: 'account-read',
+ kind: 'read:account',
params: {
listId: {
validator: $.type(ID),
- transform: transform,
},
},
@@ -38,14 +37,14 @@ export const meta = {
export default define(meta, async (ps, me) => {
// Fetch the list
- const userList = await UserList.findOne({
- _id: ps.listId,
- userId: me._id,
+ const userList = await UserLists.findOne({
+ id: ps.listId,
+ userId: me.id,
});
if (userList == null) {
throw new ApiError(meta.errors.noSuchList);
}
- return await pack(userList);
+ return await UserLists.pack(userList);
});
diff --git a/src/server/api/endpoints/users/lists/update.ts b/src/server/api/endpoints/users/lists/update.ts
index 5897693144..dc08d59f6a 100644
--- a/src/server/api/endpoints/users/lists/update.ts
+++ b/src/server/api/endpoints/users/lists/update.ts
@@ -1,8 +1,8 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import UserList, { pack } from '../../../../../models/user-list';
+import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
+import { UserLists } from '../../../../../models';
export const meta = {
desc: {
@@ -14,19 +14,18 @@ export const meta = {
requireCredential: true,
- kind: 'account-write',
+ kind: 'write:account',
params: {
listId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象となるユーザーリストのID',
'en-US': 'ID of target user list'
}
},
- title: {
+ name: {
validator: $.str.range(1, 100),
desc: {
'ja-JP': 'このユーザーリストの名前',
@@ -46,20 +45,18 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Fetch the list
- const userList = await UserList.findOne({
- _id: ps.listId,
- userId: user._id
+ const userList = await UserLists.findOne({
+ id: ps.listId,
+ userId: user.id
});
if (userList == null) {
throw new ApiError(meta.errors.noSuchList);
}
- await UserList.update({ _id: userList._id }, {
- $set: {
- title: ps.title
- }
+ await UserLists.update(userList.id, {
+ name: ps.name
});
- return await pack(userList._id);
+ return await UserLists.pack(userList.id);
});
diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
index 10d2f37fc2..6df394cbb1 100644
--- a/src/server/api/endpoints/users/notes.ts
+++ b/src/server/api/endpoints/users/notes.ts
@@ -1,10 +1,13 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note, { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import Following from '../../../../models/following';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { Notes } from '../../../../models';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { Brackets } from 'typeorm';
export const meta = {
desc: {
@@ -16,7 +19,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -42,17 +44,15 @@ export const meta = {
sinceId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
- 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
+ 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
}
},
untilId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
- 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+ 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
}
},
@@ -102,15 +102,6 @@ export const meta = {
}
},
- mediaOnly: {
- validator: $.optional.bool,
- default: false,
- deprecated: true,
- desc: {
- 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
- }
- },
-
fileType: {
validator: $.optional.arr($.str),
desc: {
@@ -150,67 +141,44 @@ export default define(meta, async (ps, me) => {
throw e;
});
- const isFollowing = me == null ? false : ((await Following.findOne({
- followerId: me._id,
- followeeId: user._id
- })) != null);
-
//#region Construct query
- const sort = { } as any;
+ const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere('note.userId = :userId', { userId: user.id })
+ .leftJoinAndSelect('note.user', 'user');
- const visibleQuery = me == null ? [{
- visibility: { $in: ['public', 'home'] }
- }] : [{
- visibility: {
- $in: isFollowing ? ['public', 'home', 'followers'] : ['public', 'home']
- }
- }, {
- // myself (for specified/private)
- userId: me._id
- }, {
- // to me (for specified)
- visibleUserIds: { $in: [ me._id ] }
- }];
+ if (me) generateVisibilityQuery(query, me);
+ if (me) generateMuteQuery(query, me);
+
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
+ }
- const query = {
- $and: [ {} ],
- deletedAt: null,
- userId: user._id,
- $or: visibleQuery
- } as any;
+ if (ps.fileType) {
+ query.andWhere('note.fileIds != \'{}\'');
+ query.andWhere(new Brackets(qb => {
+ for (const type of ps.fileType) {
+ const i = ps.fileType.indexOf(type);
+ qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
+ }
+ }));
- if (ps.sinceId) {
- sort._id = 1;
- query._id = {
- $gt: ps.sinceId
- };
- } else if (ps.untilId) {
- sort._id = -1;
- query._id = {
- $lt: ps.untilId
- };
- } else if (ps.sinceDate) {
- sort.createdAt = 1;
- query.createdAt = {
- $gt: new Date(ps.sinceDate)
- };
- } else if (ps.untilDate) {
- sort.createdAt = -1;
- query.createdAt = {
- $lt: new Date(ps.untilDate)
- };
- } else {
- sort._id = -1;
+ if (ps.excludeNsfw) {
+ // v11 TODO
+ /*query['_files.isSensitive'] = {
+ $ne: true
+ };*/
+ }
}
if (!ps.includeReplies) {
- query.replyId = null;
+ query.andWhere('note.replyId IS NULL');
}
+ /* TODO
if (ps.includeMyRenotes === false) {
query.$and.push({
$or: [{
- userId: { $ne: user._id }
+ userId: { $ne: user.id }
}, {
renoteId: null
}, {
@@ -222,35 +190,11 @@ export default define(meta, async (ps, me) => {
}]
});
}
+ */
- const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
-
- if (withFiles) {
- query.fileIds = {
- $exists: true,
- $ne: []
- };
- }
-
- if (ps.fileType) {
- query.fileIds = { $exists: true, $ne: [] };
-
- query['_files.contentType'] = {
- $in: ps.fileType
- };
-
- if (ps.excludeNsfw) {
- query['_files.metadata.isSensitive'] = {
- $ne: true
- };
- }
- }
//#endregion
- const notes = await Note.find(query, {
- limit: ps.limit,
- sort: sort
- });
+ const timeline = await query.take(ps.limit).getMany();
- return await packMany(notes, me);
+ return await Notes.packMany(timeline, user);
});
diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts
index 60710fffca..2c82d6613e 100644
--- a/src/server/api/endpoints/users/recommendation.ts
+++ b/src/server/api/endpoints/users/recommendation.ts
@@ -1,14 +1,8 @@
import * as ms from 'ms';
import $ from 'cafy';
-import User, { pack, ILocalUser } from '../../../../models/user';
-import { getFriendIds } from '../../common/get-friends';
-import * as request from 'request-promise-native';
-import config from '../../../../config';
import define from '../../define';
-import fetchMeta from '../../../../misc/fetch-meta';
-import resolveUser from '../../../../remote/resolve-user';
-import { getHideUserIds } from '../../common/get-hide-users';
-import { apiLogger } from '../../logger';
+import { Users, Followings } from '../../../../models';
+import { generateMuteQueryForUsers } from '../../common/generate-mute-query';
export const meta = {
desc: {
@@ -19,7 +13,7 @@ export const meta = {
requireCredential: true,
- kind: 'account-read',
+ kind: 'read:account',
params: {
limit: {
@@ -42,83 +36,24 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const instance = await fetchMeta();
+ 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')) })
+ .orderBy('user.followersCount', 'DESC');
- if (instance.enableExternalUserRecommendation) {
- const userName = me.username;
- const hostName = config.hostname;
- const limit = ps.limit;
- const offset = ps.offset;
- const timeout = instance.externalUserRecommendationTimeout;
- const engine = instance.externalUserRecommendationEngine;
- const url = engine
- .replace('{{host}}', hostName)
- .replace('{{user}}', userName)
- .replace('{{limit}}', limit.toString())
- .replace('{{offset}}', offset.toString());
+ generateMuteQueryForUsers(query, me);
- const users = await request({
- url: url,
- proxy: config.proxy,
- timeout: timeout,
- json: true,
- followRedirect: true,
- followAllRedirects: true
- })
- .then(body => convertUsers(body, me));
+ const followingQuery = Followings.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
- return users;
- } else {
- // ID list of the user itself and other users who the user follows
- const followingIds = await getFriendIds(me._id);
+ query
+ .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`);
- // 隠すユーザーを取得
- const hideUserIds = await getHideUserIds(me);
+ query.setParameters(followingQuery.getParameters());
- const users = await User.find({
- _id: {
- $nin: followingIds.concat(hideUserIds)
- },
- isLocked: { $ne: true },
- updatedAt: {
- $gte: new Date(Date.now() - ms('7days'))
- },
- host: null
- }, {
- limit: ps.limit,
- skip: ps.offset,
- sort: {
- followersCount: -1
- }
- });
+ const users = await query.take(ps.limit).skip(ps.offset).getMany();
- return await Promise.all(users.map(user => pack(user, me, { detail: true })));
- }
+ return await Users.packMany(users, me, { detail: true });
});
-
-type IRecommendUser = {
- name: string;
- username: string;
- host: string;
- description: string;
- avatarUrl: string;
-};
-
-/**
- * Resolve/Pack dummy users
- */
-async function convertUsers(src: IRecommendUser[], me: ILocalUser) {
- const packed = await Promise.all(src.map(async x => {
- const user = await resolveUser(x.username, x.host)
- .catch(() => {
- apiLogger.warn(`Can't resolve ${x.username}@${x.host}`);
- return null;
- });
-
- if (user == null) return x;
-
- return await pack(user, me, { detail: true });
- }));
-
- return packed;
-}
diff --git a/src/server/api/endpoints/users/relation.ts b/src/server/api/endpoints/users/relation.ts
index f4121aa0d0..4971738d32 100644
--- a/src/server/api/endpoints/users/relation.ts
+++ b/src/server/api/endpoints/users/relation.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import ID, { transform, ObjectId } from '../../../../misc/cafy-id';
-import { getRelation } from '../../../../models/user';
import define from '../../define';
+import { ID } from '../../../../misc/cafy-id';
+import { Users } from '../../../../models';
export const meta = {
desc: {
@@ -15,7 +15,6 @@ export const meta = {
params: {
userId: {
validator: $.either($.type(ID), $.arr($.type(ID)).unique()),
- transform: (v: any): ObjectId | ObjectId[] => Array.isArray(v) ? v.map(x => transform(x)) : transform(v),
desc: {
'ja-JP': 'ユーザーID (配列でも可)'
}
@@ -26,7 +25,7 @@ export const meta = {
export default define(meta, async (ps, me) => {
const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
- const relations = await Promise.all(ids.map(id => getRelation(me._id, id)));
+ const relations = await Promise.all(ids.map(id => Users.getRelation(me.id, id)));
return Array.isArray(ps.userId) ? relations : relations[0];
});
diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts
index 0f23f8f0c3..2ee28c9002 100644
--- a/src/server/api/endpoints/users/report-abuse.ts
+++ b/src/server/api/endpoints/users/report-abuse.ts
@@ -1,11 +1,11 @@
import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
-import User from '../../../../models/user';
-import AbuseUserReport from '../../../../models/abuse-user-report';
import { publishAdminStream } from '../../../../services/stream';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
+import { AbuseUserReports, Users } from '../../../../models';
+import { genId } from '../../../../misc/gen-id';
export const meta = {
desc: {
@@ -19,7 +19,6 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -62,7 +61,7 @@ export default define(meta, async (ps, me) => {
throw e;
});
- if (user._id.equals(me._id)) {
+ if (user.id === me.id) {
throw new ApiError(meta.errors.cannotReportYourself);
}
@@ -70,17 +69,18 @@ export default define(meta, async (ps, me) => {
throw new ApiError(meta.errors.cannotReportAdmin);
}
- const report = await AbuseUserReport.insert({
+ const report = await AbuseUserReports.save({
+ id: genId(),
createdAt: new Date(),
- userId: user._id,
- reporterId: me._id,
+ userId: user.id,
+ reporterId: me.id,
comment: ps.comment
});
// Publish event to moderators
setTimeout(async () => {
- const moderators = await User.find({
- $or: [{
+ const moderators = await Users.find({
+ where: [{
isAdmin: true
}, {
isModerator: true
@@ -88,8 +88,8 @@ export default define(meta, async (ps, me) => {
});
for (const moderator of moderators) {
- publishAdminStream(moderator._id, 'newAbuseUserReport', {
- id: report._id,
+ publishAdminStream(moderator.id, 'newAbuseUserReport', {
+ id: report.id,
userId: report.userId,
reporterId: report.reporterId,
comment: report.comment
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
index a95f6df6de..2e76546ade 100644
--- a/src/server/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
-import * as escapeRegexp from 'escape-regexp';
-import User, { pack, validateUsername, IUser } from '../../../../models/user';
import define from '../../define';
+import { Users } from '../../../../models';
+import { User } from '../../../../models/entities/user';
export const meta = {
desc: {
@@ -62,34 +62,30 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
- const isUsername = validateUsername(ps.query.replace('@', ''), !ps.localOnly);
+ const isUsername = Users.validateUsername(ps.query.replace('@', ''), !ps.localOnly);
- let users: IUser[] = [];
+ let users: User[] = [];
if (isUsername) {
- users = await User
- .find({
- host: null,
- usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())),
- isSuspended: { $ne: true }
- }, {
- limit: ps.limit,
- skip: ps.offset
- });
+ users = await Users.createQueryBuilder('user')
+ .where('user.host IS NULL')
+ .where('user.isSuspended = FALSE')
+ .where('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
+ .take(ps.limit)
+ .skip(ps.offset)
+ .getMany();
if (users.length < ps.limit && !ps.localOnly) {
- const otherUsers = await User
- .find({
- host: { $ne: null },
- usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())),
- isSuspended: { $ne: true }
- }, {
- limit: ps.limit - users.length
- });
+ const otherUsers = await Users.createQueryBuilder('user')
+ .where('user.host IS NOT NULL')
+ .where('user.isSuspended = FALSE')
+ .where('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
+ .take(ps.limit - users.length)
+ .getMany();
users = users.concat(otherUsers);
}
}
- return await Promise.all(users.map(user => pack(user, me, { detail: ps.detail })));
+ return await Users.packMany(users, me, { detail: ps.detail });
});
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index 4e59945eba..a605eaf30a 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -1,12 +1,11 @@
import $ from 'cafy';
-import ID, { transform, transformMany } from '../../../../misc/cafy-id';
-import User, { pack, isRemoteUser } from '../../../../models/user';
import resolveRemoteUser from '../../../../remote/resolve-user';
import define from '../../define';
import { apiLogger } from '../../logger';
import { ApiError } from '../../error';
-
-const cursorOption = { fields: { data: false } };
+import { ID } from '../../../../misc/cafy-id';
+import { Users } from '../../../../models';
+import { In } from 'typeorm';
export const meta = {
desc: {
@@ -20,7 +19,6 @@ export const meta = {
params: {
userId: {
validator: $.optional.type(ID),
- transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
@@ -29,7 +27,6 @@ export const meta = {
userIds: {
validator: $.optional.arr($.type(ID)).unique(),
- transform: transformMany,
desc: {
'ja-JP': 'ユーザーID (配列)'
}
@@ -68,42 +65,40 @@ export default define(meta, async (ps, me) => {
let user;
if (ps.userIds) {
- const users = await User.find({
- _id: {
- $in: ps.userIds
- }
+ const users = await Users.find({
+ id: In(ps.userIds)
});
- return await Promise.all(users.map(u => pack(u, me, {
+ return await Promise.all(users.map(u => Users.pack(u, me, {
detail: true
})));
} else {
// Lookup user
if (typeof ps.host === 'string') {
- user = await resolveRemoteUser(ps.username, ps.host, cursorOption).catch(e => {
+ user = await resolveRemoteUser(ps.username, ps.host).catch(e => {
apiLogger.warn(`failed to resolve remote user: ${e}`);
throw new ApiError(meta.errors.failedToResolveRemoteUser);
});
} else {
const q: any = ps.userId != null
- ? { _id: ps.userId }
+ ? { id: ps.userId }
: { usernameLower: ps.username.toLowerCase(), host: null };
- user = await User.findOne(q, cursorOption);
+ user = await Users.findOne(q);
}
- if (user === null) {
+ if (user == null) {
throw new ApiError(meta.errors.noSuchUser);
}
// ユーザー情報更新
- if (isRemoteUser(user)) {
+ if (Users.isRemoteUser(user)) {
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
resolveRemoteUser(ps.username, ps.host, { }, true);
}
}
- return await pack(user, me, {
+ return await Users.pack(user, me, {
detail: true
});
}
diff --git a/src/server/api/index.ts b/src/server/api/index.ts
index fac57ca06e..7858efd927 100644
--- a/src/server/api/index.ts
+++ b/src/server/api/index.ts
@@ -15,8 +15,8 @@ import signin from './private/signin';
import discord from './service/discord';
import github from './service/github';
import twitter from './service/twitter';
-import Instance from '../../models/instance';
import { toASCII } from 'punycode';
+import { Instances } from '../../models';
// Init app
const app = new Koa();
@@ -67,10 +67,9 @@ router.use(github.routes());
router.use(twitter.routes());
router.get('/v1/instance/peers', async ctx => {
- const instances = await Instance.find({
- }, {
- host: 1
- });
+ const instances = await Instances.find({
+ select: ['host']
+ });
const punyCodes = instances.map(instance => toASCII(instance.host));
diff --git a/src/server/api/limiter.ts b/src/server/api/limiter.ts
index 3d66172fd8..e29c061337 100644
--- a/src/server/api/limiter.ts
+++ b/src/server/api/limiter.ts
@@ -2,12 +2,12 @@ import * as Limiter from 'ratelimiter';
import limiterDB from '../../db/redis';
import { IEndpoint } from './endpoints';
import getAcct from '../../misc/acct/render';
-import { IUser } from '../../models/user';
+import { User } from '../../models/entities/user';
import Logger from '../../services/logger';
const logger = new Logger('limiter');
-export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) => {
+export default (endpoint: IEndpoint, user: User) => new Promise((ok, reject) => {
// Redisがインストールされてない場合は常に許可
if (limiterDB == null) {
ok();
@@ -38,7 +38,7 @@ export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) =>
// Short-term limit
function min() {
const minIntervalLimiter = new Limiter({
- id: `${user._id}:${key}:min`,
+ id: `${user.id}:${key}:min`,
duration: limitation.minInterval,
max: 1,
db: limiterDB
@@ -66,7 +66,7 @@ export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) =>
// Long term limit
function max() {
const limiter = new Limiter({
- id: `${user._id}:${key}`,
+ id: `${user.id}:${key}`,
duration: limitation.duration,
max: limitation.max,
db: limiterDB
diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts
index 70a0d6faf0..5992fee835 100644
--- a/src/server/api/openapi/schemas.ts
+++ b/src/server/api/openapi/schemas.ts
@@ -221,7 +221,7 @@ export const schemas = {
},
type: {
type: 'string',
- enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote'],
+ enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'],
description: 'The type of the notification.'
},
},
@@ -258,7 +258,7 @@ export const schemas = {
description: 'The MD5 hash of this Drive file.',
example: '15eca7fba0480996e2245f5185bf39f2'
},
- datasize: {
+ size: {
type: 'number',
description: 'The size of this Drive file. (bytes)',
example: 51469
@@ -275,7 +275,7 @@ export const schemas = {
description: 'Whether this Drive file is sensitive.',
},
},
- required: ['id', 'createdAt', 'name', 'type', 'datasize', 'md5']
+ required: ['id', 'createdAt', 'name', 'type', 'size', 'md5']
},
DriveFolder: {
@@ -318,6 +318,40 @@ export const schemas = {
required: ['id', 'createdAt', 'name']
},
+ Following: {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'id',
+ description: 'The unique identifier for this following.',
+ example: 'xxxxxxxxxxxxxxxxxxxxxxxx',
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ description: 'The date that the following was created.'
+ },
+ followeeId: {
+ type: 'string',
+ format: 'id',
+ },
+ followee: {
+ $ref: '#/components/schemas/User',
+ description: 'The followee.'
+ },
+ followerId: {
+ type: 'string',
+ format: 'id',
+ },
+ follower: {
+ $ref: '#/components/schemas/User',
+ description: 'The follower.'
+ },
+ },
+ required: ['id', 'createdAt', 'followeeId', 'followerId']
+ },
+
Muting: {
type: 'object',
properties: {
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 40bcd2c5d6..c1fd908d8a 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -1,11 +1,12 @@
import * as Koa from 'koa';
import * as bcrypt from 'bcryptjs';
import * as speakeasy from 'speakeasy';
-import User, { ILocalUser } from '../../../models/user';
-import Signin, { pack } from '../../../models/signin';
import { publishMainStream } from '../../../services/stream';
import signin from '../common/signin';
import config from '../../../config';
+import { Users, Signins } from '../../../models';
+import { ILocalUser } from '../../../models/entities/user';
+import { genId } from '../../../misc/gen-id';
export default async (ctx: Koa.BaseContext) => {
ctx.set('Access-Control-Allow-Origin', config.url);
@@ -32,17 +33,12 @@ export default async (ctx: Koa.BaseContext) => {
}
// Fetch user
- const user = await User.findOne({
+ const user = await Users.findOne({
usernameLower: username.toLowerCase(),
host: null
- }, {
- fields: {
- data: false,
- profile: false
- }
- }) as ILocalUser;
+ }) as ILocalUser;
- if (user === null) {
+ if (user == null) {
ctx.throw(404, {
error: 'user not found'
});
@@ -77,14 +73,15 @@ export default async (ctx: Koa.BaseContext) => {
}
// Append signin history
- const record = await Signin.insert({
+ const record = await Signins.save({
+ id: genId(),
createdAt: new Date(),
- userId: user._id,
+ userId: user.id,
ip: ctx.ip,
headers: ctx.headers,
success: same
});
// Publish signin event
- publishMainStream(user._id, 'signin', await pack(record));
+ publishMainStream(user.id, 'signin', await Signins.pack(record));
};
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index 89b7b330d2..1d304b8e11 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -1,14 +1,15 @@
import * as Koa from 'koa';
import * as bcrypt from 'bcryptjs';
-import { generate as generateKeypair } from '../../../crypto_key';
-import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user';
+import { generateKeyPair } from 'crypto';
import generateUserToken from '../common/generate-native-user-token';
import config from '../../../config';
-import Meta from '../../../models/meta';
-import RegistrationTicket from '../../../models/registration-tickets';
-import usersChart from '../../../services/chart/users';
import fetchMeta from '../../../misc/fetch-meta';
import * as recaptcha from 'recaptcha-promise';
+import { Users, RegistrationTickets, UserServiceLinkings, UserKeypairs } from '../../../models';
+import { genId } from '../../../misc/gen-id';
+import { usersChart } from '../../../services/chart';
+import { UserServiceLinking } from '../../../models/entities/user-service-linking';
+import { User } from '../../../models/entities/user';
export default async (ctx: Koa.BaseContext) => {
const body = ctx.request.body as any;
@@ -32,6 +33,7 @@ export default async (ctx: Koa.BaseContext) => {
const username = body['username'];
const password = body['password'];
+ const host = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null;
const invitationCode = body['invitationCode'];
if (instance && instance.disableRegistration) {
@@ -40,7 +42,7 @@ export default async (ctx: Koa.BaseContext) => {
return;
}
- const ticket = await RegistrationTicket.findOne({
+ const ticket = await RegistrationTickets.findOne({
code: invitationCode
});
@@ -49,39 +51,22 @@ export default async (ctx: Koa.BaseContext) => {
return;
}
- RegistrationTicket.remove({
- _id: ticket._id
- });
+ RegistrationTickets.delete(ticket.id);
}
// Validate username
- if (!validateUsername(username)) {
+ if (!Users.validateUsername(username)) {
ctx.status = 400;
return;
}
// Validate password
- if (!validatePassword(password)) {
+ if (!Users.validatePassword(password)) {
ctx.status = 400;
return;
}
- const usersCount = await User.count({});
-
- // Fetch exist user that same username
- const usernameExist = await User
- .count({
- usernameLower: username.toLowerCase(),
- host: null
- }, {
- limit: 1
- });
-
- // Check username already used
- if (usernameExist !== 0) {
- ctx.status = 400;
- return;
- }
+ const usersCount = await Users.count({});
// Generate hash of password
const salt = await bcrypt.genSalt(8);
@@ -90,46 +75,50 @@ export default async (ctx: Koa.BaseContext) => {
// Generate secret
const secret = generateUserToken();
- // Create account
- const account: IUser = await User.insert({
- avatarId: null,
- bannerId: null,
+ if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) {
+ ctx.status = 400;
+ return;
+ }
+
+ const account = await Users.save({
+ id: genId(),
createdAt: new Date(),
- description: null,
- followersCount: 0,
- followingCount: 0,
- name: null,
- notesCount: 0,
username: username,
usernameLower: username.toLowerCase(),
- host: null,
- keypair: generateKeypair(),
+ host: host,
token: secret,
password: hash,
isAdmin: config.autoAdmin && usersCount === 0,
autoAcceptFollowed: true,
- profile: {
- bio: null,
- birthday: null,
- location: null
- },
- settings: {
- autoWatch: false
- }
+ autoWatch: false
+ } as User);
+
+ await UserKeypairs.save({
+ id: genId(),
+ keyPem: await new Promise<string>((s, j) => generateKeyPair('rsa', {
+ modulusLength: 4096,
+ publicKeyEncoding: {
+ type: 'pkcs1',
+ format: 'pem'
+ },
+ privateKeyEncoding: {
+ type: 'pkcs1',
+ format: 'pem',
+ cipher: undefined,
+ passphrase: undefined
+ }
+ }, (e, _, x) => e ? j(e) : s(x))),
+ userId: account.id
});
- //#region Increment users count
- Meta.update({}, {
- $inc: {
- 'stats.usersCount': 1,
- 'stats.originalUsersCount': 1
- }
- }, { upsert: true });
- //#endregion
+ await UserServiceLinkings.save({
+ id: genId(),
+ userId: account.id
+ } as UserServiceLinking);
usersChart.update(account, true);
- const res = await pack(account, account, {
+ const res = await Users.pack(account, account, {
detail: true,
includeSecrets: true
});
diff --git a/src/server/api/service/discord.ts b/src/server/api/service/discord.ts
index 92f5bbf72d..4290e1ff9d 100644
--- a/src/server/api/service/discord.ts
+++ b/src/server/api/service/discord.ts
@@ -2,13 +2,14 @@ import * as Koa from 'koa';
import * as Router from 'koa-router';
import * as request from 'request';
import { OAuth2 } from 'oauth';
-import User, { pack, ILocalUser } from '../../../models/user';
import config from '../../../config';
import { publishMainStream } from '../../../services/stream';
import redis from '../../../db/redis';
import * as uuid from 'uuid';
import signin from '../common/signin';
import fetchMeta from '../../../misc/fetch-meta';
+import { Users, UserServiceLinkings } from '../../../models';
+import { ILocalUser } from '../../../models/entities/user';
function getUserToken(ctx: Koa.BaseContext) {
return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
@@ -39,19 +40,27 @@ router.get('/disconnect/discord', async ctx => {
return;
}
- const user = await User.findOneAndUpdate({
+ const user = await Users.findOne({
host: null,
- 'token': userToken
+ token: userToken
+ });
+
+ await UserServiceLinkings.update({
+ userId: user.id
}, {
- $set: {
- 'discord': null
- }
+ discord: false,
+ discordAccessToken: null,
+ discordRefreshToken: null,
+ discordExpiresDate: null,
+ discordId: null,
+ discordUsername: null,
+ discordDiscriminator: null,
});
ctx.body = `Discordの連携を解除しました :v:`;
// Publish i updated event
- publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+ publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
detail: true,
includeSecrets: true
}));
@@ -193,32 +202,30 @@ router.get('/dc/cb', async ctx => {
return;
}
- let user = await User.findOne({
- host: null,
- 'discord.id': id
- }) as ILocalUser;
+ const link = await UserServiceLinkings.createQueryBuilder()
+ .where('discord @> :discord', {
+ discord: {
+ id: id,
+ },
+ })
+ .andWhere('userHost IS NULL')
+ .getOne();
- if (!user) {
+ if (link == null) {
ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
return;
}
- user = await User.findOneAndUpdate({
- host: null,
- 'discord.id': id
- }, {
- $set: {
- discord: {
- accessToken,
- refreshToken,
- expiresDate,
- username,
- discriminator
- }
- }
- }) as ILocalUser;
+ await UserServiceLinkings.update(link.id, {
+ discord: true,
+ discordAccessToken: accessToken,
+ discordRefreshToken: refreshToken,
+ discordExpiresDate: expiresDate,
+ discordUsername: username,
+ discordDiscriminator: discriminator
+ });
- signin(ctx, user, true);
+ signin(ctx, await Users.findOne(link.userId) as ILocalUser, true);
} else {
const code = ctx.query.code;
@@ -277,26 +284,25 @@ router.get('/dc/cb', async ctx => {
return;
}
- const user = await User.findOneAndUpdate({
+ const user = await Users.findOne({
host: null,
token: userToken
- }, {
- $set: {
- discord: {
- accessToken,
- refreshToken,
- expiresDate,
- id,
- username,
- discriminator
- }
- }
+ });
+
+ await UserServiceLinkings.update({ userId: user.id }, {
+ discord: true,
+ discordAccessToken: accessToken,
+ discordRefreshToken: refreshToken,
+ discordExpiresDate: expiresDate,
+ discordId: id,
+ discordUsername: username,
+ discordDiscriminator: discriminator
});
ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
// Publish i updated event
- publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+ publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
detail: true,
includeSecrets: true
}));
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index cf3589a4b7..e59b149d19 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -2,13 +2,14 @@ import * as Koa from 'koa';
import * as Router from 'koa-router';
import * as request from 'request';
import { OAuth2 } from 'oauth';
-import User, { pack, ILocalUser } from '../../../models/user';
import config from '../../../config';
import { publishMainStream } from '../../../services/stream';
import redis from '../../../db/redis';
import * as uuid from 'uuid';
import signin from '../common/signin';
import fetchMeta from '../../../misc/fetch-meta';
+import { Users, UserServiceLinkings } from '../../../models';
+import { ILocalUser } from '../../../models/entities/user';
function getUserToken(ctx: Koa.BaseContext) {
return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
@@ -39,19 +40,24 @@ router.get('/disconnect/github', async ctx => {
return;
}
- const user = await User.findOneAndUpdate({
+ const user = await Users.findOne({
host: null,
- 'token': userToken
+ token: userToken
+ });
+
+ await UserServiceLinkings.update({
+ userId: user.id
}, {
- $set: {
- 'github': null
- }
+ github: false,
+ githubAccessToken: null,
+ githubId: null,
+ githubLogin: null,
});
ctx.body = `GitHubの連携を解除しました :v:`;
// Publish i updated event
- publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+ publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
detail: true,
includeSecrets: true
}));
@@ -185,17 +191,21 @@ router.get('/gh/cb', async ctx => {
return;
}
- const user = await User.findOne({
- host: null,
- 'github.id': id
- }) as ILocalUser;
+ const link = await UserServiceLinkings.createQueryBuilder()
+ .where('github @> :github', {
+ github: {
+ id: id,
+ },
+ })
+ .andWhere('userHost IS NULL')
+ .getOne();
- if (!user) {
+ if (link == null) {
ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
return;
}
- signin(ctx, user, true);
+ signin(ctx, await Users.findOne(link.userId) as ILocalUser, true);
} else {
const code = ctx.query.code;
@@ -248,23 +258,22 @@ router.get('/gh/cb', async ctx => {
return;
}
- const user = await User.findOneAndUpdate({
+ const user = await Users.findOne({
host: null,
token: userToken
- }, {
- $set: {
- github: {
- accessToken,
- id,
- login
- }
- }
+ });
+
+ await UserServiceLinkings.update({ userId: user.id }, {
+ github: true,
+ githubAccessToken: accessToken,
+ githubId: id,
+ githubLogin: login,
});
ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
// Publish i updated event
- publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+ publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
detail: true,
includeSecrets: true
}));
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index fc23808e21..77cf71395b 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -3,11 +3,12 @@ import * as Router from 'koa-router';
import * as uuid from 'uuid';
import autwh from 'autwh';
import redis from '../../../db/redis';
-import User, { pack, ILocalUser } from '../../../models/user';
import { publishMainStream } from '../../../services/stream';
import config from '../../../config';
import signin from '../common/signin';
import fetchMeta from '../../../misc/fetch-meta';
+import { Users, UserServiceLinkings } from '../../../models';
+import { ILocalUser } from '../../../models/entities/user';
function getUserToken(ctx: Koa.BaseContext) {
return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
@@ -38,19 +39,25 @@ router.get('/disconnect/twitter', async ctx => {
return;
}
- const user = await User.findOneAndUpdate({
+ const user = await Users.findOne({
host: null,
- 'token': userToken
+ token: userToken
+ });
+
+ await UserServiceLinkings.update({
+ userId: user.id
}, {
- $set: {
- 'twitter': null
- }
+ twitter: false,
+ twitterAccessToken: null,
+ twitterAccessTokenSecret: null,
+ twitterUserId: null,
+ twitterScreenName: null,
});
ctx.body = `Twitterの連携を解除しました :v:`;
// Publish i updated event
- publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+ publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
detail: true,
includeSecrets: true
}));
@@ -132,17 +139,21 @@ router.get('/tw/cb', async ctx => {
const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier);
- const user = await User.findOne({
- host: null,
- 'twitter.userId': result.userId
- }) as ILocalUser;
+ const link = await UserServiceLinkings.createQueryBuilder()
+ .where('twitter @> :twitter', {
+ twitter: {
+ userId: result.userId,
+ },
+ })
+ .andWhere('userHost IS NULL')
+ .getOne();
- if (user == null) {
+ if (link == null) {
ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
return;
}
- signin(ctx, user, true);
+ signin(ctx, await Users.findOne(link.userId) as ILocalUser, true);
} else {
const verifier = ctx.query.oauth_verifier;
@@ -161,24 +172,23 @@ router.get('/tw/cb', async ctx => {
const result = await twAuth.done(JSON.parse(twCtx), verifier);
- const user = await User.findOneAndUpdate({
+ const user = await Users.findOne({
host: null,
token: userToken
- }, {
- $set: {
- twitter: {
- accessToken: result.accessToken,
- accessTokenSecret: result.accessTokenSecret,
- userId: result.userId,
- screenName: result.screenName
- }
- }
+ });
+
+ await UserServiceLinkings.update({ userId: user.id }, {
+ twitter: true,
+ twitterAccessToken: result.accessToken,
+ twitterAccessTokenSecret: result.accessTokenSecret,
+ twitterUserId: result.userId,
+ twitterScreenName: result.screenName,
});
ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
// Publish i updated event
- publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+ publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
detail: true,
includeSecrets: true
}));
diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts
index bdbe4605cf..18fa651820 100644
--- a/src/server/api/stream/channel.ts
+++ b/src/server/api/stream/channel.ts
@@ -15,6 +15,14 @@ export default abstract class Channel {
return this.connection.user;
}
+ protected get following() {
+ return this.connection.following;
+ }
+
+ protected get muting() {
+ return this.connection.muting;
+ }
+
protected get subscriber() {
return this.connection.subscriber;
}
diff --git a/src/server/api/stream/channels/admin.ts b/src/server/api/stream/channels/admin.ts
index 6bcd1a7e0b..e2eba10f78 100644
--- a/src/server/api/stream/channels/admin.ts
+++ b/src/server/api/stream/channels/admin.ts
@@ -9,7 +9,7 @@ export default class extends Channel {
@autobind
public async init(params: any) {
// Subscribe admin stream
- this.subscriber.on(`adminStream:${this.user._id}`, data => {
+ this.subscriber.on(`adminStream:${this.user.id}`, data => {
this.send(data);
});
}
diff --git a/src/server/api/stream/channels/drive.ts b/src/server/api/stream/channels/drive.ts
index 391c4b5c32..671aad4366 100644
--- a/src/server/api/stream/channels/drive.ts
+++ b/src/server/api/stream/channels/drive.ts
@@ -9,7 +9,7 @@ export default class extends Channel {
@autobind
public async init(params: any) {
// Subscribe drive stream
- this.subscriber.on(`driveStream:${this.user._id}`, data => {
+ this.subscriber.on(`driveStream:${this.user.id}`, data => {
this.send(data);
});
}
diff --git a/src/server/api/stream/channels/games/reversi-game.ts b/src/server/api/stream/channels/games/reversi-game.ts
index 87df9e194c..158f108c4e 100644
--- a/src/server/api/stream/channels/games/reversi-game.ts
+++ b/src/server/api/stream/channels/games/reversi-game.ts
@@ -1,22 +1,22 @@
import autobind from 'autobind-decorator';
import * as CRC32 from 'crc-32';
-import * as mongo from 'mongodb';
-import ReversiGame, { pack } from '../../../../../models/games/reversi/game';
import { publishReversiGameStream } from '../../../../../services/stream';
import Reversi from '../../../../../games/reversi/core';
import * as maps from '../../../../../games/reversi/maps';
import Channel from '../../channel';
+import { ReversiGame } from '../../../../../models/entities/games/reversi/game';
+import { ReversiGames } from '../../../../../models';
export default class extends Channel {
public readonly chName = 'gamesReversiGame';
public static shouldShare = false;
public static requireCredential = false;
- private gameId: mongo.ObjectID;
+ private gameId: ReversiGame['id'];
@autobind
public async init(params: any) {
- this.gameId = new mongo.ObjectID(params.gameId as string);
+ this.gameId = params.gameId;
// Subscribe game stream
this.subscriber.on(`reversiGameStream:${this.gameId}`, data => {
@@ -29,7 +29,7 @@ export default class extends Channel {
switch (type) {
case 'accept': this.accept(true); break;
case 'cancelAccept': this.accept(false); break;
- case 'updateSettings': this.updateSettings(body.settings); break;
+ case 'updateSettings': this.updateSettings(body.key, body.value); break;
case 'initForm': this.initForm(body); break;
case 'updateForm': this.updateForm(body.id, body.value); break;
case 'message': this.message(body); break;
@@ -39,54 +39,55 @@ export default class extends Channel {
}
@autobind
- private async updateSettings(settings: any) {
- const game = await ReversiGame.findOne({ _id: this.gameId });
+ private async updateSettings(key: string, value: any) {
+ const game = await ReversiGames.findOne(this.gameId);
if (game.isStarted) return;
- if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
- if (game.user1Id.equals(this.user._id) && game.user1Accepted) return;
- if (game.user2Id.equals(this.user._id) && game.user2Accepted) return;
+ if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
+ if ((game.user1Id === this.user.id) && game.user1Accepted) return;
+ if ((game.user2Id === this.user.id) && game.user2Accepted) return;
- await ReversiGame.update({ _id: this.gameId }, {
- $set: {
- settings
- }
+ if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return;
+
+ await ReversiGames.update({ id: this.gameId }, {
+ [key]: value
});
- publishReversiGameStream(this.gameId, 'updateSettings', settings);
+ publishReversiGameStream(this.gameId, 'updateSettings', {
+ key: key,
+ value: value
+ });
}
@autobind
private async initForm(form: any) {
- const game = await ReversiGame.findOne({ _id: this.gameId });
+ const game = await ReversiGames.findOne(this.gameId);
if (game.isStarted) return;
- if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
+ if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
- const set = game.user1Id.equals(this.user._id) ? {
+ const set = game.user1Id === this.user.id ? {
form1: form
} : {
- form2: form
- };
+ form2: form
+ };
- await ReversiGame.update({ _id: this.gameId }, {
- $set: set
- });
+ await ReversiGames.update({ id: this.gameId }, set);
publishReversiGameStream(this.gameId, 'initForm', {
- userId: this.user._id,
+ userId: this.user.id,
form
});
}
@autobind
private async updateForm(id: string, value: any) {
- const game = await ReversiGame.findOne({ _id: this.gameId });
+ const game = await ReversiGames.findOne({ id: this.gameId });
if (game.isStarted) return;
- if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
+ if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
- const form = game.user1Id.equals(this.user._id) ? game.form2 : game.form1;
+ const form = game.user1Id === this.user.id ? game.form2 : game.form1;
const item = form.find((i: any) => i.id == id);
@@ -94,18 +95,16 @@ export default class extends Channel {
item.value = value;
- const set = game.user1Id.equals(this.user._id) ? {
+ const set = game.user1Id === this.user.id ? {
form2: form
} : {
form1: form
};
- await ReversiGame.update({ _id: this.gameId }, {
- $set: set
- });
+ await ReversiGames.update({ id: this.gameId }, set);
publishReversiGameStream(this.gameId, 'updateForm', {
- userId: this.user._id,
+ userId: this.user.id,
id,
value
});
@@ -115,24 +114,22 @@ export default class extends Channel {
private async message(message: any) {
message.id = Math.random();
publishReversiGameStream(this.gameId, 'message', {
- userId: this.user._id,
+ userId: this.user.id,
message
});
}
@autobind
private async accept(accept: boolean) {
- const game = await ReversiGame.findOne({ _id: this.gameId });
+ const game = await ReversiGames.findOne(this.gameId);
if (game.isStarted) return;
let bothAccepted = false;
- if (game.user1Id.equals(this.user._id)) {
- await ReversiGame.update({ _id: this.gameId }, {
- $set: {
- user1Accepted: accept
- }
+ if (game.user1Id === this.user.id) {
+ await ReversiGames.update({ id: this.gameId }, {
+ user1Accepted: accept
});
publishReversiGameStream(this.gameId, 'changeAccepts', {
@@ -141,11 +138,9 @@ export default class extends Channel {
});
if (accept && game.user2Accepted) bothAccepted = true;
- } else if (game.user2Id.equals(this.user._id)) {
- await ReversiGame.update({ _id: this.gameId }, {
- $set: {
- user2Accepted: accept
- }
+ } else if (game.user2Id === this.user.id) {
+ await ReversiGames.update({ id: this.gameId }, {
+ user2Accepted: accept
});
publishReversiGameStream(this.gameId, 'changeAccepts', {
@@ -161,15 +156,15 @@ export default class extends Channel {
if (bothAccepted) {
// 3秒後、まだacceptされていたらゲーム開始
setTimeout(async () => {
- const freshGame = await ReversiGame.findOne({ _id: this.gameId });
+ const freshGame = await ReversiGames.findOne(this.gameId);
if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
let bw: number;
- if (freshGame.settings.bw == 'random') {
+ if (freshGame.bw == 'random') {
bw = Math.random() > 0.5 ? 1 : 2;
} else {
- bw = freshGame.settings.bw as number;
+ bw = parseInt(freshGame.bw, 10);
}
function getRandomMap() {
@@ -178,22 +173,20 @@ export default class extends Channel {
return Object.values(maps)[rnd].data;
}
- const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap();
+ const map = freshGame.map != null ? freshGame.map : getRandomMap();
- await ReversiGame.update({ _id: this.gameId }, {
- $set: {
- startedAt: new Date(),
- isStarted: true,
- black: bw,
- 'settings.map': map
- }
+ await ReversiGames.update({ id: this.gameId }, {
+ startedAt: new Date(),
+ isStarted: true,
+ black: bw,
+ map: map
});
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
const o = new Reversi(map, {
- isLlotheo: freshGame.settings.isLlotheo,
- canPutEverywhere: freshGame.settings.canPutEverywhere,
- loopedBoard: freshGame.settings.loopedBoard
+ isLlotheo: freshGame.isLlotheo,
+ canPutEverywhere: freshGame.canPutEverywhere,
+ loopedBoard: freshGame.loopedBoard
});
if (o.isEnded) {
@@ -206,23 +199,22 @@ export default class extends Channel {
winner = null;
}
- await ReversiGame.update({
- _id: this.gameId
+ await ReversiGames.update({
+ id: this.gameId
}, {
- $set: {
- isEnded: true,
- winnerId: winner
- }
- });
+ isEnded: true,
+ winnerId: winner
+ });
publishReversiGameStream(this.gameId, 'ended', {
winnerId: winner,
- game: await pack(this.gameId, this.user)
+ game: await ReversiGames.pack(this.gameId, this.user)
});
}
//#endregion
- publishReversiGameStream(this.gameId, 'started', await pack(this.gameId, this.user));
+ publishReversiGameStream(this.gameId, 'started',
+ await ReversiGames.pack(this.gameId, this.user));
}, 3000);
}
}
@@ -230,16 +222,16 @@ export default class extends Channel {
// 石を打つ
@autobind
private async set(pos: number) {
- const game = await ReversiGame.findOne({ _id: this.gameId });
+ const game = await ReversiGames.findOne(this.gameId);
if (!game.isStarted) return;
if (game.isEnded) return;
- if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
+ if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
- const o = new Reversi(game.settings.map, {
- isLlotheo: game.settings.isLlotheo,
- canPutEverywhere: game.settings.canPutEverywhere,
- loopedBoard: game.settings.loopedBoard
+ const o = new Reversi(game.map, {
+ isLlotheo: game.isLlotheo,
+ canPutEverywhere: game.canPutEverywhere,
+ loopedBoard: game.loopedBoard
});
for (const log of game.logs) {
@@ -247,7 +239,7 @@ export default class extends Channel {
}
const myColor =
- (game.user1Id.equals(this.user._id) && game.black == 1) || (game.user2Id.equals(this.user._id) && game.black == 2)
+ ((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2)
? true
: false;
@@ -271,20 +263,18 @@ export default class extends Channel {
pos
};
- const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString());
+ const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString();
- await ReversiGame.update({
- _id: this.gameId
+ game.logs.push(log);
+
+ await ReversiGames.update({
+ id: this.gameId
}, {
- $set: {
- crc32,
- isEnded: o.isEnded,
- winnerId: winner
- },
- $push: {
- logs: log
- }
- });
+ crc32,
+ isEnded: o.isEnded,
+ winnerId: winner,
+ logs: game.logs
+ });
publishReversiGameStream(this.gameId, 'set', Object.assign(log, {
next: o.turn
@@ -293,14 +283,14 @@ export default class extends Channel {
if (o.isEnded) {
publishReversiGameStream(this.gameId, 'ended', {
winnerId: winner,
- game: await pack(this.gameId, this.user)
+ game: await ReversiGames.pack(this.gameId, this.user)
});
}
}
@autobind
private async check(crc32: string) {
- const game = await ReversiGame.findOne({ _id: this.gameId });
+ const game = await ReversiGames.findOne(this.gameId);
if (!game.isStarted) return;
@@ -308,7 +298,7 @@ export default class extends Channel {
if (game.crc32 == null) return;
if (crc32 !== game.crc32) {
- this.send('rescue', await pack(game, this.user));
+ this.send('rescue', await ReversiGames.pack(game, this.user));
}
}
}
diff --git a/src/server/api/stream/channels/games/reversi.ts b/src/server/api/stream/channels/games/reversi.ts
index 1b1ad187a3..0498e5e017 100644
--- a/src/server/api/stream/channels/games/reversi.ts
+++ b/src/server/api/stream/channels/games/reversi.ts
@@ -1,8 +1,7 @@
import autobind from 'autobind-decorator';
-import * as mongo from 'mongodb';
-import Matching, { pack } from '../../../../../models/games/reversi/matching';
import { publishMainStream } from '../../../../../services/stream';
import Channel from '../../channel';
+import { ReversiMatchings } from '../../../../../models';
export default class extends Channel {
public readonly chName = 'gamesReversi';
@@ -12,7 +11,7 @@ export default class extends Channel {
@autobind
public async init(params: any) {
// Subscribe reversi stream
- this.subscriber.on(`reversiStream:${this.user._id}`, data => {
+ this.subscriber.on(`reversiStream:${this.user.id}`, data => {
this.send(data);
});
}
@@ -22,12 +21,12 @@ export default class extends Channel {
switch (type) {
case 'ping':
if (body.id == null) return;
- const matching = await Matching.findOne({
- parentId: this.user._id,
- childId: new mongo.ObjectID(body.id)
+ const matching = await ReversiMatchings.findOne({
+ parentId: this.user.id,
+ childId: body.id
});
if (matching == null) return;
- publishMainStream(matching.childId, 'reversiInvited', await pack(matching, matching.childId));
+ publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, matching.childId));
break;
}
}
diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts
index b3689d47f5..bfb7697ba7 100644
--- a/src/server/api/stream/channels/global-timeline.ts
+++ b/src/server/api/stream/channels/global-timeline.ts
@@ -1,17 +1,14 @@
import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
-import { pack } from '../../../../models/note';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import Channel from '../channel';
import fetchMeta from '../../../../misc/fetch-meta';
+import { Notes } from '../../../../models';
export default class extends Channel {
public readonly chName = 'globalTimeline';
public static shouldShare = true;
public static requireCredential = false;
- private mutedUserIds: string[] = [];
-
@autobind
public async init(params: any) {
const meta = await fetchMeta();
@@ -20,29 +17,26 @@ export default class extends Channel {
}
// Subscribe events
- this.subscriber.on('globalTimeline', this.onNote);
-
- const mute = await Mute.find({ muterId: this.user._id });
- this.mutedUserIds = mute.map(m => m.muteeId.toString());
+ this.subscriber.on('notesStream', this.onNote);
}
@autobind
private async onNote(note: any) {
// リプライなら再pack
if (note.replyId != null) {
- note.reply = await pack(note.replyId, this.user, {
+ note.reply = await Notes.pack(note.replyId, this.user, {
detail: true
});
}
// Renoteなら再pack
if (note.renoteId != null) {
- note.renote = await pack(note.renoteId, this.user, {
+ note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true
});
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (shouldMuteThisNote(note, this.mutedUserIds)) return;
+ if (shouldMuteThisNote(note, this.muting)) return;
this.send('note', note);
}
@@ -50,6 +44,6 @@ export default class extends Channel {
@autobind
public dispose() {
// Unsubscribe events
- this.subscriber.off('globalTimeline', this.onNote);
+ this.subscriber.off('notesStream', this.onNote);
}
}
diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts
index 586ce02f06..36c56c7ab6 100644
--- a/src/server/api/stream/channels/hashtag.ts
+++ b/src/server/api/stream/channels/hashtag.ts
@@ -1,40 +1,46 @@
import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
-import { pack } from '../../../../models/note';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import Channel from '../channel';
+import { Notes } from '../../../../models';
export default class extends Channel {
public readonly chName = 'hashtag';
public static shouldShare = false;
public static requireCredential = false;
+ private q: string[][];
@autobind
public async init(params: any) {
- const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null;
- const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
+ this.q = params.q;
- const q: string[][] = params.q;
-
- if (q == null) return;
+ if (this.q == null) return;
// Subscribe stream
- this.subscriber.on('hashtag', async note => {
- const noteTags = note.tags.map((t: string) => t.toLowerCase());
- const matched = q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase())));
- if (!matched) return;
+ this.subscriber.on('notesStream', this.onNote);
+ }
+
+ @autobind
+ private async onNote(note: any) {
+ const noteTags = note.tags.map((t: string) => t.toLowerCase());
+ const matched = this.q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase())));
+ if (!matched) return;
- // Renoteなら再pack
- if (note.renoteId != null) {
- note.renote = await pack(note.renoteId, this.user, {
- detail: true
- });
- }
+ // Renoteなら再pack
+ if (note.renoteId != null) {
+ note.renote = await Notes.pack(note.renoteId, this.user, {
+ detail: true
+ });
+ }
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (shouldMuteThisNote(note, mutedUserIds)) return;
+ // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+ if (shouldMuteThisNote(note, this.muting)) return;
- this.send('note', note);
- });
+ this.send('note', note);
+ }
+
+ @autobind
+ public dispose() {
+ // Unsubscribe events
+ this.subscriber.off('notesStream', this.onNote);
}
}
diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts
index 3c0b238720..2cece0947f 100644
--- a/src/server/api/stream/channels/home-timeline.ts
+++ b/src/server/api/stream/channels/home-timeline.ts
@@ -1,42 +1,49 @@
import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
-import { pack } from '../../../../models/note';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import Channel from '../channel';
+import { Notes } from '../../../../models';
export default class extends Channel {
public readonly chName = 'homeTimeline';
public static shouldShare = true;
public static requireCredential = true;
- private mutedUserIds: string[] = [];
-
@autobind
public async init(params: any) {
// Subscribe events
- this.subscriber.on(`homeTimeline:${this.user._id}`, this.onNote);
-
- const mute = await Mute.find({ muterId: this.user._id });
- this.mutedUserIds = mute.map(m => m.muteeId.toString());
+ this.subscriber.on('notesStream', this.onNote);
}
@autobind
private async onNote(note: any) {
- // リプライなら再pack
- if (note.replyId != null) {
- note.reply = await pack(note.replyId, this.user, {
- detail: true
- });
- }
- // Renoteなら再pack
- if (note.renoteId != null) {
- note.renote = await pack(note.renoteId, this.user, {
+ // その投稿のユーザーをフォローしていなかったら弾く
+ if (this.user.id !== note.userId && !this.following.includes(note.userId)) return;
+
+ if (['followers', 'specified'].includes(note.visibility)) {
+ note = await Notes.pack(note.id, this.user, {
detail: true
});
+
+ if (note.isHidden) {
+ return;
+ }
+ } else {
+ // リプライなら再pack
+ if (note.replyId != null) {
+ note.reply = await Notes.pack(note.replyId, this.user, {
+ detail: true
+ });
+ }
+ // Renoteなら再pack
+ if (note.renoteId != null) {
+ note.renote = await Notes.pack(note.renoteId, this.user, {
+ detail: true
+ });
+ }
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (shouldMuteThisNote(note, this.mutedUserIds)) return;
+ if (shouldMuteThisNote(note, this.muting)) return;
this.send('note', note);
}
@@ -44,6 +51,6 @@ export default class extends Channel {
@autobind
public dispose() {
// Unsubscribe events
- this.subscriber.off(`homeTimeline:${this.user._id}`, this.onNote);
+ this.subscriber.off('notesStream', this.onNote);
}
}
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
deleted file mode 100644
index 35ef17b56b..0000000000
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
-import { pack } from '../../../../models/note';
-import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
-import Channel from '../channel';
-import fetchMeta from '../../../../misc/fetch-meta';
-
-export default class extends Channel {
- public readonly chName = 'hybridTimeline';
- public static shouldShare = true;
- public static requireCredential = true;
-
- private mutedUserIds: string[] = [];
-
- @autobind
- public async init(params: any) {
- const meta = await fetchMeta();
- if (meta.disableLocalTimeline && !this.user.isAdmin && !this.user.isModerator) return;
-
- // Subscribe events
- this.subscriber.on('hybridTimeline', this.onNewNote);
- this.subscriber.on(`hybridTimeline:${this.user._id}`, this.onNewNote);
-
- const mute = await Mute.find({ muterId: this.user._id });
- this.mutedUserIds = mute.map(m => m.muteeId.toString());
- }
-
- @autobind
- private async onNewNote(note: any) {
- // リプライなら再pack
- if (note.replyId != null) {
- note.reply = await pack(note.replyId, this.user, {
- detail: true
- });
- }
- // Renoteなら再pack
- if (note.renoteId != null) {
- note.renote = await pack(note.renoteId, this.user, {
- detail: true
- });
- }
-
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (shouldMuteThisNote(note, this.mutedUserIds)) return;
-
- this.send('note', note);
- }
-
- @autobind
- public dispose() {
- // Unsubscribe events
- this.subscriber.off('hybridTimeline', this.onNewNote);
- this.subscriber.off(`hybridTimeline:${this.user._id}`, this.onNewNote);
- }
-}
diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts
index 4527fb1e46..199ab0a809 100644
--- a/src/server/api/stream/channels/index.ts
+++ b/src/server/api/stream/channels/index.ts
@@ -1,7 +1,7 @@
import main from './main';
import homeTimeline from './home-timeline';
import localTimeline from './local-timeline';
-import hybridTimeline from './hybrid-timeline';
+import socialTimeline from './social-timeline';
import globalTimeline from './global-timeline';
import notesStats from './notes-stats';
import serverStats from './server-stats';
@@ -20,7 +20,7 @@ export default {
main,
homeTimeline,
localTimeline,
- hybridTimeline,
+ socialTimeline,
globalTimeline,
notesStats,
serverStats,
diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts
index 3402023192..4aec2d66b4 100644
--- a/src/server/api/stream/channels/local-timeline.ts
+++ b/src/server/api/stream/channels/local-timeline.ts
@@ -1,17 +1,14 @@
import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
-import { pack } from '../../../../models/note';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import Channel from '../channel';
import fetchMeta from '../../../../misc/fetch-meta';
+import { Notes } from '../../../../models';
export default class extends Channel {
public readonly chName = 'localTimeline';
public static shouldShare = true;
public static requireCredential = false;
- private mutedUserIds: string[] = [];
-
@autobind
public async init(params: any) {
const meta = await fetchMeta();
@@ -20,29 +17,39 @@ export default class extends Channel {
}
// Subscribe events
- this.subscriber.on('localTimeline', this.onNote);
-
- const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null;
- this.mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
+ this.subscriber.on('notesStream', this.onNote);
}
@autobind
private async onNote(note: any) {
- // リプライなら再pack
- if (note.replyId != null) {
- note.reply = await pack(note.replyId, this.user, {
- detail: true
- });
- }
- // Renoteなら再pack
- if (note.renoteId != null) {
- note.renote = await pack(note.renoteId, this.user, {
+ if (note.user.host !== null) return;
+ if (note.visibility === 'home') return;
+
+ if (['followers', 'specified'].includes(note.visibility)) {
+ note = await Notes.pack(note.id, this.user, {
detail: true
});
+
+ if (note.isHidden) {
+ return;
+ }
+ } else {
+ // リプライなら再pack
+ if (note.replyId != null) {
+ note.reply = await Notes.pack(note.replyId, this.user, {
+ detail: true
+ });
+ }
+ // Renoteなら再pack
+ if (note.renoteId != null) {
+ note.renote = await Notes.pack(note.renoteId, this.user, {
+ detail: true
+ });
+ }
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (shouldMuteThisNote(note, this.mutedUserIds)) return;
+ if (shouldMuteThisNote(note, this.muting)) return;
this.send('note', note);
}
@@ -50,6 +57,6 @@ export default class extends Channel {
@autobind
public dispose() {
// Unsubscribe events
- this.subscriber.off('localTimeline', this.onNote);
+ this.subscriber.off('notesStream', this.onNote);
}
}
diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts
index 175d914fa5..0d9bf3149d 100644
--- a/src/server/api/stream/channels/main.ts
+++ b/src/server/api/stream/channels/main.ts
@@ -1,6 +1,6 @@
import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
import Channel from '../channel';
+import { Mutings } from '../../../../models';
export default class extends Channel {
public readonly chName = 'main';
@@ -9,16 +9,15 @@ export default class extends Channel {
@autobind
public async init(params: any) {
- const mute = await Mute.find({ muterId: this.user._id });
- const mutedUserIds = mute.map(m => m.muteeId.toString());
+ const mute = await Mutings.find({ muterId: this.user.id });
// Subscribe main stream channel
- this.subscriber.on(`mainStream:${this.user._id}`, async data => {
+ this.subscriber.on(`mainStream:${this.user.id}`, async data => {
const { type, body } = data;
switch (type) {
case 'notification': {
- if (mutedUserIds.includes(body.userId)) return;
+ if (mute.map(m => m.muteeId).includes(body.userId)) return;
if (body.note && body.note.isHidden) return;
break;
}
diff --git a/src/server/api/stream/channels/messaging-index.ts b/src/server/api/stream/channels/messaging-index.ts
index 148ff7f935..648badc1dc 100644
--- a/src/server/api/stream/channels/messaging-index.ts
+++ b/src/server/api/stream/channels/messaging-index.ts
@@ -9,7 +9,7 @@ export default class extends Channel {
@autobind
public async init(params: any) {
// Subscribe messaging index stream
- this.subscriber.on(`messagingIndexStream:${this.user._id}`, data => {
+ this.subscriber.on(`messagingIndexStream:${this.user.id}`, data => {
this.send(data);
});
}
diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts
index 0d81b4e45c..b81fbb9d4c 100644
--- a/src/server/api/stream/channels/messaging.ts
+++ b/src/server/api/stream/channels/messaging.ts
@@ -14,7 +14,7 @@ export default class extends Channel {
this.otherpartyId = params.otherparty as string;
// Subscribe messaging stream
- this.subscriber.on(`messagingStream:${this.user._id}-${this.otherpartyId}`, data => {
+ this.subscriber.on(`messagingStream:${this.user.id}-${this.otherpartyId}`, data => {
this.send(data);
});
}
@@ -23,7 +23,7 @@ export default class extends Channel {
public onMessage(type: string, body: any) {
switch (type) {
case 'read':
- read(this.user._id, this.otherpartyId, body.id);
+ read(this.user.id, this.otherpartyId, body.id);
break;
}
}
diff --git a/src/server/api/stream/channels/social-timeline.ts b/src/server/api/stream/channels/social-timeline.ts
new file mode 100644
index 0000000000..1d76eed297
--- /dev/null
+++ b/src/server/api/stream/channels/social-timeline.ts
@@ -0,0 +1,64 @@
+import autobind from 'autobind-decorator';
+import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import Channel from '../channel';
+import fetchMeta from '../../../../misc/fetch-meta';
+import { Notes } from '../../../../models';
+
+export default class extends Channel {
+ public readonly chName = 'socialTimeline';
+ public static shouldShare = true;
+ public static requireCredential = true;
+
+ @autobind
+ public async init(params: any) {
+ const meta = await fetchMeta();
+ if (meta.disableLocalTimeline && !this.user.isAdmin && !this.user.isModerator) return;
+
+ // Subscribe events
+ this.subscriber.on('notesStream', this.onNote);
+ }
+
+ @autobind
+ private async onNote(note: any) {
+ // 自分自身の投稿 または その投稿のユーザーをフォローしている または ローカルの投稿 の場合だけ
+ if (!(
+ this.user.id === note.userId ||
+ this.following.includes(note.userId) ||
+ note.user.host === null
+ )) return;
+
+ if (['followers', 'specified'].includes(note.visibility)) {
+ note = await Notes.pack(note.id, this.user, {
+ detail: true
+ });
+
+ if (note.isHidden) {
+ return;
+ }
+ } else {
+ // リプライなら再pack
+ if (note.replyId != null) {
+ note.reply = await Notes.pack(note.replyId, this.user, {
+ detail: true
+ });
+ }
+ // Renoteなら再pack
+ if (note.renoteId != null) {
+ note.renote = await Notes.pack(note.renoteId, this.user, {
+ detail: true
+ });
+ }
+ }
+
+ // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+ if (shouldMuteThisNote(note, this.muting)) return;
+
+ this.send('note', note);
+ }
+
+ @autobind
+ public dispose() {
+ // Unsubscribe events
+ this.subscriber.off('notesStream', this.onNote);
+ }
+}
diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts
index 5debf41770..f5434b8f08 100644
--- a/src/server/api/stream/channels/user-list.ts
+++ b/src/server/api/stream/channels/user-list.ts
@@ -1,23 +1,81 @@
import autobind from 'autobind-decorator';
import Channel from '../channel';
-import { pack } from '../../../../models/note';
+import { Notes, UserListJoinings } from '../../../../models';
+import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import { User } from '../../../../models/entities/user';
export default class extends Channel {
public readonly chName = 'userList';
public static shouldShare = false;
public static requireCredential = false;
+ private listId: string;
+ public listUsers: User['id'][] = [];
+ private listUsersClock: NodeJS.Timer;
@autobind
public async init(params: any) {
- const listId = params.listId as string;
+ this.listId = params.listId as string;
// Subscribe stream
- this.subscriber.on(`userListStream:${listId}`, async data => {
- // 再パック
- if (data.type == 'note') data.body = await pack(data.body.id, this.user, {
+ this.subscriber.on(`userListStream:${this.listId}`, this.send);
+
+ this.subscriber.on('notesStream', this.onNote);
+
+ this.updateListUsers();
+ this.listUsersClock = setInterval(this.updateListUsers, 5000);
+ }
+
+ @autobind
+ private async updateListUsers() {
+ const users = await UserListJoinings.find({
+ where: {
+ userListId: this.listId,
+ },
+ select: ['userId']
+ });
+
+ this.listUsers = users.map(x => x.userId);
+ }
+
+ @autobind
+ private async onNote(note: any) {
+ if (!this.listUsers.includes(note.userId)) return;
+
+ if (['followers', 'specified'].includes(note.visibility)) {
+ note = await Notes.pack(note.id, this.user, {
detail: true
});
- this.send(data);
- });
+
+ if (note.isHidden) {
+ return;
+ }
+ } else {
+ // リプライなら再pack
+ if (note.replyId != null) {
+ note.reply = await Notes.pack(note.replyId, this.user, {
+ detail: true
+ });
+ }
+ // Renoteなら再pack
+ if (note.renoteId != null) {
+ note.renote = await Notes.pack(note.renoteId, this.user, {
+ detail: true
+ });
+ }
+ }
+
+ // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+ if (shouldMuteThisNote(note, this.muting)) return;
+
+ this.send('note', note);
+ }
+
+ @autobind
+ public dispose() {
+ // Unsubscribe events
+ this.subscriber.off(`userListStream:${this.listId}`, this.send);
+ this.subscriber.off('notesStream', this.onNote);
+
+ clearInterval(this.listUsersClock);
}
}
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index 22f7646cb9..abbd91ec81 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -1,33 +1,35 @@
import autobind from 'autobind-decorator';
import * as websocket from 'websocket';
-
-import User, { IUser } from '../../../models/user';
-import readNotification from '../common/read-notification';
+import { readNotification } from '../common/read-notification';
import call from '../call';
-import { IApp } from '../../../models/app';
import readNote from '../../../services/note/read';
-
import Channel from './channel';
import channels from './channels';
import { EventEmitter } from 'events';
+import { User } from '../../../models/entities/user';
+import { App } from '../../../models/entities/app';
+import { Users, Followings, Mutings } from '../../../models';
/**
* Main stream connection
*/
export default class Connection {
- public user?: IUser;
- public app: IApp;
+ public user?: User;
+ public following: User['id'][] = [];
+ public muting: User['id'][] = [];
+ public app: App;
private wsConnection: websocket.connection;
public subscriber: EventEmitter;
private channels: Channel[] = [];
private subscribingNotes: any = {};
- public sendMessageToWsOverride: any = null; // 後方互換性のため
+ private followingClock: NodeJS.Timer;
+ private mutingClock: NodeJS.Timer;
constructor(
wsConnection: websocket.connection,
subscriber: EventEmitter,
- user: IUser,
- app: IApp
+ user: User,
+ app: App
) {
this.wsConnection = wsConnection;
this.user = user;
@@ -35,6 +37,14 @@ export default class Connection {
this.subscriber = subscriber;
this.wsConnection.on('message', this.onWsConnectionMessage);
+
+ if (this.user) {
+ this.updateFollowing();
+ this.followingClock = setInterval(this.updateFollowing, 5000);
+
+ this.updateMuting();
+ this.mutingClock = setInterval(this.updateMuting, 5000);
+ }
}
/**
@@ -64,7 +74,7 @@ export default class Connection {
@autobind
private async onApiRequest(payload: any) {
// 新鮮なデータを利用するためにユーザーをフェッチ
- const user = this.user ? await User.findOne({ _id: this.user._id }) : null;
+ const user = this.user ? await Users.findOne(this.user.id) : null;
const endpoint = payload.endpoint || payload.ep; // alias
@@ -79,7 +89,7 @@ export default class Connection {
@autobind
private onReadNotification(payload: any) {
if (!payload.id) return;
- readNotification(this.user._id, payload.id);
+ readNotification(this.user.id, [payload.id]);
}
/**
@@ -100,7 +110,7 @@ export default class Connection {
}
if (payload.read) {
- readNote(this.user._id, payload.id);
+ readNote(this.user.id, payload.id);
}
}
@@ -150,7 +160,6 @@ export default class Connection {
*/
@autobind
public sendMessageToWs(type: string, payload: any) {
- if (this.sendMessageToWsOverride) return this.sendMessageToWsOverride(type, payload); // 後方互換性のため
this.wsConnection.send(JSON.stringify({
type: type,
body: payload
@@ -208,6 +217,30 @@ export default class Connection {
}
}
+ @autobind
+ private async updateFollowing() {
+ const followings = await Followings.find({
+ where: {
+ followerId: this.user.id
+ },
+ select: ['followeeId']
+ });
+
+ this.following = followings.map(x => x.followeeId);
+ }
+
+ @autobind
+ private async updateMuting() {
+ const mutings = await Mutings.find({
+ where: {
+ muterId: this.user.id
+ },
+ select: ['muteeId']
+ });
+
+ this.muting = mutings.map(x => x.muteeId);
+ }
+
/**
* ストリームが切れたとき
*/
@@ -216,5 +249,8 @@ export default class Connection {
for (const c of this.channels.filter(c => c.dispose)) {
c.dispose();
}
+
+ if (this.followingClock) clearInterval(this.followingClock);
+ if (this.mutingClock) clearInterval(this.mutingClock);
}
}
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index f8f3c0ff4a..ab66f2b6d9 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -48,33 +48,6 @@ module.exports = (server: http.Server) => {
const main = new MainStreamConnection(connection, ev, user, app);
- // 後方互換性のため
- if (request.resourceURL.pathname !== '/streaming') {
- main.sendMessageToWsOverride = (type: string, payload: any) => {
- if (type == 'channel') {
- type = payload.type;
- payload = payload.body;
- }
- if (type.startsWith('api:')) {
- type = type.replace('api:', 'api-res:');
- }
- connection.send(JSON.stringify({
- type: type,
- body: payload
- }));
- };
-
- main.connectChannel(Math.random().toString().substr(2, 8), null,
- request.resourceURL.pathname === '/' ? 'homeTimeline' :
- request.resourceURL.pathname === '/local-timeline' ? 'localTimeline' :
- request.resourceURL.pathname === '/hybrid-timeline' ? 'hybridTimeline' :
- request.resourceURL.pathname === '/global-timeline' ? 'globalTimeline' : null);
-
- if (request.resourceURL.pathname === '/') {
- main.connectChannel(Math.random().toString().substr(2, 8), null, 'main');
- }
- }
-
connection.once('close', () => {
ev.removeAllListeners();
main.dispose();
diff --git a/src/server/file/index.ts b/src/server/file/index.ts
index 973528da33..e3487a2636 100644
--- a/src/server/file/index.ts
+++ b/src/server/file/index.ts
@@ -33,8 +33,8 @@ router.get('/app-default.jpg', ctx => {
ctx.body = file;
});
-router.get('/:id', sendDriveFile);
-router.get('/:id/*', sendDriveFile);
+router.get('/:key', sendDriveFile);
+router.get('/:key/*', sendDriveFile);
// Register router
app.use(router.routes());
diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts
index e0208f3fab..f9b067b79c 100644
--- a/src/server/file/send-drive-file.ts
+++ b/src/server/file/send-drive-file.ts
@@ -1,12 +1,10 @@
import * as Koa from 'koa';
import * as send from 'koa-send';
-import * as mongodb from 'mongodb';
import * as rename from 'rename';
-import DriveFile, { getDriveFileBucket } from '../../models/drive-file';
-import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
-import DriveFileWebpublic, { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
import { serverLogger } from '..';
import { contentDisposition } from '../../misc/content-disposition';
+import { DriveFiles } from '../../models';
+import { InternalStorage } from '../../services/drive/internal-storage';
const assets = `${__dirname}/../../server/file/assets/`;
@@ -16,16 +14,14 @@ const commonReadableHandlerGenerator = (ctx: Koa.BaseContext) => (e: Error): voi
};
export default async function(ctx: Koa.BaseContext) {
- // Validate id
- if (!mongodb.ObjectID.isValid(ctx.params.id)) {
- ctx.throw(400, 'incorrect id');
- return;
- }
-
- const fileId = new mongodb.ObjectID(ctx.params.id);
+ const key = ctx.params.key;
// Fetch drive file
- const file = await DriveFile.findOne({ _id: fileId });
+ const file = await DriveFiles.createQueryBuilder('file')
+ .where('file.accessKey = :accessKey', { accessKey: key })
+ .orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
+ .orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
+ .getOne();
if (file == null) {
ctx.status = 404;
@@ -33,69 +29,30 @@ export default async function(ctx: Koa.BaseContext) {
return;
}
- if (file.metadata.deletedAt) {
- ctx.status = 410;
- await send(ctx as any, '/tombstone.png', { root: assets });
- return;
- }
-
- if (file.metadata.withoutChunks) {
+ if (!file.storedInternal) {
ctx.status = 204;
return;
}
- const sendRaw = async () => {
- if (file.metadata && file.metadata.accessKey && file.metadata.accessKey != ctx.query['original']) {
- ctx.status = 403;
- return;
- }
+ const isThumbnail = file.thumbnailAccessKey === key;
+ const isWebpublic = file.webpublicAccessKey === key;
- const bucket = await getDriveFileBucket();
- const readable = bucket.openDownloadStream(fileId);
- readable.on('error', commonReadableHandlerGenerator(ctx));
- ctx.set('Content-Type', file.contentType);
- ctx.body = readable;
- };
-
- if ('thumbnail' in ctx.query) {
- const thumb = await DriveFileThumbnail.findOne({
- 'metadata.originalId': fileId
- });
-
- if (thumb != null) {
- ctx.set('Content-Type', 'image/jpeg');
- ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.filename, { suffix: '-thumb', extname: '.jpeg' })}`));
- const bucket = await getDriveFileThumbnailBucket();
- ctx.body = bucket.openDownloadStream(thumb._id);
- } else {
- if (file.contentType.startsWith('image/')) {
- ctx.set('Content-Disposition', contentDisposition('inline', `${file.filename}`));
- await sendRaw();
- } else {
- ctx.status = 404;
- await send(ctx as any, '/dummy.png', { root: assets });
- }
- }
- } else if ('web' in ctx.query) {
- const web = await DriveFileWebpublic.findOne({
- 'metadata.originalId': fileId
- });
-
- if (web != null) {
- ctx.set('Content-Type', file.contentType);
- ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.filename, { suffix: '-web' })}`));
-
- const bucket = await getDriveFileWebpublicBucket();
- ctx.body = bucket.openDownloadStream(web._id);
- } else {
- ctx.set('Content-Disposition', contentDisposition('inline', `${file.filename}`));
- await sendRaw();
- }
+ if (isThumbnail) {
+ ctx.set('Content-Type', 'image/jpeg');
+ ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-thumb', extname: '.jpeg' })}`));
+ ctx.body = InternalStorage.read(key);
+ } else if (isWebpublic) {
+ ctx.set('Content-Type', file.type);
+ ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-web' })}`));
+ ctx.body = InternalStorage.read(key);
} else {
if ('download' in ctx.query) {
- ctx.set('Content-Disposition', contentDisposition('attachment', `${file.filename}`));
+ ctx.set('Content-Disposition', contentDisposition('attachment', `${file.name}`));
}
- await sendRaw();
+ const readable = InternalStorage.read(file.accessKey);
+ readable.on('error', commonReadableHandlerGenerator(ctx));
+ ctx.set('Content-Type', file.type);
+ ctx.body = readable;
}
}
diff --git a/src/server/index.ts b/src/server/index.ts
index 7c51923f9e..563117773e 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -19,12 +19,12 @@ import activityPub from './activitypub';
import nodeinfo from './nodeinfo';
import wellKnown from './well-known';
import config from '../config';
-import networkChart from '../services/chart/network';
import apiServer from './api';
import { sum } from '../prelude/array';
-import User from '../models/user';
import Logger from '../services/logger';
import { program } from '../argv';
+import { Users } from '../models';
+import { networkChart } from '../services/chart';
export const serverLogger = new Logger('server', 'gray', false);
@@ -73,17 +73,17 @@ router.use(nodeinfo.routes());
router.use(wellKnown.routes());
router.get('/verify-email/:code', async ctx => {
- const user = await User.findOne({ emailVerifyCode: ctx.params.code });
+ const user = await Users.findOne({
+ emailVerifyCode: ctx.params.code
+ });
if (user != null) {
ctx.body = 'Verify succeeded!';
ctx.status = 200;
- User.update({ _id: user._id }, {
- $set: {
- emailVerified: true,
- emailVerifyCode: null
- }
+ Users.update(user.id, {
+ emailVerified: true,
+ emailVerifyCode: null
});
} else {
ctx.status = 404;
diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts
index a783eea90b..686412383e 100644
--- a/src/server/nodeinfo.ts
+++ b/src/server/nodeinfo.ts
@@ -20,7 +20,24 @@ export const links = [/* (awaiting release) {
const nodeinfo2 = async () => {
const [
- { name, description, maintainer, langs, announcements, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker },
+ {
+ name,
+ description,
+ maintainerName,
+ maintainerEmail,
+ langs,
+ announcements,
+ disableRegistration,
+ disableLocalTimeline,
+ disableGlobalTimeline,
+ enableRecaptcha,
+ maxNoteTextLength,
+ enableTwitterIntegration,
+ enableGithubIntegration,
+ enableDiscordIntegration,
+ enableEmail,
+ enableServiceWorker
+ },
// total,
// activeHalfyear,
// activeMonth,
@@ -52,7 +69,26 @@ const nodeinfo2 = async () => {
// localPosts,
// localComments
},
- metadata: { name, description, maintainer, langs, announcements, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker }
+ metadata: {
+ name,
+ description,
+ maintainer: {
+ name: maintainerName,
+ email: maintainerEmail
+ },
+ langs,
+ announcements,
+ disableRegistration,
+ disableLocalTimeline,
+ disableGlobalTimeline,
+ enableRecaptcha,
+ maxNoteTextLength,
+ enableTwitterIntegration,
+ enableGithubIntegration,
+ enableDiscordIntegration,
+ enableEmail,
+ enableServiceWorker
+ }
};
};
diff --git a/src/server/web/feed.ts b/src/server/web/feed.ts
index 09ac10c576..4b4ea87973 100644
--- a/src/server/web/feed.ts
+++ b/src/server/web/feed.ts
@@ -1,25 +1,23 @@
import { Feed } from 'feed';
import config from '../../config';
-import Note from '../../models/note';
-import { IUser } from '../../models/user';
-import { getOriginalUrl } from '../../misc/get-drive-file-url';
+import { User } from '../../models/entities/user';
+import { Notes, DriveFiles } from '../../models';
+import { In } from 'typeorm';
-export default async function(user: IUser) {
+export default async function(user: User) {
const author: Author = {
link: `${config.url}/@${user.username}`,
name: user.name || user.username
};
- const notes = await Note.find({
- userId: user._id,
- renoteId: null,
- $or: [
- { visibility: 'public' },
- { visibility: 'home' }
- ]
- }, {
- sort: { createdAt: -1 },
- limit: 20
+ const notes = await Notes.find({
+ where: {
+ userId: user.id,
+ renoteId: null,
+ visibility: In(['public', 'home'])
+ },
+ order: { createdAt: -1 },
+ take: 20
});
const feed = new Feed({
@@ -38,15 +36,18 @@ export default async function(user: IUser) {
} as FeedOptions);
for (const note of notes) {
- const file = note._files && note._files.find(file => file.contentType.startsWith('image/'));
+ const files = note.fileIds.length > 0 ? await DriveFiles.find({
+ id: In(note.fileIds)
+ }) : [];
+ const file = files.find(file => file.type.startsWith('image/'));
feed.addItem({
title: `New note by ${author.name}`,
- link: `${config.url}/notes/${note._id}`,
+ link: `${config.url}/notes/${note.id}`,
date: note.createdAt,
description: note.cw,
content: note.text,
- image: file && getOriginalUrl(file)
+ image: file ? DriveFiles.getPublicUrl(file) : null
});
}
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index d8525ba114..de0d65cf33 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -9,19 +9,16 @@ import * as Router from 'koa-router';
import * as send from 'koa-send';
import * as favicon from 'koa-favicon';
import * as views from 'koa-views';
-import { ObjectID } from 'mongodb';
import docs from './docs';
import packFeed from './feed';
-import User from '../../models/user';
-import parseAcct from '../../misc/acct/parse';
-import config from '../../config';
-import Note, { pack as packNote } from '../../models/note';
-import getNoteSummary from '../../misc/get-note-summary';
import fetchMeta from '../../misc/fetch-meta';
-import Emoji from '../../models/emoji';
import * as pkg from '../../../package.json';
import { genOpenapiSpec } from '../api/openapi/gen-spec';
+import config from '../../config';
+import { Users, Notes, Emojis } from '../../models';
+import parseAcct from '../../misc/acct/parse';
+import getNoteSummary from '../../misc/get-note-summary';
const client = `${__dirname}/../../client/`;
@@ -100,7 +97,7 @@ router.get('/api.json', async ctx => {
const getFeed = async (acct: string) => {
const { username, host } = parseAcct(acct);
- const user = await User.findOne({
+ const user = await Users.findOne({
usernameLower: username.toLowerCase(),
host
});
@@ -148,7 +145,7 @@ router.get('/@:user.json', async ctx => {
// User
router.get('/@:user', async (ctx, next) => {
const { username, host } = parseAcct(ctx.params.user);
- const user = await User.findOne({
+ const user = await Users.findOne({
usernameLower: username.toLowerCase(),
host
});
@@ -157,7 +154,7 @@ router.get('/@:user', async (ctx, next) => {
const meta = await fetchMeta();
await ctx.render('user', {
user,
- instanceName: meta.name
+ instanceName: meta.name || 'Misskey'
});
ctx.set('Cache-Control', 'public, max-age=180');
} else {
@@ -167,19 +164,12 @@ router.get('/@:user', async (ctx, next) => {
});
router.get('/users/:user', async ctx => {
- if (!ObjectID.isValid(ctx.params.user)) {
- ctx.status = 404;
- return;
- }
-
- const userId = new ObjectID(ctx.params.user);
-
- const user = await User.findOne({
- _id: userId,
+ const user = await Users.findOne({
+ id: ctx.params.user,
host: null
});
- if (user === null) {
+ if (user == null) {
ctx.status = 404;
return;
}
@@ -189,26 +179,24 @@ router.get('/users/:user', async ctx => {
// Note
router.get('/notes/:note', async ctx => {
- if (ObjectID.isValid(ctx.params.note)) {
- const note = await Note.findOne({ _id: ctx.params.note });
+ const note = await Notes.findOne(ctx.params.note);
- if (note) {
- const _note = await packNote(note);
- const meta = await fetchMeta();
- await ctx.render('note', {
- note: _note,
- summary: getNoteSummary(_note),
- instanceName: meta.name
- });
-
- if (['public', 'home'].includes(note.visibility)) {
- ctx.set('Cache-Control', 'public, max-age=180');
- } else {
- ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
- }
+ if (note) {
+ const _note = await Notes.pack(note);
+ const meta = await fetchMeta();
+ await ctx.render('note', {
+ note: _note,
+ summary: getNoteSummary(_note),
+ instanceName: meta.name || 'Misskey'
+ });
- return;
+ if (['public', 'home'].includes(note.visibility)) {
+ ctx.set('Cache-Control', 'public, max-age=180');
+ } else {
+ ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
}
+
+ return;
}
ctx.status = 404;
@@ -217,10 +205,8 @@ router.get('/notes/:note', async ctx => {
router.get('/info', async ctx => {
const meta = await fetchMeta();
- const emojis = await Emoji.find({ host: null }, {
- fields: {
- _id: false
- }
+ const emojis = await Emojis.find({
+ where: { host: null }
});
await ctx.render('info', {
version: pkg.version,
@@ -232,7 +218,9 @@ router.get('/info', async ctx => {
cores: os.cpus().length
},
emojis: emojis,
- meta: meta
+ meta: meta,
+ originalUsersCount: await Users.count({ host: null }),
+ originalNotesCount: await Notes.count({ userHost: null })
});
});
@@ -247,7 +235,7 @@ router.get('*', async ctx => {
const meta = await fetchMeta();
await ctx.render('base', {
img: meta.bannerUrl,
- title: meta.name,
+ title: meta.name || 'Misskey',
desc: meta.description,
icon: meta.iconUrl
});
diff --git a/src/server/web/manifest.ts b/src/server/web/manifest.ts
index 35d3d1b666..4acfb22de5 100644
--- a/src/server/web/manifest.ts
+++ b/src/server/web/manifest.ts
@@ -1,10 +1,9 @@
import * as Koa from 'koa';
import * as manifest from '../../client/assets/manifest.json';
-import * as deepcopy from 'deepcopy';
import fetchMeta from '../../misc/fetch-meta';
module.exports = async (ctx: Koa.BaseContext) => {
- const json = deepcopy(manifest);
+ const json = JSON.parse(JSON.stringify(manifest));
const instance = await fetchMeta();
diff --git a/src/server/web/views/info.pug b/src/server/web/views/info.pug
index 1c4b272a62..c8b0bd939a 100644
--- a/src/server/web/views/info.pug
+++ b/src/server/web/views/info.pug
@@ -70,15 +70,15 @@ html
table
tr
th Instance
- td= meta.name
+ td= meta.name || 'Misskey'
tr
th Description
td= meta.description
tr
th Maintainer
td
- = meta.maintainer.name
- | &lt;#{meta.maintainer.email}&gt;
+ = meta.maintainerName
+ | &lt;#{meta.maintainerEmail}&gt;
tr
th System
td= os
@@ -93,10 +93,10 @@ html
td= cpu.model
tr
th Original users
- td= meta.stats.originalUsersCount
+ td= originalUsersCount
tr
th Original notes
- td= meta.stats.originalNotesCount
+ td= originalNotesCount
tr
th Registration
td= !meta.disableRegistration ? 'yes' : 'no'
diff --git a/src/server/well-known.ts b/src/server/well-known.ts
index 18c080acc7..7c5684d2ce 100644
--- a/src/server/well-known.ts
+++ b/src/server/well-known.ts
@@ -1,12 +1,12 @@
-import * as mongo from 'mongodb';
import * as Router from 'koa-router';
import config from '../config';
import parseAcct from '../misc/acct/parse';
-import User from '../models/user';
import Acct from '../misc/acct/type';
import { links } from './nodeinfo';
import { escapeAttribute, escapeValue } from '../prelude/xml';
+import { Users } from '../models';
+import { User } from '../models/entities/user';
// Init router
const router = new Router();
@@ -47,19 +47,19 @@ router.get('/.well-known/nodeinfo', async ctx => {
});
router.get(webFingerPath, async ctx => {
+ const fromId = (id: User['id']): Record<string, any> => ({
+ id,
+ host: null
+ });
+
const generateQuery = (resource: string) =>
resource.startsWith(`${config.url.toLowerCase()}/users/`) ?
- fromId(new mongo.ObjectID(resource.split('/').pop())) :
+ fromId(resource.split('/').pop()) :
fromAcct(parseAcct(
resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop() :
resource.startsWith('acct:') ? resource.slice('acct:'.length) :
resource));
- const fromId = (_id: mongo.ObjectID): Record<string, any> => ({
- _id,
- host: null
- });
-
const fromAcct = (acct: Acct): Record<string, any> | number =>
!acct.host || acct.host === config.host.toLowerCase() ? {
usernameLower: acct.username,
@@ -78,9 +78,9 @@ router.get(webFingerPath, async ctx => {
return;
}
- const user = await User.findOne(query);
+ const user = await Users.findOne(query);
- if (user === null) {
+ if (user == null) {
ctx.status = 404;
return;
}
@@ -89,7 +89,7 @@ router.get(webFingerPath, async ctx => {
const self = {
rel: 'self',
type: 'application/activity+json',
- href: `${config.url}/users/${user._id}`
+ href: `${config.url}/users/${user.id}`
};
const profilePage = {
rel: 'http://webfinger.net/rel/profile-page',
diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts
index c20666ef26..79ca0d59f1 100644
--- a/src/services/blocking/create.ts
+++ b/src/services/blocking/create.ts
@@ -1,6 +1,3 @@
-import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user';
-import Following from '../../models/following';
-import FollowRequest from '../../models/follow-request';
import { publishMainStream } from '../stream';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
@@ -8,11 +5,12 @@ import renderUndo from '../../remote/activitypub/renderer/undo';
import renderBlock from '../../remote/activitypub/renderer/block';
import { deliver } from '../../queue';
import renderReject from '../../remote/activitypub/renderer/reject';
-import perUserFollowingChart from '../../services/chart/per-user-following';
-import Blocking from '../../models/blocking';
-
-export default async function(blocker: IUser, blockee: IUser) {
+import { User } from '../../models/entities/user';
+import { Blockings, Users, FollowRequests, Followings } from '../../models';
+import { perUserFollowingChart } from '../chart';
+import { genId } from '../../misc/gen-id';
+export default async function(blocker: User, blockee: User) {
await Promise.all([
cancelRequest(blocker, blockee),
cancelRequest(blockee, blocker),
@@ -20,105 +18,90 @@ export default async function(blocker: IUser, blockee: IUser) {
unFollow(blockee, blocker)
]);
- await Blocking.insert({
+ await Blockings.save({
+ id: genId(),
createdAt: new Date(),
- blockerId: blocker._id,
- blockeeId: blockee._id,
+ blockerId: blocker.id,
+ blockeeId: blockee.id,
});
- if (isLocalUser(blocker) && isRemoteUser(blockee)) {
+ if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
const content = renderActivity(renderBlock(blocker, blockee));
deliver(blocker, content, blockee.inbox);
}
}
-async function cancelRequest(follower: IUser, followee: IUser) {
- const request = await FollowRequest.findOne({
- followeeId: followee._id,
- followerId: follower._id
+async function cancelRequest(follower: User, followee: User) {
+ const request = await FollowRequests.findOne({
+ followeeId: followee.id,
+ followerId: follower.id
});
if (request == null) {
return;
}
- await FollowRequest.remove({
- followeeId: followee._id,
- followerId: follower._id
- });
-
- await User.update({ _id: followee._id }, {
- $inc: {
- pendingReceivedFollowRequestsCount: -1
- }
+ await FollowRequests.delete({
+ followeeId: followee.id,
+ followerId: follower.id
});
- if (isLocalUser(followee)) {
- packUser(followee, followee, {
+ if (Users.isLocalUser(followee)) {
+ Users.pack(followee, followee, {
detail: true
- }).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
+ }).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
}
- if (isLocalUser(follower)) {
- packUser(followee, follower, {
+ if (Users.isLocalUser(follower)) {
+ Users.pack(followee, follower, {
detail: true
- }).then(packed => publishMainStream(follower._id, 'unfollow', packed));
+ }).then(packed => publishMainStream(follower.id, 'unfollow', packed));
}
// リモートにフォローリクエストをしていたらUndoFollow送信
- if (isLocalUser(follower) && isRemoteUser(followee)) {
+ if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox);
}
// リモートからフォローリクエストを受けていたらReject送信
- if (isRemoteUser(follower) && isLocalUser(followee)) {
+ if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee));
deliver(followee, content, follower.inbox);
}
}
-async function unFollow(follower: IUser, followee: IUser) {
- const following = await Following.findOne({
- followerId: follower._id,
- followeeId: followee._id
+async function unFollow(follower: User, followee: User) {
+ const following = await Followings.findOne({
+ followerId: follower.id,
+ followeeId: followee.id
});
if (following == null) {
return;
}
- Following.remove({
- _id: following._id
- });
+ Followings.delete(following.id);
//#region Decrement following count
- User.update({ _id: follower._id }, {
- $inc: {
- followingCount: -1
- }
- });
+ Users.decrement({ id: follower.id }, 'followingCount', 1);
//#endregion
//#region Decrement followers count
- User.update({ _id: followee._id }, {
- $inc: {
- followersCount: -1
- }
- });
+ Users.decrement({ id: followee.id }, 'followersCount', 1);
//#endregion
perUserFollowingChart.update(follower, followee, false);
// Publish unfollow event
- if (isLocalUser(follower)) {
- packUser(followee, follower, {
+ if (Users.isLocalUser(follower)) {
+ Users.pack(followee, follower, {
detail: true
- }).then(packed => publishMainStream(follower._id, 'unfollow', packed));
+ }).then(packed => publishMainStream(follower.id, 'unfollow', packed));
}
// リモートにフォローをしていたらUndoFollow送信
- if (isLocalUser(follower) && isRemoteUser(followee)) {
+ if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox);
}
diff --git a/src/services/blocking/delete.ts b/src/services/blocking/delete.ts
index 099fa14b37..2c05cb7f3f 100644
--- a/src/services/blocking/delete.ts
+++ b/src/services/blocking/delete.ts
@@ -1,17 +1,17 @@
-import { isLocalUser, isRemoteUser, IUser } from '../../models/user';
-import Blocking from '../../models/blocking';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderBlock from '../../remote/activitypub/renderer/block';
import renderUndo from '../../remote/activitypub/renderer/undo';
import { deliver } from '../../queue';
import Logger from '../logger';
+import { User } from '../../models/entities/user';
+import { Blockings, Users } from '../../models';
const logger = new Logger('blocking/delete');
-export default async function(blocker: IUser, blockee: IUser) {
- const blocking = await Blocking.findOne({
- blockerId: blocker._id,
- blockeeId: blockee._id
+export default async function(blocker: User, blockee: User) {
+ const blocking = await Blockings.findOne({
+ blockerId: blocker.id,
+ blockeeId: blockee.id
});
if (blocking == null) {
@@ -19,12 +19,10 @@ export default async function(blocker: IUser, blockee: IUser) {
return;
}
- Blocking.remove({
- _id: blocking._id
- });
+ Blockings.delete(blocking.id);
// deliver if remote bloking
- if (isLocalUser(blocker) && isRemoteUser(blockee)) {
+ if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker));
deliver(blocker, content, blockee.inbox);
}
diff --git a/src/services/chart/active-users.ts b/src/services/chart/active-users.ts
deleted file mode 100644
index 2a4e1a97ac..0000000000
--- a/src/services/chart/active-users.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from '.';
-import { IUser, isLocalUser } from '../../models/user';
-
-/**
- * アクティブユーザーに関するチャート
- */
-type ActiveUsersLog = {
- local: {
- /**
- * アクティブユーザー数
- */
- count: number;
- };
-
- remote: ActiveUsersLog['local'];
-};
-
-class ActiveUsersChart extends Chart<ActiveUsersLog> {
- constructor() {
- super('activeUsers');
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: ActiveUsersLog): Promise<ActiveUsersLog> {
- return {
- local: {
- count: 0
- },
- remote: {
- count: 0
- }
- };
- }
-
- @autobind
- public async update(user: IUser) {
- const update: Obj = {
- count: 1
- };
-
- await this.incIfUnique({
- [isLocalUser(user) ? 'local' : 'remote']: update
- }, 'users', user._id.toHexString());
- }
-}
-
-export default new ActiveUsersChart();
diff --git a/src/services/chart/charts/classes/active-users.ts b/src/services/chart/charts/classes/active-users.ts
new file mode 100644
index 0000000000..5128150de6
--- /dev/null
+++ b/src/services/chart/charts/classes/active-users.ts
@@ -0,0 +1,35 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { User } from '../../../../models/entities/user';
+import { SchemaType } from '../../../../misc/schema';
+import { Users } from '../../../../models';
+import { name, schema } from '../schemas/active-users';
+
+type ActiveUsersLog = SchemaType<typeof schema>;
+
+export default class ActiveUsersChart extends Chart<ActiveUsersLog> {
+ constructor() {
+ super(name, schema);
+ }
+
+ @autobind
+ protected genNewLog(latest: ActiveUsersLog): DeepPartial<ActiveUsersLog> {
+ return {};
+ }
+
+ @autobind
+ protected async fetchActual(): Promise<DeepPartial<ActiveUsersLog>> {
+ return {};
+ }
+
+ @autobind
+ public async update(user: User) {
+ const update: Obj = {
+ count: 1
+ };
+
+ await this.incIfUnique({
+ [Users.isLocalUser(user) ? 'local' : 'remote']: update
+ }, 'users', user.id);
+ }
+}
diff --git a/src/services/chart/charts/classes/drive.ts b/src/services/chart/charts/classes/drive.ts
new file mode 100644
index 0000000000..ae52df19ac
--- /dev/null
+++ b/src/services/chart/charts/classes/drive.ts
@@ -0,0 +1,69 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { DriveFiles } from '../../../../models';
+import { Not } from 'typeorm';
+import { DriveFile } from '../../../../models/entities/drive-file';
+import { name, schema } from '../schemas/drive';
+
+type DriveLog = SchemaType<typeof schema>;
+
+export default class DriveChart extends Chart<DriveLog> {
+ constructor() {
+ super(name, schema);
+ }
+
+ @autobind
+ protected genNewLog(latest: DriveLog): DeepPartial<DriveLog> {
+ return {
+ local: {
+ totalCount: latest.local.totalCount,
+ totalSize: latest.local.totalSize,
+ },
+ remote: {
+ totalCount: latest.remote.totalCount,
+ totalSize: latest.remote.totalSize,
+ }
+ };
+ }
+
+ @autobind
+ protected async fetchActual(): Promise<DeepPartial<DriveLog>> {
+ const [localCount, remoteCount, localSize, remoteSize] = await Promise.all([
+ DriveFiles.count({ userHost: null }),
+ DriveFiles.count({ userHost: Not(null) }),
+ DriveFiles.clacDriveUsageOfLocal(),
+ DriveFiles.clacDriveUsageOfRemote()
+ ]);
+
+ return {
+ local: {
+ totalCount: localCount,
+ totalSize: localSize,
+ },
+ remote: {
+ totalCount: remoteCount,
+ totalSize: remoteSize,
+ }
+ };
+ }
+
+ @autobind
+ public async update(file: DriveFile, isAdditional: boolean) {
+ const update: Obj = {};
+
+ update.totalCount = isAdditional ? 1 : -1;
+ update.totalSize = isAdditional ? file.size : -file.size;
+ if (isAdditional) {
+ update.incCount = 1;
+ update.incSize = file.size;
+ } else {
+ update.decCount = 1;
+ update.decSize = file.size;
+ }
+
+ await this.inc({
+ [file.userHost === null ? 'local' : 'remote']: update
+ });
+ }
+}
diff --git a/src/services/chart/charts/classes/federation.ts b/src/services/chart/charts/classes/federation.ts
new file mode 100644
index 0000000000..bd2c497e7b
--- /dev/null
+++ b/src/services/chart/charts/classes/federation.ts
@@ -0,0 +1,51 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { Instances } from '../../../../models';
+import { name, schema } from '../schemas/federation';
+
+type FederationLog = SchemaType<typeof schema>;
+
+export default class FederationChart extends Chart<FederationLog> {
+ constructor() {
+ super(name, schema);
+ }
+
+ @autobind
+ protected genNewLog(latest: FederationLog): DeepPartial<FederationLog> {
+ return {
+ instance: {
+ total: latest.instance.total,
+ }
+ };
+ }
+
+ @autobind
+ protected async fetchActual(): Promise<DeepPartial<FederationLog>> {
+ const [total] = await Promise.all([
+ Instances.count({})
+ ]);
+
+ return {
+ instance: {
+ total: total,
+ }
+ };
+ }
+
+ @autobind
+ public async update(isAdditional: boolean) {
+ const update: Obj = {};
+
+ update.total = isAdditional ? 1 : -1;
+ if (isAdditional) {
+ update.inc = 1;
+ } else {
+ update.dec = 1;
+ }
+
+ await this.inc({
+ instance: update
+ });
+ }
+}
diff --git a/src/services/chart/charts/classes/hashtag.ts b/src/services/chart/charts/classes/hashtag.ts
new file mode 100644
index 0000000000..38c3a94f0c
--- /dev/null
+++ b/src/services/chart/charts/classes/hashtag.ts
@@ -0,0 +1,35 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { User } from '../../../../models/entities/user';
+import { SchemaType } from '../../../../misc/schema';
+import { Users } from '../../../../models';
+import { name, schema } from '../schemas/hashtag';
+
+type HashtagLog = SchemaType<typeof schema>;
+
+export default class HashtagChart extends Chart<HashtagLog> {
+ constructor() {
+ super(name, schema, true);
+ }
+
+ @autobind
+ protected genNewLog(latest: HashtagLog): DeepPartial<HashtagLog> {
+ return {};
+ }
+
+ @autobind
+ protected async fetchActual(): Promise<DeepPartial<HashtagLog>> {
+ return {};
+ }
+
+ @autobind
+ public async update(hashtag: string, user: User) {
+ const update: Obj = {
+ count: 1
+ };
+
+ await this.incIfUnique({
+ [Users.isLocalUser(user) ? 'local' : 'remote']: update
+ }, 'users', user.id, hashtag);
+ }
+}
diff --git a/src/services/chart/charts/classes/instance.ts b/src/services/chart/charts/classes/instance.ts
new file mode 100644
index 0000000000..974eac036b
--- /dev/null
+++ b/src/services/chart/charts/classes/instance.ts
@@ -0,0 +1,160 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { DriveFiles, Followings, Users, Notes } from '../../../../models';
+import { DriveFile } from '../../../../models/entities/drive-file';
+import { name, schema } from '../schemas/instance';
+
+type InstanceLog = SchemaType<typeof schema>;
+
+export default class InstanceChart extends Chart<InstanceLog> {
+ constructor() {
+ super(name, schema);
+ }
+
+ @autobind
+ protected genNewLog(latest: InstanceLog): DeepPartial<InstanceLog> {
+ return {
+ notes: {
+ total: latest.notes.total,
+ },
+ users: {
+ total: latest.users.total,
+ },
+ following: {
+ total: latest.following.total,
+ },
+ followers: {
+ total: latest.followers.total,
+ },
+ drive: {
+ totalFiles: latest.drive.totalFiles,
+ totalUsage: latest.drive.totalUsage,
+ }
+ };
+ }
+
+ @autobind
+ protected async fetchActual(group: string): Promise<DeepPartial<InstanceLog>> {
+ const [
+ notesCount,
+ usersCount,
+ followingCount,
+ followersCount,
+ driveFiles,
+ driveUsage,
+ ] = await Promise.all([
+ Notes.count({ userHost: group }),
+ Users.count({ host: group }),
+ Followings.count({ followerHost: group }),
+ Followings.count({ followeeHost: group }),
+ DriveFiles.count({ userHost: group }),
+ DriveFiles.clacDriveUsageOfHost(group),
+ ]);
+
+ return {
+ notes: {
+ total: notesCount,
+ },
+ users: {
+ total: usersCount,
+ },
+ following: {
+ total: followingCount,
+ },
+ followers: {
+ total: followersCount,
+ },
+ drive: {
+ totalFiles: driveFiles,
+ totalUsage: driveUsage,
+ }
+ };
+ }
+
+ @autobind
+ public async requestReceived(host: string) {
+ await this.inc({
+ requests: {
+ received: 1
+ }
+ }, host);
+ }
+
+ @autobind
+ public async requestSent(host: string, isSucceeded: boolean) {
+ const update: Obj = {};
+
+ if (isSucceeded) {
+ update.succeeded = 1;
+ } else {
+ update.failed = 1;
+ }
+
+ await this.inc({
+ requests: update
+ }, host);
+ }
+
+ @autobind
+ public async newUser(host: string) {
+ await this.inc({
+ users: {
+ total: 1,
+ inc: 1
+ }
+ }, host);
+ }
+
+ @autobind
+ public async updateNote(host: string, isAdditional: boolean) {
+ await this.inc({
+ notes: {
+ total: isAdditional ? 1 : -1,
+ inc: isAdditional ? 1 : 0,
+ dec: isAdditional ? 0 : 1,
+ }
+ }, host);
+ }
+
+ @autobind
+ public async updateFollowing(host: string, isAdditional: boolean) {
+ await this.inc({
+ following: {
+ total: isAdditional ? 1 : -1,
+ inc: isAdditional ? 1 : 0,
+ dec: isAdditional ? 0 : 1,
+ }
+ }, host);
+ }
+
+ @autobind
+ public async updateFollowers(host: string, isAdditional: boolean) {
+ await this.inc({
+ followers: {
+ total: isAdditional ? 1 : -1,
+ inc: isAdditional ? 1 : 0,
+ dec: isAdditional ? 0 : 1,
+ }
+ }, host);
+ }
+
+ @autobind
+ public async updateDrive(file: DriveFile, isAdditional: boolean) {
+ const update: Obj = {};
+
+ update.totalFiles = isAdditional ? 1 : -1;
+ update.totalUsage = isAdditional ? file.size : -file.size;
+ if (isAdditional) {
+ update.incFiles = 1;
+ update.incUsage = file.size;
+ } else {
+ update.decFiles = 1;
+ update.decUsage = file.size;
+ }
+
+ await this.inc({
+ drive: update
+ }, file.userHost);
+ }
+}
diff --git a/src/services/chart/charts/classes/network.ts b/src/services/chart/charts/classes/network.ts
new file mode 100644
index 0000000000..8b26e5c4c2
--- /dev/null
+++ b/src/services/chart/charts/classes/network.ts
@@ -0,0 +1,34 @@
+import autobind from 'autobind-decorator';
+import Chart, { DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { name, schema } from '../schemas/network';
+
+type NetworkLog = SchemaType<typeof schema>;
+
+export default class NetworkChart extends Chart<NetworkLog> {
+ constructor() {
+ super(name, schema);
+ }
+
+ @autobind
+ protected genNewLog(latest: NetworkLog): DeepPartial<NetworkLog> {
+ return {};
+ }
+
+ @autobind
+ protected async fetchActual(): Promise<DeepPartial<NetworkLog>> {
+ return {};
+ }
+
+ @autobind
+ public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) {
+ const inc: DeepPartial<NetworkLog> = {
+ incomingRequests: incomingRequests,
+ totalTime: time,
+ incomingBytes: incomingBytes,
+ outgoingBytes: outgoingBytes
+ };
+
+ await this.inc(inc);
+ }
+}
diff --git a/src/services/chart/charts/classes/notes.ts b/src/services/chart/charts/classes/notes.ts
new file mode 100644
index 0000000000..85ccf000d8
--- /dev/null
+++ b/src/services/chart/charts/classes/notes.ts
@@ -0,0 +1,71 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { Notes } from '../../../../models';
+import { Not } from 'typeorm';
+import { Note } from '../../../../models/entities/note';
+import { name, schema } from '../schemas/notes';
+
+type NotesLog = SchemaType<typeof schema>;
+
+export default class NotesChart extends Chart<NotesLog> {
+ constructor() {
+ super(name, schema);
+ }
+
+ @autobind
+ protected genNewLog(latest: NotesLog): DeepPartial<NotesLog> {
+ return {
+ local: {
+ total: latest.local.total,
+ },
+ remote: {
+ total: latest.remote.total,
+ }
+ };
+ }
+
+ @autobind
+ protected async fetchActual(): Promise<DeepPartial<NotesLog>> {
+ const [localCount, remoteCount] = await Promise.all([
+ Notes.count({ userHost: null }),
+ Notes.count({ userHost: Not(null) })
+ ]);
+
+ return {
+ local: {
+ total: localCount,
+ },
+ remote: {
+ total: remoteCount,
+ }
+ };
+ }
+
+ @autobind
+ public async update(note: Note, isAdditional: boolean) {
+ const update: Obj = {
+ diffs: {}
+ };
+
+ update.total = isAdditional ? 1 : -1;
+
+ if (isAdditional) {
+ update.inc = 1;
+ } else {
+ update.dec = 1;
+ }
+
+ if (note.replyId != null) {
+ update.diffs.reply = isAdditional ? 1 : -1;
+ } else if (note.renoteId != null) {
+ update.diffs.renote = isAdditional ? 1 : -1;
+ } else {
+ update.diffs.normal = isAdditional ? 1 : -1;
+ }
+
+ await this.inc({
+ [note.userHost === null ? 'local' : 'remote']: update
+ });
+ }
+}
diff --git a/src/services/chart/charts/classes/per-user-drive.ts b/src/services/chart/charts/classes/per-user-drive.ts
new file mode 100644
index 0000000000..822f4eda0f
--- /dev/null
+++ b/src/services/chart/charts/classes/per-user-drive.ts
@@ -0,0 +1,52 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { DriveFiles } from '../../../../models';
+import { DriveFile } from '../../../../models/entities/drive-file';
+import { name, schema } from '../schemas/per-user-drive';
+
+type PerUserDriveLog = SchemaType<typeof schema>;
+
+export default class PerUserDriveChart extends Chart<PerUserDriveLog> {
+ constructor() {
+ super(name, schema, true);
+ }
+
+ @autobind
+ protected genNewLog(latest: PerUserDriveLog): DeepPartial<PerUserDriveLog> {
+ return {
+ totalCount: latest.totalCount,
+ totalSize: latest.totalSize,
+ };
+ }
+
+ @autobind
+ protected async fetchActual(group: string): Promise<DeepPartial<PerUserDriveLog>> {
+ const [count, size] = await Promise.all([
+ DriveFiles.count({ userId: group }),
+ DriveFiles.clacDriveUsageOf(group)
+ ]);
+
+ return {
+ totalCount: count,
+ totalSize: size,
+ };
+ }
+
+ @autobind
+ public async update(file: DriveFile, isAdditional: boolean) {
+ const update: Obj = {};
+
+ update.totalCount = isAdditional ? 1 : -1;
+ update.totalSize = isAdditional ? file.size : -file.size;
+ if (isAdditional) {
+ update.incCount = 1;
+ update.incSize = file.size;
+ } else {
+ update.decCount = 1;
+ update.decSize = file.size;
+ }
+
+ await this.inc(update, file.userId);
+ }
+}
diff --git a/src/services/chart/charts/classes/per-user-following.ts b/src/services/chart/charts/classes/per-user-following.ts
new file mode 100644
index 0000000000..f3809a7c94
--- /dev/null
+++ b/src/services/chart/charts/classes/per-user-following.ts
@@ -0,0 +1,91 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { Followings, Users } from '../../../../models';
+import { Not } from 'typeorm';
+import { User } from '../../../../models/entities/user';
+import { name, schema } from '../schemas/per-user-following';
+
+type PerUserFollowingLog = SchemaType<typeof schema>;
+
+export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> {
+ constructor() {
+ super(name, schema, true);
+ }
+
+ @autobind
+ protected genNewLog(latest: PerUserFollowingLog): DeepPartial<PerUserFollowingLog> {
+ return {
+ local: {
+ followings: {
+ total: latest.local.followings.total,
+ },
+ followers: {
+ total: latest.local.followers.total,
+ }
+ },
+ remote: {
+ followings: {
+ total: latest.remote.followings.total,
+ },
+ followers: {
+ total: latest.remote.followers.total,
+ }
+ }
+ };
+ }
+
+ @autobind
+ protected async fetchActual(group: string): Promise<DeepPartial<PerUserFollowingLog>> {
+ const [
+ localFollowingsCount,
+ localFollowersCount,
+ remoteFollowingsCount,
+ remoteFollowersCount
+ ] = await Promise.all([
+ Followings.count({ followerId: group, followeeHost: null }),
+ Followings.count({ followeeId: group, followerHost: null }),
+ Followings.count({ followerId: group, followeeHost: Not(null) }),
+ Followings.count({ followeeId: group, followerHost: Not(null) })
+ ]);
+
+ return {
+ local: {
+ followings: {
+ total: localFollowingsCount,
+ },
+ followers: {
+ total: localFollowersCount,
+ }
+ },
+ remote: {
+ followings: {
+ total: remoteFollowingsCount,
+ },
+ followers: {
+ total: remoteFollowersCount,
+ }
+ }
+ };
+ }
+
+ @autobind
+ public async update(follower: User, followee: User, isFollow: boolean) {
+ const update: Obj = {};
+
+ update.total = isFollow ? 1 : -1;
+
+ if (isFollow) {
+ update.inc = 1;
+ } else {
+ update.dec = 1;
+ }
+
+ this.inc({
+ [Users.isLocalUser(follower) ? 'local' : 'remote']: { followings: update }
+ }, follower.id);
+ this.inc({
+ [Users.isLocalUser(followee) ? 'local' : 'remote']: { followers: update }
+ }, followee.id);
+ }
+}
diff --git a/src/services/chart/charts/classes/per-user-notes.ts b/src/services/chart/charts/classes/per-user-notes.ts
new file mode 100644
index 0000000000..cccd495604
--- /dev/null
+++ b/src/services/chart/charts/classes/per-user-notes.ts
@@ -0,0 +1,58 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { User } from '../../../../models/entities/user';
+import { SchemaType } from '../../../../misc/schema';
+import { Notes } from '../../../../models';
+import { Note } from '../../../../models/entities/note';
+import { name, schema } from '../schemas/per-user-notes';
+
+type PerUserNotesLog = SchemaType<typeof schema>;
+
+export default class PerUserNotesChart extends Chart<PerUserNotesLog> {
+ constructor() {
+ super(name, schema, true);
+ }
+
+ @autobind
+ protected genNewLog(latest: PerUserNotesLog): DeepPartial<PerUserNotesLog> {
+ return {
+ total: latest.total,
+ };
+ }
+
+ @autobind
+ protected async fetchActual(group: string): Promise<DeepPartial<PerUserNotesLog>> {
+ const [count] = await Promise.all([
+ Notes.count({ userId: group }),
+ ]);
+
+ return {
+ total: count,
+ };
+ }
+
+ @autobind
+ public async update(user: User, note: Note, isAdditional: boolean) {
+ const update: Obj = {
+ diffs: {}
+ };
+
+ update.total = isAdditional ? 1 : -1;
+
+ if (isAdditional) {
+ update.inc = 1;
+ } else {
+ update.dec = 1;
+ }
+
+ if (note.replyId != null) {
+ update.diffs.reply = isAdditional ? 1 : -1;
+ } else if (note.renoteId != null) {
+ update.diffs.renote = isAdditional ? 1 : -1;
+ } else {
+ update.diffs.normal = isAdditional ? 1 : -1;
+ }
+
+ await this.inc(update, user.id);
+ }
+}
diff --git a/src/services/chart/charts/classes/per-user-reactions.ts b/src/services/chart/charts/classes/per-user-reactions.ts
new file mode 100644
index 0000000000..124fb4153c
--- /dev/null
+++ b/src/services/chart/charts/classes/per-user-reactions.ts
@@ -0,0 +1,32 @@
+import autobind from 'autobind-decorator';
+import Chart, { DeepPartial } from '../../core';
+import { User } from '../../../../models/entities/user';
+import { Note } from '../../../../models/entities/note';
+import { SchemaType } from '../../../../misc/schema';
+import { Users } from '../../../../models';
+import { name, schema } from '../schemas/per-user-reactions';
+
+type PerUserReactionsLog = SchemaType<typeof schema>;
+
+export default class PerUserReactionsChart extends Chart<PerUserReactionsLog> {
+ constructor() {
+ super(name, schema, true);
+ }
+
+ @autobind
+ protected genNewLog(latest: PerUserReactionsLog): DeepPartial<PerUserReactionsLog> {
+ return {};
+ }
+
+ @autobind
+ protected async fetchActual(group: string): Promise<DeepPartial<PerUserReactionsLog>> {
+ return {};
+ }
+
+ @autobind
+ public async update(user: User, note: Note) {
+ this.inc({
+ [Users.isLocalUser(user) ? 'local' : 'remote']: { count: 1 }
+ }, note.userId);
+ }
+}
diff --git a/src/services/chart/charts/classes/test-grouped.ts b/src/services/chart/charts/classes/test-grouped.ts
new file mode 100644
index 0000000000..e32cbcf416
--- /dev/null
+++ b/src/services/chart/charts/classes/test-grouped.ts
@@ -0,0 +1,47 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { name, schema } from '../schemas/test-grouped';
+
+type TestGroupedLog = SchemaType<typeof schema>;
+
+export default class TestGroupedChart extends Chart<TestGroupedLog> {
+ private total = {} as Record<string, number>;
+
+ constructor() {
+ super(name, schema, true);
+ }
+
+ @autobind
+ protected genNewLog(latest: TestGroupedLog): DeepPartial<TestGroupedLog> {
+ return {
+ foo: {
+ total: latest.foo.total,
+ },
+ };
+ }
+
+ @autobind
+ protected async fetchActual(group: string): Promise<DeepPartial<TestGroupedLog>> {
+ return {
+ foo: {
+ total: this.total[group],
+ },
+ };
+ }
+
+ @autobind
+ public async increment(group: string) {
+ if (this.total[group] == null) this.total[group] = 0;
+
+ const update: Obj = {};
+
+ update.total = 1;
+ update.inc = 1;
+ this.total[group]++;
+
+ await this.inc({
+ foo: update
+ }, group);
+ }
+}
diff --git a/src/services/chart/charts/classes/test-unique.ts b/src/services/chart/charts/classes/test-unique.ts
new file mode 100644
index 0000000000..1eb396c293
--- /dev/null
+++ b/src/services/chart/charts/classes/test-unique.ts
@@ -0,0 +1,29 @@
+import autobind from 'autobind-decorator';
+import Chart, { DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { name, schema } from '../schemas/test-unique';
+
+type TestUniqueLog = SchemaType<typeof schema>;
+
+export default class TestUniqueChart extends Chart<TestUniqueLog> {
+ constructor() {
+ super(name, schema);
+ }
+
+ @autobind
+ protected genNewLog(latest: TestUniqueLog): DeepPartial<TestUniqueLog> {
+ return {};
+ }
+
+ @autobind
+ protected async fetchActual(): Promise<DeepPartial<TestUniqueLog>> {
+ return {};
+ }
+
+ @autobind
+ public async uniqueIncrement(key: string) {
+ await this.incIfUnique({
+ foo: 1
+ }, 'foos', key);
+ }
+}
diff --git a/src/services/chart/charts/classes/test.ts b/src/services/chart/charts/classes/test.ts
new file mode 100644
index 0000000000..57c22822f2
--- /dev/null
+++ b/src/services/chart/charts/classes/test.ts
@@ -0,0 +1,45 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { name, schema } from '../schemas/test';
+
+type TestLog = SchemaType<typeof schema>;
+
+export default class TestChart extends Chart<TestLog> {
+ private total = 0;
+
+ constructor() {
+ super(name, schema);
+ }
+
+ @autobind
+ protected genNewLog(latest: TestLog): DeepPartial<TestLog> {
+ return {
+ foo: {
+ total: latest.foo.total,
+ },
+ };
+ }
+
+ @autobind
+ protected async fetchActual(): Promise<DeepPartial<TestLog>> {
+ return {
+ foo: {
+ total: this.total,
+ },
+ };
+ }
+
+ @autobind
+ public async increment() {
+ const update: Obj = {};
+
+ update.total = 1;
+ update.inc = 1;
+ this.total++;
+
+ await this.inc({
+ foo: update
+ });
+ }
+}
diff --git a/src/services/chart/charts/classes/users.ts b/src/services/chart/charts/classes/users.ts
new file mode 100644
index 0000000000..eec30de8dc
--- /dev/null
+++ b/src/services/chart/charts/classes/users.ts
@@ -0,0 +1,60 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { Users } from '../../../../models';
+import { Not } from 'typeorm';
+import { User } from '../../../../models/entities/user';
+import { name, schema } from '../schemas/users';
+
+type UsersLog = SchemaType<typeof schema>;
+
+export default class UsersChart extends Chart<UsersLog> {
+ constructor() {
+ super(name, schema);
+ }
+
+ @autobind
+ protected genNewLog(latest: UsersLog): DeepPartial<UsersLog> {
+ return {
+ local: {
+ total: latest.local.total,
+ },
+ remote: {
+ total: latest.remote.total,
+ }
+ };
+ }
+
+ @autobind
+ protected async fetchActual(): Promise<DeepPartial<UsersLog>> {
+ const [localCount, remoteCount] = await Promise.all([
+ Users.count({ host: null }),
+ Users.count({ host: Not(null) })
+ ]);
+
+ return {
+ local: {
+ total: localCount,
+ },
+ remote: {
+ total: remoteCount,
+ }
+ };
+ }
+
+ @autobind
+ public async update(user: User, isAdditional: boolean) {
+ const update: Obj = {};
+
+ update.total = isAdditional ? 1 : -1;
+ if (isAdditional) {
+ update.inc = 1;
+ } else {
+ update.dec = 1;
+ }
+
+ await this.inc({
+ [Users.isLocalUser(user) ? 'local' : 'remote']: update
+ });
+ }
+}
diff --git a/src/services/chart/charts/schemas/active-users.ts b/src/services/chart/charts/schemas/active-users.ts
new file mode 100644
index 0000000000..da8c63389c
--- /dev/null
+++ b/src/services/chart/charts/schemas/active-users.ts
@@ -0,0 +1,28 @@
+export const logSchema = {
+ /**
+ * アクティブユーザー数
+ */
+ count: {
+ type: 'number' as 'number',
+ description: 'アクティブユーザー数',
+ },
+};
+
+/**
+ * アクティブユーザーに関するチャート
+ */
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ local: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ remote: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ }
+};
+
+export const name = 'activeUsers';
diff --git a/src/services/chart/charts/schemas/drive.ts b/src/services/chart/charts/schemas/drive.ts
new file mode 100644
index 0000000000..47530e8417
--- /dev/null
+++ b/src/services/chart/charts/schemas/drive.ts
@@ -0,0 +1,65 @@
+const logSchema = {
+ /**
+ * 集計期間時点での、全ドライブファイル数
+ */
+ totalCount: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全ドライブファイル数'
+ },
+
+ /**
+ * 集計期間時点での、全ドライブファイルの合計サイズ
+ */
+ totalSize: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全ドライブファイルの合計サイズ'
+ },
+
+ /**
+ * 増加したドライブファイル数
+ */
+ incCount: {
+ type: 'number' as 'number',
+ description: '増加したドライブファイル数'
+ },
+
+ /**
+ * 増加したドライブ使用量
+ */
+ incSize: {
+ type: 'number' as 'number',
+ description: '増加したドライブ使用量'
+ },
+
+ /**
+ * 減少したドライブファイル数
+ */
+ decCount: {
+ type: 'number' as 'number',
+ description: '減少したドライブファイル数'
+ },
+
+ /**
+ * 減少したドライブ使用量
+ */
+ decSize: {
+ type: 'number' as 'number',
+ description: '減少したドライブ使用量'
+ },
+};
+
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ local: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ remote: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ }
+};
+
+export const name = 'drive';
diff --git a/src/services/chart/charts/schemas/federation.ts b/src/services/chart/charts/schemas/federation.ts
new file mode 100644
index 0000000000..d1d275fc95
--- /dev/null
+++ b/src/services/chart/charts/schemas/federation.ts
@@ -0,0 +1,27 @@
+/**
+ * フェデレーションに関するチャート
+ */
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ instance: {
+ type: 'object' as 'object',
+ properties: {
+ total: {
+ type: 'number' as 'number',
+ description: 'インスタンス数の合計'
+ },
+ inc: {
+ type: 'number' as 'number',
+ description: '増加インスタンス数'
+ },
+ dec: {
+ type: 'number' as 'number',
+ description: '減少インスタンス数'
+ },
+ }
+ }
+ }
+};
+
+export const name = 'federation';
diff --git a/src/services/chart/charts/schemas/hashtag.ts b/src/services/chart/charts/schemas/hashtag.ts
new file mode 100644
index 0000000000..c1904b6701
--- /dev/null
+++ b/src/services/chart/charts/schemas/hashtag.ts
@@ -0,0 +1,28 @@
+export const logSchema = {
+ /**
+ * 投稿された数
+ */
+ count: {
+ type: 'number' as 'number',
+ description: '投稿された数',
+ },
+};
+
+/**
+ * ハッシュタグに関するチャート
+ */
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ local: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ remote: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ }
+};
+
+export const name = 'hashtag';
diff --git a/src/services/chart/charts/schemas/instance.ts b/src/services/chart/charts/schemas/instance.ts
new file mode 100644
index 0000000000..af46b33629
--- /dev/null
+++ b/src/services/chart/charts/schemas/instance.ts
@@ -0,0 +1,124 @@
+/**
+ * インスタンスごとのチャート
+ */
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ requests: {
+ type: 'object' as 'object',
+ properties: {
+ failed: {
+ type: 'number' as 'number',
+ description: '失敗したリクエスト数'
+ },
+ succeeded: {
+ type: 'number' as 'number',
+ description: '成功したリクエスト数'
+ },
+ received: {
+ type: 'number' as 'number',
+ description: '受信したリクエスト数'
+ },
+ }
+ },
+ notes: {
+ type: 'object' as 'object',
+ properties: {
+ total: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全投稿数'
+ },
+ inc: {
+ type: 'number' as 'number',
+ description: '増加した投稿数'
+ },
+ dec: {
+ type: 'number' as 'number',
+ description: '減少した投稿数'
+ },
+ }
+ },
+ users: {
+ type: 'object' as 'object',
+ properties: {
+ total: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全ユーザー数'
+ },
+ inc: {
+ type: 'number' as 'number',
+ description: '増加したユーザー数'
+ },
+ dec: {
+ type: 'number' as 'number',
+ description: '減少したユーザー数'
+ },
+ }
+ },
+ following: {
+ type: 'object' as 'object',
+ properties: {
+ total: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全フォロー数'
+ },
+ inc: {
+ type: 'number' as 'number',
+ description: '増加したフォロー数'
+ },
+ dec: {
+ type: 'number' as 'number',
+ description: '減少したフォロー数'
+ },
+ }
+ },
+ followers: {
+ type: 'object' as 'object',
+ properties: {
+ total: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全フォロワー数'
+ },
+ inc: {
+ type: 'number' as 'number',
+ description: '増加したフォロワー数'
+ },
+ dec: {
+ type: 'number' as 'number',
+ description: '減少したフォロワー数'
+ },
+ }
+ },
+ drive: {
+ type: 'object' as 'object',
+ properties: {
+ totalFiles: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全ドライブファイル数'
+ },
+ totalUsage: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全ドライブファイルの合計サイズ'
+ },
+ incFiles: {
+ type: 'number' as 'number',
+ description: '増加したドライブファイル数'
+ },
+ incUsage: {
+ type: 'number' as 'number',
+ description: '増加したドライブ使用量'
+ },
+ decFiles: {
+ type: 'number' as 'number',
+ description: '減少したドライブファイル数'
+ },
+ decUsage: {
+ type: 'number' as 'number',
+ description: '減少したドライブ使用量'
+ },
+ }
+ },
+ }
+};
+
+export const name = 'instance';
diff --git a/src/services/chart/charts/schemas/network.ts b/src/services/chart/charts/schemas/network.ts
new file mode 100644
index 0000000000..4ef530c07c
--- /dev/null
+++ b/src/services/chart/charts/schemas/network.ts
@@ -0,0 +1,30 @@
+/**
+ * ネットワークに関するチャート
+ */
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ incomingRequests: {
+ type: 'number' as 'number',
+ description: '受信したリクエスト数'
+ },
+ outgoingRequests: {
+ type: 'number' as 'number',
+ description: '送信したリクエスト数'
+ },
+ totalTime: {
+ type: 'number' as 'number',
+ description: '応答時間の合計' // TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる
+ },
+ incomingBytes: {
+ type: 'number' as 'number',
+ description: '合計受信データ量'
+ },
+ outgoingBytes: {
+ type: 'number' as 'number',
+ description: '合計送信データ量'
+ },
+ }
+};
+
+export const name = 'network';
diff --git a/src/services/chart/charts/schemas/notes.ts b/src/services/chart/charts/schemas/notes.ts
new file mode 100644
index 0000000000..133d1e3730
--- /dev/null
+++ b/src/services/chart/charts/schemas/notes.ts
@@ -0,0 +1,52 @@
+const logSchema = {
+ total: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全投稿数'
+ },
+
+ inc: {
+ type: 'number' as 'number',
+ description: '増加した投稿数'
+ },
+
+ dec: {
+ type: 'number' as 'number',
+ description: '減少した投稿数'
+ },
+
+ diffs: {
+ type: 'object' as 'object',
+ properties: {
+ normal: {
+ type: 'number' as 'number',
+ description: '通常の投稿数の差分'
+ },
+
+ reply: {
+ type: 'number' as 'number',
+ description: 'リプライの投稿数の差分'
+ },
+
+ renote: {
+ type: 'number' as 'number',
+ description: 'Renoteの投稿数の差分'
+ },
+ }
+ },
+};
+
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ local: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ remote: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ }
+};
+
+export const name = 'notes';
diff --git a/src/services/chart/charts/schemas/per-user-drive.ts b/src/services/chart/charts/schemas/per-user-drive.ts
new file mode 100644
index 0000000000..713bd7ed84
--- /dev/null
+++ b/src/services/chart/charts/schemas/per-user-drive.ts
@@ -0,0 +1,54 @@
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ /**
+ * 集計期間時点での、全ドライブファイル数
+ */
+ totalCount: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全ドライブファイル数'
+ },
+
+ /**
+ * 集計期間時点での、全ドライブファイルの合計サイズ
+ */
+ totalSize: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全ドライブファイルの合計サイズ'
+ },
+
+ /**
+ * 増加したドライブファイル数
+ */
+ incCount: {
+ type: 'number' as 'number',
+ description: '増加したドライブファイル数'
+ },
+
+ /**
+ * 増加したドライブ使用量
+ */
+ incSize: {
+ type: 'number' as 'number',
+ description: '増加したドライブ使用量'
+ },
+
+ /**
+ * 減少したドライブファイル数
+ */
+ decCount: {
+ type: 'number' as 'number',
+ description: '減少したドライブファイル数'
+ },
+
+ /**
+ * 減少したドライブ使用量
+ */
+ decSize: {
+ type: 'number' as 'number',
+ description: '減少したドライブ使用量'
+ },
+ }
+};
+
+export const name = 'perUserDrive';
diff --git a/src/services/chart/charts/schemas/per-user-following.ts b/src/services/chart/charts/schemas/per-user-following.ts
new file mode 100644
index 0000000000..d6ca1130e0
--- /dev/null
+++ b/src/services/chart/charts/schemas/per-user-following.ts
@@ -0,0 +1,81 @@
+export const logSchema = {
+ /**
+ * フォローしている
+ */
+ followings: {
+ type: 'object' as 'object',
+ properties: {
+ /**
+ * フォローしている合計
+ */
+ total: {
+ type: 'number' as 'number',
+ description: 'フォローしている合計',
+ },
+
+ /**
+ * フォローした数
+ */
+ inc: {
+ type: 'number' as 'number',
+ description: 'フォローした数',
+ },
+
+ /**
+ * フォロー解除した数
+ */
+ dec: {
+ type: 'number' as 'number',
+ description: 'フォロー解除した数',
+ },
+ }
+ },
+
+ /**
+ * フォローされている
+ */
+ followers: {
+ type: 'object' as 'object',
+ properties: {
+ /**
+ * フォローされている合計
+ */
+ total: {
+ type: 'number' as 'number',
+ description: 'フォローされている合計',
+ },
+
+ /**
+ * フォローされた数
+ */
+ inc: {
+ type: 'number' as 'number',
+ description: 'フォローされた数',
+ },
+
+ /**
+ * フォロー解除された数
+ */
+ dec: {
+ type: 'number' as 'number',
+ description: 'フォロー解除された数',
+ },
+ }
+ },
+};
+
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ local: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ remote: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ }
+};
+
+export const name = 'perUserFollowing';
diff --git a/src/services/chart/charts/schemas/per-user-notes.ts b/src/services/chart/charts/schemas/per-user-notes.ts
new file mode 100644
index 0000000000..3c448c4cee
--- /dev/null
+++ b/src/services/chart/charts/schemas/per-user-notes.ts
@@ -0,0 +1,41 @@
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ total: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全投稿数'
+ },
+
+ inc: {
+ type: 'number' as 'number',
+ description: '増加した投稿数'
+ },
+
+ dec: {
+ type: 'number' as 'number',
+ description: '減少した投稿数'
+ },
+
+ diffs: {
+ type: 'object' as 'object',
+ properties: {
+ normal: {
+ type: 'number' as 'number',
+ description: '通常の投稿数の差分'
+ },
+
+ reply: {
+ type: 'number' as 'number',
+ description: 'リプライの投稿数の差分'
+ },
+
+ renote: {
+ type: 'number' as 'number',
+ description: 'Renoteの投稿数の差分'
+ },
+ }
+ },
+ }
+};
+
+export const name = 'perUserNotes';
diff --git a/src/services/chart/charts/schemas/per-user-reactions.ts b/src/services/chart/charts/schemas/per-user-reactions.ts
new file mode 100644
index 0000000000..1278184da6
--- /dev/null
+++ b/src/services/chart/charts/schemas/per-user-reactions.ts
@@ -0,0 +1,28 @@
+export const logSchema = {
+ /**
+ * フォローしている合計
+ */
+ count: {
+ type: 'number' as 'number',
+ description: 'リアクションされた数',
+ },
+};
+
+/**
+ * ユーザーごとのリアクションに関するチャート
+ */
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ local: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ remote: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ }
+};
+
+export const name = 'perUserReaction';
diff --git a/src/services/chart/charts/schemas/test-grouped.ts b/src/services/chart/charts/schemas/test-grouped.ts
new file mode 100644
index 0000000000..acf3fddb31
--- /dev/null
+++ b/src/services/chart/charts/schemas/test-grouped.ts
@@ -0,0 +1,26 @@
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ foo: {
+ type: 'object' as 'object',
+ properties: {
+ total: {
+ type: 'number' as 'number',
+ description: ''
+ },
+
+ inc: {
+ type: 'number' as 'number',
+ description: ''
+ },
+
+ dec: {
+ type: 'number' as 'number',
+ description: ''
+ },
+ }
+ }
+ }
+};
+
+export const name = 'testGrouped';
diff --git a/src/services/chart/charts/schemas/test-unique.ts b/src/services/chart/charts/schemas/test-unique.ts
new file mode 100644
index 0000000000..8fcfbf3c72
--- /dev/null
+++ b/src/services/chart/charts/schemas/test-unique.ts
@@ -0,0 +1,11 @@
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ foo: {
+ type: 'number' as 'number',
+ description: ''
+ },
+ }
+};
+
+export const name = 'testUnique';
diff --git a/src/services/chart/charts/schemas/test.ts b/src/services/chart/charts/schemas/test.ts
new file mode 100644
index 0000000000..b1344500bf
--- /dev/null
+++ b/src/services/chart/charts/schemas/test.ts
@@ -0,0 +1,26 @@
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ foo: {
+ type: 'object' as 'object',
+ properties: {
+ total: {
+ type: 'number' as 'number',
+ description: ''
+ },
+
+ inc: {
+ type: 'number' as 'number',
+ description: ''
+ },
+
+ dec: {
+ type: 'number' as 'number',
+ description: ''
+ },
+ }
+ }
+ }
+};
+
+export const name = 'test';
diff --git a/src/services/chart/charts/schemas/users.ts b/src/services/chart/charts/schemas/users.ts
new file mode 100644
index 0000000000..db7e2dd057
--- /dev/null
+++ b/src/services/chart/charts/schemas/users.ts
@@ -0,0 +1,41 @@
+const logSchema = {
+ /**
+ * 集計期間時点での、全ユーザー数
+ */
+ total: {
+ type: 'number' as 'number',
+ description: '集計期間時点での、全ユーザー数'
+ },
+
+ /**
+ * 増加したユーザー数
+ */
+ inc: {
+ type: 'number' as 'number',
+ description: '増加したユーザー数'
+ },
+
+ /**
+ * 減少したユーザー数
+ */
+ dec: {
+ type: 'number' as 'number',
+ description: '減少したユーザー数'
+ },
+};
+
+export const schema = {
+ type: 'object' as 'object',
+ properties: {
+ local: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ remote: {
+ type: 'object' as 'object',
+ properties: logSchema
+ },
+ }
+};
+
+export const name = 'users';
diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts
new file mode 100644
index 0000000000..2a60b1a0a3
--- /dev/null
+++ b/src/services/chart/core.ts
@@ -0,0 +1,460 @@
+/**
+ * チャートエンジン
+ *
+ * Tests located in test/chart
+ */
+
+import * as moment from 'moment';
+import * as nestedProperty from 'nested-property';
+import autobind from 'autobind-decorator';
+import Logger from '../logger';
+import { Schema } from '../../misc/schema';
+import { EntitySchema, getRepository, Repository, LessThan, MoreThanOrEqual } from 'typeorm';
+import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
+
+const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test');
+
+const utc = moment.utc;
+
+export type Obj = { [key: string]: any };
+
+export type DeepPartial<T> = {
+ [P in keyof T]?: DeepPartial<T[P]>;
+};
+
+type ArrayValue<T> = {
+ [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>;
+};
+
+type Span = 'day' | 'hour';
+
+type Log = {
+ id: number;
+
+ /**
+ * 集計のグループ
+ */
+ group: string | null;
+
+ /**
+ * 集計日時のUnixタイムスタンプ(秒)
+ */
+ date: number;
+
+ /**
+ * 集計期間
+ */
+ span: Span;
+
+ /**
+ * ユニークインクリメント用
+ */
+ unique?: Record<string, any>;
+};
+
+const camelToSnake = (str: string) => {
+ return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase());
+};
+
+/**
+ * 様々なチャートの管理を司るクラス
+ */
+export default abstract class Chart<T extends Record<string, any>> {
+ private static readonly columnPrefix = '___';
+ private static readonly columnDot = '_';
+
+ private name: string;
+ public schema: Schema;
+ protected repository: Repository<Log>;
+ protected abstract genNewLog(latest: T): DeepPartial<T>;
+ protected abstract async fetchActual(group?: string): Promise<DeepPartial<T>>;
+
+ @autobind
+ private static convertSchemaToFlatColumnDefinitions(schema: Schema) {
+ const columns = {} as any;
+ const flatColumns = (x: Obj, path?: string) => {
+ for (const [k, v] of Object.entries(x)) {
+ const p = path ? `${path}${this.columnDot}${k}` : k;
+ if (v.type === 'object') {
+ flatColumns(v.properties, p);
+ } else {
+ columns[this.columnPrefix + p] = {
+ type: 'integer',
+ };
+ }
+ }
+ };
+ flatColumns(schema.properties);
+ return columns;
+ }
+
+ @autobind
+ private static convertFlattenColumnsToObject(x: Record<string, number>) {
+ const obj = {} as any;
+ for (const k of Object.keys(x).filter(k => k.startsWith(Chart.columnPrefix))) {
+ // now k is ___x_y_z
+ const path = k.substr(Chart.columnPrefix.length).split(Chart.columnDot).join('.');
+ nestedProperty.set(obj, path, x[k]);
+ }
+ return obj;
+ }
+
+ @autobind
+ private static convertObjectToFlattenColumns(x: Record<string, any>) {
+ const columns = {} as Record<string, number>;
+ const flatten = (x: Obj, path?: string) => {
+ for (const [k, v] of Object.entries(x)) {
+ const p = path ? `${path}${this.columnDot}${k}` : k;
+ if (typeof v === 'object') {
+ flatten(v, p);
+ } else {
+ columns[this.columnPrefix + p] = v;
+ }
+ }
+ };
+ flatten(x);
+ return columns;
+ }
+
+ @autobind
+ private static convertQuery(x: Record<string, any>) {
+ const query: Record<string, Function> = {};
+
+ const columns = Chart.convertObjectToFlattenColumns(x);
+
+ for (const [k, v] of Object.entries(columns)) {
+ if (v > 0) query[k] = () => `"${k}" + ${v}`;
+ if (v < 0) query[k] = () => `"${k}" - ${v}`;
+ }
+
+ return query;
+ }
+
+ @autobind
+ private static momentToTimestamp(x: moment.Moment): Log['date'] {
+ return x.unix();
+ }
+
+ @autobind
+ public static schemaToEntity(name: string, schema: Schema): EntitySchema {
+ return new EntitySchema({
+ name: `__chart__${camelToSnake(name)}`,
+ columns: {
+ id: {
+ type: 'integer',
+ primary: true,
+ generated: true
+ },
+ date: {
+ type: 'integer',
+ },
+ group: {
+ type: 'varchar',
+ length: 128,
+ nullable: true
+ },
+ span: {
+ type: 'enum',
+ enum: ['hour', 'day']
+ },
+ unique: {
+ type: 'jsonb',
+ default: {}
+ },
+ ...Chart.convertSchemaToFlatColumnDefinitions(schema)
+ },
+ });
+ }
+
+ constructor(name: string, schema: Schema, grouped = false) {
+ this.name = name;
+ this.schema = schema;
+ const entity = Chart.schemaToEntity(name, schema);
+
+ const keys = ['span', 'date'];
+ if (grouped) keys.push('group');
+
+ entity.options.uniques = [{
+ columns: keys
+ }];
+
+ this.repository = getRepository<Log>(entity);
+ }
+
+ @autobind
+ private getNewLog(latest?: T): T {
+ const log = latest ? this.genNewLog(latest) : {};
+ const flatColumns = (x: Obj, path?: string) => {
+ for (const [k, v] of Object.entries(x)) {
+ const p = path ? `${path}.${k}` : k;
+ if (v.type === 'object') {
+ flatColumns(v.properties, p);
+ } else {
+ if (nestedProperty.get(log, p) == null) {
+ nestedProperty.set(log, p, 0);
+ }
+ }
+ }
+ };
+ flatColumns(this.schema.properties);
+ return log as T;
+ }
+
+ @autobind
+ private getCurrentDate(): [number, number, number, number] {
+ const now = moment().utc();
+
+ const y = now.year();
+ const m = now.month();
+ const d = now.date();
+ const h = now.hour();
+
+ return [y, m, d, h];
+ }
+
+ @autobind
+ private getLatestLog(span: Span, group: string = null): Promise<Log> {
+ return this.repository.findOne({
+ group: group,
+ span: span
+ }, {
+ order: {
+ date: -1
+ }
+ });
+ }
+
+ @autobind
+ private async getCurrentLog(span: Span, group: string = null): Promise<Log> {
+ const [y, m, d, h] = this.getCurrentDate();
+
+ const current =
+ span == 'day' ? utc([y, m, d]) :
+ span == 'hour' ? utc([y, m, d, h]) :
+ null;
+
+ // 現在(今日または今のHour)のログ
+ const currentLog = await this.repository.findOne({
+ span: span,
+ date: Chart.momentToTimestamp(current),
+ ...(group ? { group: group } : {})
+ });
+
+ // ログがあればそれを返して終了
+ if (currentLog != null) {
+ return currentLog;
+ }
+
+ let log: Log;
+ let data: T;
+
+ // 集計期間が変わってから、初めてのチャート更新なら
+ // 最も最近のログを持ってくる
+ // * 例えば集計期間が「日」である場合で考えると、
+ // * 昨日何もチャートを更新するような出来事がなかった場合は、
+ // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、
+ // * 「昨日の」と決め打ちせずに「もっとも最近の」とします
+ const latest = await this.getLatestLog(span, group);
+
+ if (latest != null) {
+ const obj = Chart.convertFlattenColumnsToObject(
+ latest as Record<string, any>);
+
+ // 空ログデータを作成
+ data = await this.getNewLog(obj);
+ } else {
+ // ログが存在しなかったら
+ // (Misskeyインスタンスを建てて初めてのチャート更新時)
+
+ // 初期ログデータを作成
+ data = await this.getNewLog(null);
+
+ logger.info(`${this.name}: Initial commit created`);
+ }
+
+ try {
+ // 新規ログ挿入
+ log = await this.repository.save({
+ group: group,
+ span: span,
+ date: Chart.momentToTimestamp(current),
+ ...Chart.convertObjectToFlattenColumns(data)
+ });
+ } catch (e) {
+ // duplicate key error
+ // 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある
+ // その場合は再度最も新しいログを持ってくる
+ if (isDuplicateKeyValueError(e)) {
+ log = await this.getLatestLog(span, group);
+ } else {
+ logger.error(e);
+ throw e;
+ }
+ }
+
+ return log;
+ }
+
+ @autobind
+ protected commit(query: Record<string, Function>, group: string = null, uniqueKey?: string, uniqueValue?: string): Promise<any> {
+ const update = async (log: Log) => {
+ // ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く
+ if (
+ uniqueKey &&
+ log.unique[uniqueKey] &&
+ log.unique[uniqueKey].includes(uniqueValue)
+ ) return;
+
+ // ユニークインクリメントの指定のキーに値を追加
+ if (uniqueKey) {
+ if (log.unique[uniqueKey]) {
+ const sql = `jsonb_set("unique", '{${uniqueKey}}', ("unique"->>'${uniqueKey}')::jsonb || '["${uniqueValue}"]'::jsonb)`;
+ query['unique'] = () => sql;
+ } else {
+ const sql = `jsonb_set("unique", '{${uniqueKey}}', '["${uniqueValue}"]')`;
+ query['unique'] = () => sql;
+ }
+ }
+
+ // ログ更新
+ await this.repository.createQueryBuilder()
+ .update()
+ .set(query)
+ .where('id = :id', { id: log.id })
+ .execute();
+ };
+
+ return Promise.all([
+ this.getCurrentLog('day', group).then(log => update(log)),
+ this.getCurrentLog('hour', group).then(log => update(log)),
+ ]);
+ }
+
+ @autobind
+ protected async inc(inc: DeepPartial<T>, group: string = null): Promise<void> {
+ await this.commit(Chart.convertQuery(inc as any), group);
+ }
+
+ @autobind
+ protected async incIfUnique(inc: DeepPartial<T>, key: string, value: string, group: string = null): Promise<void> {
+ await this.commit(Chart.convertQuery(inc as any), group, key, value);
+ }
+
+ @autobind
+ public async getChart(span: Span, range: number, group: string = null): Promise<ArrayValue<T>> {
+ const [y, m, d, h] = this.getCurrentDate();
+
+ const gt =
+ span == 'day' ? utc([y, m, d]).subtract(range, 'days') :
+ span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') :
+ null;
+
+ // ログ取得
+ let logs = await this.repository.find({
+ where: {
+ group: group,
+ span: span,
+ date: MoreThanOrEqual(Chart.momentToTimestamp(gt))
+ },
+ order: {
+ date: -1
+ },
+ });
+
+ // 要求された範囲にログがひとつもなかったら
+ if (logs.length === 0) {
+ // もっとも新しいログを持ってくる
+ // (すくなくともひとつログが無いと隙間埋めできないため)
+ const recentLog = await this.repository.findOne({
+ group: group,
+ span: span
+ }, {
+ order: {
+ date: -1
+ },
+ });
+
+ if (recentLog) {
+ logs = [recentLog];
+ }
+
+ // 要求された範囲の最も古い箇所に位置するログが存在しなかったら
+ } else if (!utc(logs[logs.length - 1].date * 1000).isSame(gt)) {
+ // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
+ // (隙間埋めできないため)
+ const outdatedLog = await this.repository.findOne({
+ group: group,
+ span: span,
+ date: LessThan(Chart.momentToTimestamp(gt))
+ }, {
+ order: {
+ date: -1
+ },
+ });
+
+ if (outdatedLog) {
+ logs.push(outdatedLog);
+ }
+ }
+
+ const chart: T[] = [];
+
+ // 整形
+ for (let i = (range - 1); i >= 0; i--) {
+ const current =
+ span == 'day' ? utc([y, m, d]).subtract(i, 'days') :
+ span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') :
+ null;
+
+ const log = logs.find(l => utc(l.date * 1000).isSame(current));
+
+ if (log) {
+ const data = Chart.convertFlattenColumnsToObject(log as Record<string, any>);
+ chart.unshift(data);
+ } else {
+ // 隙間埋め
+ const latest = logs.find(l => utc(l.date * 1000).isBefore(current));
+ const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null;
+ chart.unshift(this.getNewLog(data));
+ }
+ }
+
+ const res: ArrayValue<T> = {} as any;
+
+ /**
+ * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }]
+ * を
+ * { foo: [1, 2, 3], bar: [5, 6, 7] }
+ * にする
+ */
+ const dive = (x: Obj, path?: string) => {
+ for (const [k, v] of Object.entries(x)) {
+ const p = path ? `${path}.${k}` : k;
+ if (typeof v == 'object') {
+ dive(v, p);
+ } else {
+ nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p)));
+ }
+ }
+ };
+
+ dive(chart[0]);
+
+ return res;
+ }
+}
+
+export function convertLog(logSchema: Schema): Schema {
+ const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy
+ if (v.type === 'number') {
+ v.type = 'array';
+ v.items = {
+ type: 'number'
+ };
+ } else if (v.type === 'object') {
+ for (const k of Object.keys(v.properties)) {
+ v.properties[k] = convertLog(v.properties[k]);
+ }
+ }
+ return v;
+}
diff --git a/src/services/chart/drive.ts b/src/services/chart/drive.ts
deleted file mode 100644
index dd23412c7d..0000000000
--- a/src/services/chart/drive.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import DriveFile, { IDriveFile } from '../../models/drive-file';
-import { isLocalUser } from '../../models/user';
-import { SchemaType } from '../../misc/schema';
-
-const logSchema = {
- /**
- * 集計期間時点での、全ドライブファイル数
- */
- totalCount: {
- type: 'number' as 'number',
- description: '集計期間時点での、全ドライブファイル数'
- },
-
- /**
- * 集計期間時点での、全ドライブファイルの合計サイズ
- */
- totalSize: {
- type: 'number' as 'number',
- description: '集計期間時点での、全ドライブファイルの合計サイズ'
- },
-
- /**
- * 増加したドライブファイル数
- */
- incCount: {
- type: 'number' as 'number',
- description: '増加したドライブファイル数'
- },
-
- /**
- * 増加したドライブ使用量
- */
- incSize: {
- type: 'number' as 'number',
- description: '増加したドライブ使用量'
- },
-
- /**
- * 減少したドライブファイル数
- */
- decCount: {
- type: 'number' as 'number',
- description: '減少したドライブファイル数'
- },
-
- /**
- * 減少したドライブ使用量
- */
- decSize: {
- type: 'number' as 'number',
- description: '減少したドライブ使用量'
- },
-};
-
-export const driveLogSchema = {
- type: 'object' as 'object',
- properties: {
- local: {
- type: 'object' as 'object',
- properties: logSchema
- },
- remote: {
- type: 'object' as 'object',
- properties: logSchema
- },
- }
-};
-
-type DriveLog = SchemaType<typeof driveLogSchema>;
-
-class DriveChart extends Chart<DriveLog> {
- constructor() {
- super('drive');
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: DriveLog): Promise<DriveLog> {
- const calcSize = (local: boolean) => DriveFile
- .aggregate([{
- $match: {
- 'metadata._user.host': local ? null : { $ne: null },
- 'metadata.deletedAt': { $exists: false }
- }
- }, {
- $project: {
- length: true
- }
- }, {
- $group: {
- _id: null,
- usage: { $sum: '$length' }
- }
- }])
- .then(res => res.length > 0 ? res[0].usage : 0);
-
- const [localCount, remoteCount, localSize, remoteSize] = init ? await Promise.all([
- DriveFile.count({ 'metadata._user.host': null }),
- DriveFile.count({ 'metadata._user.host': { $ne: null } }),
- calcSize(true),
- calcSize(false)
- ]) : [
- latest ? latest.local.totalCount : 0,
- latest ? latest.remote.totalCount : 0,
- latest ? latest.local.totalSize : 0,
- latest ? latest.remote.totalSize : 0
- ];
-
- return {
- local: {
- totalCount: localCount,
- totalSize: localSize,
- incCount: 0,
- incSize: 0,
- decCount: 0,
- decSize: 0
- },
- remote: {
- totalCount: remoteCount,
- totalSize: remoteSize,
- incCount: 0,
- incSize: 0,
- decCount: 0,
- decSize: 0
- }
- };
- }
-
- @autobind
- public async update(file: IDriveFile, isAdditional: boolean) {
- const update: Obj = {};
-
- update.totalCount = isAdditional ? 1 : -1;
- update.totalSize = isAdditional ? file.length : -file.length;
- if (isAdditional) {
- update.incCount = 1;
- update.incSize = file.length;
- } else {
- update.decCount = 1;
- update.decSize = file.length;
- }
-
- await this.inc({
- [isLocalUser(file.metadata._user) ? 'local' : 'remote']: update
- });
- }
-}
-
-export default new DriveChart();
diff --git a/src/services/chart/entities.ts b/src/services/chart/entities.ts
new file mode 100644
index 0000000000..14fd3adba0
--- /dev/null
+++ b/src/services/chart/entities.ts
@@ -0,0 +1,8 @@
+import Chart from './core';
+
+export const entities = Object.values(require('require-all')({
+ dirname: __dirname + '/charts/schemas',
+ resolve: (x: any) => {
+ return Chart.schemaToEntity(x.name, x.schema);
+ }
+}));
diff --git a/src/services/chart/federation.ts b/src/services/chart/federation.ts
deleted file mode 100644
index 20da7a7421..0000000000
--- a/src/services/chart/federation.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from '.';
-import Instance from '../../models/instance';
-
-/**
- * フェデレーションに関するチャート
- */
-type FederationLog = {
- instance: {
- /**
- * インスタンス数の合計
- */
- total: number;
-
- /**
- * 増加インスタンス数
- */
- inc: number;
-
- /**
- * 減少インスタンス数
- */
- dec: number;
- };
-};
-
-class FederationChart extends Chart<FederationLog> {
- constructor() {
- super('federation');
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: FederationLog): Promise<FederationLog> {
- const [total] = init ? await Promise.all([
- Instance.count({})
- ]) : [
- latest ? latest.instance.total : 0
- ];
-
- return {
- instance: {
- total: total,
- inc: 0,
- dec: 0
- }
- };
- }
-
- @autobind
- public async update(isAdditional: boolean) {
- const update: Obj = {};
-
- update.total = isAdditional ? 1 : -1;
- if (isAdditional) {
- update.inc = 1;
- } else {
- update.dec = 1;
- }
-
- await this.inc({
- instance: update
- });
- }
-}
-
-export default new FederationChart();
diff --git a/src/services/chart/hashtag.ts b/src/services/chart/hashtag.ts
deleted file mode 100644
index 7a31e9cced..0000000000
--- a/src/services/chart/hashtag.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import { IUser, isLocalUser } from '../../models/user';
-import db from '../../db/mongodb';
-
-/**
- * ハッシュタグに関するチャート
- */
-type HashtagLog = {
- local: {
- /**
- * 投稿された数
- */
- count: number;
- };
-
- remote: HashtagLog['local'];
-};
-
-class HashtagChart extends Chart<HashtagLog> {
- constructor() {
- super('hashtag', true);
-
- // 後方互換性のため
- db.get('chart.hashtag').findOne().then(doc => {
- if (doc != null && doc.data.local == null) {
- db.get('chart.hashtag').drop();
- }
- });
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: HashtagLog): Promise<HashtagLog> {
- return {
- local: {
- count: 0
- },
- remote: {
- count: 0
- }
- };
- }
-
- @autobind
- public async update(hashtag: string, user: IUser) {
- const update: Obj = {
- count: 1
- };
-
- await this.incIfUnique({
- [isLocalUser(user) ? 'local' : 'remote']: update
- }, 'users', user._id.toHexString(), hashtag);
- }
-}
-
-export default new HashtagChart();
diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts
index 7a6470f4d8..9626e3d6b3 100644
--- a/src/services/chart/index.ts
+++ b/src/services/chart/index.ts
@@ -1,364 +1,25 @@
-/**
- * チャートエンジン
- */
+import FederationChart from './charts/classes/federation';
+import NotesChart from './charts/classes/notes';
+import UsersChart from './charts/classes/users';
+import NetworkChart from './charts/classes/network';
+import ActiveUsersChart from './charts/classes/active-users';
+import InstanceChart from './charts/classes/instance';
+import PerUserNotesChart from './charts/classes/per-user-notes';
+import DriveChart from './charts/classes/drive';
+import PerUserReactionsChart from './charts/classes/per-user-reactions';
+import HashtagChart from './charts/classes/hashtag';
+import PerUserFollowingChart from './charts/classes/per-user-following';
+import PerUserDriveChart from './charts/classes/per-user-drive';
-import * as moment from 'moment';
-import * as nestedProperty from 'nested-property';
-import autobind from 'autobind-decorator';
-import * as mongo from 'mongodb';
-import db from '../../db/mongodb';
-import { ICollection } from 'monk';
-import Logger from '../logger';
-import { Schema } from '../../misc/schema';
-
-const logger = new Logger('chart');
-
-const utc = moment.utc;
-
-export type Obj = { [key: string]: any };
-
-export type Partial<T> = {
- [P in keyof T]?: Partial<T[P]>;
-};
-
-type ArrayValue<T> = {
- [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>;
-};
-
-type Span = 'day' | 'hour';
-
-type Log<T extends Obj> = {
- _id: mongo.ObjectID;
-
- /**
- * 集計のグループ
- */
- group?: any;
-
- /**
- * 集計日時
- */
- date: Date;
-
- /**
- * 集計期間
- */
- span: Span;
-
- /**
- * データ
- */
- data: T;
-
- /**
- * ユニークインクリメント用
- */
- unique?: Obj;
-};
-
-/**
- * 様々なチャートの管理を司るクラス
- */
-export default abstract class Chart<T extends Obj> {
- protected collection: ICollection<Log<T>>;
- protected abstract async getTemplate(init: boolean, latest?: T, group?: any): Promise<T>;
- private name: string;
-
- constructor(name: string, grouped = false) {
- this.name = name;
- this.collection = db.get<Log<T>>(`chart.${name}`);
-
- const keys = {
- span: -1,
- date: -1
- } as { [key: string]: 1 | -1; };
- if (grouped) keys.group = -1;
-
- this.collection.createIndex(keys, { unique: true });
- }
-
- @autobind
- private convertQuery(x: Obj, path: string): Obj {
- const query: Obj = {};
-
- const dive = (x: Obj, path: string) => {
- for (const [k, v] of Object.entries(x)) {
- const p = path ? `${path}.${k}` : k;
- if (typeof v === 'number') {
- query[p] = v;
- } else {
- dive(v, p);
- }
- }
- };
-
- dive(x, path);
-
- return query;
- }
-
- @autobind
- private getCurrentDate(): [number, number, number, number] {
- const now = moment().utc();
-
- const y = now.year();
- const m = now.month();
- const d = now.date();
- const h = now.hour();
-
- return [y, m, d, h];
- }
-
- @autobind
- private getLatestLog(span: Span, group?: any): Promise<Log<T>> {
- return this.collection.findOne({
- group: group,
- span: span
- }, {
- sort: {
- date: -1
- }
- });
- }
-
- @autobind
- private async getCurrentLog(span: Span, group?: any): Promise<Log<T>> {
- const [y, m, d, h] = this.getCurrentDate();
-
- const current =
- span == 'day' ? utc([y, m, d]) :
- span == 'hour' ? utc([y, m, d, h]) :
- null;
-
- // 現在(今日または今のHour)のログ
- const currentLog = await this.collection.findOne({
- group: group,
- span: span,
- date: current.toDate()
- });
-
- // ログがあればそれを返して終了
- if (currentLog != null) {
- return currentLog;
- }
-
- let log: Log<T>;
- let data: T;
-
- // 集計期間が変わってから、初めてのチャート更新なら
- // 最も最近のログを持ってくる
- // * 例えば集計期間が「日」である場合で考えると、
- // * 昨日何もチャートを更新するような出来事がなかった場合は、
- // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、
- // * 「昨日の」と決め打ちせずに「もっとも最近の」とします
- const latest = await this.getLatestLog(span, group);
-
- if (latest != null) {
- // 空ログデータを作成
- data = await this.getTemplate(false, latest.data);
- } else {
- // ログが存在しなかったら
- // (Misskeyインスタンスを建てて初めてのチャート更新時など
- // または何らかの理由でチャートコレクションを抹消した場合)
-
- // 初期ログデータを作成
- data = await this.getTemplate(true, null, group);
-
- logger.info(`${this.name}: Initial commit created`);
- }
-
- try {
- // 新規ログ挿入
- log = await this.collection.insert({
- group: group,
- span: span,
- date: current.toDate(),
- data: data
- });
- } catch (e) {
- // 11000 is duplicate key error
- // 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある
- // その場合は再度最も新しいログを持ってくる
- if (e.code === 11000) {
- log = await this.getLatestLog(span, group);
- } else {
- logger.error(e);
- throw e;
- }
- }
-
- return log;
- }
-
- @autobind
- protected commit(query: Obj, group?: any, uniqueKey?: string, uniqueValue?: string): void {
- const update = (log: Log<T>) => {
- // ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く
- if (
- uniqueKey &&
- log.unique &&
- log.unique[uniqueKey] &&
- log.unique[uniqueKey].includes(uniqueValue)
- ) return;
-
- // ユニークインクリメントの指定のキーに値を追加
- if (uniqueKey) {
- query['$push'] = {
- [`unique.${uniqueKey}`]: uniqueValue
- };
- }
-
- // ログ更新
- this.collection.update({
- _id: log._id
- }, query);
- };
-
- this.getCurrentLog('day', group).then(log => update(log));
- this.getCurrentLog('hour', group).then(log => update(log));
- }
-
- @autobind
- protected inc(inc: Partial<T>, group?: any): void {
- this.commit({
- $inc: this.convertQuery(inc, 'data')
- }, group);
- }
-
- @autobind
- protected incIfUnique(inc: Partial<T>, key: string, value: string, group?: any): void {
- this.commit({
- $inc: this.convertQuery(inc, 'data')
- }, group, key, value);
- }
-
- @autobind
- public async getChart(span: Span, range: number, group?: any): Promise<ArrayValue<T>> {
- const promisedChart: Promise<T>[] = [];
-
- const [y, m, d, h] = this.getCurrentDate();
-
- const gt =
- span == 'day' ? utc([y, m, d]).subtract(range, 'days') :
- span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') :
- null;
-
- // ログ取得
- let logs = await this.collection.find({
- group: group,
- span: span,
- date: {
- $gte: gt.toDate()
- }
- }, {
- sort: {
- date: -1
- },
- fields: {
- _id: 0
- }
- });
-
- // 要求された範囲にログがひとつもなかったら
- if (logs.length == 0) {
- // もっとも新しいログを持ってくる
- // (すくなくともひとつログが無いと隙間埋めできないため)
- const recentLog = await this.collection.findOne({
- group: group,
- span: span
- }, {
- sort: {
- date: -1
- },
- fields: {
- _id: 0
- }
- });
-
- if (recentLog) {
- logs = [recentLog];
- }
-
- // 要求された範囲の最も古い箇所に位置するログが存在しなかったら
- } else if (!utc(logs[logs.length - 1].date).isSame(gt)) {
- // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
- // (隙間埋めできないため)
- const outdatedLog = await this.collection.findOne({
- group: group,
- span: span,
- date: {
- $lt: gt.toDate()
- }
- }, {
- sort: {
- date: -1
- },
- fields: {
- _id: 0
- }
- });
-
- if (outdatedLog) {
- logs.push(outdatedLog);
- }
- }
-
- // 整形
- for (let i = (range - 1); i >= 0; i--) {
- const current =
- span == 'day' ? utc([y, m, d]).subtract(i, 'days') :
- span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') :
- null;
-
- const log = logs.find(l => utc(l.date).isSame(current));
-
- if (log) {
- promisedChart.unshift(Promise.resolve(log.data));
- } else {
- // 隙間埋め
- const latest = logs.find(l => utc(l.date).isBefore(current));
- promisedChart.unshift(this.getTemplate(false, latest ? latest.data : null));
- }
- }
-
- const chart = await Promise.all(promisedChart);
-
- const res: ArrayValue<T> = {} as any;
-
- /**
- * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }]
- * を
- * { foo: [1, 2, 3], bar: [5, 6, 7] }
- * にする
- */
- const dive = (x: Obj, path?: string) => {
- for (const [k, v] of Object.entries(x)) {
- const p = path ? `${path}.${k}` : k;
- if (typeof v == 'object') {
- dive(v, p);
- } else {
- nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p)));
- }
- }
- };
-
- dive(chart[0]);
-
- return res;
- }
-}
-
-export function convertLog(logSchema: Schema): Schema {
- const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy
- if (v.type === 'number') {
- v.type = 'array';
- v.items = {
- type: 'number'
- };
- } else if (v.type === 'object') {
- for (const k of Object.keys(v.properties)) {
- v.properties[k] = convertLog(v.properties[k]);
- }
- }
- return v;
-}
+export const federationChart = new FederationChart();
+export const notesChart = new NotesChart();
+export const usersChart = new UsersChart();
+export const networkChart = new NetworkChart();
+export const activeUsersChart = new ActiveUsersChart();
+export const instanceChart = new InstanceChart();
+export const perUserNotesChart = new PerUserNotesChart();
+export const driveChart = new DriveChart();
+export const perUserReactionsChart = new PerUserReactionsChart();
+export const hashtagChart = new HashtagChart();
+export const perUserFollowingChart = new PerUserFollowingChart();
+export const perUserDriveChart = new PerUserDriveChart();
diff --git a/src/services/chart/instance.ts b/src/services/chart/instance.ts
deleted file mode 100644
index 5af398b902..0000000000
--- a/src/services/chart/instance.ts
+++ /dev/null
@@ -1,302 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from '.';
-import User from '../../models/user';
-import Note from '../../models/note';
-import Following from '../../models/following';
-import DriveFile, { IDriveFile } from '../../models/drive-file';
-
-/**
- * インスタンスごとのチャート
- */
-type InstanceLog = {
- requests: {
- /**
- * 失敗したリクエスト数
- */
- failed: number;
-
- /**
- * 成功したリクエスト数
- */
- succeeded: number;
-
- /**
- * 受信したリクエスト数
- */
- received: number;
- };
-
- notes: {
- /**
- * 集計期間時点での、全投稿数
- */
- total: number;
-
- /**
- * 増加した投稿数
- */
- inc: number;
-
- /**
- * 減少した投稿数
- */
- dec: number;
- };
-
- users: {
- /**
- * 集計期間時点での、全ユーザー数
- */
- total: number;
-
- /**
- * 増加したユーザー数
- */
- inc: number;
-
- /**
- * 減少したユーザー数
- */
- dec: number;
- };
-
- following: {
- /**
- * 集計期間時点での、全フォロー数
- */
- total: number;
-
- /**
- * 増加したフォロー数
- */
- inc: number;
-
- /**
- * 減少したフォロー数
- */
- dec: number;
- };
-
- followers: {
- /**
- * 集計期間時点での、全フォロワー数
- */
- total: number;
-
- /**
- * 増加したフォロワー数
- */
- inc: number;
-
- /**
- * 減少したフォロワー数
- */
- dec: number;
- };
-
- drive: {
- /**
- * 集計期間時点での、全ドライブファイル数
- */
- totalFiles: number;
-
- /**
- * 集計期間時点での、全ドライブファイルの合計サイズ
- */
- totalUsage: number;
-
- /**
- * 増加したドライブファイル数
- */
- incFiles: number;
-
- /**
- * 増加したドライブ使用量
- */
- incUsage: number;
-
- /**
- * 減少したドライブファイル数
- */
- decFiles: number;
-
- /**
- * 減少したドライブ使用量
- */
- decUsage: number;
- };
-};
-
-class InstanceChart extends Chart<InstanceLog> {
- constructor() {
- super('instance', true);
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: InstanceLog, group?: any): Promise<InstanceLog> {
- const calcUsage = () => DriveFile
- .aggregate([{
- $match: {
- 'metadata._user.host': group,
- 'metadata.deletedAt': { $exists: false }
- }
- }, {
- $project: {
- length: true
- }
- }, {
- $group: {
- _id: null,
- usage: { $sum: '$length' }
- }
- }])
- .then(res => res.length > 0 ? res[0].usage : 0);
-
- const [
- notesCount,
- usersCount,
- followingCount,
- followersCount,
- driveFiles,
- driveUsage,
- ] = init ? await Promise.all([
- Note.count({ '_user.host': group }),
- User.count({ host: group }),
- Following.count({ '_follower.host': group }),
- Following.count({ '_followee.host': group }),
- DriveFile.count({ 'metadata._user.host': group }),
- calcUsage(),
- ]) : [
- latest ? latest.notes.total : 0,
- latest ? latest.users.total : 0,
- latest ? latest.following.total : 0,
- latest ? latest.followers.total : 0,
- latest ? latest.drive.totalFiles : 0,
- latest ? latest.drive.totalUsage : 0,
- ];
-
- return {
- requests: {
- failed: 0,
- succeeded: 0,
- received: 0
- },
- notes: {
- total: notesCount,
- inc: 0,
- dec: 0
- },
- users: {
- total: usersCount,
- inc: 0,
- dec: 0
- },
- following: {
- total: followingCount,
- inc: 0,
- dec: 0
- },
- followers: {
- total: followersCount,
- inc: 0,
- dec: 0
- },
- drive: {
- totalFiles: driveFiles,
- totalUsage: driveUsage,
- incFiles: 0,
- incUsage: 0,
- decFiles: 0,
- decUsage: 0
- }
- };
- }
-
- @autobind
- public async requestReceived(host: string) {
- await this.inc({
- requests: {
- received: 1
- }
- }, host);
- }
-
- @autobind
- public async requestSent(host: string, isSucceeded: boolean) {
- const update: Obj = {};
-
- if (isSucceeded) {
- update.succeeded = 1;
- } else {
- update.failed = 1;
- }
-
- await this.inc({
- requests: update
- }, host);
- }
-
- @autobind
- public async newUser(host: string) {
- await this.inc({
- users: {
- total: 1,
- inc: 1
- }
- }, host);
- }
-
- @autobind
- public async updateNote(host: string, isAdditional: boolean) {
- await this.inc({
- notes: {
- total: isAdditional ? 1 : -1,
- inc: isAdditional ? 1 : 0,
- dec: isAdditional ? 0 : 1,
- }
- }, host);
- }
-
- @autobind
- public async updateFollowing(host: string, isAdditional: boolean) {
- await this.inc({
- following: {
- total: isAdditional ? 1 : -1,
- inc: isAdditional ? 1 : 0,
- dec: isAdditional ? 0 : 1,
- }
- }, host);
- }
-
- @autobind
- public async updateFollowers(host: string, isAdditional: boolean) {
- await this.inc({
- followers: {
- total: isAdditional ? 1 : -1,
- inc: isAdditional ? 1 : 0,
- dec: isAdditional ? 0 : 1,
- }
- }, host);
- }
-
- @autobind
- public async updateDrive(file: IDriveFile, isAdditional: boolean) {
- const update: Obj = {};
-
- update.totalFiles = isAdditional ? 1 : -1;
- update.totalUsage = isAdditional ? file.length : -file.length;
- if (isAdditional) {
- update.incFiles = 1;
- update.incUsage = file.length;
- } else {
- update.decFiles = 1;
- update.decUsage = file.length;
- }
-
- await this.inc({
- drive: update
- }, file.metadata._user.host);
- }
-}
-
-export default new InstanceChart();
diff --git a/src/services/chart/network.ts b/src/services/chart/network.ts
deleted file mode 100644
index fce47099d1..0000000000
--- a/src/services/chart/network.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Partial } from './';
-
-/**
- * ネットワークに関するチャート
- */
-type NetworkLog = {
- /**
- * 受信したリクエスト数
- */
- incomingRequests: number;
-
- /**
- * 送信したリクエスト数
- */
- outgoingRequests: number;
-
- /**
- * 応答時間の合計
- * TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる
- */
- totalTime: number;
-
- /**
- * 合計受信データ量
- */
- incomingBytes: number;
-
- /**
- * 合計送信データ量
- */
- outgoingBytes: number;
-};
-
-class NetworkChart extends Chart<NetworkLog> {
- constructor() {
- super('network');
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: NetworkLog): Promise<NetworkLog> {
- return {
- incomingRequests: 0,
- outgoingRequests: 0,
- totalTime: 0,
- incomingBytes: 0,
- outgoingBytes: 0
- };
- }
-
- @autobind
- public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) {
- const inc: Partial<NetworkLog> = {
- incomingRequests: incomingRequests,
- totalTime: time,
- incomingBytes: incomingBytes,
- outgoingBytes: outgoingBytes
- };
-
- await this.inc(inc);
- }
-}
-
-export default new NetworkChart();
diff --git a/src/services/chart/notes.ts b/src/services/chart/notes.ts
deleted file mode 100644
index b047ec273f..0000000000
--- a/src/services/chart/notes.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from '.';
-import Note, { INote } from '../../models/note';
-import { isLocalUser } from '../../models/user';
-import { SchemaType } from '../../misc/schema';
-
-const logSchema = {
- total: {
- type: 'number' as 'number',
- description: '集計期間時点での、全投稿数'
- },
-
- inc: {
- type: 'number' as 'number',
- description: '増加した投稿数'
- },
-
- dec: {
- type: 'number' as 'number',
- description: '減少した投稿数'
- },
-
- diffs: {
- type: 'object' as 'object',
- properties: {
- normal: {
- type: 'number' as 'number',
- description: '通常の投稿数の差分'
- },
-
- reply: {
- type: 'number' as 'number',
- description: 'リプライの投稿数の差分'
- },
-
- renote: {
- type: 'number' as 'number',
- description: 'Renoteの投稿数の差分'
- },
- }
- },
-};
-
-export const notesLogSchema = {
- type: 'object' as 'object',
- properties: {
- local: {
- type: 'object' as 'object',
- properties: logSchema
- },
- remote: {
- type: 'object' as 'object',
- properties: logSchema
- },
- }
-};
-
-type NotesLog = SchemaType<typeof notesLogSchema>;
-
-class NotesChart extends Chart<NotesLog> {
- constructor() {
- super('notes');
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: NotesLog): Promise<NotesLog> {
- const [localCount, remoteCount] = init ? await Promise.all([
- Note.count({ '_user.host': null }),
- Note.count({ '_user.host': { $ne: null } })
- ]) : [
- latest ? latest.local.total : 0,
- latest ? latest.remote.total : 0
- ];
-
- return {
- local: {
- total: localCount,
- inc: 0,
- dec: 0,
- diffs: {
- normal: 0,
- reply: 0,
- renote: 0
- }
- },
- remote: {
- total: remoteCount,
- inc: 0,
- dec: 0,
- diffs: {
- normal: 0,
- reply: 0,
- renote: 0
- }
- }
- };
- }
-
- @autobind
- public async update(note: INote, isAdditional: boolean) {
- const update: Obj = {
- diffs: {}
- };
-
- update.total = isAdditional ? 1 : -1;
-
- if (isAdditional) {
- update.inc = 1;
- } else {
- update.dec = 1;
- }
-
- if (note.replyId != null) {
- update.diffs.reply = isAdditional ? 1 : -1;
- } else if (note.renoteId != null) {
- update.diffs.renote = isAdditional ? 1 : -1;
- } else {
- update.diffs.normal = isAdditional ? 1 : -1;
- }
-
- await this.inc({
- [isLocalUser(note._user) ? 'local' : 'remote']: update
- });
- }
-}
-
-export default new NotesChart();
diff --git a/src/services/chart/per-user-drive.ts b/src/services/chart/per-user-drive.ts
deleted file mode 100644
index 4f335f1688..0000000000
--- a/src/services/chart/per-user-drive.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import DriveFile, { IDriveFile } from '../../models/drive-file';
-import { SchemaType } from '../../misc/schema';
-
-export const perUserDriveLogSchema = {
- type: 'object' as 'object',
- properties: {
- /**
- * 集計期間時点での、全ドライブファイル数
- */
- totalCount: {
- type: 'number' as 'number',
- description: '集計期間時点での、全ドライブファイル数'
- },
-
- /**
- * 集計期間時点での、全ドライブファイルの合計サイズ
- */
- totalSize: {
- type: 'number' as 'number',
- description: '集計期間時点での、全ドライブファイルの合計サイズ'
- },
-
- /**
- * 増加したドライブファイル数
- */
- incCount: {
- type: 'number' as 'number',
- description: '増加したドライブファイル数'
- },
-
- /**
- * 増加したドライブ使用量
- */
- incSize: {
- type: 'number' as 'number',
- description: '増加したドライブ使用量'
- },
-
- /**
- * 減少したドライブファイル数
- */
- decCount: {
- type: 'number' as 'number',
- description: '減少したドライブファイル数'
- },
-
- /**
- * 減少したドライブ使用量
- */
- decSize: {
- type: 'number' as 'number',
- description: '減少したドライブ使用量'
- },
- }
-};
-
-type PerUserDriveLog = SchemaType<typeof perUserDriveLogSchema>;
-
-class PerUserDriveChart extends Chart<PerUserDriveLog> {
- constructor() {
- super('perUserDrive', true);
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: PerUserDriveLog, group?: any): Promise<PerUserDriveLog> {
- const calcSize = () => DriveFile
- .aggregate([{
- $match: {
- 'metadata.userId': group,
- 'metadata.deletedAt': { $exists: false }
- }
- }, {
- $project: {
- length: true
- }
- }, {
- $group: {
- _id: null,
- usage: { $sum: '$length' }
- }
- }])
- .then(res => res.length > 0 ? res[0].usage : 0);
-
- const [count, size] = init ? await Promise.all([
- DriveFile.count({ 'metadata.userId': group }),
- calcSize()
- ]) : [
- latest ? latest.totalCount : 0,
- latest ? latest.totalSize : 0
- ];
-
- return {
- totalCount: count,
- totalSize: size,
- incCount: 0,
- incSize: 0,
- decCount: 0,
- decSize: 0
- };
- }
-
- @autobind
- public async update(file: IDriveFile, isAdditional: boolean) {
- const update: Obj = {};
-
- update.totalCount = isAdditional ? 1 : -1;
- update.totalSize = isAdditional ? file.length : -file.length;
- if (isAdditional) {
- update.incCount = 1;
- update.incSize = file.length;
- } else {
- update.decCount = 1;
- update.decSize = file.length;
- }
-
- await this.inc(update, file.metadata.userId);
- }
-}
-
-export default new PerUserDriveChart();
diff --git a/src/services/chart/per-user-following.ts b/src/services/chart/per-user-following.ts
deleted file mode 100644
index 8a94a4f155..0000000000
--- a/src/services/chart/per-user-following.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import Following from '../../models/following';
-import { IUser, isLocalUser } from '../../models/user';
-import { SchemaType } from '../../misc/schema';
-
-export const logSchema = {
- /**
- * フォローしている
- */
- followings: {
- type: 'object' as 'object',
- properties: {
- /**
- * フォローしている合計
- */
- total: {
- type: 'number',
- description: 'フォローしている合計',
- },
-
- /**
- * フォローした数
- */
- inc: {
- type: 'number',
- description: 'フォローした数',
- },
-
- /**
- * フォロー解除した数
- */
- dec: {
- type: 'number',
- description: 'フォロー解除した数',
- },
- }
- },
-
- /**
- * フォローされている
- */
- followers: {
- type: 'object' as 'object',
- properties: {
- /**
- * フォローされている合計
- */
- total: {
- type: 'number',
- description: 'フォローされている合計',
- },
-
- /**
- * フォローされた数
- */
- inc: {
- type: 'number',
- description: 'フォローされた数',
- },
-
- /**
- * フォロー解除された数
- */
- dec: {
- type: 'number',
- description: 'フォロー解除された数',
- },
- }
- },
-};
-
-export const perUserFollowingLogSchema = {
- type: 'object' as 'object',
- properties: {
- local: {
- type: 'object' as 'object',
- properties: logSchema
- },
- remote: {
- type: 'object' as 'object',
- properties: logSchema
- },
- }
-};
-
-type PerUserFollowingLog = SchemaType<typeof perUserFollowingLogSchema>;
-
-class PerUserFollowingChart extends Chart<PerUserFollowingLog> {
- constructor() {
- super('perUserFollowing', true);
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: PerUserFollowingLog, group?: any): Promise<PerUserFollowingLog> {
- const [
- localFollowingsCount,
- localFollowersCount,
- remoteFollowingsCount,
- remoteFollowersCount
- ] = init ? await Promise.all([
- Following.count({ followerId: group, '_followee.host': null }),
- Following.count({ followeeId: group, '_follower.host': null }),
- Following.count({ followerId: group, '_followee.host': { $ne: null } }),
- Following.count({ followeeId: group, '_follower.host': { $ne: null } })
- ]) : [
- latest ? latest.local.followings.total : 0,
- latest ? latest.local.followers.total : 0,
- latest ? latest.remote.followings.total : 0,
- latest ? latest.remote.followers.total : 0
- ];
-
- return {
- local: {
- followings: {
- total: localFollowingsCount,
- inc: 0,
- dec: 0
- },
- followers: {
- total: localFollowersCount,
- inc: 0,
- dec: 0
- }
- },
- remote: {
- followings: {
- total: remoteFollowingsCount,
- inc: 0,
- dec: 0
- },
- followers: {
- total: remoteFollowersCount,
- inc: 0,
- dec: 0
- }
- }
- };
- }
-
- @autobind
- public async update(follower: IUser, followee: IUser, isFollow: boolean) {
- const update: Obj = {};
-
- update.total = isFollow ? 1 : -1;
-
- if (isFollow) {
- update.inc = 1;
- } else {
- update.dec = 1;
- }
-
- this.inc({
- [isLocalUser(follower) ? 'local' : 'remote']: { followings: update }
- }, follower._id);
- this.inc({
- [isLocalUser(followee) ? 'local' : 'remote']: { followers: update }
- }, followee._id);
- }
-}
-
-export default new PerUserFollowingChart();
diff --git a/src/services/chart/per-user-notes.ts b/src/services/chart/per-user-notes.ts
deleted file mode 100644
index 2f4f882091..0000000000
--- a/src/services/chart/per-user-notes.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import Note, { INote } from '../../models/note';
-import { IUser } from '../../models/user';
-import { SchemaType } from '../../misc/schema';
-
-export const perUserNotesLogSchema = {
- type: 'object' as 'object',
- properties: {
- total: {
- type: 'number' as 'number',
- description: '集計期間時点での、全投稿数'
- },
-
- inc: {
- type: 'number' as 'number',
- description: '増加した投稿数'
- },
-
- dec: {
- type: 'number' as 'number',
- description: '減少した投稿数'
- },
-
- diffs: {
- type: 'object' as 'object',
- properties: {
- normal: {
- type: 'number' as 'number',
- description: '通常の投稿数の差分'
- },
-
- reply: {
- type: 'number' as 'number',
- description: 'リプライの投稿数の差分'
- },
-
- renote: {
- type: 'number' as 'number',
- description: 'Renoteの投稿数の差分'
- },
- }
- },
- }
-};
-
-type PerUserNotesLog = SchemaType<typeof perUserNotesLogSchema>;
-
-class PerUserNotesChart extends Chart<PerUserNotesLog> {
- constructor() {
- super('perUserNotes', true);
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: PerUserNotesLog, group?: any): Promise<PerUserNotesLog> {
- const [count] = init ? await Promise.all([
- Note.count({ userId: group, deletedAt: null }),
- ]) : [
- latest ? latest.total : 0
- ];
-
- return {
- total: count,
- inc: 0,
- dec: 0,
- diffs: {
- normal: 0,
- reply: 0,
- renote: 0
- }
- };
- }
-
- @autobind
- public async update(user: IUser, note: INote, isAdditional: boolean) {
- const update: Obj = {
- diffs: {}
- };
-
- update.total = isAdditional ? 1 : -1;
-
- if (isAdditional) {
- update.inc = 1;
- } else {
- update.dec = 1;
- }
-
- if (note.replyId != null) {
- update.diffs.reply = isAdditional ? 1 : -1;
- } else if (note.renoteId != null) {
- update.diffs.renote = isAdditional ? 1 : -1;
- } else {
- update.diffs.normal = isAdditional ? 1 : -1;
- }
-
- await this.inc(update, user._id);
- }
-}
-
-export default new PerUserNotesChart();
diff --git a/src/services/chart/per-user-reactions.ts b/src/services/chart/per-user-reactions.ts
deleted file mode 100644
index 60495aeb02..0000000000
--- a/src/services/chart/per-user-reactions.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart from './';
-import { IUser, isLocalUser } from '../../models/user';
-import { INote } from '../../models/note';
-
-/**
- * ユーザーごとのリアクションに関するチャート
- */
-type PerUserReactionsLog = {
- local: {
- /**
- * リアクションされた数
- */
- count: number;
- };
-
- remote: PerUserReactionsLog['local'];
-};
-
-class PerUserReactionsChart extends Chart<PerUserReactionsLog> {
- constructor() {
- super('perUserReaction', true);
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: PerUserReactionsLog, group?: any): Promise<PerUserReactionsLog> {
- return {
- local: {
- count: 0
- },
- remote: {
- count: 0
- }
- };
- }
-
- @autobind
- public async update(user: IUser, note: INote) {
- this.inc({
- [isLocalUser(user) ? 'local' : 'remote']: { count: 1 }
- }, note.userId);
- }
-}
-
-export default new PerUserReactionsChart();
diff --git a/src/services/chart/users.ts b/src/services/chart/users.ts
deleted file mode 100644
index cca9590842..0000000000
--- a/src/services/chart/users.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import User, { IUser, isLocalUser } from '../../models/user';
-import { SchemaType } from '../../misc/schema';
-
-const logSchema = {
- /**
- * 集計期間時点での、全ユーザー数
- */
- total: {
- type: 'number' as 'number',
- description: '集計期間時点での、全ユーザー数'
- },
-
- /**
- * 増加したユーザー数
- */
- inc: {
- type: 'number' as 'number',
- description: '増加したユーザー数'
- },
-
- /**
- * 減少したユーザー数
- */
- dec: {
- type: 'number' as 'number',
- description: '減少したユーザー数'
- },
-};
-
-export const usersLogSchema = {
- type: 'object' as 'object',
- properties: {
- local: {
- type: 'object' as 'object',
- properties: logSchema
- },
- remote: {
- type: 'object' as 'object',
- properties: logSchema
- },
- }
-};
-
-type UsersLog = SchemaType<typeof usersLogSchema>;
-
-class UsersChart extends Chart<UsersLog> {
- constructor() {
- super('users');
- }
-
- @autobind
- protected async getTemplate(init: boolean, latest?: UsersLog): Promise<UsersLog> {
- const [localCount, remoteCount] = init ? await Promise.all([
- User.count({ host: null }),
- User.count({ host: { $ne: null } })
- ]) : [
- latest ? latest.local.total : 0,
- latest ? latest.remote.total : 0
- ];
-
- return {
- local: {
- total: localCount,
- inc: 0,
- dec: 0
- },
- remote: {
- total: remoteCount,
- inc: 0,
- dec: 0
- }
- };
- }
-
- @autobind
- public async update(user: IUser, isAdditional: boolean) {
- const update: Obj = {};
-
- update.total = isAdditional ? 1 : -1;
- if (isAdditional) {
- update.inc = 1;
- } else {
- update.dec = 1;
- }
-
- await this.inc({
- [isLocalUser(user) ? 'local' : 'remote']: update
- });
- }
-}
-
-export default new UsersChart();
diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts
index 3e000ef2ed..bcb8214c56 100644
--- a/src/services/create-notification.ts
+++ b/src/services/create-notification.ts
@@ -1,62 +1,66 @@
-import * as mongo from 'mongodb';
-import Notification from '../models/notification';
-import Mute from '../models/mute';
-import { pack } from '../models/notification';
import { publishMainStream } from './stream';
-import User from '../models/user';
import pushSw from './push-notification';
+import { Notifications, Mutings } from '../models';
+import { genId } from '../misc/gen-id';
+import { User } from '../models/entities/user';
+import { Note } from '../models/entities/note';
+import { Notification } from '../models/entities/notification';
-export default (
- notifiee: mongo.ObjectID,
- notifier: mongo.ObjectID,
+export async function createNotification(
+ notifieeId: User['id'],
+ notifierId: User['id'],
type: string,
- content?: any
-) => new Promise<any>(async (resolve, reject) => {
- if (notifiee.equals(notifier)) {
- return resolve();
+ content?: {
+ noteId?: Note['id'];
+ reaction?: string;
+ choice?: number;
+ }
+) {
+ if (notifieeId === notifierId) {
+ return null;
}
- // Create notification
- const notification = await Notification.insert(Object.assign({
+ const data = {
+ id: genId(),
createdAt: new Date(),
- notifieeId: notifiee,
- notifierId: notifier,
+ notifieeId: notifieeId,
+ notifierId: notifierId,
type: type,
- isRead: false
- }, content));
+ isRead: false,
+ } as Partial<Notification>;
+
+ if (content) {
+ if (content.noteId) data.noteId = content.noteId;
+ if (content.reaction) data.reaction = content.reaction;
+ if (content.choice) data.choice = content.choice;
+ }
- resolve(notification);
+ // Create notification
+ const notification = await Notifications.save(data);
- const packed = await pack(notification);
+ const packed = await Notifications.pack(notification);
// Publish notification event
- publishMainStream(notifiee, 'notification', packed);
-
- // Update flag
- User.update({ _id: notifiee }, {
- $set: {
- hasUnreadNotification: true
- }
- });
+ publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(async () => {
- const fresh = await Notification.findOne({ _id: notification._id }, { isRead: true });
+ const fresh = await Notifications.findOne(notification.id);
if (!fresh.isRead) {
//#region ただしミュートしているユーザーからの通知なら無視
- const mute = await Mute.find({
- muterId: notifiee,
- deletedAt: { $exists: false }
+ const mutings = await Mutings.find({
+ muterId: notifieeId
});
- const mutedUserIds = mute.map(m => m.muteeId.toString());
- if (mutedUserIds.indexOf(notifier.toString()) != -1) {
+ if (mutings.map(m => m.muteeId).includes(notifierId)) {
return;
}
//#endregion
- publishMainStream(notifiee, 'unreadNotification', packed);
+ publishMainStream(notifieeId, 'unreadNotification', packed);
- pushSw(notifiee, 'notification', packed);
+ pushSw(notifieeId, 'notification', packed);
}
}, 2000);
-});
+
+ return notification;
+}
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index cdbcb34de4..df5eedf4c8 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -1,31 +1,27 @@
import { Buffer } from 'buffer';
import * as fs from 'fs';
-import * as mongodb from 'mongodb';
import * as crypto from 'crypto';
import * as Minio from 'minio';
import * as uuid from 'uuid';
import * as sharp from 'sharp';
-import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file';
-import DriveFolder from '../../models/drive-folder';
-import { pack } from '../../models/drive-file';
import { publishMainStream, publishDriveStream } from '../stream';
-import { isLocalUser, IUser, IRemoteUser, isRemoteUser } from '../../models/user';
import delFile from './delete-file';
import config from '../../config';
-import { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
-import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
-import driveChart from '../../services/chart/drive';
-import perUserDriveChart from '../../services/chart/per-user-drive';
-import instanceChart from '../../services/chart/instance';
import fetchMeta from '../../misc/fetch-meta';
import { GenerateVideoThumbnail } from './generate-video-thumbnail';
import { driveLogger } from './logger';
import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor';
-import Instance from '../../models/instance';
import { contentDisposition } from '../../misc/content-disposition';
import { detectMine } from '../../misc/detect-mine';
+import { DriveFiles, DriveFolders, Users, Instances } from '../../models';
+import { InternalStorage } from './internal-storage';
+import { DriveFile } from '../../models/entities/drive-file';
+import { IRemoteUser, User } from '../../models/entities/user';
+import { driveChart, perUserDriveChart, instanceChart } from '../chart';
+import { genId } from '../../misc/gen-id';
+import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
const logger = driveLogger.createSubLogger('register', 'yellow');
@@ -36,11 +32,10 @@ const logger = driveLogger.createSubLogger('register', 'yellow');
* @param type Content-Type for original
* @param hash Hash for original
* @param size Size for original
- * @param metadata
*/
-async function save(path: string, name: string, type: string, hash: string, size: number, metadata: IMetadata): Promise<IDriveFile> {
+async function save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<DriveFile> {
// thunbnail, webpublic を必要なら生成
- const alts = await generateAlts(path, type, !metadata.uri);
+ const alts = await generateAlts(path, type, !file.uri);
if (config.drive && config.drive.storage == 'minio') {
//#region ObjectStorage params
@@ -60,10 +55,10 @@ async function save(path: string, name: string, type: string, hash: string, size
const url = `${ baseUrl }/${ key }`;
// for alts
- let webpublicKey = null as string;
- let webpublicUrl = null as string;
- let thumbnailKey = null as string;
- let thumbnailUrl = null as string;
+ let webpublicKey: string = null;
+ let webpublicUrl: string = null;
+ let thumbnailKey: string = null;
+ let thumbnailUrl: string = null;
//#endregion
//#region Uploads
@@ -91,58 +86,52 @@ async function save(path: string, name: string, type: string, hash: string, size
await Promise.all(uploads);
//#endregion
- //#region DB
- Object.assign(metadata, {
- withoutChunks: true,
- storage: 'minio',
- storageProps: {
- key,
- webpublicKey,
- thumbnailKey,
- },
- url,
- webpublicUrl,
- thumbnailUrl,
- } as IMetadata);
+ file.url = url;
+ file.thumbnailUrl = thumbnailUrl;
+ file.webpublicUrl = webpublicUrl;
+ file.accessKey = key;
+ file.thumbnailAccessKey = thumbnailKey;
+ file.webpublicAccessKey = webpublicKey;
+ file.name = name;
+ file.type = type;
+ file.md5 = hash;
+ file.size = size;
+ file.storedInternal = false;
- const file = await DriveFile.insert({
- length: size,
- uploadDate: new Date(),
- md5: hash,
- filename: name,
- metadata: metadata,
- contentType: type
- });
- //#endregion
+ return await DriveFiles.save(file);
+ } else { // use internal storage
+ const accessKey = uuid.v4();
+ const thumbnailAccessKey = uuid.v4();
+ const webpublicAccessKey = uuid.v4();
- return file;
- } else { // use MongoDB GridFS
- // #region store original
- const originalDst = await getDriveFileBucket();
+ const url = InternalStorage.saveFromPath(accessKey, path);
- // web用(Exif削除済み)がある場合はオリジナルにアクセス制限
- if (alts.webpublic) metadata.accessKey = uuid.v4();
+ let thumbnailUrl: string;
+ let webpublicUrl: string;
- const originalFile = await storeOriginal(originalDst, name, path, type, metadata);
-
- logger.info(`original stored to ${originalFile._id}`);
- // #endregion store original
+ if (alts.thumbnail) {
+ thumbnailUrl = InternalStorage.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data);
+ logger.info(`thumbnail stored: ${thumbnailAccessKey}`);
+ }
- // #region store webpublic
if (alts.webpublic) {
- const webDst = await getDriveFileWebpublicBucket();
- const webFile = await storeAlts(webDst, name, alts.webpublic.data, alts.webpublic.type, originalFile._id);
- logger.info(`web stored ${webFile._id}`);
+ webpublicUrl = InternalStorage.saveFromBuffer(webpublicAccessKey, alts.webpublic.data);
+ logger.info(`web stored: ${webpublicAccessKey}`);
}
- // #endregion store webpublic
- if (alts.thumbnail) {
- const thumDst = await getDriveFileThumbnailBucket();
- const thumFile = await storeAlts(thumDst, name, alts.thumbnail.data, alts.thumbnail.type, originalFile._id);
- logger.info(`web stored ${thumFile._id}`);
- }
+ file.storedInternal = true;
+ file.url = url;
+ file.thumbnailUrl = thumbnailUrl;
+ file.webpublicUrl = webpublicUrl;
+ file.accessKey = accessKey;
+ file.thumbnailAccessKey = thumbnailAccessKey;
+ file.webpublicAccessKey = webpublicAccessKey;
+ file.name = name;
+ file.type = type;
+ file.md5 = hash;
+ file.size = size;
- return originalFile;
+ return await DriveFiles.save(file);
}
}
@@ -211,51 +200,14 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string,
await minio.putObject(config.drive.bucket, key, stream, null, metadata);
}
-/**
- * GridFSBucketにオリジナルを格納する
- */
-export async function storeOriginal(bucket: mongodb.GridFSBucket, name: string, path: string, contentType: string, metadata: any) {
- return new Promise<IDriveFile>((resolve, reject) => {
- const writeStream = bucket.openUploadStream(name, {
- contentType,
- metadata
- });
-
- writeStream.once('finish', resolve);
- writeStream.on('error', reject);
- fs.createReadStream(path).pipe(writeStream);
- });
-}
-
-/**
- * GridFSBucketにオリジナル以外を格納する
- */
-export async function storeAlts(bucket: mongodb.GridFSBucket, name: string, data: Buffer, contentType: string, originalId: mongodb.ObjectID) {
- return new Promise<IDriveFile>((resolve, reject) => {
- const writeStream = bucket.openUploadStream(name, {
- contentType,
- metadata: {
- originalId
- }
- });
-
- writeStream.once('finish', resolve);
- writeStream.on('error', reject);
- writeStream.end(data);
- });
-}
-
async function deleteOldFile(user: IRemoteUser) {
- const oldFile = await DriveFile.findOne({
- _id: {
- $nin: [user.avatarId, user.bannerId]
- },
- 'metadata.userId': user._id
- }, {
- sort: {
- _id: 1
- }
- });
+ const oldFile = await DriveFiles.createQueryBuilder()
+ .select('file')
+ .where('file.id != :avatarId', { avatarId: user.avatarId })
+ .andWhere('file.id != :bannerId', { bannerId: user.bannerId })
+ .andWhere('file.userId = :userId', { userId: user.id })
+ .orderBy('file.id', 'DESC')
+ .getOne();
if (oldFile) {
delFile(oldFile, true);
@@ -278,17 +230,17 @@ async function deleteOldFile(user: IRemoteUser) {
* @return Created drive file
*/
export default async function(
- user: IUser,
+ user: User,
path: string,
name: string = null,
comment: string = null,
- folderId: mongodb.ObjectID = null,
+ folderId: any = null,
force: boolean = false,
isLink: boolean = false,
url: string = null,
uri: string = null,
sensitive: boolean = null
-): Promise<IDriveFile> {
+): Promise<DriveFile> {
// Calc md5 hash
const calcHash = new Promise<string>((res, rej) => {
const readable = fs.createReadStream(path);
@@ -322,51 +274,29 @@ export default async function(
if (!force) {
// Check if there is a file with the same hash
- const much = await DriveFile.findOne({
+ const much = await DriveFiles.findOne({
md5: hash,
- 'metadata.userId': user._id,
- 'metadata.deletedAt': { $exists: false }
+ userId: user.id,
});
if (much) {
- logger.info(`file with same hash is found: ${much._id}`);
+ logger.info(`file with same hash is found: ${much.id}`);
return much;
}
}
//#region Check drive usage
if (!isLink) {
- const usage = await DriveFile
- .aggregate([{
- $match: {
- 'metadata.userId': user._id,
- 'metadata.deletedAt': { $exists: false }
- }
- }, {
- $project: {
- length: true
- }
- }, {
- $group: {
- _id: null,
- usage: { $sum: '$length' }
- }
- }])
- .then((aggregates: any[]) => {
- if (aggregates.length > 0) {
- return aggregates[0].usage;
- }
- return 0;
- });
-
- logger.debug(`drive usage is ${usage}`);
+ const usage = await DriveFiles.clacDriveUsageOf(user);
const instance = await fetchMeta();
- const driveCapacity = 1024 * 1024 * (isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
+ const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
+
+ logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
// If usage limit exceeded
if (usage + size > driveCapacity) {
- if (isLocalUser(user)) {
+ if (Users.isLocalUser(user)) {
throw 'no-free-space';
} else {
// (アバターまたはバナーを含まず)最も古いファイルを削除する
@@ -381,9 +311,9 @@ export default async function(
return null;
}
- const driveFolder = await DriveFolder.findOne({
- _id: folderId,
- userId: user._id
+ const driveFolder = await DriveFolders.findOne({
+ id: folderId,
+ userId: user.id
});
if (driveFolder == null) throw 'folder-not-found';
@@ -437,54 +367,48 @@ export default async function(
const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]);
- const metadata = {
- userId: user._id,
- _user: {
- host: user.host
- },
- folderId: folder !== null ? folder._id : null,
- comment: comment,
- properties: properties,
- withoutChunks: isLink,
- isRemote: isLink,
- isSensitive: isLocalUser(user) && user.settings.alwaysMarkNsfw ? true :
- (sensitive !== null && sensitive !== undefined)
- ? sensitive
- : false
- } as IMetadata;
+ let file = new DriveFile();
+ file.id = genId();
+ file.createdAt = new Date();
+ file.userId = user.id;
+ file.userHost = user.host;
+ file.folderId = folder !== null ? folder.id : null;
+ file.comment = comment;
+ file.properties = properties;
+ file.isRemote = isLink;
+ file.isSensitive = Users.isLocalUser(user) && user.alwaysMarkNsfw ? true :
+ (sensitive !== null && sensitive !== undefined)
+ ? sensitive
+ : false;
if (url !== null) {
- metadata.src = url;
+ file.src = url;
if (isLink) {
- metadata.url = url;
+ file.url = url;
}
}
if (uri !== null) {
- metadata.uri = uri;
+ file.uri = uri;
}
- let driveFile: IDriveFile;
-
if (isLink) {
try {
- driveFile = await DriveFile.insert({
- length: 0,
- uploadDate: new Date(),
- md5: hash,
- filename: detectedName,
- metadata: metadata,
- contentType: mime
- });
+ file.size = 0;
+ file.md5 = hash;
+ file.name = detectedName;
+ file.type = mime;
+
+ file = await DriveFiles.save(file);
} catch (e) {
// duplicate key error (when already registered)
- if (e.code === 11000) {
- logger.info(`already registered ${metadata.uri}`);
+ if (isDuplicateKeyValueError(e)) {
+ logger.info(`already registered ${file.uri}`);
- driveFile = await DriveFile.findOne({
- 'metadata.uri': metadata.uri,
- 'metadata.userId': user._id
+ file = await DriveFiles.findOne({
+ uri: file.uri,
+ userId: user.id
});
} else {
logger.error(e);
@@ -492,29 +416,25 @@ export default async function(
}
}
} else {
- driveFile = await (save(path, detectedName, mime, hash, size, metadata));
+ file = await (save(file, path, detectedName, mime, hash, size));
}
- logger.succ(`drive file has been created ${driveFile._id}`);
+ logger.succ(`drive file has been created ${file.id}`);
- pack(driveFile).then(packedFile => {
+ DriveFiles.pack(file).then(packedFile => {
// Publish driveFileCreated event
- publishMainStream(user._id, 'driveFileCreated', packedFile);
- publishDriveStream(user._id, 'fileCreated', packedFile);
+ publishMainStream(user.id, 'driveFileCreated', packedFile);
+ publishDriveStream(user.id, 'fileCreated', packedFile);
});
// 統計を更新
- driveChart.update(driveFile, true);
- perUserDriveChart.update(driveFile, true);
- if (isRemoteUser(driveFile.metadata._user)) {
- instanceChart.updateDrive(driveFile, true);
- Instance.update({ host: driveFile.metadata._user.host }, {
- $inc: {
- driveUsage: driveFile.length,
- driveFiles: 1
- }
- });
+ driveChart.update(file, true);
+ perUserDriveChart.update(file, true);
+ if (file.userHost !== null) {
+ instanceChart.updateDrive(file, true);
+ Instances.increment({ host: file.userHost }, 'driveUsage', file.size);
+ Instances.increment({ host: file.userHost }, 'driveFiles', 1);
}
- return driveFile;
+ return file;
}
diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts
index c5c15ca20b..adf57416fe 100644
--- a/src/services/drive/delete-file.ts
+++ b/src/services/drive/delete-file.ts
@@ -1,99 +1,53 @@
import * as Minio from 'minio';
-import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file';
-import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
import config from '../../config';
-import driveChart from '../../services/chart/drive';
-import perUserDriveChart from '../../services/chart/per-user-drive';
-import instanceChart from '../../services/chart/instance';
-import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic';
-import Instance from '../../models/instance';
-import { isRemoteUser } from '../../models/user';
+import { DriveFile } from '../../models/entities/drive-file';
+import { InternalStorage } from './internal-storage';
+import { DriveFiles, Instances } from '../../models';
+import { driveChart, perUserDriveChart, instanceChart } from '../chart';
-export default async function(file: IDriveFile, isExpired = false) {
- if (file.metadata.storage == 'minio') {
- const minio = new Minio.Client(config.drive.config);
-
- // 後方互換性のため、file.metadata.storageProps.key があるかどうかチェックしています。
- // 将来的には const obj = file.metadata.storageProps.key; とします。
- const obj = file.metadata.storageProps.key ? file.metadata.storageProps.key : `${config.drive.prefix}/${file.metadata.storageProps.id}`;
- await minio.removeObject(config.drive.bucket, obj);
+export default async function(file: DriveFile, isExpired = false) {
+ if (file.storedInternal) {
+ InternalStorage.del(file.accessKey);
- if (file.metadata.thumbnailUrl) {
- // 後方互換性のため、file.metadata.storageProps.thumbnailKey があるかどうかチェックしています。
- // 将来的には const thumbnailObj = file.metadata.storageProps.thumbnailKey; とします。
- const thumbnailObj = file.metadata.storageProps.thumbnailKey ? file.metadata.storageProps.thumbnailKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`;
- await minio.removeObject(config.drive.bucket, thumbnailObj);
+ if (file.thumbnailUrl) {
+ InternalStorage.del(file.thumbnailAccessKey);
}
- if (file.metadata.webpublicUrl) {
- const webpublicObj = file.metadata.storageProps.webpublicKey ? file.metadata.storageProps.webpublicKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-original`;
- await minio.removeObject(config.drive.bucket, webpublicObj);
+ if (file.webpublicUrl) {
+ InternalStorage.del(file.webpublicAccessKey);
}
- }
+ } else {
+ const minio = new Minio.Client(config.drive.config);
- // チャンクをすべて削除
- await DriveFileChunk.remove({
- files_id: file._id
- });
+ await minio.removeObject(config.drive.bucket, file.accessKey);
- const set = {
- metadata: {
- deletedAt: new Date(),
- isExpired: isExpired
+ if (file.thumbnailUrl) {
+ await minio.removeObject(config.drive.bucket, file.thumbnailAccessKey);
}
- } as any;
-
- // リモートファイル期限切れ削除後は直リンクにする
- if (isExpired && file.metadata && file.metadata._user && file.metadata._user.host != null) {
- set.metadata.withoutChunks = true;
- set.metadata.isRemote = true;
- set.metadata.url = file.metadata.uri;
- set.metadata.thumbnailUrl = undefined;
- set.metadata.webpublicUrl = undefined;
- }
-
- await DriveFile.update({ _id: file._id }, {
- $set: set
- });
- //#region サムネイルもあれば削除
- const thumbnail = await DriveFileThumbnail.findOne({
- 'metadata.originalId': file._id
- });
-
- if (thumbnail) {
- await DriveFileThumbnailChunk.remove({
- files_id: thumbnail._id
- });
-
- await DriveFileThumbnail.remove({ _id: thumbnail._id });
+ if (file.webpublicUrl) {
+ await minio.removeObject(config.drive.bucket, file.webpublicAccessKey);
+ }
}
- //#endregion
- //#region Web公開用もあれば削除
- const webpublic = await DriveFileWebpublic.findOne({
- 'metadata.originalId': file._id
- });
-
- if (webpublic) {
- await DriveFileWebpublicChunk.remove({
- files_id: webpublic._id
+ // リモートファイル期限切れ削除後は直リンクにする
+ if (isExpired && file.userHost !== null) {
+ DriveFiles.update(file.id, {
+ isRemote: true,
+ url: file.uri,
+ thumbnailUrl: null,
+ webpublicUrl: null
});
-
- await DriveFileWebpublic.remove({ _id: webpublic._id });
+ } else {
+ DriveFiles.delete(file.id);
}
- //#endregion
// 統計を更新
driveChart.update(file, false);
perUserDriveChart.update(file, false);
- if (isRemoteUser(file.metadata._user)) {
+ if (file.userHost !== null) {
instanceChart.updateDrive(file, false);
- Instance.update({ host: file.metadata._user.host }, {
- $inc: {
- driveUsage: -file.length,
- driveFiles: -1
- }
- });
+ Instances.decrement({ host: file.userHost }, 'driveUsage', file.size);
+ Instances.decrement({ host: file.userHost }, 'driveFiles', 1);
}
}
diff --git a/src/services/drive/internal-storage.ts b/src/services/drive/internal-storage.ts
new file mode 100644
index 0000000000..ff890d7d47
--- /dev/null
+++ b/src/services/drive/internal-storage.ts
@@ -0,0 +1,27 @@
+import * as fs from 'fs';
+import * as Path from 'path';
+import config from '../../config';
+
+export class InternalStorage {
+ private static readonly path = Path.resolve(`${__dirname}/../../../files`);
+
+ public static read(key: string) {
+ return fs.createReadStream(`${InternalStorage.path}/${key}`);
+ }
+
+ public static saveFromPath(key: string, srcPath: string) {
+ fs.mkdirSync(InternalStorage.path, { recursive: true });
+ fs.copyFileSync(srcPath, `${InternalStorage.path}/${key}`);
+ return `${config.url}/files/${key}`;
+ }
+
+ public static saveFromBuffer(key: string, data: Buffer) {
+ fs.mkdirSync(InternalStorage.path, { recursive: true });
+ fs.writeFileSync(`${InternalStorage.path}/${key}`, data);
+ return `${config.url}/files/${key}`;
+ }
+
+ public static del(key: string) {
+ fs.unlink(`${InternalStorage.path}/${key}`, () => {});
+ }
+}
diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index cdf6ba0cef..a7fe1fbd26 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -1,26 +1,26 @@
import * as URL from 'url';
-
-import { IDriveFile, validateFileName } from '../../models/drive-file';
import create from './add-file';
-import { IUser } from '../../models/user';
-import * as mongodb from 'mongodb';
+import { User } from '../../models/entities/user';
import { driveLogger } from './logger';
import { createTemp } from '../../misc/create-temp';
import { downloadUrl } from '../../misc/donwload-url';
+import { DriveFolder } from '../../models/entities/drive-folder';
+import { DriveFile } from '../../models/entities/drive-file';
+import { DriveFiles } from '../../models';
const logger = driveLogger.createSubLogger('downloader');
export default async (
url: string,
- user: IUser,
- folderId: mongodb.ObjectID = null,
+ user: User,
+ folderId: DriveFolder['id'] = null,
uri: string = null,
sensitive = false,
force = false,
link = false
-): Promise<IDriveFile> => {
+): Promise<DriveFile> => {
let name = URL.parse(url).pathname.split('/').pop();
- if (!validateFileName(name)) {
+ if (!DriveFiles.validateFileName(name)) {
name = null;
}
@@ -30,12 +30,12 @@ export default async (
// write content at URL to temp file
await downloadUrl(url, path);
- let driveFile: IDriveFile;
+ let driveFile: DriveFile;
let error;
try {
driveFile = await create(user, path, name, null, folderId, force, link, url, uri, sensitive);
- logger.succ(`Got: ${driveFile._id}`);
+ logger.succ(`Got: ${driveFile.id}`);
} catch (e) {
error = e;
logger.error(`Failed to create drive file: ${e}`, {
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
index 1eaad750f7..28e4ba3c12 100644
--- a/src/services/following/create.ts
+++ b/src/services/following/create.ts
@@ -1,100 +1,70 @@
-import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user';
-import Following from '../../models/following';
-import Blocking from '../../models/blocking';
import { publishMainStream } from '../stream';
-import notify from '../../services/create-notification';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderAccept from '../../remote/activitypub/renderer/accept';
import renderReject from '../../remote/activitypub/renderer/reject';
import { deliver } from '../../queue';
import createFollowRequest from './requests/create';
-import perUserFollowingChart from '../../services/chart/per-user-following';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
import Logger from '../logger';
-import FollowRequest from '../../models/follow-request';
import { IdentifiableError } from '../../misc/identifiable-error';
+import { User } from '../../models/entities/user';
+import { Followings, Users, FollowRequests, Blockings, Instances } from '../../models';
+import { instanceChart, perUserFollowingChart } from '../chart';
+import { genId } from '../../misc/gen-id';
+import { createNotification } from '../create-notification';
+import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
const logger = new Logger('following/create');
-export async function insertFollowingDoc(followee: IUser, follower: IUser) {
+export async function insertFollowingDoc(followee: User, follower: User) {
+ if (follower.id === followee.id) return;
+
let alreadyFollowed = false;
- await Following.insert({
+ await Followings.save({
+ id: genId(),
createdAt: new Date(),
- followerId: follower._id,
- followeeId: followee._id,
+ followerId: follower.id,
+ followeeId: followee.id,
// 非正規化
- _follower: {
- host: follower.host,
- inbox: isRemoteUser(follower) ? follower.inbox : undefined,
- sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined
- },
- _followee: {
- host: followee.host,
- inbox: isRemoteUser(followee) ? followee.inbox : undefined,
- sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined
- }
+ followerHost: follower.host,
+ followerInbox: Users.isRemoteUser(follower) ? follower.inbox : null,
+ followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : null,
+ followeeHost: followee.host,
+ followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : null,
+ followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : null
}).catch(e => {
- if (e.code === 11000 && isRemoteUser(follower) && isLocalUser(followee)) {
- logger.info(`Insert duplicated ignore. ${follower._id} => ${followee._id}`);
+ if (isDuplicateKeyValueError(e) && Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
+ logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`);
alreadyFollowed = true;
} else {
throw e;
}
});
- const removed = await FollowRequest.remove({
- followeeId: followee._id,
- followerId: follower._id
+ await FollowRequests.delete({
+ followeeId: followee.id,
+ followerId: follower.id
});
- if (removed.deletedCount === 1) {
- await User.update({ _id: followee._id }, {
- $inc: {
- pendingReceivedFollowRequestsCount: -1
- }
- });
- }
-
if (alreadyFollowed) return;
//#region Increment counts
- User.update({ _id: follower._id }, {
- $inc: {
- followingCount: 1
- }
- });
-
- User.update({ _id: followee._id }, {
- $inc: {
- followersCount: 1
- }
- });
+ Users.increment({ id: follower.id }, 'followingCount', 1);
+ Users.increment({ id: followee.id }, 'followersCount', 1);
//#endregion
//#region Update instance stats
- if (isRemoteUser(follower) && isLocalUser(followee)) {
+ if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
registerOrFetchInstanceDoc(follower.host).then(i => {
- Instance.update({ _id: i._id }, {
- $inc: {
- followingCount: 1
- }
- });
-
+ Instances.increment({ id: i.id }, 'followingCount', 1);
instanceChart.updateFollowing(i.host, true);
});
- } else if (isLocalUser(follower) && isRemoteUser(followee)) {
+ } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
registerOrFetchInstanceDoc(followee.host).then(i => {
- Instance.update({ _id: i._id }, {
- $inc: {
- followersCount: 1
- }
- });
-
+ Instances.increment({ id: i.id }, 'followersCount', 1);
instanceChart.updateFollowers(i.host, true);
});
}
@@ -103,44 +73,42 @@ export async function insertFollowingDoc(followee: IUser, follower: IUser) {
perUserFollowingChart.update(follower, followee, true);
// Publish follow event
- if (isLocalUser(follower)) {
- packUser(followee, follower, {
+ if (Users.isLocalUser(follower)) {
+ Users.pack(followee, follower, {
detail: true
- }).then(packed => publishMainStream(follower._id, 'follow', packed));
+ }).then(packed => publishMainStream(follower.id, 'follow', packed));
}
// Publish followed event
- if (isLocalUser(followee)) {
- packUser(follower, followee).then(packed => publishMainStream(followee._id, 'followed', packed)),
+ if (Users.isLocalUser(followee)) {
+ Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'followed', packed)),
// 通知を作成
- notify(followee._id, follower._id, 'follow');
+ createNotification(followee.id, follower.id, 'follow');
}
}
-export default async function(follower: IUser, followee: IUser, requestId?: string) {
+export default async function(follower: User, followee: User, requestId?: string) {
// check blocking
const [blocking, blocked] = await Promise.all([
- Blocking.findOne({
- blockerId: follower._id,
- blockeeId: followee._id,
+ Blockings.findOne({
+ blockerId: follower.id,
+ blockeeId: followee.id,
}),
- Blocking.findOne({
- blockerId: followee._id,
- blockeeId: follower._id,
+ Blockings.findOne({
+ blockerId: followee.id,
+ blockeeId: follower.id,
})
]);
- if (isRemoteUser(follower) && isLocalUser(followee) && blocked) {
+ if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocked) {
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
const content = renderActivity(renderReject(renderFollow(follower, followee, requestId), followee));
deliver(followee , content, follower.inbox);
return;
- } else if (isRemoteUser(follower) && isLocalUser(followee) && blocking) {
+ } else if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocking) {
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
- await Blocking.remove({
- _id: blocking._id
- });
+ await Blockings.delete(blocking.id);
} else {
// それ以外は単純に例外
if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
@@ -151,23 +119,23 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
- if (followee.isLocked || (followee.carefulBot && follower.isBot) || (isLocalUser(follower) && isRemoteUser(followee))) {
+ if (followee.isLocked || (followee.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) {
let autoAccept = false;
// 鍵アカウントであっても、既にフォローされていた場合はスルー
- const following = await Following.findOne({
- followerId: follower._id,
- followeeId: followee._id,
+ const following = await Followings.findOne({
+ followerId: follower.id,
+ followeeId: followee.id,
});
if (following) {
autoAccept = true;
}
// フォローしているユーザーは自動承認オプション
- if (!autoAccept && (isLocalUser(followee) && followee.autoAcceptFollowed)) {
- const followed = await Following.findOne({
- followerId: followee._id,
- followeeId: follower._id
+ if (!autoAccept && (Users.isLocalUser(followee) && followee.autoAcceptFollowed)) {
+ const followed = await Followings.findOne({
+ followerId: followee.id,
+ followeeId: follower.id
});
if (followed) autoAccept = true;
@@ -181,7 +149,7 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri
await insertFollowingDoc(followee, follower);
- if (isRemoteUser(follower) && isLocalUser(followee)) {
+ if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
const content = renderActivity(renderAccept(renderFollow(follower, followee, requestId), followee));
deliver(followee, content, follower.inbox);
}
diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts
index d85c8472bb..ad09f0e6d1 100644
--- a/src/services/following/delete.ts
+++ b/src/services/following/delete.ts
@@ -1,22 +1,20 @@
-import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user';
-import Following from '../../models/following';
import { publishMainStream } from '../stream';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderUndo from '../../remote/activitypub/renderer/undo';
import { deliver } from '../../queue';
-import perUserFollowingChart from '../../services/chart/per-user-following';
import Logger from '../logger';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
+import { User } from '../../models/entities/user';
+import { Followings, Users, Instances } from '../../models';
+import { instanceChart, perUserFollowingChart } from '../chart';
const logger = new Logger('following/delete');
-export default async function(follower: IUser, followee: IUser, silent = false) {
- const following = await Following.findOne({
- followerId: follower._id,
- followeeId: followee._id
+export default async function(follower: User, followee: User, silent = false) {
+ const following = await Followings.findOne({
+ followerId: follower.id,
+ followeeId: followee.id
});
if (following == null) {
@@ -24,45 +22,25 @@ export default async function(follower: IUser, followee: IUser, silent = false)
return;
}
- Following.remove({
- _id: following._id
- });
+ Followings.delete(following.id);
//#region Decrement following count
- User.update({ _id: follower._id }, {
- $inc: {
- followingCount: -1
- }
- });
+ Users.decrement({ id: follower.id }, 'followingCount', 1);
//#endregion
//#region Decrement followers count
- User.update({ _id: followee._id }, {
- $inc: {
- followersCount: -1
- }
- });
+ Users.decrement({ id: followee.id }, 'followersCount', 1);
//#endregion
//#region Update instance stats
- if (isRemoteUser(follower) && isLocalUser(followee)) {
+ if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
registerOrFetchInstanceDoc(follower.host).then(i => {
- Instance.update({ _id: i._id }, {
- $inc: {
- followingCount: -1
- }
- });
-
+ Instances.decrement({ id: i.id }, 'followingCount', 1);
instanceChart.updateFollowing(i.host, false);
});
- } else if (isLocalUser(follower) && isRemoteUser(followee)) {
+ } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
registerOrFetchInstanceDoc(followee.host).then(i => {
- Instance.update({ _id: i._id }, {
- $inc: {
- followersCount: -1
- }
- });
-
+ Instances.decrement({ id: i.id }, 'followersCount', 1);
instanceChart.updateFollowers(i.host, false);
});
}
@@ -71,13 +49,13 @@ export default async function(follower: IUser, followee: IUser, silent = false)
perUserFollowingChart.update(follower, followee, false);
// Publish unfollow event
- if (!silent && isLocalUser(follower)) {
- packUser(followee, follower, {
+ if (!silent && Users.isLocalUser(follower)) {
+ Users.pack(followee, follower, {
detail: true
- }).then(packed => publishMainStream(follower._id, 'unfollow', packed));
+ }).then(packed => publishMainStream(follower.id, 'unfollow', packed));
}
- if (isLocalUser(follower) && isRemoteUser(followee)) {
+ if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox);
}
diff --git a/src/services/following/requests/accept-all.ts b/src/services/following/requests/accept-all.ts
index cf1a9e923d..b61c31a513 100644
--- a/src/services/following/requests/accept-all.ts
+++ b/src/services/following/requests/accept-all.ts
@@ -1,24 +1,18 @@
-import User, { IUser } from '../../../models/user';
-import FollowRequest from '../../../models/follow-request';
import accept from './accept';
+import { User } from '../../../models/entities/user';
+import { FollowRequests, Users } from '../../../models';
/**
* 指定したユーザー宛てのフォローリクエストをすべて承認
* @param user ユーザー
*/
-export default async function(user: IUser) {
- const requests = await FollowRequest.find({
- followeeId: user._id
+export default async function(user: User) {
+ const requests = await FollowRequests.find({
+ followeeId: user.id
});
for (const request of requests) {
- const follower = await User.findOne({ _id: request.followerId });
+ const follower = await Users.findOne(request.followerId);
accept(user, follower);
}
-
- User.update({ _id: user._id }, {
- $set: {
- pendingReceivedFollowRequestsCount: 0
- }
- });
}
diff --git a/src/services/following/requests/accept.ts b/src/services/following/requests/accept.ts
index 284c6d5e19..0be8e24e1a 100644
--- a/src/services/following/requests/accept.ts
+++ b/src/services/following/requests/accept.ts
@@ -1,26 +1,26 @@
-import { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user';
-import FollowRequest from '../../../models/follow-request';
import { renderActivity } from '../../../remote/activitypub/renderer';
import renderFollow from '../../../remote/activitypub/renderer/follow';
import renderAccept from '../../../remote/activitypub/renderer/accept';
import { deliver } from '../../../queue';
import { publishMainStream } from '../../stream';
import { insertFollowingDoc } from '../create';
+import { User, ILocalUser } from '../../../models/entities/user';
+import { FollowRequests, Users } from '../../../models';
-export default async function(followee: IUser, follower: IUser) {
- const request = await FollowRequest.findOne({
- followeeId: followee._id,
- followerId: follower._id
+export default async function(followee: User, follower: User) {
+ const request = await FollowRequests.findOne({
+ followeeId: followee.id,
+ followerId: follower.id
});
await insertFollowingDoc(followee, follower);
- if (isRemoteUser(follower) && request) {
+ if (Users.isRemoteUser(follower) && request) {
const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId), followee as ILocalUser));
deliver(followee as ILocalUser, content, follower.inbox);
}
- packUser(followee, followee, {
+ Users.pack(followee, followee, {
detail: true
- }).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
+ }).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
}
diff --git a/src/services/following/requests/cancel.ts b/src/services/following/requests/cancel.ts
index af4cca85fe..98fec5d331 100644
--- a/src/services/following/requests/cancel.ts
+++ b/src/services/following/requests/cancel.ts
@@ -1,39 +1,33 @@
-import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user';
-import FollowRequest from '../../../models/follow-request';
import { renderActivity } from '../../../remote/activitypub/renderer';
import renderFollow from '../../../remote/activitypub/renderer/follow';
import renderUndo from '../../../remote/activitypub/renderer/undo';
import { deliver } from '../../../queue';
import { publishMainStream } from '../../stream';
import { IdentifiableError } from '../../../misc/identifiable-error';
+import { User, ILocalUser } from '../../../models/entities/user';
+import { Users, FollowRequests } from '../../../models';
-export default async function(followee: IUser, follower: IUser) {
- if (isRemoteUser(followee)) {
+export default async function(followee: User, follower: User) {
+ if (Users.isRemoteUser(followee)) {
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower as ILocalUser, content, followee.inbox);
}
- const request = await FollowRequest.findOne({
- followeeId: followee._id,
- followerId: follower._id
+ const request = await FollowRequests.findOne({
+ followeeId: followee.id,
+ followerId: follower.id
});
if (request == null) {
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
}
- await FollowRequest.remove({
- followeeId: followee._id,
- followerId: follower._id
+ await FollowRequests.delete({
+ followeeId: followee.id,
+ followerId: follower.id
});
- await User.update({ _id: followee._id }, {
- $inc: {
- pendingReceivedFollowRequestsCount: -1
- }
- });
-
- packUser(followee, followee, {
+ Users.pack(followee, followee, {
detail: true
- }).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
+ }).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
}
diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts
index 10c534f529..32e79d136d 100644
--- a/src/services/following/requests/create.ts
+++ b/src/services/following/requests/create.ts
@@ -1,66 +1,59 @@
-import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../../models/user';
import { publishMainStream } from '../../stream';
-import notify from '../../../services/create-notification';
import { renderActivity } from '../../../remote/activitypub/renderer';
import renderFollow from '../../../remote/activitypub/renderer/follow';
import { deliver } from '../../../queue';
-import FollowRequest from '../../../models/follow-request';
-import Blocking from '../../../models/blocking';
+import { User } from '../../../models/entities/user';
+import { Blockings, FollowRequests, Users } from '../../../models';
+import { genId } from '../../../misc/gen-id';
+import { createNotification } from '../../create-notification';
+
+export default async function(follower: User, followee: User, requestId?: string) {
+ if (follower.id === followee.id) return;
-export default async function(follower: IUser, followee: IUser, requestId?: string) {
// check blocking
const [blocking, blocked] = await Promise.all([
- Blocking.findOne({
- blockerId: follower._id,
- blockeeId: followee._id,
+ Blockings.findOne({
+ blockerId: follower.id,
+ blockeeId: followee.id,
}),
- Blocking.findOne({
- blockerId: followee._id,
- blockeeId: follower._id,
+ Blockings.findOne({
+ blockerId: followee.id,
+ blockeeId: follower.id,
})
]);
if (blocking != null) throw new Error('blocking');
if (blocked != null) throw new Error('blocked');
- await FollowRequest.insert({
+ await FollowRequests.save({
+ id: genId(),
createdAt: new Date(),
- followerId: follower._id,
- followeeId: followee._id,
+ followerId: follower.id,
+ followeeId: followee.id,
requestId,
// 非正規化
- _follower: {
- host: follower.host,
- inbox: isRemoteUser(follower) ? follower.inbox : undefined,
- sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined
- },
- _followee: {
- host: followee.host,
- inbox: isRemoteUser(followee) ? followee.inbox : undefined,
- sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined
- }
- });
-
- await User.update({ _id: followee._id }, {
- $inc: {
- pendingReceivedFollowRequestsCount: 1
- }
+ followerHost: follower.host,
+ followerInbox: Users.isRemoteUser(follower) ? follower.inbox : undefined,
+ followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : undefined,
+ followeeHost: followee.host,
+ followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : undefined,
+ followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : undefined
});
// Publish receiveRequest event
- if (isLocalUser(followee)) {
- packUser(follower, followee).then(packed => publishMainStream(followee._id, 'receiveFollowRequest', packed));
+ if (Users.isLocalUser(followee)) {
+ Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'receiveFollowRequest', packed));
- packUser(followee, followee, {
+ Users.pack(followee, followee, {
detail: true
- }).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
+ }).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
// 通知を作成
- notify(followee._id, follower._id, 'receiveFollowRequest');
+ createNotification(followee.id, follower.id, 'receiveFollowRequest');
}
- if (isLocalUser(follower) && isRemoteUser(followee)) {
+ if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderFollow(follower, followee));
deliver(follower, content, followee.inbox);
}
diff --git a/src/services/following/requests/reject.ts b/src/services/following/requests/reject.ts
index cb924df811..c590edcfd8 100644
--- a/src/services/following/requests/reject.ts
+++ b/src/services/following/requests/reject.ts
@@ -1,34 +1,28 @@
-import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user';
-import FollowRequest from '../../../models/follow-request';
import { renderActivity } from '../../../remote/activitypub/renderer';
import renderFollow from '../../../remote/activitypub/renderer/follow';
import renderReject from '../../../remote/activitypub/renderer/reject';
import { deliver } from '../../../queue';
import { publishMainStream } from '../../stream';
+import { User, ILocalUser } from '../../../models/entities/user';
+import { Users, FollowRequests } from '../../../models';
-export default async function(followee: IUser, follower: IUser) {
- if (isRemoteUser(follower)) {
- const request = await FollowRequest.findOne({
- followeeId: followee._id,
- followerId: follower._id
+export default async function(followee: User, follower: User) {
+ if (Users.isRemoteUser(follower)) {
+ const request = await FollowRequests.findOne({
+ followeeId: followee.id,
+ followerId: follower.id
});
const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee as ILocalUser));
deliver(followee as ILocalUser, content, follower.inbox);
}
- await FollowRequest.remove({
- followeeId: followee._id,
- followerId: follower._id
+ await FollowRequests.delete({
+ followeeId: followee.id,
+ followerId: follower.id
});
- User.update({ _id: followee._id }, {
- $inc: {
- pendingReceivedFollowRequestsCount: -1
- }
- });
-
- packUser(followee, follower, {
+ Users.pack(followee, follower, {
detail: true
- }).then(packed => publishMainStream(follower._id, 'unfollow', packed));
+ }).then(packed => publishMainStream(follower.id, 'unfollow', packed));
}
diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts
index 4d0ae3c149..4e43421bdc 100644
--- a/src/services/i/pin.ts
+++ b/src/services/i/pin.ts
@@ -1,59 +1,51 @@
import config from '../../config';
-import * as mongo from 'mongodb';
-import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user';
-import Note, { packMany } from '../../models/note';
-import Following from '../../models/following';
import renderAdd from '../../remote/activitypub/renderer/add';
import renderRemove from '../../remote/activitypub/renderer/remove';
import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
import { IdentifiableError } from '../../misc/identifiable-error';
+import { User, ILocalUser } from '../../models/entities/user';
+import { Note } from '../../models/entities/note';
+import { Notes, UserNotePinings, Users, Followings } from '../../models';
+import { UserNotePining } from '../../models/entities/user-note-pinings';
+import { genId } from '../../misc/gen-id';
/**
* 指定した投稿をピン留めします
* @param user
* @param noteId
*/
-export async function addPinned(user: IUser, noteId: mongo.ObjectID) {
+export async function addPinned(user: User, noteId: Note['id']) {
// Fetch pinee
- const note = await Note.findOne({
- _id: noteId,
- userId: user._id
+ const note = await Notes.findOne({
+ id: noteId,
+ userId: user.id
});
- if (note === null) {
+ if (note == null) {
throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.');
}
- let pinnedNoteIds = user.pinnedNoteIds || [];
+ const pinings = await UserNotePinings.find({ userId: user.id });
- //#region 現在ピン留め投稿している投稿が実際にデータベースに存在しているのかチェック
- // データベースの欠損などで存在していない(または破損している)場合があるので。
- // 存在していなかったらピン留め投稿から外す
- const pinnedNotes = await packMany(pinnedNoteIds, null, { detail: true });
-
- pinnedNoteIds = pinnedNoteIds.filter(id => pinnedNotes.some(n => n.id.toString() === id.toHexString()));
- //#endregion
-
- if (pinnedNoteIds.length >= 5) {
+ if (pinings.length >= 5) {
throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.');
}
- if (pinnedNoteIds.some(id => id.equals(note._id))) {
+ if (pinings.some(pining => pining.noteId === note.id)) {
throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.');
}
- pinnedNoteIds.unshift(note._id);
-
- await User.update(user._id, {
- $set: {
- pinnedNoteIds: pinnedNoteIds
- }
- });
+ await UserNotePinings.save({
+ id: genId(),
+ createdAt: new Date(),
+ userId: user.id,
+ noteId: note.id
+ } as UserNotePining);
// Deliver to remote followers
- if (isLocalUser(user)) {
- deliverPinnedChange(user._id, note._id, true);
+ if (Users.isLocalUser(user)) {
+ deliverPinnedChange(user.id, note.id, true);
}
}
@@ -62,43 +54,40 @@ export async function addPinned(user: IUser, noteId: mongo.ObjectID) {
* @param user
* @param noteId
*/
-export async function removePinned(user: IUser, noteId: mongo.ObjectID) {
+export async function removePinned(user: User, noteId: Note['id']) {
// Fetch unpinee
- const note = await Note.findOne({
- _id: noteId,
- userId: user._id
+ const note = await Notes.findOne({
+ id: noteId,
+ userId: user.id
});
- if (note === null) {
+ if (note == null) {
throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.');
}
- const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id));
-
- await User.update(user._id, {
- $set: {
- pinnedNoteIds: pinnedNoteIds
- }
+ UserNotePinings.delete({
+ userId: user.id,
+ noteId: note.id
});
// Deliver to remote followers
- if (isLocalUser(user)) {
- deliverPinnedChange(user._id, noteId, false);
+ if (Users.isLocalUser(user)) {
+ deliverPinnedChange(user.id, noteId, false);
}
}
-export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) {
- const user = await User.findOne({
- _id: userId
+export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) {
+ const user = await Users.findOne({
+ id: userId
});
- if (!isLocalUser(user)) return;
+ if (!Users.isLocalUser(user)) return;
const queue = await CreateRemoteInboxes(user);
if (queue.length < 1) return;
- const target = `${config.url}/users/${user._id}/collections/featured`;
+ const target = `${config.url}/users/${user.id}/collections/featured`;
const item = `${config.url}/notes/${noteId}`;
const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item));
@@ -112,16 +101,20 @@ export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.
* @param user ローカルユーザー
*/
async function CreateRemoteInboxes(user: ILocalUser): Promise<string[]> {
- const followers = await Following.find({
- followeeId: user._id
+ const followers = await Followings.find({
+ followeeId: user.id
});
const queue: string[] = [];
for (const following of followers) {
- const follower = following._follower;
+ const follower = {
+ host: following.followerHost,
+ inbox: following.followerInbox,
+ sharedInbox: following.followerSharedInbox,
+ };
- if (isRemoteUser(follower)) {
+ if (follower.host !== null) {
const inbox = follower.sharedInbox || follower.inbox;
if (!queue.includes(inbox)) queue.push(inbox);
}
diff --git a/src/services/i/update.ts b/src/services/i/update.ts
index 887cecb04c..7dba472e78 100644
--- a/src/services/i/update.ts
+++ b/src/services/i/update.ts
@@ -1,29 +1,26 @@
-import * as mongo from 'mongodb';
-import User, { isLocalUser, isRemoteUser } from '../../models/user';
-import Following from '../../models/following';
-import renderPerson from '../../remote/activitypub/renderer/person';
import renderUpdate from '../../remote/activitypub/renderer/update';
import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
+import { Followings, Users } from '../../models';
+import { User } from '../../models/entities/user';
+import { renderPerson } from '../../remote/activitypub/renderer/person';
-export async function publishToFollowers(userId: mongo.ObjectID) {
- const user = await User.findOne({
- _id: userId
+export async function publishToFollowers(userId: User['id']) {
+ const user = await Users.findOne({
+ id: userId
});
- const followers = await Following.find({
- followeeId: user._id
+ const followers = await Followings.find({
+ followeeId: user.id
});
const queue: string[] = [];
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
- if (isLocalUser(user)) {
+ if (Users.isLocalUser(user)) {
for (const following of followers) {
- const follower = following._follower;
-
- if (isRemoteUser(follower)) {
- const inbox = follower.sharedInbox || follower.inbox;
+ if (following.followerHost !== null) {
+ const inbox = following.followerSharedInbox || following.followerInbox;
if (!queue.includes(inbox)) queue.push(inbox);
}
}
diff --git a/src/services/logger.ts b/src/services/logger.ts
index aa93954bc1..e6a54e626d 100644
--- a/src/services/logger.ts
+++ b/src/services/logger.ts
@@ -3,7 +3,9 @@ import * as os from 'os';
import chalk from 'chalk';
import * as dateformat from 'dateformat';
import { program } from '../argv';
-import Log from '../models/log';
+import { getRepository } from 'typeorm';
+import { Log } from '../models/entities/log';
+import { genId } from '../misc/gen-id';
type Domain = {
name: string;
@@ -33,7 +35,6 @@ export default class Logger {
private log(level: Level, message: string, data: Record<string, any>, important = false, subDomains: Domain[] = [], store = true): void {
if (program.quiet) return;
- if (process.env.NODE_ENV === 'test') return;
if (!this.store) store = false;
if (this.parentLogger) {
@@ -65,15 +66,17 @@ export default class Logger {
console.log(important ? chalk.bold(log) : log);
if (store) {
- Log.insert({
+ const Logs = getRepository(Log);
+ Logs.insert({
+ id: genId(),
createdAt: new Date(),
machine: os.hostname(),
- worker: worker,
+ worker: worker.toString(),
domain: [this.domain].concat(subDomains).map(d => d.name),
level: level,
message: message,
data: data,
- });
+ } as Log);
}
}
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 85201086d4..9ac9223d3c 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -1,61 +1,54 @@
import es from '../../db/elasticsearch';
-import Note, { pack, INote, IChoice } from '../../models/note';
-import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
-import { publishMainStream, publishHomeTimelineStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../stream';
-import Following from '../../models/following';
+import { publishMainStream, publishNotesStream } from '../stream';
import { deliver } from '../../queue';
import renderNote from '../../remote/activitypub/renderer/note';
import renderCreate from '../../remote/activitypub/renderer/create';
import renderAnnounce from '../../remote/activitypub/renderer/announce';
import { renderActivity } from '../../remote/activitypub/renderer';
-import DriveFile, { IDriveFile } from '../../models/drive-file';
-import notify from '../../services/create-notification';
-import NoteWatching from '../../models/note-watching';
import watch from './watch';
-import Mute from '../../models/mute';
import { parse } from '../../mfm/parse';
-import { IApp } from '../../models/app';
-import UserList from '../../models/user-list';
import resolveUser from '../../remote/resolve-user';
-import Meta from '../../models/meta';
import config from '../../config';
import { updateHashtag } from '../update-hashtag';
-import isQuote from '../../misc/is-quote';
-import notesChart from '../../services/chart/notes';
-import perUserNotesChart from '../../services/chart/per-user-notes';
-import activeUsersChart from '../../services/chart/active-users';
-import instanceChart from '../../services/chart/instance';
-import * as deepcopy from 'deepcopy';
-
import { erase, concat } from '../../prelude/array';
import insertNoteUnread from './unread';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
import extractMentions from '../../misc/extract-mentions';
import extractEmojis from '../../misc/extract-emojis';
import extractHashtags from '../../misc/extract-hashtags';
+import { Note } from '../../models/entities/note';
+import { Mutings, Users, NoteWatchings, Followings, Notes, Instances, Polls } from '../../models';
+import { DriveFile } from '../../models/entities/drive-file';
+import { App } from '../../models/entities/app';
+import { Not } from 'typeorm';
+import { User, ILocalUser, IRemoteUser } from '../../models/entities/user';
+import { genId } from '../../misc/gen-id';
+import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '../chart';
+import { Poll, IPoll } from '../../models/entities/poll';
+import { createNotification } from '../create-notification';
+import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
class NotificationManager {
- private notifier: IUser;
- private note: INote;
+ private notifier: User;
+ private note: Note;
private queue: {
- target: ILocalUser['_id'];
+ target: ILocalUser['id'];
reason: NotificationType;
}[];
- constructor(notifier: IUser, note: INote) {
+ constructor(notifier: User, note: Note) {
this.notifier = notifier;
this.note = note;
this.queue = [];
}
- public push(notifiee: ILocalUser['_id'], reason: NotificationType) {
+ public push(notifiee: ILocalUser['id'], reason: NotificationType) {
// 自分自身へは通知しない
- if (this.notifier._id.equals(notifiee)) return;
+ if (this.notifier.id === notifiee) return;
- const exist = this.queue.find(x => x.target.equals(notifiee));
+ const exist = this.queue.find(x => x.target === notifiee);
if (exist) {
// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
@@ -73,16 +66,16 @@ class NotificationManager {
public async deliver() {
for (const x of this.queue) {
// ミュート情報を取得
- const mentioneeMutes = await Mute.find({
+ const mentioneeMutes = await Mutings.find({
muterId: x.target
});
- const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
+ const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId);
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
- if (!mentioneesMutedUserIds.includes(this.notifier._id.toString())) {
- notify(x.target, this.notifier._id, x.reason, {
- noteId: this.note._id
+ if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
+ createNotification(x.target, this.notifier.id, x.reason, {
+ noteId: this.note.id
});
}
}
@@ -93,25 +86,25 @@ type Option = {
createdAt?: Date;
name?: string;
text?: string;
- reply?: INote;
- renote?: INote;
- files?: IDriveFile[];
+ reply?: Note;
+ renote?: Note;
+ files?: DriveFile[];
geo?: any;
- poll?: any;
+ poll?: IPoll;
viaMobile?: boolean;
localOnly?: boolean;
cw?: string;
visibility?: string;
- visibleUsers?: IUser[];
- apMentions?: IUser[];
+ visibleUsers?: User[];
+ apMentions?: User[];
apHashtags?: string[];
apEmojis?: string[];
questionUri?: string;
uri?: string;
- app?: IApp;
+ app?: App;
};
-export default async (user: IUser, data: Option, silent = false) => new Promise<INote>(async (res, rej) => {
+export default async (user: User, data: Option, silent = false) => new Promise<Note>(async (res, rej) => {
const isFirstNote = user.notesCount === 0;
if (data.createdAt == null) data.createdAt = new Date();
@@ -128,16 +121,6 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
data.visibleUsers = erase(null, data.visibleUsers);
}
- // リプライ対象が削除された投稿だったらreject
- if (data.reply && data.reply.deletedAt != null) {
- return rej('Reply target has been deleted');
- }
-
- // Renote対象が削除された投稿だったらreject
- if (data.renote && data.renote.deletedAt != null) {
- return rej('Renote target has been deleted');
- }
-
// Renote対象が「ホームまたは全体」以外の公開範囲ならreject
if (data.renote && data.renote.visibility != 'public' && data.renote.visibility != 'home') {
return rej('Renote target is not public or home');
@@ -176,7 +159,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
const tokens = data.text ? parse(data.text) : [];
const cwTokens = data.cw ? parse(data.cw) : [];
const choiceTokens = data.poll && data.poll.choices
- ? concat((data.poll.choices as IChoice[]).map(choice => parse(choice.text)))
+ ? concat(data.poll.choices.map(choice => parse(choice)))
: [];
const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
@@ -188,24 +171,21 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens);
}
- // MongoDBのインデックス対象は128文字以上にできない
tags = tags.filter(tag => tag.length <= 100);
- if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) {
- mentionedUsers.push(await User.findOne({ _id: data.reply.userId }));
+ if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply.userId)) {
+ mentionedUsers.push(await Users.findOne(data.reply.userId));
}
if (data.visibility == 'specified') {
for (const u of data.visibleUsers) {
- if (!mentionedUsers.some(x => x._id.equals(u._id))) {
+ if (!mentionedUsers.some(x => x.id === u.id)) {
mentionedUsers.push(u);
}
}
- for (const u of mentionedUsers) {
- if (!data.visibleUsers.some(x => x._id.equals(u._id))) {
- data.visibleUsers.push(u);
- }
+ if (data.reply && !data.visibleUsers.some(x => x.id === data.reply.userId)) {
+ data.visibleUsers.push(await Users.findOne(data.reply.userId));
}
}
@@ -221,17 +201,12 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
notesChart.update(note, true);
perUserNotesChart.update(user, note, true);
// ローカルユーザーのチャートはタイムライン取得時に更新しているのでリモートユーザーの場合だけでよい
- if (isRemoteUser(user)) activeUsersChart.update(user);
+ if (Users.isRemoteUser(user)) activeUsersChart.update(user);
// Register host
- if (isRemoteUser(user)) {
+ if (Users.isRemoteUser(user)) {
registerOrFetchInstanceDoc(user.host).then(i => {
- Instance.update({ _id: i._id }, {
- $inc: {
- notesCount: 1
- }
- });
-
+ Instances.increment({ id: i.id }, 'notesCount', 1);
instanceChart.updateNote(i.host, true);
});
}
@@ -239,20 +214,6 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
// ハッシュタグ更新
for (const tag of tags) updateHashtag(user, tag);
- // ファイルが添付されていた場合ドライブのファイルの「このファイルが添付された投稿一覧」プロパティにこの投稿を追加
- if (data.files) {
- for (const file of data.files) {
- DriveFile.update({ _id: file._id }, {
- $push: {
- 'metadata.attachedNoteIds': note._id
- }
- });
- }
- }
-
- // Increment notes count
- incNotesCount(user);
-
// Increment notes count (user)
incNotesCountOfUser(user);
@@ -275,20 +236,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
incRenoteCount(data.renote);
}
- if (isQuote(note)) {
- saveQuote(data.renote, note);
- }
-
// Pack the note
- const noteObj = await pack(note);
+ const noteObj = await Notes.pack(note);
if (isFirstNote) {
noteObj.isFirstNote = true;
}
- if (tags.length > 0) {
- publishHashtagStream(noteObj);
- }
+ publishNotesStream(noteObj);
const nm = new NotificationManager(user, note);
const nmRelatedPromises = [];
@@ -297,7 +252,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
const noteActivity = await renderNoteOrRenoteActivity(data, note);
- if (isLocalUser(user)) {
+ if (Users.isLocalUser(user)) {
deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity);
}
@@ -307,12 +262,12 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm));
// この投稿をWatchする
- if (isLocalUser(user) && user.settings.autoWatch !== false) {
- watch(user._id, data.reply);
+ if (Users.isLocalUser(user) && user.autoWatch !== false) {
+ watch(user.id, data.reply);
}
// 通知
- if (isLocalUser(data.reply._user)) {
+ if (data.reply.userHost === null) {
nm.push(data.reply.userId, 'reply');
publishMainStream(data.reply.userId, 'reply', noteObj);
}
@@ -323,7 +278,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
const type = data.text ? 'quote' : 'renote';
// Notify
- if (isLocalUser(data.renote._user)) {
+ if (data.renote.userHost === null) {
nm.push(data.renote.userId, type);
}
@@ -331,18 +286,18 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type));
// この投稿をWatchする
- if (isLocalUser(user) && user.settings.autoWatch !== false) {
- watch(user._id, data.renote);
+ if (Users.isLocalUser(user) && user.autoWatch !== false) {
+ watch(user.id, data.renote);
}
// Publish event
- if (!user._id.equals(data.renote.userId) && isLocalUser(data.renote._user)) {
+ if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
publishMainStream(data.renote.userId, 'renote', noteObj);
}
}
if (!silent) {
- publish(user, note, noteObj, data.reply, data.renote, data.visibleUsers, noteActivity);
+ publish(user, note, data.reply, data.renote, noteActivity);
}
Promise.all(nmRelatedPromises).then(() => {
@@ -353,245 +308,166 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
index(note);
});
-async function renderNoteOrRenoteActivity(data: Option, note: INote) {
+async function renderNoteOrRenoteActivity(data: Option, note: Note) {
if (data.localOnly) return null;
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
- ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note)
+ ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note)
: renderCreate(await renderNote(note, false), note);
return renderActivity(content);
}
-function incRenoteCount(renote: INote) {
- Note.update({ _id: renote._id }, {
- $inc: {
- renoteCount: 1,
- score: 1
- }
- });
+function incRenoteCount(renote: Note) {
+ Notes.increment({ id: renote.id }, 'renoteCount', 1);
+ Notes.increment({ id: renote.id }, 'score', 1);
}
-async function publish(user: IUser, note: INote, noteObj: any, reply: INote, renote: INote, visibleUsers: IUser[], noteActivity: any) {
- if (isLocalUser(user)) {
+async function publish(user: User, note: Note, reply: Note, renote: Note, noteActivity: any) {
+ if (Users.isLocalUser(user)) {
// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
- if (reply && isRemoteUser(reply._user)) {
- deliver(user, noteActivity, reply._user.inbox);
+ if (reply && reply.userHost !== null) {
+ deliver(user, noteActivity, reply.userInbox);
}
// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
- if (renote && isRemoteUser(renote._user)) {
- deliver(user, noteActivity, renote._user.inbox);
+ if (renote && renote.userHost !== null) {
+ deliver(user, noteActivity, renote.userInbox);
}
-
- if (['followers', 'specified'].includes(note.visibility)) {
- const detailPackedNote = await pack(note, user, {
- detail: true
- });
- // Publish event to myself's stream
- publishHomeTimelineStream(note.userId, detailPackedNote);
- publishHybridTimelineStream(note.userId, detailPackedNote);
-
- if (note.visibility == 'specified') {
- for (const u of visibleUsers) {
- if (!u._id.equals(user._id)) {
- publishHomeTimelineStream(u._id, detailPackedNote);
- publishHybridTimelineStream(u._id, detailPackedNote);
- }
- }
- }
- } else {
- // Publish event to myself's stream
- publishHomeTimelineStream(note.userId, noteObj);
-
- // Publish note to local and hybrid timeline stream
- if (note.visibility != 'home') {
- publishLocalTimelineStream(noteObj);
- }
-
- if (note.visibility == 'public') {
- publishHybridTimelineStream(null, noteObj);
- } else {
- // Publish event to myself's stream
- publishHybridTimelineStream(note.userId, noteObj);
- }
- }
- }
-
- // Publish note to global timeline stream
- if (note.visibility == 'public' && note.replyId == null) {
- publishGlobalTimelineStream(noteObj);
}
if (['public', 'home', 'followers'].includes(note.visibility)) {
// フォロワーに配信
- publishToFollowers(note, user, noteActivity);
+ publishToFollowers(note, user, noteActivity, reply);
}
-
- // リストに配信
- publishToUserLists(note, noteObj);
}
-async function insertNote(user: IUser, data: Option, tags: string[], emojis: string[], mentionedUsers: IUser[]) {
- const insert: any = {
+async function insertNote(user: User, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) {
+ const insert: Partial<Note> = {
+ id: genId(data.createdAt),
createdAt: data.createdAt,
- fileIds: data.files ? data.files.map(file => file._id) : [],
- replyId: data.reply ? data.reply._id : null,
- renoteId: data.renote ? data.renote._id : null,
+ fileIds: data.files ? data.files.map(file => file.id) : [],
+ replyId: data.reply ? data.reply.id : null,
+ renoteId: data.renote ? data.renote.id : null,
name: data.name,
text: data.text,
- poll: data.poll,
+ hasPoll: data.poll != null,
cw: data.cw == null ? null : data.cw,
- tags,
- tagsLower: tags.map(tag => tag.toLowerCase()),
+ tags: tags.map(tag => tag.toLowerCase()),
emojis,
- userId: user._id,
+ userId: user.id,
viaMobile: data.viaMobile,
localOnly: data.localOnly,
geo: data.geo || null,
- appId: data.app ? data.app._id : null,
- visibility: data.visibility,
+ appId: data.app ? data.app.id : null,
+ visibility: data.visibility as any,
visibleUserIds: data.visibility == 'specified'
? data.visibleUsers
- ? data.visibleUsers.map(u => u._id)
+ ? data.visibleUsers.map(u => u.id)
: []
: [],
+ attachedFileTypes: data.files ? data.files.map(file => file.type) : [],
+
// 以下非正規化データ
- _reply: data.reply ? {
- userId: data.reply.userId,
- user: {
- host: data.reply._user.host
- }
- } : null,
- _renote: data.renote ? {
- userId: data.renote.userId,
- user: {
- host: data.renote._user.host
- }
- } : null,
- _user: {
- host: user.host,
- inbox: isRemoteUser(user) ? user.inbox : undefined
- },
- _files: data.files ? data.files : []
+ replyUserId: data.reply ? data.reply.userId : null,
+ replyUserHost: data.reply ? data.reply.userHost : null,
+ renoteUserId: data.renote ? data.renote.userId : null,
+ renoteUserHost: data.renote ? data.renote.userHost : null,
+ userHost: user.host,
+ userInbox: user.inbox,
};
if (data.uri != null) insert.uri = data.uri;
// Append mentions data
if (mentionedUsers.length > 0) {
- insert.mentions = mentionedUsers.map(u => u._id);
- insert.mentionedRemoteUsers = mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({
+ insert.mentions = mentionedUsers.map(u => u.id);
+ insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => ({
uri: (u as IRemoteUser).uri,
username: u.username,
host: u.host
- }));
+ })));
}
// 投稿を作成
try {
- return await Note.insert(insert);
+ const note = await Notes.save(insert);
+
+ if (note.hasPoll) {
+ await Polls.save({
+ id: genId(),
+ noteId: note.id,
+ choices: data.poll.choices,
+ expiresAt: data.poll.expiresAt,
+ multiple: data.poll.multiple,
+ votes: new Array(data.poll.choices.length).fill(0),
+ noteVisibility: note.visibility,
+ userId: user.id,
+ userHost: user.host
+ } as Poll);
+ }
+
+ return note;
} catch (e) {
// duplicate key error
- if (e.code === 11000) {
+ if (isDuplicateKeyValueError(e)) {
return null;
}
+ console.error(e);
+
throw 'something happened';
}
}
-function index(note: INote) {
+function index(note: Note) {
if (note.text == null || config.elasticsearch == null) return;
es.index({
index: 'misskey',
type: 'note',
- id: note._id.toString(),
+ id: note.id.toString(),
body: {
text: note.text
}
});
}
-async function notifyToWatchersOfRenotee(renote: INote, user: IUser, nm: NotificationManager, type: NotificationType) {
- const watchers = await NoteWatching.find({
- noteId: renote._id,
- userId: { $ne: user._id }
- }, {
- fields: {
- userId: true
- }
- });
+async function notifyToWatchersOfRenotee(renote: Note, user: User, nm: NotificationManager, type: NotificationType) {
+ const watchers = await NoteWatchings.find({
+ noteId: renote.id,
+ userId: Not(user.id)
+ });
for (const watcher of watchers) {
nm.push(watcher.userId, type);
}
}
-async function notifyToWatchersOfReplyee(reply: INote, user: IUser, nm: NotificationManager) {
- const watchers = await NoteWatching.find({
- noteId: reply._id,
- userId: { $ne: user._id }
- }, {
- fields: {
- userId: true
- }
- });
+async function notifyToWatchersOfReplyee(reply: Note, user: User, nm: NotificationManager) {
+ const watchers = await NoteWatchings.find({
+ noteId: reply.id,
+ userId: Not(user.id)
+ });
for (const watcher of watchers) {
nm.push(watcher.userId, 'reply');
}
}
-async function publishToUserLists(note: INote, noteObj: any) {
- const lists = await UserList.find({
- userIds: note.userId
- });
-
- for (const list of lists) {
- if (note.visibility == 'specified') {
- if (note.visibleUserIds.some(id => id.equals(list.userId))) {
- publishUserListStream(list._id, 'note', noteObj);
- }
- } else {
- publishUserListStream(list._id, 'note', noteObj);
- }
- }
-}
-
-async function publishToFollowers(note: INote, user: IUser, noteActivity: any) {
- const detailPackedNote = await pack(note, null, {
- detail: true,
- skipHide: true
- });
-
- const followers = await Following.find({
- followeeId: note.userId,
- followerId: { $ne: note.userId } // バグでフォロワーに自分がいることがあるため
+async function publishToFollowers(note: Note, user: User, noteActivity: any, reply: Note) {
+ const followers = await Followings.find({
+ followeeId: note.userId
});
const queue: string[] = [];
for (const following of followers) {
- const follower = following._follower;
-
- if (isLocalUser(follower)) {
- // この投稿が返信ならスキップ
- if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId))
- continue;
-
- // Publish event to followers stream
- publishHomeTimelineStream(following.followerId, detailPackedNote);
-
- if (isRemoteUser(user) || note.visibility != 'public') {
- publishHybridTimelineStream(following.followerId, detailPackedNote);
- }
- } else {
+ if (following.followerHost !== null) {
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
- if (isLocalUser(user)) {
- const inbox = follower.sharedInbox || follower.inbox;
+ if (Users.isLocalUser(user)) {
+ const inbox = following.followerSharedInbox || following.followerInbox;
if (!queue.includes(inbox)) queue.push(inbox);
}
}
@@ -600,104 +476,52 @@ async function publishToFollowers(note: INote, user: IUser, noteActivity: any) {
for (const inbox of queue) {
deliver(user as any, noteActivity, inbox);
}
-
- // 後方互換製のため、Questionは時間差でNoteでも送る
- // Questionに対応してないインスタンスは、2つめのNoteだけを採用する
- // Questionに対応しているインスタンスは、同IDで採番されている2つめのNoteを無視する
- setTimeout(() => {
- if (noteActivity.object.type === 'Question') {
- const asNote = deepcopy(noteActivity);
-
- asNote.object.type = 'Note';
- asNote.object.content = asNote.object._misskey_fallback_content;
-
- for (const inbox of queue) {
- deliver(user as any, asNote, inbox);
- }
- }
- }, 10 * 1000);
}
-function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocalUser, noteActivity: any) {
- for (const u of mentionedUsers.filter(u => isRemoteUser(u))) {
+function deliverNoteToMentionedRemoteUsers(mentionedUsers: User[], user: ILocalUser, noteActivity: any) {
+ for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) {
deliver(user, noteActivity, (u as IRemoteUser).inbox);
}
}
-async function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) {
- for (const u of mentionedUsers.filter(u => isLocalUser(u))) {
- const detailPackedNote = await pack(note, u, {
+async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager) {
+ for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) {
+ const detailPackedNote = await Notes.pack(note, u, {
detail: true
});
- publishMainStream(u._id, 'mention', detailPackedNote);
+ publishMainStream(u.id, 'mention', detailPackedNote);
// Create notification
- nm.push(u._id, 'mention');
+ nm.push(u.id, 'mention');
}
}
-function saveQuote(renote: INote, note: INote) {
- Note.update({ _id: renote._id }, {
- $push: {
- _quoteIds: note._id
- }
- });
-}
-
-function saveReply(reply: INote, note: INote) {
- Note.update({ _id: reply._id }, {
- $inc: {
- repliesCount: 1
- }
- });
+function saveReply(reply: Note, note: Note) {
+ Notes.increment({ id: reply.id }, 'repliesCount', 1);
}
-function incNotesCountOfUser(user: IUser) {
- User.update({ _id: user._id }, {
- $set: {
- updatedAt: new Date()
- },
- $inc: {
- notesCount: 1
- }
+function incNotesCountOfUser(user: User) {
+ Users.increment({ id: user.id }, 'notesCount', 1);
+ Users.update({ id: user.id }, {
+ updatedAt: new Date()
});
}
-function incNotesCount(user: IUser) {
- if (isLocalUser(user)) {
- Meta.update({}, {
- $inc: {
- 'stats.notesCount': 1,
- 'stats.originalNotesCount': 1
- }
- }, { upsert: true });
- } else {
- Meta.update({}, {
- $inc: {
- 'stats.notesCount': 1
- }
- }, { upsert: true });
- }
-}
-
-async function extractMentionedUsers(user: IUser, tokens: ReturnType<typeof parse>): Promise<IUser[]> {
+async function extractMentionedUsers(user: User, tokens: ReturnType<typeof parse>): Promise<User[]> {
if (tokens == null) return [];
const mentions = extractMentions(tokens);
- let mentionedUsers =
- erase(null, await Promise.all(mentions.map(async m => {
- try {
- return await resolveUser(m.username, m.host ? m.host : user.host);
- } catch (e) {
- return null;
- }
- })));
+ let mentionedUsers = await Promise.all(mentions.map(m =>
+ resolveUser(m.username, m.host || user.host).catch(() => null)
+ ));
+
+ mentionedUsers = mentionedUsers.filter(x => x != null);
// Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) =>
- i === self.findIndex(u2 => u._id.equals(u2._id))
+ i === self.findIndex(u2 => u.id === u2.id)
);
return mentionedUsers;
diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts
index d71c97b2ca..7f04d12cd5 100644
--- a/src/services/note/delete.ts
+++ b/src/services/note/delete.ts
@@ -1,99 +1,50 @@
-import Note, { INote } from '../../models/note';
-import { IUser, isLocalUser, isRemoteUser } from '../../models/user';
import { publishNoteStream } from '../stream';
import renderDelete from '../../remote/activitypub/renderer/delete';
import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
-import Following from '../../models/following';
import renderTombstone from '../../remote/activitypub/renderer/tombstone';
-import notesChart from '../../services/chart/notes';
-import perUserNotesChart from '../../services/chart/per-user-notes';
import config from '../../config';
-import NoteUnread from '../../models/note-unread';
-import read from './read';
-import DriveFile from '../../models/drive-file';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
-import Favorite from '../../models/favorite';
+import { User } from '../../models/entities/user';
+import { Note } from '../../models/entities/note';
+import { Notes, Users, Followings, Instances } from '../../models';
+import { Not } from 'typeorm';
+import { notesChart, perUserNotesChart, instanceChart } from '../chart';
/**
* 投稿を削除します。
* @param user 投稿者
* @param note 投稿
*/
-export default async function(user: IUser, note: INote, quiet = false) {
+export default async function(user: User, note: Note, quiet = false) {
const deletedAt = new Date();
- await Note.update({
- _id: note._id,
- userId: user._id
- }, {
- $set: {
- deletedAt: deletedAt,
- text: null,
- tags: [],
- fileIds: [],
- renoteId: null,
- poll: null,
- geo: null,
- cw: null
- }
+ await Notes.delete({
+ id: note.id,
+ userId: user.id
});
if (note.renoteId) {
- Note.update({ _id: note.renoteId }, {
- $inc: {
- renoteCount: -1,
- score: -1
- },
- $pull: {
- _quoteIds: note._id
- }
- });
- }
-
- // この投稿が関わる未読通知を削除
- NoteUnread.find({
- noteId: note._id
- }).then(unreads => {
- for (const unread of unreads) {
- read(unread.userId, unread.noteId);
- }
- });
-
- // この投稿をお気に入りから削除
- Favorite.remove({
- noteId: note._id
- });
-
- // ファイルが添付されていた場合ドライブのファイルの「このファイルが添付された投稿一覧」プロパティからこの投稿を削除
- if (note.fileIds) {
- for (const fileId of note.fileIds) {
- DriveFile.update({ _id: fileId }, {
- $pull: {
- 'metadata.attachedNoteIds': note._id
- }
- });
- }
+ Notes.decrement({ id: note.renoteId }, 'renoteCount', 1);
+ Notes.decrement({ id: note.renoteId }, 'score', 1);
}
if (!quiet) {
- publishNoteStream(note._id, 'deleted', {
+ publishNoteStream(note.id, 'deleted', {
deletedAt: deletedAt
});
//#region ローカルの投稿なら削除アクティビティを配送
- if (isLocalUser(user)) {
- const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user));
+ if (Users.isLocalUser(user)) {
+ const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user));
- const followings = await Following.find({
- followeeId: user._id,
- '_follower.host': { $ne: null }
+ const followings = await Followings.find({
+ followeeId: user.id,
+ followerHost: Not(null)
});
for (const following of followings) {
- deliver(user, content, following._follower.inbox);
+ deliver(user, content, following.followerInbox);
}
}
//#endregion
@@ -102,14 +53,9 @@ export default async function(user: IUser, note: INote, quiet = false) {
notesChart.update(note, false);
perUserNotesChart.update(user, note, false);
- if (isRemoteUser(user)) {
+ if (Users.isRemoteUser(user)) {
registerOrFetchInstanceDoc(user.host).then(i => {
- Instance.update({ _id: i._id }, {
- $inc: {
- notesCount: -1
- }
- });
-
+ Instances.decrement({ id: i.id }, 'notesCount', 1);
instanceChart.updateNote(i.host, false);
});
}
diff --git a/src/services/note/polls/update.ts b/src/services/note/polls/update.ts
index d4e183889d..ff8e8d59ef 100644
--- a/src/services/note/polls/update.ts
+++ b/src/services/note/polls/update.ts
@@ -1,51 +1,48 @@
-import * as mongo from 'mongodb';
-import Note, { INote } from '../../../models/note';
import { updateQuestion } from '../../../remote/activitypub/models/question';
import ms = require('ms');
import Logger from '../../logger';
-import User, { isLocalUser, isRemoteUser } from '../../../models/user';
-import Following from '../../../models/following';
import renderUpdate from '../../../remote/activitypub/renderer/update';
import { renderActivity } from '../../../remote/activitypub/renderer';
import { deliver } from '../../../queue';
import renderNote from '../../../remote/activitypub/renderer/note';
+import { Users, Notes, Followings } from '../../../models';
+import { Note } from '../../../models/entities/note';
const logger = new Logger('pollsUpdate');
-export async function triggerUpdate(note: INote) {
+export async function triggerUpdate(note: Note) {
if (!note.updatedAt || Date.now() - new Date(note.updatedAt).getTime() > ms('1min')) {
- logger.info(`Updating ${note._id}`);
+ logger.info(`Updating ${note.id}`);
try {
const updated = await updateQuestion(note.uri);
- logger.info(`Updated ${note._id} ${updated ? 'changed' : 'nochange'}`);
+ logger.info(`Updated ${note.id} ${updated ? 'changed' : 'nochange'}`);
} catch (e) {
logger.error(e);
}
}
}
-export async function deliverQuestionUpdate(noteId: mongo.ObjectID) {
- const note = await Note.findOne({
- _id: noteId,
- });
+export async function deliverQuestionUpdate(noteId: Note['id']) {
+ const note = await Notes.findOne(noteId);
- const user = await User.findOne({
- _id: note.userId
- });
+ const user = await Users.findOne(note.userId);
- const followers = await Following.find({
- followeeId: user._id
+ const followers = await Followings.find({
+ followeeId: user.id
});
const queue: string[] = [];
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
- if (isLocalUser(user)) {
+ if (Users.isLocalUser(user)) {
for (const following of followers) {
- const follower = following._follower;
+ const follower = {
+ inbox: following.followerInbox,
+ sharedInbox: following.followerSharedInbox
+ };
- if (isRemoteUser(follower)) {
+ if (following.followerHost !== null) {
const inbox = follower.sharedInbox || follower.inbox;
if (!queue.includes(inbox)) queue.push(inbox);
}
diff --git a/src/services/note/polls/vote.ts b/src/services/note/polls/vote.ts
index a23cdc1cb4..15f1ddffbc 100644
--- a/src/services/note/polls/vote.ts
+++ b/src/services/note/polls/vote.ts
@@ -1,79 +1,74 @@
-import Vote from '../../../models/poll-vote';
-import Note, { INote } from '../../../models/note';
-import Watching from '../../../models/note-watching';
import watch from '../../../services/note/watch';
import { publishNoteStream } from '../../stream';
-import notify from '../../../services/create-notification';
-import { isLocalUser, IUser } from '../../../models/user';
+import { User } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
+import { PollVotes, Users, NoteWatchings, Polls } from '../../../models';
+import { Not } from 'typeorm';
+import { genId } from '../../../misc/gen-id';
+import { createNotification } from '../../create-notification';
-export default (user: IUser, note: INote, choice: number) => new Promise(async (res, rej) => {
- if (!note.poll.choices.some(x => x.id == choice)) return rej('invalid choice param');
+export default (user: User, note: Note, choice: number) => new Promise(async (res, rej) => {
+ const poll = await Polls.findOne({ noteId: note.id });
+
+ // Check whether is valid choice
+ if (poll.choices[choice] == null) return rej('invalid choice param');
// if already voted
- const exist = await Vote.find({
- noteId: note._id,
- userId: user._id
+ const exist = await PollVotes.find({
+ noteId: note.id,
+ userId: user.id
});
- if (note.poll.multiple) {
- if (exist.some(x => x.choice === choice))
+ if (poll.multiple) {
+ if (exist.some(x => x.choice === choice)) {
return rej('already voted');
- } else if (exist.length) {
+ }
+ } else if (exist.length !== 0) {
return rej('already voted');
}
// Create vote
- await Vote.insert({
+ await PollVotes.save({
+ id: genId(),
createdAt: new Date(),
- noteId: note._id,
- userId: user._id,
+ noteId: note.id,
+ userId: user.id,
choice: choice
});
res();
- const inc: any = {};
- inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == choice)}.votes`] = 1;
-
// Increment votes count
- await Note.update({ _id: note._id }, {
- $inc: inc
- });
+ const index = choice + 1; // In SQL, array index is 1 based
+ await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE id = '${poll.id}'`);
- publishNoteStream(note._id, 'pollVoted', {
+ publishNoteStream(note.id, 'pollVoted', {
choice: choice,
- userId: user._id.toHexString()
+ userId: user.id
});
// Notify
- notify(note.userId, user._id, 'poll_vote', {
- noteId: note._id,
+ createNotification(note.userId, user.id, 'pollVote', {
+ noteId: note.id,
choice: choice
});
// Fetch watchers
- Watching
- .find({
- noteId: note._id,
- userId: { $ne: user._id },
- // 削除されたドキュメントは除く
- deletedAt: { $exists: false }
- }, {
- fields: {
- userId: true
- }
- })
- .then(watchers => {
- for (const watcher of watchers) {
- notify(watcher.userId, user._id, 'poll_vote', {
- noteId: note._id,
- choice: choice
- });
- }
- });
+ NoteWatchings.find({
+ noteId: note.id,
+ userId: Not(user.id),
+ })
+ .then(watchers => {
+ for (const watcher of watchers) {
+ createNotification(watcher.userId, user.id, 'pollVote', {
+ noteId: note.id,
+ choice: choice
+ });
+ }
+ });
// ローカルユーザーが投票した場合この投稿をWatchする
- if (isLocalUser(user) && user.settings.autoWatch !== false) {
- watch(user._id, note);
+ if (Users.isLocalUser(user) && user.autoWatch) {
+ watch(user.id, note);
}
});
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
index 4fdaf92ac6..437b213ded 100644
--- a/src/services/note/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -1,21 +1,24 @@
-import { IUser, isLocalUser, isRemoteUser } from '../../../models/user';
-import Note, { INote } from '../../../models/note';
-import NoteReaction from '../../../models/note-reaction';
import { publishNoteStream } from '../../stream';
-import notify from '../../create-notification';
-import NoteWatching from '../../../models/note-watching';
import watch from '../watch';
import renderLike from '../../../remote/activitypub/renderer/like';
import { deliver } from '../../../queue';
import { renderActivity } from '../../../remote/activitypub/renderer';
-import perUserReactionsChart from '../../../services/chart/per-user-reactions';
import { IdentifiableError } from '../../../misc/identifiable-error';
import { toDbReaction } from '../../../misc/reaction-lib';
import fetchMeta from '../../../misc/fetch-meta';
+import { User } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
+import { NoteReactions, Users, NoteWatchings, Notes } from '../../../models';
+import { Not } from 'typeorm';
+import { perUserReactionsChart } from '../../chart';
+import { genId } from '../../../misc/gen-id';
+import { NoteReaction } from '../../../models/entities/note-reaction';
+import { createNotification } from '../../create-notification';
+import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error';
-export default async (user: IUser, note: INote, reaction: string) => {
+export default async (user: User, note: Note, reaction: string) => {
// Myself
- if (note.userId.equals(user._id)) {
+ if (note.userId === user.id) {
throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note');
}
@@ -23,14 +26,15 @@ export default async (user: IUser, note: INote, reaction: string) => {
reaction = await toDbReaction(reaction, meta.enableEmojiReaction);
// Create reaction
- await NoteReaction.insert({
+ await NoteReactions.save({
+ id: genId(),
createdAt: new Date(),
- noteId: note._id,
- userId: user._id,
+ noteId: note.id,
+ userId: user.id,
reaction
- }).catch(e => {
+ } as NoteReaction).catch(e => {
// duplicate key error
- if (e.code === 11000) {
+ if (isDuplicateKeyValueError(e)) {
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298', 'already reacted');
}
@@ -38,59 +42,53 @@ export default async (user: IUser, note: INote, reaction: string) => {
});
// Increment reactions count
- await Note.update({ _id: note._id }, {
- $inc: {
- [`reactionCounts.${reaction}`]: 1,
- score: 1
- }
- });
+ const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
+ await Notes.createQueryBuilder().update()
+ .set({
+ reactions: () => sql,
+ })
+ .where('id = :id', { id: note.id })
+ .execute();
+ // v11 inc score
perUserReactionsChart.update(user, note);
- publishNoteStream(note._id, 'reacted', {
+ publishNoteStream(note.id, 'reacted', {
reaction: reaction,
- userId: user._id
+ userId: user.id
});
// リアクションされたユーザーがローカルユーザーなら通知を作成
- if (isLocalUser(note._user)) {
- notify(note.userId, user._id, 'reaction', {
- noteId: note._id,
+ if (note.userHost === null) {
+ createNotification(note.userId, user.id, 'reaction', {
+ noteId: note.id,
reaction: reaction
});
}
// Fetch watchers
- NoteWatching
- .find({
- noteId: note._id,
- userId: { $ne: user._id }
- }, {
- fields: {
- userId: true
- }
- })
- .then(watchers => {
- for (const watcher of watchers) {
- notify(watcher.userId, user._id, 'reaction', {
- noteId: note._id,
- reaction: reaction
- });
- }
- });
+ NoteWatchings.find({
+ noteId: note.id,
+ userId: Not(user.id)
+ }).then(watchers => {
+ for (const watcher of watchers) {
+ createNotification(watcher.userId, user.id, 'reaction', {
+ noteId: note.id,
+ reaction: reaction
+ });
+ }
+ });
// ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする
- if (isLocalUser(user) && user.settings.autoWatch !== false) {
- watch(user._id, note);
+ if (Users.isLocalUser(user) && user.autoWatch !== false) {
+ watch(user.id, note);
}
//#region 配信
// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
- if (isLocalUser(user) && isRemoteUser(note._user)) {
+ if (Users.isLocalUser(user) && note.userHost !== null) {
const content = renderActivity(renderLike(user, note, reaction));
- deliver(user, content, note._user.inbox);
+ deliver(user, content, note.userInbox);
}
//#endregion
-
- return;
};
diff --git a/src/services/note/reaction/delete.ts b/src/services/note/reaction/delete.ts
index 695534db61..ce180aaeca 100644
--- a/src/services/note/reaction/delete.ts
+++ b/src/services/note/reaction/delete.ts
@@ -1,50 +1,47 @@
-import { IUser, isLocalUser, isRemoteUser } from '../../../models/user';
-import Note, { INote } from '../../../models/note';
-import NoteReaction from '../../../models/note-reaction';
import { publishNoteStream } from '../../stream';
import renderLike from '../../../remote/activitypub/renderer/like';
import renderUndo from '../../../remote/activitypub/renderer/undo';
import { renderActivity } from '../../../remote/activitypub/renderer';
import { deliver } from '../../../queue';
import { IdentifiableError } from '../../../misc/identifiable-error';
+import { User } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
+import { NoteReactions, Users, Notes } from '../../../models';
-export default async (user: IUser, note: INote) => {
+export default async (user: User, note: Note) => {
// if already unreacted
- const exist = await NoteReaction.findOne({
- noteId: note._id,
- userId: user._id,
- deletedAt: { $exists: false }
+ const exist = await NoteReactions.findOne({
+ noteId: note.id,
+ userId: user.id,
});
- if (exist === null) {
+ if (exist == null) {
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
}
// Delete reaction
- await NoteReaction.remove({
- _id: exist._id
- });
-
- const dec: any = {};
- dec[`reactionCounts.${exist.reaction}`] = -1;
+ await NoteReactions.delete(exist.id);
// Decrement reactions count
- Note.update({ _id: note._id }, {
- $inc: dec
- });
+ const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
+ await Notes.createQueryBuilder().update()
+ .set({
+ reactions: () => sql,
+ })
+ .where('id = :id', { id: note.id })
+ .execute();
+ // v11 dec score
- publishNoteStream(note._id, 'unreacted', {
+ publishNoteStream(note.id, 'unreacted', {
reaction: exist.reaction,
- userId: user._id
+ userId: user.id
});
//#region 配信
// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
- if (isLocalUser(user) && isRemoteUser(note._user)) {
+ if (Users.isLocalUser(user) && (note.userHost !== null)) {
const content = renderActivity(renderUndo(renderLike(user, note, exist.reaction), user));
- deliver(user, content, note._user.inbox);
+ deliver(user, content, note.userInbox);
}
//#endregion
-
- return;
};
diff --git a/src/services/note/read.ts b/src/services/note/read.ts
index 8b52445cf0..44d75bd850 100644
--- a/src/services/note/read.ts
+++ b/src/services/note/read.ts
@@ -1,59 +1,35 @@
-import * as mongo from 'mongodb';
-import isObjectId from '../../misc/is-objectid';
import { publishMainStream } from '../stream';
-import User from '../../models/user';
-import NoteUnread from '../../models/note-unread';
+import { Note } from '../../models/entities/note';
+import { User } from '../../models/entities/user';
+import { NoteUnreads } from '../../models';
/**
* Mark a note as read
*/
export default (
- user: string | mongo.ObjectID,
- note: string | mongo.ObjectID
+ userId: User['id'],
+ noteId: Note['id']
) => new Promise<any>(async (resolve, reject) => {
-
- const userId: mongo.ObjectID = isObjectId(user)
- ? user as mongo.ObjectID
- : new mongo.ObjectID(user);
-
- const noteId: mongo.ObjectID = isObjectId(note)
- ? note as mongo.ObjectID
- : new mongo.ObjectID(note);
-
// Remove document
- const res = await NoteUnread.remove({
+ const res = await NoteUnreads.delete({
userId: userId,
noteId: noteId
});
- if (res.deletedCount == 0) {
+ // v11 TODO: https://github.com/typeorm/typeorm/issues/2415
+ if (res.affected == 0) {
return;
}
- const count1 = await NoteUnread
- .count({
- userId: userId,
- isSpecified: false
- }, {
- limit: 1
- });
-
- const count2 = await NoteUnread
- .count({
- userId: userId,
- isSpecified: true
- }, {
- limit: 1
- });
+ const count1 = await NoteUnreads.count({
+ userId: userId,
+ isSpecified: false
+ });
- if (count1 == 0 || count2 == 0) {
- User.update({ _id: userId }, {
- $set: {
- hasUnreadMentions: count1 != 0 || count2 != 0,
- hasUnreadSpecifiedNotes: count2 != 0
- }
- });
- }
+ const count2 = await NoteUnreads.count({
+ userId: userId,
+ isSpecified: true
+ });
if (count1 == 0) {
// 全て既読になったイベントを発行
diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts
index e70c63c765..203cff8d39 100644
--- a/src/services/note/unread.ts
+++ b/src/services/note/unread.ts
@@ -1,47 +1,34 @@
-import NoteUnread from '../../models/note-unread';
-import User, { IUser } from '../../models/user';
-import { INote } from '../../models/note';
-import Mute from '../../models/mute';
+import { Note } from '../../models/entities/note';
import { publishMainStream } from '../stream';
+import { User } from '../../models/entities/user';
+import { Mutings, NoteUnreads } from '../../models';
+import { genId } from '../../misc/gen-id';
-export default async function(user: IUser, note: INote, isSpecified = false) {
+export default async function(user: User, note: Note, isSpecified = false) {
//#region ミュートしているなら無視
- const mute = await Mute.find({
- muterId: user._id
+ const mute = await Mutings.find({
+ muterId: user.id
});
- const mutedUserIds = mute.map(m => m.muteeId.toString());
- if (mutedUserIds.includes(note.userId.toString())) return;
+ if (mute.map(m => m.muteeId).includes(note.userId)) return;
//#endregion
- const unread = await NoteUnread.insert({
- noteId: note._id,
- userId: user._id,
+ const unread = await NoteUnreads.save({
+ id: genId(),
+ noteId: note.id,
+ userId: user.id,
isSpecified,
- _note: {
- userId: note.userId
- }
+ noteUserId: note.userId
});
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(async () => {
- const exist = await NoteUnread.findOne({ _id: unread._id });
+ const exist = await NoteUnreads.findOne(unread.id);
if (exist == null) return;
- User.update({
- _id: user._id
- }, {
- $set: isSpecified ? {
- hasUnreadSpecifiedNotes: true,
- hasUnreadMentions: true
- } : {
- hasUnreadMentions: true
- }
- });
-
- publishMainStream(user._id, 'unreadMention', note._id);
+ publishMainStream(user.id, 'unreadMention', note.id);
if (isSpecified) {
- publishMainStream(user._id, 'unreadSpecifiedNote', note._id);
+ publishMainStream(user.id, 'unreadSpecifiedNote', note.id);
}
}, 2000);
}
diff --git a/src/services/note/unwatch.ts b/src/services/note/unwatch.ts
index ef5783231b..047ac343be 100644
--- a/src/services/note/unwatch.ts
+++ b/src/services/note/unwatch.ts
@@ -1,9 +1,10 @@
-import * as mongodb from 'mongodb';
-import Watching from '../../models/note-watching';
+import { User } from '../../models/entities/user';
+import { NoteWatchings } from '../../models';
+import { Note } from '../../models/entities/note';
-export default async (me: mongodb.ObjectID, note: object) => {
- await Watching.remove({
- noteId: (note as any)._id,
+export default async (me: User['id'], note: Note) => {
+ await NoteWatchings.delete({
+ noteId: note.id,
userId: me
});
};
diff --git a/src/services/note/watch.ts b/src/services/note/watch.ts
index aad53610d8..d3c9553696 100644
--- a/src/services/note/watch.ts
+++ b/src/services/note/watch.ts
@@ -1,25 +1,20 @@
-import * as mongodb from 'mongodb';
-import Watching from '../../models/note-watching';
+import { User } from '../../models/entities/user';
+import { Note } from '../../models/entities/note';
+import { NoteWatchings } from '../../models';
+import { genId } from '../../misc/gen-id';
+import { NoteWatching } from '../../models/entities/note-watching';
-export default async (me: mongodb.ObjectID, note: object) => {
+export default async (me: User['id'], note: Note) => {
// 自分の投稿はwatchできない
- if (me.equals((note as any).userId)) {
+ if (me === note.userId) {
return;
}
- // if watching now
- const exist = await Watching.findOne({
- noteId: (note as any)._id,
- userId: me
- });
-
- if (exist !== null) {
- return;
- }
-
- await Watching.insert({
+ await NoteWatchings.save({
+ id: genId(),
createdAt: new Date(),
- noteId: (note as any)._id,
- userId: me
- });
+ noteId: note.id,
+ userId: me,
+ noteUserId: note.userId
+ } as NoteWatching);
};
diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts
index ceb762b2fa..defd4d6e2d 100644
--- a/src/services/push-notification.ts
+++ b/src/services/push-notification.ts
@@ -1,11 +1,10 @@
import * as push from 'web-push';
-import * as mongo from 'mongodb';
-import Subscription from '../models/sw-subscription';
import config from '../config';
+import { SwSubscriptions } from '../models';
+import { Meta } from '../models/entities/meta';
import fetchMeta from '../misc/fetch-meta';
-import { IMeta } from '../models/meta';
-let meta: IMeta = null;
+let meta: Meta = null;
setInterval(() => {
fetchMeta().then(m => {
@@ -20,15 +19,11 @@ setInterval(() => {
});
}, 3000);
-export default async function(userId: mongo.ObjectID | string, type: string, body?: any) {
+export default async function(userId: string, type: string, body?: any) {
if (!meta.enableServiceWorker) return;
- if (typeof userId === 'string') {
- userId = new mongo.ObjectID(userId);
- }
-
// Fetch
- const subscriptions = await Subscription.find({
+ const subscriptions = await SwSubscriptions.find({
userId: userId
});
@@ -49,7 +44,7 @@ export default async function(userId: mongo.ObjectID | string, type: string, bod
//swLogger.info(err.body);
if (err.statusCode == 410) {
- Subscription.remove({
+ SwSubscriptions.delete({
userId: userId,
endpoint: subscription.endpoint,
auth: subscription.auth,
diff --git a/src/services/register-or-fetch-instance-doc.ts b/src/services/register-or-fetch-instance-doc.ts
index d418cd12ce..c96c8a1e32 100644
--- a/src/services/register-or-fetch-instance-doc.ts
+++ b/src/services/register-or-fetch-instance-doc.ts
@@ -1,15 +1,19 @@
-import Instance, { IInstance } from '../models/instance';
-import federationChart from '../services/chart/federation';
+import { Instance } from '../models/entities/instance';
+import { Instances } from '../models';
+import { federationChart } from './chart';
+import { genId } from '../misc/gen-id';
-export async function registerOrFetchInstanceDoc(host: string): Promise<IInstance> {
+export async function registerOrFetchInstanceDoc(host: string): Promise<Instance> {
if (host == null) return null;
- const index = await Instance.findOne({ host });
+ const index = await Instances.findOne({ host });
if (index == null) {
- const i = await Instance.insert({
+ const i = await Instances.save({
+ id: genId(),
host,
caughtAt: new Date(),
+ lastCommunicatedAt: new Date(),
system: null // TODO
});
diff --git a/src/services/stream.ts b/src/services/stream.ts
index 813c9eb7c0..c1d14b2779 100644
--- a/src/services/stream.ts
+++ b/src/services/stream.ts
@@ -1,8 +1,9 @@
-import * as mongo from 'mongodb';
import redis from '../db/redis';
import Xev from 'xev';
-
-type ID = string | mongo.ObjectID;
+import { User } from '../models/entities/user';
+import { Note } from '../models/entities/note';
+import { UserList } from '../models/entities/user-list';
+import { ReversiGame } from '../models/entities/games/reversi/game';
class Publisher {
private ev: Xev;
@@ -29,66 +30,50 @@ class Publisher {
}
}
- public publishMainStream = (userId: ID, type: string, value?: any): void => {
+ public publishMainStream = (userId: User['id'], type: string, value?: any): void => {
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishDriveStream = (userId: ID, type: string, value?: any): void => {
+ public publishDriveStream = (userId: User['id'], type: string, value?: any): void => {
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishNoteStream = (noteId: ID, type: string, value: any): void => {
+ public publishNoteStream = (noteId: Note['id'], type: string, value: any): void => {
this.publish(`noteStream:${noteId}`, type, {
id: noteId,
body: value
});
}
- public publishUserListStream = (listId: ID, type: string, value?: any): void => {
+ public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => {
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishMessagingStream = (userId: ID, otherpartyId: ID, type: string, value?: any): void => {
+ public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => {
this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishMessagingIndexStream = (userId: ID, type: string, value?: any): void => {
+ public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => {
this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishReversiStream = (userId: ID, type: string, value?: any): void => {
+ public publishReversiStream = (userId: User['id'], type: string, value?: any): void => {
this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishReversiGameStream = (gameId: ID, type: string, value?: any): void => {
+ public publishReversiGameStream = (gameId: ReversiGame['id'], type: string, value?: any): void => {
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishHomeTimelineStream = (userId: ID, note: any): void => {
- this.publish(`homeTimeline:${userId}`, null, note);
- }
-
- public publishLocalTimelineStream = async (note: any): Promise<void> => {
- this.publish('localTimeline', null, note);
- }
-
- public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => {
- this.publish(userId ? `hybridTimeline:${userId}` : 'hybridTimeline', null, note);
- }
-
- public publishGlobalTimelineStream = (note: any): void => {
- this.publish('globalTimeline', null, note);
- }
-
- public publishHashtagStream = (note: any): void => {
- this.publish('hashtag', null, note);
+ public publishNotesStream = (note: any): void => {
+ this.publish('notesStream', null, note);
}
public publishApLogStream = (log: any): void => {
this.publish('apLog', null, log);
}
- public publishAdminStream = (userId: ID, type: string, value?: any): void => {
+ public publishAdminStream = (userId: User['id'], type: string, value?: any): void => {
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
}
@@ -100,15 +85,11 @@ export default publisher;
export const publishMainStream = publisher.publishMainStream;
export const publishDriveStream = publisher.publishDriveStream;
export const publishNoteStream = publisher.publishNoteStream;
+export const publishNotesStream = publisher.publishNotesStream;
export const publishUserListStream = publisher.publishUserListStream;
export const publishMessagingStream = publisher.publishMessagingStream;
export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
export const publishReversiStream = publisher.publishReversiStream;
export const publishReversiGameStream = publisher.publishReversiGameStream;
-export const publishHomeTimelineStream = publisher.publishHomeTimelineStream;
-export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
-export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
-export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
-export const publishHashtagStream = publisher.publishHashtagStream;
export const publishApLogStream = publisher.publishApLogStream;
export const publishAdminStream = publisher.publishAdminStream;
diff --git a/src/services/update-hashtag.ts b/src/services/update-hashtag.ts
index 23c39312c0..6f6d5c4691 100644
--- a/src/services/update-hashtag.ts
+++ b/src/services/update-hashtag.ts
@@ -1,103 +1,104 @@
-import { IUser, isLocalUser, isRemoteUser } from '../models/user';
-import Hashtag from '../models/hashtag';
-import hashtagChart from './chart/hashtag';
+import { User } from '../models/entities/user';
+import { Hashtags, Users } from '../models';
+import { hashtagChart } from './chart';
+import { genId } from '../misc/gen-id';
+import { Hashtag } from '../models/entities/hashtag';
-export async function updateHashtag(user: IUser, tag: string, isUserAttached = false, inc = true) {
+export async function updateHashtag(user: User, tag: string, isUserAttached = false, inc = true) {
tag = tag.toLowerCase();
- const index = await Hashtag.findOne({ tag });
+ const index = await Hashtags.findOne({ name: tag });
if (index == null && !inc) return;
if (index != null) {
- const $push = {} as any;
- const $pull = {} as any;
- const $inc = {} as any;
+ const q = Hashtags.createQueryBuilder('tag').update()
+ .where('tag.name = :name', { name: tag });
+
+ const set = {} as any;
if (isUserAttached) {
if (inc) {
// 自分が初めてこのタグを使ったなら
- if (!index.attachedUserIds.some(id => id.equals(user._id))) {
- $push.attachedUserIds = user._id;
- $inc.attachedUsersCount = 1;
+ if (!index.attachedUserIds.some(id => id === user.id)) {
+ set.attachedUserIds = () => `array_append(tag.attachedUserIds, '${user.id}')`;
+ set.attachedUsersCount = () => `tag.attachedUsersCount + 1`;
}
// 自分が(ローカル内で)初めてこのタグを使ったなら
- if (isLocalUser(user) && !index.attachedLocalUserIds.some(id => id.equals(user._id))) {
- $push.attachedLocalUserIds = user._id;
- $inc.attachedLocalUsersCount = 1;
+ if (Users.isLocalUser(user) && !index.attachedLocalUserIds.some(id => id === user.id)) {
+ set.attachedLocalUserIds = () => `array_append(tag.attachedLocalUserIds, '${user.id}')`;
+ set.attachedLocalUsersCount = () => `tag.attachedLocalUsersCount + 1`;
}
// 自分が(リモートで)初めてこのタグを使ったなら
- if (isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id.equals(user._id))) {
- $push.attachedRemoteUserIds = user._id;
- $inc.attachedRemoteUsersCount = 1;
+ if (Users.isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id === user.id)) {
+ set.attachedRemoteUserIds = () => `array_append(tag.attachedRemoteUserIds, '${user.id}')`;
+ set.attachedRemoteUsersCount = () => `tag.attachedRemoteUsersCount + 1`;
}
} else {
- $pull.attachedUserIds = user._id;
- $inc.attachedUsersCount = -1;
- if (isLocalUser(user)) {
- $pull.attachedLocalUserIds = user._id;
- $inc.attachedLocalUsersCount = -1;
+ set.attachedUserIds = () => `array_remove(tag.attachedUserIds, '${user.id}')`;
+ set.attachedUsersCount = () => `tag.attachedUsersCount - 1`;
+ if (Users.isLocalUser(user)) {
+ set.attachedLocalUserIds = () => `array_remove(tag.attachedLocalUserIds, '${user.id}')`;
+ set.attachedLocalUsersCount = () => `tag.attachedLocalUsersCount - 1`;
} else {
- $pull.attachedRemoteUserIds = user._id;
- $inc.attachedRemoteUsersCount = -1;
+ set.attachedRemoteUserIds = () => `array_remove(tag.attachedRemoteUserIds, '${user.id}')`;
+ set.attachedRemoteUsersCount = () => `tag.attachedRemoteUsersCount - 1`;
}
}
} else {
// 自分が初めてこのタグを使ったなら
- if (!index.mentionedUserIds.some(id => id.equals(user._id))) {
- $push.mentionedUserIds = user._id;
- $inc.mentionedUsersCount = 1;
+ if (!index.mentionedUserIds.some(id => id === user.id)) {
+ set.mentionedUserIds = () => `array_append(tag.mentionedUserIds, '${user.id}')`;
+ set.mentionedUsersCount = () => `tag.mentionedUsersCount + 1`;
}
// 自分が(ローカル内で)初めてこのタグを使ったなら
- if (isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id.equals(user._id))) {
- $push.mentionedLocalUserIds = user._id;
- $inc.mentionedLocalUsersCount = 1;
+ if (Users.isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id === user.id)) {
+ set.mentionedLocalUserIds = () => `array_append(tag.mentionedLocalUserIds, '${user.id}')`;
+ set.mentionedLocalUsersCount = () => `tag.mentionedLocalUsersCount + 1`;
}
// 自分が(リモートで)初めてこのタグを使ったなら
- if (isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id.equals(user._id))) {
- $push.mentionedRemoteUserIds = user._id;
- $inc.mentionedRemoteUsersCount = 1;
+ if (Users.isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id === user.id)) {
+ set.mentionedRemoteUserIds = () => `array_append(tag.mentionedRemoteUserIds, '${user.id}')`;
+ set.mentionedRemoteUsersCount = () => `tag.mentionedRemoteUsersCount + 1`;
}
}
- const q = {} as any;
- if (Object.keys($push).length > 0) q.$push = $push;
- if (Object.keys($pull).length > 0) q.$pull = $pull;
- if (Object.keys($inc).length > 0) q.$inc = $inc;
- if (Object.keys(q).length > 0) Hashtag.update({ tag }, q);
+ q.execute();
} else {
if (isUserAttached) {
- Hashtag.insert({
- tag,
+ Hashtags.save({
+ id: genId(),
+ name: tag,
mentionedUserIds: [],
mentionedUsersCount: 0,
mentionedLocalUserIds: [],
mentionedLocalUsersCount: 0,
mentionedRemoteUserIds: [],
mentionedRemoteUsersCount: 0,
- attachedUserIds: [user._id],
+ attachedUserIds: [user.id],
attachedUsersCount: 1,
- attachedLocalUserIds: isLocalUser(user) ? [user._id] : [],
- attachedLocalUsersCount: isLocalUser(user) ? 1 : 0,
- attachedRemoteUserIds: isRemoteUser(user) ? [user._id] : [],
- attachedRemoteUsersCount: isRemoteUser(user) ? 1 : 0,
- });
+ attachedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [],
+ attachedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0,
+ attachedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [],
+ attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0,
+ } as Hashtag);
} else {
- Hashtag.insert({
- tag,
- mentionedUserIds: [user._id],
+ Hashtags.save({
+ id: genId(),
+ name: tag,
+ mentionedUserIds: [user.id],
mentionedUsersCount: 1,
- mentionedLocalUserIds: isLocalUser(user) ? [user._id] : [],
- mentionedLocalUsersCount: isLocalUser(user) ? 1 : 0,
- mentionedRemoteUserIds: isRemoteUser(user) ? [user._id] : [],
- mentionedRemoteUsersCount: isRemoteUser(user) ? 1 : 0,
+ mentionedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [],
+ mentionedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0,
+ mentionedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [],
+ mentionedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0,
attachedUserIds: [],
attachedUsersCount: 0,
attachedLocalUserIds: [],
attachedLocalUsersCount: 0,
attachedRemoteUserIds: [],
attachedRemoteUsersCount: 0,
- });
+ } as Hashtag);
}
}
diff --git a/src/services/user-list/push.ts b/src/services/user-list/push.ts
index 5ad4a14827..958d54b090 100644
--- a/src/services/user-list/push.ts
+++ b/src/services/user-list/push.ts
@@ -1,21 +1,26 @@
-import { pack as packUser, IUser, isRemoteUser, fetchProxyAccount } from '../../models/user';
-import UserList, { IUserList } from '../../models/user-list';
import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
import renderFollow from '../../remote/activitypub/renderer/follow';
import { publishUserListStream } from '../stream';
+import { User } from '../../models/entities/user';
+import { UserList } from '../../models/entities/user-list';
+import { UserListJoinings, Users } from '../../models';
+import { UserListJoining } from '../../models/entities/user-list-joining';
+import { genId } from '../../misc/gen-id';
+import { fetchProxyAccount } from '../../misc/fetch-proxy-account';
-export async function pushUserToUserList(target: IUser, list: IUserList) {
- await UserList.update({ _id: list._id }, {
- $push: {
- userIds: target._id
- }
- });
+export async function pushUserToUserList(target: User, list: UserList) {
+ await UserListJoinings.save({
+ id: genId(),
+ createdAt: new Date(),
+ userId: target.id,
+ userListId: list.id
+ } as UserListJoining);
- publishUserListStream(list._id, 'userAdded', await packUser(target));
+ publishUserListStream(list.id, 'userAdded', await Users.pack(target));
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
- if (isRemoteUser(target)) {
+ if (Users.isRemoteUser(target)) {
const proxy = await fetchProxyAccount();
const content = renderActivity(renderFollow(proxy, target));
deliver(proxy, content, target.inbox);
diff --git a/src/tools/add-emoji.ts b/src/tools/add-emoji.ts
index 2aa99e37ae..a75798bdad 100644
--- a/src/tools/add-emoji.ts
+++ b/src/tools/add-emoji.ts
@@ -1,9 +1,11 @@
-import Emoji from '../models/emoji';
+import { Emojis } from '../models';
+import { genId } from '../misc/gen-id';
async function main(name: string, url: string, alias?: string): Promise<any> {
const aliases = alias != null ? [ alias ] : [];
- await Emoji.insert({
+ await Emojis.save({
+ id: genId(),
host: null,
name,
url,
diff --git a/src/tools/clean-remote-files.ts b/src/tools/clean-remote-files.ts
index 28c76345c7..f64affea97 100644
--- a/src/tools/clean-remote-files.ts
+++ b/src/tools/clean-remote-files.ts
@@ -1,18 +1,13 @@
import * as promiseLimit from 'promise-limit';
-import DriveFile, { IDriveFile } from '../models/drive-file';
import del from '../services/drive/delete-file';
+import { DriveFiles } from '../models';
+import { Not } from 'typeorm';
+import { DriveFile } from '../models/entities/drive-file';
const limit = promiseLimit(16);
-DriveFile.find({
- 'metadata._user.host': {
- $ne: null
- },
- 'metadata.deletedAt': { $exists: false }
-}, {
- fields: {
- _id: true
- }
+DriveFiles.find({
+ userHost: Not(null)
}).then(async files => {
console.log(`there is ${files.length} files`);
@@ -21,10 +16,10 @@ DriveFile.find({
console.log('ALL DONE');
});
-async function job(file: IDriveFile): Promise<any> {
- file = await DriveFile.findOne({ _id: file._id });
+async function job(file: DriveFile): Promise<any> {
+ file = await DriveFiles.findOne(file.id);
await del(file, true);
- console.log('done', file._id);
+ console.log('done', file.id);
}
diff --git a/src/tools/move-drive-files.ts b/src/tools/move-drive-files.ts
deleted file mode 100644
index 8a1e944503..0000000000
--- a/src/tools/move-drive-files.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import * as Minio from 'minio';
-import * as uuid from 'uuid';
-import * as promiseLimit from 'promise-limit';
-import DriveFile, { DriveFileChunk, getDriveFileBucket, IDriveFile } from '../models/drive-file';
-import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../models/drive-file-thumbnail';
-import config from '../config';
-
-const limit = promiseLimit(16);
-
-DriveFile.find({
- $or: [{
- 'metadata.withoutChunks': { $exists: false }
- }, {
- 'metadata.withoutChunks': false
- }],
- 'metadata.deletedAt': { $exists: false }
-}, {
- fields: {
- _id: true
- }
-}).then(async files => {
- console.log(`there is ${files.length} files`);
-
- await Promise.all(files.map(file => limit(() => job(file))));
-
- console.log('ALL DONE');
-});
-
-async function job(file: IDriveFile): Promise<any> {
- file = await DriveFile.findOne({ _id: file._id });
-
- const minio = new Minio.Client(config.drive.config);
-
- const name = file.filename.substr(0, 50);
- const keyDir = `${config.drive.prefix}/${uuid.v4()}`;
- const key = `${keyDir}/${name}`;
- const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`;
- const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`;
-
- const baseUrl = config.drive.baseUrl
- || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`;
-
- const bucket = await getDriveFileBucket();
- const readable = bucket.openDownloadStream(file._id);
-
- await minio.putObject(config.drive.bucket, key, readable, file.length, {
- 'Content-Type': file.contentType,
- 'Cache-Control': 'max-age=31536000, immutable'
- });
-
- await DriveFile.findOneAndUpdate({ _id: file._id }, {
- $set: {
- 'metadata.withoutChunks': true,
- 'metadata.storage': 'minio',
- 'metadata.storageProps': {
- key: key,
- thumbnailKey: thumbnailKey
- },
- 'metadata.url': `${ baseUrl }/${ keyDir }/${ encodeURIComponent(name) }`,
- }
- });
-
- // チャンクをすべて削除
- await DriveFileChunk.remove({
- files_id: file._id
- });
-
- //#region サムネイルもあれば削除
- const thumbnail = await DriveFileThumbnail.findOne({
- 'metadata.originalId': file._id
- });
-
- if (thumbnail) {
- await DriveFileThumbnailChunk.remove({
- files_id: thumbnail._id
- });
-
- await DriveFileThumbnail.remove({ _id: thumbnail._id });
- }
- //#endregion
-
- console.log('done', file._id);
-}
diff --git a/src/tools/show-signin-history.ts b/src/tools/show-signin-history.ts
index e770710322..584bece6bb 100644
--- a/src/tools/show-signin-history.ts
+++ b/src/tools/show-signin-history.ts
@@ -1,3 +1,5 @@
+import { Users, Signins } from '../models';
+
// node built/tools/show-signin-history username
// => {Success} {Date} {IPAddrsss}
@@ -7,19 +9,16 @@
// node built/tools/show-signin-history username all
// with full request headers
-import User from '../models/user';
-import Signin from '../models/signin';
-
async function main(username: string, headers: string[]) {
- const user = await User.findOne({
+ const user = await Users.findOne({
host: null,
usernameLower: username.toLowerCase(),
});
- if (user === null) throw 'User not found';
+ if (user == null) throw 'User not found';
- const history = await Signin.find({
- userId: user._id
+ const history = await Signins.find({
+ userId: user.id
});
for (const signin of history) {
diff --git a/test/api-visibility.ts b/test/api-visibility.ts
index 8380d54f1d..894d0d0753 100644
--- a/test/api-visibility.ts
+++ b/test/api-visibility.ts
@@ -6,40 +6,33 @@
*
* To specify test:
* > mocha test/api-visibility.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/
-import * as http from 'http';
-import * as assert from 'chai';
-import { async, _signup, _request, _uploadFile, _post, _react, resetDb } from './utils';
-
-const expect = assert.expect;
-
-//#region process
-Error.stackTraceLimit = Infinity;
-// During the test the env variable is set to test
process.env.NODE_ENV = 'test';
-// Display detail of unhandled promise rejection
-process.on('unhandledRejection', console.dir);
-//#endregion
-
-const app = require('../built/server/api').default;
-const db = require('../built/db/mongodb').default;
-
-const server = http.createServer(app.callback());
-
-//#region Utilities
-const request = _request(server);
-const signup = _signup(request);
-const post = _post(request);
-//#endregion
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post } from './utils';
describe('API visibility', () => {
- // Reset database each test
- before(resetDb(db));
+ let p: childProcess.ChildProcess;
+
+ before(done => {
+ p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+ stdio: ['inherit', 'inherit', 'ipc'],
+ env: { NODE_ENV: 'test' }
+ });
+ p.on('message', message => {
+ if (message === 'ok') done();
+ });
+ });
after(() => {
- server.close();
+ p.kill();
});
describe('Note visibility', async () => {
@@ -61,8 +54,6 @@ describe('API visibility', () => {
let fol: any;
/** specified-post */
let spe: any;
- /** private-post */
- let pri: any;
/** public-reply to target's post */
let pubR: any;
@@ -72,8 +63,6 @@ describe('API visibility', () => {
let folR: any;
/** specified-reply to target's post */
let speR: any;
- /** private-reply to target's post */
- let priR: any;
/** public-mention to target */
let pubM: any;
@@ -83,8 +72,6 @@ describe('API visibility', () => {
let folM: any;
/** specified-mention to target */
let speM: any;
- /** private-mention to target */
- let priM: any;
/** reply target post */
let tgt: any;
@@ -112,7 +99,6 @@ describe('API visibility', () => {
home = await post(alice, { text: 'x', visibility: 'home' });
fol = await post(alice, { text: 'x', visibility: 'followers' });
spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
- pri = await post(alice, { text: 'x', visibility: 'private' });
// replies
tgt = await post(target, { text: 'y', visibility: 'public' });
@@ -120,14 +106,12 @@ describe('API visibility', () => {
homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' });
folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
- priR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'private' });
// mentions
pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' });
folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
speM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'specified' });
- priM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'private' });
//#endregion
});
@@ -135,111 +119,90 @@ describe('API visibility', () => {
// public
it('[show] public-postを自分が見れる', async(async () => {
const res = await show(pub.id, alice);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-postをフォロワーが見れる', async(async () => {
const res = await show(pub.id, follower);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-postを非フォロワーが見れる', async(async () => {
const res = await show(pub.id, other);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-postを未認証が見れる', async(async () => {
const res = await show(pub.id, null);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
// home
it('[show] home-postを自分が見れる', async(async () => {
const res = await show(home.id, alice);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-postをフォロワーが見れる', async(async () => {
const res = await show(home.id, follower);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-postを非フォロワーが見れる', async(async () => {
const res = await show(home.id, other);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-postを未認証が見れる', async(async () => {
const res = await show(home.id, null);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
// followers
it('[show] followers-postを自分が見れる', async(async () => {
const res = await show(fol.id, alice);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-postをフォロワーが見れる', async(async () => {
const res = await show(fol.id, follower);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-postを非フォロワーが見れない', async(async () => {
const res = await show(fol.id, other);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] followers-postを未認証が見れない', async(async () => {
const res = await show(fol.id, null);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
// specified
it('[show] specified-postを自分が見れる', async(async () => {
const res = await show(spe.id, alice);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-postを指定ユーザーが見れる', async(async () => {
const res = await show(spe.id, target);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-postをフォロワーが見れない', async(async () => {
const res = await show(spe.id, follower);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-postを非フォロワーが見れない', async(async () => {
const res = await show(spe.id, other);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-postを未認証が見れない', async(async () => {
const res = await show(spe.id, null);
- expect(res.body).have.property('isHidden').eql(true);
- }));
-
- // private
- it('[show] private-postを自分が見れる', async(async () => {
- const res = await show(pri.id, alice);
- expect(res.body).have.property('text').eql('x');
- }));
-
- it('[show] private-postをフォロワーが見れない', async(async () => {
- const res = await show(pri.id, follower);
- expect(res.body).have.property('isHidden').eql(true);
- }));
-
- it('[show] private-postを非フォロワーが見れない', async(async () => {
- const res = await show(pri.id, other);
- expect(res.body).have.property('isHidden').eql(true);
- }));
-
- it('[show] private-postを未認証が見れない', async(async () => {
- const res = await show(pri.id, null);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
//#endregion
@@ -247,131 +210,110 @@ describe('API visibility', () => {
// public
it('[show] public-replyを自分が見れる', async(async () => {
const res = await show(pubR.id, alice);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyをされた人が見れる', async(async () => {
const res = await show(pubR.id, target);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyをフォロワーが見れる', async(async () => {
const res = await show(pubR.id, follower);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyを非フォロワーが見れる', async(async () => {
const res = await show(pubR.id, other);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyを未認証が見れる', async(async () => {
const res = await show(pubR.id, null);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
// home
it('[show] home-replyを自分が見れる', async(async () => {
const res = await show(homeR.id, alice);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyをされた人が見れる', async(async () => {
const res = await show(homeR.id, target);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyをフォロワーが見れる', async(async () => {
const res = await show(homeR.id, follower);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyを非フォロワーが見れる', async(async () => {
const res = await show(homeR.id, other);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyを未認証が見れる', async(async () => {
const res = await show(homeR.id, null);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
// followers
it('[show] followers-replyを自分が見れる', async(async () => {
const res = await show(folR.id, alice);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async(async () => {
const res = await show(folR.id, target);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-replyをフォロワーが見れる', async(async () => {
const res = await show(folR.id, follower);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-replyを非フォロワーが見れない', async(async () => {
const res = await show(folR.id, other);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] followers-replyを未認証が見れない', async(async () => {
const res = await show(folR.id, null);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
// specified
it('[show] specified-replyを自分が見れる', async(async () => {
const res = await show(speR.id, alice);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-replyを指定ユーザーが見れる', async(async () => {
const res = await show(speR.id, target);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-replyをされた人が指定されてなくても見れる', async(async () => {
const res = await show(speR.id, target);
- expect(res.body).have.property('text').eql('x');
+ assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-replyをフォロワーが見れない', async(async () => {
const res = await show(speR.id, follower);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-replyを非フォロワーが見れない', async(async () => {
const res = await show(speR.id, other);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-replyを未認証が見れない', async(async () => {
const res = await show(speR.id, null);
- expect(res.body).have.property('isHidden').eql(true);
- }));
-
- // private
- it('[show] private-replyを自分が見れる', async(async () => {
- const res = await show(priR.id, alice);
- expect(res.body).have.property('text').eql('x');
- }));
-
- it('[show] private-replyをフォロワーが見れない', async(async () => {
- const res = await show(priR.id, follower);
- expect(res.body).have.property('isHidden').eql(true);
- }));
-
- it('[show] private-replyを非フォロワーが見れない', async(async () => {
- const res = await show(priR.id, other);
- expect(res.body).have.property('isHidden').eql(true);
- }));
-
- it('[show] private-replyを未認証が見れない', async(async () => {
- const res = await show(priR.id, null);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
//#endregion
@@ -379,193 +321,172 @@ describe('API visibility', () => {
// public
it('[show] public-mentionを自分が見れる', async(async () => {
const res = await show(pubM.id, alice);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionをされた人が見れる', async(async () => {
const res = await show(pubM.id, target);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionをフォロワーが見れる', async(async () => {
const res = await show(pubM.id, follower);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionを非フォロワーが見れる', async(async () => {
const res = await show(pubM.id, other);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionを未認証が見れる', async(async () => {
const res = await show(pubM.id, null);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
// home
it('[show] home-mentionを自分が見れる', async(async () => {
const res = await show(homeM.id, alice);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionをされた人が見れる', async(async () => {
const res = await show(homeM.id, target);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionをフォロワーが見れる', async(async () => {
const res = await show(homeM.id, follower);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionを非フォロワーが見れる', async(async () => {
const res = await show(homeM.id, other);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionを未認証が見れる', async(async () => {
const res = await show(homeM.id, null);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
// followers
it('[show] followers-mentionを自分が見れる', async(async () => {
const res = await show(folM.id, alice);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
- it('[show] followers-mentionを非フォロワーでもメンションされていれば見れる', async(async () => {
+ it('[show] followers-mentionを非フォロワーがメンションされていても見れない', async(async () => {
const res = await show(folM.id, target);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] followers-mentionをフォロワーが見れる', async(async () => {
const res = await show(folM.id, follower);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
it('[show] followers-mentionを非フォロワーが見れない', async(async () => {
const res = await show(folM.id, other);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] followers-mentionを未認証が見れない', async(async () => {
const res = await show(folM.id, null);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
// specified
it('[show] specified-mentionを自分が見れる', async(async () => {
const res = await show(speM.id, alice);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
it('[show] specified-mentionを指定ユーザーが見れる', async(async () => {
const res = await show(speM.id, target);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.text, '@target x');
}));
- it('[show] specified-mentionをされた人が指定されてなくても見れる', async(async () => {
+ it('[show] specified-mentionをされた人が指定されてなかったら見れない', async(async () => {
const res = await show(speM.id, target);
- expect(res.body).have.property('text').eql('@target x');
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-mentionをフォロワーが見れない', async(async () => {
const res = await show(speM.id, follower);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-mentionを非フォロワーが見れない', async(async () => {
const res = await show(speM.id, other);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-mentionを未認証が見れない', async(async () => {
const res = await show(speM.id, null);
- expect(res.body).have.property('isHidden').eql(true);
- }));
-
- // private
- it('[show] private-mentionを自分が見れる', async(async () => {
- const res = await show(priM.id, alice);
- expect(res.body).have.property('text').eql('@target x');
- }));
-
- it('[show] private-mentionをフォロワーが見れない', async(async () => {
- const res = await show(priM.id, follower);
- expect(res.body).have.property('isHidden').eql(true);
- }));
-
- it('[show] private-mentionを非フォロワーが見れない', async(async () => {
- const res = await show(priM.id, other);
- expect(res.body).have.property('isHidden').eql(true);
- }));
-
- it('[show] private-mentionを未認証が見れない', async(async () => {
- const res = await show(priM.id, null);
- expect(res.body).have.property('isHidden').eql(true);
+ assert.strictEqual(res.body.isHidden, true);
}));
//#endregion
//#region HTL
it('[HTL] public-post が 自分が見れる', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, alice);
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == pub.id);
- expect(notes[0]).have.property('text').eql('x');
+ assert.strictEqual(notes[0].text, 'x');
}));
it('[HTL] public-post が 非フォロワーから見れない', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, other);
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == pub.id);
- expect(notes).length(0);
+ assert.strictEqual(notes.length, 0);
}));
it('[HTL] followers-post が フォロワーから見れる', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, follower);
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == fol.id);
- expect(notes[0]).have.property('text').eql('x');
+ assert.strictEqual(notes[0].text, 'x');
}));
//#endregion
//#region RTL
it('[replies] followers-reply が フォロワーから見れる', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
- expect(notes[0]).have.property('text').eql('x');
+ assert.strictEqual(notes[0].text, 'x');
}));
it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
- expect(notes).length(0);
+ assert.strictEqual(notes.length, 0);
}));
it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
- expect(notes[0]).have.property('text').eql('x');
+ assert.strictEqual(notes[0].text, 'x');
}));
//#endregion
//#region MTL
it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
- expect(notes[0]).have.property('text').eql('x');
+ assert.strictEqual(notes[0].text, 'x');
}));
it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async(async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folM.id);
- expect(notes[0]).have.property('text').eql('@target x');
+ assert.strictEqual(notes[0].text, '@target x');
}));
//#endregion
});
diff --git a/test/api.ts b/test/api.ts
index cc4521d3dc..71443c5730 100644
--- a/test/api.ts
+++ b/test/api.ts
@@ -6,44 +6,35 @@
*
* To specify test:
* > mocha test/api.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/
-import * as http from 'http';
-import * as fs from 'fs';
-import * as assert from 'chai';
-import { async, _signup, _request, _uploadFile, _post, _react, resetDb } from './utils';
-
-const expect = assert.expect;
-
-//#region process
-Error.stackTraceLimit = Infinity;
-
-// During the test the env variable is set to test
process.env.NODE_ENV = 'test';
-// Display detail of unhandled promise rejection
-process.on('unhandledRejection', console.dir);
-//#endregion
-
-const app = require('../built/server/api').default;
-const db = require('../built/db/mongodb').default;
-
-const server = http.createServer(app.callback());
-
-//#region Utilities
-const request = _request(server);
-const signup = _signup(request);
-const post = _post(request);
-const react = _react(request);
-const uploadFile = _uploadFile(server);
-//#endregion
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post, react, uploadFile } from './utils';
describe('API', () => {
- // Reset database each test
- beforeEach(resetDb(db));
+ let p: childProcess.ChildProcess;
- after(() => {
- server.close();
+ beforeEach(done => {
+ p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+ stdio: ['inherit', 'inherit', 'ipc'],
+ env: { NODE_ENV: 'test' }
+ });
+ p.on('message', message => {
+ if (message === 'ok') {
+ done();
+ }
+ });
+ });
+
+ afterEach(() => {
+ p.kill();
});
describe('signup', () => {
@@ -52,7 +43,7 @@ describe('API', () => {
username: 'test.',
password: 'test'
});
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('空のパスワードでアカウントが作成できない', async(async () => {
@@ -60,7 +51,7 @@ describe('API', () => {
username: 'test',
password: ''
});
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('正しくアカウントが作成できる', async(async () => {
@@ -71,9 +62,9 @@ describe('API', () => {
const res = await request('/signup', me);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('username').eql(me.username);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.username, me.username);
}));
it('同じユーザー名のアカウントは作成できない', async(async () => {
@@ -86,7 +77,7 @@ describe('API', () => {
password: 'test'
});
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
});
@@ -102,7 +93,7 @@ describe('API', () => {
password: 'bar'
});
- expect(res).have.status(403);
+ assert.strictEqual(res.status, 403);
}));
it('クエリをインジェクションできない', async(async () => {
@@ -117,7 +108,7 @@ describe('API', () => {
}
});
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('正しい情報でサインインできる', async(async () => {
@@ -131,7 +122,7 @@ describe('API', () => {
password: 'foo'
});
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
}));
});
@@ -149,12 +140,11 @@ describe('API', () => {
birthday: myBirthday
}, me);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('name').eql(myName);
- expect(res.body).have.nested.property('profile').a('object');
- expect(res.body).have.nested.property('profile.location').eql(myLocation);
- expect(res.body).have.nested.property('profile.birthday').eql(myBirthday);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, myName);
+ assert.strictEqual(res.body.location, myLocation);
+ assert.strictEqual(res.body.birthday, myBirthday);
}));
it('名前を空白にできない', async(async () => {
@@ -162,7 +152,7 @@ describe('API', () => {
const res = await request('/i/update', {
name: ' '
}, me);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('誕生日の設定を削除できる', async(async () => {
@@ -175,10 +165,9 @@ describe('API', () => {
birthday: null
}, me);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.nested.property('profile').a('object');
- expect(res.body).have.nested.property('profile.birthday').eql(null);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.birthday, null);
}));
it('不正な誕生日の形式で怒られる', async(async () => {
@@ -186,7 +175,7 @@ describe('API', () => {
const res = await request('/i/update', {
birthday: '2000/09/07'
}, me);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
});
@@ -198,365 +187,23 @@ describe('API', () => {
userId: me.id
}, me);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('id').eql(me.id);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.id, me.id);
}));
it('ユーザーが存在しなかったら怒る', async(async () => {
const res = await request('/users/show', {
userId: '000000000000000000000000'
});
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/users/show', {
userId: 'kyoppie'
});
- expect(res).have.status(400);
- }));
- });
-
- describe('notes/create', () => {
- it('投稿できる', async(async () => {
- const me = await signup();
- const post = {
- text: 'test'
- };
-
- const res = await request('/notes/create', post, me);
-
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('createdNote');
- expect(res.body.createdNote).have.property('text').eql(post.text);
- }));
-
- it('ファイルを添付できる', async(async () => {
- const me = await signup();
- const file = await uploadFile(me);
-
- const res = await request('/notes/create', {
- fileIds: [file.id]
- }, me);
-
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('createdNote');
- expect(res.body.createdNote).have.property('fileIds').eql([file.id]);
- }));
-
- it('他人のファイルは無視', async(async () => {
- const me = await signup({ username: 'alice' });
- const bob = await signup({ username: 'bob' });
- const file = await uploadFile(bob);
-
- const res = await request('/notes/create', {
- text: 'test',
- fileIds: [file.id]
- }, me);
-
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('createdNote');
- expect(res.body.createdNote).have.property('fileIds').eql([]);
- }));
-
- it('存在しないファイルは無視', async(async () => {
- const me = await signup();
-
- const res = await request('/notes/create', {
- text: 'test',
- fileIds: ['000000000000000000000000']
- }, me);
-
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('createdNote');
- expect(res.body.createdNote).have.property('fileIds').eql([]);
- }));
-
- it('不正なファイルIDで怒られる', async(async () => {
- const me = await signup();
- const res = await request('/notes/create', {
- fileIds: ['kyoppie']
- }, me);
- expect(res).have.status(400);
- }));
-
- it('返信できる', async(async () => {
- const bob = await signup({ username: 'bob' });
- const bobPost = await post(bob);
-
- const alice = await signup({ username: 'alice' });
- const alicePost = {
- text: 'test',
- replyId: bobPost.id
- };
-
- const res = await request('/notes/create', alicePost, alice);
-
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('createdNote');
- expect(res.body.createdNote).have.property('text').eql(alicePost.text);
- expect(res.body.createdNote).have.property('replyId').eql(alicePost.replyId);
- expect(res.body.createdNote).have.property('reply');
- expect(res.body.createdNote.reply).have.property('text').eql(alicePost.text);
- }));
-
- it('renoteできる', async(async () => {
- const bob = await signup({ username: 'bob' });
- const bobPost = await post(bob, {
- text: 'test'
- });
-
- const alice = await signup({ username: 'alice' });
- const alicePost = {
- renoteId: bobPost.id
- };
-
- const res = await request('/notes/create', alicePost, alice);
-
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('createdNote');
- expect(res.body.createdNote).have.property('renoteId').eql(alicePost.renoteId);
- expect(res.body.createdNote).have.property('renote');
- expect(res.body.createdNote.renote).have.property('text').eql(bobPost.text);
- }));
-
- it('引用renoteできる', async(async () => {
- const bob = await signup({ username: 'bob' });
- const bobPost = await post(bob, {
- text: 'test'
- });
-
- const alice = await signup({ username: 'alice' });
- const alicePost = {
- text: 'test',
- renoteId: bobPost.id
- };
-
- const res = await request('/notes/create', alicePost, alice);
-
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('createdNote');
- expect(res.body.createdNote).have.property('text').eql(alicePost.text);
- expect(res.body.createdNote).have.property('renoteId').eql(alicePost.renoteId);
- expect(res.body.createdNote).have.property('renote');
- expect(res.body.createdNote.renote).have.property('text').eql(bobPost.text);
- }));
-
- it('文字数ぎりぎりで怒られない', async(async () => {
- const me = await signup();
- const post = {
- text: '!'.repeat(1000)
- };
- const res = await request('/notes/create', post, me);
- expect(res).have.status(200);
- }));
-
- it('文字数オーバーで怒られる', async(async () => {
- const me = await signup();
- const post = {
- text: '!'.repeat(1001)
- };
- const res = await request('/notes/create', post, me);
- expect(res).have.status(400);
- }));
-
- it('存在しないリプライ先で怒られる', async(async () => {
- const me = await signup();
- const post = {
- text: 'test',
- replyId: '000000000000000000000000'
- };
- const res = await request('/notes/create', post, me);
- expect(res).have.status(400);
- }));
-
- it('存在しないrenote対象で怒られる', async(async () => {
- const me = await signup();
- const post = {
- renoteId: '000000000000000000000000'
- };
- const res = await request('/notes/create', post, me);
- expect(res).have.status(400);
- }));
-
- it('不正なリプライ先IDで怒られる', async(async () => {
- const me = await signup();
- const post = {
- text: 'test',
- replyId: 'foo'
- };
- const res = await request('/notes/create', post, me);
- expect(res).have.status(400);
- }));
-
- it('不正なrenote対象IDで怒られる', async(async () => {
- const me = await signup();
- const post = {
- renoteId: 'foo'
- };
- const res = await request('/notes/create', post, me);
- expect(res).have.status(400);
- }));
-
- it('投票を添付できる', async(async () => {
- const me = await signup();
-
- const res = await request('/notes/create', {
- text: 'test',
- poll: {
- choices: ['foo', 'bar']
- }
- }, me);
-
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('createdNote');
- expect(res.body.createdNote).have.property('poll');
- }));
-
- it('投票の選択肢が無くて怒られる', async(async () => {
- const me = await signup();
- const res = await request('/notes/create', {
- poll: {}
- }, me);
- expect(res).have.status(400);
- }));
-
- it('投票の選択肢が無くて怒られる (空の配列)', async(async () => {
- const me = await signup();
- const res = await request('/notes/create', {
- poll: {
- choices: []
- }
- }, me);
- expect(res).have.status(400);
- }));
-
- it('投票の選択肢が1つで怒られる', async(async () => {
- const me = await signup();
- const res = await request('/notes/create', {
- poll: {
- choices: ['Strawberry Pasta']
- }
- }, me);
- expect(res).have.status(400);
- }));
-
- it('投票できる', async(async () => {
- const me = await signup();
-
- const { body } = await request('/notes/create', {
- text: 'test',
- poll: {
- choices: ['sakura', 'izumi', 'ako']
- }
- }, me);
-
- const res = await request('/notes/polls/vote', {
- noteId: body.createdNote.id,
- choice: 1
- }, me);
-
- expect(res).have.status(204);
- }));
-
- it('複数投票できない', async(async () => {
- const me = await signup();
-
- const { body } = await request('/notes/create', {
- text: 'test',
- poll: {
- choices: ['sakura', 'izumi', 'ako']
- }
- }, me);
-
- await request('/notes/polls/vote', {
- noteId: body.createdNote.id,
- choice: 0
- }, me);
-
- const res = await request('/notes/polls/vote', {
- noteId: body.createdNote.id,
- choice: 2
- }, me);
-
- expect(res).have.status(400);
- }));
-
- it('許可されている場合は複数投票できる', async(async () => {
- const me = await signup();
-
- const { body } = await request('/notes/create', {
- text: 'test',
- poll: {
- choices: ['sakura', 'izumi', 'ako'],
- multiple: true
- }
- }, me);
-
- await request('/notes/polls/vote', {
- noteId: body.createdNote.id,
- choice: 0
- }, me);
-
- await request('/notes/polls/vote', {
- noteId: body.createdNote.id,
- choice: 1
- }, me);
-
- const res = await request('/notes/polls/vote', {
- noteId: body.createdNote.id,
- choice: 2
- }, me);
-
- expect(res).have.status(204);
- }));
-
- it('締め切られている場合は投票できない', async(async () => {
- const me = await signup();
-
- const { body } = await request('/notes/create', {
- text: 'test',
- poll: {
- choices: ['sakura', 'izumi', 'ako'],
- expiredAfter: 1
- }
- }, me);
-
- await new Promise(x => setTimeout(x, 2));
-
- const res = await request('/notes/polls/vote', {
- noteId: body.createdNote.id,
- choice: 1
- }, me);
-
- expect(res).have.status(400);
- }));
-
- it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => {
- const alice = await signup({ username: 'alice' });
- const bob = await signup({ username: 'bob' });
- const post = {
- text: '@bob @bob @bob yo'
- };
-
- const res = await request('/notes/create', post, alice);
-
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('createdNote');
- expect(res.body.createdNote).have.property('text').eql(post.text);
-
- const noteDoc = await db.get('notes').findOne({ _id: res.body.createdNote.id });
- expect(noteDoc.mentions.map((id: any) => id.toString())).eql([bob.id.toString()]);
+ assert.strictEqual(res.status, 400);
}));
});
@@ -571,24 +218,24 @@ describe('API', () => {
noteId: myPost.id
}, me);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('id').eql(myPost.id);
- expect(res.body).have.property('text').eql(myPost.text);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.id, myPost.id);
+ assert.strictEqual(res.body.text, myPost.text);
}));
it('投稿が存在しなかったら怒る', async(async () => {
const res = await request('/notes/show', {
noteId: '000000000000000000000000'
});
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/notes/show', {
noteId: 'kyoppie'
});
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
});
@@ -603,7 +250,7 @@ describe('API', () => {
reaction: 'like'
}, alice);
- expect(res).have.status(204);
+ assert.strictEqual(res.status, 204);
}));
it('自分の投稿にはリアクションできない', async(async () => {
@@ -615,7 +262,7 @@ describe('API', () => {
reaction: 'like'
}, me);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('二重にリアクションできない', async(async () => {
@@ -630,7 +277,7 @@ describe('API', () => {
reaction: 'like'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('存在しない投稿にはリアクションできない', async(async () => {
@@ -641,7 +288,7 @@ describe('API', () => {
reaction: 'like'
}, me);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
@@ -649,7 +296,7 @@ describe('API', () => {
const res = await request('/notes/reactions/create', {}, me);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
@@ -660,7 +307,7 @@ describe('API', () => {
reaction: 'like'
}, me);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
});
@@ -673,7 +320,7 @@ describe('API', () => {
userId: alice.id
}, bob);
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
}));
it('既にフォローしている場合は怒る', async(async () => {
@@ -687,7 +334,7 @@ describe('API', () => {
userId: alice.id
}, bob);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーはフォローできない', async(async () => {
@@ -697,7 +344,7 @@ describe('API', () => {
userId: '000000000000000000000000'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('自分自身はフォローできない', async(async () => {
@@ -707,7 +354,7 @@ describe('API', () => {
userId: alice.id
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
@@ -715,7 +362,7 @@ describe('API', () => {
const res = await request('/following/create', {}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
@@ -725,7 +372,7 @@ describe('API', () => {
userId: 'foo'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
});
@@ -741,7 +388,7 @@ describe('API', () => {
userId: alice.id
}, bob);
- expect(res).have.status(200);
+ assert.strictEqual(res.status, 200);
}));
it('フォローしていない場合は怒る', async(async () => {
@@ -752,7 +399,7 @@ describe('API', () => {
userId: alice.id
}, bob);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーはフォロー解除できない', async(async () => {
@@ -762,7 +409,7 @@ describe('API', () => {
userId: '000000000000000000000000'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('自分自身はフォロー解除できない', async(async () => {
@@ -772,7 +419,7 @@ describe('API', () => {
userId: alice.id
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
@@ -780,7 +427,7 @@ describe('API', () => {
const res = await request('/following/delete', {}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
@@ -790,7 +437,7 @@ describe('API', () => {
userId: 'kyoppie'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
});
@@ -799,20 +446,20 @@ describe('API', () => {
it('ドライブ情報を取得できる', async(async () => {
const bob = await signup({ username: 'bob' });
await uploadFile({
- userId: me._id,
- datasize: 256
+ userId: me.id,
+ size: 256
});
await uploadFile({
- userId: me._id,
- datasize: 512
+ userId: me.id,
+ size: 512
});
await uploadFile({
- userId: me._id,
- datasize: 1024
+ userId: me.id,
+ size: 1024
});
const res = await request('/drive', {}, me);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).have.property('usage').eql(1792);
}));*/
});
@@ -821,14 +468,11 @@ describe('API', () => {
it('ファイルを作成できる', async(async () => {
const alice = await signup({ username: 'alice' });
- const res = await assert.request(server)
- .post('/drive/files/create')
- .field('i', alice.token)
- .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
+ const res = await uploadFile(alice);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('name').eql('Lenna.png');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, 'Lenna.png');
}));
it('ファイル無しで怒られる', async(async () => {
@@ -836,21 +480,18 @@ describe('API', () => {
const res = await request('/drive/files/create', {}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('SVGファイルを作成できる', async(async () => {
const izumi = await signup({ username: 'izumi' });
- const res = await assert.request(server)
- .post('/drive/files/create')
- .field('i', izumi.token)
- .attach('file', fs.readFileSync(__dirname + '/resources/image.svg'), 'image.svg');
+ const res = await uploadFile(izumi, __dirname + '/resources/image.svg');
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('name').eql('image.svg');
- expect(res.body).have.property('type').eql('image/svg+xml');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, 'image.svg');
+ assert.strictEqual(res.body.type, 'image/svg+xml');
}));
});
@@ -865,9 +506,9 @@ describe('API', () => {
name: newName
}, alice);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('name').eql(newName);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, newName);
}));
it('他人のファイルは更新できない', async(async () => {
@@ -880,7 +521,7 @@ describe('API', () => {
name: 'いちごパスタ.png'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('親フォルダを更新できる', async(async () => {
@@ -895,9 +536,9 @@ describe('API', () => {
folderId: folder.id
}, alice);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('folderId').eql(folder.id);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.folderId, folder.id);
}));
it('親フォルダを無しにできる', async(async () => {
@@ -918,9 +559,9 @@ describe('API', () => {
folderId: null
}, alice);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('folderId').eql(null);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.folderId, null);
}));
it('他人のフォルダには入れられない', async(async () => {
@@ -936,7 +577,7 @@ describe('API', () => {
folderId: folder.id
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('存在しないフォルダで怒られる', async(async () => {
@@ -948,7 +589,7 @@ describe('API', () => {
folderId: '000000000000000000000000'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('不正なフォルダIDで怒られる', async(async () => {
@@ -960,7 +601,7 @@ describe('API', () => {
folderId: 'foo'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('ファイルが存在しなかったら怒る', async(async () => {
@@ -971,7 +612,7 @@ describe('API', () => {
name: 'いちごパスタ.png'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
@@ -982,7 +623,7 @@ describe('API', () => {
name: 'いちごパスタ.png'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
});
@@ -994,9 +635,9 @@ describe('API', () => {
name: 'test'
}, alice);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('name').eql('test');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, 'test');
}));
});
@@ -1012,9 +653,9 @@ describe('API', () => {
name: 'new name'
}, alice);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('name').eql('new name');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, 'new name');
}));
it('他人のフォルダを更新できない', async(async () => {
@@ -1029,7 +670,7 @@ describe('API', () => {
name: 'new name'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('親フォルダを更新できる', async(async () => {
@@ -1046,9 +687,9 @@ describe('API', () => {
parentId: parentFolder.id
}, alice);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('parentId').eql(parentFolder.id);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.parentId, parentFolder.id);
}));
it('親フォルダを無しに更新できる', async(async () => {
@@ -1069,9 +710,9 @@ describe('API', () => {
parentId: null
}, alice);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('parentId').eql(null);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.parentId, null);
}));
it('他人のフォルダを親フォルダに設定できない', async(async () => {
@@ -1089,7 +730,7 @@ describe('API', () => {
parentId: parentFolder.id
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない', async(async () => {
@@ -1110,7 +751,7 @@ describe('API', () => {
parentId: parentFolder.id
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない(再帰的)', async(async () => {
@@ -1138,7 +779,7 @@ describe('API', () => {
parentId: folderC.id
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない(自身)', async(async () => {
@@ -1166,7 +807,7 @@ describe('API', () => {
parentId: '000000000000000000000000'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('不正な親フォルダIDで怒られる', async(async () => {
@@ -1180,7 +821,7 @@ describe('API', () => {
parentId: 'foo'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('存在しないフォルダを更新できない', async(async () => {
@@ -1190,7 +831,7 @@ describe('API', () => {
folderId: '000000000000000000000000'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('不正なフォルダIDで怒られる', async(async () => {
@@ -1200,7 +841,7 @@ describe('API', () => {
folderId: 'foo'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
});
@@ -1214,9 +855,9 @@ describe('API', () => {
text: 'test'
}, alice);
- expect(res).have.status(200);
- expect(res.body).be.a('object');
- expect(res.body).have.property('text').eql('test');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.text, 'test');
}));
it('自分自身にはメッセージを送信できない', async(async () => {
@@ -1227,7 +868,7 @@ describe('API', () => {
text: 'Yo'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーにはメッセージを送信できない', async(async () => {
@@ -1238,7 +879,7 @@ describe('API', () => {
text: 'test'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('不正なユーザーIDで怒られる', async(async () => {
@@ -1249,7 +890,7 @@ describe('API', () => {
text: 'test'
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('テキストが無くて怒られる', async(async () => {
@@ -1260,7 +901,7 @@ describe('API', () => {
userId: bob.id
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
it('文字数オーバーで怒られる', async(async () => {
@@ -1272,7 +913,7 @@ describe('API', () => {
text: '!'.repeat(1001)
}, alice);
- expect(res).have.status(400);
+ assert.strictEqual(res.status, 400);
}));
});
@@ -1297,9 +938,9 @@ describe('API', () => {
noteId: alicePost.id
}, carol);
- expect(res).have.status(200);
- expect(res.body).be.a('array');
- expect(res.body).length(0);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.length, 0);
}));
});
@@ -1319,10 +960,10 @@ describe('API', () => {
const res = await request('/notes/timeline', {}, bob);
- expect(res).have.status(200);
- expect(res.body).be.a('array');
- expect(res.body).length(1);
- expect(res.body[0].id).equals(alicePost.id);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.length, 1);
+ assert.strictEqual(res.body[0].id, alicePost.id);
}));
});
});
diff --git a/test/chart.ts b/test/chart.ts
new file mode 100644
index 0000000000..b3976b03ba
--- /dev/null
+++ b/test/chart.ts
@@ -0,0 +1,323 @@
+/*
+ * Tests of chart engine
+ *
+ * How to run the tests:
+ * > mocha test/chart.ts --require ts-node/register
+ *
+ * To specify test:
+ * > mocha test/chart.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import * as lolex from 'lolex';
+import { async } from './utils';
+import { getConnection, createConnection } from 'typeorm';
+const config = require('../built/config').default;
+const Chart = require('../built/services/chart/core').default;
+const _TestChart = require('../built/services/chart/charts/schemas/test');
+const _TestGroupedChart = require('../built/services/chart/charts/schemas/test-grouped');
+const _TestUniqueChart = require('../built/services/chart/charts/schemas/test-unique');
+
+function initDb() {
+ try {
+ const conn = getConnection();
+ return Promise.resolve(conn);
+ } catch (e) {}
+
+ return createConnection({
+ type: 'postgres',
+ host: config.db.host,
+ port: config.db.port,
+ username: config.db.user,
+ password: config.db.pass,
+ database: config.db.db,
+ synchronize: true,
+ dropSchema: true,
+ entities: [
+ Chart.schemaToEntity(_TestChart.name, _TestChart.schema),
+ Chart.schemaToEntity(_TestGroupedChart.name, _TestGroupedChart.schema),
+ Chart.schemaToEntity(_TestUniqueChart.name, _TestUniqueChart.schema)
+ ]
+ });
+}
+
+describe('Chart', () => {
+ let testChart: any;
+ let testGroupedChart: any;
+ let testUniqueChart: any;
+ let connection: any;
+ let clock: lolex.InstalledClock<lolex.Clock>;
+
+ before(done => {
+ initDb().then(c => {
+ connection = c;
+ done();
+ });
+ });
+
+ beforeEach(done => {
+ const TestChart = require('../built/services/chart/charts/classes/test').default;
+ testChart = new TestChart();
+
+ const TestGroupedChart = require('../built/services/chart/charts/classes/test-grouped').default;
+ testGroupedChart = new TestGroupedChart();
+
+ const TestUniqueChart = require('../built/services/chart/charts/classes/test-unique').default;
+ testUniqueChart = new TestUniqueChart();
+
+ clock = lolex.install({
+ now: new Date('2000-01-01 00:00:00')
+ });
+
+ connection.synchronize().then(done);
+ });
+
+ afterEach(done => {
+ clock.uninstall();
+ connection.dropDatabase().then(done);
+ });
+
+ it('Can updates', async(async () => {
+ await testChart.increment();
+
+ const chartHours = await testChart.getChart('hour', 3);
+ const chartDays = await testChart.getChart('day', 3);
+
+ assert.deepStrictEqual(chartHours, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [1, 0, 0],
+ total: [1, 0, 0]
+ },
+ });
+
+ assert.deepStrictEqual(chartDays, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [1, 0, 0],
+ total: [1, 0, 0]
+ },
+ });
+ }));
+
+ it('Empty chart', async(async () => {
+ const chartHours = await testChart.getChart('hour', 3);
+ const chartDays = await testChart.getChart('day', 3);
+
+ assert.deepStrictEqual(chartHours, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [0, 0, 0],
+ total: [0, 0, 0]
+ },
+ });
+
+ assert.deepStrictEqual(chartDays, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [0, 0, 0],
+ total: [0, 0, 0]
+ },
+ });
+ }));
+
+ it('Can updates at multiple times at same time', async(async () => {
+ await testChart.increment();
+ await testChart.increment();
+ await testChart.increment();
+
+ const chartHours = await testChart.getChart('hour', 3);
+ const chartDays = await testChart.getChart('day', 3);
+
+ assert.deepStrictEqual(chartHours, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [3, 0, 0],
+ total: [3, 0, 0]
+ },
+ });
+
+ assert.deepStrictEqual(chartDays, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [3, 0, 0],
+ total: [3, 0, 0]
+ },
+ });
+ }));
+
+ it('Can updates at different times', async(async () => {
+ await testChart.increment();
+
+ clock.tick('01:00:00');
+
+ await testChart.increment();
+
+ const chartHours = await testChart.getChart('hour', 3);
+ const chartDays = await testChart.getChart('day', 3);
+
+ assert.deepStrictEqual(chartHours, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [1, 1, 0],
+ total: [2, 1, 0]
+ },
+ });
+
+ assert.deepStrictEqual(chartDays, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [2, 0, 0],
+ total: [2, 0, 0]
+ },
+ });
+ }));
+
+ it('Can padding', async(async () => {
+ await testChart.increment();
+
+ clock.tick('02:00:00');
+
+ await testChart.increment();
+
+ const chartHours = await testChart.getChart('hour', 3);
+ const chartDays = await testChart.getChart('day', 3);
+
+ assert.deepStrictEqual(chartHours, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [1, 0, 1],
+ total: [2, 1, 1]
+ },
+ });
+
+ assert.deepStrictEqual(chartDays, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [2, 0, 0],
+ total: [2, 0, 0]
+ },
+ });
+ }));
+
+ // 要求された範囲にログがひとつもない場合でもパディングできる
+ it('Can padding from past range', async(async () => {
+ await testChart.increment();
+
+ clock.tick('05:00:00');
+
+ const chartHours = await testChart.getChart('hour', 3);
+ const chartDays = await testChart.getChart('day', 3);
+
+ assert.deepStrictEqual(chartHours, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [0, 0, 0],
+ total: [1, 1, 1]
+ },
+ });
+
+ assert.deepStrictEqual(chartDays, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [1, 0, 0],
+ total: [1, 0, 0]
+ },
+ });
+ }));
+
+ // 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
+ // Issue #3190
+ it('Can padding from past range 2', async(async () => {
+ await testChart.increment();
+ clock.tick('05:00:00');
+ await testChart.increment();
+
+ const chartHours = await testChart.getChart('hour', 3);
+ const chartDays = await testChart.getChart('day', 3);
+
+ assert.deepStrictEqual(chartHours, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [1, 0, 0],
+ total: [2, 1, 1]
+ },
+ });
+
+ assert.deepStrictEqual(chartDays, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [2, 0, 0],
+ total: [2, 0, 0]
+ },
+ });
+ }));
+
+ describe('Grouped', () => {
+ it('Can updates', async(async () => {
+ await testGroupedChart.increment('alice');
+
+ const aliceChartHours = await testGroupedChart.getChart('hour', 3, 'alice');
+ const aliceChartDays = await testGroupedChart.getChart('day', 3, 'alice');
+ const bobChartHours = await testGroupedChart.getChart('hour', 3, 'bob');
+ const bobChartDays = await testGroupedChart.getChart('day', 3, 'bob');
+
+ assert.deepStrictEqual(aliceChartHours, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [1, 0, 0],
+ total: [1, 0, 0]
+ },
+ });
+
+ assert.deepStrictEqual(aliceChartDays, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [1, 0, 0],
+ total: [1, 0, 0]
+ },
+ });
+
+ assert.deepStrictEqual(bobChartHours, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [0, 0, 0],
+ total: [0, 0, 0]
+ },
+ });
+
+ assert.deepStrictEqual(bobChartDays, {
+ foo: {
+ dec: [0, 0, 0],
+ inc: [0, 0, 0],
+ total: [0, 0, 0]
+ },
+ });
+ }));
+ });
+
+ describe('Unique increment', () => {
+ it('Can updates', async(async () => {
+ await testUniqueChart.uniqueIncrement('alice');
+ await testUniqueChart.uniqueIncrement('alice');
+ await testUniqueChart.uniqueIncrement('bob');
+
+ const chartHours = await testUniqueChart.getChart('hour', 3);
+ const chartDays = await testUniqueChart.getChart('day', 3);
+
+ assert.deepStrictEqual(chartHours, {
+ foo: [2, 0, 0],
+ });
+
+ assert.deepStrictEqual(chartDays, {
+ foo: [2, 0, 0],
+ });
+ }));
+ });
+});
diff --git a/test/mfm.ts b/test/mfm.ts
index 191ee5e0ed..69260a5415 100644
--- a/test/mfm.ts
+++ b/test/mfm.ts
@@ -6,6 +6,10 @@
*
* To specify test:
* > mocha test/mfm.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/
import * as assert from 'assert';
diff --git a/test/mocha.opts b/test/mocha.opts
index 907011807d..e114c53bd8 100644
--- a/test/mocha.opts
+++ b/test/mocha.opts
@@ -1 +1,2 @@
---timeout 10000
+--timeout 30000
+--slow 1000
diff --git a/test/mute.ts b/test/mute.ts
new file mode 100644
index 0000000000..bf24b55ee5
--- /dev/null
+++ b/test/mute.ts
@@ -0,0 +1,170 @@
+/*
+ * Tests of mute
+ *
+ * How to run the tests:
+ * > mocha test/mute.ts --require ts-node/register
+ *
+ * To specify test:
+ * > mocha test/mute.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post, react, connectStream } from './utils';
+
+describe('Mute', () => {
+ let p: childProcess.ChildProcess;
+
+ // alice mutes carol
+ let alice: any;
+ let bob: any;
+ let carol: any;
+
+ before(done => {
+ p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+ stdio: ['inherit', 'inherit', 'ipc'],
+ env: { NODE_ENV: 'test' }
+ });
+ p.on('message', async message => {
+ if (message === 'ok') {
+ (p.channel as any).onread = () => {};
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ carol = await signup({ username: 'carol' });
+ done();
+ }
+ });
+ });
+
+ after(() => {
+ p.kill();
+ });
+
+ it('ミュート作成', async(async () => {
+ const res = await request('/mute/create', {
+ userId: carol.id
+ }, alice);
+
+ assert.strictEqual(res.status, 204);
+ }));
+
+ it('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async(async () => {
+ const bobNote = await post(bob, { text: '@alice hi' });
+ const carolNote = await post(carol, { text: '@alice hi' });
+
+ const res = await request('/notes/mentions', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+ assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
+ }));
+
+ it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async(async () => {
+ // 状態リセット
+ await request('/i/read-all-unread-notes', {}, alice);
+
+ await post(carol, { text: '@alice hi' });
+
+ const res = await request('/i', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.body.hasUnreadMentions, false);
+ }));
+
+ it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
+ // 状態リセット
+ await request('/i/read-all-unread-notes', {}, alice);
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'main', ({ type }) => {
+ if (type == 'unreadMention') {
+ fired = true;
+ }
+ });
+
+ post(carol, { text: '@alice hi' });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 5000);
+ }));
+
+ it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', () => new Promise(async done => {
+ // 状態リセット
+ await request('/i/read-all-unread-notes', {}, alice);
+ await request('/notifications/mark-all-as-read', {}, alice);
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'main', ({ type }) => {
+ if (type == 'unreadNotification') {
+ fired = true;
+ }
+ });
+
+ post(carol, { text: '@alice hi' });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 5000);
+ }));
+
+ describe('Timeline', () => {
+ it('タイムラインにミュートしているユーザーの投稿が含まれない', async(async () => {
+ const aliceNote = await post(alice);
+ const bobNote = await post(bob);
+ const carolNote = await post(carol);
+
+ const res = await request('/notes/local-timeline', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+ assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+ assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
+ }));
+
+ it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async(async () => {
+ const aliceNote = await post(alice);
+ const carolNote = await post(carol);
+ const bobNote = await post(bob, {
+ renoteId: carolNote.id
+ });
+
+ const res = await request('/notes/local-timeline', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+ assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+ assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
+ }));
+ });
+
+ describe('Notification', () => {
+ it('通知にミュートしているユーザーの通知が含まれない(リアクション)', async(async () => {
+ const aliceNote = await post(alice);
+ await react(bob, aliceNote, 'like');
+ await react(carol, aliceNote, 'like');
+
+ const res = await request('/i/notifications', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some(notification => notification.userId === bob.id), true);
+ assert.strictEqual(res.body.some(notification => notification.userId === carol.id), false);
+ }));
+ });
+});
diff --git a/test/note.ts b/test/note.ts
new file mode 100644
index 0000000000..7a05930eae
--- /dev/null
+++ b/test/note.ts
@@ -0,0 +1,361 @@
+/*
+ * Tests of Note
+ *
+ * How to run the tests:
+ * > mocha test/note.ts --require ts-node/register
+ *
+ * To specify test:
+ * > mocha test/note.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post, uploadFile } from './utils';
+import { Note } from '../built/models/entities/note';
+const initDb = require('../built/db/postgre.js').initDb;
+
+describe('Note', () => {
+ let p: childProcess.ChildProcess;
+ let Notes: any;
+
+ let alice: any;
+ let bob: any;
+
+ before(done => {
+ p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+ stdio: ['inherit', 'inherit', 'ipc'],
+ env: { NODE_ENV: 'test' }
+ });
+ p.on('message', message => {
+ if (message === 'ok') {
+ (p.channel as any).onread = () => {};
+ initDb(true).then(async connection => {
+ Notes = connection.getRepository(Note);
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ done();
+ });
+ }
+ });
+ });
+
+ after(() => {
+ p.kill();
+ });
+
+ it('投稿できる', async(async () => {
+ const post = {
+ text: 'test'
+ };
+
+ const res = await request('/notes/create', post, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.text, post.text);
+ }));
+
+ it('ファイルを添付できる', async(async () => {
+ const file = await uploadFile(alice);
+
+ const res = await request('/notes/create', {
+ fileIds: [file.id]
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]);
+ }));
+
+ it('他人のファイルは無視', async(async () => {
+ const file = await uploadFile(bob);
+
+ const res = await request('/notes/create', {
+ text: 'test',
+ fileIds: [file.id]
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.deepStrictEqual(res.body.createdNote.fileIds, []);
+ }));
+
+ it('存在しないファイルは無視', async(async () => {
+ const res = await request('/notes/create', {
+ text: 'test',
+ fileIds: ['000000000000000000000000']
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.deepStrictEqual(res.body.createdNote.fileIds, []);
+ }));
+
+ it('不正なファイルIDで怒られる', async(async () => {
+ const res = await request('/notes/create', {
+ fileIds: ['kyoppie']
+ }, alice);
+ assert.strictEqual(res.status, 400);
+ }));
+
+ it('返信できる', async(async () => {
+ const bobPost = await post(bob, {
+ text: 'foo'
+ });
+
+ const alicePost = {
+ text: 'bar',
+ replyId: bobPost.id
+ };
+
+ const res = await request('/notes/create', alicePost, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.text, alicePost.text);
+ assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
+ assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
+ }));
+
+ it('renoteできる', async(async () => {
+ const bobPost = await post(bob, {
+ text: 'test'
+ });
+
+ const alicePost = {
+ renoteId: bobPost.id
+ };
+
+ const res = await request('/notes/create', alicePost, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
+ assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
+ }));
+
+ it('引用renoteできる', async(async () => {
+ const bobPost = await post(bob, {
+ text: 'test'
+ });
+
+ const alicePost = {
+ text: 'test',
+ renoteId: bobPost.id
+ };
+
+ const res = await request('/notes/create', alicePost, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.text, alicePost.text);
+ assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
+ assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
+ }));
+
+ it('文字数ぎりぎりで怒られない', async(async () => {
+ const post = {
+ text: '!'.repeat(1000)
+ };
+ const res = await request('/notes/create', post, alice);
+ assert.strictEqual(res.status, 200);
+ }));
+
+ it('文字数オーバーで怒られる', async(async () => {
+ const post = {
+ text: '!'.repeat(1001)
+ };
+ const res = await request('/notes/create', post, alice);
+ assert.strictEqual(res.status, 400);
+ }));
+
+ it('存在しないリプライ先で怒られる', async(async () => {
+ const post = {
+ text: 'test',
+ replyId: '000000000000000000000000'
+ };
+ const res = await request('/notes/create', post, alice);
+ assert.strictEqual(res.status, 400);
+ }));
+
+ it('存在しないrenote対象で怒られる', async(async () => {
+ const post = {
+ renoteId: '000000000000000000000000'
+ };
+ const res = await request('/notes/create', post, alice);
+ assert.strictEqual(res.status, 400);
+ }));
+
+ it('不正なリプライ先IDで怒られる', async(async () => {
+ const post = {
+ text: 'test',
+ replyId: 'foo'
+ };
+ const res = await request('/notes/create', post, alice);
+ assert.strictEqual(res.status, 400);
+ }));
+
+ it('不正なrenote対象IDで怒られる', async(async () => {
+ const post = {
+ renoteId: 'foo'
+ };
+ const res = await request('/notes/create', post, alice);
+ assert.strictEqual(res.status, 400);
+ }));
+
+ it('存在しないユーザーにメンションできる', async(async () => {
+ const post = {
+ text: '@ghost yo'
+ };
+
+ const res = await request('/notes/create', post, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.text, post.text);
+ }));
+
+ it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => {
+ const post = {
+ text: '@bob @bob @bob yo'
+ };
+
+ const res = await request('/notes/create', post, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.text, post.text);
+
+ const noteDoc = await Notes.findOne(res.body.createdNote.id);
+ assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
+ }));
+
+ describe('notes/create', () => {
+ it('投票を添付できる', async(async () => {
+ const res = await request('/notes/create', {
+ text: 'test',
+ poll: {
+ choices: ['foo', 'bar']
+ }
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.poll != null, true);
+ }));
+
+ it('投票の選択肢が無くて怒られる', async(async () => {
+ const res = await request('/notes/create', {
+ poll: {}
+ }, alice);
+ assert.strictEqual(res.status, 400);
+ }));
+
+ it('投票の選択肢が無くて怒られる (空の配列)', async(async () => {
+ const res = await request('/notes/create', {
+ poll: {
+ choices: []
+ }
+ }, alice);
+ assert.strictEqual(res.status, 400);
+ }));
+
+ it('投票の選択肢が1つで怒られる', async(async () => {
+ const res = await request('/notes/create', {
+ poll: {
+ choices: ['Strawberry Pasta']
+ }
+ }, alice);
+ assert.strictEqual(res.status, 400);
+ }));
+
+ it('投票できる', async(async () => {
+ const { body } = await request('/notes/create', {
+ text: 'test',
+ poll: {
+ choices: ['sakura', 'izumi', 'ako']
+ }
+ }, alice);
+
+ const res = await request('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 1
+ }, alice);
+
+ assert.strictEqual(res.status, 204);
+ }));
+
+ it('複数投票できない', async(async () => {
+ const { body } = await request('/notes/create', {
+ text: 'test',
+ poll: {
+ choices: ['sakura', 'izumi', 'ako']
+ }
+ }, alice);
+
+ await request('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 0
+ }, alice);
+
+ const res = await request('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 2
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ }));
+
+ it('許可されている場合は複数投票できる', async(async () => {
+ const { body } = await request('/notes/create', {
+ text: 'test',
+ poll: {
+ choices: ['sakura', 'izumi', 'ako'],
+ multiple: true
+ }
+ }, alice);
+
+ await request('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 0
+ }, alice);
+
+ await request('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 1
+ }, alice);
+
+ const res = await request('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 2
+ }, alice);
+
+ assert.strictEqual(res.status, 204);
+ }));
+
+ it('締め切られている場合は投票できない', async(async () => {
+ const { body } = await request('/notes/create', {
+ text: 'test',
+ poll: {
+ choices: ['sakura', 'izumi', 'ako'],
+ expiredAfter: 1
+ }
+ }, alice);
+
+ await new Promise(x => setTimeout(x, 2));
+
+ const res = await request('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 1
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ }));
+ });
+});
diff --git a/test/reaction-lib.ts b/test/reaction-lib.ts
index 2f6c8ea81b..3a7ff1ab33 100644
--- a/test/reaction-lib.ts
+++ b/test/reaction-lib.ts
@@ -6,6 +6,10 @@
*
* To specify test:
* > mocha test/reaction-lib.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/
/*
diff --git a/test/streaming.ts b/test/streaming.ts
index 500324d520..74a5aaa0b4 100644
--- a/test/streaming.ts
+++ b/test/streaming.ts
@@ -6,143 +6,844 @@
*
* To specify test:
* > mocha test/streaming.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/
-import * as http from 'http';
-import * as WebSocket from 'ws';
+process.env.NODE_ENV = 'test';
+
import * as assert from 'assert';
-import { _signup, _request, _uploadFile, _post, _react, resetDb } from './utils';
+import * as childProcess from 'child_process';
+import { connectStream, signup, request, post } from './utils';
+import { Following } from '../built/models/entities/following';
+const initDb = require('../built/db/postgre.js').initDb;
-//#region process
-Error.stackTraceLimit = Infinity;
+describe('Streaming', () => {
+ let p: childProcess.ChildProcess;
+ let Followings: any;
-// During the test the env variable is set to test
-process.env.NODE_ENV = 'test';
+ beforeEach(done => {
+ p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+ stdio: ['inherit', 'inherit', 'ipc'],
+ env: { NODE_ENV: 'test' }
+ });
+ p.on('message', message => {
+ if (message === 'ok') {
+ (p.channel as any).onread = () => {};
+ initDb(true).then(async connection => {
+ Followings = connection.getRepository(Following);
+ done();
+ });
+ }
+ });
+ });
-// Display detail of unhandled promise rejection
-process.on('unhandledRejection', console.dir);
-//#endregion
+ afterEach(() => {
+ p.kill();
+ });
-const app = require('../built/server/api').default;
-const server = require('../built/server').startServer();
-const db = require('../built/db/mongodb').default;
+ const follow = async (follower, followee) => {
+ await Followings.save({
+ id: 'a',
+ createdAt: new Date(),
+ followerId: follower.id,
+ followeeId: followee.id,
+ followerHost: follower.host,
+ followerInbox: null,
+ followerSharedInbox: null,
+ followeeHost: followee.host,
+ followeeInbox: null,
+ followeeSharedInbox: null
+ });
+ };
-const apiServer = http.createServer(app.callback());
+ it('mention event', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
-//#region Utilities
-const request = _request(apiServer);
-const signup = _signup(request);
-const post = _post(request);
-//#endregion
+ const ws = await connectStream(bob, 'main', ({ type, body }) => {
+ if (type == 'mention') {
+ assert.deepStrictEqual(body.userId, alice.id);
+ ws.close();
+ done();
+ }
+ });
-describe('Streaming', () => {
- // Reset database each test
- beforeEach(resetDb(db));
+ post(alice, {
+ text: 'foo @bob bar'
+ });
+ }));
+
+ it('renote event', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+ const bobNote = await post(bob, {
+ text: 'foo'
+ });
+
+ const ws = await connectStream(bob, 'main', ({ type, body }) => {
+ if (type == 'renote') {
+ assert.deepStrictEqual(body.renoteId, bobNote.id);
+ ws.close();
+ done();
+ }
+ });
+
+ post(alice, {
+ renoteId: bobNote.id
+ });
+ }));
+
+ describe('Home Timeline', () => {
+ it('自分の投稿が流れる', () => new Promise(async done => {
+ const post = {
+ text: 'foo'
+ };
+
+ const me = await signup();
+
+ const ws = await connectStream(me, 'homeTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.text, post.text);
+ ws.close();
+ done();
+ }
+ });
+
+ request('/notes/create', post, me);
+ }));
+
+ it('フォローしているユーザーの投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ // Alice が Bob をフォロー
+ await request('/following/create', {
+ userId: bob.id
+ }, alice);
+
+ const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ ws.close();
+ done();
+ }
+ });
+
+ post(bob, {
+ text: 'foo'
+ });
+ }));
+
+ it('フォローしていないユーザーの投稿は流れない', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ fired = true;
+ }
+ });
+
+ post(bob, {
+ text: 'foo'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+
+ it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ // Alice が Bob をフォロー
+ await request('/following/create', {
+ userId: bob.id
+ }, alice);
+
+ const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ assert.deepStrictEqual(body.text, 'foo');
+ ws.close();
+ done();
+ }
+ });
+
+ // Bob が Alice 宛てのダイレクト投稿
+ post(bob, {
+ text: 'foo',
+ visibility: 'specified',
+ visibleUserIds: [alice.id]
+ });
+ }));
+
+ it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+ const carol = await signup({ username: 'carol' });
+
+ // Alice が Bob をフォロー
+ await request('/following/create', {
+ userId: bob.id
+ }, alice);
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ fired = true;
+ }
+ });
- after(() => {
- server.close();
+ // Bob が Carol 宛てのダイレクト投稿
+ post(bob, {
+ text: 'foo',
+ visibility: 'specified',
+ visibleUserIds: [carol.id]
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 3000);
+ }));
});
- it('投稿がタイムラインに流れる', () => new Promise(async done => {
- const post = {
- text: 'foo'
- };
+ describe('Local Timeline', () => {
+ it('自分の投稿が流れる', () => new Promise(async done => {
+ const me = await signup();
+
+ const ws = await connectStream(me, 'localTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, me.id);
+ ws.close();
+ done();
+ }
+ });
+
+ post(me, {
+ text: 'foo'
+ });
+ }));
- const me = await signup();
- const ws = new WebSocket(`ws://localhost/streaming?i=${me.token}`);
+ it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
- ws.on('open', () => {
- ws.on('message', data => {
- const msg = JSON.parse(data.toString());
- if (msg.type == 'channel' && msg.body.id == 'a') {
- if (msg.body.type == 'note') {
- assert.deepStrictEqual(msg.body.body.text, post.text);
- ws.close();
- done();
- }
- } else if (msg.type == 'connected' && msg.body.id == 'a') {
- request('/notes/create', post, me);
+ const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ ws.close();
+ done();
}
});
- ws.send(JSON.stringify({
- type: 'connect',
- body: {
- channel: 'homeTimeline',
- id: 'a',
- pong: true
+ post(bob, {
+ text: 'foo'
+ });
+ }));
+
+ it('リモートユーザーの投稿は流れない', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob', host: 'example.com' });
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ fired = true;
}
- }));
- });
- }));
+ });
- it('mention event', () => new Promise(async done => {
- const alice = await signup({ username: 'alice' });
- const bob = await signup({ username: 'bob' });
- const aliceNote = {
- text: 'foo @bob bar'
- };
+ post(bob, {
+ text: 'foo'
+ });
- const ws = new WebSocket(`ws://localhost/streaming?i=${bob.token}`);
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 3000);
+ }));
- ws.on('open', () => {
- ws.on('message', data => {
- const msg = JSON.parse(data.toString());
- if (msg.type == 'channel' && msg.body.id == 'a') {
- if (msg.body.type == 'mention') {
- assert.deepStrictEqual(msg.body.body.text, aliceNote.text);
- ws.close();
- done();
- }
- } else if (msg.type == 'connected' && msg.body.id == 'a') {
- request('/notes/create', aliceNote, alice);
+ it('フォローしてたとしてもリモートユーザーの投稿は流れない', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob', host: 'example.com' });
+
+ // Alice が Bob をフォロー
+ await request('/following/create', {
+ userId: bob.id
+ }, alice);
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ fired = true;
}
});
- ws.send(JSON.stringify({
- type: 'connect',
- body: {
- channel: 'main',
- id: 'a',
- pong: true
+ post(bob, {
+ text: 'foo'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+
+ it('ホーム指定の投稿は流れない', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ fired = true;
}
- }));
- });
- }));
+ });
- it('renote event', () => new Promise(async done => {
- const alice = await signup({ username: 'alice' });
- const bob = await signup({ username: 'bob' });
- const bobNote = await post(bob, {
- text: 'foo'
- });
+ // ホーム指定
+ post(bob, {
+ text: 'foo',
+ visibility: 'home'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+
+ it('フォローしているローカルユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
- const ws = new WebSocket(`ws://localhost/streaming?i=${bob.token}`);
+ // Alice が Bob をフォロー
+ await request('/following/create', {
+ userId: bob.id
+ }, alice);
- ws.on('open', () => {
- ws.on('message', data => {
- const msg = JSON.parse(data.toString());
- if (msg.type == 'channel' && msg.body.id == 'a') {
- if (msg.body.type == 'renote') {
- assert.deepStrictEqual(msg.body.body.renoteId, bobNote.id);
- ws.close();
- done();
- }
- } else if (msg.type == 'connected' && msg.body.id == 'a') {
- request('/notes/create', {
- renoteId: bobNote.id
- }, alice);
+ const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ assert.deepStrictEqual(body.text, 'foo');
+ ws.close();
+ done();
}
});
- ws.send(JSON.stringify({
- type: 'connect',
- body: {
- channel: 'main',
- id: 'a',
- pong: true
+ // Bob が Alice 宛てのダイレクト投稿
+ post(bob, {
+ text: 'foo',
+ visibility: 'specified',
+ visibleUserIds: [alice.id]
+ });
+ }));
+
+ it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ fired = true;
}
- }));
- });
- }));
+ });
+
+ // フォロワー宛て投稿
+ post(bob, {
+ text: 'foo',
+ visibility: 'followers'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+ });
+
+ describe('Social Timeline', () => {
+ it('自分の投稿が流れる', () => new Promise(async done => {
+ const me = await signup();
+
+ const ws = await connectStream(me, 'socialTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, me.id);
+ ws.close();
+ done();
+ }
+ });
+
+ post(me, {
+ text: 'foo'
+ });
+ }));
+
+ it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ ws.close();
+ done();
+ }
+ });
+
+ post(bob, {
+ text: 'foo'
+ });
+ }));
+
+ it('フォローしているリモートユーザーの投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob', host: 'example.com' });
+
+ // Alice が Bob をフォロー
+ await follow(alice, bob);
+
+ const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ ws.close();
+ done();
+ }
+ });
+
+ post(bob, {
+ text: 'foo'
+ });
+ }));
+
+ it('フォローしていないリモートユーザーの投稿は流れない', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob', host: 'example.com' });
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ fired = true;
+ }
+ });
+
+ post(bob, {
+ text: 'foo'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+
+ it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ // Alice が Bob をフォロー
+ await request('/following/create', {
+ userId: bob.id
+ }, alice);
+
+ const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ assert.deepStrictEqual(body.text, 'foo');
+ ws.close();
+ done();
+ }
+ });
+
+ // Bob が Alice 宛てのダイレクト投稿
+ post(bob, {
+ text: 'foo',
+ visibility: 'specified',
+ visibleUserIds: [alice.id]
+ });
+ }));
+
+ it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ fired = true;
+ }
+ });
+
+ // フォロワー宛て投稿
+ post(bob, {
+ text: 'foo',
+ visibility: 'followers'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+ });
+
+ describe('Global Timeline', () => {
+ it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ ws.close();
+ done();
+ }
+ });
+
+ post(bob, {
+ text: 'foo'
+ });
+ }));
+
+ it('フォローしていないリモートユーザーの投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob', host: 'example.com' });
+
+ const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ ws.close();
+ done();
+ }
+ });
+
+ post(bob, {
+ text: 'foo'
+ });
+ }));
+ });
+
+ describe('UserList Timeline', () => {
+ it('リストに入れているユーザーの投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ // リスト作成
+ const list = await request('/users/lists/create', {
+ title: 'my list'
+ }, alice).then(x => x.body);
+
+ // Alice が Bob をリスイン
+ await request('/users/lists/push', {
+ listId: list.id,
+ userId: bob.id
+ }, alice);
+
+ const ws = await connectStream(alice, 'userList', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ ws.close();
+ done();
+ }
+ }, {
+ listId: list.id
+ });
+
+ post(bob, {
+ text: 'foo'
+ });
+ }));
+
+ it('リストに入れていないユーザーの投稿は流れない', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ // リスト作成
+ const list = await request('/users/lists/create', {
+ title: 'my list'
+ }, alice).then(x => x.body);
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'userList', ({ type, body }) => {
+ if (type == 'note') {
+ fired = true;
+ }
+ }, {
+ listId: list.id
+ });
+
+ post(bob, {
+ text: 'foo'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+
+ // #4471
+ it('リストに入れているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ // リスト作成
+ const list = await request('/users/lists/create', {
+ title: 'my list'
+ }, alice).then(x => x.body);
+
+ // Alice が Bob をリスイン
+ await request('/users/lists/push', {
+ listId: list.id,
+ userId: bob.id
+ }, alice);
+
+ const ws = await connectStream(alice, 'userList', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.userId, bob.id);
+ assert.deepStrictEqual(body.text, 'foo');
+ ws.close();
+ done();
+ }
+ }, {
+ listId: list.id
+ });
+
+ // Bob が Alice 宛てのダイレクト投稿
+ post(bob, {
+ text: 'foo',
+ visibility: 'specified',
+ visibleUserIds: [alice.id]
+ });
+ }));
+
+ // #4335
+ it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
+ const alice = await signup({ username: 'alice' });
+ const bob = await signup({ username: 'bob' });
+
+ // リスト作成
+ const list = await request('/users/lists/create', {
+ title: 'my list'
+ }, alice).then(x => x.body);
+
+ // Alice が Bob をリスイン
+ await request('/users/lists/push', {
+ listId: list.id,
+ userId: bob.id
+ }, alice);
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'userList', ({ type, body }) => {
+ if (type == 'note') {
+ fired = true;
+ }
+ }, {
+ listId: list.id
+ });
+
+ // フォロワー宛て投稿
+ post(bob, {
+ text: 'foo',
+ visibility: 'followers'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+ });
+
+ describe('Hashtag Timeline', () => {
+ it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => {
+ const me = await signup();
+
+ const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
+ if (type == 'note') {
+ assert.deepStrictEqual(body.text, '#foo');
+ ws.close();
+ done();
+ }
+ }, {
+ q: [
+ ['foo']
+ ]
+ });
+
+ post(me, {
+ text: '#foo'
+ });
+ }));
+
+ it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => {
+ const me = await signup();
+
+ let fooCount = 0;
+ let barCount = 0;
+ let fooBarCount = 0;
+
+ const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
+ if (type == 'note') {
+ if (body.text === '#foo') fooCount++;
+ if (body.text === '#bar') barCount++;
+ if (body.text === '#foo #bar') fooBarCount++;
+ }
+ }, {
+ q: [
+ ['foo', 'bar']
+ ]
+ });
+
+ post(me, {
+ text: '#foo'
+ });
+
+ post(me, {
+ text: '#bar'
+ });
+
+ post(me, {
+ text: '#foo #bar'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fooCount, 0);
+ assert.strictEqual(barCount, 0);
+ assert.strictEqual(fooBarCount, 1);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+
+ it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => {
+ const me = await signup();
+
+ let fooCount = 0;
+ let barCount = 0;
+ let fooBarCount = 0;
+ let piyoCount = 0;
+
+ const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
+ if (type == 'note') {
+ if (body.text === '#foo') fooCount++;
+ if (body.text === '#bar') barCount++;
+ if (body.text === '#foo #bar') fooBarCount++;
+ if (body.text === '#piyo') piyoCount++;
+ }
+ }, {
+ q: [
+ ['foo'],
+ ['bar']
+ ]
+ });
+
+ post(me, {
+ text: '#foo'
+ });
+
+ post(me, {
+ text: '#bar'
+ });
+
+ post(me, {
+ text: '#foo #bar'
+ });
+
+ post(me, {
+ text: '#piyo'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fooCount, 1);
+ assert.strictEqual(barCount, 1);
+ assert.strictEqual(fooBarCount, 1);
+ assert.strictEqual(piyoCount, 0);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+
+ it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => {
+ const me = await signup();
+
+ let fooCount = 0;
+ let barCount = 0;
+ let fooBarCount = 0;
+ let piyoCount = 0;
+ let waaaCount = 0;
+
+ const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
+ if (type == 'note') {
+ if (body.text === '#foo') fooCount++;
+ if (body.text === '#bar') barCount++;
+ if (body.text === '#foo #bar') fooBarCount++;
+ if (body.text === '#piyo') piyoCount++;
+ if (body.text === '#waaa') waaaCount++;
+ }
+ }, {
+ q: [
+ ['foo', 'bar'],
+ ['piyo']
+ ]
+ });
+
+ post(me, {
+ text: '#foo'
+ });
+
+ post(me, {
+ text: '#bar'
+ });
+
+ post(me, {
+ text: '#foo #bar'
+ });
+
+ post(me, {
+ text: '#piyo'
+ });
+
+ post(me, {
+ text: '#waaa'
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fooCount, 0);
+ assert.strictEqual(barCount, 0);
+ assert.strictEqual(fooBarCount, 1);
+ assert.strictEqual(piyoCount, 1);
+ assert.strictEqual(waaaCount, 0);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+ });
});
diff --git a/test/user-notes.ts b/test/user-notes.ts
new file mode 100644
index 0000000000..5e457d6692
--- /dev/null
+++ b/test/user-notes.ts
@@ -0,0 +1,86 @@
+/*
+ * Tests of Note
+ *
+ * How to run the tests:
+ * > mocha test/user-notes.ts --require ts-node/register
+ *
+ * To specify test:
+ * > mocha test/user-notes.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post, uploadFile } from './utils';
+
+describe('users/notes', () => {
+ let p: childProcess.ChildProcess;
+
+ let alice: any;
+ let jpgNote: any;
+ let pngNote: any;
+ let jpgPngNote: any;
+
+ before(done => {
+ p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+ stdio: ['inherit', 'inherit', 'ipc'],
+ env: { NODE_ENV: 'test' }
+ });
+ p.on('message', async message => {
+ if (message === 'ok') {
+ (p.channel as any).onread = () => {};
+
+ alice = await signup({ username: 'alice' });
+ const jpg = await uploadFile(alice, __dirname + '/resources/Lenna.jpg');
+ const png = await uploadFile(alice, __dirname + '/resources/Lenna.png');
+ jpgNote = await post(alice, {
+ fileIds: [jpg.id]
+ });
+ pngNote = await post(alice, {
+ fileIds: [png.id]
+ });
+ jpgPngNote = await post(alice, {
+ fileIds: [jpg.id, png.id]
+ });
+
+ done();
+ }
+ });
+ });
+
+ after(() => {
+ p.kill();
+ });
+
+ it('ファイルタイプ指定 (jpg)', async(async () => {
+ const res = await request('/users/notes', {
+ userId: alice.id,
+ fileType: ['image/jpeg']
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.length, 2);
+ assert.strictEqual(res.body.some(note => note.id === jpgNote.id), true);
+ assert.strictEqual(res.body.some(note => note.id === jpgPngNote.id), true);
+ }));
+
+ it('ファイルタイプ指定 (jpg or png)', async(async () => {
+ const res = await request('/users/notes', {
+ userId: alice.id,
+ fileType: ['image/jpeg', 'image/png']
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.length, 3);
+ assert.strictEqual(res.body.some(note => note.id === jpgNote.id), true);
+ assert.strictEqual(res.body.some(note => note.id === pngNote.id), true);
+ assert.strictEqual(res.body.some(note => note.id === jpgPngNote.id), true);
+ }));
+});
diff --git a/test/utils.ts b/test/utils.ts
index 1377122478..fbba9a68c9 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -1,7 +1,7 @@
import * as fs from 'fs';
-import * as http from 'http';
-import * as assert from 'chai';
-assert.use(require('chai-http'));
+import * as WebSocket from 'ws';
+const fetch = require('node-fetch');
+import * as req from 'request';
export const async = (fn: Function) => (done: Function) => {
fn().then(() => {
@@ -11,19 +11,31 @@ export const async = (fn: Function) => (done: Function) => {
});
};
-export const _request = (server: http.Server) => async (endpoint: string, params: any, me?: any): Promise<ChaiHttp.Response> => {
+export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
const auth = me ? {
i: me.token
} : {};
- const res = await assert.request(server)
- .post(endpoint)
- .send(Object.assign(auth, params));
+ try {
+ const res = await fetch('http://localhost:80/api' + endpoint, {
+ method: 'POST',
+ body: JSON.stringify(Object.assign(auth, params))
+ });
- return res;
+ const status = res.status;
+ const body = res.status !== 204 ? await res.json().catch() : null;
+
+ return {
+ body, status
+ };
+ } catch (e) {
+ return {
+ body: null, status: 500
+ };
+ }
};
-export const _signup = (request: ReturnType<typeof _request>) => async (params?: any): Promise<any> => {
+export const signup = async (params?: any): Promise<any> => {
const q = Object.assign({
username: 'test',
password: 'test'
@@ -34,50 +46,59 @@ export const _signup = (request: ReturnType<typeof _request>) => async (params?:
return res.body;
};
-export const _post = (request: ReturnType<typeof _request>) => async (user: any, params?: any): Promise<any> => {
+export const post = async (user: any, params?: any): Promise<any> => {
const q = Object.assign({
text: 'test'
}, params);
const res = await request('/notes/create', q, user);
- return res.body.createdNote;
+ return res.body ? res.body.createdNote : null;
};
-export const _react = (request: ReturnType<typeof _request>) => async (user: any, note: any, reaction: string): Promise<any> => {
+export const react = async (user: any, note: any, reaction: string): Promise<any> => {
await request('/notes/reactions/create', {
noteId: note.id,
reaction: reaction
}, user);
};
-export const _uploadFile = (server: http.Server) => async (user: any): Promise<any> => {
- const res = await assert.request(server)
- .post('/drive/files/create')
- .field('i', user.token)
- .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
+export const uploadFile = (user: any, path?: string): Promise<any> => new Promise((ok, rej) => {
+ req.post({
+ url: 'http://localhost:80/api/drive/files/create',
+ formData: {
+ i: user.token,
+ file: fs.createReadStream(path || __dirname + '/resources/Lenna.png')
+ },
+ json: true
+ }, (err, httpResponse, body) => {
+ ok(body);
+ });
+});
- return res.body;
-};
+export function connectStream(user: any, channel: string, listener: any, params?: any): Promise<WebSocket> {
+ return new Promise((res, rej) => {
+ const ws = new WebSocket(`ws://localhost/streaming?i=${user.token}`);
-export const resetDb = (db: any) => () => new Promise(res => {
- // APIがなにかレスポンスを返した後に、後処理を行う場合があり、
- // レスポンスを受け取ってすぐデータベースをリセットすると
- // その後処理と競合し(テスト自体は合格するものの)エラーがコンソールに出力され
- // 見た目的に気持ち悪くなるので、後処理が終るのを待つために500msくらい待ってから
- // データベースをリセットするようにする
- setTimeout(async () => {
- await Promise.all([
- db.get('users').drop(),
- db.get('notes').drop(),
- db.get('driveFiles.files').drop(),
- db.get('driveFiles.chunks').drop(),
- db.get('driveFolders').drop(),
- db.get('apps').drop(),
- db.get('accessTokens').drop(),
- db.get('authSessions').drop()
- ]);
+ ws.on('open', () => {
+ ws.on('message', data => {
+ const msg = JSON.parse(data.toString());
+ if (msg.type == 'channel' && msg.body.id == 'a') {
+ listener(msg.body);
+ } else if (msg.type == 'connected' && msg.body.id == 'a') {
+ res(ws);
+ }
+ });
- res();
- }, 500);
-});
+ ws.send(JSON.stringify({
+ type: 'connect',
+ body: {
+ channel: channel,
+ id: 'a',
+ pong: true,
+ params: params
+ }
+ }));
+ });
+ });
+}
diff --git a/tsconfig.json b/tsconfig.json
index 09da750c35..6bd8571207 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -16,6 +16,7 @@
"strict": true,
"strictNullChecks": false,
"experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
"resolveJsonModule": true,
"typeRoots": [
"node_modules/@types",