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

* ✌️

* wip

* Update note.ts

* Update postgre.ts

* wip

* wip

* wip

* wip

* Update add-file.ts

* wip

* wip

* wip

* Clean up

* Update logs.ts

* wip

* 🍕

* 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

* ✌️

* clean up

* docs

* Update push.ts

* wip

* Update api.ts

* wip

* ✌️

* Update make-pagination-query.ts

* ✌️

* 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

* 🎨

* 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

* ✌️

* 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

* 🎨

* 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

* ✌️

* Refactoring

#1540

* test

* test

* test

* test

* test

* test

* test

* Fix bug

* Fix test

* 🍣

* wip

* #4471

* Add test for #4335

* Refactor

* Fix test

* Add tests

* 🕓

* Fix bug

* Add test

* Add test

* rename

* Fix bug
This commit is contained in:
syuilo 2019-04-07 21:50:36 +09:00 committed by GitHub
parent 13caf37991
commit f0a29721c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
592 changed files with 13463 additions and 14147 deletions

View file

@ -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 FollowRequests.delete({
followeeId: followee.id,
followerId: follower.id
});
await User.update({ _id: followee._id }, {
$inc: {
pendingReceivedFollowRequestsCount: -1
}
});
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);
}

View file

@ -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);
}

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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
});
}
}

View file

@ -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
});
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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
});
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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
});
}
}

View file

@ -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
});
}
}

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -0,0 +1,11 @@
export const schema = {
type: 'object' as 'object',
properties: {
foo: {
type: 'number' as 'number',
description: ''
},
}
};
export const name = 'testUnique';

View file

@ -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';

View file

@ -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';

460
src/services/chart/core.ts Normal file
View file

@ -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;
}

View file

@ -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();

View file

@ -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);
}
}));

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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;
}
const data = {
id: genId(),
createdAt: new Date(),
notifieeId: notifieeId,
notifierId: notifierId,
type: type,
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;
}
// Create notification
const notification = await Notification.insert(Object.assign({
createdAt: new Date(),
notifieeId: notifiee,
notifierId: notifier,
type: type,
isRead: false
}, content));
const notification = await Notifications.save(data);
resolve(notification);
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;
}

View file

@ -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();
const originalFile = await storeOriginal(originalDst, name, path, type, metadata);
logger.info(`original stored to ${originalFile._id}`);
// #endregion store original
// #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}`);
}
// #endregion store webpublic
let thumbnailUrl: string;
let webpublicUrl: string;
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}`);
thumbnailUrl = InternalStorage.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data);
logger.info(`thumbnail stored: ${thumbnailAccessKey}`);
}
return originalFile;
if (alts.webpublic) {
webpublicUrl = InternalStorage.saveFromBuffer(webpublicAccessKey, alts.webpublic.data);
logger.info(`web stored: ${webpublicAccessKey}`);
}
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 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;
}

View file

@ -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') {
export default async function(file: DriveFile, isExpired = false) {
if (file.storedInternal) {
InternalStorage.del(file.accessKey);
if (file.thumbnailUrl) {
InternalStorage.del(file.thumbnailAccessKey);
}
if (file.webpublicUrl) {
InternalStorage.del(file.webpublicAccessKey);
}
} else {
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);
await minio.removeObject(config.drive.bucket, 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) {
await minio.removeObject(config.drive.bucket, 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) {
await minio.removeObject(config.drive.bucket, file.webpublicAccessKey);
}
}
// チャンクをすべて削除
await DriveFileChunk.remove({
files_id: file._id
});
const set = {
metadata: {
deletedAt: new Date(),
isExpired: isExpired
}
} 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
if (isExpired && file.userHost !== null) {
DriveFiles.update(file.id, {
isRemote: true,
url: file.uri,
thumbnailUrl: null,
webpublicUrl: null
});
await DriveFileThumbnail.remove({ _id: thumbnail._id });
} else {
DriveFiles.delete(file.id);
}
//#endregion
//#region Web公開用もあれば削除
const webpublic = await DriveFileWebpublic.findOne({
'metadata.originalId': file._id
});
if (webpublic) {
await DriveFileWebpublicChunk.remove({
files_id: webpublic._id
});
await DriveFileWebpublic.remove({ _id: webpublic._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);
}
}

View file

@ -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}`, () => {});
}
}

View file

@ -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}`, {

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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
}
});
}

View file

@ -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));
}

View file

@ -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));
}

View file

@ -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);
}

View file

@ -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));
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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: Note, note: Note) {
Notes.increment({ id: reply.id }, 'repliesCount', 1);
}
function incNotesCountOfUser(user: User) {
Users.increment({ id: user.id }, 'notesCount', 1);
Users.update({ id: user.id }, {
updatedAt: new Date()
});
}
function saveReply(reply: INote, note: INote) {
Note.update({ _id: reply._id }, {
$inc: {
repliesCount: 1
}
});
}
function incNotesCountOfUser(user: IUser) {
User.update({ _id: user._id }, {
$set: {
updatedAt: new Date()
},
$inc: {
notesCount: 1
}
});
}
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;

View file

@ -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);
});
}

View file

@ -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);
}

View file

@ -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);
}
});

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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 count1 = await NoteUnreads.count({
userId: userId,
isSpecified: false
});
const count2 = await NoteUnread
.count({
userId: userId,
isSpecified: true
}, {
limit: 1
});
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) {
// 全て既読になったイベントを発行

View file

@ -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);
}

View file

@ -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
});
};

View file

@ -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);
};

View file

@ -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,

View file

@ -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
});

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);