= {
export type SchemaType =
p['type'] extends 'number' ? number :
p['type'] extends 'string' ? string :
- p['type'] extends 'array' ? MyType
[] :
- p['type'] extends 'object' ? ObjType
:
+ p['type'] extends 'array' ? MyType>[] :
+ p['type'] extends 'object' ? ObjType> :
any;
export function convertOpenApiSchema(schema: Schema) {
diff --git a/src/misc/should-mute-this-note.ts b/src/misc/should-mute-this-note.ts
index b1d29c6a28..8f606a2943 100644
--- a/src/misc/should-mute-this-note.ts
+++ b/src/misc/should-mute-this-note.ts
@@ -1,20 +1,13 @@
-import * as mongo from 'mongodb';
-import isObjectId from './is-objectid';
-
-function toString(id: any) {
- return isObjectId(id) ? (id as mongo.ObjectID).toHexString() : id;
-}
-
export default function(note: any, mutedUserIds: string[]): boolean {
- if (mutedUserIds.includes(toString(note.userId))) {
+ if (mutedUserIds.includes(note.userId)) {
return true;
}
- if (note.reply != null && mutedUserIds.includes(toString(note.reply.userId))) {
+ if (note.reply != null && mutedUserIds.includes(note.reply.userId)) {
return true;
}
- if (note.renote != null && mutedUserIds.includes(toString(note.renote.userId))) {
+ if (note.renote != null && mutedUserIds.includes(note.renote.userId)) {
return true;
}
diff --git a/src/models/abuse-user-report.ts b/src/models/abuse-user-report.ts
deleted file mode 100644
index f3900d348d..0000000000
--- a/src/models/abuse-user-report.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packUser } from './user';
-
-const AbuseUserReport = db.get('abuseUserReports');
-AbuseUserReport.createIndex('userId');
-AbuseUserReport.createIndex('reporterId');
-AbuseUserReport.createIndex(['userId', 'reporterId'], { unique: true });
-export default AbuseUserReport;
-
-export interface IAbuseUserReport {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID;
- reporterId: mongo.ObjectID;
- comment: string;
-}
-
-export const packMany = (
- reports: (string | mongo.ObjectID | IAbuseUserReport)[]
-) => {
- return Promise.all(reports.map(x => pack(x)));
-};
-
-export const pack = (
- report: any
-) => new Promise(async (resolve, reject) => {
- let _report: any;
-
- if (isObjectId(report)) {
- _report = await AbuseUserReport.findOne({
- _id: report
- });
- } else if (typeof report === 'string') {
- _report = await AbuseUserReport.findOne({
- _id: new mongo.ObjectID(report)
- });
- } else {
- _report = deepcopy(report);
- }
-
- // Rename _id to id
- _report.id = _report._id;
- delete _report._id;
-
- _report.reporter = await packUser(_report.reporterId, null, { detail: true });
- _report.user = await packUser(_report.userId, null, { detail: true });
-
- resolve(_report);
-});
diff --git a/src/models/access-token.ts b/src/models/access-token.ts
deleted file mode 100644
index 66c5c91c0b..0000000000
--- a/src/models/access-token.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const AccessToken = db.get('accessTokens');
-AccessToken.createIndex('token');
-AccessToken.createIndex('hash');
-export default AccessToken;
-
-export type IAccessToken = {
- _id: mongo.ObjectID;
- createdAt: Date;
- appId: mongo.ObjectID;
- userId: mongo.ObjectID;
- token: string;
- hash: string;
-};
diff --git a/src/models/app.ts b/src/models/app.ts
deleted file mode 100644
index 45d50bccda..0000000000
--- a/src/models/app.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import AccessToken from './access-token';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import config from '../config';
-import { dbLogger } from '../db/logger';
-
-const App = db.get('apps');
-App.createIndex('secret');
-export default App;
-
-export type IApp = {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID | null;
- secret: string;
- name: string;
- description: string;
- permission: string[];
- callbackUrl: string;
-};
-
-/**
- * Pack an app for API response
- */
-export const pack = (
- app: any,
- me?: any,
- options?: {
- detail?: boolean,
- includeSecret?: boolean,
- includeProfileImageIds?: boolean
- }
-) => new Promise(async (resolve, reject) => {
- const opts = Object.assign({
- detail: false,
- includeSecret: false,
- includeProfileImageIds: false
- }, options);
-
- let _app: any;
-
- const fields = opts.detail ? {} : {
- name: true
- };
-
- // Populate the app if 'app' is ID
- if (isObjectId(app)) {
- _app = await App.findOne({
- _id: app
- });
- } else if (typeof app === 'string') {
- _app = await App.findOne({
- _id: new mongo.ObjectID(app)
- }, { fields });
- } else {
- _app = deepcopy(app);
- }
-
- // Me
- if (me && !isObjectId(me)) {
- if (typeof me === 'string') {
- me = new mongo.ObjectID(me);
- } else {
- me = me._id;
- }
- }
-
- // (データベースの欠損などで)アプリがデータベース上に見つからなかったとき
- if (_app == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: app :: ${app}`);
- return null;
- }
-
- // Rename _id to id
- _app.id = _app._id;
- delete _app._id;
-
- // Visible by only owner
- if (!opts.includeSecret) {
- delete _app.secret;
- }
-
- _app.iconUrl = _app.icon != null
- ? `${config.driveUrl}/${_app.icon}`
- : `${config.driveUrl}/app-default.jpg`;
-
- if (me) {
- // 既に連携しているか
- const exist = await AccessToken.count({
- appId: _app.id,
- userId: me,
- }, {
- limit: 1
- });
-
- _app.isAuthorized = exist === 1;
- }
-
- resolve(_app);
-});
diff --git a/src/models/auth-session.ts b/src/models/auth-session.ts
deleted file mode 100644
index 428c707470..0000000000
--- a/src/models/auth-session.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packApp } from './app';
-
-const AuthSession = db.get('authSessions');
-export default AuthSession;
-
-export interface IAuthSession {
- _id: mongo.ObjectID;
- createdAt: Date;
- appId: mongo.ObjectID;
- userId: mongo.ObjectID;
- token: string;
-}
-
-/**
- * Pack an auth session for API response
- *
- * @param {any} session
- * @param {any} me?
- * @return {Promise}
- */
-export const pack = (
- session: any,
- me?: any
-) => new Promise(async (resolve, reject) => {
- let _session: any;
-
- // TODO: Populate session if it ID
- _session = deepcopy(session);
-
- // Me
- if (me && !isObjectId(me)) {
- if (typeof me === 'string') {
- me = new mongo.ObjectID(me);
- } else {
- me = me._id;
- }
- }
-
- delete _session._id;
-
- // Populate app
- _session.app = await packApp(_session.appId, me);
-
- resolve(_session);
-});
diff --git a/src/models/blocking.ts b/src/models/blocking.ts
deleted file mode 100644
index 4bdaa741e9..0000000000
--- a/src/models/blocking.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import * as deepcopy from 'deepcopy';
-import { pack as packUser, IUser } from './user';
-
-const Blocking = db.get('blocking');
-Blocking.createIndex('blockerId');
-Blocking.createIndex('blockeeId');
-Blocking.createIndex(['blockerId', 'blockeeId'], { unique: true });
-export default Blocking;
-
-export type IBlocking = {
- _id: mongo.ObjectID;
- createdAt: Date;
- blockeeId: mongo.ObjectID;
- blockerId: mongo.ObjectID;
-};
-
-export const packMany = (
- blockings: (string | mongo.ObjectID | IBlocking)[],
- me?: string | mongo.ObjectID | IUser
-) => {
- return Promise.all(blockings.map(x => pack(x, me)));
-};
-
-export const pack = (
- blocking: any,
- me?: any
-) => new Promise(async (resolve, reject) => {
- let _blocking: any;
-
- // Populate the blocking if 'blocking' is ID
- if (isObjectId(blocking)) {
- _blocking = await Blocking.findOne({
- _id: blocking
- });
- } else if (typeof blocking === 'string') {
- _blocking = await Blocking.findOne({
- _id: new mongo.ObjectID(blocking)
- });
- } else {
- _blocking = deepcopy(blocking);
- }
-
- // Rename _id to id
- _blocking.id = _blocking._id;
- delete _blocking._id;
-
- // Populate blockee
- _blocking.blockee = await packUser(_blocking.blockeeId, me, {
- detail: true
- });
-
- resolve(_blocking);
-});
diff --git a/src/models/drive-file-thumbnail.ts b/src/models/drive-file-thumbnail.ts
deleted file mode 100644
index bdb3d010e6..0000000000
--- a/src/models/drive-file-thumbnail.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as mongo from 'mongodb';
-import monkDb, { nativeDbConn } from '../db/mongodb';
-
-const DriveFileThumbnail = monkDb.get('driveFileThumbnails.files');
-DriveFileThumbnail.createIndex('metadata.originalId', { sparse: true, unique: true });
-export default DriveFileThumbnail;
-
-export const DriveFileThumbnailChunk = monkDb.get('driveFileThumbnails.chunks');
-
-export const getDriveFileThumbnailBucket = async (): Promise => {
- const db = await nativeDbConn();
- const bucket = new mongo.GridFSBucket(db, {
- bucketName: 'driveFileThumbnails'
- });
- return bucket;
-};
-
-export type IMetadata = {
- originalId: mongo.ObjectID;
-};
-
-export type IDriveFileThumbnail = {
- _id: mongo.ObjectID;
- uploadDate: Date;
- md5: string;
- filename: string;
- contentType: string;
- metadata: IMetadata;
-};
diff --git a/src/models/drive-file-webpublic.ts b/src/models/drive-file-webpublic.ts
deleted file mode 100644
index d087c355d3..0000000000
--- a/src/models/drive-file-webpublic.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as mongo from 'mongodb';
-import monkDb, { nativeDbConn } from '../db/mongodb';
-
-const DriveFileWebpublic = monkDb.get('driveFileWebpublics.files');
-DriveFileWebpublic.createIndex('metadata.originalId', { sparse: true, unique: true });
-export default DriveFileWebpublic;
-
-export const DriveFileWebpublicChunk = monkDb.get('driveFileWebpublics.chunks');
-
-export const getDriveFileWebpublicBucket = async (): Promise => {
- const db = await nativeDbConn();
- const bucket = new mongo.GridFSBucket(db, {
- bucketName: 'driveFileWebpublics'
- });
- return bucket;
-};
-
-export type IMetadata = {
- originalId: mongo.ObjectID;
-};
-
-export type IDriveFileWebpublic = {
- _id: mongo.ObjectID;
- uploadDate: Date;
- md5: string;
- filename: string;
- contentType: string;
- metadata: IMetadata;
-};
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
deleted file mode 100644
index c31e9a709f..0000000000
--- a/src/models/drive-file.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import { pack as packFolder } from './drive-folder';
-import { pack as packUser } from './user';
-import monkDb, { nativeDbConn } from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import getDriveFileUrl, { getOriginalUrl } from '../misc/get-drive-file-url';
-import { dbLogger } from '../db/logger';
-
-const DriveFile = monkDb.get('driveFiles.files');
-DriveFile.createIndex('md5');
-DriveFile.createIndex('metadata.uri');
-DriveFile.createIndex('metadata.userId');
-DriveFile.createIndex('metadata.folderId');
-DriveFile.createIndex('metadata._user.host');
-export default DriveFile;
-
-export const DriveFileChunk = monkDb.get('driveFiles.chunks');
-
-export const getDriveFileBucket = async (): Promise => {
- const db = await nativeDbConn();
- const bucket = new mongo.GridFSBucket(db, {
- bucketName: 'driveFiles'
- });
- return bucket;
-};
-
-export type IMetadata = {
- properties: any;
- userId: mongo.ObjectID;
- _user: any;
- folderId: mongo.ObjectID;
- comment: string;
-
- /**
- * リモートインスタンスから取得した場合の元URL
- */
- uri?: string;
-
- /**
- * URL for web(生成されている場合) or original
- * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
- */
- url?: string;
-
- /**
- * URL for thumbnail (thumbnailがなければなし)
- * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
- */
- thumbnailUrl?: string;
-
- /**
- * URL for original (web用が生成されてない場合はurlがoriginalを指す)
- * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
- */
- webpublicUrl?: string;
-
- accessKey?: string;
-
- src?: string;
- deletedAt?: Date;
-
- /**
- * このファイルの中身データがMongoDB内に保存されていないか否か
- * オブジェクトストレージを利用している or リモートサーバーへの直リンクである
- * な場合は true になります
- */
- withoutChunks?: boolean;
-
- storage?: string;
-
- /***
- * ObjectStorage の格納先の情報
- */
- storageProps?: IStorageProps;
- isSensitive?: boolean;
-
- /**
- * このファイルが添付された投稿のID一覧
- */
- attachedNoteIds?: mongo.ObjectID[];
-
- /**
- * 外部の(信頼されていない)URLへの直リンクか否か
- */
- isRemote?: boolean;
-};
-
-export type IStorageProps = {
- /**
- * ObjectStorage key for original
- */
- key: string;
-
- /***
- * ObjectStorage key for thumbnail (thumbnailがなければなし)
- */
- thumbnailKey?: string;
-
- /***
- * ObjectStorage key for webpublic (webpublicがなければなし)
- */
- webpublicKey?: string;
-
- id?: string;
-};
-
-export type IDriveFile = {
- _id: mongo.ObjectID;
- uploadDate: Date;
- md5: string;
- filename: string;
- contentType: string;
- metadata: IMetadata;
-
- /**
- * ファイルサイズ
- */
- length: number;
-};
-
-export function validateFileName(name: string): boolean {
- return (
- (name.trim().length > 0) &&
- (name.length <= 200) &&
- (name.indexOf('\\') === -1) &&
- (name.indexOf('/') === -1) &&
- (name.indexOf('..') === -1)
- );
-}
-
-export const packMany = (
- files: any[],
- options?: {
- detail?: boolean
- self?: boolean,
- withUser?: boolean,
- }
-) => {
- return Promise.all(files.map(f => pack(f, options)));
-};
-
-/**
- * Pack a drive file for API response
- */
-export const pack = (
- file: any,
- options?: {
- detail?: boolean,
- self?: boolean,
- withUser?: boolean,
- }
-) => new Promise(async (resolve, reject) => {
- const opts = Object.assign({
- detail: false,
- self: false
- }, options);
-
- let _file: any;
-
- // Populate the file if 'file' is ID
- if (isObjectId(file)) {
- _file = await DriveFile.findOne({
- _id: file
- });
- } else if (typeof file === 'string') {
- _file = await DriveFile.findOne({
- _id: new mongo.ObjectID(file)
- });
- } else {
- _file = deepcopy(file);
- }
-
- // (データベースの欠損などで)ファイルがデータベース上に見つからなかったとき
- if (_file == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: driveFile :: ${file}`);
- return resolve(null);
- }
-
- // rendered target
- let _target: any = {};
-
- _target.id = _file._id;
- _target.createdAt = _file.uploadDate;
- _target.name = _file.filename;
- _target.type = _file.contentType;
- _target.datasize = _file.length;
- _target.md5 = _file.md5;
-
- _target = Object.assign(_target, _file.metadata);
-
- _target.url = getDriveFileUrl(_file);
- _target.thumbnailUrl = getDriveFileUrl(_file, true);
- _target.isRemote = _file.metadata.isRemote;
-
- if (_target.properties == null) _target.properties = {};
-
- if (opts.detail) {
- if (_target.folderId) {
- // Populate folder
- _target.folder = await packFolder(_target.folderId, {
- detail: true
- });
- }
-
- /*
- if (_target.tags) {
- // Populate tags
- _target.tags = await _target.tags.map(async (tag: any) =>
- await serializeDriveTag(tag)
- );
- }
- */
- }
-
- if (opts.withUser) {
- // Populate user
- _target.user = await packUser(_file.metadata.userId);
- }
-
- delete _target.withoutChunks;
- delete _target.storage;
- delete _target.storageProps;
- delete _target.isRemote;
- delete _target._user;
-
- if (opts.self) {
- _target.url = getOriginalUrl(_file);
- }
-
- resolve(_target);
-});
diff --git a/src/models/drive-folder.ts b/src/models/drive-folder.ts
deleted file mode 100644
index b0f6e4273e..0000000000
--- a/src/models/drive-folder.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import DriveFile from './drive-file';
-
-const DriveFolder = db.get('driveFolders');
-DriveFolder.createIndex('userId');
-export default DriveFolder;
-
-export type IDriveFolder = {
- _id: mongo.ObjectID;
- createdAt: Date;
- name: string;
- userId: mongo.ObjectID;
- parentId: mongo.ObjectID;
-};
-
-export function isValidFolderName(name: string): boolean {
- return (
- (name.trim().length > 0) &&
- (name.length <= 200)
- );
-}
-
-/**
- * Pack a drive folder for API response
- */
-export const pack = (
- folder: any,
- options?: {
- detail: boolean
- }
-) => new Promise(async (resolve, reject) => {
- const opts = Object.assign({
- detail: false
- }, options);
-
- let _folder: any;
-
- // Populate the folder if 'folder' is ID
- if (isObjectId(folder)) {
- _folder = await DriveFolder.findOne({ _id: folder });
- } else if (typeof folder === 'string') {
- _folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) });
- } else {
- _folder = deepcopy(folder);
- }
-
- // Rename _id to id
- _folder.id = _folder._id;
- delete _folder._id;
-
- if (opts.detail) {
- const childFoldersCount = await DriveFolder.count({
- parentId: _folder.id
- });
-
- const childFilesCount = await DriveFile.count({
- 'metadata.folderId': _folder.id
- });
-
- _folder.foldersCount = childFoldersCount;
- _folder.filesCount = childFilesCount;
- }
-
- if (opts.detail && _folder.parentId) {
- // Populate parent folder
- _folder.parent = await pack(_folder.parentId, {
- detail: true
- });
- }
-
- resolve(_folder);
-});
diff --git a/src/models/emoji.ts b/src/models/emoji.ts
deleted file mode 100644
index cbf939222e..0000000000
--- a/src/models/emoji.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Emoji = db.get('emoji');
-Emoji.createIndex('name');
-Emoji.createIndex('host');
-Emoji.createIndex(['name', 'host'], { unique: true });
-
-export default Emoji;
-
-export type IEmoji = {
- _id: mongo.ObjectID;
- name: string;
- host: string;
- url: string;
- aliases?: string[];
- updatedAt?: Date;
- /** AP object id */
- uri?: string;
- type?: string;
-};
diff --git a/src/models/entities/abuse-user-report.ts b/src/models/entities/abuse-user-report.ts
new file mode 100644
index 0000000000..43ab56023a
--- /dev/null
+++ b/src/models/entities/abuse-user-report.ts
@@ -0,0 +1,41 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'reporterId'], { unique: true })
+export class AbuseUserReport {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the AbuseUserReport.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column(id())
+ public reporterId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public reporter: User | null;
+
+ @Column('varchar', {
+ length: 512,
+ })
+ public comment: string;
+}
diff --git a/src/models/entities/access-token.ts b/src/models/entities/access-token.ts
new file mode 100644
index 0000000000..d08930cf5a
--- /dev/null
+++ b/src/models/entities/access-token.ts
@@ -0,0 +1,45 @@
+import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn, RelationId } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { id } from '../id';
+
+@Entity()
+export class AccessToken {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the AccessToken.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column('varchar', {
+ length: 128
+ })
+ public token: string;
+
+ @Index()
+ @Column('varchar', {
+ length: 128
+ })
+ public hash: string;
+
+ @RelationId((self: AccessToken) => self.user)
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column(id())
+ public appId: App['id'];
+
+ @ManyToOne(type => App, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public app: App | null;
+}
diff --git a/src/models/entities/app.ts b/src/models/entities/app.ts
new file mode 100644
index 0000000000..ea87546311
--- /dev/null
+++ b/src/models/entities/app.ts
@@ -0,0 +1,60 @@
+import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class App {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the App.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The owner ID.'
+ })
+ public userId: User['id'] | null;
+
+ @ManyToOne(type => User, {
+ onDelete: 'SET NULL',
+ nullable: true,
+ })
+ public user: User | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 64,
+ comment: 'The secret key of the App.'
+ })
+ public secret: string;
+
+ @Column('varchar', {
+ length: 128,
+ comment: 'The name of the App.'
+ })
+ public name: string;
+
+ @Column('varchar', {
+ length: 512,
+ comment: 'The description of the App.'
+ })
+ public description: string;
+
+ @Column('varchar', {
+ length: 64, array: true,
+ comment: 'The permission of the App.'
+ })
+ public permission: string[];
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The callbackUrl of the App.'
+ })
+ public callbackUrl: string | null;
+}
diff --git a/src/models/entities/auth-session.ts b/src/models/entities/auth-session.ts
new file mode 100644
index 0000000000..4eec27e3f6
--- /dev/null
+++ b/src/models/entities/auth-session.ts
@@ -0,0 +1,43 @@
+import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { id } from '../id';
+
+@Entity()
+export class AuthSession {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the AuthSession.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column('varchar', {
+ length: 128
+ })
+ public token: string;
+
+ @Column({
+ ...id(),
+ nullable: true
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE',
+ nullable: true
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column(id())
+ public appId: App['id'];
+
+ @ManyToOne(type => App, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public app: App | null;
+}
diff --git a/src/models/entities/blocking.ts b/src/models/entities/blocking.ts
new file mode 100644
index 0000000000..48487cb086
--- /dev/null
+++ b/src/models/entities/blocking.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['blockerId', 'blockeeId'], { unique: true })
+export class Blocking {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Blocking.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The blockee user ID.'
+ })
+ public blockeeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public blockee: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The blocker user ID.'
+ })
+ public blockerId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public blocker: User | null;
+}
diff --git a/src/models/entities/drive-file.ts b/src/models/entities/drive-file.ts
new file mode 100644
index 0000000000..130af39ede
--- /dev/null
+++ b/src/models/entities/drive-file.ts
@@ -0,0 +1,154 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { DriveFolder } from './drive-folder';
+import { id } from '../id';
+
+@Entity()
+export class DriveFile {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the DriveFile.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The owner ID.'
+ })
+ public userId: User['id'] | null;
+
+ @ManyToOne(type => User, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The host of owner. It will be null if the user in local.'
+ })
+ public userHost: string | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 32,
+ comment: 'The MD5 hash of the DriveFile.'
+ })
+ public md5: string;
+
+ @Column('varchar', {
+ length: 256,
+ comment: 'The file name of the DriveFile.'
+ })
+ public name: string;
+
+ @Index()
+ @Column('varchar', {
+ length: 128,
+ comment: 'The content type (MIME) of the DriveFile.'
+ })
+ public type: string;
+
+ @Column('integer', {
+ comment: 'The file size (bytes) of the DriveFile.'
+ })
+ public size: number;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The comment of the DriveFile.'
+ })
+ public comment: string | null;
+
+ @Column('jsonb', {
+ default: {},
+ comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
+ })
+ public properties: Record;
+
+ @Column('boolean')
+ public storedInternal: boolean;
+
+ @Column('varchar', {
+ length: 512,
+ comment: 'The URL of the DriveFile.'
+ })
+ public url: string;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The URL of the thumbnail of the DriveFile.'
+ })
+ public thumbnailUrl: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The URL of the webpublic of the DriveFile.'
+ })
+ public webpublicUrl: string | null;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public accessKey: string | null;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public thumbnailAccessKey: string | null;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public webpublicAccessKey: string | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.'
+ })
+ public uri: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ })
+ public src: string | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The parent folder ID. If null, it means the DriveFile is located in root.'
+ })
+ public folderId: DriveFolder['id'] | null;
+
+ @ManyToOne(type => DriveFolder, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public folder: DriveFolder | null;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the DriveFile is NSFW.'
+ })
+ public isSensitive: boolean;
+
+ /**
+ * 外部の(信頼されていない)URLへの直リンクか否か
+ */
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the DriveFile is direct link to remote server.'
+ })
+ public isLink: boolean;
+}
diff --git a/src/models/entities/drive-folder.ts b/src/models/entities/drive-folder.ts
new file mode 100644
index 0000000000..a80d075855
--- /dev/null
+++ b/src/models/entities/drive-folder.ts
@@ -0,0 +1,49 @@
+import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class DriveFolder {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the DriveFolder.'
+ })
+ public createdAt: Date;
+
+ @Column('varchar', {
+ length: 128,
+ comment: 'The name of the DriveFolder.'
+ })
+ public name: string;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The owner ID.'
+ })
+ public userId: User['id'] | null;
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.'
+ })
+ public parentId: DriveFolder['id'] | null;
+
+ @ManyToOne(type => DriveFolder, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public parent: DriveFolder | null;
+}
diff --git a/src/models/entities/emoji.ts b/src/models/entities/emoji.ts
new file mode 100644
index 0000000000..020636a7fb
--- /dev/null
+++ b/src/models/entities/emoji.ts
@@ -0,0 +1,46 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+@Index(['name', 'host'], { unique: true })
+export class Emoji {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ nullable: true
+ })
+ public updatedAt: Date | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 128
+ })
+ public name: string;
+
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true
+ })
+ public host: string | null;
+
+ @Column('varchar', {
+ length: 512,
+ })
+ public url: string;
+
+ @Column('varchar', {
+ length: 512, nullable: true
+ })
+ public uri: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true
+ })
+ public type: string | null;
+
+ @Column('varchar', {
+ array: true, length: 128, default: '{}'
+ })
+ public aliases: string[];
+}
diff --git a/src/models/entities/follow-request.ts b/src/models/entities/follow-request.ts
new file mode 100644
index 0000000000..22ec263962
--- /dev/null
+++ b/src/models/entities/follow-request.ts
@@ -0,0 +1,85 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['followerId', 'followeeId'], { unique: true })
+export class FollowRequest {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the FollowRequest.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The followee user ID.'
+ })
+ public followeeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public followee: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The follower user ID.'
+ })
+ public followerId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public follower: User | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'id of Follow Activity.'
+ })
+ public requestId: string | null;
+
+ //#region Denormalized fields
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerHost: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerInbox: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerSharedInbox: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeHost: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeInbox: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeSharedInbox: string | null;
+ //#endregion
+}
diff --git a/src/models/entities/following.ts b/src/models/entities/following.ts
new file mode 100644
index 0000000000..ee3286a1a1
--- /dev/null
+++ b/src/models/entities/following.ts
@@ -0,0 +1,80 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['followerId', 'followeeId'], { unique: true })
+export class Following {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Following.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The followee user ID.'
+ })
+ public followeeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public followee: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The follower user ID.'
+ })
+ public followerId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public follower: User | null;
+
+ //#region Denormalized fields
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerHost: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerInbox: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followerSharedInbox: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeHost: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeInbox: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public followeeSharedInbox: string | null;
+ //#endregion
+}
diff --git a/src/models/entities/games/reversi/game.ts b/src/models/entities/games/reversi/game.ts
new file mode 100644
index 0000000000..9deacaf5c6
--- /dev/null
+++ b/src/models/entities/games/reversi/game.ts
@@ -0,0 +1,133 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from '../../user';
+import { id } from '../../../id';
+
+@Entity()
+export class ReversiGame {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the ReversiGame.'
+ })
+ public createdAt: Date;
+
+ @Column('timestamp with time zone', {
+ nullable: true,
+ comment: 'The started date of the ReversiGame.'
+ })
+ public startedAt: Date | null;
+
+ @Column(id())
+ public user1Id: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user1: User | null;
+
+ @Column(id())
+ public user2Id: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user2: User | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public user1Accepted: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public user2Accepted: boolean;
+
+ /**
+ * どちらのプレイヤーが先行(黒)か
+ * 1 ... user1
+ * 2 ... user2
+ */
+ @Column('integer', {
+ nullable: true,
+ })
+ public black: number | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isStarted: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isEnded: boolean;
+
+ @Column({
+ ...id(),
+ nullable: true
+ })
+ public winnerId: User['id'] | null;
+
+ @Column({
+ ...id(),
+ nullable: true
+ })
+ public surrendered: User['id'] | null;
+
+ @Column('jsonb', {
+ default: [],
+ })
+ public logs: {
+ at: Date;
+ color: boolean;
+ pos: number;
+ }[];
+
+ @Column('varchar', {
+ array: true, length: 64,
+ })
+ public map: string[];
+
+ @Column('varchar', {
+ length: 32
+ })
+ public bw: string;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isLlotheo: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public canPutEverywhere: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public loopedBoard: boolean;
+
+ @Column('jsonb', {
+ nullable: true, default: null,
+ })
+ public form1: any | null;
+
+ @Column('jsonb', {
+ nullable: true, default: null,
+ })
+ public form2: any | null;
+
+ /**
+ * ログのposを文字列としてすべて連結したもののCRC32値
+ */
+ @Column('varchar', {
+ length: 32, nullable: true
+ })
+ public crc32: string | null;
+}
diff --git a/src/models/entities/games/reversi/matching.ts b/src/models/entities/games/reversi/matching.ts
new file mode 100644
index 0000000000..477a29316e
--- /dev/null
+++ b/src/models/entities/games/reversi/matching.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from '../../user';
+import { id } from '../../../id';
+
+@Entity()
+export class ReversiMatching {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the ReversiMatching.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public parentId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public parent: User | null;
+
+ @Index()
+ @Column(id())
+ public childId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public child: User | null;
+}
diff --git a/src/models/entities/hashtag.ts b/src/models/entities/hashtag.ts
new file mode 100644
index 0000000000..842cdaa562
--- /dev/null
+++ b/src/models/entities/hashtag.ts
@@ -0,0 +1,87 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class Hashtag {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 128
+ })
+ public name: string;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public mentionedUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public mentionedUsersCount: number;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public mentionedLocalUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public mentionedLocalUsersCount: number;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public mentionedRemoteUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public mentionedRemoteUsersCount: number;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public attachedUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public attachedUsersCount: number;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public attachedLocalUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public attachedLocalUsersCount: number;
+
+ @Column({
+ ...id(),
+ array: true,
+ })
+ public attachedRemoteUserIds: User['id'][];
+
+ @Index()
+ @Column('integer', {
+ default: 0
+ })
+ public attachedRemoteUsersCount: number;
+}
diff --git a/src/models/entities/instance.ts b/src/models/entities/instance.ts
new file mode 100644
index 0000000000..977054263c
--- /dev/null
+++ b/src/models/entities/instance.ts
@@ -0,0 +1,132 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Instance {
+ @PrimaryColumn(id())
+ public id: string;
+
+ /**
+ * このインスタンスを捕捉した日時
+ */
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The caught date of the Instance.'
+ })
+ public caughtAt: Date;
+
+ /**
+ * ホスト
+ */
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 128,
+ comment: 'The host of the Instance.'
+ })
+ public host: string;
+
+ /**
+ * インスタンスのシステム (MastodonとかMisskeyとかPleromaとか)
+ */
+ @Column('varchar', {
+ length: 64, nullable: true,
+ comment: 'The system of the Instance.'
+ })
+ public system: string | null;
+
+ /**
+ * インスタンスのユーザー数
+ */
+ @Column('integer', {
+ default: 0,
+ comment: 'The count of the users of the Instance.'
+ })
+ public usersCount: number;
+
+ /**
+ * インスタンスの投稿数
+ */
+ @Column('integer', {
+ default: 0,
+ comment: 'The count of the notes of the Instance.'
+ })
+ public notesCount: number;
+
+ /**
+ * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
+ */
+ @Column('integer', {
+ default: 0,
+ })
+ public followingCount: number;
+
+ /**
+ * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
+ */
+ @Column('integer', {
+ default: 0,
+ })
+ public followersCount: number;
+
+ /**
+ * ドライブ使用量
+ */
+ @Column('integer', {
+ default: 0,
+ })
+ public driveUsage: number;
+
+ /**
+ * ドライブのファイル数
+ */
+ @Column('integer', {
+ default: 0,
+ })
+ public driveFiles: number;
+
+ /**
+ * 直近のリクエスト送信日時
+ */
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public latestRequestSentAt: Date | null;
+
+ /**
+ * 直近のリクエスト送信時のHTTPステータスコード
+ */
+ @Column('integer', {
+ nullable: true,
+ })
+ public latestStatus: number | null;
+
+ /**
+ * 直近のリクエスト受信日時
+ */
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public latestRequestReceivedAt: Date | null;
+
+ /**
+ * このインスタンスと最後にやり取りした日時
+ */
+ @Column('timestamp with time zone')
+ public lastCommunicatedAt: Date;
+
+ /**
+ * このインスタンスと不通かどうか
+ */
+ @Column('boolean', {
+ default: false
+ })
+ public isNotResponding: boolean;
+
+ /**
+ * このインスタンスが閉鎖済みとしてマークされているか
+ */
+ @Column('boolean', {
+ default: false
+ })
+ public isMarkedAsClosed: boolean;
+}
diff --git a/src/models/entities/log.ts b/src/models/entities/log.ts
new file mode 100644
index 0000000000..99e1e8947e
--- /dev/null
+++ b/src/models/entities/log.ts
@@ -0,0 +1,46 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Log {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Log.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column('varchar', {
+ length: 64, array: true, default: '{}'
+ })
+ public domain: string[];
+
+ @Index()
+ @Column('enum', {
+ enum: ['error', 'warning', 'info', 'success', 'debug']
+ })
+ public level: string;
+
+ @Column('varchar', {
+ length: 8
+ })
+ public worker: string;
+
+ @Column('varchar', {
+ length: 128
+ })
+ public machine: string;
+
+ @Column('varchar', {
+ length: 1024
+ })
+ public message: string;
+
+ @Column('jsonb', {
+ default: {}
+ })
+ public data: Record;
+}
diff --git a/src/models/entities/messaging-message.ts b/src/models/entities/messaging-message.ts
new file mode 100644
index 0000000000..d3c3eab3a2
--- /dev/null
+++ b/src/models/entities/messaging-message.ts
@@ -0,0 +1,64 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+
+@Entity()
+export class MessagingMessage {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the MessagingMessage.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The sender user ID.'
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The recipient user ID.'
+ })
+ public recipientId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public recipient: User | null;
+
+ @Column('varchar', {
+ length: 4096, nullable: true
+ })
+ public text: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isRead: boolean;
+
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public fileId: DriveFile['id'] | null;
+
+ @ManyToOne(type => DriveFile, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public file: DriveFile | null;
+}
diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts
new file mode 100644
index 0000000000..f3ac23bac7
--- /dev/null
+++ b/src/models/entities/meta.ts
@@ -0,0 +1,264 @@
+import { Entity, Column, PrimaryColumn } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Meta {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('varchar', {
+ length: 128, nullable: true
+ })
+ public name: string | null;
+
+ @Column('varchar', {
+ length: 1024, nullable: true
+ })
+ public description: string | null;
+
+ /**
+ * メンテナの名前
+ */
+ @Column('varchar', {
+ length: 128, nullable: true
+ })
+ public maintainerName: string | null;
+
+ /**
+ * メンテナの連絡先
+ */
+ @Column('varchar', {
+ length: 128, nullable: true
+ })
+ public maintainerEmail: string | null;
+
+ @Column('jsonb', {
+ default: [],
+ })
+ public announcements: Record[];
+
+ @Column('boolean', {
+ default: false,
+ })
+ public disableRegistration: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public disableLocalTimeline: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public disableGlobalTimeline: boolean;
+
+ @Column('boolean', {
+ default: true,
+ })
+ public enableEmojiReaction: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public useStarForReactionFallback: boolean;
+
+ @Column('varchar', {
+ length: 64, array: true, default: '{}'
+ })
+ public langs: string[];
+
+ @Column('varchar', {
+ length: 256, array: true, default: '{}'
+ })
+ public hiddenTags: string[];
+
+ @Column('varchar', {
+ length: 256, array: true, default: '{}'
+ })
+ public blockedHosts: string[];
+
+ @Column('varchar', {
+ length: 512,
+ nullable: true,
+ default: '/assets/ai.png'
+ })
+ public mascotImageUrl: string | null;
+
+ @Column('varchar', {
+ length: 512,
+ nullable: true
+ })
+ public bannerUrl: string | null;
+
+ @Column('varchar', {
+ length: 512,
+ nullable: true,
+ default: 'https://xn--931a.moe/aiart/yubitun.png'
+ })
+ public errorImageUrl: string | null;
+
+ @Column('varchar', {
+ length: 512,
+ nullable: true
+ })
+ public iconUrl: string | null;
+
+ @Column('boolean', {
+ default: true,
+ })
+ public cacheRemoteFiles: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public proxyAccount: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableRecaptcha: boolean;
+
+ @Column('varchar', {
+ length: 64,
+ nullable: true
+ })
+ public recaptchaSiteKey: string | null;
+
+ @Column('varchar', {
+ length: 64,
+ nullable: true
+ })
+ public recaptchaSecretKey: string | null;
+
+ @Column('integer', {
+ default: 1024,
+ comment: 'Drive capacity of a local user (MB)'
+ })
+ public localDriveCapacityMb: number;
+
+ @Column('integer', {
+ default: 32,
+ comment: 'Drive capacity of a remote user (MB)'
+ })
+ public remoteDriveCapacityMb: number;
+
+ @Column('integer', {
+ default: 500,
+ comment: 'Max allowed note text length in characters'
+ })
+ public maxNoteTextLength: number;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public summalyProxy: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableEmail: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public email: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public smtpSecure: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public smtpHost: string | null;
+
+ @Column('integer', {
+ nullable: true
+ })
+ public smtpPort: number | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public smtpUser: string | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public smtpPass: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableServiceWorker: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public swPublicKey: string | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public swPrivateKey: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableTwitterIntegration: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public twitterConsumerKey: string | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public twitterConsumerSecret: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableGithubIntegration: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public githubClientId: string | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public githubClientSecret: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public enableDiscordIntegration: boolean;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public discordClientId: string | null;
+
+ @Column('varchar', {
+ length: 128,
+ nullable: true
+ })
+ public discordClientSecret: string | null;
+}
diff --git a/src/models/entities/muting.ts b/src/models/entities/muting.ts
new file mode 100644
index 0000000000..0084213bcc
--- /dev/null
+++ b/src/models/entities/muting.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['muterId', 'muteeId'], { unique: true })
+export class Muting {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Muting.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The mutee user ID.'
+ })
+ public muteeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public mutee: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The muter user ID.'
+ })
+ public muterId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public muter: User | null;
+}
diff --git a/src/models/entities/note-favorite.ts b/src/models/entities/note-favorite.ts
new file mode 100644
index 0000000000..0713c3ae56
--- /dev/null
+++ b/src/models/entities/note-favorite.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteFavorite {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the NoteFavorite.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column(id())
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+}
diff --git a/src/models/entities/note-reaction.ts b/src/models/entities/note-reaction.ts
new file mode 100644
index 0000000000..1ce5d841fb
--- /dev/null
+++ b/src/models/entities/note-reaction.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteReaction {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the NoteReaction.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column(id())
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ @Column('varchar', {
+ length: 32
+ })
+ public reaction: string;
+}
diff --git a/src/models/entities/note-unread.ts b/src/models/entities/note-unread.ts
new file mode 100644
index 0000000000..2d18728256
--- /dev/null
+++ b/src/models/entities/note-unread.ts
@@ -0,0 +1,43 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteUnread {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column(id())
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ @Column({
+ ...id(),
+ comment: '[Denormalized]'
+ })
+ public noteUserId: User['id'];
+
+ /**
+ * ダイレクト投稿か
+ */
+ @Column('boolean')
+ public isSpecified: boolean;
+}
diff --git a/src/models/entities/note-watching.ts b/src/models/entities/note-watching.ts
new file mode 100644
index 0000000000..741a1c0c8b
--- /dev/null
+++ b/src/models/entities/note-watching.ts
@@ -0,0 +1,52 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteWatching {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the NoteWatching.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The watcher ID.'
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The target Note ID.'
+ })
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ //#region Denormalized fields
+ @Index()
+ @Column({
+ ...id(),
+ comment: '[Denormalized]'
+ })
+ public noteUserId: Note['userId'];
+ //#endregion
+}
diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts
new file mode 100644
index 0000000000..969363da3b
--- /dev/null
+++ b/src/models/entities/note.ts
@@ -0,0 +1,231 @@
+import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+
+@Entity()
+export class Note {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Note.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The ID of reply target.'
+ })
+ public replyId: Note['id'] | null;
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public reply: Note | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The ID of renote target.'
+ })
+ public renoteId: Note['id'] | null;
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public renote: Note | null;
+
+ @Column({
+ type: 'text', nullable: true
+ })
+ public text: string | null;
+
+ @Column('varchar', {
+ length: 256, nullable: true
+ })
+ public name: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true
+ })
+ public cw: string | null;
+
+ @Column({
+ ...id(),
+ nullable: true
+ })
+ public appId: App['id'] | null;
+
+ @ManyToOne(type => App, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public app: App | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The ID of author.'
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('boolean', {
+ default: false
+ })
+ public viaMobile: boolean;
+
+ @Column('boolean', {
+ default: false
+ })
+ public localOnly: boolean;
+
+ @Column('integer', {
+ default: 0
+ })
+ public renoteCount: number;
+
+ @Column('integer', {
+ default: 0
+ })
+ public repliesCount: number;
+
+ @Column('jsonb', {
+ default: {}
+ })
+ public reactions: Record;
+
+ /**
+ * public ... 公開
+ * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
+ * followers ... フォロワーのみ
+ * specified ... visibleUserIds で指定したユーザーのみ
+ */
+ @Column('enum', { enum: ['public', 'home', 'followers', 'specified'] })
+ public visibility: 'public' | 'home' | 'followers' | 'specified';
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The URI of a note. it will be null when the note is local.'
+ })
+ public uri: string | null;
+
+ @Column('integer', {
+ default: 0, select: false
+ })
+ public score: number;
+
+ @Column({
+ ...id(),
+ array: true, default: '{}'
+ })
+ public fileIds: DriveFile['id'][];
+
+ @Column('varchar', {
+ length: 256, array: true, default: '{}'
+ })
+ public attachedFileTypes: string[];
+
+ @Index()
+ @Column({
+ ...id(),
+ array: true, default: '{}'
+ })
+ public visibleUserIds: User['id'][];
+
+ @Index()
+ @Column({
+ ...id(),
+ array: true, default: '{}'
+ })
+ public mentions: User['id'][];
+
+ @Column('text', {
+ default: '[]'
+ })
+ public mentionedRemoteUsers: string;
+
+ @Column('varchar', {
+ length: 128, array: true, default: '{}'
+ })
+ public emojis: string[];
+
+ @Index()
+ @Column('varchar', {
+ length: 128, array: true, default: '{}'
+ })
+ public tags: string[];
+
+ @Column('boolean', {
+ default: false
+ })
+ public hasPoll: boolean;
+
+ @Column('jsonb', {
+ nullable: true, default: null
+ })
+ public geo: any | null;
+
+ //#region Denormalized fields
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public userHost: string | null;
+
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: '[Denormalized]'
+ })
+ public replyUserId: User['id'] | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public replyUserHost: string | null;
+
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: '[Denormalized]'
+ })
+ public renoteUserId: User['id'] | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public renoteUserHost: string | null;
+ //#endregion
+
+ constructor(data: Partial) {
+ if (data == null) return;
+
+ for (const [k, v] of Object.entries(data)) {
+ (this as any)[k] = v;
+ }
+ }
+}
+
+export type IMentionedRemoteUsers = {
+ uri: string;
+ username: string;
+ host: string;
+}[];
diff --git a/src/models/entities/notification.ts b/src/models/entities/notification.ts
new file mode 100644
index 0000000000..627a57bece
--- /dev/null
+++ b/src/models/entities/notification.ts
@@ -0,0 +1,94 @@
+import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { Note } from './note';
+
+@Entity()
+export class Notification {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Notification.'
+ })
+ public createdAt: Date;
+
+ /**
+ * 通知の受信者
+ */
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The ID of recipient user of the Notification.'
+ })
+ public notifieeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public notifiee: User | null;
+
+ /**
+ * 通知の送信者(initiator)
+ */
+ @Column({
+ ...id(),
+ comment: 'The ID of sender user of the Notification.'
+ })
+ public notifierId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public notifier: User | null;
+
+ /**
+ * 通知の種類。
+ * follow - フォローされた
+ * mention - 投稿で自分が言及された
+ * reply - (自分または自分がWatchしている)投稿が返信された
+ * renote - (自分または自分がWatchしている)投稿がRenoteされた
+ * quote - (自分または自分がWatchしている)投稿が引用Renoteされた
+ * reaction - (自分または自分がWatchしている)投稿にリアクションされた
+ * pollVote - (自分または自分がWatchしている)投稿の投票に投票された
+ */
+ @Column('varchar', {
+ length: 32,
+ comment: 'The type of the Notification.'
+ })
+ public type: string;
+
+ /**
+ * 通知が読まれたかどうか
+ */
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the Notification is read.'
+ })
+ public isRead: boolean;
+
+ @Column({
+ ...id(),
+ nullable: true
+ })
+ public noteId: Note['id'] | null;
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true
+ })
+ public reaction: string;
+
+ @Column('integer', {
+ nullable: true
+ })
+ public choice: number;
+}
diff --git a/src/models/entities/poll-vote.ts b/src/models/entities/poll-vote.ts
new file mode 100644
index 0000000000..709376f909
--- /dev/null
+++ b/src/models/entities/poll-vote.ts
@@ -0,0 +1,40 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId', 'choice'], { unique: true })
+export class PollVote {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the PollVote.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column(id())
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ @Column('integer')
+ public choice: number;
+}
diff --git a/src/models/entities/poll.ts b/src/models/entities/poll.ts
new file mode 100644
index 0000000000..6bb67163a2
--- /dev/null
+++ b/src/models/entities/poll.ts
@@ -0,0 +1,71 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { id } from '../id';
+import { Note } from './note';
+import { User } from './user';
+
+@Entity()
+export class Poll {
+ @PrimaryColumn(id())
+ public noteId: Note['id'];
+
+ @OneToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+
+ @Column('timestamp with time zone', {
+ nullable: true
+ })
+ public expiresAt: Date | null;
+
+ @Column('boolean')
+ public multiple: boolean;
+
+ @Column('varchar', {
+ length: 128, array: true, default: '{}'
+ })
+ public choices: string[];
+
+ @Column('integer', {
+ array: true,
+ })
+ public votes: number[];
+
+ //#region Denormalized fields
+ @Column('enum', {
+ enum: ['public', 'home', 'followers', 'specified'],
+ comment: '[Denormalized]'
+ })
+ public noteVisibility: 'public' | 'home' | 'followers' | 'specified';
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: '[Denormalized]'
+ })
+ public userId: User['id'];
+
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public userHost: string | null;
+ //#endregion
+
+ constructor(data: Partial) {
+ if (data == null) return;
+
+ for (const [k, v] of Object.entries(data)) {
+ (this as any)[k] = v;
+ }
+ }
+}
+
+export type IPoll = {
+ choices: string[];
+ votes?: number[];
+ multiple: boolean;
+ expiresAt: Date | null;
+};
diff --git a/src/models/entities/registration-tickets.ts b/src/models/entities/registration-tickets.ts
new file mode 100644
index 0000000000..d962f78a78
--- /dev/null
+++ b/src/models/entities/registration-tickets.ts
@@ -0,0 +1,17 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class RegistrationTicket {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone')
+ public createdAt: Date;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 64,
+ })
+ public code: string;
+}
diff --git a/src/models/entities/signin.ts b/src/models/entities/signin.ts
new file mode 100644
index 0000000000..7e047084b1
--- /dev/null
+++ b/src/models/entities/signin.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class Signin {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Signin.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('varchar', {
+ length: 128,
+ })
+ public ip: string;
+
+ @Column('jsonb')
+ public headers: Record;
+
+ @Column('boolean')
+ public success: boolean;
+}
diff --git a/src/models/entities/sw-subscription.ts b/src/models/entities/sw-subscription.ts
new file mode 100644
index 0000000000..7c3f6f0a6c
--- /dev/null
+++ b/src/models/entities/sw-subscription.ts
@@ -0,0 +1,37 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class SwSubscription {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone')
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('varchar', {
+ length: 512,
+ })
+ public endpoint: string;
+
+ @Column('varchar', {
+ length: 256,
+ })
+ public auth: string;
+
+ @Column('varchar', {
+ length: 128,
+ })
+ public publickey: string;
+}
diff --git a/src/models/entities/user-keypair.ts b/src/models/entities/user-keypair.ts
new file mode 100644
index 0000000000..603321d758
--- /dev/null
+++ b/src/models/entities/user-keypair.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserKeypair {
+ @PrimaryColumn(id())
+ public userId: User['id'];
+
+ @OneToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('varchar', {
+ length: 4096,
+ })
+ public publicKey: string;
+
+ @Column('varchar', {
+ length: 4096,
+ })
+ public privateKey: string;
+
+ constructor(data: Partial) {
+ if (data == null) return;
+
+ for (const [k, v] of Object.entries(data)) {
+ (this as any)[k] = v;
+ }
+ }
+}
diff --git a/src/models/entities/user-list-joining.ts b/src/models/entities/user-list-joining.ts
new file mode 100644
index 0000000000..8af4efb6a7
--- /dev/null
+++ b/src/models/entities/user-list-joining.ts
@@ -0,0 +1,41 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { UserList } from './user-list';
+import { id } from '../id';
+
+@Entity()
+export class UserListJoining {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the UserListJoining.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The user ID.'
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The list ID.'
+ })
+ public userListId: UserList['id'];
+
+ @ManyToOne(type => UserList, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public userList: UserList | null;
+}
diff --git a/src/models/entities/user-list.ts b/src/models/entities/user-list.ts
new file mode 100644
index 0000000000..35a83ef8c3
--- /dev/null
+++ b/src/models/entities/user-list.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserList {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the UserList.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The owner ID.'
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('varchar', {
+ length: 128,
+ comment: 'The name of the UserList.'
+ })
+ public name: string;
+}
diff --git a/src/models/entities/user-note-pinings.ts b/src/models/entities/user-note-pinings.ts
new file mode 100644
index 0000000000..04a6f8f645
--- /dev/null
+++ b/src/models/entities/user-note-pinings.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class UserNotePining {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the UserNotePinings.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column(id())
+ public noteId: Note['id'];
+
+ @ManyToOne(type => Note, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public note: Note | null;
+}
diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts
new file mode 100644
index 0000000000..a2d7b8d2c2
--- /dev/null
+++ b/src/models/entities/user-profile.ts
@@ -0,0 +1,209 @@
+import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
+import { id } from '../id';
+import { User } from './user';
+
+@Entity()
+export class UserProfile {
+ @PrimaryColumn(id())
+ public userId: User['id'];
+
+ @OneToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The location of the User.'
+ })
+ public location: string | null;
+
+ @Column('char', {
+ length: 10, nullable: true,
+ comment: 'The birthday (YYYY-MM-DD) of the User.'
+ })
+ public birthday: string | null;
+
+ @Column('varchar', {
+ length: 1024, nullable: true,
+ comment: 'The description (bio) of the User.'
+ })
+ public description: string | null;
+
+ @Column('jsonb', {
+ default: [],
+ })
+ public fields: {
+ name: string;
+ value: string;
+ }[];
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'Remote URL of the user.'
+ })
+ public url: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The email address of the User.'
+ })
+ public email: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public emailVerifyCode: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public emailVerified: boolean;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public twoFactorTempSecret: string | null;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public twoFactorSecret: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public twoFactorEnabled: boolean;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The password hash of the User. It will be null if the origin of the user is local.'
+ })
+ public password: string | null;
+
+ @Column('jsonb', {
+ default: {},
+ comment: 'The client-specific data of the User.'
+ })
+ public clientData: Record;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public autoWatch: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public autoAcceptFollowed: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public alwaysMarkNsfw: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public carefulBot: boolean;
+
+ //#region Linking
+ @Column('boolean', {
+ default: false,
+ })
+ public twitter: boolean;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public twitterAccessToken: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public twitterAccessTokenSecret: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public twitterUserId: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public twitterScreenName: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public github: boolean;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public githubAccessToken: string | null;
+
+ @Column('integer', {
+ nullable: true, default: null,
+ })
+ public githubId: number | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public githubLogin: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public discord: boolean;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public discordAccessToken: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public discordRefreshToken: string | null;
+
+ @Column('integer', {
+ nullable: true, default: null,
+ })
+ public discordExpiresDate: number | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public discordId: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public discordUsername: string | null;
+
+ @Column('varchar', {
+ length: 64, nullable: true, default: null,
+ })
+ public discordDiscriminator: string | null;
+ //#endregion
+
+ //#region Denormalized fields
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: '[Denormalized]'
+ })
+ public userHost: string | null;
+ //#endregion
+
+ constructor(data: Partial) {
+ if (data == null) return;
+
+ for (const [k, v] of Object.entries(data)) {
+ (this as any)[k] = v;
+ }
+ }
+}
diff --git a/src/models/entities/user-publickey.ts b/src/models/entities/user-publickey.ts
new file mode 100644
index 0000000000..21edc3e9e2
--- /dev/null
+++ b/src/models/entities/user-publickey.ts
@@ -0,0 +1,34 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserPublickey {
+ @PrimaryColumn(id())
+ public userId: User['id'];
+
+ @OneToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 256,
+ })
+ public keyId: string;
+
+ @Column('varchar', {
+ length: 4096,
+ })
+ public keyPem: string;
+
+ constructor(data: Partial) {
+ if (data == null) return;
+
+ for (const [k, v] of Object.entries(data)) {
+ (this as any)[k] = v;
+ }
+ }
+}
diff --git a/src/models/entities/user.ts b/src/models/entities/user.ts
new file mode 100644
index 0000000000..e40c32a76f
--- /dev/null
+++ b/src/models/entities/user.ts
@@ -0,0 +1,224 @@
+import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+
+@Entity()
+@Index(['usernameLower', 'host'], { unique: true })
+export class User {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the User.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ nullable: true,
+ comment: 'The updated date of the User.'
+ })
+ public updatedAt: Date | null;
+
+ @Column('timestamp with time zone', {
+ nullable: true
+ })
+ public lastFetchedAt: Date | null;
+
+ @Column('varchar', {
+ length: 128,
+ comment: 'The username of the User.'
+ })
+ public username: string;
+
+ @Index()
+ @Column('varchar', {
+ length: 128, select: false,
+ comment: 'The username (lowercased) of the User.'
+ })
+ public usernameLower: string;
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The name of the User.'
+ })
+ public name: string | null;
+
+ @Column('integer', {
+ default: 0,
+ comment: 'The count of followers.'
+ })
+ public followersCount: number;
+
+ @Column('integer', {
+ default: 0,
+ comment: 'The count of following.'
+ })
+ public followingCount: number;
+
+ @Column('integer', {
+ default: 0,
+ comment: 'The count of notes.'
+ })
+ public notesCount: number;
+
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The ID of avatar DriveFile.'
+ })
+ public avatarId: DriveFile['id'] | null;
+
+ @OneToOne(type => DriveFile, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public avatar: DriveFile | null;
+
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: 'The ID of banner DriveFile.'
+ })
+ public bannerId: DriveFile['id'] | null;
+
+ @OneToOne(type => DriveFile, {
+ onDelete: 'SET NULL'
+ })
+ @JoinColumn()
+ public banner: DriveFile | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 128, array: true, default: '{}'
+ })
+ public tags: string[];
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ })
+ public avatarUrl: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ })
+ public bannerUrl: string | null;
+
+ @Column('varchar', {
+ length: 32, nullable: true,
+ })
+ public avatarColor: string | null;
+
+ @Column('varchar', {
+ length: 32, nullable: true,
+ })
+ public bannerColor: string | null;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is suspended.'
+ })
+ public isSuspended: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is silenced.'
+ })
+ public isSilenced: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is locked.'
+ })
+ public isLocked: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is a bot.'
+ })
+ public isBot: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is a cat.'
+ })
+ public isCat: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is the admin.'
+ })
+ public isAdmin: boolean;
+
+ @Column('boolean', {
+ default: false,
+ comment: 'Whether the User is a moderator.'
+ })
+ public isModerator: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isVerified: boolean;
+
+ @Column('varchar', {
+ length: 128, array: true, default: '{}'
+ })
+ public emojis: string[];
+
+ @Index()
+ @Column('varchar', {
+ length: 128, nullable: true,
+ comment: 'The host of the User. It will be null if the origin of the user is local.'
+ })
+ public host: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The inbox URL of the User. It will be null if the origin of the user is local.'
+ })
+ public inbox: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'
+ })
+ public sharedInbox: string | null;
+
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The featured URL of the User. It will be null if the origin of the user is local.'
+ })
+ public featured: string | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 512, nullable: true,
+ comment: 'The URI of the User. It will be null if the origin of the user is local.'
+ })
+ public uri: string | null;
+
+ @Index({ unique: true })
+ @Column('char', {
+ length: 16, nullable: true, unique: true,
+ comment: 'The native access token of the User. It will be null if the origin of the user is local.'
+ })
+ public token: string | null;
+
+ constructor(data: Partial) {
+ if (data == null) return;
+
+ for (const [k, v] of Object.entries(data)) {
+ (this as any)[k] = v;
+ }
+ }
+}
+
+export interface ILocalUser extends User {
+ host: null;
+}
+
+export interface IRemoteUser extends User {
+ host: string;
+}
diff --git a/src/models/favorite.ts b/src/models/favorite.ts
deleted file mode 100644
index 2008edbfaf..0000000000
--- a/src/models/favorite.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packNote } from './note';
-import { dbLogger } from '../db/logger';
-
-const Favorite = db.get('favorites');
-Favorite.createIndex('userId');
-Favorite.createIndex(['userId', 'noteId'], { unique: true });
-export default Favorite;
-
-export type IFavorite = {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID;
- noteId: mongo.ObjectID;
-};
-
-export const packMany = (
- favorites: any[],
- me: any
-) => {
- return Promise.all(favorites.map(f => pack(f, me)));
-};
-
-/**
- * Pack a favorite for API response
- */
-export const pack = (
- favorite: any,
- me: any
-) => new Promise(async (resolve, reject) => {
- let _favorite: any;
-
- // Populate the favorite if 'favorite' is ID
- if (isObjectId(favorite)) {
- _favorite = await Favorite.findOne({
- _id: favorite
- });
- } else if (typeof favorite === 'string') {
- _favorite = await Favorite.findOne({
- _id: new mongo.ObjectID(favorite)
- });
- } else {
- _favorite = deepcopy(favorite);
- }
-
- // Rename _id to id
- _favorite.id = _favorite._id;
- delete _favorite._id;
-
- // Populate note
- _favorite.note = await packNote(_favorite.noteId, me, {
- detail: true
- });
-
- // (データベースの不具合などで)投稿が見つからなかったら
- if (_favorite.note == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: favorite -> note :: ${_favorite.id} (note ${_favorite.noteId})`);
- return resolve(null);
- }
-
- resolve(_favorite);
-});
diff --git a/src/models/follow-request.ts b/src/models/follow-request.ts
deleted file mode 100644
index 4f75c63a32..0000000000
--- a/src/models/follow-request.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packUser } from './user';
-
-const FollowRequest = db.get('followRequests');
-FollowRequest.createIndex('followerId');
-FollowRequest.createIndex('followeeId');
-FollowRequest.createIndex(['followerId', 'followeeId'], { unique: true });
-export default FollowRequest;
-
-export type IFollowRequest = {
- _id: mongo.ObjectID;
- createdAt: Date;
- followeeId: mongo.ObjectID;
- followerId: mongo.ObjectID;
- requestId?: string; // id of Follow Activity
-
- // 非正規化
- _followee: {
- host: string;
- inbox?: string;
- sharedInbox?: string;
- },
- _follower: {
- host: string;
- inbox?: string;
- sharedInbox?: string;
- }
-};
-
-/**
- * Pack a request for API response
- */
-export const pack = (
- request: any,
- me?: any
-) => new Promise(async (resolve, reject) => {
- let _request: any;
-
- // Populate the request if 'request' is ID
- if (isObjectId(request)) {
- _request = await FollowRequest.findOne({
- _id: request
- });
- } else if (typeof request === 'string') {
- _request = await FollowRequest.findOne({
- _id: new mongo.ObjectID(request)
- });
- } else {
- _request = deepcopy(request);
- }
-
- // Rename _id to id
- _request.id = _request._id;
- delete _request._id;
-
- // Populate follower
- _request.follower = await packUser(_request.followerId, me);
-
- // Populate followee
- _request.followee = await packUser(_request.followeeId, me);
-
- resolve(_request);
-});
diff --git a/src/models/following.ts b/src/models/following.ts
deleted file mode 100644
index 12cc27211b..0000000000
--- a/src/models/following.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Following = db.get('following');
-Following.createIndex('followerId');
-Following.createIndex('followeeId');
-Following.createIndex(['followerId', 'followeeId'], { unique: true });
-export default Following;
-
-export type IFollowing = {
- _id: mongo.ObjectID;
- createdAt: Date;
- followeeId: mongo.ObjectID;
- followerId: mongo.ObjectID;
-
- // 非正規化
- _followee: {
- host: string;
- inbox?: string;
- sharedInbox?: string;
- },
- _follower: {
- host: string;
- inbox?: string;
- sharedInbox?: string;
- }
-};
diff --git a/src/models/games/reversi/game.ts b/src/models/games/reversi/game.ts
deleted file mode 100644
index 57c493cff5..0000000000
--- a/src/models/games/reversi/game.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../../../db/mongodb';
-import isObjectId from '../../../misc/is-objectid';
-import { IUser, pack as packUser } from '../../user';
-
-const ReversiGame = db.get('reversiGames');
-export default ReversiGame;
-
-export interface IReversiGame {
- _id: mongo.ObjectID;
- createdAt: Date;
- startedAt: Date;
- user1Id: mongo.ObjectID;
- user2Id: mongo.ObjectID;
- user1Accepted: boolean;
- user2Accepted: boolean;
-
- /**
- * どちらのプレイヤーが先行(黒)か
- * 1 ... user1
- * 2 ... user2
- */
- black: number;
-
- isStarted: boolean;
- isEnded: boolean;
- winnerId: mongo.ObjectID;
- surrendered: mongo.ObjectID;
- logs: {
- at: Date;
- color: boolean;
- pos: number;
- }[];
- settings: {
- map: string[];
- bw: string | number;
- isLlotheo: boolean;
- canPutEverywhere: boolean;
- loopedBoard: boolean;
- };
- form1: any;
- form2: any;
-
- // ログのposを文字列としてすべて連結したもののCRC32値
- crc32: string;
-}
-
-/**
- * Pack an reversi game for API response
- */
-export const pack = (
- game: any,
- me?: string | mongo.ObjectID | IUser,
- options?: {
- detail?: boolean
- }
-) => new Promise(async (resolve, reject) => {
- const opts = Object.assign({
- detail: true
- }, options);
-
- let _game: any;
-
- // Populate the game if 'game' is ID
- if (isObjectId(game)) {
- _game = await ReversiGame.findOne({
- _id: game
- });
- } else if (typeof game === 'string') {
- _game = await ReversiGame.findOne({
- _id: new mongo.ObjectID(game)
- });
- } else {
- _game = deepcopy(game);
- }
-
- // Me
- const meId: mongo.ObjectID = me
- ? isObjectId(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- // Rename _id to id
- _game.id = _game._id;
- delete _game._id;
-
- if (opts.detail === false) {
- delete _game.logs;
- delete _game.settings.map;
- } else {
- // 互換性のため
- if (_game.settings.map.hasOwnProperty('size')) {
- _game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g'));
- }
- }
-
- // Populate user
- _game.user1 = await packUser(_game.user1Id, meId);
- _game.user2 = await packUser(_game.user2Id, meId);
- if (_game.winnerId) {
- _game.winner = await packUser(_game.winnerId, meId);
- } else {
- _game.winner = null;
- }
-
- resolve(_game);
-});
diff --git a/src/models/games/reversi/matching.ts b/src/models/games/reversi/matching.ts
deleted file mode 100644
index ba2ac1bc05..0000000000
--- a/src/models/games/reversi/matching.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../../../db/mongodb';
-import isObjectId from '../../../misc/is-objectid';
-import { IUser, pack as packUser } from '../../user';
-
-const Matching = db.get('reversiMatchings');
-export default Matching;
-
-export interface IMatching {
- _id: mongo.ObjectID;
- createdAt: Date;
- parentId: mongo.ObjectID;
- childId: mongo.ObjectID;
-}
-
-/**
- * Pack an reversi matching for API response
- */
-export const pack = (
- matching: any,
- me?: string | mongo.ObjectID | IUser
-) => new Promise(async (resolve, reject) => {
-
- // Me
- const meId: mongo.ObjectID = me
- ? isObjectId(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- const _matching = deepcopy(matching);
-
- // Rename _id to id
- _matching.id = _matching._id;
- delete _matching._id;
-
- // Populate user
- _matching.parent = await packUser(_matching.parentId, meId);
- _matching.child = await packUser(_matching.childId, meId);
-
- resolve(_matching);
-});
diff --git a/src/models/hashtag.ts b/src/models/hashtag.ts
deleted file mode 100644
index c1de42086e..0000000000
--- a/src/models/hashtag.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Hashtag = db.get('hashtags');
-Hashtag.createIndex('tag', { unique: true });
-Hashtag.createIndex('mentionedUsersCount');
-Hashtag.createIndex('mentionedLocalUsersCount');
-Hashtag.createIndex('mentionedRemoteUsersCount');
-Hashtag.createIndex('attachedUsersCount');
-Hashtag.createIndex('attachedLocalUsersCount');
-Hashtag.createIndex('attachedRemoteUsersCount');
-export default Hashtag;
-
-// 後方互換性のため
-Hashtag.findOne({ attachedUserIds: { $exists: false }}).then(h => {
- if (h != null) {
- Hashtag.update({}, {
- $rename: {
- mentionedUserIdsCount: 'mentionedUsersCount'
- },
- $set: {
- mentionedLocalUserIds: [],
- mentionedLocalUsersCount: 0,
- attachedUserIds: [],
- attachedUsersCount: 0,
- attachedLocalUserIds: [],
- attachedLocalUsersCount: 0,
- }
- }, {
- multi: true
- });
- }
-});
-Hashtag.findOne({ attachedRemoteUserIds: { $exists: false }}).then(h => {
- if (h != null) {
- Hashtag.update({}, {
- $set: {
- mentionedRemoteUserIds: [],
- mentionedRemoteUsersCount: 0,
- attachedRemoteUserIds: [],
- attachedRemoteUsersCount: 0,
- }
- }, {
- multi: true
- });
- }
-});
-
-export interface IHashtags {
- tag: string;
- mentionedUserIds: mongo.ObjectID[];
- mentionedUsersCount: number;
- mentionedLocalUserIds: mongo.ObjectID[];
- mentionedLocalUsersCount: number;
- mentionedRemoteUserIds: mongo.ObjectID[];
- mentionedRemoteUsersCount: number;
- attachedUserIds: mongo.ObjectID[];
- attachedUsersCount: number;
- attachedLocalUserIds: mongo.ObjectID[];
- attachedLocalUsersCount: number;
- attachedRemoteUserIds: mongo.ObjectID[];
- attachedRemoteUsersCount: number;
-}
diff --git a/src/models/id.ts b/src/models/id.ts
new file mode 100644
index 0000000000..be2cccfe3b
--- /dev/null
+++ b/src/models/id.ts
@@ -0,0 +1,4 @@
+export const id = () => ({
+ type: 'varchar' as 'varchar',
+ length: 32
+});
diff --git a/src/models/index.ts b/src/models/index.ts
new file mode 100644
index 0000000000..d66e4e710a
--- /dev/null
+++ b/src/models/index.ts
@@ -0,0 +1,74 @@
+import { getRepository, getCustomRepository } from 'typeorm';
+import { Instance } from './entities/instance';
+import { Emoji } from './entities/emoji';
+import { Poll } from './entities/poll';
+import { PollVote } from './entities/poll-vote';
+import { Meta } from './entities/meta';
+import { SwSubscription } from './entities/sw-subscription';
+import { NoteWatching } from './entities/note-watching';
+import { UserListJoining } from './entities/user-list-joining';
+import { Hashtag } from './entities/hashtag';
+import { NoteUnread } from './entities/note-unread';
+import { RegistrationTicket } from './entities/registration-tickets';
+import { UserRepository } from './repositories/user';
+import { NoteRepository } from './repositories/note';
+import { DriveFileRepository } from './repositories/drive-file';
+import { DriveFolderRepository } from './repositories/drive-folder';
+import { Log } from './entities/log';
+import { AccessToken } from './entities/access-token';
+import { UserNotePining } from './entities/user-note-pinings';
+import { SigninRepository } from './repositories/signin';
+import { MessagingMessageRepository } from './repositories/messaging-message';
+import { ReversiGameRepository } from './repositories/games/reversi/game';
+import { UserListRepository } from './repositories/user-list';
+import { FollowRequestRepository } from './repositories/follow-request';
+import { MutingRepository } from './repositories/muting';
+import { BlockingRepository } from './repositories/blocking';
+import { NoteReactionRepository } from './repositories/note-reaction';
+import { NotificationRepository } from './repositories/notification';
+import { NoteFavoriteRepository } from './repositories/note-favorite';
+import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
+import { UserPublickey } from './entities/user-publickey';
+import { UserKeypair } from './entities/user-keypair';
+import { AppRepository } from './repositories/app';
+import { FollowingRepository } from './repositories/following';
+import { AbuseUserReportRepository } from './repositories/abuse-user-report';
+import { AuthSessionRepository } from './repositories/auth-session';
+import { UserProfile } from './entities/user-profile';
+
+export const Apps = getCustomRepository(AppRepository);
+export const Notes = getCustomRepository(NoteRepository);
+export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
+export const NoteWatchings = getRepository(NoteWatching);
+export const NoteReactions = getCustomRepository(NoteReactionRepository);
+export const NoteUnreads = getRepository(NoteUnread);
+export const Polls = getRepository(Poll);
+export const PollVotes = getRepository(PollVote);
+export const Users = getCustomRepository(UserRepository);
+export const UserProfiles = getRepository(UserProfile);
+export const UserKeypairs = getRepository(UserKeypair);
+export const UserPublickeys = getRepository(UserPublickey);
+export const UserLists = getCustomRepository(UserListRepository);
+export const UserListJoinings = getRepository(UserListJoining);
+export const UserNotePinings = getRepository(UserNotePining);
+export const Followings = getCustomRepository(FollowingRepository);
+export const FollowRequests = getCustomRepository(FollowRequestRepository);
+export const Instances = getRepository(Instance);
+export const Emojis = getRepository(Emoji);
+export const DriveFiles = getCustomRepository(DriveFileRepository);
+export const DriveFolders = getCustomRepository(DriveFolderRepository);
+export const Notifications = getCustomRepository(NotificationRepository);
+export const Metas = getRepository(Meta);
+export const Mutings = getCustomRepository(MutingRepository);
+export const Blockings = getCustomRepository(BlockingRepository);
+export const SwSubscriptions = getRepository(SwSubscription);
+export const Hashtags = getRepository(Hashtag);
+export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository);
+export const RegistrationTickets = getRepository(RegistrationTicket);
+export const AuthSessions = getCustomRepository(AuthSessionRepository);
+export const AccessTokens = getRepository(AccessToken);
+export const Signins = getCustomRepository(SigninRepository);
+export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
+export const ReversiGames = getCustomRepository(ReversiGameRepository);
+export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
+export const Logs = getRepository(Log);
diff --git a/src/models/instance.ts b/src/models/instance.ts
deleted file mode 100644
index cdce570a4b..0000000000
--- a/src/models/instance.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Instance = db.get('instances');
-Instance.createIndex('host', { unique: true });
-export default Instance;
-
-export interface IInstance {
- _id: mongo.ObjectID;
-
- /**
- * ホスト
- */
- host: string;
-
- /**
- * このインスタンスを捕捉した日時
- */
- caughtAt: Date;
-
- /**
- * このインスタンスのシステム (MastodonとかMisskeyとかPleromaとか)
- */
- system: string;
-
- /**
- * このインスタンスのユーザー数
- */
- usersCount: number;
-
- /**
- * このインスタンスから受け取った投稿数
- */
- notesCount: number;
-
- /**
- * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
- */
- followingCount: number;
-
- /**
- * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
- */
- followersCount: number;
-
- /**
- * ドライブ使用量
- */
- driveUsage: number;
-
- /**
- * ドライブのファイル数
- */
- driveFiles: number;
-
- /**
- * 直近のリクエスト送信日時
- */
- latestRequestSentAt?: Date;
-
- /**
- * 直近のリクエスト送信時のHTTPステータスコード
- */
- latestStatus?: number;
-
- /**
- * 直近のリクエスト受信日時
- */
- latestRequestReceivedAt?: Date;
-
- /**
- * このインスタンスと不通かどうか
- */
- isNotResponding: boolean;
-
- /**
- * このインスタンスと最後にやり取りした日時
- */
- lastCommunicatedAt: Date;
-
- /**
- * このインスタンスをブロックしているか
- */
- isBlocked: boolean;
-
- /**
- * このインスタンスが閉鎖済みとしてマークされているか
- */
- isMarkedAsClosed: boolean;
-}
diff --git a/src/models/log.ts b/src/models/log.ts
deleted file mode 100644
index 6f79e83c78..0000000000
--- a/src/models/log.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Log = db.get('logs');
-Log.createIndex('createdAt', { expireAfterSeconds: 3600 * 24 * 3 });
-Log.createIndex('level');
-Log.createIndex('domain');
-export default Log;
-
-export interface ILog {
- _id: mongo.ObjectID;
- createdAt: Date;
- machine: string;
- worker: string;
- domain: string[];
- level: string;
- message: string;
- data: any;
-}
diff --git a/src/models/messaging-message.ts b/src/models/messaging-message.ts
deleted file mode 100644
index 67abb4d111..0000000000
--- a/src/models/messaging-message.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import { pack as packUser } from './user';
-import { pack as packFile } from './drive-file';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { length } from 'stringz';
-
-const MessagingMessage = db.get('messagingMessages');
-MessagingMessage.createIndex('userId');
-MessagingMessage.createIndex('recipientId');
-export default MessagingMessage;
-
-export interface IMessagingMessage {
- _id: mongo.ObjectID;
- createdAt: Date;
- text: string;
- userId: mongo.ObjectID;
- recipientId: mongo.ObjectID;
- isRead: boolean;
- fileId: mongo.ObjectID;
-}
-
-export function isValidText(text: string): boolean {
- return length(text.trim()) <= 1000 && text.trim() != '';
-}
-
-/**
- * Pack a messaging message for API response
- */
-export const pack = (
- message: any,
- me?: any,
- options?: {
- populateRecipient: boolean
- }
-) => new Promise(async (resolve, reject) => {
- const opts = options || {
- populateRecipient: true
- };
-
- let _message: any;
-
- // Populate the message if 'message' is ID
- if (isObjectId(message)) {
- _message = await MessagingMessage.findOne({
- _id: message
- });
- } else if (typeof message === 'string') {
- _message = await MessagingMessage.findOne({
- _id: new mongo.ObjectID(message)
- });
- } else {
- _message = deepcopy(message);
- }
-
- // Rename _id to id
- _message.id = _message._id;
- delete _message._id;
-
- // Populate user
- _message.user = await packUser(_message.userId, me);
-
- if (_message.fileId) {
- // Populate file
- _message.file = await packFile(_message.fileId);
- }
-
- if (opts.populateRecipient) {
- // Populate recipient
- _message.recipient = await packUser(_message.recipientId, me);
- }
-
- resolve(_message);
-});
diff --git a/src/models/meta.ts b/src/models/meta.ts
deleted file mode 100644
index 5ca0f01236..0000000000
--- a/src/models/meta.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-import db from '../db/mongodb';
-import config from '../config';
-import User from './user';
-import { transform } from '../misc/cafy-id';
-
-const Meta = db.get('meta');
-export default Meta;
-
-// 後方互換性のため。
-// 過去のMisskeyではインスタンス名や紹介を設定ファイルに記述していたのでそれを移行
-if ((config as any).name) {
- Meta.findOne({}).then(m => {
- if (m != null && m.name == null) {
- Meta.update({}, {
- $set: {
- name: (config as any).name
- }
- });
- }
- });
-}
-if ((config as any).description) {
- Meta.findOne({}).then(m => {
- if (m != null && m.description == null) {
- Meta.update({}, {
- $set: {
- description: (config as any).description
- }
- });
- }
- });
-}
-if ((config as any).localDriveCapacityMb) {
- Meta.findOne({}).then(m => {
- if (m != null && m.localDriveCapacityMb == null) {
- Meta.update({}, {
- $set: {
- localDriveCapacityMb: (config as any).localDriveCapacityMb
- }
- });
- }
- });
-}
-if ((config as any).remoteDriveCapacityMb) {
- Meta.findOne({}).then(m => {
- if (m != null && m.remoteDriveCapacityMb == null) {
- Meta.update({}, {
- $set: {
- remoteDriveCapacityMb: (config as any).remoteDriveCapacityMb
- }
- });
- }
- });
-}
-if ((config as any).preventCacheRemoteFiles) {
- Meta.findOne({}).then(m => {
- if (m != null && m.cacheRemoteFiles == null) {
- Meta.update({}, {
- $set: {
- cacheRemoteFiles: !(config as any).preventCacheRemoteFiles
- }
- });
- }
- });
-}
-if ((config as any).recaptcha) {
- Meta.findOne({}).then(m => {
- if (m != null && m.enableRecaptcha == null) {
- Meta.update({}, {
- $set: {
- enableRecaptcha: (config as any).recaptcha != null,
- recaptchaSiteKey: (config as any).recaptcha.site_key,
- recaptchaSecretKey: (config as any).recaptcha.secret_key,
- }
- });
- }
- });
-}
-if ((config as any).ghost) {
- Meta.findOne({}).then(async m => {
- if (m != null && m.proxyAccount == null) {
- const account = await User.findOne({ _id: transform((config as any).ghost) });
- Meta.update({}, {
- $set: {
- proxyAccount: account.username
- }
- });
- }
- });
-}
-if ((config as any).maintainer) {
- Meta.findOne({}).then(m => {
- if (m != null && m.maintainer == null) {
- Meta.update({}, {
- $set: {
- maintainer: (config as any).maintainer
- }
- });
- }
- });
-}
-if ((config as any).twitter) {
- Meta.findOne({}).then(m => {
- if (m != null && m.enableTwitterIntegration == null) {
- Meta.update({}, {
- $set: {
- enableTwitterIntegration: true,
- twitterConsumerKey: (config as any).twitter.consumer_key,
- twitterConsumerSecret: (config as any).twitter.consumer_secret
- }
- });
- }
- });
-}
-if ((config as any).github) {
- Meta.findOne({}).then(m => {
- if (m != null && m.enableGithubIntegration == null) {
- Meta.update({}, {
- $set: {
- enableGithubIntegration: true,
- githubClientId: (config as any).github.client_id,
- githubClientSecret: (config as any).github.client_secret
- }
- });
- }
- });
-}
-if ((config as any).user_recommendation) {
- Meta.findOne({}).then(m => {
- if (m != null && m.enableExternalUserRecommendation == null) {
- Meta.update({}, {
- $set: {
- enableExternalUserRecommendation: true,
- externalUserRecommendationEngine: (config as any).user_recommendation.engine,
- externalUserRecommendationTimeout: (config as any).user_recommendation.timeout
- }
- });
- }
- });
-}
-if ((config as any).sw) {
- Meta.findOne({}).then(m => {
- if (m != null && m.enableServiceWorker == null) {
- Meta.update({}, {
- $set: {
- enableServiceWorker: true,
- swPublicKey: (config as any).sw.public_key,
- swPrivateKey: (config as any).sw.private_key
- }
- });
- }
- });
-}
-Meta.findOne({}).then(m => {
- if (m != null && (m as any).broadcasts != null) {
- Meta.update({}, {
- $rename: {
- broadcasts: 'announcements'
- }
- });
- }
-});
-
-export type IMeta = {
- name?: string;
- description?: string;
-
- /**
- * メンテナ情報
- */
- maintainer: {
- /**
- * メンテナの名前
- */
- name: string;
-
- /**
- * メンテナの連絡先
- */
- email?: string;
- };
-
- langs?: string[];
-
- announcements?: any[];
-
- stats?: {
- notesCount: number;
- originalNotesCount: number;
- usersCount: number;
- originalUsersCount: number;
- };
-
- disableRegistration?: boolean;
- disableLocalTimeline?: boolean;
- disableGlobalTimeline?: boolean;
- enableEmojiReaction?: boolean;
- useStarForReactionFallback?: boolean;
- hidedTags?: string[];
- mascotImageUrl?: string;
- bannerUrl?: string;
- errorImageUrl?: string;
- iconUrl?: string;
-
- cacheRemoteFiles?: boolean;
-
- proxyAccount?: string;
-
- enableRecaptcha?: boolean;
- recaptchaSiteKey?: string;
- recaptchaSecretKey?: string;
-
- /**
- * Drive capacity of a local user (MB)
- */
- localDriveCapacityMb?: number;
-
- /**
- * Drive capacity of a remote user (MB)
- */
- remoteDriveCapacityMb?: number;
-
- /**
- * Max allowed note text length in characters
- */
- maxNoteTextLength?: number;
-
- summalyProxy?: string;
-
- enableTwitterIntegration?: boolean;
- twitterConsumerKey?: string;
- twitterConsumerSecret?: string;
-
- enableGithubIntegration?: boolean;
- githubClientId?: string;
- githubClientSecret?: string;
-
- enableDiscordIntegration?: boolean;
- discordClientId?: string;
- discordClientSecret?: string;
-
- enableExternalUserRecommendation?: boolean;
- externalUserRecommendationEngine?: string;
- externalUserRecommendationTimeout?: number;
-
- enableEmail?: boolean;
- email?: string;
- smtpSecure?: boolean;
- smtpHost?: string;
- smtpPort?: number;
- smtpUser?: string;
- smtpPass?: string;
-
- enableServiceWorker?: boolean;
- swPublicKey?: string;
- swPrivateKey?: string;
-};
diff --git a/src/models/mute.ts b/src/models/mute.ts
deleted file mode 100644
index 52775e13ca..0000000000
--- a/src/models/mute.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import * as deepcopy from 'deepcopy';
-import { pack as packUser, IUser } from './user';
-
-const Mute = db.get('mute');
-Mute.createIndex('muterId');
-Mute.createIndex('muteeId');
-Mute.createIndex(['muterId', 'muteeId'], { unique: true });
-export default Mute;
-
-export interface IMute {
- _id: mongo.ObjectID;
- createdAt: Date;
- muterId: mongo.ObjectID;
- muteeId: mongo.ObjectID;
-}
-
-export const packMany = (
- mutes: (string | mongo.ObjectID | IMute)[],
- me?: string | mongo.ObjectID | IUser
-) => {
- return Promise.all(mutes.map(x => pack(x, me)));
-};
-
-export const pack = (
- mute: any,
- me?: any
-) => new Promise(async (resolve, reject) => {
- let _mute: any;
-
- // Populate the mute if 'mute' is ID
- if (isObjectId(mute)) {
- _mute = await Mute.findOne({
- _id: mute
- });
- } else if (typeof mute === 'string') {
- _mute = await Mute.findOne({
- _id: new mongo.ObjectID(mute)
- });
- } else {
- _mute = deepcopy(mute);
- }
-
- // Rename _id to id
- _mute.id = _mute._id;
- delete _mute._id;
-
- // Populate mutee
- _mute.mutee = await packUser(_mute.muteeId, me, {
- detail: true
- });
-
- resolve(_mute);
-});
diff --git a/src/models/note-reaction.ts b/src/models/note-reaction.ts
deleted file mode 100644
index 89b7529350..0000000000
--- a/src/models/note-reaction.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packUser } from './user';
-
-const NoteReaction = db.get('noteReactions');
-NoteReaction.createIndex('noteId');
-NoteReaction.createIndex('userId');
-NoteReaction.createIndex(['userId', 'noteId'], { unique: true });
-export default NoteReaction;
-
-export interface INoteReaction {
- _id: mongo.ObjectID;
- createdAt: Date;
- noteId: mongo.ObjectID;
- userId: mongo.ObjectID;
- reaction: string;
-}
-
-/**
- * Pack a reaction for API response
- */
-export const pack = (
- reaction: any,
- me?: any
-) => new Promise(async (resolve, reject) => {
- let _reaction: any;
-
- // Populate the reaction if 'reaction' is ID
- if (isObjectId(reaction)) {
- _reaction = await NoteReaction.findOne({
- _id: reaction
- });
- } else if (typeof reaction === 'string') {
- _reaction = await NoteReaction.findOne({
- _id: new mongo.ObjectID(reaction)
- });
- } else {
- _reaction = deepcopy(reaction);
- }
-
- // Rename _id to id
- _reaction.id = _reaction._id;
- delete _reaction._id;
-
- // Populate user
- _reaction.user = await packUser(_reaction.userId, me);
-
- resolve(_reaction);
-});
diff --git a/src/models/note-unread.ts b/src/models/note-unread.ts
deleted file mode 100644
index dd08640d85..0000000000
--- a/src/models/note-unread.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const NoteUnread = db.get('noteUnreads');
-NoteUnread.createIndex('userId');
-NoteUnread.createIndex('noteId');
-NoteUnread.createIndex(['userId', 'noteId'], { unique: true });
-export default NoteUnread;
-
-export interface INoteUnread {
- _id: mongo.ObjectID;
- noteId: mongo.ObjectID;
- userId: mongo.ObjectID;
- isSpecified: boolean;
-
- _note: {
- userId: mongo.ObjectID;
- };
-}
diff --git a/src/models/note-watching.ts b/src/models/note-watching.ts
deleted file mode 100644
index 83aaf8ad06..0000000000
--- a/src/models/note-watching.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const NoteWatching = db.get('noteWatching');
-NoteWatching.createIndex('userId');
-NoteWatching.createIndex('noteId');
-NoteWatching.createIndex(['userId', 'noteId'], { unique: true });
-export default NoteWatching;
-
-export interface INoteWatching {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID;
- noteId: mongo.ObjectID;
-}
diff --git a/src/models/note.ts b/src/models/note.ts
deleted file mode 100644
index 8c71c1940c..0000000000
--- a/src/models/note.ts
+++ /dev/null
@@ -1,418 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import rap from '@prezzemolo/rap';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { length } from 'stringz';
-import { IUser, pack as packUser } from './user';
-import { pack as packApp } from './app';
-import PollVote from './poll-vote';
-import NoteReaction from './note-reaction';
-import { packMany as packFileMany, IDriveFile } from './drive-file';
-import Following from './following';
-import Emoji from './emoji';
-import { dbLogger } from '../db/logger';
-import { unique, concat } from '../prelude/array';
-
-const Note = db.get('notes');
-Note.createIndex('uri', { sparse: true, unique: true });
-Note.createIndex('userId');
-Note.createIndex('mentions');
-Note.createIndex('visibleUserIds');
-Note.createIndex('replyId');
-Note.createIndex('renoteId');
-Note.createIndex('tagsLower');
-Note.createIndex('_user.host');
-Note.createIndex('_files._id');
-Note.createIndex('_files.contentType');
-Note.createIndex({ createdAt: -1 });
-Note.createIndex({ score: -1 }, { sparse: true });
-export default Note;
-
-export function isValidCw(text: string): boolean {
- return length(text.trim()) <= 100;
-}
-
-export type INote = {
- _id: mongo.ObjectID;
- createdAt: Date;
- deletedAt: Date;
- updatedAt?: Date;
- fileIds: mongo.ObjectID[];
- replyId: mongo.ObjectID;
- renoteId: mongo.ObjectID;
- poll: IPoll;
- name?: string;
- text: string;
- tags: string[];
- tagsLower: string[];
- emojis: string[];
- cw: string;
- userId: mongo.ObjectID;
- appId: mongo.ObjectID;
- viaMobile: boolean;
- localOnly: boolean;
- renoteCount: number;
- repliesCount: number;
- reactionCounts: Record;
- mentions: mongo.ObjectID[];
- mentionedRemoteUsers: {
- uri: string;
- username: string;
- host: string;
- }[];
-
- /**
- * public ... 公開
- * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
- * followers ... フォロワーのみ
- * specified ... visibleUserIds で指定したユーザーのみ
- */
- visibility: 'public' | 'home' | 'followers' | 'specified';
-
- visibleUserIds: mongo.ObjectID[];
-
- geo: {
- coordinates: number[];
- altitude: number;
- accuracy: number;
- altitudeAccuracy: number;
- heading: number;
- speed: number;
- };
-
- uri: string;
-
- /**
- * 人気の投稿度合いを表すスコア
- */
- score: number;
-
- // 非正規化
- _reply?: {
- userId: mongo.ObjectID;
- };
- _renote?: {
- userId: mongo.ObjectID;
- };
- _user: {
- host: string;
- inbox?: string;
- };
- _files?: IDriveFile[];
-};
-
-export type IPoll = {
- choices: IChoice[];
- multiple?: boolean;
- expiresAt?: Date;
-};
-
-export type IChoice = {
- id: number;
- text: string;
- votes: number;
-};
-
-export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
- let hide = false;
-
- // visibility が private かつ投稿者のIDが自分のIDではなかったら非表示(後方互換性のため)
- if (packedNote.visibility == 'private' && (meId == null || !meId.equals(packedNote.userId))) {
- hide = true;
- }
-
- // visibility が specified かつ自分が指定されていなかったら非表示
- if (packedNote.visibility == 'specified') {
- if (meId == null) {
- hide = true;
- } else if (meId.equals(packedNote.userId)) {
- hide = false;
- } else {
- // 指定されているかどうか
- const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id));
-
- if (specified) {
- hide = false;
- } else {
- hide = true;
- }
- }
- }
-
- // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
- if (packedNote.visibility == 'followers') {
- if (meId == null) {
- hide = true;
- } else if (meId.equals(packedNote.userId)) {
- hide = false;
- } else if (packedNote.reply && meId.equals(packedNote.reply.userId)) {
- // 自分の投稿に対するリプライ
- hide = false;
- } else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId.equals(id))) {
- // 自分へのメンション
- hide = false;
- } else {
- // フォロワーかどうか
- const following = await Following.findOne({
- followeeId: packedNote.userId,
- followerId: meId
- });
-
- if (following == null) {
- hide = true;
- } else {
- hide = false;
- }
- }
- }
-
- if (hide) {
- packedNote.fileIds = [];
- packedNote.files = [];
- packedNote.text = null;
- packedNote.poll = null;
- packedNote.cw = null;
- packedNote.tags = [];
- packedNote.geo = null;
- packedNote.isHidden = true;
- }
-};
-
-export const packMany = (
- notes: (string | mongo.ObjectID | INote)[],
- me?: string | mongo.ObjectID | IUser,
- options?: {
- detail?: boolean;
- skipHide?: boolean;
- }
-) => {
- return Promise.all(notes.map(n => pack(n, me, options)));
-};
-
-/**
- * Pack a note for API response
- *
- * @param note target
- * @param me? serializee
- * @param options? serialize options
- * @return response
- */
-export const pack = async (
- note: string | mongo.ObjectID | INote,
- me?: string | mongo.ObjectID | IUser,
- options?: {
- detail?: boolean;
- skipHide?: boolean;
- }
-) => {
- const opts = Object.assign({
- detail: true,
- skipHide: false
- }, options);
-
- // Me
- const meId: mongo.ObjectID = me
- ? isObjectId(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- let _note: any;
-
- // Populate the note if 'note' is ID
- if (isObjectId(note)) {
- _note = await Note.findOne({
- _id: note
- });
- } else if (typeof note === 'string') {
- _note = await Note.findOne({
- _id: new mongo.ObjectID(note)
- });
- } else {
- _note = deepcopy(note);
- }
-
- // (データベースの欠損などで)投稿がデータベース上に見つからなかったとき
- if (_note == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: note :: ${note}`);
- return null;
- }
-
- const id = _note._id;
-
- // Some counts
- _note.renoteCount = _note.renoteCount || 0;
- _note.repliesCount = _note.repliesCount || 0;
- _note.reactionCounts = _note.reactionCounts || {};
-
- // _note._userを消す前か、_note.userを解決した後でないとホストがわからない
- if (_note._user) {
- const host = _note._user.host;
- // 互換性のため。(古いMisskeyではNoteにemojisが無い)
- if (_note.emojis == null) {
- _note.emojis = Emoji.find({
- host: host
- }, {
- fields: { _id: false }
- });
- } else {
- _note.emojis = unique(concat([_note.emojis, Object.keys(_note.reactionCounts).map(x => x.replace(/:/g, ''))]));
-
- _note.emojis = Emoji.find({
- name: { $in: _note.emojis },
- host: host
- }, {
- fields: { _id: false }
- });
- }
- }
-
- // Rename _id to id
- _note.id = _note._id;
- delete _note._id;
-
- delete _note.prev;
- delete _note.next;
- delete _note.tagsLower;
- delete _note.score;
- delete _note._user;
- delete _note._reply;
- delete _note._renote;
- delete _note._files;
- delete _note._replyIds;
- delete _note.mentionedRemoteUsers;
-
- if (_note.geo) delete _note.geo.type;
-
- // Populate user
- _note.user = packUser(_note.userId, meId);
-
- // Populate app
- if (_note.appId) {
- _note.app = packApp(_note.appId);
- }
-
- // Populate files
- _note.files = packFileMany(_note.fileIds || []);
-
- // 後方互換性のため
- _note.mediaIds = _note.fileIds;
- _note.media = _note.files;
-
- // When requested a detailed note data
- if (opts.detail) {
- if (_note.replyId) {
- // Populate reply to note
- _note.reply = pack(_note.replyId, meId, {
- detail: false
- });
- }
-
- if (_note.renoteId) {
- // Populate renote
- _note.renote = pack(_note.renoteId, meId, {
- detail: _note.text == null
- });
- }
-
- // Poll
- if (meId && _note.poll) {
- _note.poll = (async poll => {
- if (poll.multiple) {
- const votes = await PollVote.find({
- userId: meId,
- noteId: id
- });
-
- const myChoices = (poll.choices as IChoice[]).filter(x => votes.some(y => x.id == y.choice));
- for (const myChoice of myChoices) {
- (myChoice as any).isVoted = true;
- }
-
- return poll;
- } else {
- poll.multiple = false;
- }
-
- const vote = await PollVote
- .findOne({
- userId: meId,
- noteId: id
- });
-
- if (vote) {
- const myChoice = (poll.choices as IChoice[])
- .filter(x => x.id == vote.choice)[0] as any;
-
- myChoice.isVoted = true;
- }
-
- return poll;
- })(_note.poll);
- }
-
- if (meId) {
- // Fetch my reaction
- _note.myReaction = (async () => {
- const reaction = await NoteReaction
- .findOne({
- userId: meId,
- noteId: id,
- deletedAt: { $exists: false }
- });
-
- if (reaction) {
- return reaction.reaction;
- }
-
- return null;
- })();
- }
- }
-
- // resolve promises in _note object
- _note = await rap(_note);
-
- //#region (データベースの欠損などで)参照しているデータがデータベース上に見つからなかったとき
- if (_note.user == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> user :: ${_note.id} (user ${_note.userId})`);
- return null;
- }
-
- if (opts.detail) {
- if (_note.replyId != null && _note.reply == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> reply :: ${_note.id} (reply ${_note.replyId})`);
- return null;
- }
-
- if (_note.renoteId != null && _note.renote == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> renote :: ${_note.id} (renote ${_note.renoteId})`);
- return null;
- }
- }
- //#endregion
-
- if (_note.name) {
- _note.text = `【${_note.name}】\n${_note.text}`;
- }
-
- if (_note.user.isCat && _note.text) {
- _note.text = (_note.text
- // ja-JP
- .replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
- // ko-KR
- .replace(/[나-낳]/g, (match: string) => String.fromCharCode(
- match.codePointAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
- ))
- );
- }
-
- if (!opts.skipHide) {
- await hideNote(_note, meId);
- }
-
- return _note;
-};
diff --git a/src/models/notification.ts b/src/models/notification.ts
deleted file mode 100644
index 75456af57b..0000000000
--- a/src/models/notification.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { IUser, pack as packUser } from './user';
-import { pack as packNote } from './note';
-import { dbLogger } from '../db/logger';
-
-const Notification = db.get('notifications');
-Notification.createIndex('notifieeId');
-export default Notification;
-
-export interface INotification {
- _id: mongo.ObjectID;
- createdAt: Date;
-
- /**
- * 通知の受信者
- */
- notifiee?: IUser;
-
- /**
- * 通知の受信者
- */
- notifieeId: mongo.ObjectID;
-
- /**
- * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
- */
- notifier?: IUser;
-
- /**
- * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
- */
- notifierId: mongo.ObjectID;
-
- /**
- * 通知の種類。
- * follow - フォローされた
- * mention - 投稿で自分が言及された
- * reply - (自分または自分がWatchしている)投稿が返信された
- * renote - (自分または自分がWatchしている)投稿がRenoteされた
- * quote - (自分または自分がWatchしている)投稿が引用Renoteされた
- * reaction - (自分または自分がWatchしている)投稿にリアクションされた
- * poll_vote - (自分または自分がWatchしている)投稿の投票に投票された
- */
- type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'poll_vote';
-
- /**
- * 通知が読まれたかどうか
- */
- isRead: boolean;
-}
-
-export const packMany = (
- notifications: any[]
-) => {
- return Promise.all(notifications.map(n => pack(n)));
-};
-
-/**
- * Pack a notification for API response
- */
-export const pack = (notification: any) => new Promise(async (resolve, reject) => {
- let _notification: any;
-
- // Populate the notification if 'notification' is ID
- if (isObjectId(notification)) {
- _notification = await Notification.findOne({
- _id: notification
- });
- } else if (typeof notification === 'string') {
- _notification = await Notification.findOne({
- _id: new mongo.ObjectID(notification)
- });
- } else {
- _notification = deepcopy(notification);
- }
-
- // Rename _id to id
- _notification.id = _notification._id;
- delete _notification._id;
-
- // Rename notifierId to userId
- _notification.userId = _notification.notifierId;
- delete _notification.notifierId;
-
- const me = _notification.notifieeId;
- delete _notification.notifieeId;
-
- // Populate notifier
- _notification.user = await packUser(_notification.userId, me);
-
- switch (_notification.type) {
- case 'follow':
- case 'receiveFollowRequest':
- // nope
- break;
- case 'mention':
- case 'reply':
- case 'renote':
- case 'quote':
- case 'reaction':
- case 'poll_vote':
- // Populate note
- _notification.note = await packNote(_notification.noteId, me);
-
- // (データベースの不具合などで)投稿が見つからなかったら
- if (_notification.note == null) {
- dbLogger.warn(`[DAMAGED DB] (missing) pkg: notification -> note :: ${_notification.id} (note ${_notification.noteId})`);
- return resolve(null);
- }
- break;
- default:
- dbLogger.error(`Unknown type: ${_notification.type}`);
- break;
- }
-
- resolve(_notification);
-});
diff --git a/src/models/poll-vote.ts b/src/models/poll-vote.ts
deleted file mode 100644
index e6178cbc26..0000000000
--- a/src/models/poll-vote.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const PollVote = db.get('pollVotes');
-PollVote.dropIndex(['userId', 'noteId'], { unique: true }).catch(() => {});
-PollVote.createIndex('userId');
-PollVote.createIndex('noteId');
-PollVote.createIndex(['userId', 'noteId', 'choice'], { unique: true });
-export default PollVote;
-
-export interface IPollVote {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID;
- noteId: mongo.ObjectID;
- choice: number;
-}
diff --git a/src/models/registration-tickets.ts b/src/models/registration-tickets.ts
deleted file mode 100644
index 846acefedf..0000000000
--- a/src/models/registration-tickets.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const RegistrationTicket = db.get('registrationTickets');
-RegistrationTicket.createIndex('code', { unique: true });
-export default RegistrationTicket;
-
-export interface IRegistrationTicket {
- _id: mongo.ObjectID;
- createdAt: Date;
- code: string;
-}
diff --git a/src/models/repositories/abuse-user-report.ts b/src/models/repositories/abuse-user-report.ts
new file mode 100644
index 0000000000..f619d6e37f
--- /dev/null
+++ b/src/models/repositories/abuse-user-report.ts
@@ -0,0 +1,33 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { AbuseUserReport } from '../entities/abuse-user-report';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(AbuseUserReport)
+export class AbuseUserReportRepository extends Repository {
+ public packMany(
+ reports: any[],
+ ) {
+ return Promise.all(reports.map(x => this.pack(x)));
+ }
+
+ public async pack(
+ src: AbuseUserReport['id'] | AbuseUserReport,
+ ) {
+ const report = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return await rap({
+ id: report.id,
+ createdAt: report.createdAt,
+ reporterId: report.reporterId,
+ userId: report.userId,
+ reporter: Users.pack(report.reporter || report.reporterId, null, {
+ detail: true
+ }),
+ user: Users.pack(report.user || report.userId, null, {
+ detail: true
+ }),
+ });
+ }
+}
diff --git a/src/models/repositories/app.ts b/src/models/repositories/app.ts
new file mode 100644
index 0000000000..a0c0cf68cb
--- /dev/null
+++ b/src/models/repositories/app.ts
@@ -0,0 +1,37 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { App } from '../entities/app';
+import { AccessTokens } from '..';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(App)
+export class AppRepository extends Repository {
+ public async pack(
+ src: App['id'] | App,
+ me?: any,
+ options?: {
+ detail?: boolean,
+ includeSecret?: boolean,
+ includeProfileImageIds?: boolean
+ }
+ ) {
+ const opts = Object.assign({
+ detail: false,
+ includeSecret: false,
+ includeProfileImageIds: false
+ }, options);
+
+ const app = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return {
+ id: app.id,
+ name: app.name,
+ ...(opts.includeSecret ? { secret: app.secret } : {}),
+ ...(me ? {
+ isAuthorized: await AccessTokens.count({
+ appId: app.id,
+ userId: me,
+ }).then(count => count > 0)
+ } : {})
+ };
+ }
+}
diff --git a/src/models/repositories/auth-session.ts b/src/models/repositories/auth-session.ts
new file mode 100644
index 0000000000..540c5466f5
--- /dev/null
+++ b/src/models/repositories/auth-session.ts
@@ -0,0 +1,20 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Apps } from '..';
+import rap from '@prezzemolo/rap';
+import { AuthSession } from '../entities/auth-session';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(AuthSession)
+export class AuthSessionRepository extends Repository {
+ public async pack(
+ src: AuthSession['id'] | AuthSession,
+ me?: any
+ ) {
+ const session = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return await rap({
+ id: session.id,
+ app: Apps.pack(session.appId, me)
+ });
+ }
+}
diff --git a/src/models/repositories/blocking.ts b/src/models/repositories/blocking.ts
new file mode 100644
index 0000000000..e18aa591f3
--- /dev/null
+++ b/src/models/repositories/blocking.ts
@@ -0,0 +1,29 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { Blocking } from '../entities/blocking';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(Blocking)
+export class BlockingRepository extends Repository {
+ public packMany(
+ blockings: any[],
+ me: any
+ ) {
+ return Promise.all(blockings.map(x => this.pack(x, me)));
+ }
+
+ public async pack(
+ src: Blocking['id'] | Blocking,
+ me?: any
+ ) {
+ const blocking = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return await rap({
+ id: blocking.id,
+ blockee: Users.pack(blocking.blockeeId, me, {
+ detail: true
+ })
+ });
+ }
+}
diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts
new file mode 100644
index 0000000000..7dd794d031
--- /dev/null
+++ b/src/models/repositories/drive-file.ts
@@ -0,0 +1,111 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { DriveFile } from '../entities/drive-file';
+import { Users, DriveFolders } from '..';
+import rap from '@prezzemolo/rap';
+import { User } from '../entities/user';
+import { toPuny } from '../../misc/convert-host';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(DriveFile)
+export class DriveFileRepository extends Repository {
+ public validateFileName(name: string): boolean {
+ return (
+ (name.trim().length > 0) &&
+ (name.length <= 200) &&
+ (name.indexOf('\\') === -1) &&
+ (name.indexOf('/') === -1) &&
+ (name.indexOf('..') === -1)
+ );
+ }
+
+ public getPublicUrl(file: DriveFile, thumbnail = false): string | null {
+ return thumbnail ? (file.thumbnailUrl || file.webpublicUrl || null) : (file.webpublicUrl || file.thumbnailUrl || file.url);
+ }
+
+ public async clacDriveUsageOf(user: User['id'] | User): Promise {
+ const id = typeof user === 'object' ? user.id : user;
+
+ const { sum } = await this
+ .createQueryBuilder('file')
+ .where('file.userId = :id', { id: id })
+ .select('SUM(file.size)', 'sum')
+ .getRawOne();
+
+ return parseInt(sum, 10) || 0;
+ }
+
+ public async clacDriveUsageOfHost(host: string): Promise {
+ const { sum } = await this
+ .createQueryBuilder('file')
+ .where('file.userHost = :host', { host: toPuny(host) })
+ .select('SUM(file.size)', 'sum')
+ .getRawOne();
+
+ return parseInt(sum, 10) || 0;
+ }
+
+ public async clacDriveUsageOfLocal(): Promise {
+ const { sum } = await this
+ .createQueryBuilder('file')
+ .where('file.userHost IS NULL')
+ .select('SUM(file.size)', 'sum')
+ .getRawOne();
+
+ return parseInt(sum, 10) || 0;
+ }
+
+ public async clacDriveUsageOfRemote(): Promise {
+ const { sum } = await this
+ .createQueryBuilder('file')
+ .where('file.userHost IS NOT NULL')
+ .select('SUM(file.size)', 'sum')
+ .getRawOne();
+
+ return parseInt(sum, 10) || 0;
+ }
+
+ public packMany(
+ files: any[],
+ options?: {
+ detail?: boolean
+ self?: boolean,
+ withUser?: boolean,
+ }
+ ) {
+ return Promise.all(files.map(f => this.pack(f, options)));
+ }
+
+ public async pack(
+ src: DriveFile['id'] | DriveFile,
+ options?: {
+ detail?: boolean,
+ self?: boolean,
+ withUser?: boolean,
+ }
+ ) {
+ const opts = Object.assign({
+ detail: false,
+ self: false
+ }, options);
+
+ const file = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return await rap({
+ id: file.id,
+ createdAt: file.createdAt,
+ name: file.name,
+ type: file.type,
+ md5: file.md5,
+ size: file.size,
+ isSensitive: file.isSensitive,
+ properties: file.properties,
+ url: opts.self ? file.url : this.getPublicUrl(file, false),
+ thumbnailUrl: this.getPublicUrl(file, true),
+ folderId: file.folderId,
+ folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
+ detail: true
+ }) : null,
+ user: opts.withUser ? Users.pack(file.userId!) : null
+ });
+ }
+}
diff --git a/src/models/repositories/drive-folder.ts b/src/models/repositories/drive-folder.ts
new file mode 100644
index 0000000000..ce88adefa4
--- /dev/null
+++ b/src/models/repositories/drive-folder.ts
@@ -0,0 +1,50 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { DriveFolders, DriveFiles } from '..';
+import rap from '@prezzemolo/rap';
+import { DriveFolder } from '../entities/drive-folder';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(DriveFolder)
+export class DriveFolderRepository extends Repository {
+ public validateFolderName(name: string): boolean {
+ return (
+ (name.trim().length > 0) &&
+ (name.length <= 200)
+ );
+ }
+
+ public async pack(
+ src: DriveFolder['id'] | DriveFolder,
+ options?: {
+ detail: boolean
+ }
+ ): Promise> {
+ const opts = Object.assign({
+ detail: false
+ }, options);
+
+ const folder = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return await rap({
+ id: folder.id,
+ createdAt: folder.createdAt,
+ name: folder.name,
+ parentId: folder.parentId,
+
+ ...(opts.detail ? {
+ foldersCount: DriveFolders.count({
+ parentId: folder.id
+ }),
+ filesCount: DriveFiles.count({
+ folderId: folder.id
+ }),
+
+ ...(folder.parentId ? {
+ parent: this.pack(folder.parentId, {
+ detail: true
+ })
+ } : {})
+ } : {})
+ });
+ }
+}
diff --git a/src/models/repositories/follow-request.ts b/src/models/repositories/follow-request.ts
new file mode 100644
index 0000000000..451ed8e2d5
--- /dev/null
+++ b/src/models/repositories/follow-request.ts
@@ -0,0 +1,20 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { FollowRequest } from '../entities/follow-request';
+import { Users } from '..';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(FollowRequest)
+export class FollowRequestRepository extends Repository {
+ public async pack(
+ src: FollowRequest['id'] | FollowRequest,
+ me?: any
+ ) {
+ const request = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return {
+ id: request.id,
+ follower: await Users.pack(request.followerId, me),
+ followee: await Users.pack(request.followeeId, me),
+ };
+ }
+}
diff --git a/src/models/repositories/following.ts b/src/models/repositories/following.ts
new file mode 100644
index 0000000000..3fff57866f
--- /dev/null
+++ b/src/models/repositories/following.ts
@@ -0,0 +1,85 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { Following } from '../entities/following';
+import { ensure } from '../../prelude/ensure';
+
+type LocalFollowerFollowing = Following & {
+ followerHost: null;
+ followerInbox: null;
+ followerSharedInbox: null;
+};
+
+type RemoteFollowerFollowing = Following & {
+ followerHost: string;
+ followerInbox: string;
+ followerSharedInbox: string;
+};
+
+type LocalFolloweeFollowing = Following & {
+ followeeHost: null;
+ followeeInbox: null;
+ followeeSharedInbox: null;
+};
+
+type RemoteFolloweeFollowing = Following & {
+ followeeHost: string;
+ followeeInbox: string;
+ followeeSharedInbox: string;
+};
+
+@EntityRepository(Following)
+export class FollowingRepository extends Repository {
+ public isLocalFollower(following: Following): following is LocalFollowerFollowing {
+ return following.followerHost == null;
+ }
+
+ public isRemoteFollower(following: Following): following is RemoteFollowerFollowing {
+ return following.followerHost != null;
+ }
+
+ public isLocalFollowee(following: Following): following is LocalFolloweeFollowing {
+ return following.followeeHost == null;
+ }
+
+ public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing {
+ return following.followeeHost != null;
+ }
+
+ public packMany(
+ followings: any[],
+ me?: any,
+ opts?: {
+ populateFollowee?: boolean;
+ populateFollower?: boolean;
+ }
+ ) {
+ return Promise.all(followings.map(x => this.pack(x, me, opts)));
+ }
+
+ public async pack(
+ src: Following['id'] | Following,
+ me?: any,
+ opts?: {
+ populateFollowee?: boolean;
+ populateFollower?: boolean;
+ }
+ ) {
+ const following = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ if (opts == null) opts = {};
+
+ return await rap({
+ id: following.id,
+ createdAt: following.createdAt,
+ followeeId: following.followeeId,
+ followerId: following.followerId,
+ followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, {
+ detail: true
+ }) : null,
+ follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, {
+ detail: true
+ }) : null,
+ });
+ }
+}
diff --git a/src/models/repositories/games/reversi/game.ts b/src/models/repositories/games/reversi/game.ts
new file mode 100644
index 0000000000..c380f5251e
--- /dev/null
+++ b/src/models/repositories/games/reversi/game.ts
@@ -0,0 +1,50 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '../../..';
+import { ReversiGame } from '../../../entities/games/reversi/game';
+import { ensure } from '../../../../prelude/ensure';
+
+@EntityRepository(ReversiGame)
+export class ReversiGameRepository extends Repository {
+ public async pack(
+ src: ReversiGame['id'] | ReversiGame,
+ me?: any,
+ options?: {
+ detail?: boolean
+ }
+ ) {
+ const opts = Object.assign({
+ detail: true
+ }, options);
+
+ const game = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+ const meId = me ? typeof me === 'string' ? me : me.id : null;
+
+ return {
+ id: game.id,
+ createdAt: game.createdAt,
+ startedAt: game.startedAt,
+ isStarted: game.isStarted,
+ isEnded: game.isEnded,
+ form1: game.form1,
+ form2: game.form2,
+ user1Accepted: game.user1Accepted,
+ user2Accepted: game.user2Accepted,
+ user1Id: game.user1Id,
+ user2Id: game.user2Id,
+ user1: await Users.pack(game.user1Id, meId),
+ user2: await Users.pack(game.user2Id, meId),
+ winnerId: game.winnerId,
+ winner: game.winnerId ? await Users.pack(game.winnerId, meId) : null,
+ surrendered: game.surrendered,
+ black: game.black,
+ bw: game.bw,
+ isLlotheo: game.isLlotheo,
+ canPutEverywhere: game.canPutEverywhere,
+ loopedBoard: game.loopedBoard,
+ ...(opts.detail ? {
+ logs: game.logs,
+ map: game.map,
+ } : {})
+ };
+ }
+}
diff --git a/src/models/repositories/games/reversi/matching.ts b/src/models/repositories/games/reversi/matching.ts
new file mode 100644
index 0000000000..4d99c6ef76
--- /dev/null
+++ b/src/models/repositories/games/reversi/matching.ts
@@ -0,0 +1,28 @@
+import { EntityRepository, Repository } from 'typeorm';
+import rap from '@prezzemolo/rap';
+import { ReversiMatching } from '../../../entities/games/reversi/matching';
+import { Users } from '../../..';
+import { ensure } from '../../../../prelude/ensure';
+
+@EntityRepository(ReversiMatching)
+export class ReversiMatchingRepository extends Repository {
+ public async pack(
+ src: ReversiMatching['id'] | ReversiMatching,
+ me: any
+ ) {
+ const matching = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return await rap({
+ id: matching.id,
+ createdAt: matching.createdAt,
+ parentId: matching.parentId,
+ parent: Users.pack(matching.parentId, me, {
+ detail: true
+ }),
+ childId: matching.childId,
+ child: Users.pack(matching.childId, me, {
+ detail: true
+ })
+ });
+ }
+}
diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts
new file mode 100644
index 0000000000..6659273539
--- /dev/null
+++ b/src/models/repositories/messaging-message.ts
@@ -0,0 +1,38 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { MessagingMessage } from '../entities/messaging-message';
+import { Users, DriveFiles } from '..';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(MessagingMessage)
+export class MessagingMessageRepository extends Repository {
+ public isValidText(text: string): boolean {
+ return text.trim().length <= 1000 && text.trim() != '';
+ }
+
+ public async pack(
+ src: MessagingMessage['id'] | MessagingMessage,
+ me?: any,
+ options?: {
+ populateRecipient: boolean
+ }
+ ) {
+ const opts = options || {
+ populateRecipient: true
+ };
+
+ const message = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return {
+ id: message.id,
+ createdAt: message.createdAt,
+ text: message.text,
+ userId: message.userId,
+ user: await Users.pack(message.user || message.userId, me),
+ recipientId: message.recipientId,
+ recipient: opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : null,
+ fileId: message.fileId,
+ file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
+ isRead: message.isRead
+ };
+ }
+}
diff --git a/src/models/repositories/muting.ts b/src/models/repositories/muting.ts
new file mode 100644
index 0000000000..1812e2e713
--- /dev/null
+++ b/src/models/repositories/muting.ts
@@ -0,0 +1,29 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { Muting } from '../entities/muting';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(Muting)
+export class MutingRepository extends Repository {
+ public packMany(
+ mutings: any[],
+ me: any
+ ) {
+ return Promise.all(mutings.map(x => this.pack(x, me)));
+ }
+
+ public async pack(
+ src: Muting['id'] | Muting,
+ me?: any
+ ) {
+ const muting = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return await rap({
+ id: muting.id,
+ mutee: Users.pack(muting.muteeId, me, {
+ detail: true
+ })
+ });
+ }
+}
diff --git a/src/models/repositories/note-favorite.ts b/src/models/repositories/note-favorite.ts
new file mode 100644
index 0000000000..f428903c13
--- /dev/null
+++ b/src/models/repositories/note-favorite.ts
@@ -0,0 +1,26 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { NoteFavorite } from '../entities/note-favorite';
+import { Notes } from '..';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(NoteFavorite)
+export class NoteFavoriteRepository extends Repository {
+ public packMany(
+ favorites: any[],
+ me: any
+ ) {
+ return Promise.all(favorites.map(x => this.pack(x, me)));
+ }
+
+ public async pack(
+ src: NoteFavorite['id'] | NoteFavorite,
+ me?: any
+ ) {
+ const favorite = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return {
+ id: favorite.id,
+ note: await Notes.pack(favorite.note || favorite.noteId, me),
+ };
+ }
+}
diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts
new file mode 100644
index 0000000000..28191d4ab0
--- /dev/null
+++ b/src/models/repositories/note-reaction.ts
@@ -0,0 +1,19 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { NoteReaction } from '../entities/note-reaction';
+import { Users } from '..';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(NoteReaction)
+export class NoteReactionRepository extends Repository {
+ public async pack(
+ src: NoteReaction['id'] | NoteReaction,
+ me?: any
+ ) {
+ const reaction = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return {
+ id: reaction.id,
+ user: await Users.pack(reaction.userId, me),
+ };
+ }
+}
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
new file mode 100644
index 0000000000..7a48d24e13
--- /dev/null
+++ b/src/models/repositories/note.ts
@@ -0,0 +1,215 @@
+import { EntityRepository, Repository, In } from 'typeorm';
+import { Note } from '../entities/note';
+import { User } from '../entities/user';
+import { unique, concat } from '../../prelude/array';
+import { nyaize } from '../../misc/nyaize';
+import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
+import rap from '@prezzemolo/rap';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(Note)
+export class NoteRepository extends Repository {
+ public validateCw(x: string) {
+ return x.trim().length <= 100;
+ }
+
+ private async hideNote(packedNote: any, meId: User['id'] | null) {
+ let hide = false;
+
+ // visibility が specified かつ自分が指定されていなかったら非表示
+ if (packedNote.visibility == 'specified') {
+ if (meId == null) {
+ hide = true;
+ } else if (meId === packedNote.userId) {
+ hide = false;
+ } else {
+ // 指定されているかどうか
+ const specified = packedNote.visibleUserIds.some((id: any) => meId === id);
+
+ if (specified) {
+ hide = false;
+ } else {
+ hide = true;
+ }
+ }
+ }
+
+ // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
+ if (packedNote.visibility == 'followers') {
+ if (meId == null) {
+ hide = true;
+ } else if (meId === packedNote.userId) {
+ hide = false;
+ } else if (packedNote.reply && (meId === packedNote.reply.userId)) {
+ // 自分の投稿に対するリプライ
+ hide = false;
+ } else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId === id)) {
+ // 自分へのメンション
+ hide = false;
+ } else {
+ // フォロワーかどうか
+ const following = await Followings.findOne({
+ followeeId: packedNote.userId,
+ followerId: meId
+ });
+
+ if (following == null) {
+ hide = true;
+ } else {
+ hide = false;
+ }
+ }
+ }
+
+ if (hide) {
+ packedNote.visibleUserIds = null;
+ packedNote.fileIds = [];
+ packedNote.files = [];
+ packedNote.text = null;
+ packedNote.poll = null;
+ packedNote.cw = null;
+ packedNote.tags = [];
+ packedNote.geo = null;
+ packedNote.isHidden = true;
+ }
+ }
+
+ public packMany(
+ notes: (Note['id'] | Note)[],
+ me?: User['id'] | User | null | undefined,
+ options?: {
+ detail?: boolean;
+ skipHide?: boolean;
+ }
+ ) {
+ return Promise.all(notes.map(n => this.pack(n, me, options)));
+ }
+
+ public async pack(
+ src: Note['id'] | Note,
+ me?: User['id'] | User | null | undefined,
+ options?: {
+ detail?: boolean;
+ skipHide?: boolean;
+ }
+ ): Promise> {
+ const opts = Object.assign({
+ detail: true,
+ skipHide: false
+ }, options);
+
+ const meId = me ? typeof me === 'string' ? me : me.id : null;
+ const note = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+ const host = note.userHost;
+
+ async function populatePoll() {
+ const poll = await Polls.findOne({ noteId: note.id }).then(ensure);
+ const choices = poll.choices.map(c => ({
+ text: c,
+ votes: poll.votes[poll.choices.indexOf(c)],
+ isVoted: false
+ }));
+
+ if (poll.multiple) {
+ const votes = await PollVotes.find({
+ userId: meId!,
+ noteId: note.id
+ });
+
+ const myChoices = votes.map(v => v.choice);
+ for (const myChoice of myChoices) {
+ choices[myChoice].isVoted = true;
+ }
+ } else {
+ const vote = await PollVotes.findOne({
+ userId: meId!,
+ noteId: note.id
+ });
+
+ if (vote) {
+ choices[vote.choice].isVoted = true;
+ }
+ }
+
+ return {
+ multiple: poll.multiple,
+ expiresAt: poll.expiresAt,
+ choices
+ };
+ }
+
+ async function populateMyReaction() {
+ const reaction = await NoteReactions.findOne({
+ userId: meId!,
+ noteId: note.id,
+ });
+
+ if (reaction) {
+ return reaction.reaction;
+ }
+
+ return undefined;
+ }
+
+ let text = note.text;
+
+ if (note.name) {
+ text = `【${note.name}】\n${note.text}`;
+ }
+
+ const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)]));
+
+ const packed = await rap({
+ id: note.id,
+ createdAt: note.createdAt,
+ app: note.appId ? Apps.pack(note.appId) : undefined,
+ userId: note.userId,
+ user: Users.pack(note.user || note.userId, meId),
+ text: text,
+ cw: note.cw,
+ visibility: note.visibility,
+ localOnly: note.localOnly || undefined,
+ visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
+ viaMobile: note.viaMobile || undefined,
+ renoteCount: note.renoteCount,
+ repliesCount: note.repliesCount,
+ reactions: note.reactions,
+ emojis: reactionEmojis.length > 0 ? Emojis.find({
+ name: In(reactionEmojis),
+ host: host
+ }) : [],
+ tags: note.tags,
+ fileIds: note.fileIds,
+ files: DriveFiles.packMany(note.fileIds),
+ replyId: note.replyId,
+ renoteId: note.renoteId,
+ uri: note.uri,
+
+ ...(opts.detail ? {
+ reply: note.replyId ? this.pack(note.replyId, meId, {
+ detail: false
+ }) : undefined,
+
+ renote: note.renoteId ? this.pack(note.renoteId, meId, {
+ detail: true
+ }) : undefined,
+
+ poll: note.hasPoll ? populatePoll() : undefined,
+
+ ...(meId ? {
+ myReaction: populateMyReaction()
+ } : {})
+ } : {})
+ });
+
+ if (packed.user.isCat && packed.text) {
+ packed.text = nyaize(packed.text);
+ }
+
+ if (!opts.skipHide) {
+ await this.hideNote(packed, meId);
+ }
+
+ return packed;
+ }
+}
diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts
new file mode 100644
index 0000000000..4781d4c065
--- /dev/null
+++ b/src/models/repositories/notification.ts
@@ -0,0 +1,48 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users, Notes } from '..';
+import rap from '@prezzemolo/rap';
+import { Notification } from '../entities/notification';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(Notification)
+export class NotificationRepository extends Repository {
+ public packMany(
+ notifications: any[],
+ ) {
+ return Promise.all(notifications.map(x => this.pack(x)));
+ }
+
+ public async pack(
+ src: Notification['id'] | Notification,
+ ) {
+ const notification = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return await rap({
+ id: notification.id,
+ createdAt: notification.createdAt,
+ type: notification.type,
+ userId: notification.notifierId,
+ user: Users.pack(notification.notifier || notification.notifierId),
+ ...(notification.type === 'mention' ? {
+ note: Notes.pack(notification.note || notification.noteId!),
+ } : {}),
+ ...(notification.type === 'reply' ? {
+ note: Notes.pack(notification.note || notification.noteId!),
+ } : {}),
+ ...(notification.type === 'renote' ? {
+ note: Notes.pack(notification.note || notification.noteId!),
+ } : {}),
+ ...(notification.type === 'quote' ? {
+ note: Notes.pack(notification.note || notification.noteId!),
+ } : {}),
+ ...(notification.type === 'reaction' ? {
+ note: Notes.pack(notification.note || notification.noteId!),
+ reaction: notification.reaction
+ } : {}),
+ ...(notification.type === 'pollVote' ? {
+ note: Notes.pack(notification.note || notification.noteId!),
+ choice: notification.choice
+ } : {})
+ });
+ }
+}
diff --git a/src/models/repositories/signin.ts b/src/models/repositories/signin.ts
new file mode 100644
index 0000000000..f5b90c0e9e
--- /dev/null
+++ b/src/models/repositories/signin.ts
@@ -0,0 +1,11 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Signin } from '../entities/signin';
+
+@EntityRepository(Signin)
+export class SigninRepository extends Repository {
+ public async pack(
+ src: any,
+ ) {
+ return src;
+ }
+}
diff --git a/src/models/repositories/user-list.ts b/src/models/repositories/user-list.ts
new file mode 100644
index 0000000000..e591794b8b
--- /dev/null
+++ b/src/models/repositories/user-list.ts
@@ -0,0 +1,23 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { UserList } from '../entities/user-list';
+import { ensure } from '../../prelude/ensure';
+import { UserListJoinings } from '..';
+
+@EntityRepository(UserList)
+export class UserListRepository extends Repository {
+ public async pack(
+ src: UserList['id'] | UserList,
+ ) {
+ const userList = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ const users = await UserListJoinings.find({
+ userListId: userList.id
+ });
+
+ return {
+ id: userList.id,
+ name: userList.name,
+ userIds: users.map(x => x.userId)
+ };
+ }
+}
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
new file mode 100644
index 0000000000..7e67ead72a
--- /dev/null
+++ b/src/models/repositories/user.ts
@@ -0,0 +1,213 @@
+import { EntityRepository, Repository, In } from 'typeorm';
+import { User, ILocalUser, IRemoteUser } from '../entities/user';
+import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..';
+import rap from '@prezzemolo/rap';
+import { ensure } from '../../prelude/ensure';
+
+@EntityRepository(User)
+export class UserRepository extends Repository {
+ public async getRelation(me: User['id'], target: User['id']) {
+ const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
+ Followings.findOne({
+ followerId: me,
+ followeeId: target
+ }),
+ Followings.findOne({
+ followerId: target,
+ followeeId: me
+ }),
+ FollowRequests.findOne({
+ followerId: me,
+ followeeId: target
+ }),
+ FollowRequests.findOne({
+ followerId: target,
+ followeeId: me
+ }),
+ Blockings.findOne({
+ blockerId: me,
+ blockeeId: target
+ }),
+ Blockings.findOne({
+ blockerId: target,
+ blockeeId: me
+ }),
+ Mutings.findOne({
+ muterId: me,
+ muteeId: target
+ })
+ ]);
+
+ return {
+ id: target,
+ isFollowing: following1 != null,
+ hasPendingFollowRequestFromYou: followReq1 != null,
+ hasPendingFollowRequestToYou: followReq2 != null,
+ isFollowed: following2 != null,
+ isBlocking: toBlocking != null,
+ isBlocked: fromBlocked != null,
+ isMuted: mute != null
+ };
+ }
+
+ public packMany(
+ users: (User['id'] | User)[],
+ me?: User['id'] | User | null | undefined,
+ options?: {
+ detail?: boolean,
+ includeSecrets?: boolean,
+ includeHasUnreadNotes?: boolean
+ }
+ ) {
+ return Promise.all(users.map(u => this.pack(u, me, options)));
+ }
+
+ public async pack(
+ src: User['id'] | User,
+ me?: User['id'] | User | null | undefined,
+ options?: {
+ detail?: boolean,
+ includeSecrets?: boolean,
+ includeHasUnreadNotes?: boolean
+ }
+ ): Promise> {
+ const opts = Object.assign({
+ detail: false,
+ includeSecrets: false
+ }, options);
+
+ const user = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+ const meId = me ? typeof me === 'string' ? me : me.id : null;
+
+ const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
+ const pins = opts.detail ? await UserNotePinings.find({ userId: user.id }) : [];
+ const profile = opts.detail ? await UserProfiles.findOne({ userId: user.id }).then(ensure) : null;
+
+ return await rap({
+ id: user.id,
+ name: user.name,
+ username: user.username,
+ host: user.host,
+ avatarUrl: user.avatarUrl,
+ avatarColor: user.avatarColor,
+ isAdmin: user.isAdmin || undefined,
+ isBot: user.isBot || undefined,
+ isCat: user.isCat || undefined,
+ isVerified: user.isVerified || undefined,
+
+ // カスタム絵文字添付
+ emojis: user.emojis.length > 0 ? Emojis.find({
+ where: {
+ name: In(user.emojis),
+ host: user.host
+ },
+ select: ['name', 'host', 'url', 'aliases']
+ }) : [],
+
+ ...(opts.includeHasUnreadNotes ? {
+ hasUnreadSpecifiedNotes: NoteUnreads.count({
+ where: { userId: user.id, isSpecified: true },
+ take: 1
+ }).then(count => count > 0),
+ hasUnreadMentions: NoteUnreads.count({
+ where: { userId: user.id },
+ take: 1
+ }).then(count => count > 0),
+ } : {}),
+
+ ...(opts.detail ? {
+ url: profile!.url,
+ createdAt: user.createdAt,
+ updatedAt: user.updatedAt,
+ bannerUrl: user.bannerUrl,
+ bannerColor: user.bannerColor,
+ isLocked: user.isLocked,
+ description: profile!.description,
+ location: profile!.location,
+ birthday: profile!.birthday,
+ followersCount: user.followersCount,
+ followingCount: user.followingCount,
+ notesCount: user.notesCount,
+ pinnedNoteIds: pins.map(pin => pin.noteId),
+ pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, {
+ detail: true
+ }),
+ } : {}),
+
+ ...(opts.detail && meId === user.id ? {
+ avatarId: user.avatarId,
+ bannerId: user.bannerId,
+ autoWatch: profile!.autoWatch,
+ alwaysMarkNsfw: profile!.alwaysMarkNsfw,
+ carefulBot: profile!.carefulBot,
+ hasUnreadMessagingMessage: MessagingMessages.count({
+ where: {
+ recipientId: user.id,
+ isRead: false
+ },
+ take: 1
+ }).then(count => count > 0),
+ hasUnreadNotification: Notifications.count({
+ where: {
+ notifieeId: user.id,
+ isRead: false
+ },
+ take: 1
+ }).then(count => count > 0),
+ pendingReceivedFollowRequestsCount: FollowRequests.count({
+ followeeId: user.id
+ }),
+ } : {}),
+
+ ...(opts.includeSecrets ? {
+ clientData: profile!.clientData,
+ email: profile!.email,
+ emailVerified: profile!.emailVerified,
+ } : {}),
+
+ ...(relation ? {
+ isFollowing: relation.isFollowing,
+ isFollowed: relation.isFollowed,
+ hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
+ hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
+ isBlocking: relation.isBlocking,
+ isBlocked: relation.isBlocked,
+ isMuted: relation.isMuted,
+ } : {})
+ });
+ }
+
+ public isLocalUser(user: User): user is ILocalUser {
+ return user.host == null;
+ }
+
+ public isRemoteUser(user: User): user is IRemoteUser {
+ return !this.isLocalUser(user);
+ }
+
+ //#region Validators
+ public validateUsername(username: string, remote = false): boolean {
+ return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
+ }
+
+ public validatePassword(password: string): boolean {
+ return typeof password == 'string' && password != '';
+ }
+
+ public isValidName(name?: string): boolean {
+ return name === null || (typeof name == 'string' && name.length < 50 && name.trim() != '');
+ }
+
+ public isValidDescription(description: string): boolean {
+ return typeof description == 'string' && description.length < 500 && description.trim() != '';
+ }
+
+ public isValidLocation(location: string): boolean {
+ return typeof location == 'string' && location.length < 50 && location.trim() != '';
+ }
+
+ public isValidBirthday(birthday: string): boolean {
+ return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
+ }
+ //#endregion
+}
diff --git a/src/models/signin.ts b/src/models/signin.ts
deleted file mode 100644
index d8b05c0e30..0000000000
--- a/src/models/signin.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-
-const Signin = db.get('signin');
-export default Signin;
-
-export interface ISignin {
- _id: mongo.ObjectID;
- createdAt: Date;
- userId: mongo.ObjectID;
- ip: string;
- headers: any;
- success: boolean;
-}
-
-/**
- * Pack a signin record for API response
- *
- * @param {any} record
- * @return {Promise}
- */
-export const pack = (
- record: any
-) => new Promise(async (resolve, reject) => {
-
- const _record = deepcopy(record);
-
- // Rename _id to id
- _record.id = _record._id;
- delete _record._id;
-
- resolve(_record);
-});
diff --git a/src/models/sw-subscription.ts b/src/models/sw-subscription.ts
deleted file mode 100644
index 743d0d2dd9..0000000000
--- a/src/models/sw-subscription.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const SwSubscription = db.get('swSubscriptions');
-export default SwSubscription;
-
-export interface ISwSubscription {
- _id: mongo.ObjectID;
- userId: mongo.ObjectID;
- endpoint: string;
- auth: string;
- publickey: string;
-}
diff --git a/src/models/user-list.ts b/src/models/user-list.ts
deleted file mode 100644
index e7dd74bdd1..0000000000
--- a/src/models/user-list.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-
-const UserList = db.get('userList');
-export default UserList;
-
-export interface IUserList {
- _id: mongo.ObjectID;
- createdAt: Date;
- title: string;
- userId: mongo.ObjectID;
- userIds: mongo.ObjectID[];
-}
-
-export const pack = (
- userList: string | mongo.ObjectID | IUserList
-) => new Promise(async (resolve, reject) => {
- let _userList: any;
-
- if (isObjectId(userList)) {
- _userList = await UserList.findOne({
- _id: userList
- });
- } else if (typeof userList === 'string') {
- _userList = await UserList.findOne({
- _id: new mongo.ObjectID(userList)
- });
- } else {
- _userList = deepcopy(userList);
- }
-
- if (!_userList) throw `invalid userList arg ${userList}`;
-
- // Rename _id to id
- _userList.id = _userList._id;
- delete _userList._id;
-
- resolve(_userList);
-});
diff --git a/src/models/user.ts b/src/models/user.ts
deleted file mode 100644
index 0c3f7b5508..0000000000
--- a/src/models/user.ts
+++ /dev/null
@@ -1,438 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import rap from '@prezzemolo/rap';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { packMany as packNoteMany } from './note';
-import Following from './following';
-import Blocking from './blocking';
-import Mute from './mute';
-import { getFriendIds } from '../server/api/common/get-friends';
-import config from '../config';
-import FollowRequest from './follow-request';
-import fetchMeta from '../misc/fetch-meta';
-import Emoji from './emoji';
-import { dbLogger } from '../db/logger';
-
-const User = db.get('users');
-
-User.createIndex('createdAt');
-User.createIndex('updatedAt');
-User.createIndex('followersCount');
-User.createIndex('tags');
-User.createIndex('isSuspended');
-User.createIndex('username');
-User.createIndex('usernameLower');
-User.createIndex('host');
-User.createIndex(['username', 'host'], { unique: true });
-User.createIndex(['usernameLower', 'host'], { unique: true });
-User.createIndex('token', { sparse: true, unique: true });
-User.createIndex('uri', { sparse: true, unique: true });
-
-export default User;
-
-type IUserBase = {
- _id: mongo.ObjectID;
- createdAt: Date;
- updatedAt?: Date;
- deletedAt?: Date;
- followersCount: number;
- followingCount: number;
- name?: string;
- notesCount: number;
- username: string;
- usernameLower: string;
- avatarId: mongo.ObjectID;
- bannerId: mongo.ObjectID;
- avatarUrl?: string;
- bannerUrl?: string;
- avatarColor?: any;
- bannerColor?: any;
- wallpaperId: mongo.ObjectID;
- wallpaperUrl?: string;
- data: any;
- description: string;
- lang?: string;
- pinnedNoteIds: mongo.ObjectID[];
- emojis?: string[];
- tags?: string[];
-
- isDeleted: boolean;
-
- /**
- * 凍結されているか否か
- */
- isSuspended: boolean;
-
- /**
- * サイレンスされているか否か
- */
- isSilenced: boolean;
-
- /**
- * 鍵アカウントか否か
- */
- isLocked: boolean;
-
- /**
- * Botか否か
- */
- isBot: boolean;
-
- /**
- * Botからのフォローを承認制にするか
- */
- carefulBot: boolean;
-
- /**
- * フォローしているユーザーからのフォローリクエストを自動承認するか
- */
- autoAcceptFollowed: boolean;
-
- /**
- * このアカウントに届いているフォローリクエストの数
- */
- pendingReceivedFollowRequestsCount: number;
-
- host: string;
-};
-
-export interface ILocalUser extends IUserBase {
- host: null;
- keypair: string;
- email: string;
- emailVerified?: boolean;
- emailVerifyCode?: string;
- password: string;
- token: string;
- twitter: {
- accessToken: string;
- accessTokenSecret: string;
- userId: string;
- screenName: string;
- };
- github: {
- accessToken: string;
- id: string;
- login: string;
- };
- discord: {
- accessToken: string;
- refreshToken: string;
- expiresDate: number;
- id: string;
- username: string;
- discriminator: string;
- };
- profile: {
- location: string;
- birthday: string; // 'YYYY-MM-DD'
- tags: string[];
- };
- fields?: {
- name: string;
- value: string;
- }[];
- isCat: boolean;
- isAdmin?: boolean;
- isModerator?: boolean;
- isVerified?: boolean;
- twoFactorSecret: string;
- twoFactorEnabled: boolean;
- twoFactorTempSecret?: string;
- clientSettings: any;
- settings: {
- autoWatch: boolean;
- alwaysMarkNsfw?: boolean;
- };
- hasUnreadNotification: boolean;
- hasUnreadMessagingMessage: boolean;
-}
-
-export interface IRemoteUser extends IUserBase {
- inbox: string;
- sharedInbox?: string;
- featured?: string;
- endpoints: string[];
- uri: string;
- url?: string;
- publicKey: {
- id: string;
- publicKeyPem: string;
- };
- lastFetchedAt: Date;
- isAdmin: false;
- isModerator: false;
-}
-
-export type IUser = ILocalUser | IRemoteUser;
-
-export const isLocalUser = (user: any): user is ILocalUser =>
- user.host === null;
-
-export const isRemoteUser = (user: any): user is IRemoteUser =>
- !isLocalUser(user);
-
-//#region Validators
-export function validateUsername(username: string, remote = false): boolean {
- return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
-}
-
-export function validatePassword(password: string): boolean {
- return typeof password == 'string' && password != '';
-}
-
-export function isValidName(name?: string): boolean {
- return name === null || (typeof name == 'string' && name.length < 50 && name.trim() != '');
-}
-
-export function isValidDescription(description: string): boolean {
- return typeof description == 'string' && description.length < 500 && description.trim() != '';
-}
-
-export function isValidLocation(location: string): boolean {
- return typeof location == 'string' && location.length < 50 && location.trim() != '';
-}
-
-export function isValidBirthday(birthday: string): boolean {
- return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
-}
-//#endregion
-
-export async function getRelation(me: mongo.ObjectId, target: mongo.ObjectId) {
- const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
- Following.findOne({
- followerId: me,
- followeeId: target
- }),
- Following.findOne({
- followerId: target,
- followeeId: me
- }),
- FollowRequest.findOne({
- followerId: me,
- followeeId: target
- }),
- FollowRequest.findOne({
- followerId: target,
- followeeId: me
- }),
- Blocking.findOne({
- blockerId: me,
- blockeeId: target
- }),
- Blocking.findOne({
- blockerId: target,
- blockeeId: me
- }),
- Mute.findOne({
- muterId: me,
- muteeId: target
- })
- ]);
-
- return {
- id: target,
- isFollowing: following1 !== null,
- hasPendingFollowRequestFromYou: followReq1 !== null,
- hasPendingFollowRequestToYou: followReq2 !== null,
- isFollowed: following2 !== null,
- isBlocking: toBlocking !== null,
- isBlocked: fromBlocked !== null,
- isMuted: mute !== null
- };
-}
-
-/**
- * Pack a user for API response
- *
- * @param user target
- * @param me? serializee
- * @param options? serialize options
- * @return Packed user
- */
-export const pack = (
- user: string | mongo.ObjectID | IUser,
- me?: string | mongo.ObjectID | IUser,
- options?: {
- detail?: boolean,
- includeSecrets?: boolean,
- includeHasUnreadNotes?: boolean
- }
-) => new Promise(async (resolve, reject) => {
- const opts = Object.assign({
- detail: false,
- includeSecrets: false
- }, options);
-
- let _user: any;
-
- const fields = opts.detail ? {} : {
- name: true,
- username: true,
- host: true,
- avatarColor: true,
- avatarUrl: true,
- emojis: true,
- isCat: true,
- isBot: true,
- isAdmin: true,
- isVerified: true
- };
-
- // Populate the user if 'user' is ID
- if (isObjectId(user)) {
- _user = await User.findOne({
- _id: user
- }, { fields });
- } else if (typeof user === 'string') {
- _user = await User.findOne({
- _id: new mongo.ObjectID(user)
- }, { fields });
- } else {
- _user = deepcopy(user);
- }
-
- // (データベースの欠損などで)ユーザーがデータベース上に見つからなかったとき
- if (_user == null) {
- dbLogger.warn(`user not found on database: ${user}`);
- return resolve(null);
- }
-
- // Me
- const meId: mongo.ObjectID = me
- ? isObjectId(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- // Rename _id to id
- _user.id = _user._id;
- delete _user._id;
-
- delete _user.usernameLower;
- delete _user.emailVerifyCode;
-
- if (_user.host == null) {
- // Remove private properties
- delete _user.keypair;
- delete _user.password;
- delete _user.token;
- delete _user.twoFactorTempSecret;
- delete _user.two_factor_temp_secret; // 後方互換性のため
- delete _user.twoFactorSecret;
- if (_user.twitter) {
- delete _user.twitter.accessToken;
- delete _user.twitter.accessTokenSecret;
- }
- if (_user.github) {
- delete _user.github.accessToken;
- }
- if (_user.discord) {
- delete _user.discord.accessToken;
- delete _user.discord.refreshToken;
- delete _user.discord.expiresDate;
- }
-
- // Visible via only the official client
- if (!opts.includeSecrets) {
- delete _user.email;
- delete _user.emailVerified;
- delete _user.settings;
- delete _user.clientSettings;
- }
-
- if (!opts.detail) {
- delete _user.twoFactorEnabled;
- }
- } else {
- delete _user.publicKey;
- }
-
- if (_user.avatarUrl == null) {
- _user.avatarUrl = `${config.driveUrl}/default-avatar.jpg`;
- }
-
- if (!meId || !meId.equals(_user.id) || !opts.detail) {
- delete _user.avatarId;
- delete _user.bannerId;
- delete _user.hasUnreadMessagingMessage;
- delete _user.hasUnreadNotification;
- }
-
- if (meId && !meId.equals(_user.id) && opts.detail) {
- const relation = await getRelation(meId, _user.id);
-
- _user.isFollowing = relation.isFollowing;
- _user.isFollowed = relation.isFollowed;
- _user.hasPendingFollowRequestFromYou = relation.hasPendingFollowRequestFromYou;
- _user.hasPendingFollowRequestToYou = relation.hasPendingFollowRequestToYou;
- _user.isBlocking = relation.isBlocking;
- _user.isBlocked = relation.isBlocked;
- _user.isMuted = relation.isMuted;
- }
-
- if (opts.detail) {
- if (_user.pinnedNoteIds) {
- // Populate pinned notes
- _user.pinnedNotes = packNoteMany(_user.pinnedNoteIds, meId, {
- detail: true
- });
- }
-
- if (meId && !meId.equals(_user.id)) {
- const myFollowingIds = await getFriendIds(meId);
-
- // Get following you know count
- _user.followingYouKnowCount = Following.count({
- followeeId: { $in: myFollowingIds },
- followerId: _user.id
- });
-
- // Get followers you know count
- _user.followersYouKnowCount = Following.count({
- followeeId: _user.id,
- followerId: { $in: myFollowingIds }
- });
- }
- }
-
- if (!opts.includeHasUnreadNotes) {
- delete _user.hasUnreadSpecifiedNotes;
- delete _user.hasUnreadMentions;
- }
-
- // カスタム絵文字添付
- if (_user.emojis) {
- _user.emojis = Emoji.find({
- name: { $in: _user.emojis },
- host: _user.host
- }, {
- fields: { _id: false }
- });
- }
-
- // resolve promises in _user object
- _user = await rap(_user);
-
- resolve(_user);
-});
-
-/*
-function img(url) {
- return {
- thumbnail: {
- large: `${url}`,
- medium: '',
- small: ''
- }
- };
-}
-*/
-
-export async function fetchProxyAccount(): Promise {
- const meta = await fetchMeta();
- return await User.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser;
-}
diff --git a/src/prelude/ensure.ts b/src/prelude/ensure.ts
new file mode 100644
index 0000000000..1af281c056
--- /dev/null
+++ b/src/prelude/ensure.ts
@@ -0,0 +1,10 @@
+/**
+ * 値が null または undefined の場合はエラーを発生させ、そうでない場合は値をそのまま返します
+ */
+export function ensure(x: T): NonNullable {
+ if (x == null) {
+ throw new Error('ぬるぽ');
+ } else {
+ return x!;
+ }
+}
diff --git a/src/prelude/maybe.ts b/src/prelude/maybe.ts
index f9ac95c0b5..0b4b543ca5 100644
--- a/src/prelude/maybe.ts
+++ b/src/prelude/maybe.ts
@@ -1,19 +1,19 @@
-export interface Maybe {
- isJust(): this is Just;
+export interface IMaybe {
+ isJust(): this is IJust;
}
-export type Just = Maybe & {
- get(): T
-};
+export interface IJust extends IMaybe {
+ get(): T;
+}
-export function just(value: T): Just {
+export function just(value: T): IJust {
return {
isJust: () => true,
get: () => value
};
}
-export function nothing(): Maybe {
+export function nothing(): IMaybe {
return {
isJust: () => false,
};
diff --git a/src/queue/index.ts b/src/queue/index.ts
index d8328a1d57..a010004f15 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -2,17 +2,17 @@ import * as Queue from 'bull';
import * as httpSignature from 'http-signature';
import config from '../config';
-import { ILocalUser } from '../models/user';
+import { ILocalUser } from '../models/entities/user';
import { program } from '../argv';
import processDeliver from './processors/deliver';
import processInbox from './processors/inbox';
import processDb from './processors/db';
import { queueLogger } from './logger';
-import { IDriveFile } from '../models/drive-file';
+import { DriveFile } from '../models/entities/drive-file';
function initializeQueue(name: string) {
- return new Queue(name, config.redis != null ? {
+ return new Queue(name, {
redis: {
port: config.redis.port,
host: config.redis.host,
@@ -20,7 +20,15 @@ function initializeQueue(name: string) {
db: config.redis.db || 0,
},
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue'
- } : null);
+ });
+}
+
+function renderError(e: Error): any {
+ return {
+ stack: e.stack,
+ message: e.message,
+ name: e.name
+ };
}
export const deliverQueue = initializeQueue('deliver');
@@ -34,16 +42,16 @@ deliverQueue
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => deliverLogger.debug(`active id=${job.id} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) id=${job.id} to=${job.data.to}`))
- .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) id=${job.id} to=${job.data.to}`))
- .on('error', (error) => deliverLogger.error(`error ${error}`))
+ .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) id=${job.id} to=${job.data.to}`, { job, e: renderError(err) }))
+ .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) }))
.on('stalled', (job) => deliverLogger.warn(`stalled id=${job.id} to=${job.data.to}`));
inboxQueue
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => inboxLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`))
- .on('error', (error) => inboxLogger.error(`error ${error}`))
+ .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) }))
+ .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) }))
.on('stalled', (job) => inboxLogger.warn(`stalled id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
export function deliver(user: ILocalUser, content: any, to: any) {
@@ -83,15 +91,6 @@ export function inbox(activity: any, signature: httpSignature.IParsedSignature)
});
}
-export function createDeleteNotesJob(user: ILocalUser) {
- return dbQueue.add('deleteNotes', {
- user: user
- }, {
- removeOnComplete: true,
- removeOnFail: true
- });
-}
-
export function createDeleteDriveFilesJob(user: ILocalUser) {
return dbQueue.add('deleteDriveFiles', {
user: user
@@ -146,7 +145,7 @@ export function createExportUserListsJob(user: ILocalUser) {
});
}
-export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_id']) {
+export function createImportFollowingJob(user: ILocalUser, fileId: DriveFile['id']) {
return dbQueue.add('importFollowing', {
user: user,
fileId: fileId
@@ -156,7 +155,7 @@ export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_
});
}
-export function createImportUserListsJob(user: ILocalUser, fileId: IDriveFile['_id']) {
+export function createImportUserListsJob(user: ILocalUser, fileId: DriveFile['id']) {
return dbQueue.add('importUserLists', {
user: user,
fileId: fileId
diff --git a/src/queue/processors/db/delete-drive-files.ts b/src/queue/processors/db/delete-drive-files.ts
index 3de960a25e..4e4eab86b7 100644
--- a/src/queue/processors/db/delete-drive-files.ts
+++ b/src/queue/processors/db/delete-drive-files.ts
@@ -1,55 +1,55 @@
import * as Bull from 'bull';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
-import User from '../../../models/user';
-import DriveFile from '../../../models/drive-file';
import deleteFile from '../../../services/drive/delete-file';
+import { Users, DriveFiles } from '../../../models';
+import { MoreThan } from 'typeorm';
const logger = queueLogger.createSubLogger('delete-drive-files');
export async function deleteDriveFiles(job: Bull.Job, done: any): Promise {
- logger.info(`Deleting drive files of ${job.data.user._id} ...`);
+ logger.info(`Deleting drive files of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
- });
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
let deletedCount = 0;
- let ended = false;
let cursor: any = null;
- while (!ended) {
- const files = await DriveFile.find({
- userId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
+ while (true) {
+ const files = await DriveFiles.find({
+ where: {
+ userId: user.id,
+ ...(cursor ? { id: MoreThan(cursor) } : {})
+ },
+ take: 100,
+ order: {
+ id: 1
}
});
if (files.length === 0) {
- ended = true;
job.progress(100);
break;
}
- cursor = files[files.length - 1]._id;
+ cursor = files[files.length - 1].id;
for (const file of files) {
await deleteFile(file);
deletedCount++;
}
- const total = await DriveFile.count({
- userId: user._id,
+ const total = await DriveFiles.count({
+ userId: user.id,
});
job.progress(deletedCount / total);
}
- logger.succ(`All drive files (${deletedCount}) of ${user._id} has been deleted.`);
+ logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`);
done();
}
diff --git a/src/queue/processors/db/delete-notes.ts b/src/queue/processors/db/delete-notes.ts
deleted file mode 100644
index 021db8062e..0000000000
--- a/src/queue/processors/db/delete-notes.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import * as Bull from 'bull';
-import * as mongo from 'mongodb';
-
-import { queueLogger } from '../../logger';
-import Note from '../../../models/note';
-import deleteNote from '../../../services/note/delete';
-import User from '../../../models/user';
-
-const logger = queueLogger.createSubLogger('delete-notes');
-
-export async function deleteNotes(job: Bull.Job, done: any): Promise {
- logger.info(`Deleting notes of ${job.data.user._id} ...`);
-
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
- });
-
- let deletedCount = 0;
- let ended = false;
- let cursor: any = null;
-
- while (!ended) {
- const notes = await Note.find({
- userId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
- }
- });
-
- if (notes.length === 0) {
- ended = true;
- job.progress(100);
- break;
- }
-
- cursor = notes[notes.length - 1]._id;
-
- for (const note of notes) {
- await deleteNote(user, note, true);
- deletedCount++;
- }
-
- const total = await Note.count({
- userId: user._id,
- });
-
- job.progress(deletedCount / total);
- }
-
- logger.succ(`All notes (${deletedCount}) of ${user._id} has been deleted.`);
- done();
-}
diff --git a/src/queue/processors/db/export-blocking.ts b/src/queue/processors/db/export-blocking.ts
index 7f32c06472..c4b8c9438d 100644
--- a/src/queue/processors/db/export-blocking.ts
+++ b/src/queue/processors/db/export-blocking.ts
@@ -1,23 +1,24 @@
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
import dateFormat = require('dateformat');
-import Blocking from '../../../models/blocking';
import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, Blockings } from '../../../models';
+import { MoreThan } from 'typeorm';
const logger = queueLogger.createSubLogger('export-blocking');
export async function exportBlocking(job: Bull.Job, done: any): Promise {
- logger.info(`Exporting blocking of ${job.data.user._id} ...`);
+ logger.info(`Exporting blocking of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
- });
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
// Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
@@ -32,30 +33,33 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise {
const stream = fs.createWriteStream(path, { flags: 'a' });
let exportedCount = 0;
- let ended = false;
let cursor: any = null;
- while (!ended) {
- const blockings = await Blocking.find({
- blockerId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
+ while (true) {
+ const blockings = await Blockings.find({
+ where: {
+ blockerId: user.id,
+ ...(cursor ? { id: MoreThan(cursor) } : {})
+ },
+ take: 100,
+ order: {
+ id: 1
}
});
if (blockings.length === 0) {
- ended = true;
job.progress(100);
break;
}
- cursor = blockings[blockings.length - 1]._id;
+ cursor = blockings[blockings.length - 1].id;
for (const block of blockings) {
- const u = await User.findOne({ _id: block.blockeeId }, { fields: { username: true, host: true } });
+ const u = await Users.findOne({ id: block.blockeeId });
+ if (u == null) {
+ exportedCount++; continue;
+ }
+
const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
@@ -70,8 +74,8 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise {
exportedCount++;
}
- const total = await Blocking.count({
- blockerId: user._id,
+ const total = await Blockings.count({
+ blockerId: user.id,
});
job.progress(exportedCount / total);
@@ -83,7 +87,7 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise {
const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
- logger.succ(`Exported to: ${driveFile._id}`);
+ logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done();
}
diff --git a/src/queue/processors/db/export-following.ts b/src/queue/processors/db/export-following.ts
index 019414072a..9fab5bb21a 100644
--- a/src/queue/processors/db/export-following.ts
+++ b/src/queue/processors/db/export-following.ts
@@ -1,23 +1,24 @@
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
import dateFormat = require('dateformat');
-import Following from '../../../models/following';
import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, Followings } from '../../../models';
+import { MoreThan } from 'typeorm';
const logger = queueLogger.createSubLogger('export-following');
export async function exportFollowing(job: Bull.Job, done: any): Promise {
- logger.info(`Exporting following of ${job.data.user._id} ...`);
+ logger.info(`Exporting following of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
- });
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
// Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
@@ -32,30 +33,33 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise {
const stream = fs.createWriteStream(path, { flags: 'a' });
let exportedCount = 0;
- let ended = false;
let cursor: any = null;
- while (!ended) {
- const followings = await Following.find({
- followerId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
+ while (true) {
+ const followings = await Followings.find({
+ where: {
+ followerId: user.id,
+ ...(cursor ? { id: MoreThan(cursor) } : {})
+ },
+ take: 100,
+ order: {
+ id: 1
}
});
if (followings.length === 0) {
- ended = true;
job.progress(100);
break;
}
- cursor = followings[followings.length - 1]._id;
+ cursor = followings[followings.length - 1].id;
for (const following of followings) {
- const u = await User.findOne({ _id: following.followeeId }, { fields: { username: true, host: true } });
+ const u = await Users.findOne({ id: following.followeeId });
+ if (u == null) {
+ exportedCount++; continue;
+ }
+
const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
@@ -70,8 +74,8 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise {
exportedCount++;
}
- const total = await Following.count({
- followerId: user._id,
+ const total = await Followings.count({
+ followerId: user.id,
});
job.progress(exportedCount / total);
@@ -83,7 +87,7 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise {
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
- logger.succ(`Exported to: ${driveFile._id}`);
+ logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done();
}
diff --git a/src/queue/processors/db/export-mute.ts b/src/queue/processors/db/export-mute.ts
index 5ded7cf651..b957b48b20 100644
--- a/src/queue/processors/db/export-mute.ts
+++ b/src/queue/processors/db/export-mute.ts
@@ -1,23 +1,24 @@
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
import dateFormat = require('dateformat');
-import Mute from '../../../models/mute';
import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, Mutings } from '../../../models';
+import { MoreThan } from 'typeorm';
const logger = queueLogger.createSubLogger('export-mute');
export async function exportMute(job: Bull.Job, done: any): Promise {
- logger.info(`Exporting mute of ${job.data.user._id} ...`);
+ logger.info(`Exporting mute of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
- });
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
// Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
@@ -32,30 +33,33 @@ export async function exportMute(job: Bull.Job, done: any): Promise {
const stream = fs.createWriteStream(path, { flags: 'a' });
let exportedCount = 0;
- let ended = false;
let cursor: any = null;
- while (!ended) {
- const mutes = await Mute.find({
- muterId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
+ while (true) {
+ const mutes = await Mutings.find({
+ where: {
+ muterId: user.id,
+ ...(cursor ? { id: MoreThan(cursor) } : {})
+ },
+ take: 100,
+ order: {
+ id: 1
}
});
if (mutes.length === 0) {
- ended = true;
job.progress(100);
break;
}
- cursor = mutes[mutes.length - 1]._id;
+ cursor = mutes[mutes.length - 1].id;
for (const mute of mutes) {
- const u = await User.findOne({ _id: mute.muteeId }, { fields: { username: true, host: true } });
+ const u = await Users.findOne({ id: mute.muteeId });
+ if (u == null) {
+ exportedCount++; continue;
+ }
+
const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
@@ -70,8 +74,8 @@ export async function exportMute(job: Bull.Job, done: any): Promise {
exportedCount++;
}
- const total = await Mute.count({
- muterId: user._id,
+ const total = await Mutings.count({
+ muterId: user.id,
});
job.progress(exportedCount / total);
@@ -83,7 +87,7 @@ export async function exportMute(job: Bull.Job, done: any): Promise {
const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
- logger.succ(`Exported to: ${driveFile._id}`);
+ logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done();
}
diff --git a/src/queue/processors/db/export-notes.ts b/src/queue/processors/db/export-notes.ts
index 8f3cdc5b99..d03a216a59 100644
--- a/src/queue/processors/db/export-notes.ts
+++ b/src/queue/processors/db/export-notes.ts
@@ -1,22 +1,26 @@
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
-import Note, { INote } from '../../../models/note';
import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
import dateFormat = require('dateformat');
+import { Users, Notes, Polls } from '../../../models';
+import { MoreThan } from 'typeorm';
+import { Note } from '../../../models/entities/note';
+import { Poll } from '../../../models/entities/poll';
+import { ensure } from '../../../prelude/ensure';
const logger = queueLogger.createSubLogger('export-notes');
export async function exportNotes(job: Bull.Job, done: any): Promise {
- logger.info(`Exporting notes of ${job.data.user._id} ...`);
+ logger.info(`Exporting notes of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
- });
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
// Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
@@ -42,30 +46,33 @@ export async function exportNotes(job: Bull.Job, done: any): Promise {
});
let exportedNotesCount = 0;
- let ended = false;
let cursor: any = null;
- while (!ended) {
- const notes = await Note.find({
- userId: user._id,
- ...(cursor ? { _id: { $gt: cursor } } : {})
- }, {
- limit: 100,
- sort: {
- _id: 1
+ while (true) {
+ const notes = await Notes.find({
+ where: {
+ userId: user.id,
+ ...(cursor ? { id: MoreThan(cursor) } : {})
+ },
+ take: 100,
+ order: {
+ id: 1
}
});
if (notes.length === 0) {
- ended = true;
job.progress(100);
break;
}
- cursor = notes[notes.length - 1]._id;
+ cursor = notes[notes.length - 1].id;
for (const note of notes) {
- const content = JSON.stringify(serialize(note));
+ let poll: Poll | undefined;
+ if (note.hasPoll) {
+ poll = await Polls.findOne({ noteId: note.id }).then(ensure);
+ }
+ const content = JSON.stringify(serialize(note, poll));
await new Promise((res, rej) => {
stream.write(exportedNotesCount === 0 ? content : ',\n' + content, err => {
if (err) {
@@ -79,8 +86,8 @@ export async function exportNotes(job: Bull.Job, done: any): Promise {
exportedNotesCount++;
}
- const total = await Note.count({
- userId: user._id,
+ const total = await Notes.count({
+ userId: user.id,
});
job.progress(exportedNotesCount / total);
@@ -103,20 +110,20 @@ export async function exportNotes(job: Bull.Job, done: any): Promise {
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
const driveFile = await addFile(user, path, fileName);
- logger.succ(`Exported to: ${driveFile._id}`);
+ logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done();
}
-function serialize(note: INote): any {
+function serialize(note: Note, poll: Poll | null = null): any {
return {
- id: note._id,
+ id: note.id,
text: note.text,
createdAt: note.createdAt,
fileIds: note.fileIds,
replyId: note.replyId,
renoteId: note.renoteId,
- poll: note.poll,
+ poll: poll,
cw: note.cw,
viaMobile: note.viaMobile,
visibility: note.visibility,
diff --git a/src/queue/processors/db/export-user-lists.ts b/src/queue/processors/db/export-user-lists.ts
index dfbf152ec0..5cd978c1aa 100644
--- a/src/queue/processors/db/export-user-lists.ts
+++ b/src/queue/processors/db/export-user-lists.ts
@@ -1,26 +1,27 @@
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
import dateFormat = require('dateformat');
-import UserList from '../../../models/user-list';
import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, UserLists, UserListJoinings } from '../../../models';
+import { In } from 'typeorm';
const logger = queueLogger.createSubLogger('export-user-lists');
export async function exportUserLists(job: Bull.Job, done: any): Promise {
- logger.info(`Exporting user lists of ${job.data.user._id} ...`);
+ logger.info(`Exporting user lists of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
- });
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
- const lists = await UserList.find({
- userId: user._id
+ const lists = await UserLists.find({
+ userId: user.id
});
// Create temp file
@@ -36,18 +37,14 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise {
const stream = fs.createWriteStream(path, { flags: 'a' });
for (const list of lists) {
- const users = await User.find({
- _id: { $in: list.userIds }
- }, {
- fields: {
- username: true,
- host: true
- }
+ const joinings = await UserListJoinings.find({ userListId: list.id });
+ const users = await Users.find({
+ id: In(joinings.map(j => j.userId))
});
for (const u of users) {
const acct = getFullApAccount(u.username, u.host);
- const content = `${list.title},${acct}`;
+ const content = `${list.name},${acct}`;
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
@@ -67,7 +64,7 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise {
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
- logger.succ(`Exported to: ${driveFile._id}`);
+ logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done();
}
diff --git a/src/queue/processors/db/import-following.ts b/src/queue/processors/db/import-following.ts
index 069afa74c4..8de3193e46 100644
--- a/src/queue/processors/db/import-following.ts
+++ b/src/queue/processors/db/import-following.ts
@@ -1,32 +1,33 @@
import * as Bull from 'bull';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
-import User from '../../../models/user';
import follow from '../../../services/following/create';
-import DriveFile from '../../../models/drive-file';
-import { getOriginalUrl } from '../../../misc/get-drive-file-url';
import parseAcct from '../../../misc/acct/parse';
-import resolveUser from '../../../remote/resolve-user';
+import { resolveUser } from '../../../remote/resolve-user';
import { downloadTextFile } from '../../../misc/download-text-file';
-import { isSelfHost, toDbHost } from '../../../misc/convert-host';
+import { isSelfHost, toPuny } from '../../../misc/convert-host';
+import { Users, DriveFiles } from '../../../models';
const logger = queueLogger.createSubLogger('import-following');
export async function importFollowing(job: Bull.Job, done: any): Promise {
- logger.info(`Importing following of ${job.data.user._id} ...`);
+ logger.info(`Importing following of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
+
+ const file = await DriveFiles.findOne({
+ id: job.data.fileId
});
+ if (file == null) {
+ done();
+ return;
+ }
- const file = await DriveFile.findOne({
- _id: new mongo.ObjectID(job.data.fileId.toString())
- });
-
- const url = getOriginalUrl(file);
-
- const csv = await downloadTextFile(url);
+ const csv = await downloadTextFile(file.url);
let linenum = 0;
@@ -36,11 +37,11 @@ export async function importFollowing(job: Bull.Job, done: any): Promise {
try {
const { username, host } = parseAcct(line.trim());
- let target = isSelfHost(host) ? await User.findOne({
+ let target = isSelfHost(host!) ? await Users.findOne({
host: null,
usernameLower: username.toLowerCase()
- }) : await User.findOne({
- host: toDbHost(host),
+ }) : await Users.findOne({
+ host: toPuny(host!),
usernameLower: username.toLowerCase()
});
@@ -55,9 +56,9 @@ export async function importFollowing(job: Bull.Job, done: any): Promise {
}
// skip myself
- if (target._id.equals(job.data.user._id)) continue;
+ if (target.id === job.data.user.id) continue;
- logger.info(`Follow[${linenum}] ${target._id} ...`);
+ logger.info(`Follow[${linenum}] ${target.id} ...`);
follow(user, target);
} catch (e) {
diff --git a/src/queue/processors/db/import-user-lists.ts b/src/queue/processors/db/import-user-lists.ts
index 50d3c649d4..1e852be945 100644
--- a/src/queue/processors/db/import-user-lists.ts
+++ b/src/queue/processors/db/import-user-lists.ts
@@ -1,67 +1,68 @@
import * as Bull from 'bull';
-import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
-import User from '../../../models/user';
-import UserList from '../../../models/user-list';
-import DriveFile from '../../../models/drive-file';
-import { getOriginalUrl } from '../../../misc/get-drive-file-url';
import parseAcct from '../../../misc/acct/parse';
-import resolveUser from '../../../remote/resolve-user';
+import { resolveUser } from '../../../remote/resolve-user';
import { pushUserToUserList } from '../../../services/user-list/push';
import { downloadTextFile } from '../../../misc/download-text-file';
-import { isSelfHost, toDbHost } from '../../../misc/convert-host';
+import { isSelfHost, toPuny } from '../../../misc/convert-host';
+import { DriveFiles, Users, UserLists, UserListJoinings } from '../../../models';
+import { genId } from '../../../misc/gen-id';
const logger = queueLogger.createSubLogger('import-user-lists');
export async function importUserLists(job: Bull.Job, done: any): Promise {
- logger.info(`Importing user lists of ${job.data.user._id} ...`);
+ logger.info(`Importing user lists of ${job.data.user.id} ...`);
- const user = await User.findOne({
- _id: new mongo.ObjectID(job.data.user._id.toString())
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
+
+ const file = await DriveFiles.findOne({
+ id: job.data.fileId
});
+ if (file == null) {
+ done();
+ return;
+ }
- const file = await DriveFile.findOne({
- _id: new mongo.ObjectID(job.data.fileId.toString())
- });
-
- const url = getOriginalUrl(file);
-
- const csv = await downloadTextFile(url);
+ const csv = await downloadTextFile(file.url);
for (const line of csv.trim().split('\n')) {
const listName = line.split(',')[0].trim();
const { username, host } = parseAcct(line.split(',')[1].trim());
- let list = await UserList.findOne({
- userId: user._id,
- title: listName
+ let list = await UserLists.findOne({
+ userId: user.id,
+ name: listName
});
if (list == null) {
- list = await UserList.insert({
+ list = await UserLists.save({
+ id: genId(),
createdAt: new Date(),
- userId: user._id,
- title: listName,
+ userId: user.id,
+ name: listName,
userIds: []
});
}
- let target = isSelfHost(host) ? await User.findOne({
+ let target = isSelfHost(host!) ? await Users.findOne({
host: null,
usernameLower: username.toLowerCase()
- }) : await User.findOne({
- host: toDbHost(host),
+ }) : await Users.findOne({
+ host: toPuny(host!),
usernameLower: username.toLowerCase()
});
- if (host == null && target == null) continue;
- if (list.userIds.some(id => id.equals(target._id))) continue;
-
if (target == null) {
target = await resolveUser(username, host);
}
+ if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue;
+
pushUserToUserList(target, list);
}
diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts
index 1bc9a9af7c..921cdf7ab1 100644
--- a/src/queue/processors/db/index.ts
+++ b/src/queue/processors/db/index.ts
@@ -1,5 +1,4 @@
import * as Bull from 'bull';
-import { deleteNotes } from './delete-notes';
import { deleteDriveFiles } from './delete-drive-files';
import { exportNotes } from './export-notes';
import { exportFollowing } from './export-following';
@@ -10,7 +9,6 @@ import { importFollowing } from './import-following';
import { importUserLists } from './import-user-lists';
const jobs = {
- deleteNotes,
deleteDriveFiles,
exportNotes,
exportFollowing,
diff --git a/src/queue/processors/deliver.ts b/src/queue/processors/deliver.ts
index 28d3a17f6b..8837c80d87 100644
--- a/src/queue/processors/deliver.ts
+++ b/src/queue/processors/deliver.ts
@@ -1,13 +1,13 @@
import * as Bull from 'bull';
import request from '../../remote/activitypub/request';
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
import Logger from '../../services/logger';
+import { Instances } from '../../models';
+import { instanceChart } from '../../services/chart';
const logger = new Logger('deliver');
-let latest: string = null;
+let latest: string | null = null;
export default async (job: Bull.Job) => {
const { host } = new URL(job.data.to);
@@ -21,13 +21,11 @@ export default async (job: Bull.Job) => {
// Update stats
registerOrFetchInstanceDoc(host).then(i => {
- Instance.update({ _id: i._id }, {
- $set: {
- latestRequestSentAt: new Date(),
- latestStatus: 200,
- lastCommunicatedAt: new Date(),
- isNotResponding: false
- }
+ Instances.update(i.id, {
+ latestRequestSentAt: new Date(),
+ latestStatus: 200,
+ lastCommunicatedAt: new Date(),
+ isNotResponding: false
});
instanceChart.requestSent(i.host, true);
@@ -37,12 +35,10 @@ export default async (job: Bull.Job) => {
} catch (res) {
// Update stats
registerOrFetchInstanceDoc(host).then(i => {
- Instance.update({ _id: i._id }, {
- $set: {
- latestRequestSentAt: new Date(),
- latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null,
- isNotResponding: true
- }
+ Instances.update(i.id, {
+ latestRequestSentAt: new Date(),
+ latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null,
+ isNotResponding: true
});
instanceChart.requestSent(i.host, false);
diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts
index 436f3335c8..05fed0566d 100644
--- a/src/queue/processors/inbox.ts
+++ b/src/queue/processors/inbox.ts
@@ -1,16 +1,20 @@
import * as Bull from 'bull';
import * as httpSignature from 'http-signature';
import parseAcct from '../../misc/acct/parse';
-import User, { IRemoteUser } from '../../models/user';
+import { IRemoteUser } from '../../models/entities/user';
import perform from '../../remote/activitypub/perform';
import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
-import { toUnicode } from 'punycode';
import { URL } from 'url';
import { publishApLogStream } from '../../services/stream';
import Logger from '../../services/logger';
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
+import { Instances, Users, UserPublickeys } from '../../models';
+import { instanceChart } from '../../services/chart';
+import { UserPublickey } from '../../models/entities/user-publickey';
+import fetchMeta from '../../misc/fetch-meta';
+import { toPuny, toPunyNullable } from '../../misc/convert-host';
+import { validActor } from '../../remote/activitypub/type';
+import { ensure } from '../../prelude/ensure';
const logger = new Logger('inbox');
@@ -28,9 +32,13 @@ export default async (job: Bull.Job): Promise => {
const keyIdLower = signature.keyId.toLowerCase();
let user: IRemoteUser;
+ let key: UserPublickey;
if (keyIdLower.startsWith('acct:')) {
- const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
+ const acct = parseAcct(keyIdLower.slice('acct:'.length));
+ const host = toPunyNullable(acct.host);
+ const username = toPuny(acct.username);
+
if (host === null) {
logger.warn(`request was made by local user: @${username}`);
return;
@@ -46,16 +54,21 @@ export default async (job: Bull.Job): Promise => {
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
- const instance = await Instance.findOne({ host: host.toLowerCase() });
- if (instance && instance.isBlocked) {
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(host)) {
logger.info(`Blocked request: ${host}`);
return;
}
- user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser;
+ user = await Users.findOne({
+ usernameLower: username.toLowerCase(),
+ host: host
+ }) as IRemoteUser;
+
+ key = await UserPublickeys.findOne(user.id).then(ensure);
} else {
// アクティビティ内のホストの検証
- const host = toUnicode(new URL(signature.keyId).hostname.toLowerCase());
+ const host = toPuny(new URL(signature.keyId).hostname);
try {
ValidateActivity(activity, host);
} catch (e) {
@@ -65,24 +78,25 @@ export default async (job: Bull.Job): Promise => {
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
- const instance = await Instance.findOne({ host: host.toLowerCase() });
- if (instance && instance.isBlocked) {
- logger.warn(`Blocked request: ${host}`);
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(host)) {
+ logger.info(`Blocked request: ${host}`);
return;
}
- user = await User.findOne({
- host: { $ne: null },
- 'publicKey.id': signature.keyId
- }) as IRemoteUser;
+ key = await UserPublickeys.findOne({
+ keyId: signature.keyId
+ }).then(ensure);
+
+ user = await Users.findOne(key.userId) as IRemoteUser;
}
// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
if (activity.type === 'Update') {
- if (activity.object && activity.object.type === 'Person') {
+ if (activity.object && validActor.includes(activity.object.type)) {
if (user == null) {
logger.warn('Update activity received, but user not registed.');
- } else if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
+ } else if (!httpSignature.verifySignature(signature, key.keyPem)) {
logger.warn('Update activity received, but signature verification failed.');
} else {
updatePerson(activity.actor, null, activity.object);
@@ -92,15 +106,15 @@ export default async (job: Bull.Job): Promise => {
}
// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
- if (user === null) {
+ if (user == null) {
user = await resolvePerson(activity.actor) as IRemoteUser;
}
- if (user === null) {
+ if (user == null) {
throw new Error('failed to resolve user');
}
- if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
+ if (!httpSignature.verifySignature(signature, key.keyPem)) {
logger.error('signature verification failed');
return;
}
@@ -116,12 +130,10 @@ export default async (job: Bull.Job): Promise => {
// Update stats
registerOrFetchInstanceDoc(user.host).then(i => {
- Instance.update({ _id: i._id }, {
- $set: {
- latestRequestReceivedAt: new Date(),
- lastCommunicatedAt: new Date(),
- isNotResponding: false
- }
+ Instances.update(i.id, {
+ latestRequestReceivedAt: new Date(),
+ lastCommunicatedAt: new Date(),
+ isNotResponding: false
});
instanceChart.requestReceived(i.host);
@@ -139,7 +151,7 @@ export default async (job: Bull.Job): Promise => {
function ValidateActivity(activity: any, host: string) {
// id (if exists)
if (typeof activity.id === 'string') {
- const uriHost = toUnicode(new URL(activity.id).hostname.toLowerCase());
+ const uriHost = toPuny(new URL(activity.id).hostname);
if (host !== uriHost) {
const diag = activity.signature ? '. Has LD-Signature. Forwarded?' : '';
throw new Error(`activity.id(${activity.id}) has different host(${host})${diag}`);
@@ -148,7 +160,7 @@ function ValidateActivity(activity: any, host: string) {
// actor (if exists)
if (typeof activity.actor === 'string') {
- const uriHost = toUnicode(new URL(activity.actor).hostname.toLowerCase());
+ const uriHost = toPuny(new URL(activity.actor).hostname);
if (host !== uriHost) throw new Error('activity.actor has different host');
}
@@ -156,13 +168,13 @@ function ValidateActivity(activity: any, host: string) {
if (activity.type === 'Create' && activity.object) {
// object.id (if exists)
if (typeof activity.object.id === 'string') {
- const uriHost = toUnicode(new URL(activity.object.id).hostname.toLowerCase());
+ const uriHost = toPuny(new URL(activity.object.id).hostname);
if (host !== uriHost) throw new Error('activity.object.id has different host');
}
// object.attributedTo (if exists)
if (typeof activity.object.attributedTo === 'string') {
- const uriHost = toUnicode(new URL(activity.object.attributedTo).hostname.toLowerCase());
+ const uriHost = toPuny(new URL(activity.object.attributedTo).hostname);
if (host !== uriHost) throw new Error('activity.object.attributedTo has different host');
}
}
diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts
index 07c820c28a..377b8dac42 100644
--- a/src/remote/activitypub/kernel/accept/follow.ts
+++ b/src/remote/activitypub/kernel/accept/follow.ts
@@ -1,21 +1,22 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import config from '../../../../config';
import accept from '../../../../services/following/requests/accept';
import { IFollow } from '../../type';
+import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
+ if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {
- return null;
+ return;
}
- const follower = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
+ const follower = await Users.findOne({
+ id: id.split('/').pop()
});
- if (follower === null) {
+ if (follower == null) {
throw new Error('follower not found');
}
diff --git a/src/remote/activitypub/kernel/accept/index.ts b/src/remote/activitypub/kernel/accept/index.ts
index 443c1935d6..5a27ce1d4d 100644
--- a/src/remote/activitypub/kernel/accept/index.ts
+++ b/src/remote/activitypub/kernel/accept/index.ts
@@ -1,5 +1,5 @@
import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import acceptFollow from './follow';
import { IAccept, IFollow } from '../../type';
import { apLogger } from '../../logger';
diff --git a/src/remote/activitypub/kernel/add/index.ts b/src/remote/activitypub/kernel/add/index.ts
index eb2dba5b21..a5b2687416 100644
--- a/src/remote/activitypub/kernel/add/index.ts
+++ b/src/remote/activitypub/kernel/add/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { IAdd } from '../../type';
import { resolveNote } from '../../models/note';
import { addPinned } from '../../../../services/i/pin';
@@ -14,7 +14,8 @@ export default async (actor: IRemoteUser, activity: IAdd): Promise => {
if (activity.target === actor.featured) {
const note = await resolveNote(activity.object);
- await addPinned(actor, note._id);
+ if (note == null) throw new Error('note not found');
+ await addPinned(actor, note.id);
return;
}
diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts
index 5f738da6c7..ebd5a27b92 100644
--- a/src/remote/activitypub/kernel/announce/index.ts
+++ b/src/remote/activitypub/kernel/announce/index.ts
@@ -1,5 +1,5 @@
import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import announceNote from './note';
import { IAnnounce, INote } from '../../type';
import { apLogger } from '../../logger';
diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
index 912936bef8..f9822c5187 100644
--- a/src/remote/activitypub/kernel/announce/note.ts
+++ b/src/remote/activitypub/kernel/announce/note.ts
@@ -1,12 +1,12 @@
import Resolver from '../../resolver';
import post from '../../../../services/note/create';
-import { IRemoteUser, IUser } from '../../../../models/user';
+import { IRemoteUser, User } from '../../../../models/entities/user';
import { IAnnounce, INote } from '../../type';
import { fetchNote, resolveNote } from '../../models/note';
import { resolvePerson } from '../../models/person';
import { apLogger } from '../../logger';
import { extractDbHost } from '../../../../misc/convert-host';
-import Instance from '../../../../models/instance';
+import fetchMeta from '../../../../misc/fetch-meta';
const logger = apLogger;
@@ -27,8 +27,8 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
// アナウンス先をブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
- const instance = await Instance.findOne({ host: extractDbHost(uri) });
- if (instance && instance.isBlocked) return;
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(extractDbHost(uri))) return;
// 既に同じURIを持つものが登録されていないかチェック
const exist = await fetchNote(uri);
@@ -53,16 +53,16 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
logger.info(`Creating the (Re)Note: ${uri}`);
//#region Visibility
- const visibility = getVisibility(activity.to, activity.cc, actor);
+ const visibility = getVisibility(activity.to || [], activity.cc || [], actor);
- let visibleUsers: IUser[] = [];
+ let visibleUsers: User[] = [];
if (visibility == 'specified') {
- visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
+ visibleUsers = await Promise.all((note.to || []).map(uri => resolvePerson(uri)));
}
//#endergion
await post(actor, {
- createdAt: new Date(activity.published),
+ createdAt: activity.published ? new Date(activity.published) : null,
renote,
visibility,
visibleUsers,
@@ -75,9 +75,6 @@ type visibility = 'public' | 'home' | 'followers' | 'specified';
function getVisibility(to: string[], cc: string[], actor: IRemoteUser): visibility {
const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
- to = to || [];
- cc = cc || [];
-
if (to.includes(PUBLIC)) {
return 'public';
} else if (cc.includes(PUBLIC)) {
diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts
index a10163016c..5c247326cb 100644
--- a/src/remote/activitypub/kernel/block/index.ts
+++ b/src/remote/activitypub/kernel/block/index.ts
@@ -1,28 +1,27 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import { IBlock } from '../../type';
import block from '../../../../services/blocking/create';
import { apLogger } from '../../logger';
+import { Users } from '../../../../models';
+import { IRemoteUser } from '../../../../models/entities/user';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IBlock): Promise => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
+ if (id == null) throw new Error('missing id');
const uri = activity.id || activity;
logger.info(`Block: ${uri}`);
if (!id.startsWith(config.url + '/')) {
- return null;
+ return;
}
- const blockee = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
- });
+ const blockee = await Users.findOne(id.split('/').pop());
- if (blockee === null) {
+ if (blockee == null) {
throw new Error('blockee not found');
}
diff --git a/src/remote/activitypub/kernel/create/image.ts b/src/remote/activitypub/kernel/create/image.ts
index 9c19abbcc4..7720e8f1bd 100644
--- a/src/remote/activitypub/kernel/create/image.ts
+++ b/src/remote/activitypub/kernel/create/image.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { createImage } from '../../models/image';
export default async function(actor: IRemoteUser, image: any): Promise {
diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts
index 6e314d0b82..0326b591f8 100644
--- a/src/remote/activitypub/kernel/create/index.ts
+++ b/src/remote/activitypub/kernel/create/index.ts
@@ -1,5 +1,5 @@
import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import createImage from './image';
import createNote from './note';
import { ICreate } from '../../type';
diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts
index 0f874b9fbf..70e61bdf1b 100644
--- a/src/remote/activitypub/kernel/create/note.ts
+++ b/src/remote/activitypub/kernel/create/note.ts
@@ -1,5 +1,5 @@
import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { createNote, fetchNote } from '../../models/note';
/**
diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts
index c9c385b1fa..fab5e7ab64 100644
--- a/src/remote/activitypub/kernel/delete/index.ts
+++ b/src/remote/activitypub/kernel/delete/index.ts
@@ -1,9 +1,9 @@
import Resolver from '../../resolver';
import deleteNote from './note';
-import Note from '../../../../models/note';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { IDelete } from '../../type';
import { apLogger } from '../../logger';
+import { Notes } from '../../../../models';
/**
* 削除アクティビティを捌きます
@@ -27,7 +27,7 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise => {
break;
case 'Tombstone':
- const note = await Note.findOne({ uri });
+ const note = await Notes.findOne({ uri });
if (note != null) {
deleteNote(actor, uri);
}
diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts
index f67919c56b..b146e68a07 100644
--- a/src/remote/activitypub/kernel/delete/note.ts
+++ b/src/remote/activitypub/kernel/delete/note.ts
@@ -1,20 +1,20 @@
-import Note from '../../../../models/note';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import deleteNode from '../../../../services/note/delete';
import { apLogger } from '../../logger';
+import { Notes } from '../../../../models';
const logger = apLogger;
export default async function(actor: IRemoteUser, uri: string): Promise {
logger.info(`Deleting the Note: ${uri}`);
- const note = await Note.findOne({ uri });
+ const note = await Notes.findOne({ uri });
if (note == null) {
throw new Error('note not found');
}
- if (!note.userId.equals(actor._id)) {
+ if (note.userId !== actor.id) {
throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません');
}
diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts
index e2db70b20d..c255067bfd 100644
--- a/src/remote/activitypub/kernel/follow.ts
+++ b/src/remote/activitypub/kernel/follow.ts
@@ -1,21 +1,20 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../models/user';
+import { IRemoteUser } from '../../../models/entities/user';
import config from '../../../config';
import follow from '../../../services/following/create';
import { IFollow } from '../type';
+import { Users } from '../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
+ if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {
- return null;
+ return;
}
- const followee = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
- });
+ const followee = await Users.findOne(id.split('/').pop());
- if (followee === null) {
+ if (followee == null) {
throw new Error('followee not found');
}
diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts
index 4f7a5c91fd..d1251817fa 100644
--- a/src/remote/activitypub/kernel/index.ts
+++ b/src/remote/activitypub/kernel/index.ts
@@ -1,5 +1,5 @@
import { Object } from '../type';
-import { IRemoteUser } from '../../../models/user';
+import { IRemoteUser } from '../../../models/entities/user';
import create from './create';
import performDeleteActivity from './delete';
import performUpdateActivity from './update';
@@ -71,7 +71,7 @@ const self = async (actor: IRemoteUser, activity: Object): Promise => {
default:
apLogger.warn(`unknown activity type: ${(activity as any).type}`);
- return null;
+ return;
}
};
diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts
index ed35da8133..a08b453a89 100644
--- a/src/remote/activitypub/kernel/like.ts
+++ b/src/remote/activitypub/kernel/like.ts
@@ -1,19 +1,19 @@
-import * as mongo from 'mongodb';
-import Note from '../../../models/note';
-import { IRemoteUser } from '../../../models/user';
+import { IRemoteUser } from '../../../models/entities/user';
import { ILike } from '../type';
import create from '../../../services/note/reaction/create';
+import { Notes } from '../../../models';
export default async (actor: IRemoteUser, activity: ILike) => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
+ if (id == null) throw new Error('missing id');
// Transform:
// https://misskey.ex/notes/xxxx to
// xxxx
- const noteId = new mongo.ObjectID(id.split('/').pop());
+ const noteId = id.split('/').pop();
- const note = await Note.findOne({ _id: noteId });
- if (note === null) {
+ const note = await Notes.findOne(noteId);
+ if (note == null) {
throw new Error();
}
diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts
index 35cd2ec0c9..d8b5a4b9b9 100644
--- a/src/remote/activitypub/kernel/reject/follow.ts
+++ b/src/remote/activitypub/kernel/reject/follow.ts
@@ -1,21 +1,20 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import config from '../../../../config';
import reject from '../../../../services/following/requests/reject';
import { IFollow } from '../../type';
+import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
+ if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {
- return null;
+ return;
}
- const follower = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
- });
+ const follower = await Users.findOne(id.split('/').pop());
- if (follower === null) {
+ if (follower == null) {
throw new Error('follower not found');
}
diff --git a/src/remote/activitypub/kernel/reject/index.ts b/src/remote/activitypub/kernel/reject/index.ts
index c3585abbb6..8ece5cf174 100644
--- a/src/remote/activitypub/kernel/reject/index.ts
+++ b/src/remote/activitypub/kernel/reject/index.ts
@@ -1,5 +1,5 @@
import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import rejectFollow from './follow';
import { IReject, IFollow } from '../../type';
import { apLogger } from '../../logger';
diff --git a/src/remote/activitypub/kernel/remove/index.ts b/src/remote/activitypub/kernel/remove/index.ts
index 91b207c80d..32b8d66471 100644
--- a/src/remote/activitypub/kernel/remove/index.ts
+++ b/src/remote/activitypub/kernel/remove/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { IRemove } from '../../type';
import { resolveNote } from '../../models/note';
import { removePinned } from '../../../../services/i/pin';
@@ -14,7 +14,8 @@ export default async (actor: IRemoteUser, activity: IRemove): Promise => {
if (activity.target === actor.featured) {
const note = await resolveNote(activity.object);
- await removePinned(actor, note._id);
+ if (note == null) throw new Error('note not found');
+ await removePinned(actor, note.id);
return;
}
diff --git a/src/remote/activitypub/kernel/undo/block.ts b/src/remote/activitypub/kernel/undo/block.ts
index 4a22ac7924..8ef70a9bef 100644
--- a/src/remote/activitypub/kernel/undo/block.ts
+++ b/src/remote/activitypub/kernel/undo/block.ts
@@ -1,28 +1,27 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import { IBlock } from '../../type';
import unblock from '../../../../services/blocking/delete';
import { apLogger } from '../../logger';
+import { IRemoteUser } from '../../../../models/entities/user';
+import { Users } from '../../../../models';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IBlock): Promise => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
+ if (id == null) throw new Error('missing id');
const uri = activity.id || activity;
logger.info(`UnBlock: ${uri}`);
if (!id.startsWith(config.url + '/')) {
- return null;
+ return;
}
- const blockee = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
- });
+ const blockee = await Users.findOne(id.split('/').pop());
- if (blockee === null) {
+ if (blockee == null) {
throw new Error('blockee not found');
}
diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts
index af06aa5b31..d75f055640 100644
--- a/src/remote/activitypub/kernel/undo/follow.ts
+++ b/src/remote/activitypub/kernel/undo/follow.ts
@@ -1,24 +1,21 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import unfollow from '../../../../services/following/delete';
import cancelRequest from '../../../../services/following/requests/cancel';
import { IFollow } from '../../type';
-import FollowRequest from '../../../../models/follow-request';
-import Following from '../../../../models/following';
+import { IRemoteUser } from '../../../../models/entities/user';
+import { Users, FollowRequests, Followings } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
+ if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {
- return null;
+ return;
}
- const followee = await User.findOne({
- _id: new mongo.ObjectID(id.split('/').pop())
- });
+ const followee = await Users.findOne(id.split('/').pop());
- if (followee === null) {
+ if (followee == null) {
throw new Error('followee not found');
}
@@ -26,14 +23,14 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise => {
throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません');
}
- const req = await FollowRequest.findOne({
- followerId: actor._id,
- followeeId: followee._id
+ const req = await FollowRequests.findOne({
+ followerId: actor.id,
+ followeeId: followee.id
});
- const following = await Following.findOne({
- followerId: actor._id,
- followeeId: followee._id
+ const following = await Followings.findOne({
+ followerId: actor.id,
+ followeeId: followee.id
});
if (req) {
diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts
index 80b44fae04..5f2e58c3bf 100644
--- a/src/remote/activitypub/kernel/undo/index.ts
+++ b/src/remote/activitypub/kernel/undo/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { IUndo, IFollow, IBlock, ILike } from '../../type';
import unfollow from './follow';
import unblock from './block';
@@ -39,6 +39,4 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise => {
undoLike(actor, object as ILike);
break;
}
-
- return null;
};
diff --git a/src/remote/activitypub/kernel/undo/like.ts b/src/remote/activitypub/kernel/undo/like.ts
index b324ec854c..2678828a9a 100644
--- a/src/remote/activitypub/kernel/undo/like.ts
+++ b/src/remote/activitypub/kernel/undo/like.ts
@@ -1,20 +1,20 @@
-import * as mongo from 'mongodb';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { ILike } from '../../type';
-import Note from '../../../../models/note';
import deleteReaction from '../../../../services/note/reaction/delete';
+import { Notes } from '../../../../models';
/**
* Process Undo.Like activity
*/
export default async (actor: IRemoteUser, activity: ILike): Promise => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
+ if (id == null) throw new Error('missing id');
- const noteId = new mongo.ObjectID(id.split('/').pop());
+ const noteId = id.split('/').pop();
- const note = await Note.findOne({ _id: noteId });
- if (note === null) {
- throw 'note not found';
+ const note = await Notes.findOne(noteId);
+ if (note == null) {
+ throw new Error('note not found');
}
await deleteReaction(actor, note);
diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts
index 49b730391a..b8dff73395 100644
--- a/src/remote/activitypub/kernel/update/index.ts
+++ b/src/remote/activitypub/kernel/update/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
import { IUpdate, IObject } from '../../type';
import { apLogger } from '../../logger';
import { updateQuestion } from '../../models/question';
diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts
index 967ee65544..dba915fee9 100644
--- a/src/remote/activitypub/misc/get-note-html.ts
+++ b/src/remote/activitypub/misc/get-note-html.ts
@@ -1,9 +1,9 @@
-import { INote } from '../../../models/note';
+import { Note } from '../../../models/entities/note';
import { toHtml } from '../../../mfm/toHtml';
import { parse } from '../../../mfm/parse';
-export default function(note: INote) {
- let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
+export default function(note: Note) {
+ let html = toHtml(parse(note.text), JSON.parse(note.mentionedRemoteUsers));
if (html == null) html = '.
';
return html;
diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts
index bd97d13d27..f8b35ea21c 100644
--- a/src/remote/activitypub/models/image.ts
+++ b/src/remote/activitypub/models/image.ts
@@ -1,19 +1,21 @@
import uploadFromUrl from '../../../services/drive/upload-from-url';
-import { IRemoteUser } from '../../../models/user';
-import DriveFile, { IDriveFile } from '../../../models/drive-file';
+import { IRemoteUser } from '../../../models/entities/user';
import Resolver from '../resolver';
import fetchMeta from '../../../misc/fetch-meta';
import { apLogger } from '../logger';
+import { DriveFile } from '../../../models/entities/drive-file';
+import { DriveFiles } from '../../../models';
+import { ensure } from '../../../prelude/ensure';
const logger = apLogger;
/**
* Imageを作成します。
*/
-export async function createImage(actor: IRemoteUser, value: any): Promise {
+export async function createImage(actor: IRemoteUser, value: any): Promise {
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
- return null;
+ throw new Error('actor has been suspended');
}
const image = await new Resolver().resolve(value) as any;
@@ -27,30 +29,18 @@ export async function createImage(actor: IRemoteUser, value: any): Promise= 400 && e < 500) {
- logger.warn(`Ignored image: ${image.url} - ${e}`);
- return null;
- }
- throw e;
- }
+ let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
- if (file.metadata.isRemote) {
+ if (file.isLink) {
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
// URLを更新する
- if (file.metadata.url !== image.url) {
- file = await DriveFile.findOneAndUpdate({ _id: file._id }, {
- $set: {
- 'metadata.url': image.url,
- 'metadata.uri': image.url
- }
- }, {
- returnNewDocument: true
+ if (file.url !== image.url) {
+ await DriveFiles.update({ id: file.id }, {
+ url: image.url,
+ uri: image.url
});
+
+ file = await DriveFiles.findOne(file.id).then(ensure);
}
}
@@ -63,7 +53,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise {
+export async function resolveImage(actor: IRemoteUser, value: any): Promise {
// TODO
// リモートサーバーからフェッチしてきて登録
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index 6251621527..8842342342 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -1,26 +1,27 @@
-import * as mongo from 'mongodb';
import * as promiseLimit from 'promise-limit';
import config from '../../../config';
import Resolver from '../resolver';
-import Note, { INote } from '../../../models/note';
import post from '../../../services/note/create';
-import { INote as INoteActivityStreamsObject, IObject } from '../type';
import { resolvePerson, updatePerson } from './person';
import { resolveImage } from './image';
-import { IRemoteUser, IUser } from '../../../models/user';
+import { IRemoteUser, User } from '../../../models/entities/user';
import { fromHtml } from '../../../mfm/fromHtml';
-import Emoji, { IEmoji } from '../../../models/emoji';
import { ITag, extractHashtags } from './tag';
-import { toUnicode } from 'punycode';
import { unique, concat, difference } from '../../../prelude/array';
import { extractPollFromQuestion } from './question';
import vote from '../../../services/note/polls/vote';
import { apLogger } from '../logger';
-import { IDriveFile } from '../../../models/drive-file';
+import { DriveFile } from '../../../models/entities/drive-file';
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
-import Instance from '../../../models/instance';
-import { extractDbHost } from '../../../misc/convert-host';
+import { extractDbHost, toPuny } from '../../../misc/convert-host';
+import { Notes, Emojis, Polls } from '../../../models';
+import { Note } from '../../../models/entities/note';
+import { IObject, INote } from '../type';
+import { Emoji } from '../../../models/entities/emoji';
+import { genId } from '../../../misc/gen-id';
+import fetchMeta from '../../../misc/fetch-meta';
+import { ensure } from '../../../prelude/ensure';
const logger = apLogger;
@@ -29,17 +30,18 @@ const logger = apLogger;
*
* Misskeyに対象のNoteが登録されていればそれを返します。
*/
-export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise {
+export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise {
const uri = typeof value == 'string' ? value : value.id;
+ if (uri == null) throw new Error('missing uri');
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) {
- const id = new mongo.ObjectID(uri.split('/').pop());
- return await Note.findOne({ _id: id });
+ const id = uri.split('/').pop();
+ return await Notes.findOne(id).then(x => x || null);
}
//#region このサーバーに既に登録されていたらそれを返す
- const exist = await Note.findOne({ uri });
+ const exist = await Notes.findOne({ uri });
if (exist) {
return exist;
@@ -52,7 +54,7 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
/**
* Noteを作成します。
*/
-export async function createNote(value: any, resolver?: Resolver, silent = false): Promise {
+export async function createNote(value: any, resolver?: Resolver, silent = false): Promise {
if (resolver == null) resolver = new Resolver();
const object: any = await resolver.resolve(value);
@@ -65,21 +67,21 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
value: value,
object: object
});
- return null;
+ throw new Error('invalid note');
}
- const note: INoteActivityStreamsObject = object;
+ const note: INote = object;
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ
- const actor = await resolvePerson(note.attributedTo, null, resolver) as IRemoteUser;
+ const actor = await resolvePerson(note.attributedTo, resolver) as IRemoteUser;
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
- return null;
+ throw new Error('actor has been suspended');
}
//#region Visibility
@@ -87,7 +89,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc;
let visibility = 'public';
- let visibleUsers: IUser[] = [];
+ let visibleUsers: User[] = [];
if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
visibility = 'home';
@@ -95,9 +97,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
visibility = 'followers';
} else {
visibility = 'specified';
- visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver)));
+ visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, resolver)));
}
-}
+ }
//#endergion
const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver);
@@ -113,25 +115,27 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
const files = note.attachment
.map(attach => attach.sensitive = note.sensitive)
- ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise)))
+ ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise)))
.filter(image => image != null)
: [];
// リプライ
- const reply: INote = note.inReplyTo
- ? await resolveNote(note.inReplyTo, resolver).catch(e => {
- // 4xxの場合はリプライしてないことにする
- if (e.statusCode >= 400 && e.statusCode < 500) {
- logger.warn(`Ignored inReplyTo ${note.inReplyTo} - ${e.statusCode} `);
- return null;
+ const reply: Note | null = note.inReplyTo
+ ? await resolveNote(note.inReplyTo, resolver).then(x => {
+ if (x == null) {
+ logger.warn(`Specified inReplyTo, but nout found`);
+ throw new Error('inReplyTo not found');
+ } else {
+ return x;
}
+ }).catch(e => {
logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`);
throw e;
})
: null;
// 引用
- let quote: INote;
+ let quote: Note | undefined | null;
if (note._misskey_quote && typeof note._misskey_quote == 'string') {
quote = await resolveNote(note._misskey_quote).catch(e => {
@@ -148,25 +152,27 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
- const text = note._misskey_content || fromHtml(note.content);
+ const text = note._misskey_content || (note.content ? fromHtml(note.content) : null);
// vote
- if (reply && reply.poll) {
+ if (reply && reply.hasPoll) {
+ const poll = await Polls.findOne(reply.id).then(ensure);
+
const tryCreateVote = async (name: string, index: number): Promise => {
- if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) {
+ if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
} else if (index >= 0) {
logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
await vote(actor, reply, index);
// リモートフォロワーにUpdate配信
- deliverQuestionUpdate(reply._id);
+ deliverQuestionUpdate(reply.id);
}
return null;
};
if (note.name) {
- return await tryCreateVote(note.name, reply.poll.choices.findIndex(x => x.text === note.name));
+ return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
}
// 後方互換性のため
@@ -179,9 +185,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
}
}
- const emojis = await extractEmojis(note.tag, actor.host).catch(e => {
+ const emojis = await extractEmojis(note.tag || [], actor.host).catch(e => {
logger.info(`extractEmojis: ${e}`);
- return [] as IEmoji[];
+ return [] as Emoji[];
});
const apEmojis = emojis.map(emoji => emoji.name);
@@ -195,7 +201,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
}
return await post(actor, {
- createdAt: new Date(note.published),
+ createdAt: note.published ? new Date(note.published) : null,
files,
reply,
renote: quote,
@@ -222,13 +228,14 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
* Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
-export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise {
+export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise {
const uri = typeof value == 'string' ? value : value.id;
+ if (uri == null) throw new Error('missing uri');
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
- const instance = await Instance.findOne({ host: extractDbHost(uri) });
- if (instance && instance.isBlocked) throw { statusCode: 451 };
+ const meta = await fetchMeta();
+ if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 };
//#region このサーバーに既に登録されていたらそれを返す
const exist = await fetchNote(uri);
@@ -241,65 +248,81 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
// リモートサーバーからフェッチしてきて登録
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
- return await createNote(uri, resolver);
+ return await createNote(uri, resolver).catch(e => {
+ if (e.name === 'duplicated') {
+ return fetchNote(uri).then(note => {
+ if (note == null) {
+ throw new Error('something happened');
+ } else {
+ return note;
+ }
+ });
+ } else {
+ throw e;
+ }
+ });
}
-export async function extractEmojis(tags: ITag[], host_: string) {
- const host = toUnicode(host_.toLowerCase());
+export async function extractEmojis(tags: ITag[], host: string): Promise {
+ host = toPuny(host);
if (!tags) return [];
- const eomjiTags = tags.filter(tag => tag.type === 'Emoji' && tag.icon && tag.icon.url);
+ const eomjiTags = tags.filter(tag => tag.type === 'Emoji' && tag.icon && tag.icon.url && tag.name);
- return await Promise.all(
- eomjiTags.map(async tag => {
- const name = tag.name.replace(/^:/, '').replace(/:$/, '');
+ return await Promise.all(eomjiTags.map(async tag => {
+ const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
- const exists = await Emoji.findOne({
- host,
- name
- });
+ const exists = await Emojis.findOne({
+ host,
+ name
+ });
- if (exists) {
- if ((tag.updated != null && exists.updatedAt == null)
- || (tag.id != null && exists.uri == null)
- || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)) {
- return await Emoji.findOneAndUpdate({
- host,
- name,
- }, {
- $set: {
- uri: tag.id,
- url: tag.icon.url,
- updatedAt: new Date(tag.updated),
- }
- });
- }
- return exists;
+ if (exists) {
+ if ((tag.updated != null && exists.updatedAt == null)
+ || (tag.id != null && exists.uri == null)
+ || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
+ ) {
+ await Emojis.update({
+ host,
+ name,
+ }, {
+ uri: tag.id,
+ url: tag.icon!.url,
+ updatedAt: new Date(tag.updated!),
+ });
+
+ return await Emojis.findOne({
+ host,
+ name
+ }) as Emoji;
}
- logger.info(`register emoji host=${host}, name=${name}`);
+ return exists;
+ }
- return await Emoji.insert({
- host,
- name,
- uri: tag.id,
- url: tag.icon.url,
- updatedAt: tag.updated ? new Date(tag.updated) : undefined,
- aliases: []
- });
- })
- );
+ logger.info(`register emoji host=${host}, name=${name}`);
+
+ return await Emojis.save({
+ id: genId(),
+ host,
+ name,
+ uri: tag.id,
+ url: tag.icon!.url,
+ updatedAt: tag.updated ? new Date(tag.updated) : undefined,
+ aliases: []
+ } as Partial);
+ }));
}
async function extractMentionedUsers(actor: IRemoteUser, to: string[], cc: string[], resolver: Resolver) {
const ignoreUris = ['https://www.w3.org/ns/activitystreams#Public', `${actor.uri}/followers`];
const uris = difference(unique(concat([to || [], cc || []])), ignoreUris);
- const limit = promiseLimit(2);
+ const limit = promiseLimit(2);
const users = await Promise.all(
- uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise)
+ uris.map(uri => limit(() => resolvePerson(uri, resolver).catch(() => null)) as Promise)
);
- return users.filter(x => x != null);
+ return users.filter(x => x != null) as User[];
}
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index d27c937988..c1c07c7bbf 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -1,29 +1,32 @@
-import * as mongo from 'mongodb';
import * as promiseLimit from 'promise-limit';
-import { toUnicode } from 'punycode';
import config from '../../../config';
-import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user';
import Resolver from '../resolver';
import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type';
-import { IDriveFile } from '../../../models/drive-file';
-import Meta from '../../../models/meta';
+import { DriveFile } from '../../../models/entities/drive-file';
import { fromHtml } from '../../../mfm/fromHtml';
-import usersChart from '../../../services/chart/users';
-import instanceChart from '../../../services/chart/instance';
import { URL } from 'url';
import { resolveNote, extractEmojis } from './note';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
-import Instance from '../../../models/instance';
-import getDriveFileUrl from '../../../misc/get-drive-file-url';
-import { IEmoji } from '../../../models/emoji';
import { ITag, extractHashtags } from './tag';
-import Following from '../../../models/following';
import { IIdentifier } from './identifier';
import { apLogger } from '../logger';
-import { INote } from '../../../models/note';
+import { Note } from '../../../models/entities/note';
import { updateHashtag } from '../../../services/update-hashtag';
+import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '../../../models';
+import { User, IRemoteUser } from '../../../models/entities/user';
+import { Emoji } from '../../../models/entities/emoji';
+import { UserNotePining } from '../../../models/entities/user-note-pinings';
+import { genId } from '../../../misc/gen-id';
+import { instanceChart, usersChart } from '../../../services/chart';
+import { UserPublickey } from '../../../models/entities/user-publickey';
+import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error';
+import { toPuny } from '../../../misc/convert-host';
+import { UserProfile } from '../../../models/entities/user-profile';
+import { validActor } from '../../../remote/activitypub/type';
+import { getConnection } from 'typeorm';
+import { ensure } from '../../../prelude/ensure';
const logger = apLogger;
/**
@@ -32,13 +35,13 @@ const logger = apLogger;
* @param uri Fetch target URI
*/
function validatePerson(x: any, uri: string) {
- const expectHost = toUnicode(new URL(uri).hostname.toLowerCase());
+ const expectHost = toPuny(new URL(uri).hostname);
if (x == null) {
return new Error('invalid person: object is null');
}
- if (x.type != 'Person' && x.type != 'Service') {
+ if (!validActor.includes(x.type)) {
return new Error(`invalid person: object is not a person or service '${x.type}'`);
}
@@ -50,11 +53,11 @@ function validatePerson(x: any, uri: string) {
return new Error('invalid person: inbox is not a string');
}
- if (!validateUsername(x.preferredUsername, true)) {
+ if (!Users.validateUsername(x.preferredUsername, true)) {
return new Error('invalid person: invalid username');
}
- if (!isValidName(x.name == '' ? null : x.name)) {
+ if (!Users.isValidName(x.name == '' ? null : x.name)) {
return new Error('invalid person: invalid name');
}
@@ -62,7 +65,7 @@ function validatePerson(x: any, uri: string) {
return new Error('invalid person: id is not a string');
}
- const idHost = toUnicode(new URL(x.id).hostname.toLowerCase());
+ const idHost = toPuny(new URL(x.id).hostname);
if (idHost !== expectHost) {
return new Error('invalid person: id has different host');
}
@@ -71,7 +74,7 @@ function validatePerson(x: any, uri: string) {
return new Error('invalid person: publicKey.id is not a string');
}
- const publicKeyIdHost = toUnicode(new URL(x.publicKey.id).hostname.toLowerCase());
+ const publicKeyIdHost = toPuny(new URL(x.publicKey.id).hostname);
if (publicKeyIdHost !== expectHost) {
return new Error('invalid person: publicKey.id has different host');
}
@@ -84,17 +87,17 @@ function validatePerson(x: any, uri: string) {
*
* Misskeyに対象のPersonが登録されていればそれを返します。
*/
-export async function fetchPerson(uri: string, resolver?: Resolver): Promise {
- if (typeof uri !== 'string') throw 'uri is not string';
+export async function fetchPerson(uri: string, resolver?: Resolver): Promise {
+ if (typeof uri !== 'string') throw new Error('uri is not string');
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) {
- const id = new mongo.ObjectID(uri.split('/').pop());
- return await User.findOne({ _id: id });
+ const id = uri.split('/').pop();
+ return await Users.findOne(id).then(x => x || null);
}
//#region このサーバーに既に登録されていたらそれを返す
- const exist = await User.findOne({ uri });
+ const exist = await Users.findOne({ uri });
if (exist) {
return exist;
@@ -107,8 +110,8 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise {
- if (typeof uri !== 'string') throw 'uri is not string';
+export async function createPerson(uri: string, resolver?: Resolver): Promise {
+ if (typeof uri !== 'string') throw new Error('uri is not string');
if (resolver == null) resolver = new Resolver();
@@ -124,24 +127,9 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
- () => undefined
- ),
- resolver.resolve(person.following).then(
- resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
- () => undefined
- ),
- resolver.resolve(person.outbox).then(
- resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
- () => undefined
- )
- ]);
+ const host = toPuny(new URL(object.id).hostname);
- const host = toUnicode(new URL(object.id).hostname.toLowerCase());
-
- const { fields, services } = analyzeAttachments(person.attachment);
+ const { fields } = analyzeAttachments(person.attachment || []);
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase());
@@ -150,39 +138,45 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise {
+ user = await transactionalEntityManager.save(new User({
+ id: genId(),
+ avatarId: null,
+ bannerId: null,
+ createdAt: new Date(),
+ lastFetchedAt: new Date(),
+ name: person.name,
+ isLocked: person.manuallyApprovesFollowers,
+ username: person.preferredUsername,
+ usernameLower: person.preferredUsername.toLowerCase(),
+ host,
+ inbox: person.inbox,
+ sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
+ featured: person.featured,
+ uri: person.id,
+ tags,
+ isBot,
+ isCat: (person as any).isCat === true
+ })) as IRemoteUser;
+
+ await transactionalEntityManager.save(new UserProfile({
+ userId: user.id,
+ description: person.summary ? fromHtml(person.summary) : null,
+ url: person.url,
+ fields,
+ userHost: host
+ }));
+
+ await transactionalEntityManager.save(new UserPublickey({
+ userId: user.id,
+ keyId: person.publicKey.id,
+ keyPem: person.publicKey.publicKeyPem
+ }));
+ });
} catch (e) {
// duplicate key error
- if (e.code === 11000) {
+ if (isDuplicateKeyValueError(e)) {
throw new Error('already registered');
}
@@ -192,83 +186,66 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise {
- Instance.update({ _id: i._id }, {
- $inc: {
- usersCount: 1
- }
- });
-
+ Instances.increment({ id: i.id }, 'usersCount', 1);
instanceChart.newUser(i.host);
});
- //#region Increment users count
- Meta.update({}, {
- $inc: {
- 'stats.usersCount': 1
- }
- }, { upsert: true });
-
- usersChart.update(user, true);
- //#endregion
+ usersChart.update(user!, true);
// ハッシュタグ更新
- for (const tag of tags) updateHashtag(user, tag, true, true);
- for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
+ for (const tag of tags) updateHashtag(user!, tag, true, true);
+ for (const tag of (user!.tags || []).filter(x => !tags.includes(x))) updateHashtag(user!, tag, true, false);
//#region アイコンとヘッダー画像をフェッチ
- const [avatar, banner] = (await Promise.all([
+ const [avatar, banner] = (await Promise.all([
person.icon,
person.image
].map(img =>
img == null
? Promise.resolve(null)
- : resolveImage(user, img).catch(() => null)
+ : resolveImage(user!, img).catch(() => null)
)));
- const avatarId = avatar ? avatar._id : null;
- const bannerId = banner ? banner._id : null;
- const avatarUrl = getDriveFileUrl(avatar, true);
- const bannerUrl = getDriveFileUrl(banner, false);
- const avatarColor = avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null;
- const bannerColor = banner && avatar.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null;
+ const avatarId = avatar ? avatar.id : null;
+ const bannerId = banner ? banner.id : null;
+ const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar) : null;
+ const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
+ const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
+ const bannerColor = banner && banner.properties.avgColor ? banner.properties.avgColor : null;
- await User.update({ _id: user._id }, {
- $set: {
- avatarId,
- bannerId,
- avatarUrl,
- bannerUrl,
- avatarColor,
- bannerColor
- }
+ await Users.update(user!.id, {
+ avatarId,
+ bannerId,
+ avatarUrl,
+ bannerUrl,
+ avatarColor,
+ bannerColor
});
- user.avatarId = avatarId;
- user.bannerId = bannerId;
- user.avatarUrl = avatarUrl;
- user.bannerUrl = bannerUrl;
- user.avatarColor = avatarColor;
- user.bannerColor = bannerColor;
+ user!.avatarId = avatarId;
+ user!.bannerId = bannerId;
+ user!.avatarUrl = avatarUrl;
+ user!.bannerUrl = bannerUrl;
+ user!.avatarColor = avatarColor;
+ user!.bannerColor = bannerColor;
//#endregion
//#region カスタム絵文字取得
- const emojis = await extractEmojis(person.tag, host).catch(e => {
+ const emojis = await extractEmojis(person.tag || [], host).catch(e => {
logger.info(`extractEmojis: ${e}`);
- return [] as IEmoji[];
+ return [] as Emoji[];
});
const emojiNames = emojis.map(emoji => emoji.name);
- await User.update({ _id: user._id }, {
- $set: {
- emojis: emojiNames
- }
+ await Users.update(user!.id, {
+ emojis: emojiNames
});
//#endregion
- await updateFeatured(user._id).catch(err => logger.error(err));
+ await updateFeatured(user!.id).catch(err => logger.error(err));
- return user;
+ return user!;
}
/**
@@ -278,8 +255,8 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise {
- if (typeof uri !== 'string') throw 'uri is not string';
+export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: object): Promise