From f0a29721c9fb10f97faf386bc9d6b1b2fad97895 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 7 Apr 2019 21:50:36 +0900 Subject: Use PostgreSQL instead of MongoDB (#4572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- src/models/abuse-user-report.ts | 52 --- src/models/access-token.ts | 16 - src/models/app.ts | 102 ----- src/models/auth-session.ts | 49 --- src/models/blocking.ts | 56 --- src/models/drive-file-thumbnail.ts | 29 -- src/models/drive-file-webpublic.ts | 29 -- src/models/drive-file.ts | 232 ------------ src/models/drive-folder.ts | 75 ---- src/models/emoji.ts | 21 -- src/models/entities/abuse-user-report.ts | 41 ++ src/models/entities/access-token.ts | 45 +++ src/models/entities/app.ts | 60 +++ src/models/entities/auth-session.ts | 39 ++ src/models/entities/blocking.ts | 42 +++ src/models/entities/drive-file.ts | 154 ++++++++ src/models/entities/drive-folder.ts | 49 +++ src/models/entities/emoji.ts | 46 +++ src/models/entities/follow-request.ts | 85 +++++ src/models/entities/following.ts | 80 ++++ src/models/entities/games/reversi/game.ts | 133 +++++++ src/models/entities/games/reversi/matching.ts | 35 ++ src/models/entities/hashtag.ts | 87 +++++ src/models/entities/instance.ts | 132 +++++++ src/models/entities/log.ts | 46 +++ src/models/entities/messaging-message.ts | 64 ++++ src/models/entities/meta.ts | 264 +++++++++++++ src/models/entities/muting.ts | 42 +++ src/models/entities/note-favorite.ts | 35 ++ src/models/entities/note-reaction.ts | 42 +++ src/models/entities/note-unread.ts | 43 +++ src/models/entities/note-watching.ts | 52 +++ src/models/entities/note.ts | 236 ++++++++++++ src/models/entities/notification.ts | 94 +++++ src/models/entities/poll-vote.ts | 40 ++ src/models/entities/poll.ts | 67 ++++ src/models/entities/registration-tickets.ts | 17 + src/models/entities/signin.ts | 35 ++ src/models/entities/sw-subscription.ts | 37 ++ src/models/entities/user-keypair.ts | 24 ++ src/models/entities/user-list-joining.ts | 41 ++ src/models/entities/user-list.ts | 33 ++ src/models/entities/user-note-pinings.ts | 35 ++ src/models/entities/user-publickey.ts | 30 ++ src/models/entities/user-service-linking.ts | 108 ++++++ src/models/entities/user.ts | 297 +++++++++++++++ src/models/favorite.ts | 65 ---- src/models/follow-request.ts | 66 ---- src/models/following.ts | 27 -- src/models/games/reversi/game.ts | 111 ------ src/models/games/reversi/matching.ts | 45 --- src/models/hashtag.ts | 63 ---- src/models/id.ts | 4 + src/models/index.ts | 74 ++++ src/models/instance.ts | 90 ----- src/models/log.ts | 19 - src/models/messaging-message.ts | 75 ---- src/models/meta.ts | 257 ------------- src/models/mute.ts | 56 --- src/models/note-reaction.ts | 51 --- src/models/note-unread.ts | 19 - src/models/note-watching.ts | 15 - src/models/note.ts | 418 --------------------- src/models/notification.ts | 120 ------ src/models/poll-vote.ts | 17 - src/models/registration-tickets.ts | 12 - src/models/repositories/abuse-user-report.ts | 32 ++ src/models/repositories/app.ts | 36 ++ src/models/repositories/auth-session.ts | 19 + src/models/repositories/blocking.ts | 28 ++ src/models/repositories/drive-file.ts | 113 ++++++ src/models/repositories/drive-folder.ts | 49 +++ src/models/repositories/follow-request.ts | 19 + src/models/repositories/following.ts | 44 +++ src/models/repositories/games/reversi/game.ts | 49 +++ src/models/repositories/games/reversi/matching.ts | 27 ++ src/models/repositories/messaging-message.ts | 37 ++ src/models/repositories/muting.ts | 28 ++ src/models/repositories/note-favorite.ts | 25 ++ src/models/repositories/note-reaction.ts | 18 + src/models/repositories/note.ts | 210 +++++++++++ src/models/repositories/notification.ts | 47 +++ src/models/repositories/signin.ts | 11 + src/models/repositories/user-list.ts | 16 + src/models/repositories/user.ts | 198 ++++++++++ src/models/signin.ts | 34 -- src/models/sw-subscription.ts | 13 - src/models/user-list.ts | 41 -- src/models/user.ts | 438 ---------------------- 89 files changed, 3794 insertions(+), 2713 deletions(-) delete mode 100644 src/models/abuse-user-report.ts delete mode 100644 src/models/access-token.ts delete mode 100644 src/models/app.ts delete mode 100644 src/models/auth-session.ts delete mode 100644 src/models/blocking.ts delete mode 100644 src/models/drive-file-thumbnail.ts delete mode 100644 src/models/drive-file-webpublic.ts delete mode 100644 src/models/drive-file.ts delete mode 100644 src/models/drive-folder.ts delete mode 100644 src/models/emoji.ts create mode 100644 src/models/entities/abuse-user-report.ts create mode 100644 src/models/entities/access-token.ts create mode 100644 src/models/entities/app.ts create mode 100644 src/models/entities/auth-session.ts create mode 100644 src/models/entities/blocking.ts create mode 100644 src/models/entities/drive-file.ts create mode 100644 src/models/entities/drive-folder.ts create mode 100644 src/models/entities/emoji.ts create mode 100644 src/models/entities/follow-request.ts create mode 100644 src/models/entities/following.ts create mode 100644 src/models/entities/games/reversi/game.ts create mode 100644 src/models/entities/games/reversi/matching.ts create mode 100644 src/models/entities/hashtag.ts create mode 100644 src/models/entities/instance.ts create mode 100644 src/models/entities/log.ts create mode 100644 src/models/entities/messaging-message.ts create mode 100644 src/models/entities/meta.ts create mode 100644 src/models/entities/muting.ts create mode 100644 src/models/entities/note-favorite.ts create mode 100644 src/models/entities/note-reaction.ts create mode 100644 src/models/entities/note-unread.ts create mode 100644 src/models/entities/note-watching.ts create mode 100644 src/models/entities/note.ts create mode 100644 src/models/entities/notification.ts create mode 100644 src/models/entities/poll-vote.ts create mode 100644 src/models/entities/poll.ts create mode 100644 src/models/entities/registration-tickets.ts create mode 100644 src/models/entities/signin.ts create mode 100644 src/models/entities/sw-subscription.ts create mode 100644 src/models/entities/user-keypair.ts create mode 100644 src/models/entities/user-list-joining.ts create mode 100644 src/models/entities/user-list.ts create mode 100644 src/models/entities/user-note-pinings.ts create mode 100644 src/models/entities/user-publickey.ts create mode 100644 src/models/entities/user-service-linking.ts create mode 100644 src/models/entities/user.ts delete mode 100644 src/models/favorite.ts delete mode 100644 src/models/follow-request.ts delete mode 100644 src/models/following.ts delete mode 100644 src/models/games/reversi/game.ts delete mode 100644 src/models/games/reversi/matching.ts delete mode 100644 src/models/hashtag.ts create mode 100644 src/models/id.ts create mode 100644 src/models/index.ts delete mode 100644 src/models/instance.ts delete mode 100644 src/models/log.ts delete mode 100644 src/models/messaging-message.ts delete mode 100644 src/models/meta.ts delete mode 100644 src/models/mute.ts delete mode 100644 src/models/note-reaction.ts delete mode 100644 src/models/note-unread.ts delete mode 100644 src/models/note-watching.ts delete mode 100644 src/models/note.ts delete mode 100644 src/models/notification.ts delete mode 100644 src/models/poll-vote.ts delete mode 100644 src/models/registration-tickets.ts create mode 100644 src/models/repositories/abuse-user-report.ts create mode 100644 src/models/repositories/app.ts create mode 100644 src/models/repositories/auth-session.ts create mode 100644 src/models/repositories/blocking.ts create mode 100644 src/models/repositories/drive-file.ts create mode 100644 src/models/repositories/drive-folder.ts create mode 100644 src/models/repositories/follow-request.ts create mode 100644 src/models/repositories/following.ts create mode 100644 src/models/repositories/games/reversi/game.ts create mode 100644 src/models/repositories/games/reversi/matching.ts create mode 100644 src/models/repositories/messaging-message.ts create mode 100644 src/models/repositories/muting.ts create mode 100644 src/models/repositories/note-favorite.ts create mode 100644 src/models/repositories/note-reaction.ts create mode 100644 src/models/repositories/note.ts create mode 100644 src/models/repositories/notification.ts create mode 100644 src/models/repositories/signin.ts create mode 100644 src/models/repositories/user-list.ts create mode 100644 src/models/repositories/user.ts delete mode 100644 src/models/signin.ts delete mode 100644 src/models/sw-subscription.ts delete mode 100644 src/models/user-list.ts delete mode 100644 src/models/user.ts (limited to 'src/models') 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('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(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('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('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(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('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} - */ -export const pack = ( - session: any, - me?: any -) => new Promise(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('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(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('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 => { - 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('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 => { - 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('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 => { - 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(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('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(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('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; + + @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; +} 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[]; + + @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; + + /** + * 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; + + @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; + + @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('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(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('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(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('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('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(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('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(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('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('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('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('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(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('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('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(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('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(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('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('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('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; - 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('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(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('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('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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + public validateFolderName(name: string): boolean { + return ( + (name.trim().length > 0) && + (name.length <= 200) + ); + } + + public async pack( + src: DriveFolder['id'] | DriveFolder, + options?: { + detail: boolean + } + ): Promise> { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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> { + 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 { + 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 { + 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 { + 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 { + 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> { + 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('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} - */ -export const pack = ( - record: any -) => new Promise(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('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('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(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('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(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 { - const meta = await fetchMeta(); - return await User.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser; -} -- cgit v1.2.3-freya