diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2019-04-07 21:50:36 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-04-07 21:50:36 +0900 |
| commit | f0a29721c9fb10f97faf386bc9d6b1b2fad97895 (patch) | |
| tree | b5c1d38d698589bb444c0881a431391db91eb5bc /src/services | |
| parent | Update README.md [AUTOGEN] (#4639) (diff) | |
| download | sharkey-f0a29721c9fb10f97faf386bc9d6b1b2fad97895.tar.gz sharkey-f0a29721c9fb10f97faf386bc9d6b1b2fad97895.tar.bz2 sharkey-f0a29721c9fb10f97faf386bc9d6b1b2fad97895.zip | |
Use PostgreSQL instead of MongoDB (#4572)
* wip
* Update note.ts
* Update timeline.ts
* Update core.ts
* wip
* Update generate-visibility-query.ts
* wip
* wip
* wip
* wip
* wip
* Update global-timeline.ts
* wip
* wip
* wip
* Update vote.ts
* wip
* wip
* Update create.ts
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update files.ts
* wip
* wip
* Update CONTRIBUTING.md
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update read-notification.ts
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update cancel.ts
* wip
* wip
* wip
* Update show.ts
* wip
* wip
* Update gen-id.ts
* Update create.ts
* Update id.ts
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Docker: Update files about Docker (#4599)
* Docker: Use cache if files used by `yarn install` was not updated
This patch reduces the number of times to installing node_modules.
For example, `yarn install` step will be skipped when only ".config/default.yml" is updated.
* Docker: Migrate MongoDB to Postgresql
Misskey uses Postgresql as a database instead of Mongodb since version 11.
* Docker: Uncomment about data persistence
This patch will save a lot of databases.
* wip
* wip
* wip
* Update activitypub.ts
* wip
* wip
* wip
* Update logs.ts
* wip
* Update drive-file.ts
* Update register.ts
* wip
* wip
* Update mentions.ts
* wip
* wip
* wip
* Update recommendation.ts
* wip
* Update index.ts
* wip
* Update recommendation.ts
* Doc: Update docker.ja.md and docker.en.md (#1) (#4608)
Update how to set up misskey.
* wip
* :v:
* wip
* Update note.ts
* Update postgre.ts
* wip
* wip
* wip
* wip
* Update add-file.ts
* wip
* wip
* wip
* Clean up
* Update logs.ts
* wip
* :pizza:
* wip
* Ad notes
* wip
* Update api-visibility.ts
* Update note.ts
* Update add-file.ts
* tests
* tests
* Update postgre.ts
* Update utils.ts
* wip
* wip
* Refactor
* wip
* Refactor
* wip
* wip
* Update show-users.ts
* Update update-instance.ts
* wip
* Update feed.ts
* Update outbox.ts
* Update outbox.ts
* Update user.ts
* wip
* Update list.ts
* Update update-hashtag.ts
* wip
* Update update-hashtag.ts
* Refactor
* Update update.ts
* wip
* wip
* :v:
* clean up
* docs
* Update push.ts
* wip
* Update api.ts
* wip
* :v:
* Update make-pagination-query.ts
* :v:
* Delete hashtags.ts
* Update instances.ts
* Update instances.ts
* Update create.ts
* Update search.ts
* Update reversi-game.ts
* Update signup.ts
* Update user.ts
* id
* Update example.yml
* :art:
* objectid
* fix
* reversi
* reversi
* Fix bug of chart engine
* Add test of chart engine
* Improve test
* Better testing
* Improve chart engine
* Refactor
* Add test of chart engine
* Refactor
* Add chart test
* Fix bug
* コミットし忘れ
* Refactoring
* :v:
* Add tests
* Add test
* Extarct note tests
* Refactor
* 存在しないユーザーにメンションできなくなっていた問題を修正
* Fix bug
* Update update-meta.ts
* Fix bug
* Update mention.vue
* Fix bug
* Update meta.ts
* Update CONTRIBUTING.md
* Fix bug
* Fix bug
* Fix bug
* Clean up
* Clean up
* Update notification.ts
* Clean up
* Add mute tests
* Add test
* Refactor
* Add test
* Fix test
* Refactor
* Refactor
* Add tests
* Update utils.ts
* Update utils.ts
* Fix test
* Update package.json
* Update update.ts
* Update manifest.ts
* Fix bug
* Fix bug
* Add test
* :art:
* Update endpoint permissions
* Updaye permisison
* Update person.ts
#4299
* データベースと同期しないように
* Fix bug
* Fix bug
* Update reversi-game.ts
* Use a feature of Node v11.7.0 to extract a public key (#4644)
* wip
* wip
* :v:
* Refactoring
#1540
* test
* test
* test
* test
* test
* test
* test
* Fix bug
* Fix test
* :sushi:
* wip
* #4471
* Add test for #4335
* Refactor
* Fix test
* Add tests
* :clock4:
* Fix bug
* Add test
* Add test
* rename
* Fix bug
Diffstat (limited to 'src/services')
77 files changed, 2931 insertions, 3105 deletions
diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts index c20666ef26..79ca0d59f1 100644 --- a/src/services/blocking/create.ts +++ b/src/services/blocking/create.ts @@ -1,6 +1,3 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; -import FollowRequest from '../../models/follow-request'; import { publishMainStream } from '../stream'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; @@ -8,11 +5,12 @@ import renderUndo from '../../remote/activitypub/renderer/undo'; import renderBlock from '../../remote/activitypub/renderer/block'; import { deliver } from '../../queue'; import renderReject from '../../remote/activitypub/renderer/reject'; -import perUserFollowingChart from '../../services/chart/per-user-following'; -import Blocking from '../../models/blocking'; - -export default async function(blocker: IUser, blockee: IUser) { +import { User } from '../../models/entities/user'; +import { Blockings, Users, FollowRequests, Followings } from '../../models'; +import { perUserFollowingChart } from '../chart'; +import { genId } from '../../misc/gen-id'; +export default async function(blocker: User, blockee: User) { await Promise.all([ cancelRequest(blocker, blockee), cancelRequest(blockee, blocker), @@ -20,105 +18,90 @@ export default async function(blocker: IUser, blockee: IUser) { unFollow(blockee, blocker) ]); - await Blocking.insert({ + await Blockings.save({ + id: genId(), createdAt: new Date(), - blockerId: blocker._id, - blockeeId: blockee._id, + blockerId: blocker.id, + blockeeId: blockee.id, }); - if (isLocalUser(blocker) && isRemoteUser(blockee)) { + if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { const content = renderActivity(renderBlock(blocker, blockee)); deliver(blocker, content, blockee.inbox); } } -async function cancelRequest(follower: IUser, followee: IUser) { - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id +async function cancelRequest(follower: User, followee: User) { + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); if (request == null) { return; } - await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id - }); - - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - if (isLocalUser(followee)) { - packUser(followee, followee, { + if (Users.isLocalUser(followee)) { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); } - if (isLocalUser(follower)) { - packUser(followee, follower, { + if (Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } // リモートにフォローリクエストをしていたらUndoFollow送信 - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } // リモートからフォローリクエストを受けていたらReject送信 - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee)); deliver(followee, content, follower.inbox); } } -async function unFollow(follower: IUser, followee: IUser) { - const following = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id +async function unFollow(follower: User, followee: User) { + const following = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id }); if (following == null) { return; } - Following.remove({ - _id: following._id - }); + Followings.delete(following.id); //#region Decrement following count - User.update({ _id: follower._id }, { - $inc: { - followingCount: -1 - } - }); + Users.decrement({ id: follower.id }, 'followingCount', 1); //#endregion //#region Decrement followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: -1 - } - }); + Users.decrement({ id: followee.id }, 'followersCount', 1); //#endregion perUserFollowingChart.update(follower, followee, false); // Publish unfollow event - if (isLocalUser(follower)) { - packUser(followee, follower, { + if (Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } // リモートにフォローをしていたらUndoFollow送信 - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } diff --git a/src/services/blocking/delete.ts b/src/services/blocking/delete.ts index 099fa14b37..2c05cb7f3f 100644 --- a/src/services/blocking/delete.ts +++ b/src/services/blocking/delete.ts @@ -1,17 +1,17 @@ -import { isLocalUser, isRemoteUser, IUser } from '../../models/user'; -import Blocking from '../../models/blocking'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderBlock from '../../remote/activitypub/renderer/block'; import renderUndo from '../../remote/activitypub/renderer/undo'; import { deliver } from '../../queue'; import Logger from '../logger'; +import { User } from '../../models/entities/user'; +import { Blockings, Users } from '../../models'; const logger = new Logger('blocking/delete'); -export default async function(blocker: IUser, blockee: IUser) { - const blocking = await Blocking.findOne({ - blockerId: blocker._id, - blockeeId: blockee._id +export default async function(blocker: User, blockee: User) { + const blocking = await Blockings.findOne({ + blockerId: blocker.id, + blockeeId: blockee.id }); if (blocking == null) { @@ -19,12 +19,10 @@ export default async function(blocker: IUser, blockee: IUser) { return; } - Blocking.remove({ - _id: blocking._id - }); + Blockings.delete(blocking.id); // deliver if remote bloking - if (isLocalUser(blocker) && isRemoteUser(blockee)) { + if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker)); deliver(blocker, content, blockee.inbox); } diff --git a/src/services/chart/active-users.ts b/src/services/chart/active-users.ts deleted file mode 100644 index 2a4e1a97ac..0000000000 --- a/src/services/chart/active-users.ts +++ /dev/null @@ -1,48 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import { IUser, isLocalUser } from '../../models/user'; - -/** - * アクティブユーザーに関するチャート - */ -type ActiveUsersLog = { - local: { - /** - * アクティブユーザー数 - */ - count: number; - }; - - remote: ActiveUsersLog['local']; -}; - -class ActiveUsersChart extends Chart<ActiveUsersLog> { - constructor() { - super('activeUsers'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: ActiveUsersLog): Promise<ActiveUsersLog> { - return { - local: { - count: 0 - }, - remote: { - count: 0 - } - }; - } - - @autobind - public async update(user: IUser) { - const update: Obj = { - count: 1 - }; - - await this.incIfUnique({ - [isLocalUser(user) ? 'local' : 'remote']: update - }, 'users', user._id.toHexString()); - } -} - -export default new ActiveUsersChart(); diff --git a/src/services/chart/charts/classes/active-users.ts b/src/services/chart/charts/classes/active-users.ts new file mode 100644 index 0000000000..5128150de6 --- /dev/null +++ b/src/services/chart/charts/classes/active-users.ts @@ -0,0 +1,35 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { name, schema } from '../schemas/active-users'; + +type ActiveUsersLog = SchemaType<typeof schema>; + +export default class ActiveUsersChart extends Chart<ActiveUsersLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: ActiveUsersLog): DeepPartial<ActiveUsersLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<ActiveUsersLog>> { + return {}; + } + + @autobind + public async update(user: User) { + const update: Obj = { + count: 1 + }; + + await this.incIfUnique({ + [Users.isLocalUser(user) ? 'local' : 'remote']: update + }, 'users', user.id); + } +} diff --git a/src/services/chart/charts/classes/drive.ts b/src/services/chart/charts/classes/drive.ts new file mode 100644 index 0000000000..ae52df19ac --- /dev/null +++ b/src/services/chart/charts/classes/drive.ts @@ -0,0 +1,69 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { DriveFiles } from '../../../../models'; +import { Not } from 'typeorm'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { name, schema } from '../schemas/drive'; + +type DriveLog = SchemaType<typeof schema>; + +export default class DriveChart extends Chart<DriveLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: DriveLog): DeepPartial<DriveLog> { + return { + local: { + totalCount: latest.local.totalCount, + totalSize: latest.local.totalSize, + }, + remote: { + totalCount: latest.remote.totalCount, + totalSize: latest.remote.totalSize, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<DriveLog>> { + const [localCount, remoteCount, localSize, remoteSize] = await Promise.all([ + DriveFiles.count({ userHost: null }), + DriveFiles.count({ userHost: Not(null) }), + DriveFiles.clacDriveUsageOfLocal(), + DriveFiles.clacDriveUsageOfRemote() + ]); + + return { + local: { + totalCount: localCount, + totalSize: localSize, + }, + remote: { + totalCount: remoteCount, + totalSize: remoteSize, + } + }; + } + + @autobind + public async update(file: DriveFile, isAdditional: boolean) { + const update: Obj = {}; + + update.totalCount = isAdditional ? 1 : -1; + update.totalSize = isAdditional ? file.size : -file.size; + if (isAdditional) { + update.incCount = 1; + update.incSize = file.size; + } else { + update.decCount = 1; + update.decSize = file.size; + } + + await this.inc({ + [file.userHost === null ? 'local' : 'remote']: update + }); + } +} diff --git a/src/services/chart/charts/classes/federation.ts b/src/services/chart/charts/classes/federation.ts new file mode 100644 index 0000000000..bd2c497e7b --- /dev/null +++ b/src/services/chart/charts/classes/federation.ts @@ -0,0 +1,51 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Instances } from '../../../../models'; +import { name, schema } from '../schemas/federation'; + +type FederationLog = SchemaType<typeof schema>; + +export default class FederationChart extends Chart<FederationLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: FederationLog): DeepPartial<FederationLog> { + return { + instance: { + total: latest.instance.total, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<FederationLog>> { + const [total] = await Promise.all([ + Instances.count({}) + ]); + + return { + instance: { + total: total, + } + }; + } + + @autobind + public async update(isAdditional: boolean) { + const update: Obj = {}; + + update.total = isAdditional ? 1 : -1; + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + await this.inc({ + instance: update + }); + } +} diff --git a/src/services/chart/charts/classes/hashtag.ts b/src/services/chart/charts/classes/hashtag.ts new file mode 100644 index 0000000000..38c3a94f0c --- /dev/null +++ b/src/services/chart/charts/classes/hashtag.ts @@ -0,0 +1,35 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { name, schema } from '../schemas/hashtag'; + +type HashtagLog = SchemaType<typeof schema>; + +export default class HashtagChart extends Chart<HashtagLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: HashtagLog): DeepPartial<HashtagLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<HashtagLog>> { + return {}; + } + + @autobind + public async update(hashtag: string, user: User) { + const update: Obj = { + count: 1 + }; + + await this.incIfUnique({ + [Users.isLocalUser(user) ? 'local' : 'remote']: update + }, 'users', user.id, hashtag); + } +} diff --git a/src/services/chart/charts/classes/instance.ts b/src/services/chart/charts/classes/instance.ts new file mode 100644 index 0000000000..974eac036b --- /dev/null +++ b/src/services/chart/charts/classes/instance.ts @@ -0,0 +1,160 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { DriveFiles, Followings, Users, Notes } from '../../../../models'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { name, schema } from '../schemas/instance'; + +type InstanceLog = SchemaType<typeof schema>; + +export default class InstanceChart extends Chart<InstanceLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: InstanceLog): DeepPartial<InstanceLog> { + return { + notes: { + total: latest.notes.total, + }, + users: { + total: latest.users.total, + }, + following: { + total: latest.following.total, + }, + followers: { + total: latest.followers.total, + }, + drive: { + totalFiles: latest.drive.totalFiles, + totalUsage: latest.drive.totalUsage, + } + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<InstanceLog>> { + const [ + notesCount, + usersCount, + followingCount, + followersCount, + driveFiles, + driveUsage, + ] = await Promise.all([ + Notes.count({ userHost: group }), + Users.count({ host: group }), + Followings.count({ followerHost: group }), + Followings.count({ followeeHost: group }), + DriveFiles.count({ userHost: group }), + DriveFiles.clacDriveUsageOfHost(group), + ]); + + return { + notes: { + total: notesCount, + }, + users: { + total: usersCount, + }, + following: { + total: followingCount, + }, + followers: { + total: followersCount, + }, + drive: { + totalFiles: driveFiles, + totalUsage: driveUsage, + } + }; + } + + @autobind + public async requestReceived(host: string) { + await this.inc({ + requests: { + received: 1 + } + }, host); + } + + @autobind + public async requestSent(host: string, isSucceeded: boolean) { + const update: Obj = {}; + + if (isSucceeded) { + update.succeeded = 1; + } else { + update.failed = 1; + } + + await this.inc({ + requests: update + }, host); + } + + @autobind + public async newUser(host: string) { + await this.inc({ + users: { + total: 1, + inc: 1 + } + }, host); + } + + @autobind + public async updateNote(host: string, isAdditional: boolean) { + await this.inc({ + notes: { + total: isAdditional ? 1 : -1, + inc: isAdditional ? 1 : 0, + dec: isAdditional ? 0 : 1, + } + }, host); + } + + @autobind + public async updateFollowing(host: string, isAdditional: boolean) { + await this.inc({ + following: { + total: isAdditional ? 1 : -1, + inc: isAdditional ? 1 : 0, + dec: isAdditional ? 0 : 1, + } + }, host); + } + + @autobind + public async updateFollowers(host: string, isAdditional: boolean) { + await this.inc({ + followers: { + total: isAdditional ? 1 : -1, + inc: isAdditional ? 1 : 0, + dec: isAdditional ? 0 : 1, + } + }, host); + } + + @autobind + public async updateDrive(file: DriveFile, isAdditional: boolean) { + const update: Obj = {}; + + update.totalFiles = isAdditional ? 1 : -1; + update.totalUsage = isAdditional ? file.size : -file.size; + if (isAdditional) { + update.incFiles = 1; + update.incUsage = file.size; + } else { + update.decFiles = 1; + update.decUsage = file.size; + } + + await this.inc({ + drive: update + }, file.userHost); + } +} diff --git a/src/services/chart/charts/classes/network.ts b/src/services/chart/charts/classes/network.ts new file mode 100644 index 0000000000..8b26e5c4c2 --- /dev/null +++ b/src/services/chart/charts/classes/network.ts @@ -0,0 +1,34 @@ +import autobind from 'autobind-decorator'; +import Chart, { DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/network'; + +type NetworkLog = SchemaType<typeof schema>; + +export default class NetworkChart extends Chart<NetworkLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: NetworkLog): DeepPartial<NetworkLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<NetworkLog>> { + return {}; + } + + @autobind + public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) { + const inc: DeepPartial<NetworkLog> = { + incomingRequests: incomingRequests, + totalTime: time, + incomingBytes: incomingBytes, + outgoingBytes: outgoingBytes + }; + + await this.inc(inc); + } +} diff --git a/src/services/chart/charts/classes/notes.ts b/src/services/chart/charts/classes/notes.ts new file mode 100644 index 0000000000..85ccf000d8 --- /dev/null +++ b/src/services/chart/charts/classes/notes.ts @@ -0,0 +1,71 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Notes } from '../../../../models'; +import { Not } from 'typeorm'; +import { Note } from '../../../../models/entities/note'; +import { name, schema } from '../schemas/notes'; + +type NotesLog = SchemaType<typeof schema>; + +export default class NotesChart extends Chart<NotesLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: NotesLog): DeepPartial<NotesLog> { + return { + local: { + total: latest.local.total, + }, + remote: { + total: latest.remote.total, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<NotesLog>> { + const [localCount, remoteCount] = await Promise.all([ + Notes.count({ userHost: null }), + Notes.count({ userHost: Not(null) }) + ]); + + return { + local: { + total: localCount, + }, + remote: { + total: remoteCount, + } + }; + } + + @autobind + public async update(note: Note, isAdditional: boolean) { + const update: Obj = { + diffs: {} + }; + + update.total = isAdditional ? 1 : -1; + + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + if (note.replyId != null) { + update.diffs.reply = isAdditional ? 1 : -1; + } else if (note.renoteId != null) { + update.diffs.renote = isAdditional ? 1 : -1; + } else { + update.diffs.normal = isAdditional ? 1 : -1; + } + + await this.inc({ + [note.userHost === null ? 'local' : 'remote']: update + }); + } +} diff --git a/src/services/chart/charts/classes/per-user-drive.ts b/src/services/chart/charts/classes/per-user-drive.ts new file mode 100644 index 0000000000..822f4eda0f --- /dev/null +++ b/src/services/chart/charts/classes/per-user-drive.ts @@ -0,0 +1,52 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { DriveFiles } from '../../../../models'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { name, schema } from '../schemas/per-user-drive'; + +type PerUserDriveLog = SchemaType<typeof schema>; + +export default class PerUserDriveChart extends Chart<PerUserDriveLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserDriveLog): DeepPartial<PerUserDriveLog> { + return { + totalCount: latest.totalCount, + totalSize: latest.totalSize, + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserDriveLog>> { + const [count, size] = await Promise.all([ + DriveFiles.count({ userId: group }), + DriveFiles.clacDriveUsageOf(group) + ]); + + return { + totalCount: count, + totalSize: size, + }; + } + + @autobind + public async update(file: DriveFile, isAdditional: boolean) { + const update: Obj = {}; + + update.totalCount = isAdditional ? 1 : -1; + update.totalSize = isAdditional ? file.size : -file.size; + if (isAdditional) { + update.incCount = 1; + update.incSize = file.size; + } else { + update.decCount = 1; + update.decSize = file.size; + } + + await this.inc(update, file.userId); + } +} diff --git a/src/services/chart/charts/classes/per-user-following.ts b/src/services/chart/charts/classes/per-user-following.ts new file mode 100644 index 0000000000..f3809a7c94 --- /dev/null +++ b/src/services/chart/charts/classes/per-user-following.ts @@ -0,0 +1,91 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Followings, Users } from '../../../../models'; +import { Not } from 'typeorm'; +import { User } from '../../../../models/entities/user'; +import { name, schema } from '../schemas/per-user-following'; + +type PerUserFollowingLog = SchemaType<typeof schema>; + +export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserFollowingLog): DeepPartial<PerUserFollowingLog> { + return { + local: { + followings: { + total: latest.local.followings.total, + }, + followers: { + total: latest.local.followers.total, + } + }, + remote: { + followings: { + total: latest.remote.followings.total, + }, + followers: { + total: latest.remote.followers.total, + } + } + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserFollowingLog>> { + const [ + localFollowingsCount, + localFollowersCount, + remoteFollowingsCount, + remoteFollowersCount + ] = await Promise.all([ + Followings.count({ followerId: group, followeeHost: null }), + Followings.count({ followeeId: group, followerHost: null }), + Followings.count({ followerId: group, followeeHost: Not(null) }), + Followings.count({ followeeId: group, followerHost: Not(null) }) + ]); + + return { + local: { + followings: { + total: localFollowingsCount, + }, + followers: { + total: localFollowersCount, + } + }, + remote: { + followings: { + total: remoteFollowingsCount, + }, + followers: { + total: remoteFollowersCount, + } + } + }; + } + + @autobind + public async update(follower: User, followee: User, isFollow: boolean) { + const update: Obj = {}; + + update.total = isFollow ? 1 : -1; + + if (isFollow) { + update.inc = 1; + } else { + update.dec = 1; + } + + this.inc({ + [Users.isLocalUser(follower) ? 'local' : 'remote']: { followings: update } + }, follower.id); + this.inc({ + [Users.isLocalUser(followee) ? 'local' : 'remote']: { followers: update } + }, followee.id); + } +} diff --git a/src/services/chart/charts/classes/per-user-notes.ts b/src/services/chart/charts/classes/per-user-notes.ts new file mode 100644 index 0000000000..cccd495604 --- /dev/null +++ b/src/services/chart/charts/classes/per-user-notes.ts @@ -0,0 +1,58 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { SchemaType } from '../../../../misc/schema'; +import { Notes } from '../../../../models'; +import { Note } from '../../../../models/entities/note'; +import { name, schema } from '../schemas/per-user-notes'; + +type PerUserNotesLog = SchemaType<typeof schema>; + +export default class PerUserNotesChart extends Chart<PerUserNotesLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserNotesLog): DeepPartial<PerUserNotesLog> { + return { + total: latest.total, + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserNotesLog>> { + const [count] = await Promise.all([ + Notes.count({ userId: group }), + ]); + + return { + total: count, + }; + } + + @autobind + public async update(user: User, note: Note, isAdditional: boolean) { + const update: Obj = { + diffs: {} + }; + + update.total = isAdditional ? 1 : -1; + + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + if (note.replyId != null) { + update.diffs.reply = isAdditional ? 1 : -1; + } else if (note.renoteId != null) { + update.diffs.renote = isAdditional ? 1 : -1; + } else { + update.diffs.normal = isAdditional ? 1 : -1; + } + + await this.inc(update, user.id); + } +} diff --git a/src/services/chart/charts/classes/per-user-reactions.ts b/src/services/chart/charts/classes/per-user-reactions.ts new file mode 100644 index 0000000000..124fb4153c --- /dev/null +++ b/src/services/chart/charts/classes/per-user-reactions.ts @@ -0,0 +1,32 @@ +import autobind from 'autobind-decorator'; +import Chart, { DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { Note } from '../../../../models/entities/note'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { name, schema } from '../schemas/per-user-reactions'; + +type PerUserReactionsLog = SchemaType<typeof schema>; + +export default class PerUserReactionsChart extends Chart<PerUserReactionsLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserReactionsLog): DeepPartial<PerUserReactionsLog> { + return {}; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserReactionsLog>> { + return {}; + } + + @autobind + public async update(user: User, note: Note) { + this.inc({ + [Users.isLocalUser(user) ? 'local' : 'remote']: { count: 1 } + }, note.userId); + } +} diff --git a/src/services/chart/charts/classes/test-grouped.ts b/src/services/chart/charts/classes/test-grouped.ts new file mode 100644 index 0000000000..e32cbcf416 --- /dev/null +++ b/src/services/chart/charts/classes/test-grouped.ts @@ -0,0 +1,47 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/test-grouped'; + +type TestGroupedLog = SchemaType<typeof schema>; + +export default class TestGroupedChart extends Chart<TestGroupedLog> { + private total = {} as Record<string, number>; + + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: TestGroupedLog): DeepPartial<TestGroupedLog> { + return { + foo: { + total: latest.foo.total, + }, + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<TestGroupedLog>> { + return { + foo: { + total: this.total[group], + }, + }; + } + + @autobind + public async increment(group: string) { + if (this.total[group] == null) this.total[group] = 0; + + const update: Obj = {}; + + update.total = 1; + update.inc = 1; + this.total[group]++; + + await this.inc({ + foo: update + }, group); + } +} diff --git a/src/services/chart/charts/classes/test-unique.ts b/src/services/chart/charts/classes/test-unique.ts new file mode 100644 index 0000000000..1eb396c293 --- /dev/null +++ b/src/services/chart/charts/classes/test-unique.ts @@ -0,0 +1,29 @@ +import autobind from 'autobind-decorator'; +import Chart, { DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/test-unique'; + +type TestUniqueLog = SchemaType<typeof schema>; + +export default class TestUniqueChart extends Chart<TestUniqueLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: TestUniqueLog): DeepPartial<TestUniqueLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<TestUniqueLog>> { + return {}; + } + + @autobind + public async uniqueIncrement(key: string) { + await this.incIfUnique({ + foo: 1 + }, 'foos', key); + } +} diff --git a/src/services/chart/charts/classes/test.ts b/src/services/chart/charts/classes/test.ts new file mode 100644 index 0000000000..57c22822f2 --- /dev/null +++ b/src/services/chart/charts/classes/test.ts @@ -0,0 +1,45 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/test'; + +type TestLog = SchemaType<typeof schema>; + +export default class TestChart extends Chart<TestLog> { + private total = 0; + + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: TestLog): DeepPartial<TestLog> { + return { + foo: { + total: latest.foo.total, + }, + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<TestLog>> { + return { + foo: { + total: this.total, + }, + }; + } + + @autobind + public async increment() { + const update: Obj = {}; + + update.total = 1; + update.inc = 1; + this.total++; + + await this.inc({ + foo: update + }); + } +} diff --git a/src/services/chart/charts/classes/users.ts b/src/services/chart/charts/classes/users.ts new file mode 100644 index 0000000000..eec30de8dc --- /dev/null +++ b/src/services/chart/charts/classes/users.ts @@ -0,0 +1,60 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { Not } from 'typeorm'; +import { User } from '../../../../models/entities/user'; +import { name, schema } from '../schemas/users'; + +type UsersLog = SchemaType<typeof schema>; + +export default class UsersChart extends Chart<UsersLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: UsersLog): DeepPartial<UsersLog> { + return { + local: { + total: latest.local.total, + }, + remote: { + total: latest.remote.total, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<UsersLog>> { + const [localCount, remoteCount] = await Promise.all([ + Users.count({ host: null }), + Users.count({ host: Not(null) }) + ]); + + return { + local: { + total: localCount, + }, + remote: { + total: remoteCount, + } + }; + } + + @autobind + public async update(user: User, isAdditional: boolean) { + const update: Obj = {}; + + update.total = isAdditional ? 1 : -1; + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + await this.inc({ + [Users.isLocalUser(user) ? 'local' : 'remote']: update + }); + } +} diff --git a/src/services/chart/charts/schemas/active-users.ts b/src/services/chart/charts/schemas/active-users.ts new file mode 100644 index 0000000000..da8c63389c --- /dev/null +++ b/src/services/chart/charts/schemas/active-users.ts @@ -0,0 +1,28 @@ +export const logSchema = { + /** + * アクティブユーザー数 + */ + count: { + type: 'number' as 'number', + description: 'アクティブユーザー数', + }, +}; + +/** + * アクティブユーザーに関するチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'activeUsers'; diff --git a/src/services/chart/charts/schemas/drive.ts b/src/services/chart/charts/schemas/drive.ts new file mode 100644 index 0000000000..47530e8417 --- /dev/null +++ b/src/services/chart/charts/schemas/drive.ts @@ -0,0 +1,65 @@ +const logSchema = { + /** + * 集計期間時点での、全ドライブファイル数 + */ + totalCount: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイル数' + }, + + /** + * 集計期間時点での、全ドライブファイルの合計サイズ + */ + totalSize: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイルの合計サイズ' + }, + + /** + * 増加したドライブファイル数 + */ + incCount: { + type: 'number' as 'number', + description: '増加したドライブファイル数' + }, + + /** + * 増加したドライブ使用量 + */ + incSize: { + type: 'number' as 'number', + description: '増加したドライブ使用量' + }, + + /** + * 減少したドライブファイル数 + */ + decCount: { + type: 'number' as 'number', + description: '減少したドライブファイル数' + }, + + /** + * 減少したドライブ使用量 + */ + decSize: { + type: 'number' as 'number', + description: '減少したドライブ使用量' + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'drive'; diff --git a/src/services/chart/charts/schemas/federation.ts b/src/services/chart/charts/schemas/federation.ts new file mode 100644 index 0000000000..d1d275fc95 --- /dev/null +++ b/src/services/chart/charts/schemas/federation.ts @@ -0,0 +1,27 @@ +/** + * フェデレーションに関するチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + instance: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: 'インスタンス数の合計' + }, + inc: { + type: 'number' as 'number', + description: '増加インスタンス数' + }, + dec: { + type: 'number' as 'number', + description: '減少インスタンス数' + }, + } + } + } +}; + +export const name = 'federation'; diff --git a/src/services/chart/charts/schemas/hashtag.ts b/src/services/chart/charts/schemas/hashtag.ts new file mode 100644 index 0000000000..c1904b6701 --- /dev/null +++ b/src/services/chart/charts/schemas/hashtag.ts @@ -0,0 +1,28 @@ +export const logSchema = { + /** + * 投稿された数 + */ + count: { + type: 'number' as 'number', + description: '投稿された数', + }, +}; + +/** + * ハッシュタグに関するチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'hashtag'; diff --git a/src/services/chart/charts/schemas/instance.ts b/src/services/chart/charts/schemas/instance.ts new file mode 100644 index 0000000000..af46b33629 --- /dev/null +++ b/src/services/chart/charts/schemas/instance.ts @@ -0,0 +1,124 @@ +/** + * インスタンスごとのチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + requests: { + type: 'object' as 'object', + properties: { + failed: { + type: 'number' as 'number', + description: '失敗したリクエスト数' + }, + succeeded: { + type: 'number' as 'number', + description: '成功したリクエスト数' + }, + received: { + type: 'number' as 'number', + description: '受信したリクエスト数' + }, + } + }, + notes: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全投稿数' + }, + inc: { + type: 'number' as 'number', + description: '増加した投稿数' + }, + dec: { + type: 'number' as 'number', + description: '減少した投稿数' + }, + } + }, + users: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全ユーザー数' + }, + inc: { + type: 'number' as 'number', + description: '増加したユーザー数' + }, + dec: { + type: 'number' as 'number', + description: '減少したユーザー数' + }, + } + }, + following: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全フォロー数' + }, + inc: { + type: 'number' as 'number', + description: '増加したフォロー数' + }, + dec: { + type: 'number' as 'number', + description: '減少したフォロー数' + }, + } + }, + followers: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全フォロワー数' + }, + inc: { + type: 'number' as 'number', + description: '増加したフォロワー数' + }, + dec: { + type: 'number' as 'number', + description: '減少したフォロワー数' + }, + } + }, + drive: { + type: 'object' as 'object', + properties: { + totalFiles: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイル数' + }, + totalUsage: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイルの合計サイズ' + }, + incFiles: { + type: 'number' as 'number', + description: '増加したドライブファイル数' + }, + incUsage: { + type: 'number' as 'number', + description: '増加したドライブ使用量' + }, + decFiles: { + type: 'number' as 'number', + description: '減少したドライブファイル数' + }, + decUsage: { + type: 'number' as 'number', + description: '減少したドライブ使用量' + }, + } + }, + } +}; + +export const name = 'instance'; diff --git a/src/services/chart/charts/schemas/network.ts b/src/services/chart/charts/schemas/network.ts new file mode 100644 index 0000000000..4ef530c07c --- /dev/null +++ b/src/services/chart/charts/schemas/network.ts @@ -0,0 +1,30 @@ +/** + * ネットワークに関するチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + incomingRequests: { + type: 'number' as 'number', + description: '受信したリクエスト数' + }, + outgoingRequests: { + type: 'number' as 'number', + description: '送信したリクエスト数' + }, + totalTime: { + type: 'number' as 'number', + description: '応答時間の合計' // TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる + }, + incomingBytes: { + type: 'number' as 'number', + description: '合計受信データ量' + }, + outgoingBytes: { + type: 'number' as 'number', + description: '合計送信データ量' + }, + } +}; + +export const name = 'network'; diff --git a/src/services/chart/charts/schemas/notes.ts b/src/services/chart/charts/schemas/notes.ts new file mode 100644 index 0000000000..133d1e3730 --- /dev/null +++ b/src/services/chart/charts/schemas/notes.ts @@ -0,0 +1,52 @@ +const logSchema = { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全投稿数' + }, + + inc: { + type: 'number' as 'number', + description: '増加した投稿数' + }, + + dec: { + type: 'number' as 'number', + description: '減少した投稿数' + }, + + diffs: { + type: 'object' as 'object', + properties: { + normal: { + type: 'number' as 'number', + description: '通常の投稿数の差分' + }, + + reply: { + type: 'number' as 'number', + description: 'リプライの投稿数の差分' + }, + + renote: { + type: 'number' as 'number', + description: 'Renoteの投稿数の差分' + }, + } + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'notes'; diff --git a/src/services/chart/charts/schemas/per-user-drive.ts b/src/services/chart/charts/schemas/per-user-drive.ts new file mode 100644 index 0000000000..713bd7ed84 --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-drive.ts @@ -0,0 +1,54 @@ +export const schema = { + type: 'object' as 'object', + properties: { + /** + * 集計期間時点での、全ドライブファイル数 + */ + totalCount: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイル数' + }, + + /** + * 集計期間時点での、全ドライブファイルの合計サイズ + */ + totalSize: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイルの合計サイズ' + }, + + /** + * 増加したドライブファイル数 + */ + incCount: { + type: 'number' as 'number', + description: '増加したドライブファイル数' + }, + + /** + * 増加したドライブ使用量 + */ + incSize: { + type: 'number' as 'number', + description: '増加したドライブ使用量' + }, + + /** + * 減少したドライブファイル数 + */ + decCount: { + type: 'number' as 'number', + description: '減少したドライブファイル数' + }, + + /** + * 減少したドライブ使用量 + */ + decSize: { + type: 'number' as 'number', + description: '減少したドライブ使用量' + }, + } +}; + +export const name = 'perUserDrive'; diff --git a/src/services/chart/charts/schemas/per-user-following.ts b/src/services/chart/charts/schemas/per-user-following.ts new file mode 100644 index 0000000000..d6ca1130e0 --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-following.ts @@ -0,0 +1,81 @@ +export const logSchema = { + /** + * フォローしている + */ + followings: { + type: 'object' as 'object', + properties: { + /** + * フォローしている合計 + */ + total: { + type: 'number' as 'number', + description: 'フォローしている合計', + }, + + /** + * フォローした数 + */ + inc: { + type: 'number' as 'number', + description: 'フォローした数', + }, + + /** + * フォロー解除した数 + */ + dec: { + type: 'number' as 'number', + description: 'フォロー解除した数', + }, + } + }, + + /** + * フォローされている + */ + followers: { + type: 'object' as 'object', + properties: { + /** + * フォローされている合計 + */ + total: { + type: 'number' as 'number', + description: 'フォローされている合計', + }, + + /** + * フォローされた数 + */ + inc: { + type: 'number' as 'number', + description: 'フォローされた数', + }, + + /** + * フォロー解除された数 + */ + dec: { + type: 'number' as 'number', + description: 'フォロー解除された数', + }, + } + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'perUserFollowing'; diff --git a/src/services/chart/charts/schemas/per-user-notes.ts b/src/services/chart/charts/schemas/per-user-notes.ts new file mode 100644 index 0000000000..3c448c4cee --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-notes.ts @@ -0,0 +1,41 @@ +export const schema = { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全投稿数' + }, + + inc: { + type: 'number' as 'number', + description: '増加した投稿数' + }, + + dec: { + type: 'number' as 'number', + description: '減少した投稿数' + }, + + diffs: { + type: 'object' as 'object', + properties: { + normal: { + type: 'number' as 'number', + description: '通常の投稿数の差分' + }, + + reply: { + type: 'number' as 'number', + description: 'リプライの投稿数の差分' + }, + + renote: { + type: 'number' as 'number', + description: 'Renoteの投稿数の差分' + }, + } + }, + } +}; + +export const name = 'perUserNotes'; diff --git a/src/services/chart/charts/schemas/per-user-reactions.ts b/src/services/chart/charts/schemas/per-user-reactions.ts new file mode 100644 index 0000000000..1278184da6 --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-reactions.ts @@ -0,0 +1,28 @@ +export const logSchema = { + /** + * フォローしている合計 + */ + count: { + type: 'number' as 'number', + description: 'リアクションされた数', + }, +}; + +/** + * ユーザーごとのリアクションに関するチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'perUserReaction'; diff --git a/src/services/chart/charts/schemas/test-grouped.ts b/src/services/chart/charts/schemas/test-grouped.ts new file mode 100644 index 0000000000..acf3fddb31 --- /dev/null +++ b/src/services/chart/charts/schemas/test-grouped.ts @@ -0,0 +1,26 @@ +export const schema = { + type: 'object' as 'object', + properties: { + foo: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '' + }, + + inc: { + type: 'number' as 'number', + description: '' + }, + + dec: { + type: 'number' as 'number', + description: '' + }, + } + } + } +}; + +export const name = 'testGrouped'; diff --git a/src/services/chart/charts/schemas/test-unique.ts b/src/services/chart/charts/schemas/test-unique.ts new file mode 100644 index 0000000000..8fcfbf3c72 --- /dev/null +++ b/src/services/chart/charts/schemas/test-unique.ts @@ -0,0 +1,11 @@ +export const schema = { + type: 'object' as 'object', + properties: { + foo: { + type: 'number' as 'number', + description: '' + }, + } +}; + +export const name = 'testUnique'; diff --git a/src/services/chart/charts/schemas/test.ts b/src/services/chart/charts/schemas/test.ts new file mode 100644 index 0000000000..b1344500bf --- /dev/null +++ b/src/services/chart/charts/schemas/test.ts @@ -0,0 +1,26 @@ +export const schema = { + type: 'object' as 'object', + properties: { + foo: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '' + }, + + inc: { + type: 'number' as 'number', + description: '' + }, + + dec: { + type: 'number' as 'number', + description: '' + }, + } + } + } +}; + +export const name = 'test'; diff --git a/src/services/chart/charts/schemas/users.ts b/src/services/chart/charts/schemas/users.ts new file mode 100644 index 0000000000..db7e2dd057 --- /dev/null +++ b/src/services/chart/charts/schemas/users.ts @@ -0,0 +1,41 @@ +const logSchema = { + /** + * 集計期間時点での、全ユーザー数 + */ + total: { + type: 'number' as 'number', + description: '集計期間時点での、全ユーザー数' + }, + + /** + * 増加したユーザー数 + */ + inc: { + type: 'number' as 'number', + description: '増加したユーザー数' + }, + + /** + * 減少したユーザー数 + */ + dec: { + type: 'number' as 'number', + description: '減少したユーザー数' + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'users'; diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts new file mode 100644 index 0000000000..2a60b1a0a3 --- /dev/null +++ b/src/services/chart/core.ts @@ -0,0 +1,460 @@ +/** + * チャートエンジン + * + * Tests located in test/chart + */ + +import * as moment from 'moment'; +import * as nestedProperty from 'nested-property'; +import autobind from 'autobind-decorator'; +import Logger from '../logger'; +import { Schema } from '../../misc/schema'; +import { EntitySchema, getRepository, Repository, LessThan, MoreThanOrEqual } from 'typeorm'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; + +const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test'); + +const utc = moment.utc; + +export type Obj = { [key: string]: any }; + +export type DeepPartial<T> = { + [P in keyof T]?: DeepPartial<T[P]>; +}; + +type ArrayValue<T> = { + [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>; +}; + +type Span = 'day' | 'hour'; + +type Log = { + id: number; + + /** + * 集計のグループ + */ + group: string | null; + + /** + * 集計日時のUnixタイムスタンプ(秒) + */ + date: number; + + /** + * 集計期間 + */ + span: Span; + + /** + * ユニークインクリメント用 + */ + unique?: Record<string, any>; +}; + +const camelToSnake = (str: string) => { + return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase()); +}; + +/** + * 様々なチャートの管理を司るクラス + */ +export default abstract class Chart<T extends Record<string, any>> { + private static readonly columnPrefix = '___'; + private static readonly columnDot = '_'; + + private name: string; + public schema: Schema; + protected repository: Repository<Log>; + protected abstract genNewLog(latest: T): DeepPartial<T>; + protected abstract async fetchActual(group?: string): Promise<DeepPartial<T>>; + + @autobind + private static convertSchemaToFlatColumnDefinitions(schema: Schema) { + const columns = {} as any; + const flatColumns = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}${this.columnDot}${k}` : k; + if (v.type === 'object') { + flatColumns(v.properties, p); + } else { + columns[this.columnPrefix + p] = { + type: 'integer', + }; + } + } + }; + flatColumns(schema.properties); + return columns; + } + + @autobind + private static convertFlattenColumnsToObject(x: Record<string, number>) { + const obj = {} as any; + for (const k of Object.keys(x).filter(k => k.startsWith(Chart.columnPrefix))) { + // now k is ___x_y_z + const path = k.substr(Chart.columnPrefix.length).split(Chart.columnDot).join('.'); + nestedProperty.set(obj, path, x[k]); + } + return obj; + } + + @autobind + private static convertObjectToFlattenColumns(x: Record<string, any>) { + const columns = {} as Record<string, number>; + const flatten = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}${this.columnDot}${k}` : k; + if (typeof v === 'object') { + flatten(v, p); + } else { + columns[this.columnPrefix + p] = v; + } + } + }; + flatten(x); + return columns; + } + + @autobind + private static convertQuery(x: Record<string, any>) { + const query: Record<string, Function> = {}; + + const columns = Chart.convertObjectToFlattenColumns(x); + + for (const [k, v] of Object.entries(columns)) { + if (v > 0) query[k] = () => `"${k}" + ${v}`; + if (v < 0) query[k] = () => `"${k}" - ${v}`; + } + + return query; + } + + @autobind + private static momentToTimestamp(x: moment.Moment): Log['date'] { + return x.unix(); + } + + @autobind + public static schemaToEntity(name: string, schema: Schema): EntitySchema { + return new EntitySchema({ + name: `__chart__${camelToSnake(name)}`, + columns: { + id: { + type: 'integer', + primary: true, + generated: true + }, + date: { + type: 'integer', + }, + group: { + type: 'varchar', + length: 128, + nullable: true + }, + span: { + type: 'enum', + enum: ['hour', 'day'] + }, + unique: { + type: 'jsonb', + default: {} + }, + ...Chart.convertSchemaToFlatColumnDefinitions(schema) + }, + }); + } + + constructor(name: string, schema: Schema, grouped = false) { + this.name = name; + this.schema = schema; + const entity = Chart.schemaToEntity(name, schema); + + const keys = ['span', 'date']; + if (grouped) keys.push('group'); + + entity.options.uniques = [{ + columns: keys + }]; + + this.repository = getRepository<Log>(entity); + } + + @autobind + private getNewLog(latest?: T): T { + const log = latest ? this.genNewLog(latest) : {}; + const flatColumns = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}.${k}` : k; + if (v.type === 'object') { + flatColumns(v.properties, p); + } else { + if (nestedProperty.get(log, p) == null) { + nestedProperty.set(log, p, 0); + } + } + } + }; + flatColumns(this.schema.properties); + return log as T; + } + + @autobind + private getCurrentDate(): [number, number, number, number] { + const now = moment().utc(); + + const y = now.year(); + const m = now.month(); + const d = now.date(); + const h = now.hour(); + + return [y, m, d, h]; + } + + @autobind + private getLatestLog(span: Span, group: string = null): Promise<Log> { + return this.repository.findOne({ + group: group, + span: span + }, { + order: { + date: -1 + } + }); + } + + @autobind + private async getCurrentLog(span: Span, group: string = null): Promise<Log> { + const [y, m, d, h] = this.getCurrentDate(); + + const current = + span == 'day' ? utc([y, m, d]) : + span == 'hour' ? utc([y, m, d, h]) : + null; + + // 現在(今日または今のHour)のログ + const currentLog = await this.repository.findOne({ + span: span, + date: Chart.momentToTimestamp(current), + ...(group ? { group: group } : {}) + }); + + // ログがあればそれを返して終了 + if (currentLog != null) { + return currentLog; + } + + let log: Log; + let data: T; + + // 集計期間が変わってから、初めてのチャート更新なら + // 最も最近のログを持ってくる + // * 例えば集計期間が「日」である場合で考えると、 + // * 昨日何もチャートを更新するような出来事がなかった場合は、 + // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、 + // * 「昨日の」と決め打ちせずに「もっとも最近の」とします + const latest = await this.getLatestLog(span, group); + + if (latest != null) { + const obj = Chart.convertFlattenColumnsToObject( + latest as Record<string, any>); + + // 空ログデータを作成 + data = await this.getNewLog(obj); + } else { + // ログが存在しなかったら + // (Misskeyインスタンスを建てて初めてのチャート更新時) + + // 初期ログデータを作成 + data = await this.getNewLog(null); + + logger.info(`${this.name}: Initial commit created`); + } + + try { + // 新規ログ挿入 + log = await this.repository.save({ + group: group, + span: span, + date: Chart.momentToTimestamp(current), + ...Chart.convertObjectToFlattenColumns(data) + }); + } catch (e) { + // duplicate key error + // 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある + // その場合は再度最も新しいログを持ってくる + if (isDuplicateKeyValueError(e)) { + log = await this.getLatestLog(span, group); + } else { + logger.error(e); + throw e; + } + } + + return log; + } + + @autobind + protected commit(query: Record<string, Function>, group: string = null, uniqueKey?: string, uniqueValue?: string): Promise<any> { + const update = async (log: Log) => { + // ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く + if ( + uniqueKey && + log.unique[uniqueKey] && + log.unique[uniqueKey].includes(uniqueValue) + ) return; + + // ユニークインクリメントの指定のキーに値を追加 + if (uniqueKey) { + if (log.unique[uniqueKey]) { + const sql = `jsonb_set("unique", '{${uniqueKey}}', ("unique"->>'${uniqueKey}')::jsonb || '["${uniqueValue}"]'::jsonb)`; + query['unique'] = () => sql; + } else { + const sql = `jsonb_set("unique", '{${uniqueKey}}', '["${uniqueValue}"]')`; + query['unique'] = () => sql; + } + } + + // ログ更新 + await this.repository.createQueryBuilder() + .update() + .set(query) + .where('id = :id', { id: log.id }) + .execute(); + }; + + return Promise.all([ + this.getCurrentLog('day', group).then(log => update(log)), + this.getCurrentLog('hour', group).then(log => update(log)), + ]); + } + + @autobind + protected async inc(inc: DeepPartial<T>, group: string = null): Promise<void> { + await this.commit(Chart.convertQuery(inc as any), group); + } + + @autobind + protected async incIfUnique(inc: DeepPartial<T>, key: string, value: string, group: string = null): Promise<void> { + await this.commit(Chart.convertQuery(inc as any), group, key, value); + } + + @autobind + public async getChart(span: Span, range: number, group: string = null): Promise<ArrayValue<T>> { + const [y, m, d, h] = this.getCurrentDate(); + + const gt = + span == 'day' ? utc([y, m, d]).subtract(range, 'days') : + span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') : + null; + + // ログ取得 + let logs = await this.repository.find({ + where: { + group: group, + span: span, + date: MoreThanOrEqual(Chart.momentToTimestamp(gt)) + }, + order: { + date: -1 + }, + }); + + // 要求された範囲にログがひとつもなかったら + if (logs.length === 0) { + // もっとも新しいログを持ってくる + // (すくなくともひとつログが無いと隙間埋めできないため) + const recentLog = await this.repository.findOne({ + group: group, + span: span + }, { + order: { + date: -1 + }, + }); + + if (recentLog) { + logs = [recentLog]; + } + + // 要求された範囲の最も古い箇所に位置するログが存在しなかったら + } else if (!utc(logs[logs.length - 1].date * 1000).isSame(gt)) { + // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する + // (隙間埋めできないため) + const outdatedLog = await this.repository.findOne({ + group: group, + span: span, + date: LessThan(Chart.momentToTimestamp(gt)) + }, { + order: { + date: -1 + }, + }); + + if (outdatedLog) { + logs.push(outdatedLog); + } + } + + const chart: T[] = []; + + // 整形 + for (let i = (range - 1); i >= 0; i--) { + const current = + span == 'day' ? utc([y, m, d]).subtract(i, 'days') : + span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') : + null; + + const log = logs.find(l => utc(l.date * 1000).isSame(current)); + + if (log) { + const data = Chart.convertFlattenColumnsToObject(log as Record<string, any>); + chart.unshift(data); + } else { + // 隙間埋め + const latest = logs.find(l => utc(l.date * 1000).isBefore(current)); + const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null; + chart.unshift(this.getNewLog(data)); + } + } + + const res: ArrayValue<T> = {} as any; + + /** + * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }] + * を + * { foo: [1, 2, 3], bar: [5, 6, 7] } + * にする + */ + const dive = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}.${k}` : k; + if (typeof v == 'object') { + dive(v, p); + } else { + nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p))); + } + } + }; + + dive(chart[0]); + + return res; + } +} + +export function convertLog(logSchema: Schema): Schema { + const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy + if (v.type === 'number') { + v.type = 'array'; + v.items = { + type: 'number' + }; + } else if (v.type === 'object') { + for (const k of Object.keys(v.properties)) { + v.properties[k] = convertLog(v.properties[k]); + } + } + return v; +} diff --git a/src/services/chart/drive.ts b/src/services/chart/drive.ts deleted file mode 100644 index dd23412c7d..0000000000 --- a/src/services/chart/drive.ts +++ /dev/null @@ -1,150 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; -import { isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -const logSchema = { - /** - * 集計期間時点での、全ドライブファイル数 - */ - totalCount: { - type: 'number' as 'number', - description: '集計期間時点での、全ドライブファイル数' - }, - - /** - * 集計期間時点での、全ドライブファイルの合計サイズ - */ - totalSize: { - type: 'number' as 'number', - description: '集計期間時点での、全ドライブファイルの合計サイズ' - }, - - /** - * 増加したドライブファイル数 - */ - incCount: { - type: 'number' as 'number', - description: '増加したドライブファイル数' - }, - - /** - * 増加したドライブ使用量 - */ - incSize: { - type: 'number' as 'number', - description: '増加したドライブ使用量' - }, - - /** - * 減少したドライブファイル数 - */ - decCount: { - type: 'number' as 'number', - description: '減少したドライブファイル数' - }, - - /** - * 減少したドライブ使用量 - */ - decSize: { - type: 'number' as 'number', - description: '減少したドライブ使用量' - }, -}; - -export const driveLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type DriveLog = SchemaType<typeof driveLogSchema>; - -class DriveChart extends Chart<DriveLog> { - constructor() { - super('drive'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: DriveLog): Promise<DriveLog> { - const calcSize = (local: boolean) => DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': local ? null : { $ne: null }, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(res => res.length > 0 ? res[0].usage : 0); - - const [localCount, remoteCount, localSize, remoteSize] = init ? await Promise.all([ - DriveFile.count({ 'metadata._user.host': null }), - DriveFile.count({ 'metadata._user.host': { $ne: null } }), - calcSize(true), - calcSize(false) - ]) : [ - latest ? latest.local.totalCount : 0, - latest ? latest.remote.totalCount : 0, - latest ? latest.local.totalSize : 0, - latest ? latest.remote.totalSize : 0 - ]; - - return { - local: { - totalCount: localCount, - totalSize: localSize, - incCount: 0, - incSize: 0, - decCount: 0, - decSize: 0 - }, - remote: { - totalCount: remoteCount, - totalSize: remoteSize, - incCount: 0, - incSize: 0, - decCount: 0, - decSize: 0 - } - }; - } - - @autobind - public async update(file: IDriveFile, isAdditional: boolean) { - const update: Obj = {}; - - update.totalCount = isAdditional ? 1 : -1; - update.totalSize = isAdditional ? file.length : -file.length; - if (isAdditional) { - update.incCount = 1; - update.incSize = file.length; - } else { - update.decCount = 1; - update.decSize = file.length; - } - - await this.inc({ - [isLocalUser(file.metadata._user) ? 'local' : 'remote']: update - }); - } -} - -export default new DriveChart(); diff --git a/src/services/chart/entities.ts b/src/services/chart/entities.ts new file mode 100644 index 0000000000..14fd3adba0 --- /dev/null +++ b/src/services/chart/entities.ts @@ -0,0 +1,8 @@ +import Chart from './core'; + +export const entities = Object.values(require('require-all')({ + dirname: __dirname + '/charts/schemas', + resolve: (x: any) => { + return Chart.schemaToEntity(x.name, x.schema); + } +})); diff --git a/src/services/chart/federation.ts b/src/services/chart/federation.ts deleted file mode 100644 index 20da7a7421..0000000000 --- a/src/services/chart/federation.ts +++ /dev/null @@ -1,66 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import Instance from '../../models/instance'; - -/** - * フェデレーションに関するチャート - */ -type FederationLog = { - instance: { - /** - * インスタンス数の合計 - */ - total: number; - - /** - * 増加インスタンス数 - */ - inc: number; - - /** - * 減少インスタンス数 - */ - dec: number; - }; -}; - -class FederationChart extends Chart<FederationLog> { - constructor() { - super('federation'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: FederationLog): Promise<FederationLog> { - const [total] = init ? await Promise.all([ - Instance.count({}) - ]) : [ - latest ? latest.instance.total : 0 - ]; - - return { - instance: { - total: total, - inc: 0, - dec: 0 - } - }; - } - - @autobind - public async update(isAdditional: boolean) { - const update: Obj = {}; - - update.total = isAdditional ? 1 : -1; - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - await this.inc({ - instance: update - }); - } -} - -export default new FederationChart(); diff --git a/src/services/chart/hashtag.ts b/src/services/chart/hashtag.ts deleted file mode 100644 index 7a31e9cced..0000000000 --- a/src/services/chart/hashtag.ts +++ /dev/null @@ -1,56 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import { IUser, isLocalUser } from '../../models/user'; -import db from '../../db/mongodb'; - -/** - * ハッシュタグに関するチャート - */ -type HashtagLog = { - local: { - /** - * 投稿された数 - */ - count: number; - }; - - remote: HashtagLog['local']; -}; - -class HashtagChart extends Chart<HashtagLog> { - constructor() { - super('hashtag', true); - - // 後方互換性のため - db.get('chart.hashtag').findOne().then(doc => { - if (doc != null && doc.data.local == null) { - db.get('chart.hashtag').drop(); - } - }); - } - - @autobind - protected async getTemplate(init: boolean, latest?: HashtagLog): Promise<HashtagLog> { - return { - local: { - count: 0 - }, - remote: { - count: 0 - } - }; - } - - @autobind - public async update(hashtag: string, user: IUser) { - const update: Obj = { - count: 1 - }; - - await this.incIfUnique({ - [isLocalUser(user) ? 'local' : 'remote']: update - }, 'users', user._id.toHexString(), hashtag); - } -} - -export default new HashtagChart(); diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts index 7a6470f4d8..9626e3d6b3 100644 --- a/src/services/chart/index.ts +++ b/src/services/chart/index.ts @@ -1,364 +1,25 @@ -/** - * チャートエンジン - */ +import FederationChart from './charts/classes/federation'; +import NotesChart from './charts/classes/notes'; +import UsersChart from './charts/classes/users'; +import NetworkChart from './charts/classes/network'; +import ActiveUsersChart from './charts/classes/active-users'; +import InstanceChart from './charts/classes/instance'; +import PerUserNotesChart from './charts/classes/per-user-notes'; +import DriveChart from './charts/classes/drive'; +import PerUserReactionsChart from './charts/classes/per-user-reactions'; +import HashtagChart from './charts/classes/hashtag'; +import PerUserFollowingChart from './charts/classes/per-user-following'; +import PerUserDriveChart from './charts/classes/per-user-drive'; -import * as moment from 'moment'; -import * as nestedProperty from 'nested-property'; -import autobind from 'autobind-decorator'; -import * as mongo from 'mongodb'; -import db from '../../db/mongodb'; -import { ICollection } from 'monk'; -import Logger from '../logger'; -import { Schema } from '../../misc/schema'; - -const logger = new Logger('chart'); - -const utc = moment.utc; - -export type Obj = { [key: string]: any }; - -export type Partial<T> = { - [P in keyof T]?: Partial<T[P]>; -}; - -type ArrayValue<T> = { - [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>; -}; - -type Span = 'day' | 'hour'; - -type Log<T extends Obj> = { - _id: mongo.ObjectID; - - /** - * 集計のグループ - */ - group?: any; - - /** - * 集計日時 - */ - date: Date; - - /** - * 集計期間 - */ - span: Span; - - /** - * データ - */ - data: T; - - /** - * ユニークインクリメント用 - */ - unique?: Obj; -}; - -/** - * 様々なチャートの管理を司るクラス - */ -export default abstract class Chart<T extends Obj> { - protected collection: ICollection<Log<T>>; - protected abstract async getTemplate(init: boolean, latest?: T, group?: any): Promise<T>; - private name: string; - - constructor(name: string, grouped = false) { - this.name = name; - this.collection = db.get<Log<T>>(`chart.${name}`); - - const keys = { - span: -1, - date: -1 - } as { [key: string]: 1 | -1; }; - if (grouped) keys.group = -1; - - this.collection.createIndex(keys, { unique: true }); - } - - @autobind - private convertQuery(x: Obj, path: string): Obj { - const query: Obj = {}; - - const dive = (x: Obj, path: string) => { - for (const [k, v] of Object.entries(x)) { - const p = path ? `${path}.${k}` : k; - if (typeof v === 'number') { - query[p] = v; - } else { - dive(v, p); - } - } - }; - - dive(x, path); - - return query; - } - - @autobind - private getCurrentDate(): [number, number, number, number] { - const now = moment().utc(); - - const y = now.year(); - const m = now.month(); - const d = now.date(); - const h = now.hour(); - - return [y, m, d, h]; - } - - @autobind - private getLatestLog(span: Span, group?: any): Promise<Log<T>> { - return this.collection.findOne({ - group: group, - span: span - }, { - sort: { - date: -1 - } - }); - } - - @autobind - private async getCurrentLog(span: Span, group?: any): Promise<Log<T>> { - const [y, m, d, h] = this.getCurrentDate(); - - const current = - span == 'day' ? utc([y, m, d]) : - span == 'hour' ? utc([y, m, d, h]) : - null; - - // 現在(今日または今のHour)のログ - const currentLog = await this.collection.findOne({ - group: group, - span: span, - date: current.toDate() - }); - - // ログがあればそれを返して終了 - if (currentLog != null) { - return currentLog; - } - - let log: Log<T>; - let data: T; - - // 集計期間が変わってから、初めてのチャート更新なら - // 最も最近のログを持ってくる - // * 例えば集計期間が「日」である場合で考えると、 - // * 昨日何もチャートを更新するような出来事がなかった場合は、 - // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、 - // * 「昨日の」と決め打ちせずに「もっとも最近の」とします - const latest = await this.getLatestLog(span, group); - - if (latest != null) { - // 空ログデータを作成 - data = await this.getTemplate(false, latest.data); - } else { - // ログが存在しなかったら - // (Misskeyインスタンスを建てて初めてのチャート更新時など - // または何らかの理由でチャートコレクションを抹消した場合) - - // 初期ログデータを作成 - data = await this.getTemplate(true, null, group); - - logger.info(`${this.name}: Initial commit created`); - } - - try { - // 新規ログ挿入 - log = await this.collection.insert({ - group: group, - span: span, - date: current.toDate(), - data: data - }); - } catch (e) { - // 11000 is duplicate key error - // 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある - // その場合は再度最も新しいログを持ってくる - if (e.code === 11000) { - log = await this.getLatestLog(span, group); - } else { - logger.error(e); - throw e; - } - } - - return log; - } - - @autobind - protected commit(query: Obj, group?: any, uniqueKey?: string, uniqueValue?: string): void { - const update = (log: Log<T>) => { - // ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く - if ( - uniqueKey && - log.unique && - log.unique[uniqueKey] && - log.unique[uniqueKey].includes(uniqueValue) - ) return; - - // ユニークインクリメントの指定のキーに値を追加 - if (uniqueKey) { - query['$push'] = { - [`unique.${uniqueKey}`]: uniqueValue - }; - } - - // ログ更新 - this.collection.update({ - _id: log._id - }, query); - }; - - this.getCurrentLog('day', group).then(log => update(log)); - this.getCurrentLog('hour', group).then(log => update(log)); - } - - @autobind - protected inc(inc: Partial<T>, group?: any): void { - this.commit({ - $inc: this.convertQuery(inc, 'data') - }, group); - } - - @autobind - protected incIfUnique(inc: Partial<T>, key: string, value: string, group?: any): void { - this.commit({ - $inc: this.convertQuery(inc, 'data') - }, group, key, value); - } - - @autobind - public async getChart(span: Span, range: number, group?: any): Promise<ArrayValue<T>> { - const promisedChart: Promise<T>[] = []; - - const [y, m, d, h] = this.getCurrentDate(); - - const gt = - span == 'day' ? utc([y, m, d]).subtract(range, 'days') : - span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') : - null; - - // ログ取得 - let logs = await this.collection.find({ - group: group, - span: span, - date: { - $gte: gt.toDate() - } - }, { - sort: { - date: -1 - }, - fields: { - _id: 0 - } - }); - - // 要求された範囲にログがひとつもなかったら - if (logs.length == 0) { - // もっとも新しいログを持ってくる - // (すくなくともひとつログが無いと隙間埋めできないため) - const recentLog = await this.collection.findOne({ - group: group, - span: span - }, { - sort: { - date: -1 - }, - fields: { - _id: 0 - } - }); - - if (recentLog) { - logs = [recentLog]; - } - - // 要求された範囲の最も古い箇所に位置するログが存在しなかったら - } else if (!utc(logs[logs.length - 1].date).isSame(gt)) { - // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する - // (隙間埋めできないため) - const outdatedLog = await this.collection.findOne({ - group: group, - span: span, - date: { - $lt: gt.toDate() - } - }, { - sort: { - date: -1 - }, - fields: { - _id: 0 - } - }); - - if (outdatedLog) { - logs.push(outdatedLog); - } - } - - // 整形 - for (let i = (range - 1); i >= 0; i--) { - const current = - span == 'day' ? utc([y, m, d]).subtract(i, 'days') : - span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') : - null; - - const log = logs.find(l => utc(l.date).isSame(current)); - - if (log) { - promisedChart.unshift(Promise.resolve(log.data)); - } else { - // 隙間埋め - const latest = logs.find(l => utc(l.date).isBefore(current)); - promisedChart.unshift(this.getTemplate(false, latest ? latest.data : null)); - } - } - - const chart = await Promise.all(promisedChart); - - const res: ArrayValue<T> = {} as any; - - /** - * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }] - * を - * { foo: [1, 2, 3], bar: [5, 6, 7] } - * にする - */ - const dive = (x: Obj, path?: string) => { - for (const [k, v] of Object.entries(x)) { - const p = path ? `${path}.${k}` : k; - if (typeof v == 'object') { - dive(v, p); - } else { - nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p))); - } - } - }; - - dive(chart[0]); - - return res; - } -} - -export function convertLog(logSchema: Schema): Schema { - const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy - if (v.type === 'number') { - v.type = 'array'; - v.items = { - type: 'number' - }; - } else if (v.type === 'object') { - for (const k of Object.keys(v.properties)) { - v.properties[k] = convertLog(v.properties[k]); - } - } - return v; -} +export const federationChart = new FederationChart(); +export const notesChart = new NotesChart(); +export const usersChart = new UsersChart(); +export const networkChart = new NetworkChart(); +export const activeUsersChart = new ActiveUsersChart(); +export const instanceChart = new InstanceChart(); +export const perUserNotesChart = new PerUserNotesChart(); +export const driveChart = new DriveChart(); +export const perUserReactionsChart = new PerUserReactionsChart(); +export const hashtagChart = new HashtagChart(); +export const perUserFollowingChart = new PerUserFollowingChart(); +export const perUserDriveChart = new PerUserDriveChart(); diff --git a/src/services/chart/instance.ts b/src/services/chart/instance.ts deleted file mode 100644 index 5af398b902..0000000000 --- a/src/services/chart/instance.ts +++ /dev/null @@ -1,302 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import User from '../../models/user'; -import Note from '../../models/note'; -import Following from '../../models/following'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; - -/** - * インスタンスごとのチャート - */ -type InstanceLog = { - requests: { - /** - * 失敗したリクエスト数 - */ - failed: number; - - /** - * 成功したリクエスト数 - */ - succeeded: number; - - /** - * 受信したリクエスト数 - */ - received: number; - }; - - notes: { - /** - * 集計期間時点での、全投稿数 - */ - total: number; - - /** - * 増加した投稿数 - */ - inc: number; - - /** - * 減少した投稿数 - */ - dec: number; - }; - - users: { - /** - * 集計期間時点での、全ユーザー数 - */ - total: number; - - /** - * 増加したユーザー数 - */ - inc: number; - - /** - * 減少したユーザー数 - */ - dec: number; - }; - - following: { - /** - * 集計期間時点での、全フォロー数 - */ - total: number; - - /** - * 増加したフォロー数 - */ - inc: number; - - /** - * 減少したフォロー数 - */ - dec: number; - }; - - followers: { - /** - * 集計期間時点での、全フォロワー数 - */ - total: number; - - /** - * 増加したフォロワー数 - */ - inc: number; - - /** - * 減少したフォロワー数 - */ - dec: number; - }; - - drive: { - /** - * 集計期間時点での、全ドライブファイル数 - */ - totalFiles: number; - - /** - * 集計期間時点での、全ドライブファイルの合計サイズ - */ - totalUsage: number; - - /** - * 増加したドライブファイル数 - */ - incFiles: number; - - /** - * 増加したドライブ使用量 - */ - incUsage: number; - - /** - * 減少したドライブファイル数 - */ - decFiles: number; - - /** - * 減少したドライブ使用量 - */ - decUsage: number; - }; -}; - -class InstanceChart extends Chart<InstanceLog> { - constructor() { - super('instance', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: InstanceLog, group?: any): Promise<InstanceLog> { - const calcUsage = () => DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': group, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(res => res.length > 0 ? res[0].usage : 0); - - const [ - notesCount, - usersCount, - followingCount, - followersCount, - driveFiles, - driveUsage, - ] = init ? await Promise.all([ - Note.count({ '_user.host': group }), - User.count({ host: group }), - Following.count({ '_follower.host': group }), - Following.count({ '_followee.host': group }), - DriveFile.count({ 'metadata._user.host': group }), - calcUsage(), - ]) : [ - latest ? latest.notes.total : 0, - latest ? latest.users.total : 0, - latest ? latest.following.total : 0, - latest ? latest.followers.total : 0, - latest ? latest.drive.totalFiles : 0, - latest ? latest.drive.totalUsage : 0, - ]; - - return { - requests: { - failed: 0, - succeeded: 0, - received: 0 - }, - notes: { - total: notesCount, - inc: 0, - dec: 0 - }, - users: { - total: usersCount, - inc: 0, - dec: 0 - }, - following: { - total: followingCount, - inc: 0, - dec: 0 - }, - followers: { - total: followersCount, - inc: 0, - dec: 0 - }, - drive: { - totalFiles: driveFiles, - totalUsage: driveUsage, - incFiles: 0, - incUsage: 0, - decFiles: 0, - decUsage: 0 - } - }; - } - - @autobind - public async requestReceived(host: string) { - await this.inc({ - requests: { - received: 1 - } - }, host); - } - - @autobind - public async requestSent(host: string, isSucceeded: boolean) { - const update: Obj = {}; - - if (isSucceeded) { - update.succeeded = 1; - } else { - update.failed = 1; - } - - await this.inc({ - requests: update - }, host); - } - - @autobind - public async newUser(host: string) { - await this.inc({ - users: { - total: 1, - inc: 1 - } - }, host); - } - - @autobind - public async updateNote(host: string, isAdditional: boolean) { - await this.inc({ - notes: { - total: isAdditional ? 1 : -1, - inc: isAdditional ? 1 : 0, - dec: isAdditional ? 0 : 1, - } - }, host); - } - - @autobind - public async updateFollowing(host: string, isAdditional: boolean) { - await this.inc({ - following: { - total: isAdditional ? 1 : -1, - inc: isAdditional ? 1 : 0, - dec: isAdditional ? 0 : 1, - } - }, host); - } - - @autobind - public async updateFollowers(host: string, isAdditional: boolean) { - await this.inc({ - followers: { - total: isAdditional ? 1 : -1, - inc: isAdditional ? 1 : 0, - dec: isAdditional ? 0 : 1, - } - }, host); - } - - @autobind - public async updateDrive(file: IDriveFile, isAdditional: boolean) { - const update: Obj = {}; - - update.totalFiles = isAdditional ? 1 : -1; - update.totalUsage = isAdditional ? file.length : -file.length; - if (isAdditional) { - update.incFiles = 1; - update.incUsage = file.length; - } else { - update.decFiles = 1; - update.decUsage = file.length; - } - - await this.inc({ - drive: update - }, file.metadata._user.host); - } -} - -export default new InstanceChart(); diff --git a/src/services/chart/network.ts b/src/services/chart/network.ts deleted file mode 100644 index fce47099d1..0000000000 --- a/src/services/chart/network.ts +++ /dev/null @@ -1,64 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Partial } from './'; - -/** - * ネットワークに関するチャート - */ -type NetworkLog = { - /** - * 受信したリクエスト数 - */ - incomingRequests: number; - - /** - * 送信したリクエスト数 - */ - outgoingRequests: number; - - /** - * 応答時間の合計 - * TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる - */ - totalTime: number; - - /** - * 合計受信データ量 - */ - incomingBytes: number; - - /** - * 合計送信データ量 - */ - outgoingBytes: number; -}; - -class NetworkChart extends Chart<NetworkLog> { - constructor() { - super('network'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: NetworkLog): Promise<NetworkLog> { - return { - incomingRequests: 0, - outgoingRequests: 0, - totalTime: 0, - incomingBytes: 0, - outgoingBytes: 0 - }; - } - - @autobind - public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) { - const inc: Partial<NetworkLog> = { - incomingRequests: incomingRequests, - totalTime: time, - incomingBytes: incomingBytes, - outgoingBytes: outgoingBytes - }; - - await this.inc(inc); - } -} - -export default new NetworkChart(); diff --git a/src/services/chart/notes.ts b/src/services/chart/notes.ts deleted file mode 100644 index b047ec273f..0000000000 --- a/src/services/chart/notes.ts +++ /dev/null @@ -1,127 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import Note, { INote } from '../../models/note'; -import { isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -const logSchema = { - total: { - type: 'number' as 'number', - description: '集計期間時点での、全投稿数' - }, - - inc: { - type: 'number' as 'number', - description: '増加した投稿数' - }, - - dec: { - type: 'number' as 'number', - description: '減少した投稿数' - }, - - diffs: { - type: 'object' as 'object', - properties: { - normal: { - type: 'number' as 'number', - description: '通常の投稿数の差分' - }, - - reply: { - type: 'number' as 'number', - description: 'リプライの投稿数の差分' - }, - - renote: { - type: 'number' as 'number', - description: 'Renoteの投稿数の差分' - }, - } - }, -}; - -export const notesLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type NotesLog = SchemaType<typeof notesLogSchema>; - -class NotesChart extends Chart<NotesLog> { - constructor() { - super('notes'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: NotesLog): Promise<NotesLog> { - const [localCount, remoteCount] = init ? await Promise.all([ - Note.count({ '_user.host': null }), - Note.count({ '_user.host': { $ne: null } }) - ]) : [ - latest ? latest.local.total : 0, - latest ? latest.remote.total : 0 - ]; - - return { - local: { - total: localCount, - inc: 0, - dec: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }, - remote: { - total: remoteCount, - inc: 0, - dec: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - } - }; - } - - @autobind - public async update(note: INote, isAdditional: boolean) { - const update: Obj = { - diffs: {} - }; - - update.total = isAdditional ? 1 : -1; - - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - if (note.replyId != null) { - update.diffs.reply = isAdditional ? 1 : -1; - } else if (note.renoteId != null) { - update.diffs.renote = isAdditional ? 1 : -1; - } else { - update.diffs.normal = isAdditional ? 1 : -1; - } - - await this.inc({ - [isLocalUser(note._user) ? 'local' : 'remote']: update - }); - } -} - -export default new NotesChart(); diff --git a/src/services/chart/per-user-drive.ts b/src/services/chart/per-user-drive.ts deleted file mode 100644 index 4f335f1688..0000000000 --- a/src/services/chart/per-user-drive.ts +++ /dev/null @@ -1,122 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; -import { SchemaType } from '../../misc/schema'; - -export const perUserDriveLogSchema = { - type: 'object' as 'object', - properties: { - /** - * 集計期間時点での、全ドライブファイル数 - */ - totalCount: { - type: 'number' as 'number', - description: '集計期間時点での、全ドライブファイル数' - }, - - /** - * 集計期間時点での、全ドライブファイルの合計サイズ - */ - totalSize: { - type: 'number' as 'number', - description: '集計期間時点での、全ドライブファイルの合計サイズ' - }, - - /** - * 増加したドライブファイル数 - */ - incCount: { - type: 'number' as 'number', - description: '増加したドライブファイル数' - }, - - /** - * 増加したドライブ使用量 - */ - incSize: { - type: 'number' as 'number', - description: '増加したドライブ使用量' - }, - - /** - * 減少したドライブファイル数 - */ - decCount: { - type: 'number' as 'number', - description: '減少したドライブファイル数' - }, - - /** - * 減少したドライブ使用量 - */ - decSize: { - type: 'number' as 'number', - description: '減少したドライブ使用量' - }, - } -}; - -type PerUserDriveLog = SchemaType<typeof perUserDriveLogSchema>; - -class PerUserDriveChart extends Chart<PerUserDriveLog> { - constructor() { - super('perUserDrive', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserDriveLog, group?: any): Promise<PerUserDriveLog> { - const calcSize = () => DriveFile - .aggregate([{ - $match: { - 'metadata.userId': group, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(res => res.length > 0 ? res[0].usage : 0); - - const [count, size] = init ? await Promise.all([ - DriveFile.count({ 'metadata.userId': group }), - calcSize() - ]) : [ - latest ? latest.totalCount : 0, - latest ? latest.totalSize : 0 - ]; - - return { - totalCount: count, - totalSize: size, - incCount: 0, - incSize: 0, - decCount: 0, - decSize: 0 - }; - } - - @autobind - public async update(file: IDriveFile, isAdditional: boolean) { - const update: Obj = {}; - - update.totalCount = isAdditional ? 1 : -1; - update.totalSize = isAdditional ? file.length : -file.length; - if (isAdditional) { - update.incCount = 1; - update.incSize = file.length; - } else { - update.decCount = 1; - update.decSize = file.length; - } - - await this.inc(update, file.metadata.userId); - } -} - -export default new PerUserDriveChart(); diff --git a/src/services/chart/per-user-following.ts b/src/services/chart/per-user-following.ts deleted file mode 100644 index 8a94a4f155..0000000000 --- a/src/services/chart/per-user-following.ts +++ /dev/null @@ -1,162 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import Following from '../../models/following'; -import { IUser, isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -export const logSchema = { - /** - * フォローしている - */ - followings: { - type: 'object' as 'object', - properties: { - /** - * フォローしている合計 - */ - total: { - type: 'number', - description: 'フォローしている合計', - }, - - /** - * フォローした数 - */ - inc: { - type: 'number', - description: 'フォローした数', - }, - - /** - * フォロー解除した数 - */ - dec: { - type: 'number', - description: 'フォロー解除した数', - }, - } - }, - - /** - * フォローされている - */ - followers: { - type: 'object' as 'object', - properties: { - /** - * フォローされている合計 - */ - total: { - type: 'number', - description: 'フォローされている合計', - }, - - /** - * フォローされた数 - */ - inc: { - type: 'number', - description: 'フォローされた数', - }, - - /** - * フォロー解除された数 - */ - dec: { - type: 'number', - description: 'フォロー解除された数', - }, - } - }, -}; - -export const perUserFollowingLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type PerUserFollowingLog = SchemaType<typeof perUserFollowingLogSchema>; - -class PerUserFollowingChart extends Chart<PerUserFollowingLog> { - constructor() { - super('perUserFollowing', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserFollowingLog, group?: any): Promise<PerUserFollowingLog> { - const [ - localFollowingsCount, - localFollowersCount, - remoteFollowingsCount, - remoteFollowersCount - ] = init ? await Promise.all([ - Following.count({ followerId: group, '_followee.host': null }), - Following.count({ followeeId: group, '_follower.host': null }), - Following.count({ followerId: group, '_followee.host': { $ne: null } }), - Following.count({ followeeId: group, '_follower.host': { $ne: null } }) - ]) : [ - latest ? latest.local.followings.total : 0, - latest ? latest.local.followers.total : 0, - latest ? latest.remote.followings.total : 0, - latest ? latest.remote.followers.total : 0 - ]; - - return { - local: { - followings: { - total: localFollowingsCount, - inc: 0, - dec: 0 - }, - followers: { - total: localFollowersCount, - inc: 0, - dec: 0 - } - }, - remote: { - followings: { - total: remoteFollowingsCount, - inc: 0, - dec: 0 - }, - followers: { - total: remoteFollowersCount, - inc: 0, - dec: 0 - } - } - }; - } - - @autobind - public async update(follower: IUser, followee: IUser, isFollow: boolean) { - const update: Obj = {}; - - update.total = isFollow ? 1 : -1; - - if (isFollow) { - update.inc = 1; - } else { - update.dec = 1; - } - - this.inc({ - [isLocalUser(follower) ? 'local' : 'remote']: { followings: update } - }, follower._id); - this.inc({ - [isLocalUser(followee) ? 'local' : 'remote']: { followers: update } - }, followee._id); - } -} - -export default new PerUserFollowingChart(); diff --git a/src/services/chart/per-user-notes.ts b/src/services/chart/per-user-notes.ts deleted file mode 100644 index 2f4f882091..0000000000 --- a/src/services/chart/per-user-notes.ts +++ /dev/null @@ -1,100 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import Note, { INote } from '../../models/note'; -import { IUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -export const perUserNotesLogSchema = { - type: 'object' as 'object', - properties: { - total: { - type: 'number' as 'number', - description: '集計期間時点での、全投稿数' - }, - - inc: { - type: 'number' as 'number', - description: '増加した投稿数' - }, - - dec: { - type: 'number' as 'number', - description: '減少した投稿数' - }, - - diffs: { - type: 'object' as 'object', - properties: { - normal: { - type: 'number' as 'number', - description: '通常の投稿数の差分' - }, - - reply: { - type: 'number' as 'number', - description: 'リプライの投稿数の差分' - }, - - renote: { - type: 'number' as 'number', - description: 'Renoteの投稿数の差分' - }, - } - }, - } -}; - -type PerUserNotesLog = SchemaType<typeof perUserNotesLogSchema>; - -class PerUserNotesChart extends Chart<PerUserNotesLog> { - constructor() { - super('perUserNotes', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserNotesLog, group?: any): Promise<PerUserNotesLog> { - const [count] = init ? await Promise.all([ - Note.count({ userId: group, deletedAt: null }), - ]) : [ - latest ? latest.total : 0 - ]; - - return { - total: count, - inc: 0, - dec: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }; - } - - @autobind - public async update(user: IUser, note: INote, isAdditional: boolean) { - const update: Obj = { - diffs: {} - }; - - update.total = isAdditional ? 1 : -1; - - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - if (note.replyId != null) { - update.diffs.reply = isAdditional ? 1 : -1; - } else if (note.renoteId != null) { - update.diffs.renote = isAdditional ? 1 : -1; - } else { - update.diffs.normal = isAdditional ? 1 : -1; - } - - await this.inc(update, user._id); - } -} - -export default new PerUserNotesChart(); diff --git a/src/services/chart/per-user-reactions.ts b/src/services/chart/per-user-reactions.ts deleted file mode 100644 index 60495aeb02..0000000000 --- a/src/services/chart/per-user-reactions.ts +++ /dev/null @@ -1,45 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart from './'; -import { IUser, isLocalUser } from '../../models/user'; -import { INote } from '../../models/note'; - -/** - * ユーザーごとのリアクションに関するチャート - */ -type PerUserReactionsLog = { - local: { - /** - * リアクションされた数 - */ - count: number; - }; - - remote: PerUserReactionsLog['local']; -}; - -class PerUserReactionsChart extends Chart<PerUserReactionsLog> { - constructor() { - super('perUserReaction', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserReactionsLog, group?: any): Promise<PerUserReactionsLog> { - return { - local: { - count: 0 - }, - remote: { - count: 0 - } - }; - } - - @autobind - public async update(user: IUser, note: INote) { - this.inc({ - [isLocalUser(user) ? 'local' : 'remote']: { count: 1 } - }, note.userId); - } -} - -export default new PerUserReactionsChart(); diff --git a/src/services/chart/users.ts b/src/services/chart/users.ts deleted file mode 100644 index cca9590842..0000000000 --- a/src/services/chart/users.ts +++ /dev/null @@ -1,94 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import User, { IUser, isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -const logSchema = { - /** - * 集計期間時点での、全ユーザー数 - */ - total: { - type: 'number' as 'number', - description: '集計期間時点での、全ユーザー数' - }, - - /** - * 増加したユーザー数 - */ - inc: { - type: 'number' as 'number', - description: '増加したユーザー数' - }, - - /** - * 減少したユーザー数 - */ - dec: { - type: 'number' as 'number', - description: '減少したユーザー数' - }, -}; - -export const usersLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type UsersLog = SchemaType<typeof usersLogSchema>; - -class UsersChart extends Chart<UsersLog> { - constructor() { - super('users'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: UsersLog): Promise<UsersLog> { - const [localCount, remoteCount] = init ? await Promise.all([ - User.count({ host: null }), - User.count({ host: { $ne: null } }) - ]) : [ - latest ? latest.local.total : 0, - latest ? latest.remote.total : 0 - ]; - - return { - local: { - total: localCount, - inc: 0, - dec: 0 - }, - remote: { - total: remoteCount, - inc: 0, - dec: 0 - } - }; - } - - @autobind - public async update(user: IUser, isAdditional: boolean) { - const update: Obj = {}; - - update.total = isAdditional ? 1 : -1; - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - await this.inc({ - [isLocalUser(user) ? 'local' : 'remote']: update - }); - } -} - -export default new UsersChart(); diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts index 3e000ef2ed..bcb8214c56 100644 --- a/src/services/create-notification.ts +++ b/src/services/create-notification.ts @@ -1,62 +1,66 @@ -import * as mongo from 'mongodb'; -import Notification from '../models/notification'; -import Mute from '../models/mute'; -import { pack } from '../models/notification'; import { publishMainStream } from './stream'; -import User from '../models/user'; import pushSw from './push-notification'; +import { Notifications, Mutings } from '../models'; +import { genId } from '../misc/gen-id'; +import { User } from '../models/entities/user'; +import { Note } from '../models/entities/note'; +import { Notification } from '../models/entities/notification'; -export default ( - notifiee: mongo.ObjectID, - notifier: mongo.ObjectID, +export async function createNotification( + notifieeId: User['id'], + notifierId: User['id'], type: string, - content?: any -) => new Promise<any>(async (resolve, reject) => { - if (notifiee.equals(notifier)) { - return resolve(); + content?: { + noteId?: Note['id']; + reaction?: string; + choice?: number; + } +) { + if (notifieeId === notifierId) { + return null; } - // Create notification - const notification = await Notification.insert(Object.assign({ + const data = { + id: genId(), createdAt: new Date(), - notifieeId: notifiee, - notifierId: notifier, + notifieeId: notifieeId, + notifierId: notifierId, type: type, - isRead: false - }, content)); + isRead: false, + } as Partial<Notification>; + + if (content) { + if (content.noteId) data.noteId = content.noteId; + if (content.reaction) data.reaction = content.reaction; + if (content.choice) data.choice = content.choice; + } - resolve(notification); + // Create notification + const notification = await Notifications.save(data); - const packed = await pack(notification); + const packed = await Notifications.pack(notification); // Publish notification event - publishMainStream(notifiee, 'notification', packed); - - // Update flag - User.update({ _id: notifiee }, { - $set: { - hasUnreadNotification: true - } - }); + publishMainStream(notifieeId, 'notification', packed); // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する setTimeout(async () => { - const fresh = await Notification.findOne({ _id: notification._id }, { isRead: true }); + const fresh = await Notifications.findOne(notification.id); if (!fresh.isRead) { //#region ただしミュートしているユーザーからの通知なら無視 - const mute = await Mute.find({ - muterId: notifiee, - deletedAt: { $exists: false } + const mutings = await Mutings.find({ + muterId: notifieeId }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); - if (mutedUserIds.indexOf(notifier.toString()) != -1) { + if (mutings.map(m => m.muteeId).includes(notifierId)) { return; } //#endregion - publishMainStream(notifiee, 'unreadNotification', packed); + publishMainStream(notifieeId, 'unreadNotification', packed); - pushSw(notifiee, 'notification', packed); + pushSw(notifieeId, 'notification', packed); } }, 2000); -}); + + return notification; +} diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index cdbcb34de4..df5eedf4c8 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -1,31 +1,27 @@ import { Buffer } from 'buffer'; import * as fs from 'fs'; -import * as mongodb from 'mongodb'; import * as crypto from 'crypto'; import * as Minio from 'minio'; import * as uuid from 'uuid'; import * as sharp from 'sharp'; -import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file'; -import DriveFolder from '../../models/drive-folder'; -import { pack } from '../../models/drive-file'; import { publishMainStream, publishDriveStream } from '../stream'; -import { isLocalUser, IUser, IRemoteUser, isRemoteUser } from '../../models/user'; import delFile from './delete-file'; import config from '../../config'; -import { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic'; -import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; -import driveChart from '../../services/chart/drive'; -import perUserDriveChart from '../../services/chart/per-user-drive'; -import instanceChart from '../../services/chart/instance'; import fetchMeta from '../../misc/fetch-meta'; import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { driveLogger } from './logger'; import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor'; -import Instance from '../../models/instance'; import { contentDisposition } from '../../misc/content-disposition'; import { detectMine } from '../../misc/detect-mine'; +import { DriveFiles, DriveFolders, Users, Instances } from '../../models'; +import { InternalStorage } from './internal-storage'; +import { DriveFile } from '../../models/entities/drive-file'; +import { IRemoteUser, User } from '../../models/entities/user'; +import { driveChart, perUserDriveChart, instanceChart } from '../chart'; +import { genId } from '../../misc/gen-id'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; const logger = driveLogger.createSubLogger('register', 'yellow'); @@ -36,11 +32,10 @@ const logger = driveLogger.createSubLogger('register', 'yellow'); * @param type Content-Type for original * @param hash Hash for original * @param size Size for original - * @param metadata */ -async function save(path: string, name: string, type: string, hash: string, size: number, metadata: IMetadata): Promise<IDriveFile> { +async function save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<DriveFile> { // thunbnail, webpublic を必要なら生成 - const alts = await generateAlts(path, type, !metadata.uri); + const alts = await generateAlts(path, type, !file.uri); if (config.drive && config.drive.storage == 'minio') { //#region ObjectStorage params @@ -60,10 +55,10 @@ async function save(path: string, name: string, type: string, hash: string, size const url = `${ baseUrl }/${ key }`; // for alts - let webpublicKey = null as string; - let webpublicUrl = null as string; - let thumbnailKey = null as string; - let thumbnailUrl = null as string; + let webpublicKey: string = null; + let webpublicUrl: string = null; + let thumbnailKey: string = null; + let thumbnailUrl: string = null; //#endregion //#region Uploads @@ -91,58 +86,52 @@ async function save(path: string, name: string, type: string, hash: string, size await Promise.all(uploads); //#endregion - //#region DB - Object.assign(metadata, { - withoutChunks: true, - storage: 'minio', - storageProps: { - key, - webpublicKey, - thumbnailKey, - }, - url, - webpublicUrl, - thumbnailUrl, - } as IMetadata); + file.url = url; + file.thumbnailUrl = thumbnailUrl; + file.webpublicUrl = webpublicUrl; + file.accessKey = key; + file.thumbnailAccessKey = thumbnailKey; + file.webpublicAccessKey = webpublicKey; + file.name = name; + file.type = type; + file.md5 = hash; + file.size = size; + file.storedInternal = false; - const file = await DriveFile.insert({ - length: size, - uploadDate: new Date(), - md5: hash, - filename: name, - metadata: metadata, - contentType: type - }); - //#endregion + return await DriveFiles.save(file); + } else { // use internal storage + const accessKey = uuid.v4(); + const thumbnailAccessKey = uuid.v4(); + const webpublicAccessKey = uuid.v4(); - return file; - } else { // use MongoDB GridFS - // #region store original - const originalDst = await getDriveFileBucket(); + const url = InternalStorage.saveFromPath(accessKey, path); - // web用(Exif削除済み)がある場合はオリジナルにアクセス制限 - if (alts.webpublic) metadata.accessKey = uuid.v4(); + let thumbnailUrl: string; + let webpublicUrl: string; - const originalFile = await storeOriginal(originalDst, name, path, type, metadata); - - logger.info(`original stored to ${originalFile._id}`); - // #endregion store original + if (alts.thumbnail) { + thumbnailUrl = InternalStorage.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data); + logger.info(`thumbnail stored: ${thumbnailAccessKey}`); + } - // #region store webpublic if (alts.webpublic) { - const webDst = await getDriveFileWebpublicBucket(); - const webFile = await storeAlts(webDst, name, alts.webpublic.data, alts.webpublic.type, originalFile._id); - logger.info(`web stored ${webFile._id}`); + webpublicUrl = InternalStorage.saveFromBuffer(webpublicAccessKey, alts.webpublic.data); + logger.info(`web stored: ${webpublicAccessKey}`); } - // #endregion store webpublic - if (alts.thumbnail) { - const thumDst = await getDriveFileThumbnailBucket(); - const thumFile = await storeAlts(thumDst, name, alts.thumbnail.data, alts.thumbnail.type, originalFile._id); - logger.info(`web stored ${thumFile._id}`); - } + file.storedInternal = true; + file.url = url; + file.thumbnailUrl = thumbnailUrl; + file.webpublicUrl = webpublicUrl; + file.accessKey = accessKey; + file.thumbnailAccessKey = thumbnailAccessKey; + file.webpublicAccessKey = webpublicAccessKey; + file.name = name; + file.type = type; + file.md5 = hash; + file.size = size; - return originalFile; + return await DriveFiles.save(file); } } @@ -211,51 +200,14 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, await minio.putObject(config.drive.bucket, key, stream, null, metadata); } -/** - * GridFSBucketにオリジナルを格納する - */ -export async function storeOriginal(bucket: mongodb.GridFSBucket, name: string, path: string, contentType: string, metadata: any) { - return new Promise<IDriveFile>((resolve, reject) => { - const writeStream = bucket.openUploadStream(name, { - contentType, - metadata - }); - - writeStream.once('finish', resolve); - writeStream.on('error', reject); - fs.createReadStream(path).pipe(writeStream); - }); -} - -/** - * GridFSBucketにオリジナル以外を格納する - */ -export async function storeAlts(bucket: mongodb.GridFSBucket, name: string, data: Buffer, contentType: string, originalId: mongodb.ObjectID) { - return new Promise<IDriveFile>((resolve, reject) => { - const writeStream = bucket.openUploadStream(name, { - contentType, - metadata: { - originalId - } - }); - - writeStream.once('finish', resolve); - writeStream.on('error', reject); - writeStream.end(data); - }); -} - async function deleteOldFile(user: IRemoteUser) { - const oldFile = await DriveFile.findOne({ - _id: { - $nin: [user.avatarId, user.bannerId] - }, - 'metadata.userId': user._id - }, { - sort: { - _id: 1 - } - }); + const oldFile = await DriveFiles.createQueryBuilder() + .select('file') + .where('file.id != :avatarId', { avatarId: user.avatarId }) + .andWhere('file.id != :bannerId', { bannerId: user.bannerId }) + .andWhere('file.userId = :userId', { userId: user.id }) + .orderBy('file.id', 'DESC') + .getOne(); if (oldFile) { delFile(oldFile, true); @@ -278,17 +230,17 @@ async function deleteOldFile(user: IRemoteUser) { * @return Created drive file */ export default async function( - user: IUser, + user: User, path: string, name: string = null, comment: string = null, - folderId: mongodb.ObjectID = null, + folderId: any = null, force: boolean = false, isLink: boolean = false, url: string = null, uri: string = null, sensitive: boolean = null -): Promise<IDriveFile> { +): Promise<DriveFile> { // Calc md5 hash const calcHash = new Promise<string>((res, rej) => { const readable = fs.createReadStream(path); @@ -322,51 +274,29 @@ export default async function( if (!force) { // Check if there is a file with the same hash - const much = await DriveFile.findOne({ + const much = await DriveFiles.findOne({ md5: hash, - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } + userId: user.id, }); if (much) { - logger.info(`file with same hash is found: ${much._id}`); + logger.info(`file with same hash is found: ${much.id}`); return much; } } //#region Check drive usage if (!isLink) { - const usage = await DriveFile - .aggregate([{ - $match: { - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then((aggregates: any[]) => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - logger.debug(`drive usage is ${usage}`); + const usage = await DriveFiles.clacDriveUsageOf(user); const instance = await fetchMeta(); - const driveCapacity = 1024 * 1024 * (isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + + logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); // If usage limit exceeded if (usage + size > driveCapacity) { - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { throw 'no-free-space'; } else { // (アバターまたはバナーを含まず)最も古いファイルを削除する @@ -381,9 +311,9 @@ export default async function( return null; } - const driveFolder = await DriveFolder.findOne({ - _id: folderId, - userId: user._id + const driveFolder = await DriveFolders.findOne({ + id: folderId, + userId: user.id }); if (driveFolder == null) throw 'folder-not-found'; @@ -437,54 +367,48 @@ export default async function( const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]); - const metadata = { - userId: user._id, - _user: { - host: user.host - }, - folderId: folder !== null ? folder._id : null, - comment: comment, - properties: properties, - withoutChunks: isLink, - isRemote: isLink, - isSensitive: isLocalUser(user) && user.settings.alwaysMarkNsfw ? true : - (sensitive !== null && sensitive !== undefined) - ? sensitive - : false - } as IMetadata; + let file = new DriveFile(); + file.id = genId(); + file.createdAt = new Date(); + file.userId = user.id; + file.userHost = user.host; + file.folderId = folder !== null ? folder.id : null; + file.comment = comment; + file.properties = properties; + file.isRemote = isLink; + file.isSensitive = Users.isLocalUser(user) && user.alwaysMarkNsfw ? true : + (sensitive !== null && sensitive !== undefined) + ? sensitive + : false; if (url !== null) { - metadata.src = url; + file.src = url; if (isLink) { - metadata.url = url; + file.url = url; } } if (uri !== null) { - metadata.uri = uri; + file.uri = uri; } - let driveFile: IDriveFile; - if (isLink) { try { - driveFile = await DriveFile.insert({ - length: 0, - uploadDate: new Date(), - md5: hash, - filename: detectedName, - metadata: metadata, - contentType: mime - }); + file.size = 0; + file.md5 = hash; + file.name = detectedName; + file.type = mime; + + file = await DriveFiles.save(file); } catch (e) { // duplicate key error (when already registered) - if (e.code === 11000) { - logger.info(`already registered ${metadata.uri}`); + if (isDuplicateKeyValueError(e)) { + logger.info(`already registered ${file.uri}`); - driveFile = await DriveFile.findOne({ - 'metadata.uri': metadata.uri, - 'metadata.userId': user._id + file = await DriveFiles.findOne({ + uri: file.uri, + userId: user.id }); } else { logger.error(e); @@ -492,29 +416,25 @@ export default async function( } } } else { - driveFile = await (save(path, detectedName, mime, hash, size, metadata)); + file = await (save(file, path, detectedName, mime, hash, size)); } - logger.succ(`drive file has been created ${driveFile._id}`); + logger.succ(`drive file has been created ${file.id}`); - pack(driveFile).then(packedFile => { + DriveFiles.pack(file).then(packedFile => { // Publish driveFileCreated event - publishMainStream(user._id, 'driveFileCreated', packedFile); - publishDriveStream(user._id, 'fileCreated', packedFile); + publishMainStream(user.id, 'driveFileCreated', packedFile); + publishDriveStream(user.id, 'fileCreated', packedFile); }); // 統計を更新 - driveChart.update(driveFile, true); - perUserDriveChart.update(driveFile, true); - if (isRemoteUser(driveFile.metadata._user)) { - instanceChart.updateDrive(driveFile, true); - Instance.update({ host: driveFile.metadata._user.host }, { - $inc: { - driveUsage: driveFile.length, - driveFiles: 1 - } - }); + driveChart.update(file, true); + perUserDriveChart.update(file, true); + if (file.userHost !== null) { + instanceChart.updateDrive(file, true); + Instances.increment({ host: file.userHost }, 'driveUsage', file.size); + Instances.increment({ host: file.userHost }, 'driveFiles', 1); } - return driveFile; + return file; } diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts index c5c15ca20b..adf57416fe 100644 --- a/src/services/drive/delete-file.ts +++ b/src/services/drive/delete-file.ts @@ -1,99 +1,53 @@ import * as Minio from 'minio'; -import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file'; -import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail'; import config from '../../config'; -import driveChart from '../../services/chart/drive'; -import perUserDriveChart from '../../services/chart/per-user-drive'; -import instanceChart from '../../services/chart/instance'; -import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic'; -import Instance from '../../models/instance'; -import { isRemoteUser } from '../../models/user'; +import { DriveFile } from '../../models/entities/drive-file'; +import { InternalStorage } from './internal-storage'; +import { DriveFiles, Instances } from '../../models'; +import { driveChart, perUserDriveChart, instanceChart } from '../chart'; -export default async function(file: IDriveFile, isExpired = false) { - if (file.metadata.storage == 'minio') { - const minio = new Minio.Client(config.drive.config); - - // 後方互換性のため、file.metadata.storageProps.key があるかどうかチェックしています。 - // 将来的には const obj = file.metadata.storageProps.key; とします。 - const obj = file.metadata.storageProps.key ? file.metadata.storageProps.key : `${config.drive.prefix}/${file.metadata.storageProps.id}`; - await minio.removeObject(config.drive.bucket, obj); +export default async function(file: DriveFile, isExpired = false) { + if (file.storedInternal) { + InternalStorage.del(file.accessKey); - if (file.metadata.thumbnailUrl) { - // 後方互換性のため、file.metadata.storageProps.thumbnailKey があるかどうかチェックしています。 - // 将来的には const thumbnailObj = file.metadata.storageProps.thumbnailKey; とします。 - const thumbnailObj = file.metadata.storageProps.thumbnailKey ? file.metadata.storageProps.thumbnailKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`; - await minio.removeObject(config.drive.bucket, thumbnailObj); + if (file.thumbnailUrl) { + InternalStorage.del(file.thumbnailAccessKey); } - if (file.metadata.webpublicUrl) { - const webpublicObj = file.metadata.storageProps.webpublicKey ? file.metadata.storageProps.webpublicKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-original`; - await minio.removeObject(config.drive.bucket, webpublicObj); + if (file.webpublicUrl) { + InternalStorage.del(file.webpublicAccessKey); } - } + } else { + const minio = new Minio.Client(config.drive.config); - // チャンクをすべて削除 - await DriveFileChunk.remove({ - files_id: file._id - }); + await minio.removeObject(config.drive.bucket, file.accessKey); - const set = { - metadata: { - deletedAt: new Date(), - isExpired: isExpired + if (file.thumbnailUrl) { + await minio.removeObject(config.drive.bucket, file.thumbnailAccessKey); } - } as any; - - // リモートファイル期限切れ削除後は直リンクにする - if (isExpired && file.metadata && file.metadata._user && file.metadata._user.host != null) { - set.metadata.withoutChunks = true; - set.metadata.isRemote = true; - set.metadata.url = file.metadata.uri; - set.metadata.thumbnailUrl = undefined; - set.metadata.webpublicUrl = undefined; - } - - await DriveFile.update({ _id: file._id }, { - $set: set - }); - //#region サムネイルもあれば削除 - const thumbnail = await DriveFileThumbnail.findOne({ - 'metadata.originalId': file._id - }); - - if (thumbnail) { - await DriveFileThumbnailChunk.remove({ - files_id: thumbnail._id - }); - - await DriveFileThumbnail.remove({ _id: thumbnail._id }); + if (file.webpublicUrl) { + await minio.removeObject(config.drive.bucket, file.webpublicAccessKey); + } } - //#endregion - //#region Web公開用もあれば削除 - const webpublic = await DriveFileWebpublic.findOne({ - 'metadata.originalId': file._id - }); - - if (webpublic) { - await DriveFileWebpublicChunk.remove({ - files_id: webpublic._id + // リモートファイル期限切れ削除後は直リンクにする + if (isExpired && file.userHost !== null) { + DriveFiles.update(file.id, { + isRemote: true, + url: file.uri, + thumbnailUrl: null, + webpublicUrl: null }); - - await DriveFileWebpublic.remove({ _id: webpublic._id }); + } else { + DriveFiles.delete(file.id); } - //#endregion // 統計を更新 driveChart.update(file, false); perUserDriveChart.update(file, false); - if (isRemoteUser(file.metadata._user)) { + if (file.userHost !== null) { instanceChart.updateDrive(file, false); - Instance.update({ host: file.metadata._user.host }, { - $inc: { - driveUsage: -file.length, - driveFiles: -1 - } - }); + Instances.decrement({ host: file.userHost }, 'driveUsage', file.size); + Instances.decrement({ host: file.userHost }, 'driveFiles', 1); } } diff --git a/src/services/drive/internal-storage.ts b/src/services/drive/internal-storage.ts new file mode 100644 index 0000000000..ff890d7d47 --- /dev/null +++ b/src/services/drive/internal-storage.ts @@ -0,0 +1,27 @@ +import * as fs from 'fs'; +import * as Path from 'path'; +import config from '../../config'; + +export class InternalStorage { + private static readonly path = Path.resolve(`${__dirname}/../../../files`); + + public static read(key: string) { + return fs.createReadStream(`${InternalStorage.path}/${key}`); + } + + public static saveFromPath(key: string, srcPath: string) { + fs.mkdirSync(InternalStorage.path, { recursive: true }); + fs.copyFileSync(srcPath, `${InternalStorage.path}/${key}`); + return `${config.url}/files/${key}`; + } + + public static saveFromBuffer(key: string, data: Buffer) { + fs.mkdirSync(InternalStorage.path, { recursive: true }); + fs.writeFileSync(`${InternalStorage.path}/${key}`, data); + return `${config.url}/files/${key}`; + } + + public static del(key: string) { + fs.unlink(`${InternalStorage.path}/${key}`, () => {}); + } +} diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts index cdf6ba0cef..a7fe1fbd26 100644 --- a/src/services/drive/upload-from-url.ts +++ b/src/services/drive/upload-from-url.ts @@ -1,26 +1,26 @@ import * as URL from 'url'; - -import { IDriveFile, validateFileName } from '../../models/drive-file'; import create from './add-file'; -import { IUser } from '../../models/user'; -import * as mongodb from 'mongodb'; +import { User } from '../../models/entities/user'; import { driveLogger } from './logger'; import { createTemp } from '../../misc/create-temp'; import { downloadUrl } from '../../misc/donwload-url'; +import { DriveFolder } from '../../models/entities/drive-folder'; +import { DriveFile } from '../../models/entities/drive-file'; +import { DriveFiles } from '../../models'; const logger = driveLogger.createSubLogger('downloader'); export default async ( url: string, - user: IUser, - folderId: mongodb.ObjectID = null, + user: User, + folderId: DriveFolder['id'] = null, uri: string = null, sensitive = false, force = false, link = false -): Promise<IDriveFile> => { +): Promise<DriveFile> => { let name = URL.parse(url).pathname.split('/').pop(); - if (!validateFileName(name)) { + if (!DriveFiles.validateFileName(name)) { name = null; } @@ -30,12 +30,12 @@ export default async ( // write content at URL to temp file await downloadUrl(url, path); - let driveFile: IDriveFile; + let driveFile: DriveFile; let error; try { driveFile = await create(user, path, name, null, folderId, force, link, url, uri, sensitive); - logger.succ(`Got: ${driveFile._id}`); + logger.succ(`Got: ${driveFile.id}`); } catch (e) { error = e; logger.error(`Failed to create drive file: ${e}`, { diff --git a/src/services/following/create.ts b/src/services/following/create.ts index 1eaad750f7..28e4ba3c12 100644 --- a/src/services/following/create.ts +++ b/src/services/following/create.ts @@ -1,100 +1,70 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; -import Blocking from '../../models/blocking'; import { publishMainStream } from '../stream'; -import notify from '../../services/create-notification'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; import renderAccept from '../../remote/activitypub/renderer/accept'; import renderReject from '../../remote/activitypub/renderer/reject'; import { deliver } from '../../queue'; import createFollowRequest from './requests/create'; -import perUserFollowingChart from '../../services/chart/per-user-following'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; import Logger from '../logger'; -import FollowRequest from '../../models/follow-request'; import { IdentifiableError } from '../../misc/identifiable-error'; +import { User } from '../../models/entities/user'; +import { Followings, Users, FollowRequests, Blockings, Instances } from '../../models'; +import { instanceChart, perUserFollowingChart } from '../chart'; +import { genId } from '../../misc/gen-id'; +import { createNotification } from '../create-notification'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; const logger = new Logger('following/create'); -export async function insertFollowingDoc(followee: IUser, follower: IUser) { +export async function insertFollowingDoc(followee: User, follower: User) { + if (follower.id === followee.id) return; + let alreadyFollowed = false; - await Following.insert({ + await Followings.save({ + id: genId(), createdAt: new Date(), - followerId: follower._id, - followeeId: followee._id, + followerId: follower.id, + followeeId: followee.id, // 非正規化 - _follower: { - host: follower.host, - inbox: isRemoteUser(follower) ? follower.inbox : undefined, - sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined - }, - _followee: { - host: followee.host, - inbox: isRemoteUser(followee) ? followee.inbox : undefined, - sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined - } + followerHost: follower.host, + followerInbox: Users.isRemoteUser(follower) ? follower.inbox : null, + followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : null, + followeeHost: followee.host, + followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : null, + followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : null }).catch(e => { - if (e.code === 11000 && isRemoteUser(follower) && isLocalUser(followee)) { - logger.info(`Insert duplicated ignore. ${follower._id} => ${followee._id}`); + if (isDuplicateKeyValueError(e) && Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { + logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`); alreadyFollowed = true; } else { throw e; } }); - const removed = await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - if (removed.deletedCount === 1) { - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } - }); - } - if (alreadyFollowed) return; //#region Increment counts - User.update({ _id: follower._id }, { - $inc: { - followingCount: 1 - } - }); - - User.update({ _id: followee._id }, { - $inc: { - followersCount: 1 - } - }); + Users.increment({ id: follower.id }, 'followingCount', 1); + Users.increment({ id: followee.id }, 'followersCount', 1); //#endregion //#region Update instance stats - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { registerOrFetchInstanceDoc(follower.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followingCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'followingCount', 1); instanceChart.updateFollowing(i.host, true); }); - } else if (isLocalUser(follower) && isRemoteUser(followee)) { + } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { registerOrFetchInstanceDoc(followee.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followersCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'followersCount', 1); instanceChart.updateFollowers(i.host, true); }); } @@ -103,44 +73,42 @@ export async function insertFollowingDoc(followee: IUser, follower: IUser) { perUserFollowingChart.update(follower, followee, true); // Publish follow event - if (isLocalUser(follower)) { - packUser(followee, follower, { + if (Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'follow', packed)); + }).then(packed => publishMainStream(follower.id, 'follow', packed)); } // Publish followed event - if (isLocalUser(followee)) { - packUser(follower, followee).then(packed => publishMainStream(followee._id, 'followed', packed)), + if (Users.isLocalUser(followee)) { + Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'followed', packed)), // 通知を作成 - notify(followee._id, follower._id, 'follow'); + createNotification(followee.id, follower.id, 'follow'); } } -export default async function(follower: IUser, followee: IUser, requestId?: string) { +export default async function(follower: User, followee: User, requestId?: string) { // check blocking const [blocking, blocked] = await Promise.all([ - Blocking.findOne({ - blockerId: follower._id, - blockeeId: followee._id, + Blockings.findOne({ + blockerId: follower.id, + blockeeId: followee.id, }), - Blocking.findOne({ - blockerId: followee._id, - blockeeId: follower._id, + Blockings.findOne({ + blockerId: followee.id, + blockeeId: follower.id, }) ]); - if (isRemoteUser(follower) && isLocalUser(followee) && blocked) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocked) { // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 const content = renderActivity(renderReject(renderFollow(follower, followee, requestId), followee)); deliver(followee , content, follower.inbox); return; - } else if (isRemoteUser(follower) && isLocalUser(followee) && blocking) { + } else if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocking) { // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 - await Blocking.remove({ - _id: blocking._id - }); + await Blockings.delete(blocking.id); } else { // それ以外は単純に例外 if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); @@ -151,23 +119,23 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく - if (followee.isLocked || (followee.carefulBot && follower.isBot) || (isLocalUser(follower) && isRemoteUser(followee))) { + if (followee.isLocked || (followee.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) { let autoAccept = false; // 鍵アカウントであっても、既にフォローされていた場合はスルー - const following = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id, + const following = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id, }); if (following) { autoAccept = true; } // フォローしているユーザーは自動承認オプション - if (!autoAccept && (isLocalUser(followee) && followee.autoAcceptFollowed)) { - const followed = await Following.findOne({ - followerId: followee._id, - followeeId: follower._id + if (!autoAccept && (Users.isLocalUser(followee) && followee.autoAcceptFollowed)) { + const followed = await Followings.findOne({ + followerId: followee.id, + followeeId: follower.id }); if (followed) autoAccept = true; @@ -181,7 +149,7 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri await insertFollowingDoc(followee, follower); - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { const content = renderActivity(renderAccept(renderFollow(follower, followee, requestId), followee)); deliver(followee, content, follower.inbox); } diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts index d85c8472bb..ad09f0e6d1 100644 --- a/src/services/following/delete.ts +++ b/src/services/following/delete.ts @@ -1,22 +1,20 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; import { publishMainStream } from '../stream'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; import renderUndo from '../../remote/activitypub/renderer/undo'; import { deliver } from '../../queue'; -import perUserFollowingChart from '../../services/chart/per-user-following'; import Logger from '../logger'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; +import { User } from '../../models/entities/user'; +import { Followings, Users, Instances } from '../../models'; +import { instanceChart, perUserFollowingChart } from '../chart'; const logger = new Logger('following/delete'); -export default async function(follower: IUser, followee: IUser, silent = false) { - const following = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id +export default async function(follower: User, followee: User, silent = false) { + const following = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id }); if (following == null) { @@ -24,45 +22,25 @@ export default async function(follower: IUser, followee: IUser, silent = false) return; } - Following.remove({ - _id: following._id - }); + Followings.delete(following.id); //#region Decrement following count - User.update({ _id: follower._id }, { - $inc: { - followingCount: -1 - } - }); + Users.decrement({ id: follower.id }, 'followingCount', 1); //#endregion //#region Decrement followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: -1 - } - }); + Users.decrement({ id: followee.id }, 'followersCount', 1); //#endregion //#region Update instance stats - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { registerOrFetchInstanceDoc(follower.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followingCount: -1 - } - }); - + Instances.decrement({ id: i.id }, 'followingCount', 1); instanceChart.updateFollowing(i.host, false); }); - } else if (isLocalUser(follower) && isRemoteUser(followee)) { + } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { registerOrFetchInstanceDoc(followee.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followersCount: -1 - } - }); - + Instances.decrement({ id: i.id }, 'followersCount', 1); instanceChart.updateFollowers(i.host, false); }); } @@ -71,13 +49,13 @@ export default async function(follower: IUser, followee: IUser, silent = false) perUserFollowingChart.update(follower, followee, false); // Publish unfollow event - if (!silent && isLocalUser(follower)) { - packUser(followee, follower, { + if (!silent && Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } diff --git a/src/services/following/requests/accept-all.ts b/src/services/following/requests/accept-all.ts index cf1a9e923d..b61c31a513 100644 --- a/src/services/following/requests/accept-all.ts +++ b/src/services/following/requests/accept-all.ts @@ -1,24 +1,18 @@ -import User, { IUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import accept from './accept'; +import { User } from '../../../models/entities/user'; +import { FollowRequests, Users } from '../../../models'; /** * 指定したユーザー宛てのフォローリクエストをすべて承認 * @param user ユーザー */ -export default async function(user: IUser) { - const requests = await FollowRequest.find({ - followeeId: user._id +export default async function(user: User) { + const requests = await FollowRequests.find({ + followeeId: user.id }); for (const request of requests) { - const follower = await User.findOne({ _id: request.followerId }); + const follower = await Users.findOne(request.followerId); accept(user, follower); } - - User.update({ _id: user._id }, { - $set: { - pendingReceivedFollowRequestsCount: 0 - } - }); } diff --git a/src/services/following/requests/accept.ts b/src/services/following/requests/accept.ts index 284c6d5e19..0be8e24e1a 100644 --- a/src/services/following/requests/accept.ts +++ b/src/services/following/requests/accept.ts @@ -1,26 +1,26 @@ -import { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderAccept from '../../../remote/activitypub/renderer/accept'; import { deliver } from '../../../queue'; import { publishMainStream } from '../../stream'; import { insertFollowingDoc } from '../create'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { FollowRequests, Users } from '../../../models'; -export default async function(followee: IUser, follower: IUser) { - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id +export default async function(followee: User, follower: User) { + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); await insertFollowingDoc(followee, follower); - if (isRemoteUser(follower) && request) { + if (Users.isRemoteUser(follower) && request) { const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId), followee as ILocalUser)); deliver(followee as ILocalUser, content, follower.inbox); } - packUser(followee, followee, { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); } diff --git a/src/services/following/requests/cancel.ts b/src/services/following/requests/cancel.ts index af4cca85fe..98fec5d331 100644 --- a/src/services/following/requests/cancel.ts +++ b/src/services/following/requests/cancel.ts @@ -1,39 +1,33 @@ -import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderUndo from '../../../remote/activitypub/renderer/undo'; import { deliver } from '../../../queue'; import { publishMainStream } from '../../stream'; import { IdentifiableError } from '../../../misc/identifiable-error'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { Users, FollowRequests } from '../../../models'; -export default async function(followee: IUser, follower: IUser) { - if (isRemoteUser(followee)) { +export default async function(followee: User, follower: User) { + if (Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower as ILocalUser, content, followee.inbox); } - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); if (request == null) { throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); } - await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } - }); - - packUser(followee, followee, { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); } diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts index 10c534f529..32e79d136d 100644 --- a/src/services/following/requests/create.ts +++ b/src/services/following/requests/create.ts @@ -1,66 +1,59 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../../models/user'; import { publishMainStream } from '../../stream'; -import notify from '../../../services/create-notification'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import { deliver } from '../../../queue'; -import FollowRequest from '../../../models/follow-request'; -import Blocking from '../../../models/blocking'; +import { User } from '../../../models/entities/user'; +import { Blockings, FollowRequests, Users } from '../../../models'; +import { genId } from '../../../misc/gen-id'; +import { createNotification } from '../../create-notification'; + +export default async function(follower: User, followee: User, requestId?: string) { + if (follower.id === followee.id) return; -export default async function(follower: IUser, followee: IUser, requestId?: string) { // check blocking const [blocking, blocked] = await Promise.all([ - Blocking.findOne({ - blockerId: follower._id, - blockeeId: followee._id, + Blockings.findOne({ + blockerId: follower.id, + blockeeId: followee.id, }), - Blocking.findOne({ - blockerId: followee._id, - blockeeId: follower._id, + Blockings.findOne({ + blockerId: followee.id, + blockeeId: follower.id, }) ]); if (blocking != null) throw new Error('blocking'); if (blocked != null) throw new Error('blocked'); - await FollowRequest.insert({ + await FollowRequests.save({ + id: genId(), createdAt: new Date(), - followerId: follower._id, - followeeId: followee._id, + followerId: follower.id, + followeeId: followee.id, requestId, // 非正規化 - _follower: { - host: follower.host, - inbox: isRemoteUser(follower) ? follower.inbox : undefined, - sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined - }, - _followee: { - host: followee.host, - inbox: isRemoteUser(followee) ? followee.inbox : undefined, - sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined - } - }); - - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: 1 - } + followerHost: follower.host, + followerInbox: Users.isRemoteUser(follower) ? follower.inbox : undefined, + followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : undefined, + followeeHost: followee.host, + followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : undefined, + followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : undefined }); // Publish receiveRequest event - if (isLocalUser(followee)) { - packUser(follower, followee).then(packed => publishMainStream(followee._id, 'receiveFollowRequest', packed)); + if (Users.isLocalUser(followee)) { + Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'receiveFollowRequest', packed)); - packUser(followee, followee, { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); // 通知を作成 - notify(followee._id, follower._id, 'receiveFollowRequest'); + createNotification(followee.id, follower.id, 'receiveFollowRequest'); } - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderFollow(follower, followee)); deliver(follower, content, followee.inbox); } diff --git a/src/services/following/requests/reject.ts b/src/services/following/requests/reject.ts index cb924df811..c590edcfd8 100644 --- a/src/services/following/requests/reject.ts +++ b/src/services/following/requests/reject.ts @@ -1,34 +1,28 @@ -import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderReject from '../../../remote/activitypub/renderer/reject'; import { deliver } from '../../../queue'; import { publishMainStream } from '../../stream'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { Users, FollowRequests } from '../../../models'; -export default async function(followee: IUser, follower: IUser) { - if (isRemoteUser(follower)) { - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id +export default async function(followee: User, follower: User) { + if (Users.isRemoteUser(follower)) { + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee as ILocalUser)); deliver(followee as ILocalUser, content, follower.inbox); } - await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } - }); - - packUser(followee, follower, { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts index 4d0ae3c149..4e43421bdc 100644 --- a/src/services/i/pin.ts +++ b/src/services/i/pin.ts @@ -1,59 +1,51 @@ import config from '../../config'; -import * as mongo from 'mongodb'; -import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user'; -import Note, { packMany } from '../../models/note'; -import Following from '../../models/following'; import renderAdd from '../../remote/activitypub/renderer/add'; import renderRemove from '../../remote/activitypub/renderer/remove'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; import { IdentifiableError } from '../../misc/identifiable-error'; +import { User, ILocalUser } from '../../models/entities/user'; +import { Note } from '../../models/entities/note'; +import { Notes, UserNotePinings, Users, Followings } from '../../models'; +import { UserNotePining } from '../../models/entities/user-note-pinings'; +import { genId } from '../../misc/gen-id'; /** * 指定した投稿をピン留めします * @param user * @param noteId */ -export async function addPinned(user: IUser, noteId: mongo.ObjectID) { +export async function addPinned(user: User, noteId: Note['id']) { // Fetch pinee - const note = await Note.findOne({ - _id: noteId, - userId: user._id + const note = await Notes.findOne({ + id: noteId, + userId: user.id }); - if (note === null) { + if (note == null) { throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.'); } - let pinnedNoteIds = user.pinnedNoteIds || []; + const pinings = await UserNotePinings.find({ userId: user.id }); - //#region 現在ピン留め投稿している投稿が実際にデータベースに存在しているのかチェック - // データベースの欠損などで存在していない(または破損している)場合があるので。 - // 存在していなかったらピン留め投稿から外す - const pinnedNotes = await packMany(pinnedNoteIds, null, { detail: true }); - - pinnedNoteIds = pinnedNoteIds.filter(id => pinnedNotes.some(n => n.id.toString() === id.toHexString())); - //#endregion - - if (pinnedNoteIds.length >= 5) { + if (pinings.length >= 5) { throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.'); } - if (pinnedNoteIds.some(id => id.equals(note._id))) { + if (pinings.some(pining => pining.noteId === note.id)) { throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.'); } - pinnedNoteIds.unshift(note._id); - - await User.update(user._id, { - $set: { - pinnedNoteIds: pinnedNoteIds - } - }); + await UserNotePinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + noteId: note.id + } as UserNotePining); // Deliver to remote followers - if (isLocalUser(user)) { - deliverPinnedChange(user._id, note._id, true); + if (Users.isLocalUser(user)) { + deliverPinnedChange(user.id, note.id, true); } } @@ -62,43 +54,40 @@ export async function addPinned(user: IUser, noteId: mongo.ObjectID) { * @param user * @param noteId */ -export async function removePinned(user: IUser, noteId: mongo.ObjectID) { +export async function removePinned(user: User, noteId: Note['id']) { // Fetch unpinee - const note = await Note.findOne({ - _id: noteId, - userId: user._id + const note = await Notes.findOne({ + id: noteId, + userId: user.id }); - if (note === null) { + if (note == null) { throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.'); } - const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id)); - - await User.update(user._id, { - $set: { - pinnedNoteIds: pinnedNoteIds - } + UserNotePinings.delete({ + userId: user.id, + noteId: note.id }); // Deliver to remote followers - if (isLocalUser(user)) { - deliverPinnedChange(user._id, noteId, false); + if (Users.isLocalUser(user)) { + deliverPinnedChange(user.id, noteId, false); } } -export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) { - const user = await User.findOne({ - _id: userId +export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) { + const user = await Users.findOne({ + id: userId }); - if (!isLocalUser(user)) return; + if (!Users.isLocalUser(user)) return; const queue = await CreateRemoteInboxes(user); if (queue.length < 1) return; - const target = `${config.url}/users/${user._id}/collections/featured`; + const target = `${config.url}/users/${user.id}/collections/featured`; const item = `${config.url}/notes/${noteId}`; const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item)); @@ -112,16 +101,20 @@ export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo. * @param user ローカルユーザー */ async function CreateRemoteInboxes(user: ILocalUser): Promise<string[]> { - const followers = await Following.find({ - followeeId: user._id + const followers = await Followings.find({ + followeeId: user.id }); const queue: string[] = []; for (const following of followers) { - const follower = following._follower; + const follower = { + host: following.followerHost, + inbox: following.followerInbox, + sharedInbox: following.followerSharedInbox, + }; - if (isRemoteUser(follower)) { + if (follower.host !== null) { const inbox = follower.sharedInbox || follower.inbox; if (!queue.includes(inbox)) queue.push(inbox); } diff --git a/src/services/i/update.ts b/src/services/i/update.ts index 887cecb04c..7dba472e78 100644 --- a/src/services/i/update.ts +++ b/src/services/i/update.ts @@ -1,29 +1,26 @@ -import * as mongo from 'mongodb'; -import User, { isLocalUser, isRemoteUser } from '../../models/user'; -import Following from '../../models/following'; -import renderPerson from '../../remote/activitypub/renderer/person'; import renderUpdate from '../../remote/activitypub/renderer/update'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; +import { Followings, Users } from '../../models'; +import { User } from '../../models/entities/user'; +import { renderPerson } from '../../remote/activitypub/renderer/person'; -export async function publishToFollowers(userId: mongo.ObjectID) { - const user = await User.findOne({ - _id: userId +export async function publishToFollowers(userId: User['id']) { + const user = await Users.findOne({ + id: userId }); - const followers = await Following.find({ - followeeId: user._id + const followers = await Followings.find({ + followeeId: user.id }); const queue: string[] = []; // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { for (const following of followers) { - const follower = following._follower; - - if (isRemoteUser(follower)) { - const inbox = follower.sharedInbox || follower.inbox; + if (following.followerHost !== null) { + const inbox = following.followerSharedInbox || following.followerInbox; if (!queue.includes(inbox)) queue.push(inbox); } } diff --git a/src/services/logger.ts b/src/services/logger.ts index aa93954bc1..e6a54e626d 100644 --- a/src/services/logger.ts +++ b/src/services/logger.ts @@ -3,7 +3,9 @@ import * as os from 'os'; import chalk from 'chalk'; import * as dateformat from 'dateformat'; import { program } from '../argv'; -import Log from '../models/log'; +import { getRepository } from 'typeorm'; +import { Log } from '../models/entities/log'; +import { genId } from '../misc/gen-id'; type Domain = { name: string; @@ -33,7 +35,6 @@ export default class Logger { private log(level: Level, message: string, data: Record<string, any>, important = false, subDomains: Domain[] = [], store = true): void { if (program.quiet) return; - if (process.env.NODE_ENV === 'test') return; if (!this.store) store = false; if (this.parentLogger) { @@ -65,15 +66,17 @@ export default class Logger { console.log(important ? chalk.bold(log) : log); if (store) { - Log.insert({ + const Logs = getRepository(Log); + Logs.insert({ + id: genId(), createdAt: new Date(), machine: os.hostname(), - worker: worker, + worker: worker.toString(), domain: [this.domain].concat(subDomains).map(d => d.name), level: level, message: message, data: data, - }); + } as Log); } } diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 85201086d4..9ac9223d3c 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -1,61 +1,54 @@ import es from '../../db/elasticsearch'; -import Note, { pack, INote, IChoice } from '../../models/note'; -import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user'; -import { publishMainStream, publishHomeTimelineStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../stream'; -import Following from '../../models/following'; +import { publishMainStream, publishNotesStream } from '../stream'; import { deliver } from '../../queue'; import renderNote from '../../remote/activitypub/renderer/note'; import renderCreate from '../../remote/activitypub/renderer/create'; import renderAnnounce from '../../remote/activitypub/renderer/announce'; import { renderActivity } from '../../remote/activitypub/renderer'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; -import notify from '../../services/create-notification'; -import NoteWatching from '../../models/note-watching'; import watch from './watch'; -import Mute from '../../models/mute'; import { parse } from '../../mfm/parse'; -import { IApp } from '../../models/app'; -import UserList from '../../models/user-list'; import resolveUser from '../../remote/resolve-user'; -import Meta from '../../models/meta'; import config from '../../config'; import { updateHashtag } from '../update-hashtag'; -import isQuote from '../../misc/is-quote'; -import notesChart from '../../services/chart/notes'; -import perUserNotesChart from '../../services/chart/per-user-notes'; -import activeUsersChart from '../../services/chart/active-users'; -import instanceChart from '../../services/chart/instance'; -import * as deepcopy from 'deepcopy'; - import { erase, concat } from '../../prelude/array'; import insertNoteUnread from './unread'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; import extractMentions from '../../misc/extract-mentions'; import extractEmojis from '../../misc/extract-emojis'; import extractHashtags from '../../misc/extract-hashtags'; +import { Note } from '../../models/entities/note'; +import { Mutings, Users, NoteWatchings, Followings, Notes, Instances, Polls } from '../../models'; +import { DriveFile } from '../../models/entities/drive-file'; +import { App } from '../../models/entities/app'; +import { Not } from 'typeorm'; +import { User, ILocalUser, IRemoteUser } from '../../models/entities/user'; +import { genId } from '../../misc/gen-id'; +import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '../chart'; +import { Poll, IPoll } from '../../models/entities/poll'; +import { createNotification } from '../create-notification'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; class NotificationManager { - private notifier: IUser; - private note: INote; + private notifier: User; + private note: Note; private queue: { - target: ILocalUser['_id']; + target: ILocalUser['id']; reason: NotificationType; }[]; - constructor(notifier: IUser, note: INote) { + constructor(notifier: User, note: Note) { this.notifier = notifier; this.note = note; this.queue = []; } - public push(notifiee: ILocalUser['_id'], reason: NotificationType) { + public push(notifiee: ILocalUser['id'], reason: NotificationType) { // 自分自身へは通知しない - if (this.notifier._id.equals(notifiee)) return; + if (this.notifier.id === notifiee) return; - const exist = this.queue.find(x => x.target.equals(notifiee)); + const exist = this.queue.find(x => x.target === notifiee); if (exist) { // 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする @@ -73,16 +66,16 @@ class NotificationManager { public async deliver() { for (const x of this.queue) { // ミュート情報を取得 - const mentioneeMutes = await Mute.find({ + const mentioneeMutes = await Mutings.find({ muterId: x.target }); - const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId); // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する - if (!mentioneesMutedUserIds.includes(this.notifier._id.toString())) { - notify(x.target, this.notifier._id, x.reason, { - noteId: this.note._id + if (!mentioneesMutedUserIds.includes(this.notifier.id)) { + createNotification(x.target, this.notifier.id, x.reason, { + noteId: this.note.id }); } } @@ -93,25 +86,25 @@ type Option = { createdAt?: Date; name?: string; text?: string; - reply?: INote; - renote?: INote; - files?: IDriveFile[]; + reply?: Note; + renote?: Note; + files?: DriveFile[]; geo?: any; - poll?: any; + poll?: IPoll; viaMobile?: boolean; localOnly?: boolean; cw?: string; visibility?: string; - visibleUsers?: IUser[]; - apMentions?: IUser[]; + visibleUsers?: User[]; + apMentions?: User[]; apHashtags?: string[]; apEmojis?: string[]; questionUri?: string; uri?: string; - app?: IApp; + app?: App; }; -export default async (user: IUser, data: Option, silent = false) => new Promise<INote>(async (res, rej) => { +export default async (user: User, data: Option, silent = false) => new Promise<Note>(async (res, rej) => { const isFirstNote = user.notesCount === 0; if (data.createdAt == null) data.createdAt = new Date(); @@ -128,16 +121,6 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< data.visibleUsers = erase(null, data.visibleUsers); } - // リプライ対象が削除された投稿だったらreject - if (data.reply && data.reply.deletedAt != null) { - return rej('Reply target has been deleted'); - } - - // Renote対象が削除された投稿だったらreject - if (data.renote && data.renote.deletedAt != null) { - return rej('Renote target has been deleted'); - } - // Renote対象が「ホームまたは全体」以外の公開範囲ならreject if (data.renote && data.renote.visibility != 'public' && data.renote.visibility != 'home') { return rej('Renote target is not public or home'); @@ -176,7 +159,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const tokens = data.text ? parse(data.text) : []; const cwTokens = data.cw ? parse(data.cw) : []; const choiceTokens = data.poll && data.poll.choices - ? concat((data.poll.choices as IChoice[]).map(choice => parse(choice.text))) + ? concat(data.poll.choices.map(choice => parse(choice))) : []; const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); @@ -188,24 +171,21 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens); } - // MongoDBのインデックス対象は128文字以上にできない tags = tags.filter(tag => tag.length <= 100); - if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) { - mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); + if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply.userId)) { + mentionedUsers.push(await Users.findOne(data.reply.userId)); } if (data.visibility == 'specified') { for (const u of data.visibleUsers) { - if (!mentionedUsers.some(x => x._id.equals(u._id))) { + if (!mentionedUsers.some(x => x.id === u.id)) { mentionedUsers.push(u); } } - for (const u of mentionedUsers) { - if (!data.visibleUsers.some(x => x._id.equals(u._id))) { - data.visibleUsers.push(u); - } + if (data.reply && !data.visibleUsers.some(x => x.id === data.reply.userId)) { + data.visibleUsers.push(await Users.findOne(data.reply.userId)); } } @@ -221,17 +201,12 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< notesChart.update(note, true); perUserNotesChart.update(user, note, true); // ローカルユーザーのチャートはタイムライン取得時に更新しているのでリモートユーザーの場合だけでよい - if (isRemoteUser(user)) activeUsersChart.update(user); + if (Users.isRemoteUser(user)) activeUsersChart.update(user); // Register host - if (isRemoteUser(user)) { + if (Users.isRemoteUser(user)) { registerOrFetchInstanceDoc(user.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - notesCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'notesCount', 1); instanceChart.updateNote(i.host, true); }); } @@ -239,20 +214,6 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< // ハッシュタグ更新 for (const tag of tags) updateHashtag(user, tag); - // ファイルが添付されていた場合ドライブのファイルの「このファイルが添付された投稿一覧」プロパティにこの投稿を追加 - if (data.files) { - for (const file of data.files) { - DriveFile.update({ _id: file._id }, { - $push: { - 'metadata.attachedNoteIds': note._id - } - }); - } - } - - // Increment notes count - incNotesCount(user); - // Increment notes count (user) incNotesCountOfUser(user); @@ -275,20 +236,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< incRenoteCount(data.renote); } - if (isQuote(note)) { - saveQuote(data.renote, note); - } - // Pack the note - const noteObj = await pack(note); + const noteObj = await Notes.pack(note); if (isFirstNote) { noteObj.isFirstNote = true; } - if (tags.length > 0) { - publishHashtagStream(noteObj); - } + publishNotesStream(noteObj); const nm = new NotificationManager(user, note); const nmRelatedPromises = []; @@ -297,7 +252,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const noteActivity = await renderNoteOrRenoteActivity(data, note); - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity); } @@ -307,12 +262,12 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm)); // この投稿をWatchする - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, data.reply); + if (Users.isLocalUser(user) && user.autoWatch !== false) { + watch(user.id, data.reply); } // 通知 - if (isLocalUser(data.reply._user)) { + if (data.reply.userHost === null) { nm.push(data.reply.userId, 'reply'); publishMainStream(data.reply.userId, 'reply', noteObj); } @@ -323,7 +278,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const type = data.text ? 'quote' : 'renote'; // Notify - if (isLocalUser(data.renote._user)) { + if (data.renote.userHost === null) { nm.push(data.renote.userId, type); } @@ -331,18 +286,18 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type)); // この投稿をWatchする - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, data.renote); + if (Users.isLocalUser(user) && user.autoWatch !== false) { + watch(user.id, data.renote); } // Publish event - if (!user._id.equals(data.renote.userId) && isLocalUser(data.renote._user)) { + if ((user.id !== data.renote.userId) && data.renote.userHost === null) { publishMainStream(data.renote.userId, 'renote', noteObj); } } if (!silent) { - publish(user, note, noteObj, data.reply, data.renote, data.visibleUsers, noteActivity); + publish(user, note, data.reply, data.renote, noteActivity); } Promise.all(nmRelatedPromises).then(() => { @@ -353,245 +308,166 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< index(note); }); -async function renderNoteOrRenoteActivity(data: Option, note: INote) { +async function renderNoteOrRenoteActivity(data: Option, note: Note) { if (data.localOnly) return null; const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0) - ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note) + ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note) : renderCreate(await renderNote(note, false), note); return renderActivity(content); } -function incRenoteCount(renote: INote) { - Note.update({ _id: renote._id }, { - $inc: { - renoteCount: 1, - score: 1 - } - }); +function incRenoteCount(renote: Note) { + Notes.increment({ id: renote.id }, 'renoteCount', 1); + Notes.increment({ id: renote.id }, 'score', 1); } -async function publish(user: IUser, note: INote, noteObj: any, reply: INote, renote: INote, visibleUsers: IUser[], noteActivity: any) { - if (isLocalUser(user)) { +async function publish(user: User, note: Note, reply: Note, renote: Note, noteActivity: any) { + if (Users.isLocalUser(user)) { // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 - if (reply && isRemoteUser(reply._user)) { - deliver(user, noteActivity, reply._user.inbox); + if (reply && reply.userHost !== null) { + deliver(user, noteActivity, reply.userInbox); } // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 - if (renote && isRemoteUser(renote._user)) { - deliver(user, noteActivity, renote._user.inbox); + if (renote && renote.userHost !== null) { + deliver(user, noteActivity, renote.userInbox); } - - if (['followers', 'specified'].includes(note.visibility)) { - const detailPackedNote = await pack(note, user, { - detail: true - }); - // Publish event to myself's stream - publishHomeTimelineStream(note.userId, detailPackedNote); - publishHybridTimelineStream(note.userId, detailPackedNote); - - if (note.visibility == 'specified') { - for (const u of visibleUsers) { - if (!u._id.equals(user._id)) { - publishHomeTimelineStream(u._id, detailPackedNote); - publishHybridTimelineStream(u._id, detailPackedNote); - } - } - } - } else { - // Publish event to myself's stream - publishHomeTimelineStream(note.userId, noteObj); - - // Publish note to local and hybrid timeline stream - if (note.visibility != 'home') { - publishLocalTimelineStream(noteObj); - } - - if (note.visibility == 'public') { - publishHybridTimelineStream(null, noteObj); - } else { - // Publish event to myself's stream - publishHybridTimelineStream(note.userId, noteObj); - } - } - } - - // Publish note to global timeline stream - if (note.visibility == 'public' && note.replyId == null) { - publishGlobalTimelineStream(noteObj); } if (['public', 'home', 'followers'].includes(note.visibility)) { // フォロワーに配信 - publishToFollowers(note, user, noteActivity); + publishToFollowers(note, user, noteActivity, reply); } - - // リストに配信 - publishToUserLists(note, noteObj); } -async function insertNote(user: IUser, data: Option, tags: string[], emojis: string[], mentionedUsers: IUser[]) { - const insert: any = { +async function insertNote(user: User, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) { + const insert: Partial<Note> = { + id: genId(data.createdAt), createdAt: data.createdAt, - fileIds: data.files ? data.files.map(file => file._id) : [], - replyId: data.reply ? data.reply._id : null, - renoteId: data.renote ? data.renote._id : null, + fileIds: data.files ? data.files.map(file => file.id) : [], + replyId: data.reply ? data.reply.id : null, + renoteId: data.renote ? data.renote.id : null, name: data.name, text: data.text, - poll: data.poll, + hasPoll: data.poll != null, cw: data.cw == null ? null : data.cw, - tags, - tagsLower: tags.map(tag => tag.toLowerCase()), + tags: tags.map(tag => tag.toLowerCase()), emojis, - userId: user._id, + userId: user.id, viaMobile: data.viaMobile, localOnly: data.localOnly, geo: data.geo || null, - appId: data.app ? data.app._id : null, - visibility: data.visibility, + appId: data.app ? data.app.id : null, + visibility: data.visibility as any, visibleUserIds: data.visibility == 'specified' ? data.visibleUsers - ? data.visibleUsers.map(u => u._id) + ? data.visibleUsers.map(u => u.id) : [] : [], + attachedFileTypes: data.files ? data.files.map(file => file.type) : [], + // 以下非正規化データ - _reply: data.reply ? { - userId: data.reply.userId, - user: { - host: data.reply._user.host - } - } : null, - _renote: data.renote ? { - userId: data.renote.userId, - user: { - host: data.renote._user.host - } - } : null, - _user: { - host: user.host, - inbox: isRemoteUser(user) ? user.inbox : undefined - }, - _files: data.files ? data.files : [] + replyUserId: data.reply ? data.reply.userId : null, + replyUserHost: data.reply ? data.reply.userHost : null, + renoteUserId: data.renote ? data.renote.userId : null, + renoteUserHost: data.renote ? data.renote.userHost : null, + userHost: user.host, + userInbox: user.inbox, }; if (data.uri != null) insert.uri = data.uri; // Append mentions data if (mentionedUsers.length > 0) { - insert.mentions = mentionedUsers.map(u => u._id); - insert.mentionedRemoteUsers = mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({ + insert.mentions = mentionedUsers.map(u => u.id); + insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => ({ uri: (u as IRemoteUser).uri, username: u.username, host: u.host - })); + }))); } // 投稿を作成 try { - return await Note.insert(insert); + const note = await Notes.save(insert); + + if (note.hasPoll) { + await Polls.save({ + id: genId(), + noteId: note.id, + choices: data.poll.choices, + expiresAt: data.poll.expiresAt, + multiple: data.poll.multiple, + votes: new Array(data.poll.choices.length).fill(0), + noteVisibility: note.visibility, + userId: user.id, + userHost: user.host + } as Poll); + } + + return note; } catch (e) { // duplicate key error - if (e.code === 11000) { + if (isDuplicateKeyValueError(e)) { return null; } + console.error(e); + throw 'something happened'; } } -function index(note: INote) { +function index(note: Note) { if (note.text == null || config.elasticsearch == null) return; es.index({ index: 'misskey', type: 'note', - id: note._id.toString(), + id: note.id.toString(), body: { text: note.text } }); } -async function notifyToWatchersOfRenotee(renote: INote, user: IUser, nm: NotificationManager, type: NotificationType) { - const watchers = await NoteWatching.find({ - noteId: renote._id, - userId: { $ne: user._id } - }, { - fields: { - userId: true - } - }); +async function notifyToWatchersOfRenotee(renote: Note, user: User, nm: NotificationManager, type: NotificationType) { + const watchers = await NoteWatchings.find({ + noteId: renote.id, + userId: Not(user.id) + }); for (const watcher of watchers) { nm.push(watcher.userId, type); } } -async function notifyToWatchersOfReplyee(reply: INote, user: IUser, nm: NotificationManager) { - const watchers = await NoteWatching.find({ - noteId: reply._id, - userId: { $ne: user._id } - }, { - fields: { - userId: true - } - }); +async function notifyToWatchersOfReplyee(reply: Note, user: User, nm: NotificationManager) { + const watchers = await NoteWatchings.find({ + noteId: reply.id, + userId: Not(user.id) + }); for (const watcher of watchers) { nm.push(watcher.userId, 'reply'); } } -async function publishToUserLists(note: INote, noteObj: any) { - const lists = await UserList.find({ - userIds: note.userId - }); - - for (const list of lists) { - if (note.visibility == 'specified') { - if (note.visibleUserIds.some(id => id.equals(list.userId))) { - publishUserListStream(list._id, 'note', noteObj); - } - } else { - publishUserListStream(list._id, 'note', noteObj); - } - } -} - -async function publishToFollowers(note: INote, user: IUser, noteActivity: any) { - const detailPackedNote = await pack(note, null, { - detail: true, - skipHide: true - }); - - const followers = await Following.find({ - followeeId: note.userId, - followerId: { $ne: note.userId } // バグでフォロワーに自分がいることがあるため +async function publishToFollowers(note: Note, user: User, noteActivity: any, reply: Note) { + const followers = await Followings.find({ + followeeId: note.userId }); const queue: string[] = []; for (const following of followers) { - const follower = following._follower; - - if (isLocalUser(follower)) { - // この投稿が返信ならスキップ - if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) - continue; - - // Publish event to followers stream - publishHomeTimelineStream(following.followerId, detailPackedNote); - - if (isRemoteUser(user) || note.visibility != 'public') { - publishHybridTimelineStream(following.followerId, detailPackedNote); - } - } else { + if (following.followerHost !== null) { // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 - if (isLocalUser(user)) { - const inbox = follower.sharedInbox || follower.inbox; + if (Users.isLocalUser(user)) { + const inbox = following.followerSharedInbox || following.followerInbox; if (!queue.includes(inbox)) queue.push(inbox); } } @@ -600,104 +476,52 @@ async function publishToFollowers(note: INote, user: IUser, noteActivity: any) { for (const inbox of queue) { deliver(user as any, noteActivity, inbox); } - - // 後方互換製のため、Questionは時間差でNoteでも送る - // Questionに対応してないインスタンスは、2つめのNoteだけを採用する - // Questionに対応しているインスタンスは、同IDで採番されている2つめのNoteを無視する - setTimeout(() => { - if (noteActivity.object.type === 'Question') { - const asNote = deepcopy(noteActivity); - - asNote.object.type = 'Note'; - asNote.object.content = asNote.object._misskey_fallback_content; - - for (const inbox of queue) { - deliver(user as any, asNote, inbox); - } - } - }, 10 * 1000); } -function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocalUser, noteActivity: any) { - for (const u of mentionedUsers.filter(u => isRemoteUser(u))) { +function deliverNoteToMentionedRemoteUsers(mentionedUsers: User[], user: ILocalUser, noteActivity: any) { + for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) { deliver(user, noteActivity, (u as IRemoteUser).inbox); } } -async function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) { - for (const u of mentionedUsers.filter(u => isLocalUser(u))) { - const detailPackedNote = await pack(note, u, { +async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager) { + for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) { + const detailPackedNote = await Notes.pack(note, u, { detail: true }); - publishMainStream(u._id, 'mention', detailPackedNote); + publishMainStream(u.id, 'mention', detailPackedNote); // Create notification - nm.push(u._id, 'mention'); + nm.push(u.id, 'mention'); } } -function saveQuote(renote: INote, note: INote) { - Note.update({ _id: renote._id }, { - $push: { - _quoteIds: note._id - } - }); -} - -function saveReply(reply: INote, note: INote) { - Note.update({ _id: reply._id }, { - $inc: { - repliesCount: 1 - } - }); +function saveReply(reply: Note, note: Note) { + Notes.increment({ id: reply.id }, 'repliesCount', 1); } -function incNotesCountOfUser(user: IUser) { - User.update({ _id: user._id }, { - $set: { - updatedAt: new Date() - }, - $inc: { - notesCount: 1 - } +function incNotesCountOfUser(user: User) { + Users.increment({ id: user.id }, 'notesCount', 1); + Users.update({ id: user.id }, { + updatedAt: new Date() }); } -function incNotesCount(user: IUser) { - if (isLocalUser(user)) { - Meta.update({}, { - $inc: { - 'stats.notesCount': 1, - 'stats.originalNotesCount': 1 - } - }, { upsert: true }); - } else { - Meta.update({}, { - $inc: { - 'stats.notesCount': 1 - } - }, { upsert: true }); - } -} - -async function extractMentionedUsers(user: IUser, tokens: ReturnType<typeof parse>): Promise<IUser[]> { +async function extractMentionedUsers(user: User, tokens: ReturnType<typeof parse>): Promise<User[]> { if (tokens == null) return []; const mentions = extractMentions(tokens); - let mentionedUsers = - erase(null, await Promise.all(mentions.map(async m => { - try { - return await resolveUser(m.username, m.host ? m.host : user.host); - } catch (e) { - return null; - } - }))); + let mentionedUsers = await Promise.all(mentions.map(m => + resolveUser(m.username, m.host || user.host).catch(() => null) + )); + + mentionedUsers = mentionedUsers.filter(x => x != null); // Drop duplicate users mentionedUsers = mentionedUsers.filter((u, i, self) => - i === self.findIndex(u2 => u._id.equals(u2._id)) + i === self.findIndex(u2 => u.id === u2.id) ); return mentionedUsers; diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts index d71c97b2ca..7f04d12cd5 100644 --- a/src/services/note/delete.ts +++ b/src/services/note/delete.ts @@ -1,99 +1,50 @@ -import Note, { INote } from '../../models/note'; -import { IUser, isLocalUser, isRemoteUser } from '../../models/user'; import { publishNoteStream } from '../stream'; import renderDelete from '../../remote/activitypub/renderer/delete'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; -import Following from '../../models/following'; import renderTombstone from '../../remote/activitypub/renderer/tombstone'; -import notesChart from '../../services/chart/notes'; -import perUserNotesChart from '../../services/chart/per-user-notes'; import config from '../../config'; -import NoteUnread from '../../models/note-unread'; -import read from './read'; -import DriveFile from '../../models/drive-file'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; -import Favorite from '../../models/favorite'; +import { User } from '../../models/entities/user'; +import { Note } from '../../models/entities/note'; +import { Notes, Users, Followings, Instances } from '../../models'; +import { Not } from 'typeorm'; +import { notesChart, perUserNotesChart, instanceChart } from '../chart'; /** * 投稿を削除します。 * @param user 投稿者 * @param note 投稿 */ -export default async function(user: IUser, note: INote, quiet = false) { +export default async function(user: User, note: Note, quiet = false) { const deletedAt = new Date(); - await Note.update({ - _id: note._id, - userId: user._id - }, { - $set: { - deletedAt: deletedAt, - text: null, - tags: [], - fileIds: [], - renoteId: null, - poll: null, - geo: null, - cw: null - } + await Notes.delete({ + id: note.id, + userId: user.id }); if (note.renoteId) { - Note.update({ _id: note.renoteId }, { - $inc: { - renoteCount: -1, - score: -1 - }, - $pull: { - _quoteIds: note._id - } - }); - } - - // この投稿が関わる未読通知を削除 - NoteUnread.find({ - noteId: note._id - }).then(unreads => { - for (const unread of unreads) { - read(unread.userId, unread.noteId); - } - }); - - // この投稿をお気に入りから削除 - Favorite.remove({ - noteId: note._id - }); - - // ファイルが添付されていた場合ドライブのファイルの「このファイルが添付された投稿一覧」プロパティからこの投稿を削除 - if (note.fileIds) { - for (const fileId of note.fileIds) { - DriveFile.update({ _id: fileId }, { - $pull: { - 'metadata.attachedNoteIds': note._id - } - }); - } + Notes.decrement({ id: note.renoteId }, 'renoteCount', 1); + Notes.decrement({ id: note.renoteId }, 'score', 1); } if (!quiet) { - publishNoteStream(note._id, 'deleted', { + publishNoteStream(note.id, 'deleted', { deletedAt: deletedAt }); //#region ローカルの投稿なら削除アクティビティを配送 - if (isLocalUser(user)) { - const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user)); + if (Users.isLocalUser(user)) { + const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user)); - const followings = await Following.find({ - followeeId: user._id, - '_follower.host': { $ne: null } + const followings = await Followings.find({ + followeeId: user.id, + followerHost: Not(null) }); for (const following of followings) { - deliver(user, content, following._follower.inbox); + deliver(user, content, following.followerInbox); } } //#endregion @@ -102,14 +53,9 @@ export default async function(user: IUser, note: INote, quiet = false) { notesChart.update(note, false); perUserNotesChart.update(user, note, false); - if (isRemoteUser(user)) { + if (Users.isRemoteUser(user)) { registerOrFetchInstanceDoc(user.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - notesCount: -1 - } - }); - + Instances.decrement({ id: i.id }, 'notesCount', 1); instanceChart.updateNote(i.host, false); }); } diff --git a/src/services/note/polls/update.ts b/src/services/note/polls/update.ts index d4e183889d..ff8e8d59ef 100644 --- a/src/services/note/polls/update.ts +++ b/src/services/note/polls/update.ts @@ -1,51 +1,48 @@ -import * as mongo from 'mongodb'; -import Note, { INote } from '../../../models/note'; import { updateQuestion } from '../../../remote/activitypub/models/question'; import ms = require('ms'); import Logger from '../../logger'; -import User, { isLocalUser, isRemoteUser } from '../../../models/user'; -import Following from '../../../models/following'; import renderUpdate from '../../../remote/activitypub/renderer/update'; import { renderActivity } from '../../../remote/activitypub/renderer'; import { deliver } from '../../../queue'; import renderNote from '../../../remote/activitypub/renderer/note'; +import { Users, Notes, Followings } from '../../../models'; +import { Note } from '../../../models/entities/note'; const logger = new Logger('pollsUpdate'); -export async function triggerUpdate(note: INote) { +export async function triggerUpdate(note: Note) { if (!note.updatedAt || Date.now() - new Date(note.updatedAt).getTime() > ms('1min')) { - logger.info(`Updating ${note._id}`); + logger.info(`Updating ${note.id}`); try { const updated = await updateQuestion(note.uri); - logger.info(`Updated ${note._id} ${updated ? 'changed' : 'nochange'}`); + logger.info(`Updated ${note.id} ${updated ? 'changed' : 'nochange'}`); } catch (e) { logger.error(e); } } } -export async function deliverQuestionUpdate(noteId: mongo.ObjectID) { - const note = await Note.findOne({ - _id: noteId, - }); +export async function deliverQuestionUpdate(noteId: Note['id']) { + const note = await Notes.findOne(noteId); - const user = await User.findOne({ - _id: note.userId - }); + const user = await Users.findOne(note.userId); - const followers = await Following.find({ - followeeId: user._id + const followers = await Followings.find({ + followeeId: user.id }); const queue: string[] = []; // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { for (const following of followers) { - const follower = following._follower; + const follower = { + inbox: following.followerInbox, + sharedInbox: following.followerSharedInbox + }; - if (isRemoteUser(follower)) { + if (following.followerHost !== null) { const inbox = follower.sharedInbox || follower.inbox; if (!queue.includes(inbox)) queue.push(inbox); } diff --git a/src/services/note/polls/vote.ts b/src/services/note/polls/vote.ts index a23cdc1cb4..15f1ddffbc 100644 --- a/src/services/note/polls/vote.ts +++ b/src/services/note/polls/vote.ts @@ -1,79 +1,74 @@ -import Vote from '../../../models/poll-vote'; -import Note, { INote } from '../../../models/note'; -import Watching from '../../../models/note-watching'; import watch from '../../../services/note/watch'; import { publishNoteStream } from '../../stream'; -import notify from '../../../services/create-notification'; -import { isLocalUser, IUser } from '../../../models/user'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { PollVotes, Users, NoteWatchings, Polls } from '../../../models'; +import { Not } from 'typeorm'; +import { genId } from '../../../misc/gen-id'; +import { createNotification } from '../../create-notification'; -export default (user: IUser, note: INote, choice: number) => new Promise(async (res, rej) => { - if (!note.poll.choices.some(x => x.id == choice)) return rej('invalid choice param'); +export default (user: User, note: Note, choice: number) => new Promise(async (res, rej) => { + const poll = await Polls.findOne({ noteId: note.id }); + + // Check whether is valid choice + if (poll.choices[choice] == null) return rej('invalid choice param'); // if already voted - const exist = await Vote.find({ - noteId: note._id, - userId: user._id + const exist = await PollVotes.find({ + noteId: note.id, + userId: user.id }); - if (note.poll.multiple) { - if (exist.some(x => x.choice === choice)) + if (poll.multiple) { + if (exist.some(x => x.choice === choice)) { return rej('already voted'); - } else if (exist.length) { + } + } else if (exist.length !== 0) { return rej('already voted'); } // Create vote - await Vote.insert({ + await PollVotes.save({ + id: genId(), createdAt: new Date(), - noteId: note._id, - userId: user._id, + noteId: note.id, + userId: user.id, choice: choice }); res(); - const inc: any = {}; - inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == choice)}.votes`] = 1; - // Increment votes count - await Note.update({ _id: note._id }, { - $inc: inc - }); + const index = choice + 1; // In SQL, array index is 1 based + await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE id = '${poll.id}'`); - publishNoteStream(note._id, 'pollVoted', { + publishNoteStream(note.id, 'pollVoted', { choice: choice, - userId: user._id.toHexString() + userId: user.id }); // Notify - notify(note.userId, user._id, 'poll_vote', { - noteId: note._id, + createNotification(note.userId, user.id, 'pollVote', { + noteId: note.id, choice: choice }); // Fetch watchers - Watching - .find({ - noteId: note._id, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - for (const watcher of watchers) { - notify(watcher.userId, user._id, 'poll_vote', { - noteId: note._id, - choice: choice - }); - } - }); + NoteWatchings.find({ + noteId: note.id, + userId: Not(user.id), + }) + .then(watchers => { + for (const watcher of watchers) { + createNotification(watcher.userId, user.id, 'pollVote', { + noteId: note.id, + choice: choice + }); + } + }); // ローカルユーザーが投票した場合この投稿をWatchする - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, note); + if (Users.isLocalUser(user) && user.autoWatch) { + watch(user.id, note); } }); diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 4fdaf92ac6..437b213ded 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -1,21 +1,24 @@ -import { IUser, isLocalUser, isRemoteUser } from '../../../models/user'; -import Note, { INote } from '../../../models/note'; -import NoteReaction from '../../../models/note-reaction'; import { publishNoteStream } from '../../stream'; -import notify from '../../create-notification'; -import NoteWatching from '../../../models/note-watching'; import watch from '../watch'; import renderLike from '../../../remote/activitypub/renderer/like'; import { deliver } from '../../../queue'; import { renderActivity } from '../../../remote/activitypub/renderer'; -import perUserReactionsChart from '../../../services/chart/per-user-reactions'; import { IdentifiableError } from '../../../misc/identifiable-error'; import { toDbReaction } from '../../../misc/reaction-lib'; import fetchMeta from '../../../misc/fetch-meta'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { NoteReactions, Users, NoteWatchings, Notes } from '../../../models'; +import { Not } from 'typeorm'; +import { perUserReactionsChart } from '../../chart'; +import { genId } from '../../../misc/gen-id'; +import { NoteReaction } from '../../../models/entities/note-reaction'; +import { createNotification } from '../../create-notification'; +import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; -export default async (user: IUser, note: INote, reaction: string) => { +export default async (user: User, note: Note, reaction: string) => { // Myself - if (note.userId.equals(user._id)) { + if (note.userId === user.id) { throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note'); } @@ -23,14 +26,15 @@ export default async (user: IUser, note: INote, reaction: string) => { reaction = await toDbReaction(reaction, meta.enableEmojiReaction); // Create reaction - await NoteReaction.insert({ + await NoteReactions.save({ + id: genId(), createdAt: new Date(), - noteId: note._id, - userId: user._id, + noteId: note.id, + userId: user.id, reaction - }).catch(e => { + } as NoteReaction).catch(e => { // duplicate key error - if (e.code === 11000) { + if (isDuplicateKeyValueError(e)) { throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298', 'already reacted'); } @@ -38,59 +42,53 @@ export default async (user: IUser, note: INote, reaction: string) => { }); // Increment reactions count - await Note.update({ _id: note._id }, { - $inc: { - [`reactionCounts.${reaction}`]: 1, - score: 1 - } - }); + const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; + await Notes.createQueryBuilder().update() + .set({ + reactions: () => sql, + }) + .where('id = :id', { id: note.id }) + .execute(); + // v11 inc score perUserReactionsChart.update(user, note); - publishNoteStream(note._id, 'reacted', { + publishNoteStream(note.id, 'reacted', { reaction: reaction, - userId: user._id + userId: user.id }); // リアクションされたユーザーがローカルユーザーなら通知を作成 - if (isLocalUser(note._user)) { - notify(note.userId, user._id, 'reaction', { - noteId: note._id, + if (note.userHost === null) { + createNotification(note.userId, user.id, 'reaction', { + noteId: note.id, reaction: reaction }); } // Fetch watchers - NoteWatching - .find({ - noteId: note._id, - userId: { $ne: user._id } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - for (const watcher of watchers) { - notify(watcher.userId, user._id, 'reaction', { - noteId: note._id, - reaction: reaction - }); - } - }); + NoteWatchings.find({ + noteId: note.id, + userId: Not(user.id) + }).then(watchers => { + for (const watcher of watchers) { + createNotification(watcher.userId, user.id, 'reaction', { + noteId: note.id, + reaction: reaction + }); + } + }); // ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, note); + if (Users.isLocalUser(user) && user.autoWatch !== false) { + watch(user.id, note); } //#region 配信 // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 - if (isLocalUser(user) && isRemoteUser(note._user)) { + if (Users.isLocalUser(user) && note.userHost !== null) { const content = renderActivity(renderLike(user, note, reaction)); - deliver(user, content, note._user.inbox); + deliver(user, content, note.userInbox); } //#endregion - - return; }; diff --git a/src/services/note/reaction/delete.ts b/src/services/note/reaction/delete.ts index 695534db61..ce180aaeca 100644 --- a/src/services/note/reaction/delete.ts +++ b/src/services/note/reaction/delete.ts @@ -1,50 +1,47 @@ -import { IUser, isLocalUser, isRemoteUser } from '../../../models/user'; -import Note, { INote } from '../../../models/note'; -import NoteReaction from '../../../models/note-reaction'; import { publishNoteStream } from '../../stream'; import renderLike from '../../../remote/activitypub/renderer/like'; import renderUndo from '../../../remote/activitypub/renderer/undo'; import { renderActivity } from '../../../remote/activitypub/renderer'; import { deliver } from '../../../queue'; import { IdentifiableError } from '../../../misc/identifiable-error'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { NoteReactions, Users, Notes } from '../../../models'; -export default async (user: IUser, note: INote) => { +export default async (user: User, note: Note) => { // if already unreacted - const exist = await NoteReaction.findOne({ - noteId: note._id, - userId: user._id, - deletedAt: { $exists: false } + const exist = await NoteReactions.findOne({ + noteId: note.id, + userId: user.id, }); - if (exist === null) { + if (exist == null) { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } // Delete reaction - await NoteReaction.remove({ - _id: exist._id - }); - - const dec: any = {}; - dec[`reactionCounts.${exist.reaction}`] = -1; + await NoteReactions.delete(exist.id); // Decrement reactions count - Note.update({ _id: note._id }, { - $inc: dec - }); + const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; + await Notes.createQueryBuilder().update() + .set({ + reactions: () => sql, + }) + .where('id = :id', { id: note.id }) + .execute(); + // v11 dec score - publishNoteStream(note._id, 'unreacted', { + publishNoteStream(note.id, 'unreacted', { reaction: exist.reaction, - userId: user._id + userId: user.id }); //#region 配信 // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 - if (isLocalUser(user) && isRemoteUser(note._user)) { + if (Users.isLocalUser(user) && (note.userHost !== null)) { const content = renderActivity(renderUndo(renderLike(user, note, exist.reaction), user)); - deliver(user, content, note._user.inbox); + deliver(user, content, note.userInbox); } //#endregion - - return; }; diff --git a/src/services/note/read.ts b/src/services/note/read.ts index 8b52445cf0..44d75bd850 100644 --- a/src/services/note/read.ts +++ b/src/services/note/read.ts @@ -1,59 +1,35 @@ -import * as mongo from 'mongodb'; -import isObjectId from '../../misc/is-objectid'; import { publishMainStream } from '../stream'; -import User from '../../models/user'; -import NoteUnread from '../../models/note-unread'; +import { Note } from '../../models/entities/note'; +import { User } from '../../models/entities/user'; +import { NoteUnreads } from '../../models'; /** * Mark a note as read */ export default ( - user: string | mongo.ObjectID, - note: string | mongo.ObjectID + userId: User['id'], + noteId: Note['id'] ) => new Promise<any>(async (resolve, reject) => { - - const userId: mongo.ObjectID = isObjectId(user) - ? user as mongo.ObjectID - : new mongo.ObjectID(user); - - const noteId: mongo.ObjectID = isObjectId(note) - ? note as mongo.ObjectID - : new mongo.ObjectID(note); - // Remove document - const res = await NoteUnread.remove({ + const res = await NoteUnreads.delete({ userId: userId, noteId: noteId }); - if (res.deletedCount == 0) { + // v11 TODO: https://github.com/typeorm/typeorm/issues/2415 + if (res.affected == 0) { return; } - const count1 = await NoteUnread - .count({ - userId: userId, - isSpecified: false - }, { - limit: 1 - }); - - const count2 = await NoteUnread - .count({ - userId: userId, - isSpecified: true - }, { - limit: 1 - }); + const count1 = await NoteUnreads.count({ + userId: userId, + isSpecified: false + }); - if (count1 == 0 || count2 == 0) { - User.update({ _id: userId }, { - $set: { - hasUnreadMentions: count1 != 0 || count2 != 0, - hasUnreadSpecifiedNotes: count2 != 0 - } - }); - } + const count2 = await NoteUnreads.count({ + userId: userId, + isSpecified: true + }); if (count1 == 0) { // 全て既読になったイベントを発行 diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts index e70c63c765..203cff8d39 100644 --- a/src/services/note/unread.ts +++ b/src/services/note/unread.ts @@ -1,47 +1,34 @@ -import NoteUnread from '../../models/note-unread'; -import User, { IUser } from '../../models/user'; -import { INote } from '../../models/note'; -import Mute from '../../models/mute'; +import { Note } from '../../models/entities/note'; import { publishMainStream } from '../stream'; +import { User } from '../../models/entities/user'; +import { Mutings, NoteUnreads } from '../../models'; +import { genId } from '../../misc/gen-id'; -export default async function(user: IUser, note: INote, isSpecified = false) { +export default async function(user: User, note: Note, isSpecified = false) { //#region ミュートしているなら無視 - const mute = await Mute.find({ - muterId: user._id + const mute = await Mutings.find({ + muterId: user.id }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); - if (mutedUserIds.includes(note.userId.toString())) return; + if (mute.map(m => m.muteeId).includes(note.userId)) return; //#endregion - const unread = await NoteUnread.insert({ - noteId: note._id, - userId: user._id, + const unread = await NoteUnreads.save({ + id: genId(), + noteId: note.id, + userId: user.id, isSpecified, - _note: { - userId: note.userId - } + noteUserId: note.userId }); // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する setTimeout(async () => { - const exist = await NoteUnread.findOne({ _id: unread._id }); + const exist = await NoteUnreads.findOne(unread.id); if (exist == null) return; - User.update({ - _id: user._id - }, { - $set: isSpecified ? { - hasUnreadSpecifiedNotes: true, - hasUnreadMentions: true - } : { - hasUnreadMentions: true - } - }); - - publishMainStream(user._id, 'unreadMention', note._id); + publishMainStream(user.id, 'unreadMention', note.id); if (isSpecified) { - publishMainStream(user._id, 'unreadSpecifiedNote', note._id); + publishMainStream(user.id, 'unreadSpecifiedNote', note.id); } }, 2000); } diff --git a/src/services/note/unwatch.ts b/src/services/note/unwatch.ts index ef5783231b..047ac343be 100644 --- a/src/services/note/unwatch.ts +++ b/src/services/note/unwatch.ts @@ -1,9 +1,10 @@ -import * as mongodb from 'mongodb'; -import Watching from '../../models/note-watching'; +import { User } from '../../models/entities/user'; +import { NoteWatchings } from '../../models'; +import { Note } from '../../models/entities/note'; -export default async (me: mongodb.ObjectID, note: object) => { - await Watching.remove({ - noteId: (note as any)._id, +export default async (me: User['id'], note: Note) => { + await NoteWatchings.delete({ + noteId: note.id, userId: me }); }; diff --git a/src/services/note/watch.ts b/src/services/note/watch.ts index aad53610d8..d3c9553696 100644 --- a/src/services/note/watch.ts +++ b/src/services/note/watch.ts @@ -1,25 +1,20 @@ -import * as mongodb from 'mongodb'; -import Watching from '../../models/note-watching'; +import { User } from '../../models/entities/user'; +import { Note } from '../../models/entities/note'; +import { NoteWatchings } from '../../models'; +import { genId } from '../../misc/gen-id'; +import { NoteWatching } from '../../models/entities/note-watching'; -export default async (me: mongodb.ObjectID, note: object) => { +export default async (me: User['id'], note: Note) => { // 自分の投稿はwatchできない - if (me.equals((note as any).userId)) { + if (me === note.userId) { return; } - // if watching now - const exist = await Watching.findOne({ - noteId: (note as any)._id, - userId: me - }); - - if (exist !== null) { - return; - } - - await Watching.insert({ + await NoteWatchings.save({ + id: genId(), createdAt: new Date(), - noteId: (note as any)._id, - userId: me - }); + noteId: note.id, + userId: me, + noteUserId: note.userId + } as NoteWatching); }; diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts index ceb762b2fa..defd4d6e2d 100644 --- a/src/services/push-notification.ts +++ b/src/services/push-notification.ts @@ -1,11 +1,10 @@ import * as push from 'web-push'; -import * as mongo from 'mongodb'; -import Subscription from '../models/sw-subscription'; import config from '../config'; +import { SwSubscriptions } from '../models'; +import { Meta } from '../models/entities/meta'; import fetchMeta from '../misc/fetch-meta'; -import { IMeta } from '../models/meta'; -let meta: IMeta = null; +let meta: Meta = null; setInterval(() => { fetchMeta().then(m => { @@ -20,15 +19,11 @@ setInterval(() => { }); }, 3000); -export default async function(userId: mongo.ObjectID | string, type: string, body?: any) { +export default async function(userId: string, type: string, body?: any) { if (!meta.enableServiceWorker) return; - if (typeof userId === 'string') { - userId = new mongo.ObjectID(userId); - } - // Fetch - const subscriptions = await Subscription.find({ + const subscriptions = await SwSubscriptions.find({ userId: userId }); @@ -49,7 +44,7 @@ export default async function(userId: mongo.ObjectID | string, type: string, bod //swLogger.info(err.body); if (err.statusCode == 410) { - Subscription.remove({ + SwSubscriptions.delete({ userId: userId, endpoint: subscription.endpoint, auth: subscription.auth, diff --git a/src/services/register-or-fetch-instance-doc.ts b/src/services/register-or-fetch-instance-doc.ts index d418cd12ce..c96c8a1e32 100644 --- a/src/services/register-or-fetch-instance-doc.ts +++ b/src/services/register-or-fetch-instance-doc.ts @@ -1,15 +1,19 @@ -import Instance, { IInstance } from '../models/instance'; -import federationChart from '../services/chart/federation'; +import { Instance } from '../models/entities/instance'; +import { Instances } from '../models'; +import { federationChart } from './chart'; +import { genId } from '../misc/gen-id'; -export async function registerOrFetchInstanceDoc(host: string): Promise<IInstance> { +export async function registerOrFetchInstanceDoc(host: string): Promise<Instance> { if (host == null) return null; - const index = await Instance.findOne({ host }); + const index = await Instances.findOne({ host }); if (index == null) { - const i = await Instance.insert({ + const i = await Instances.save({ + id: genId(), host, caughtAt: new Date(), + lastCommunicatedAt: new Date(), system: null // TODO }); diff --git a/src/services/stream.ts b/src/services/stream.ts index 813c9eb7c0..c1d14b2779 100644 --- a/src/services/stream.ts +++ b/src/services/stream.ts @@ -1,8 +1,9 @@ -import * as mongo from 'mongodb'; import redis from '../db/redis'; import Xev from 'xev'; - -type ID = string | mongo.ObjectID; +import { User } from '../models/entities/user'; +import { Note } from '../models/entities/note'; +import { UserList } from '../models/entities/user-list'; +import { ReversiGame } from '../models/entities/games/reversi/game'; class Publisher { private ev: Xev; @@ -29,66 +30,50 @@ class Publisher { } } - public publishMainStream = (userId: ID, type: string, value?: any): void => { + public publishMainStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishDriveStream = (userId: ID, type: string, value?: any): void => { + public publishDriveStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishNoteStream = (noteId: ID, type: string, value: any): void => { + public publishNoteStream = (noteId: Note['id'], type: string, value: any): void => { this.publish(`noteStream:${noteId}`, type, { id: noteId, body: value }); } - public publishUserListStream = (listId: ID, type: string, value?: any): void => { + public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => { this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); } - public publishMessagingStream = (userId: ID, otherpartyId: ID, type: string, value?: any): void => { + public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => { this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); } - public publishMessagingIndexStream = (userId: ID, type: string, value?: any): void => { + public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishReversiStream = (userId: ID, type: string, value?: any): void => { + public publishReversiStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishReversiGameStream = (gameId: ID, type: string, value?: any): void => { + public publishReversiGameStream = (gameId: ReversiGame['id'], type: string, value?: any): void => { this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value); } - public publishHomeTimelineStream = (userId: ID, note: any): void => { - this.publish(`homeTimeline:${userId}`, null, note); - } - - public publishLocalTimelineStream = async (note: any): Promise<void> => { - this.publish('localTimeline', null, note); - } - - public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => { - this.publish(userId ? `hybridTimeline:${userId}` : 'hybridTimeline', null, note); - } - - public publishGlobalTimelineStream = (note: any): void => { - this.publish('globalTimeline', null, note); - } - - public publishHashtagStream = (note: any): void => { - this.publish('hashtag', null, note); + public publishNotesStream = (note: any): void => { + this.publish('notesStream', null, note); } public publishApLogStream = (log: any): void => { this.publish('apLog', null, log); } - public publishAdminStream = (userId: ID, type: string, value?: any): void => { + public publishAdminStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } } @@ -100,15 +85,11 @@ export default publisher; export const publishMainStream = publisher.publishMainStream; export const publishDriveStream = publisher.publishDriveStream; export const publishNoteStream = publisher.publishNoteStream; +export const publishNotesStream = publisher.publishNotesStream; export const publishUserListStream = publisher.publishUserListStream; export const publishMessagingStream = publisher.publishMessagingStream; export const publishMessagingIndexStream = publisher.publishMessagingIndexStream; export const publishReversiStream = publisher.publishReversiStream; export const publishReversiGameStream = publisher.publishReversiGameStream; -export const publishHomeTimelineStream = publisher.publishHomeTimelineStream; -export const publishLocalTimelineStream = publisher.publishLocalTimelineStream; -export const publishHybridTimelineStream = publisher.publishHybridTimelineStream; -export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream; -export const publishHashtagStream = publisher.publishHashtagStream; export const publishApLogStream = publisher.publishApLogStream; export const publishAdminStream = publisher.publishAdminStream; diff --git a/src/services/update-hashtag.ts b/src/services/update-hashtag.ts index 23c39312c0..6f6d5c4691 100644 --- a/src/services/update-hashtag.ts +++ b/src/services/update-hashtag.ts @@ -1,103 +1,104 @@ -import { IUser, isLocalUser, isRemoteUser } from '../models/user'; -import Hashtag from '../models/hashtag'; -import hashtagChart from './chart/hashtag'; +import { User } from '../models/entities/user'; +import { Hashtags, Users } from '../models'; +import { hashtagChart } from './chart'; +import { genId } from '../misc/gen-id'; +import { Hashtag } from '../models/entities/hashtag'; -export async function updateHashtag(user: IUser, tag: string, isUserAttached = false, inc = true) { +export async function updateHashtag(user: User, tag: string, isUserAttached = false, inc = true) { tag = tag.toLowerCase(); - const index = await Hashtag.findOne({ tag }); + const index = await Hashtags.findOne({ name: tag }); if (index == null && !inc) return; if (index != null) { - const $push = {} as any; - const $pull = {} as any; - const $inc = {} as any; + const q = Hashtags.createQueryBuilder('tag').update() + .where('tag.name = :name', { name: tag }); + + const set = {} as any; if (isUserAttached) { if (inc) { // 自分が初めてこのタグを使ったなら - if (!index.attachedUserIds.some(id => id.equals(user._id))) { - $push.attachedUserIds = user._id; - $inc.attachedUsersCount = 1; + if (!index.attachedUserIds.some(id => id === user.id)) { + set.attachedUserIds = () => `array_append(tag.attachedUserIds, '${user.id}')`; + set.attachedUsersCount = () => `tag.attachedUsersCount + 1`; } // 自分が(ローカル内で)初めてこのタグを使ったなら - if (isLocalUser(user) && !index.attachedLocalUserIds.some(id => id.equals(user._id))) { - $push.attachedLocalUserIds = user._id; - $inc.attachedLocalUsersCount = 1; + if (Users.isLocalUser(user) && !index.attachedLocalUserIds.some(id => id === user.id)) { + set.attachedLocalUserIds = () => `array_append(tag.attachedLocalUserIds, '${user.id}')`; + set.attachedLocalUsersCount = () => `tag.attachedLocalUsersCount + 1`; } // 自分が(リモートで)初めてこのタグを使ったなら - if (isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id.equals(user._id))) { - $push.attachedRemoteUserIds = user._id; - $inc.attachedRemoteUsersCount = 1; + if (Users.isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id === user.id)) { + set.attachedRemoteUserIds = () => `array_append(tag.attachedRemoteUserIds, '${user.id}')`; + set.attachedRemoteUsersCount = () => `tag.attachedRemoteUsersCount + 1`; } } else { - $pull.attachedUserIds = user._id; - $inc.attachedUsersCount = -1; - if (isLocalUser(user)) { - $pull.attachedLocalUserIds = user._id; - $inc.attachedLocalUsersCount = -1; + set.attachedUserIds = () => `array_remove(tag.attachedUserIds, '${user.id}')`; + set.attachedUsersCount = () => `tag.attachedUsersCount - 1`; + if (Users.isLocalUser(user)) { + set.attachedLocalUserIds = () => `array_remove(tag.attachedLocalUserIds, '${user.id}')`; + set.attachedLocalUsersCount = () => `tag.attachedLocalUsersCount - 1`; } else { - $pull.attachedRemoteUserIds = user._id; - $inc.attachedRemoteUsersCount = -1; + set.attachedRemoteUserIds = () => `array_remove(tag.attachedRemoteUserIds, '${user.id}')`; + set.attachedRemoteUsersCount = () => `tag.attachedRemoteUsersCount - 1`; } } } else { // 自分が初めてこのタグを使ったなら - if (!index.mentionedUserIds.some(id => id.equals(user._id))) { - $push.mentionedUserIds = user._id; - $inc.mentionedUsersCount = 1; + if (!index.mentionedUserIds.some(id => id === user.id)) { + set.mentionedUserIds = () => `array_append(tag.mentionedUserIds, '${user.id}')`; + set.mentionedUsersCount = () => `tag.mentionedUsersCount + 1`; } // 自分が(ローカル内で)初めてこのタグを使ったなら - if (isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id.equals(user._id))) { - $push.mentionedLocalUserIds = user._id; - $inc.mentionedLocalUsersCount = 1; + if (Users.isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id === user.id)) { + set.mentionedLocalUserIds = () => `array_append(tag.mentionedLocalUserIds, '${user.id}')`; + set.mentionedLocalUsersCount = () => `tag.mentionedLocalUsersCount + 1`; } // 自分が(リモートで)初めてこのタグを使ったなら - if (isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id.equals(user._id))) { - $push.mentionedRemoteUserIds = user._id; - $inc.mentionedRemoteUsersCount = 1; + if (Users.isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id === user.id)) { + set.mentionedRemoteUserIds = () => `array_append(tag.mentionedRemoteUserIds, '${user.id}')`; + set.mentionedRemoteUsersCount = () => `tag.mentionedRemoteUsersCount + 1`; } } - const q = {} as any; - if (Object.keys($push).length > 0) q.$push = $push; - if (Object.keys($pull).length > 0) q.$pull = $pull; - if (Object.keys($inc).length > 0) q.$inc = $inc; - if (Object.keys(q).length > 0) Hashtag.update({ tag }, q); + q.execute(); } else { if (isUserAttached) { - Hashtag.insert({ - tag, + Hashtags.save({ + id: genId(), + name: tag, mentionedUserIds: [], mentionedUsersCount: 0, mentionedLocalUserIds: [], mentionedLocalUsersCount: 0, mentionedRemoteUserIds: [], mentionedRemoteUsersCount: 0, - attachedUserIds: [user._id], + attachedUserIds: [user.id], attachedUsersCount: 1, - attachedLocalUserIds: isLocalUser(user) ? [user._id] : [], - attachedLocalUsersCount: isLocalUser(user) ? 1 : 0, - attachedRemoteUserIds: isRemoteUser(user) ? [user._id] : [], - attachedRemoteUsersCount: isRemoteUser(user) ? 1 : 0, - }); + attachedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [], + attachedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0, + attachedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [], + attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, + } as Hashtag); } else { - Hashtag.insert({ - tag, - mentionedUserIds: [user._id], + Hashtags.save({ + id: genId(), + name: tag, + mentionedUserIds: [user.id], mentionedUsersCount: 1, - mentionedLocalUserIds: isLocalUser(user) ? [user._id] : [], - mentionedLocalUsersCount: isLocalUser(user) ? 1 : 0, - mentionedRemoteUserIds: isRemoteUser(user) ? [user._id] : [], - mentionedRemoteUsersCount: isRemoteUser(user) ? 1 : 0, + mentionedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [], + mentionedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0, + mentionedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [], + mentionedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, attachedUserIds: [], attachedUsersCount: 0, attachedLocalUserIds: [], attachedLocalUsersCount: 0, attachedRemoteUserIds: [], attachedRemoteUsersCount: 0, - }); + } as Hashtag); } } diff --git a/src/services/user-list/push.ts b/src/services/user-list/push.ts index 5ad4a14827..958d54b090 100644 --- a/src/services/user-list/push.ts +++ b/src/services/user-list/push.ts @@ -1,21 +1,26 @@ -import { pack as packUser, IUser, isRemoteUser, fetchProxyAccount } from '../../models/user'; -import UserList, { IUserList } from '../../models/user-list'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; import renderFollow from '../../remote/activitypub/renderer/follow'; import { publishUserListStream } from '../stream'; +import { User } from '../../models/entities/user'; +import { UserList } from '../../models/entities/user-list'; +import { UserListJoinings, Users } from '../../models'; +import { UserListJoining } from '../../models/entities/user-list-joining'; +import { genId } from '../../misc/gen-id'; +import { fetchProxyAccount } from '../../misc/fetch-proxy-account'; -export async function pushUserToUserList(target: IUser, list: IUserList) { - await UserList.update({ _id: list._id }, { - $push: { - userIds: target._id - } - }); +export async function pushUserToUserList(target: User, list: UserList) { + await UserListJoinings.save({ + id: genId(), + createdAt: new Date(), + userId: target.id, + userListId: list.id + } as UserListJoining); - publishUserListStream(list._id, 'userAdded', await packUser(target)); + publishUserListStream(list.id, 'userAdded', await Users.pack(target)); // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする - if (isRemoteUser(target)) { + if (Users.isRemoteUser(target)) { const proxy = await fetchProxyAccount(); const content = renderActivity(renderFollow(proxy, target)); deliver(proxy, content, target.inbox); |