Use PostgreSQL instead of MongoDB (#4572)

* wip

* Update note.ts

* Update timeline.ts

* Update core.ts

* wip

* Update generate-visibility-query.ts

* wip

* wip

* wip

* wip

* wip

* Update global-timeline.ts

* wip

* wip

* wip

* Update vote.ts

* wip

* wip

* Update create.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update files.ts

* wip

* wip

* Update CONTRIBUTING.md

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update read-notification.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update cancel.ts

* wip

* wip

* wip

* Update show.ts

* wip

* wip

* Update gen-id.ts

* Update create.ts

* Update id.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Docker: Update files about Docker (#4599)

* Docker: Use cache if files used by `yarn install` was not updated

This patch reduces the number of times to installing node_modules.
For example, `yarn install` step will be skipped when only ".config/default.yml" is updated.

* Docker: Migrate MongoDB to Postgresql

Misskey uses Postgresql as a database instead of Mongodb since version 11.

* Docker: Uncomment about data persistence

This patch will save a lot of databases.

* wip

* wip

* wip

* Update activitypub.ts

* wip

* wip

* wip

* Update logs.ts

* wip

* Update drive-file.ts

* Update register.ts

* wip

* wip

* Update mentions.ts

* wip

* wip

* wip

* Update recommendation.ts

* wip

* Update index.ts

* wip

* Update recommendation.ts

* Doc: Update docker.ja.md and docker.en.md (#1) (#4608)

Update how to set up misskey.

* wip

* ✌️

* wip

* Update note.ts

* Update postgre.ts

* wip

* wip

* wip

* wip

* Update add-file.ts

* wip

* wip

* wip

* Clean up

* Update logs.ts

* wip

* 🍕

* wip

* Ad notes

* wip

* Update api-visibility.ts

* Update note.ts

* Update add-file.ts

* tests

* tests

* Update postgre.ts

* Update utils.ts

* wip

* wip

* Refactor

* wip

* Refactor

* wip

* wip

* Update show-users.ts

* Update update-instance.ts

* wip

* Update feed.ts

* Update outbox.ts

* Update outbox.ts

* Update user.ts

* wip

* Update list.ts

* Update update-hashtag.ts

* wip

* Update update-hashtag.ts

* Refactor

* Update update.ts

* wip

* wip

* ✌️

* clean up

* docs

* Update push.ts

* wip

* Update api.ts

* wip

* ✌️

* Update make-pagination-query.ts

* ✌️

* Delete hashtags.ts

* Update instances.ts

* Update instances.ts

* Update create.ts

* Update search.ts

* Update reversi-game.ts

* Update signup.ts

* Update user.ts

* id

* Update example.yml

* 🎨

* objectid

* fix

* reversi

* reversi

* Fix bug of chart engine

* Add test of chart engine

* Improve test

* Better testing

* Improve chart engine

* Refactor

* Add test of chart engine

* Refactor

* Add chart test

* Fix bug

* コミットし忘れ

* Refactoring

* ✌️

* Add tests

* Add test

* Extarct note tests

* Refactor

* 存在しないユーザーにメンションできなくなっていた問題を修正

* Fix bug

* Update update-meta.ts

* Fix bug

* Update mention.vue

* Fix bug

* Update meta.ts

* Update CONTRIBUTING.md

* Fix bug

* Fix bug

* Fix bug

* Clean up

* Clean up

* Update notification.ts

* Clean up

* Add mute tests

* Add test

* Refactor

* Add test

* Fix test

* Refactor

* Refactor

* Add tests

* Update utils.ts

* Update utils.ts

* Fix test

* Update package.json

* Update update.ts

* Update manifest.ts

* Fix bug

* Fix bug

* Add test

* 🎨

* Update endpoint permissions

* Updaye permisison

* Update person.ts

#4299

* データベースと同期しないように

* Fix bug

* Fix bug

* Update reversi-game.ts

* Use a feature of Node v11.7.0 to extract a public key (#4644)

* wip

* wip

* ✌️

* Refactoring

#1540

* test

* test

* test

* test

* test

* test

* test

* Fix bug

* Fix test

* 🍣

* wip

* #4471

* Add test for #4335

* Refactor

* Fix test

* Add tests

* 🕓

* Fix bug

* Add test

* Add test

* rename

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

View file

@ -1,52 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import { pack as packUser } from './user';
const AbuseUserReport = db.get<IAbuseUserReport>('abuseUserReports');
AbuseUserReport.createIndex('userId');
AbuseUserReport.createIndex('reporterId');
AbuseUserReport.createIndex(['userId', 'reporterId'], { unique: true });
export default AbuseUserReport;
export interface IAbuseUserReport {
_id: mongo.ObjectID;
createdAt: Date;
userId: mongo.ObjectID;
reporterId: mongo.ObjectID;
comment: string;
}
export const packMany = (
reports: (string | mongo.ObjectID | IAbuseUserReport)[]
) => {
return Promise.all(reports.map(x => pack(x)));
};
export const pack = (
report: any
) => new Promise<any>(async (resolve, reject) => {
let _report: any;
if (isObjectId(report)) {
_report = await AbuseUserReport.findOne({
_id: report
});
} else if (typeof report === 'string') {
_report = await AbuseUserReport.findOne({
_id: new mongo.ObjectID(report)
});
} else {
_report = deepcopy(report);
}
// Rename _id to id
_report.id = _report._id;
delete _report._id;
_report.reporter = await packUser(_report.reporterId, null, { detail: true });
_report.user = await packUser(_report.userId, null, { detail: true });
resolve(_report);
});

View file

@ -1,16 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const AccessToken = db.get<IAccessToken>('accessTokens');
AccessToken.createIndex('token');
AccessToken.createIndex('hash');
export default AccessToken;
export type IAccessToken = {
_id: mongo.ObjectID;
createdAt: Date;
appId: mongo.ObjectID;
userId: mongo.ObjectID;
token: string;
hash: string;
};

View file

@ -1,102 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import AccessToken from './access-token';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import config from '../config';
import { dbLogger } from '../db/logger';
const App = db.get<IApp>('apps');
App.createIndex('secret');
export default App;
export type IApp = {
_id: mongo.ObjectID;
createdAt: Date;
userId: mongo.ObjectID | null;
secret: string;
name: string;
description: string;
permission: string[];
callbackUrl: string;
};
/**
* Pack an app for API response
*/
export const pack = (
app: any,
me?: any,
options?: {
detail?: boolean,
includeSecret?: boolean,
includeProfileImageIds?: boolean
}
) => new Promise<any>(async (resolve, reject) => {
const opts = Object.assign({
detail: false,
includeSecret: false,
includeProfileImageIds: false
}, options);
let _app: any;
const fields = opts.detail ? {} : {
name: true
};
// Populate the app if 'app' is ID
if (isObjectId(app)) {
_app = await App.findOne({
_id: app
});
} else if (typeof app === 'string') {
_app = await App.findOne({
_id: new mongo.ObjectID(app)
}, { fields });
} else {
_app = deepcopy(app);
}
// Me
if (me && !isObjectId(me)) {
if (typeof me === 'string') {
me = new mongo.ObjectID(me);
} else {
me = me._id;
}
}
// (データベースの欠損などで)アプリがデータベース上に見つからなかったとき
if (_app == null) {
dbLogger.warn(`[DAMAGED DB] (missing) pkg: app :: ${app}`);
return null;
}
// Rename _id to id
_app.id = _app._id;
delete _app._id;
// Visible by only owner
if (!opts.includeSecret) {
delete _app.secret;
}
_app.iconUrl = _app.icon != null
? `${config.driveUrl}/${_app.icon}`
: `${config.driveUrl}/app-default.jpg`;
if (me) {
// 既に連携しているか
const exist = await AccessToken.count({
appId: _app.id,
userId: me,
}, {
limit: 1
});
_app.isAuthorized = exist === 1;
}
resolve(_app);
});

View file

@ -1,49 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import { pack as packApp } from './app';
const AuthSession = db.get<IAuthSession>('authSessions');
export default AuthSession;
export interface IAuthSession {
_id: mongo.ObjectID;
createdAt: Date;
appId: mongo.ObjectID;
userId: mongo.ObjectID;
token: string;
}
/**
* Pack an auth session for API response
*
* @param {any} session
* @param {any} me?
* @return {Promise<any>}
*/
export const pack = (
session: any,
me?: any
) => new Promise<any>(async (resolve, reject) => {
let _session: any;
// TODO: Populate session if it ID
_session = deepcopy(session);
// Me
if (me && !isObjectId(me)) {
if (typeof me === 'string') {
me = new mongo.ObjectID(me);
} else {
me = me._id;
}
}
delete _session._id;
// Populate app
_session.app = await packApp(_session.appId, me);
resolve(_session);
});

View file

@ -1,56 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import * as deepcopy from 'deepcopy';
import { pack as packUser, IUser } from './user';
const Blocking = db.get<IBlocking>('blocking');
Blocking.createIndex('blockerId');
Blocking.createIndex('blockeeId');
Blocking.createIndex(['blockerId', 'blockeeId'], { unique: true });
export default Blocking;
export type IBlocking = {
_id: mongo.ObjectID;
createdAt: Date;
blockeeId: mongo.ObjectID;
blockerId: mongo.ObjectID;
};
export const packMany = (
blockings: (string | mongo.ObjectID | IBlocking)[],
me?: string | mongo.ObjectID | IUser
) => {
return Promise.all(blockings.map(x => pack(x, me)));
};
export const pack = (
blocking: any,
me?: any
) => new Promise<any>(async (resolve, reject) => {
let _blocking: any;
// Populate the blocking if 'blocking' is ID
if (isObjectId(blocking)) {
_blocking = await Blocking.findOne({
_id: blocking
});
} else if (typeof blocking === 'string') {
_blocking = await Blocking.findOne({
_id: new mongo.ObjectID(blocking)
});
} else {
_blocking = deepcopy(blocking);
}
// Rename _id to id
_blocking.id = _blocking._id;
delete _blocking._id;
// Populate blockee
_blocking.blockee = await packUser(_blocking.blockeeId, me, {
detail: true
});
resolve(_blocking);
});

View file

@ -1,29 +0,0 @@
import * as mongo from 'mongodb';
import monkDb, { nativeDbConn } from '../db/mongodb';
const DriveFileThumbnail = monkDb.get<IDriveFileThumbnail>('driveFileThumbnails.files');
DriveFileThumbnail.createIndex('metadata.originalId', { sparse: true, unique: true });
export default DriveFileThumbnail;
export const DriveFileThumbnailChunk = monkDb.get('driveFileThumbnails.chunks');
export const getDriveFileThumbnailBucket = async (): Promise<mongo.GridFSBucket> => {
const db = await nativeDbConn();
const bucket = new mongo.GridFSBucket(db, {
bucketName: 'driveFileThumbnails'
});
return bucket;
};
export type IMetadata = {
originalId: mongo.ObjectID;
};
export type IDriveFileThumbnail = {
_id: mongo.ObjectID;
uploadDate: Date;
md5: string;
filename: string;
contentType: string;
metadata: IMetadata;
};

View file

@ -1,29 +0,0 @@
import * as mongo from 'mongodb';
import monkDb, { nativeDbConn } from '../db/mongodb';
const DriveFileWebpublic = monkDb.get<IDriveFileWebpublic>('driveFileWebpublics.files');
DriveFileWebpublic.createIndex('metadata.originalId', { sparse: true, unique: true });
export default DriveFileWebpublic;
export const DriveFileWebpublicChunk = monkDb.get('driveFileWebpublics.chunks');
export const getDriveFileWebpublicBucket = async (): Promise<mongo.GridFSBucket> => {
const db = await nativeDbConn();
const bucket = new mongo.GridFSBucket(db, {
bucketName: 'driveFileWebpublics'
});
return bucket;
};
export type IMetadata = {
originalId: mongo.ObjectID;
};
export type IDriveFileWebpublic = {
_id: mongo.ObjectID;
uploadDate: Date;
md5: string;
filename: string;
contentType: string;
metadata: IMetadata;
};

View file

@ -1,232 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import { pack as packFolder } from './drive-folder';
import { pack as packUser } from './user';
import monkDb, { nativeDbConn } from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import getDriveFileUrl, { getOriginalUrl } from '../misc/get-drive-file-url';
import { dbLogger } from '../db/logger';
const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
DriveFile.createIndex('md5');
DriveFile.createIndex('metadata.uri');
DriveFile.createIndex('metadata.userId');
DriveFile.createIndex('metadata.folderId');
DriveFile.createIndex('metadata._user.host');
export default DriveFile;
export const DriveFileChunk = monkDb.get('driveFiles.chunks');
export const getDriveFileBucket = async (): Promise<mongo.GridFSBucket> => {
const db = await nativeDbConn();
const bucket = new mongo.GridFSBucket(db, {
bucketName: 'driveFiles'
});
return bucket;
};
export type IMetadata = {
properties: any;
userId: mongo.ObjectID;
_user: any;
folderId: mongo.ObjectID;
comment: string;
/**
* URL
*/
uri?: string;
/**
* URL for web() or original
* * or
*/
url?: string;
/**
* URL for thumbnail (thumbnailがなければなし)
* * or
*/
thumbnailUrl?: string;
/**
* URL for original (web用が生成されてない場合はurlがoriginalを指す)
* * or
*/
webpublicUrl?: string;
accessKey?: string;
src?: string;
deletedAt?: Date;
/**
* MongoDB内に保存されていないか否か
* or
* true
*/
withoutChunks?: boolean;
storage?: string;
/***
* ObjectStorage
*/
storageProps?: IStorageProps;
isSensitive?: boolean;
/**
* 稿ID一覧
*/
attachedNoteIds?: mongo.ObjectID[];
/**
* ()URLへの直リンクか否か
*/
isRemote?: boolean;
};
export type IStorageProps = {
/**
* ObjectStorage key for original
*/
key: string;
/***
* ObjectStorage key for thumbnail (thumbnailがなければなし)
*/
thumbnailKey?: string;
/***
* ObjectStorage key for webpublic (webpublicがなければなし)
*/
webpublicKey?: string;
id?: string;
};
export type IDriveFile = {
_id: mongo.ObjectID;
uploadDate: Date;
md5: string;
filename: string;
contentType: string;
metadata: IMetadata;
/**
*
*/
length: number;
};
export function validateFileName(name: string): boolean {
return (
(name.trim().length > 0) &&
(name.length <= 200) &&
(name.indexOf('\\') === -1) &&
(name.indexOf('/') === -1) &&
(name.indexOf('..') === -1)
);
}
export const packMany = (
files: any[],
options?: {
detail?: boolean
self?: boolean,
withUser?: boolean,
}
) => {
return Promise.all(files.map(f => pack(f, options)));
};
/**
* Pack a drive file for API response
*/
export const pack = (
file: any,
options?: {
detail?: boolean,
self?: boolean,
withUser?: boolean,
}
) => new Promise<any>(async (resolve, reject) => {
const opts = Object.assign({
detail: false,
self: false
}, options);
let _file: any;
// Populate the file if 'file' is ID
if (isObjectId(file)) {
_file = await DriveFile.findOne({
_id: file
});
} else if (typeof file === 'string') {
_file = await DriveFile.findOne({
_id: new mongo.ObjectID(file)
});
} else {
_file = deepcopy(file);
}
// (データベースの欠損などで)ファイルがデータベース上に見つからなかったとき
if (_file == null) {
dbLogger.warn(`[DAMAGED DB] (missing) pkg: driveFile :: ${file}`);
return resolve(null);
}
// rendered target
let _target: any = {};
_target.id = _file._id;
_target.createdAt = _file.uploadDate;
_target.name = _file.filename;
_target.type = _file.contentType;
_target.datasize = _file.length;
_target.md5 = _file.md5;
_target = Object.assign(_target, _file.metadata);
_target.url = getDriveFileUrl(_file);
_target.thumbnailUrl = getDriveFileUrl(_file, true);
_target.isRemote = _file.metadata.isRemote;
if (_target.properties == null) _target.properties = {};
if (opts.detail) {
if (_target.folderId) {
// Populate folder
_target.folder = await packFolder(_target.folderId, {
detail: true
});
}
/*
if (_target.tags) {
// Populate tags
_target.tags = await _target.tags.map(async (tag: any) =>
await serializeDriveTag(tag)
);
}
*/
}
if (opts.withUser) {
// Populate user
_target.user = await packUser(_file.metadata.userId);
}
delete _target.withoutChunks;
delete _target.storage;
delete _target.storageProps;
delete _target.isRemote;
delete _target._user;
if (opts.self) {
_target.url = getOriginalUrl(_file);
}
resolve(_target);
});

View file

@ -1,75 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import DriveFile from './drive-file';
const DriveFolder = db.get<IDriveFolder>('driveFolders');
DriveFolder.createIndex('userId');
export default DriveFolder;
export type IDriveFolder = {
_id: mongo.ObjectID;
createdAt: Date;
name: string;
userId: mongo.ObjectID;
parentId: mongo.ObjectID;
};
export function isValidFolderName(name: string): boolean {
return (
(name.trim().length > 0) &&
(name.length <= 200)
);
}
/**
* Pack a drive folder for API response
*/
export const pack = (
folder: any,
options?: {
detail: boolean
}
) => new Promise<any>(async (resolve, reject) => {
const opts = Object.assign({
detail: false
}, options);
let _folder: any;
// Populate the folder if 'folder' is ID
if (isObjectId(folder)) {
_folder = await DriveFolder.findOne({ _id: folder });
} else if (typeof folder === 'string') {
_folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) });
} else {
_folder = deepcopy(folder);
}
// Rename _id to id
_folder.id = _folder._id;
delete _folder._id;
if (opts.detail) {
const childFoldersCount = await DriveFolder.count({
parentId: _folder.id
});
const childFilesCount = await DriveFile.count({
'metadata.folderId': _folder.id
});
_folder.foldersCount = childFoldersCount;
_folder.filesCount = childFilesCount;
}
if (opts.detail && _folder.parentId) {
// Populate parent folder
_folder.parent = await pack(_folder.parentId, {
detail: true
});
}
resolve(_folder);
});

View file

@ -1,21 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const Emoji = db.get<IEmoji>('emoji');
Emoji.createIndex('name');
Emoji.createIndex('host');
Emoji.createIndex(['name', 'host'], { unique: true });
export default Emoji;
export type IEmoji = {
_id: mongo.ObjectID;
name: string;
host: string;
url: string;
aliases?: string[];
updatedAt?: Date;
/** AP object id */
uri?: string;
type?: string;
};

View file

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

View file

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

View file

@ -0,0 +1,60 @@
import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class App {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the App.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The owner ID.'
})
public userId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'SET NULL',
nullable: true,
})
public user: User | null;
@Index()
@Column('varchar', {
length: 64,
comment: 'The secret key of the App.'
})
public secret: string;
@Column('varchar', {
length: 128,
comment: 'The name of the App.'
})
public name: string;
@Column('varchar', {
length: 512,
comment: 'The description of the App.'
})
public description: string;
@Column('varchar', {
length: 64, array: true,
comment: 'The permission of the App.'
})
public permission: string[];
@Column('varchar', {
length: 256, nullable: true,
comment: 'The callbackUrl of the App.'
})
public callbackUrl: string | null;
}

View file

@ -0,0 +1,39 @@
import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user';
import { App } from './app';
import { id } from '../id';
@Entity()
export class AuthSession {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the AuthSession.'
})
public createdAt: Date;
@Index()
@Column('varchar', {
length: 128
})
public token: string;
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column(id())
public appId: App['id'];
@ManyToOne(type => App, {
onDelete: 'CASCADE'
})
@JoinColumn()
public app: App | null;
}

View file

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

View file

@ -0,0 +1,154 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { DriveFolder } from './drive-folder';
import { id } from '../id';
@Entity()
export class DriveFile {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the DriveFile.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The owner ID.'
})
public userId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'SET NULL'
})
@JoinColumn()
public user: User | null;
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: 'The host of owner. It will be null if the user in local.'
})
public userHost: string | null;
@Index()
@Column('varchar', {
length: 32,
comment: 'The MD5 hash of the DriveFile.'
})
public md5: string;
@Column('varchar', {
length: 256,
comment: 'The file name of the DriveFile.'
})
public name: string;
@Index()
@Column('varchar', {
length: 128,
comment: 'The content type (MIME) of the DriveFile.'
})
public type: string;
@Column('integer', {
comment: 'The file size (bytes) of the DriveFile.'
})
public size: number;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The comment of the DriveFile.'
})
public comment: string | null;
@Column('jsonb', {
default: {},
comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
})
public properties: Record<string, any>;
@Column('boolean')
public storedInternal: boolean;
@Column('varchar', {
length: 512,
comment: 'The URL of the DriveFile.'
})
public url: string;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The URL of the thumbnail of the DriveFile.'
})
public thumbnailUrl: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The URL of the webpublic of the DriveFile.'
})
public webpublicUrl: string | null;
@Index({ unique: true })
@Column('varchar', {
length: 256,
})
public accessKey: string;
@Index({ unique: true })
@Column('varchar', {
length: 256, nullable: true,
})
public thumbnailAccessKey: string | null;
@Index({ unique: true })
@Column('varchar', {
length: 256, nullable: true,
})
public webpublicAccessKey: string | null;
@Index()
@Column('varchar', {
length: 512, nullable: true,
comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.'
})
public uri: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public src: string | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The parent folder ID. If null, it means the DriveFile is located in root.'
})
public folderId: DriveFolder['id'] | null;
@ManyToOne(type => DriveFolder, {
onDelete: 'SET NULL'
})
@JoinColumn()
public folder: DriveFolder | null;
@Column('boolean', {
default: false,
comment: 'Whether the DriveFile is NSFW.'
})
public isSensitive: boolean;
/**
* ()URLへの直リンクか否か
*/
@Column('boolean', {
default: false,
comment: 'Whether the DriveFile is direct link to remote server.'
})
public isRemote: boolean;
}

View file

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

View file

@ -0,0 +1,46 @@
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
import { id } from '../id';
@Entity()
@Index(['name', 'host'], { unique: true })
export class Emoji {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
nullable: true
})
public updatedAt: Date | null;
@Index()
@Column('varchar', {
length: 128
})
public name: string;
@Index()
@Column('varchar', {
length: 128, nullable: true
})
public host: string | null;
@Column('varchar', {
length: 256,
})
public url: string;
@Column('varchar', {
length: 256, nullable: true
})
public uri: string | null;
@Column('varchar', {
length: 64, nullable: true
})
public type: string | null;
@Column('varchar', {
array: true, length: 128, default: '{}'
})
public aliases: string[];
}

View file

@ -0,0 +1,85 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
@Index(['followerId', 'followeeId'], { unique: true })
export class FollowRequest {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the FollowRequest.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The followee user ID.'
})
public followeeId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public followee: User | null;
@Index()
@Column({
...id(),
comment: 'The follower user ID.'
})
public followerId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public follower: User | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'id of Follow Activity.'
})
public requestId: string | null;
//#region Denormalized fields
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public followerHost: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: '[Denormalized]'
})
public followerInbox: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: '[Denormalized]'
})
public followerSharedInbox: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public followeeHost: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: '[Denormalized]'
})
public followeeInbox: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: '[Denormalized]'
})
public followeeSharedInbox: string | null;
//#endregion
}

View file

@ -0,0 +1,80 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
@Index(['followerId', 'followeeId'], { unique: true })
export class Following {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Following.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The followee user ID.'
})
public followeeId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public followee: User | null;
@Index()
@Column({
...id(),
comment: 'The follower user ID.'
})
public followerId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public follower: User | null;
//#region Denormalized fields
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public followerHost: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: '[Denormalized]'
})
public followerInbox: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: '[Denormalized]'
})
public followerSharedInbox: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public followeeHost: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: '[Denormalized]'
})
public followeeInbox: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: '[Denormalized]'
})
public followeeSharedInbox: string | null;
//#endregion
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,46 @@
import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
import { id } from '../id';
@Entity()
export class Log {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Log.'
})
public createdAt: Date;
@Index()
@Column('varchar', {
length: 64, array: true, default: '{}'
})
public domain: string[];
@Index()
@Column('enum', {
enum: ['error', 'warning', 'info', 'success', 'debug']
})
public level: string;
@Column('varchar', {
length: 8
})
public worker: string;
@Column('varchar', {
length: 128
})
public machine: string;
@Column('varchar', {
length: 1024
})
public message: string;
@Column('jsonb', {
default: {}
})
public data: Record<string, any>;
}

View file

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

264
src/models/entities/meta.ts Normal file
View file

@ -0,0 +1,264 @@
import { Entity, Column, PrimaryColumn } from 'typeorm';
import { id } from '../id';
@Entity()
export class Meta {
@PrimaryColumn(id())
public id: string;
@Column('varchar', {
length: 128, nullable: true
})
public name: string | null;
@Column('varchar', {
length: 1024, nullable: true
})
public description: string | null;
/**
*
*/
@Column('varchar', {
length: 128, nullable: true
})
public maintainerName: string | null;
/**
*
*/
@Column('varchar', {
length: 128, nullable: true
})
public maintainerEmail: string | null;
@Column('jsonb', {
default: [],
})
public announcements: Record<string, any>[];
@Column('boolean', {
default: false,
})
public disableRegistration: boolean;
@Column('boolean', {
default: false,
})
public disableLocalTimeline: boolean;
@Column('boolean', {
default: false,
})
public disableGlobalTimeline: boolean;
@Column('boolean', {
default: true,
})
public enableEmojiReaction: boolean;
@Column('boolean', {
default: false,
})
public useStarForReactionFallback: boolean;
@Column('varchar', {
length: 64, array: true, default: '{}'
})
public langs: string[];
@Column('varchar', {
length: 256, array: true, default: '{}'
})
public hiddenTags: string[];
@Column('varchar', {
length: 256, array: true, default: '{}'
})
public blockedHosts: string[];
@Column('varchar', {
length: 256,
nullable: true,
default: '/assets/ai.png'
})
public mascotImageUrl: string | null;
@Column('varchar', {
length: 256,
nullable: true
})
public bannerUrl: string | null;
@Column('varchar', {
length: 256,
nullable: true,
default: 'https://ai.misskey.xyz/aiart/yubitun.png'
})
public errorImageUrl: string | null;
@Column('varchar', {
length: 256,
nullable: true
})
public iconUrl: string | null;
@Column('boolean', {
default: true,
})
public cacheRemoteFiles: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public proxyAccount: string | null;
@Column('boolean', {
default: false,
})
public enableRecaptcha: boolean;
@Column('varchar', {
length: 64,
nullable: true
})
public recaptchaSiteKey: string | null;
@Column('varchar', {
length: 64,
nullable: true
})
public recaptchaSecretKey: string | null;
@Column('integer', {
default: 1024,
comment: 'Drive capacity of a local user (MB)'
})
public localDriveCapacityMb: number;
@Column('integer', {
default: 32,
comment: 'Drive capacity of a remote user (MB)'
})
public remoteDriveCapacityMb: number;
@Column('integer', {
default: 500,
comment: 'Max allowed note text length in characters'
})
public maxNoteTextLength: number;
@Column('varchar', {
length: 128,
nullable: true
})
public summalyProxy: string | null;
@Column('boolean', {
default: false,
})
public enableEmail: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public email: string | null;
@Column('boolean', {
default: false,
})
public smtpSecure: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public smtpHost: string | null;
@Column('integer', {
nullable: true
})
public smtpPort: number | null;
@Column('varchar', {
length: 128,
nullable: true
})
public smtpUser: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public smtpPass: string | null;
@Column('boolean', {
default: false,
})
public enableServiceWorker: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public swPublicKey: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public swPrivateKey: string | null;
@Column('boolean', {
default: false,
})
public enableTwitterIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public twitterConsumerKey: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public twitterConsumerSecret: string | null;
@Column('boolean', {
default: false,
})
public enableGithubIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public githubClientId: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public githubClientSecret: string | null;
@Column('boolean', {
default: false,
})
public enableDiscordIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public discordClientId: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public discordClientSecret: string | null;
}

View file

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

View file

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

View file

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

View file

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

View file

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

236
src/models/entities/note.ts Normal file
View file

@ -0,0 +1,236 @@
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { User } from './user';
import { App } from './app';
import { DriveFile } from './drive-file';
import { id } from '../id';
@Entity()
export class Note {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Note.'
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
nullable: true,
comment: 'The updated date of the Note.'
})
public updatedAt: Date | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of reply target.'
})
public replyId: Note['id'] | null;
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public reply: Note | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of renote target.'
})
public renoteId: Note['id'] | null;
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public renote: Note | null;
@Column({
type: 'text', nullable: true
})
public text: string | null;
@Column('varchar', {
length: 256, nullable: true
})
public name: string | null;
@Column('varchar', {
length: 512, nullable: true
})
public cw: string | null;
@Column({
...id(),
nullable: true
})
public appId: App['id'] | null;
@ManyToOne(type => App, {
onDelete: 'SET NULL'
})
@JoinColumn()
public app: App | null;
@Index()
@Column({
...id(),
comment: 'The ID of author.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('boolean', {
default: false
})
public viaMobile: boolean;
@Column('boolean', {
default: false
})
public localOnly: boolean;
@Column('integer', {
default: 0
})
public renoteCount: number;
@Column('integer', {
default: 0
})
public repliesCount: number;
@Column('jsonb', {
default: {}
})
public reactions: Record<string, number>;
/**
* public ...
* home ... ()
* followers ...
* specified ... visibleUserIds
*/
@Column('enum', { enum: ['public', 'home', 'followers', 'specified'] })
public visibility: 'public' | 'home' | 'followers' | 'specified';
@Index({ unique: true })
@Column('varchar', {
length: 256, nullable: true,
comment: 'The URI of a note. it will be null when the note is local.'
})
public uri: string | null;
@Column('integer', {
default: 0
})
public score: number;
@Column({
...id(),
array: true, default: '{}'
})
public fileIds: DriveFile['id'][];
@Column('varchar', {
length: 256, array: true, default: '{}'
})
public attachedFileTypes: string[];
@Index()
@Column({
...id(),
array: true, default: '{}'
})
public visibleUserIds: User['id'][];
@Index()
@Column({
...id(),
array: true, default: '{}'
})
public mentions: User['id'][];
@Column('text', {
default: '[]'
})
public mentionedRemoteUsers: string;
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public emojis: string[];
@Index()
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public tags: string[];
@Column('boolean', {
default: false
})
public hasPoll: boolean;
@Column('jsonb', {
nullable: true, default: {}
})
public geo: any | null;
//#region Denormalized fields
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userHost: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userInbox: string | null;
@Column({
...id(),
nullable: true,
comment: '[Denormalized]'
})
public replyUserId: User['id'] | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public replyUserHost: string | null;
@Column({
...id(),
nullable: true,
comment: '[Denormalized]'
})
public renoteUserId: User['id'] | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public renoteUserHost: string | null;
//#endregion
}
export type IMentionedRemoteUsers = {
uri: string;
username: string;
host: string;
}[];

View file

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

View file

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

View file

@ -0,0 +1,67 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
import { id } from '../id';
import { Note } from './note';
import { User } from './user';
@Entity()
export class Poll {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column(id())
public noteId: Note['id'];
@OneToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
@Column('timestamp with time zone', {
nullable: true
})
public expiresAt: Date | null;
@Column('boolean')
public multiple: boolean;
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public choices: string[];
@Column('integer', {
array: true,
})
public votes: number[];
//#region Denormalized fields
@Column('enum', {
enum: ['public', 'home', 'followers', 'specified'],
comment: '[Denormalized]'
})
public noteVisibility: 'public' | 'home' | 'followers' | 'specified';
@Index()
@Column({
...id(),
comment: '[Denormalized]'
})
public userId: User['id'];
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userHost: string | null;
//#endregion
}
export type IPoll = {
choices: string[];
votes?: number[];
multiple: boolean;
expiresAt: Date;
};

View file

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

View file

@ -0,0 +1,35 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class Signin {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the Signin.'
})
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128,
})
public ip: string;
@Column('jsonb')
public headers: Record<string, any>;
@Column('boolean')
public success: boolean;
}

View file

@ -0,0 +1,37 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class SwSubscription {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 256,
})
public endpoint: string;
@Column('varchar', {
length: 256,
})
public auth: string;
@Column('varchar', {
length: 128,
})
public publickey: string;
}

View file

@ -0,0 +1,24 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserKeypair {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column(id())
public userId: User['id'];
@OneToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 4096,
})
public keyPem: string;
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,30 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserPublickey {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column(id())
public userId: User['id'];
@OneToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index({ unique: true })
@Column('varchar', {
length: 256,
})
public keyId: string;
@Column('varchar', {
length: 4096,
})
public keyPem: string;
}

View file

@ -0,0 +1,108 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserServiceLinking {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column(id())
public userId: User['id'];
@OneToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('boolean', {
default: false,
})
public twitter: boolean;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterAccessToken: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterAccessTokenSecret: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterUserId: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterScreenName: string | null;
@Column('boolean', {
default: false,
})
public github: boolean;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public githubAccessToken: string | null;
@Column('integer', {
nullable: true, default: null,
})
public githubId: number | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public githubLogin: string | null;
@Column('boolean', {
default: false,
})
public discord: boolean;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordAccessToken: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordRefreshToken: string | null;
@Column('integer', {
nullable: true, default: null,
})
public discordExpiresDate: number | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordId: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordUsername: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordDiscriminator: string | null;
//#region Denormalized fields
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userHost: string | null;
//#endregion
}

297
src/models/entities/user.ts Normal file
View file

@ -0,0 +1,297 @@
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { DriveFile } from './drive-file';
import { id } from '../id';
@Entity()
@Index(['usernameLower', 'host'], { unique: true })
export class User {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the User.'
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
nullable: true,
comment: 'The updated date of the User.'
})
public updatedAt: Date | null;
@Column('timestamp with time zone', {
nullable: true
})
public lastFetchedAt: Date | null;
@Column('varchar', {
length: 128,
comment: 'The username of the User.'
})
public username: string;
@Index()
@Column('varchar', {
length: 128, select: false,
comment: 'The username (lowercased) of the User.'
})
public usernameLower: string;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The name of the User.'
})
public name: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The location of the User.'
})
public location: string | null;
@Column('char', {
length: 10, nullable: true,
comment: 'The birthday (YYYY-MM-DD) of the User.'
})
public birthday: string | null;
@Column('integer', {
default: 0,
comment: 'The count of followers.'
})
public followersCount: number;
@Column('integer', {
default: 0,
comment: 'The count of following.'
})
public followingCount: number;
@Column('integer', {
default: 0,
comment: 'The count of notes.'
})
public notesCount: number;
@Column({
...id(),
nullable: true,
comment: 'The ID of avatar DriveFile.'
})
public avatarId: DriveFile['id'] | null;
@OneToOne(type => DriveFile, {
onDelete: 'SET NULL'
})
@JoinColumn()
public avatar: DriveFile | null;
@Column({
...id(),
nullable: true,
comment: 'The ID of banner DriveFile.'
})
public bannerId: DriveFile['id'] | null;
@OneToOne(type => DriveFile, {
onDelete: 'SET NULL'
})
@JoinColumn()
public banner: DriveFile | null;
@Column('varchar', {
length: 1024, nullable: true,
comment: 'The description (bio) of the User.'
})
public description: string | null;
@Index()
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public tags: string[];
@Column('varchar', {
length: 128, nullable: true,
comment: 'The email address of the User.'
})
public email: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public emailVerifyCode: string | null;
@Column('boolean', {
default: false,
})
public emailVerified: boolean;
@Column('varchar', {
length: 128, nullable: true,
})
public twoFactorTempSecret: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public twoFactorSecret: string | null;
@Column('varchar', {
length: 256, nullable: true,
})
public avatarUrl: string | null;
@Column('varchar', {
length: 256, nullable: true,
})
public bannerUrl: string | null;
@Column('varchar', {
length: 32, nullable: true,
})
public avatarColor: string | null;
@Column('varchar', {
length: 32, nullable: true,
})
public bannerColor: string | null;
@Column('boolean', {
default: false,
comment: 'Whether the User is suspended.'
})
public isSuspended: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is silenced.'
})
public isSilenced: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is locked.'
})
public isLocked: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is a bot.'
})
public isBot: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is a cat.'
})
public isCat: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is the admin.'
})
public isAdmin: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is a moderator.'
})
public isModerator: boolean;
@Column('boolean', {
default: false,
})
public isVerified: boolean;
@Column('boolean', {
default: false,
})
public twoFactorEnabled: boolean;
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public emojis: string[];
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: 'The host of the User. It will be null if the origin of the user is local.'
})
public host: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: 'The inbox of the User. It will be null if the origin of the user is local.'
})
public inbox: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: 'The sharedInbox of the User. It will be null if the origin of the user is local.'
})
public sharedInbox: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: 'The featured of the User. It will be null if the origin of the user is local.'
})
public featured: string | null;
@Index()
@Column('varchar', {
length: 256, nullable: true,
comment: 'The URI of the User. It will be null if the origin of the user is local.'
})
public uri: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The password hash of the User. It will be null if the origin of the user is local.'
})
public password: string | null;
@Index({ unique: true })
@Column('varchar', {
length: 32, nullable: true, unique: true,
comment: 'The native access token of the User. It will be null if the origin of the user is local.'
})
public token: string | null;
@Column('jsonb', {
default: {},
comment: 'The client-specific data of the User.'
})
public clientData: Record<string, any>;
@Column('boolean', {
default: false,
})
public autoWatch: boolean;
@Column('boolean', {
default: false,
})
public autoAcceptFollowed: boolean;
@Column('boolean', {
default: false,
})
public alwaysMarkNsfw: boolean;
@Column('boolean', {
default: false,
})
public carefulBot: boolean;
}
export interface ILocalUser extends User {
host: null;
}
export interface IRemoteUser extends User {
host: string;
}

View file

@ -1,65 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import { pack as packNote } from './note';
import { dbLogger } from '../db/logger';
const Favorite = db.get<IFavorite>('favorites');
Favorite.createIndex('userId');
Favorite.createIndex(['userId', 'noteId'], { unique: true });
export default Favorite;
export type IFavorite = {
_id: mongo.ObjectID;
createdAt: Date;
userId: mongo.ObjectID;
noteId: mongo.ObjectID;
};
export const packMany = (
favorites: any[],
me: any
) => {
return Promise.all(favorites.map(f => pack(f, me)));
};
/**
* Pack a favorite for API response
*/
export const pack = (
favorite: any,
me: any
) => new Promise<any>(async (resolve, reject) => {
let _favorite: any;
// Populate the favorite if 'favorite' is ID
if (isObjectId(favorite)) {
_favorite = await Favorite.findOne({
_id: favorite
});
} else if (typeof favorite === 'string') {
_favorite = await Favorite.findOne({
_id: new mongo.ObjectID(favorite)
});
} else {
_favorite = deepcopy(favorite);
}
// Rename _id to id
_favorite.id = _favorite._id;
delete _favorite._id;
// Populate note
_favorite.note = await packNote(_favorite.noteId, me, {
detail: true
});
// (データベースの不具合などで)投稿が見つからなかったら
if (_favorite.note == null) {
dbLogger.warn(`[DAMAGED DB] (missing) pkg: favorite -> note :: ${_favorite.id} (note ${_favorite.noteId})`);
return resolve(null);
}
resolve(_favorite);
});

View file

@ -1,66 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import { pack as packUser } from './user';
const FollowRequest = db.get<IFollowRequest>('followRequests');
FollowRequest.createIndex('followerId');
FollowRequest.createIndex('followeeId');
FollowRequest.createIndex(['followerId', 'followeeId'], { unique: true });
export default FollowRequest;
export type IFollowRequest = {
_id: mongo.ObjectID;
createdAt: Date;
followeeId: mongo.ObjectID;
followerId: mongo.ObjectID;
requestId?: string; // id of Follow Activity
// 非正規化
_followee: {
host: string;
inbox?: string;
sharedInbox?: string;
},
_follower: {
host: string;
inbox?: string;
sharedInbox?: string;
}
};
/**
* Pack a request for API response
*/
export const pack = (
request: any,
me?: any
) => new Promise<any>(async (resolve, reject) => {
let _request: any;
// Populate the request if 'request' is ID
if (isObjectId(request)) {
_request = await FollowRequest.findOne({
_id: request
});
} else if (typeof request === 'string') {
_request = await FollowRequest.findOne({
_id: new mongo.ObjectID(request)
});
} else {
_request = deepcopy(request);
}
// Rename _id to id
_request.id = _request._id;
delete _request._id;
// Populate follower
_request.follower = await packUser(_request.followerId, me);
// Populate followee
_request.followee = await packUser(_request.followeeId, me);
resolve(_request);
});

View file

@ -1,27 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const Following = db.get<IFollowing>('following');
Following.createIndex('followerId');
Following.createIndex('followeeId');
Following.createIndex(['followerId', 'followeeId'], { unique: true });
export default Following;
export type IFollowing = {
_id: mongo.ObjectID;
createdAt: Date;
followeeId: mongo.ObjectID;
followerId: mongo.ObjectID;
// 非正規化
_followee: {
host: string;
inbox?: string;
sharedInbox?: string;
},
_follower: {
host: string;
inbox?: string;
sharedInbox?: string;
}
};

View file

@ -1,111 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../../../db/mongodb';
import isObjectId from '../../../misc/is-objectid';
import { IUser, pack as packUser } from '../../user';
const ReversiGame = db.get<IReversiGame>('reversiGames');
export default ReversiGame;
export interface IReversiGame {
_id: mongo.ObjectID;
createdAt: Date;
startedAt: Date;
user1Id: mongo.ObjectID;
user2Id: mongo.ObjectID;
user1Accepted: boolean;
user2Accepted: boolean;
/**
* ()
* 1 ... user1
* 2 ... user2
*/
black: number;
isStarted: boolean;
isEnded: boolean;
winnerId: mongo.ObjectID;
surrendered: mongo.ObjectID;
logs: {
at: Date;
color: boolean;
pos: number;
}[];
settings: {
map: string[];
bw: string | number;
isLlotheo: boolean;
canPutEverywhere: boolean;
loopedBoard: boolean;
};
form1: any;
form2: any;
// ログのposを文字列としてすべて連結したもののCRC32値
crc32: string;
}
/**
* Pack an reversi game for API response
*/
export const pack = (
game: any,
me?: string | mongo.ObjectID | IUser,
options?: {
detail?: boolean
}
) => new Promise<any>(async (resolve, reject) => {
const opts = Object.assign({
detail: true
}, options);
let _game: any;
// Populate the game if 'game' is ID
if (isObjectId(game)) {
_game = await ReversiGame.findOne({
_id: game
});
} else if (typeof game === 'string') {
_game = await ReversiGame.findOne({
_id: new mongo.ObjectID(game)
});
} else {
_game = deepcopy(game);
}
// Me
const meId: mongo.ObjectID = me
? isObjectId(me)
? me as mongo.ObjectID
: typeof me === 'string'
? new mongo.ObjectID(me)
: (me as IUser)._id
: null;
// Rename _id to id
_game.id = _game._id;
delete _game._id;
if (opts.detail === false) {
delete _game.logs;
delete _game.settings.map;
} else {
// 互換性のため
if (_game.settings.map.hasOwnProperty('size')) {
_game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g'));
}
}
// Populate user
_game.user1 = await packUser(_game.user1Id, meId);
_game.user2 = await packUser(_game.user2Id, meId);
if (_game.winnerId) {
_game.winner = await packUser(_game.winnerId, meId);
} else {
_game.winner = null;
}
resolve(_game);
});

View file

@ -1,45 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../../../db/mongodb';
import isObjectId from '../../../misc/is-objectid';
import { IUser, pack as packUser } from '../../user';
const Matching = db.get<IMatching>('reversiMatchings');
export default Matching;
export interface IMatching {
_id: mongo.ObjectID;
createdAt: Date;
parentId: mongo.ObjectID;
childId: mongo.ObjectID;
}
/**
* Pack an reversi matching for API response
*/
export const pack = (
matching: any,
me?: string | mongo.ObjectID | IUser
) => new Promise<any>(async (resolve, reject) => {
// Me
const meId: mongo.ObjectID = me
? isObjectId(me)
? me as mongo.ObjectID
: typeof me === 'string'
? new mongo.ObjectID(me)
: (me as IUser)._id
: null;
const _matching = deepcopy(matching);
// Rename _id to id
_matching.id = _matching._id;
delete _matching._id;
// Populate user
_matching.parent = await packUser(_matching.parentId, meId);
_matching.child = await packUser(_matching.childId, meId);
resolve(_matching);
});

View file

@ -1,63 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const Hashtag = db.get<IHashtags>('hashtags');
Hashtag.createIndex('tag', { unique: true });
Hashtag.createIndex('mentionedUsersCount');
Hashtag.createIndex('mentionedLocalUsersCount');
Hashtag.createIndex('mentionedRemoteUsersCount');
Hashtag.createIndex('attachedUsersCount');
Hashtag.createIndex('attachedLocalUsersCount');
Hashtag.createIndex('attachedRemoteUsersCount');
export default Hashtag;
// 後方互換性のため
Hashtag.findOne({ attachedUserIds: { $exists: false }}).then(h => {
if (h != null) {
Hashtag.update({}, {
$rename: {
mentionedUserIdsCount: 'mentionedUsersCount'
},
$set: {
mentionedLocalUserIds: [],
mentionedLocalUsersCount: 0,
attachedUserIds: [],
attachedUsersCount: 0,
attachedLocalUserIds: [],
attachedLocalUsersCount: 0,
}
}, {
multi: true
});
}
});
Hashtag.findOne({ attachedRemoteUserIds: { $exists: false }}).then(h => {
if (h != null) {
Hashtag.update({}, {
$set: {
mentionedRemoteUserIds: [],
mentionedRemoteUsersCount: 0,
attachedRemoteUserIds: [],
attachedRemoteUsersCount: 0,
}
}, {
multi: true
});
}
});
export interface IHashtags {
tag: string;
mentionedUserIds: mongo.ObjectID[];
mentionedUsersCount: number;
mentionedLocalUserIds: mongo.ObjectID[];
mentionedLocalUsersCount: number;
mentionedRemoteUserIds: mongo.ObjectID[];
mentionedRemoteUsersCount: number;
attachedUserIds: mongo.ObjectID[];
attachedUsersCount: number;
attachedLocalUserIds: mongo.ObjectID[];
attachedLocalUsersCount: number;
attachedRemoteUserIds: mongo.ObjectID[];
attachedRemoteUsersCount: number;
}

4
src/models/id.ts Normal file
View file

@ -0,0 +1,4 @@
export const id = () => ({
type: 'varchar' as 'varchar',
length: 32
});

74
src/models/index.ts Normal file
View file

@ -0,0 +1,74 @@
import { getRepository, getCustomRepository } from 'typeorm';
import { Instance } from './entities/instance';
import { Emoji } from './entities/emoji';
import { Poll } from './entities/poll';
import { PollVote } from './entities/poll-vote';
import { Meta } from './entities/meta';
import { SwSubscription } from './entities/sw-subscription';
import { NoteWatching } from './entities/note-watching';
import { UserListJoining } from './entities/user-list-joining';
import { Hashtag } from './entities/hashtag';
import { NoteUnread } from './entities/note-unread';
import { RegistrationTicket } from './entities/registration-tickets';
import { UserRepository } from './repositories/user';
import { NoteRepository } from './repositories/note';
import { DriveFileRepository } from './repositories/drive-file';
import { DriveFolderRepository } from './repositories/drive-folder';
import { Log } from './entities/log';
import { AccessToken } from './entities/access-token';
import { UserNotePining } from './entities/user-note-pinings';
import { SigninRepository } from './repositories/signin';
import { MessagingMessageRepository } from './repositories/messaging-message';
import { ReversiGameRepository } from './repositories/games/reversi/game';
import { UserListRepository } from './repositories/user-list';
import { FollowRequestRepository } from './repositories/follow-request';
import { MutingRepository } from './repositories/muting';
import { BlockingRepository } from './repositories/blocking';
import { NoteReactionRepository } from './repositories/note-reaction';
import { UserServiceLinking } from './entities/user-service-linking';
import { NotificationRepository } from './repositories/notification';
import { NoteFavoriteRepository } from './repositories/note-favorite';
import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
import { UserPublickey } from './entities/user-publickey';
import { UserKeypair } from './entities/user-keypair';
import { AppRepository } from './repositories/app';
import { FollowingRepository } from './repositories/following';
import { AbuseUserReportRepository } from './repositories/abuse-user-report';
import { AuthSessionRepository } from './repositories/auth-session';
export const Apps = getCustomRepository(AppRepository);
export const Notes = getCustomRepository(NoteRepository);
export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
export const NoteWatchings = getRepository(NoteWatching);
export const NoteReactions = getCustomRepository(NoteReactionRepository);
export const NoteUnreads = getRepository(NoteUnread);
export const Polls = getRepository(Poll);
export const PollVotes = getRepository(PollVote);
export const Users = getCustomRepository(UserRepository);
export const UserKeypairs = getRepository(UserKeypair);
export const UserPublickeys = getRepository(UserPublickey);
export const UserLists = getCustomRepository(UserListRepository);
export const UserListJoinings = getRepository(UserListJoining);
export const UserNotePinings = getRepository(UserNotePining);
export const UserServiceLinkings = getRepository(UserServiceLinking);
export const Followings = getCustomRepository(FollowingRepository);
export const FollowRequests = getCustomRepository(FollowRequestRepository);
export const Instances = getRepository(Instance);
export const Emojis = getRepository(Emoji);
export const DriveFiles = getCustomRepository(DriveFileRepository);
export const DriveFolders = getCustomRepository(DriveFolderRepository);
export const Notifications = getCustomRepository(NotificationRepository);
export const Metas = getRepository(Meta);
export const Mutings = getCustomRepository(MutingRepository);
export const Blockings = getCustomRepository(BlockingRepository);
export const SwSubscriptions = getRepository(SwSubscription);
export const Hashtags = getRepository(Hashtag);
export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository);
export const RegistrationTickets = getRepository(RegistrationTicket);
export const AuthSessions = getCustomRepository(AuthSessionRepository);
export const AccessTokens = getRepository(AccessToken);
export const Signins = getCustomRepository(SigninRepository);
export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
export const ReversiGames = getCustomRepository(ReversiGameRepository);
export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
export const Logs = getRepository(Log);

View file

@ -1,90 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const Instance = db.get<IInstance>('instances');
Instance.createIndex('host', { unique: true });
export default Instance;
export interface IInstance {
_id: mongo.ObjectID;
/**
*
*/
host: string;
/**
*
*/
caughtAt: Date;
/**
* (MastodonとかMisskeyとかPleromaとか)
*/
system: string;
/**
*
*/
usersCount: number;
/**
* 稿
*/
notesCount: number;
/**
*
*/
followingCount: number;
/**
*
*/
followersCount: number;
/**
* 使
*/
driveUsage: number;
/**
*
*/
driveFiles: number;
/**
*
*/
latestRequestSentAt?: Date;
/**
* HTTPステータスコード
*/
latestStatus?: number;
/**
*
*/
latestRequestReceivedAt?: Date;
/**
*
*/
isNotResponding: boolean;
/**
*
*/
lastCommunicatedAt: Date;
/**
*
*/
isBlocked: boolean;
/**
*
*/
isMarkedAsClosed: boolean;
}

View file

@ -1,19 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const Log = db.get<ILog>('logs');
Log.createIndex('createdAt', { expireAfterSeconds: 3600 * 24 * 3 });
Log.createIndex('level');
Log.createIndex('domain');
export default Log;
export interface ILog {
_id: mongo.ObjectID;
createdAt: Date;
machine: string;
worker: string;
domain: string[];
level: string;
message: string;
data: any;
}

View file

@ -1,75 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import { pack as packUser } from './user';
import { pack as packFile } from './drive-file';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import { length } from 'stringz';
const MessagingMessage = db.get<IMessagingMessage>('messagingMessages');
MessagingMessage.createIndex('userId');
MessagingMessage.createIndex('recipientId');
export default MessagingMessage;
export interface IMessagingMessage {
_id: mongo.ObjectID;
createdAt: Date;
text: string;
userId: mongo.ObjectID;
recipientId: mongo.ObjectID;
isRead: boolean;
fileId: mongo.ObjectID;
}
export function isValidText(text: string): boolean {
return length(text.trim()) <= 1000 && text.trim() != '';
}
/**
* Pack a messaging message for API response
*/
export const pack = (
message: any,
me?: any,
options?: {
populateRecipient: boolean
}
) => new Promise<any>(async (resolve, reject) => {
const opts = options || {
populateRecipient: true
};
let _message: any;
// Populate the message if 'message' is ID
if (isObjectId(message)) {
_message = await MessagingMessage.findOne({
_id: message
});
} else if (typeof message === 'string') {
_message = await MessagingMessage.findOne({
_id: new mongo.ObjectID(message)
});
} else {
_message = deepcopy(message);
}
// Rename _id to id
_message.id = _message._id;
delete _message._id;
// Populate user
_message.user = await packUser(_message.userId, me);
if (_message.fileId) {
// Populate file
_message.file = await packFile(_message.fileId);
}
if (opts.populateRecipient) {
// Populate recipient
_message.recipient = await packUser(_message.recipientId, me);
}
resolve(_message);
});

View file

@ -1,257 +0,0 @@
import db from '../db/mongodb';
import config from '../config';
import User from './user';
import { transform } from '../misc/cafy-id';
const Meta = db.get<IMeta>('meta');
export default Meta;
// 後方互換性のため。
// 過去のMisskeyではインスタンス名や紹介を設定ファイルに記述していたのでそれを移行
if ((config as any).name) {
Meta.findOne({}).then(m => {
if (m != null && m.name == null) {
Meta.update({}, {
$set: {
name: (config as any).name
}
});
}
});
}
if ((config as any).description) {
Meta.findOne({}).then(m => {
if (m != null && m.description == null) {
Meta.update({}, {
$set: {
description: (config as any).description
}
});
}
});
}
if ((config as any).localDriveCapacityMb) {
Meta.findOne({}).then(m => {
if (m != null && m.localDriveCapacityMb == null) {
Meta.update({}, {
$set: {
localDriveCapacityMb: (config as any).localDriveCapacityMb
}
});
}
});
}
if ((config as any).remoteDriveCapacityMb) {
Meta.findOne({}).then(m => {
if (m != null && m.remoteDriveCapacityMb == null) {
Meta.update({}, {
$set: {
remoteDriveCapacityMb: (config as any).remoteDriveCapacityMb
}
});
}
});
}
if ((config as any).preventCacheRemoteFiles) {
Meta.findOne({}).then(m => {
if (m != null && m.cacheRemoteFiles == null) {
Meta.update({}, {
$set: {
cacheRemoteFiles: !(config as any).preventCacheRemoteFiles
}
});
}
});
}
if ((config as any).recaptcha) {
Meta.findOne({}).then(m => {
if (m != null && m.enableRecaptcha == null) {
Meta.update({}, {
$set: {
enableRecaptcha: (config as any).recaptcha != null,
recaptchaSiteKey: (config as any).recaptcha.site_key,
recaptchaSecretKey: (config as any).recaptcha.secret_key,
}
});
}
});
}
if ((config as any).ghost) {
Meta.findOne({}).then(async m => {
if (m != null && m.proxyAccount == null) {
const account = await User.findOne({ _id: transform((config as any).ghost) });
Meta.update({}, {
$set: {
proxyAccount: account.username
}
});
}
});
}
if ((config as any).maintainer) {
Meta.findOne({}).then(m => {
if (m != null && m.maintainer == null) {
Meta.update({}, {
$set: {
maintainer: (config as any).maintainer
}
});
}
});
}
if ((config as any).twitter) {
Meta.findOne({}).then(m => {
if (m != null && m.enableTwitterIntegration == null) {
Meta.update({}, {
$set: {
enableTwitterIntegration: true,
twitterConsumerKey: (config as any).twitter.consumer_key,
twitterConsumerSecret: (config as any).twitter.consumer_secret
}
});
}
});
}
if ((config as any).github) {
Meta.findOne({}).then(m => {
if (m != null && m.enableGithubIntegration == null) {
Meta.update({}, {
$set: {
enableGithubIntegration: true,
githubClientId: (config as any).github.client_id,
githubClientSecret: (config as any).github.client_secret
}
});
}
});
}
if ((config as any).user_recommendation) {
Meta.findOne({}).then(m => {
if (m != null && m.enableExternalUserRecommendation == null) {
Meta.update({}, {
$set: {
enableExternalUserRecommendation: true,
externalUserRecommendationEngine: (config as any).user_recommendation.engine,
externalUserRecommendationTimeout: (config as any).user_recommendation.timeout
}
});
}
});
}
if ((config as any).sw) {
Meta.findOne({}).then(m => {
if (m != null && m.enableServiceWorker == null) {
Meta.update({}, {
$set: {
enableServiceWorker: true,
swPublicKey: (config as any).sw.public_key,
swPrivateKey: (config as any).sw.private_key
}
});
}
});
}
Meta.findOne({}).then(m => {
if (m != null && (m as any).broadcasts != null) {
Meta.update({}, {
$rename: {
broadcasts: 'announcements'
}
});
}
});
export type IMeta = {
name?: string;
description?: string;
/**
*
*/
maintainer: {
/**
*
*/
name: string;
/**
*
*/
email?: string;
};
langs?: string[];
announcements?: any[];
stats?: {
notesCount: number;
originalNotesCount: number;
usersCount: number;
originalUsersCount: number;
};
disableRegistration?: boolean;
disableLocalTimeline?: boolean;
disableGlobalTimeline?: boolean;
enableEmojiReaction?: boolean;
useStarForReactionFallback?: boolean;
hidedTags?: string[];
mascotImageUrl?: string;
bannerUrl?: string;
errorImageUrl?: string;
iconUrl?: string;
cacheRemoteFiles?: boolean;
proxyAccount?: string;
enableRecaptcha?: boolean;
recaptchaSiteKey?: string;
recaptchaSecretKey?: string;
/**
* Drive capacity of a local user (MB)
*/
localDriveCapacityMb?: number;
/**
* Drive capacity of a remote user (MB)
*/
remoteDriveCapacityMb?: number;
/**
* Max allowed note text length in characters
*/
maxNoteTextLength?: number;
summalyProxy?: string;
enableTwitterIntegration?: boolean;
twitterConsumerKey?: string;
twitterConsumerSecret?: string;
enableGithubIntegration?: boolean;
githubClientId?: string;
githubClientSecret?: string;
enableDiscordIntegration?: boolean;
discordClientId?: string;
discordClientSecret?: string;
enableExternalUserRecommendation?: boolean;
externalUserRecommendationEngine?: string;
externalUserRecommendationTimeout?: number;
enableEmail?: boolean;
email?: string;
smtpSecure?: boolean;
smtpHost?: string;
smtpPort?: number;
smtpUser?: string;
smtpPass?: string;
enableServiceWorker?: boolean;
swPublicKey?: string;
swPrivateKey?: string;
};

View file

@ -1,56 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import * as deepcopy from 'deepcopy';
import { pack as packUser, IUser } from './user';
const Mute = db.get<IMute>('mute');
Mute.createIndex('muterId');
Mute.createIndex('muteeId');
Mute.createIndex(['muterId', 'muteeId'], { unique: true });
export default Mute;
export interface IMute {
_id: mongo.ObjectID;
createdAt: Date;
muterId: mongo.ObjectID;
muteeId: mongo.ObjectID;
}
export const packMany = (
mutes: (string | mongo.ObjectID | IMute)[],
me?: string | mongo.ObjectID | IUser
) => {
return Promise.all(mutes.map(x => pack(x, me)));
};
export const pack = (
mute: any,
me?: any
) => new Promise<any>(async (resolve, reject) => {
let _mute: any;
// Populate the mute if 'mute' is ID
if (isObjectId(mute)) {
_mute = await Mute.findOne({
_id: mute
});
} else if (typeof mute === 'string') {
_mute = await Mute.findOne({
_id: new mongo.ObjectID(mute)
});
} else {
_mute = deepcopy(mute);
}
// Rename _id to id
_mute.id = _mute._id;
delete _mute._id;
// Populate mutee
_mute.mutee = await packUser(_mute.muteeId, me, {
detail: true
});
resolve(_mute);
});

View file

@ -1,51 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import { pack as packUser } from './user';
const NoteReaction = db.get<INoteReaction>('noteReactions');
NoteReaction.createIndex('noteId');
NoteReaction.createIndex('userId');
NoteReaction.createIndex(['userId', 'noteId'], { unique: true });
export default NoteReaction;
export interface INoteReaction {
_id: mongo.ObjectID;
createdAt: Date;
noteId: mongo.ObjectID;
userId: mongo.ObjectID;
reaction: string;
}
/**
* Pack a reaction for API response
*/
export const pack = (
reaction: any,
me?: any
) => new Promise<any>(async (resolve, reject) => {
let _reaction: any;
// Populate the reaction if 'reaction' is ID
if (isObjectId(reaction)) {
_reaction = await NoteReaction.findOne({
_id: reaction
});
} else if (typeof reaction === 'string') {
_reaction = await NoteReaction.findOne({
_id: new mongo.ObjectID(reaction)
});
} else {
_reaction = deepcopy(reaction);
}
// Rename _id to id
_reaction.id = _reaction._id;
delete _reaction._id;
// Populate user
_reaction.user = await packUser(_reaction.userId, me);
resolve(_reaction);
});

View file

@ -1,19 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const NoteUnread = db.get<INoteUnread>('noteUnreads');
NoteUnread.createIndex('userId');
NoteUnread.createIndex('noteId');
NoteUnread.createIndex(['userId', 'noteId'], { unique: true });
export default NoteUnread;
export interface INoteUnread {
_id: mongo.ObjectID;
noteId: mongo.ObjectID;
userId: mongo.ObjectID;
isSpecified: boolean;
_note: {
userId: mongo.ObjectID;
};
}

View file

@ -1,15 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const NoteWatching = db.get<INoteWatching>('noteWatching');
NoteWatching.createIndex('userId');
NoteWatching.createIndex('noteId');
NoteWatching.createIndex(['userId', 'noteId'], { unique: true });
export default NoteWatching;
export interface INoteWatching {
_id: mongo.ObjectID;
createdAt: Date;
userId: mongo.ObjectID;
noteId: mongo.ObjectID;
}

View file

@ -1,418 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import rap from '@prezzemolo/rap';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import { length } from 'stringz';
import { IUser, pack as packUser } from './user';
import { pack as packApp } from './app';
import PollVote from './poll-vote';
import NoteReaction from './note-reaction';
import { packMany as packFileMany, IDriveFile } from './drive-file';
import Following from './following';
import Emoji from './emoji';
import { dbLogger } from '../db/logger';
import { unique, concat } from '../prelude/array';
const Note = db.get<INote>('notes');
Note.createIndex('uri', { sparse: true, unique: true });
Note.createIndex('userId');
Note.createIndex('mentions');
Note.createIndex('visibleUserIds');
Note.createIndex('replyId');
Note.createIndex('renoteId');
Note.createIndex('tagsLower');
Note.createIndex('_user.host');
Note.createIndex('_files._id');
Note.createIndex('_files.contentType');
Note.createIndex({ createdAt: -1 });
Note.createIndex({ score: -1 }, { sparse: true });
export default Note;
export function isValidCw(text: string): boolean {
return length(text.trim()) <= 100;
}
export type INote = {
_id: mongo.ObjectID;
createdAt: Date;
deletedAt: Date;
updatedAt?: Date;
fileIds: mongo.ObjectID[];
replyId: mongo.ObjectID;
renoteId: mongo.ObjectID;
poll: IPoll;
name?: string;
text: string;
tags: string[];
tagsLower: string[];
emojis: string[];
cw: string;
userId: mongo.ObjectID;
appId: mongo.ObjectID;
viaMobile: boolean;
localOnly: boolean;
renoteCount: number;
repliesCount: number;
reactionCounts: Record<string, number>;
mentions: mongo.ObjectID[];
mentionedRemoteUsers: {
uri: string;
username: string;
host: string;
}[];
/**
* public ...
* home ... ()
* followers ...
* specified ... visibleUserIds
*/
visibility: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds: mongo.ObjectID[];
geo: {
coordinates: number[];
altitude: number;
accuracy: number;
altitudeAccuracy: number;
heading: number;
speed: number;
};
uri: string;
/**
* 稿
*/
score: number;
// 非正規化
_reply?: {
userId: mongo.ObjectID;
};
_renote?: {
userId: mongo.ObjectID;
};
_user: {
host: string;
inbox?: string;
};
_files?: IDriveFile[];
};
export type IPoll = {
choices: IChoice[];
multiple?: boolean;
expiresAt?: Date;
};
export type IChoice = {
id: number;
text: string;
votes: number;
};
export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
let hide = false;
// visibility が private かつ投稿者のIDが自分のIDではなかったら非表示(後方互換性のため)
if (packedNote.visibility == 'private' && (meId == null || !meId.equals(packedNote.userId))) {
hide = true;
}
// visibility が specified かつ自分が指定されていなかったら非表示
if (packedNote.visibility == 'specified') {
if (meId == null) {
hide = true;
} else if (meId.equals(packedNote.userId)) {
hide = false;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id));
if (specified) {
hide = false;
} else {
hide = true;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (packedNote.visibility == 'followers') {
if (meId == null) {
hide = true;
} else if (meId.equals(packedNote.userId)) {
hide = false;
} else if (packedNote.reply && meId.equals(packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
hide = false;
} else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId.equals(id))) {
// 自分へのメンション
hide = false;
} else {
// フォロワーかどうか
const following = await Following.findOne({
followeeId: packedNote.userId,
followerId: meId
});
if (following == null) {
hide = true;
} else {
hide = false;
}
}
}
if (hide) {
packedNote.fileIds = [];
packedNote.files = [];
packedNote.text = null;
packedNote.poll = null;
packedNote.cw = null;
packedNote.tags = [];
packedNote.geo = null;
packedNote.isHidden = true;
}
};
export const packMany = (
notes: (string | mongo.ObjectID | INote)[],
me?: string | mongo.ObjectID | IUser,
options?: {
detail?: boolean;
skipHide?: boolean;
}
) => {
return Promise.all(notes.map(n => pack(n, me, options)));
};
/**
* Pack a note for API response
*
* @param note target
* @param me? serializee
* @param options? serialize options
* @return response
*/
export const pack = async (
note: string | mongo.ObjectID | INote,
me?: string | mongo.ObjectID | IUser,
options?: {
detail?: boolean;
skipHide?: boolean;
}
) => {
const opts = Object.assign({
detail: true,
skipHide: false
}, options);
// Me
const meId: mongo.ObjectID = me
? isObjectId(me)
? me as mongo.ObjectID
: typeof me === 'string'
? new mongo.ObjectID(me)
: (me as IUser)._id
: null;
let _note: any;
// Populate the note if 'note' is ID
if (isObjectId(note)) {
_note = await Note.findOne({
_id: note
});
} else if (typeof note === 'string') {
_note = await Note.findOne({
_id: new mongo.ObjectID(note)
});
} else {
_note = deepcopy(note);
}
// (データベースの欠損などで)投稿がデータベース上に見つからなかったとき
if (_note == null) {
dbLogger.warn(`[DAMAGED DB] (missing) pkg: note :: ${note}`);
return null;
}
const id = _note._id;
// Some counts
_note.renoteCount = _note.renoteCount || 0;
_note.repliesCount = _note.repliesCount || 0;
_note.reactionCounts = _note.reactionCounts || {};
// _note._userを消す前か、_note.userを解決した後でないとホストがわからない
if (_note._user) {
const host = _note._user.host;
// 互換性のため。(古いMisskeyではNoteにemojisが無い)
if (_note.emojis == null) {
_note.emojis = Emoji.find({
host: host
}, {
fields: { _id: false }
});
} else {
_note.emojis = unique(concat([_note.emojis, Object.keys(_note.reactionCounts).map(x => x.replace(/:/g, ''))]));
_note.emojis = Emoji.find({
name: { $in: _note.emojis },
host: host
}, {
fields: { _id: false }
});
}
}
// Rename _id to id
_note.id = _note._id;
delete _note._id;
delete _note.prev;
delete _note.next;
delete _note.tagsLower;
delete _note.score;
delete _note._user;
delete _note._reply;
delete _note._renote;
delete _note._files;
delete _note._replyIds;
delete _note.mentionedRemoteUsers;
if (_note.geo) delete _note.geo.type;
// Populate user
_note.user = packUser(_note.userId, meId);
// Populate app
if (_note.appId) {
_note.app = packApp(_note.appId);
}
// Populate files
_note.files = packFileMany(_note.fileIds || []);
// 後方互換性のため
_note.mediaIds = _note.fileIds;
_note.media = _note.files;
// When requested a detailed note data
if (opts.detail) {
if (_note.replyId) {
// Populate reply to note
_note.reply = pack(_note.replyId, meId, {
detail: false
});
}
if (_note.renoteId) {
// Populate renote
_note.renote = pack(_note.renoteId, meId, {
detail: _note.text == null
});
}
// Poll
if (meId && _note.poll) {
_note.poll = (async poll => {
if (poll.multiple) {
const votes = await PollVote.find({
userId: meId,
noteId: id
});
const myChoices = (poll.choices as IChoice[]).filter(x => votes.some(y => x.id == y.choice));
for (const myChoice of myChoices) {
(myChoice as any).isVoted = true;
}
return poll;
} else {
poll.multiple = false;
}
const vote = await PollVote
.findOne({
userId: meId,
noteId: id
});
if (vote) {
const myChoice = (poll.choices as IChoice[])
.filter(x => x.id == vote.choice)[0] as any;
myChoice.isVoted = true;
}
return poll;
})(_note.poll);
}
if (meId) {
// Fetch my reaction
_note.myReaction = (async () => {
const reaction = await NoteReaction
.findOne({
userId: meId,
noteId: id,
deletedAt: { $exists: false }
});
if (reaction) {
return reaction.reaction;
}
return null;
})();
}
}
// resolve promises in _note object
_note = await rap(_note);
//#region (データベースの欠損などで)参照しているデータがデータベース上に見つからなかったとき
if (_note.user == null) {
dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> user :: ${_note.id} (user ${_note.userId})`);
return null;
}
if (opts.detail) {
if (_note.replyId != null && _note.reply == null) {
dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> reply :: ${_note.id} (reply ${_note.replyId})`);
return null;
}
if (_note.renoteId != null && _note.renote == null) {
dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> renote :: ${_note.id} (renote ${_note.renoteId})`);
return null;
}
}
//#endregion
if (_note.name) {
_note.text = `${_note.name}\n${_note.text}`;
}
if (_note.user.isCat && _note.text) {
_note.text = (_note.text
// ja-JP
.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
// ko-KR
.replace(/[나-낳]/g, (match: string) => String.fromCharCode(
match.codePointAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
))
);
}
if (!opts.skipHide) {
await hideNote(_note, meId);
}
return _note;
};

View file

@ -1,120 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import { IUser, pack as packUser } from './user';
import { pack as packNote } from './note';
import { dbLogger } from '../db/logger';
const Notification = db.get<INotification>('notifications');
Notification.createIndex('notifieeId');
export default Notification;
export interface INotification {
_id: mongo.ObjectID;
createdAt: Date;
/**
*
*/
notifiee?: IUser;
/**
*
*/
notifieeId: mongo.ObjectID;
/**
* (initiator)Origin
*/
notifier?: IUser;
/**
* (initiator)Origin
*/
notifierId: mongo.ObjectID;
/**
*
* follow -
* mention - 稿
* reply - (Watchしている)稿
* renote - (Watchしている)稿Renoteされた
* quote - (Watchしている)稿Renoteされた
* reaction - (Watchしている)稿
* poll_vote - (Watchしている)稿
*/
type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'poll_vote';
/**
*
*/
isRead: boolean;
}
export const packMany = (
notifications: any[]
) => {
return Promise.all(notifications.map(n => pack(n)));
};
/**
* Pack a notification for API response
*/
export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => {
let _notification: any;
// Populate the notification if 'notification' is ID
if (isObjectId(notification)) {
_notification = await Notification.findOne({
_id: notification
});
} else if (typeof notification === 'string') {
_notification = await Notification.findOne({
_id: new mongo.ObjectID(notification)
});
} else {
_notification = deepcopy(notification);
}
// Rename _id to id
_notification.id = _notification._id;
delete _notification._id;
// Rename notifierId to userId
_notification.userId = _notification.notifierId;
delete _notification.notifierId;
const me = _notification.notifieeId;
delete _notification.notifieeId;
// Populate notifier
_notification.user = await packUser(_notification.userId, me);
switch (_notification.type) {
case 'follow':
case 'receiveFollowRequest':
// nope
break;
case 'mention':
case 'reply':
case 'renote':
case 'quote':
case 'reaction':
case 'poll_vote':
// Populate note
_notification.note = await packNote(_notification.noteId, me);
// (データベースの不具合などで)投稿が見つからなかったら
if (_notification.note == null) {
dbLogger.warn(`[DAMAGED DB] (missing) pkg: notification -> note :: ${_notification.id} (note ${_notification.noteId})`);
return resolve(null);
}
break;
default:
dbLogger.error(`Unknown type: ${_notification.type}`);
break;
}
resolve(_notification);
});

View file

@ -1,17 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const PollVote = db.get<IPollVote>('pollVotes');
PollVote.dropIndex(['userId', 'noteId'], { unique: true }).catch(() => {});
PollVote.createIndex('userId');
PollVote.createIndex('noteId');
PollVote.createIndex(['userId', 'noteId', 'choice'], { unique: true });
export default PollVote;
export interface IPollVote {
_id: mongo.ObjectID;
createdAt: Date;
userId: mongo.ObjectID;
noteId: mongo.ObjectID;
choice: number;
}

View file

@ -1,12 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const RegistrationTicket = db.get<IRegistrationTicket>('registrationTickets');
RegistrationTicket.createIndex('code', { unique: true });
export default RegistrationTicket;
export interface IRegistrationTicket {
_id: mongo.ObjectID;
createdAt: Date;
code: string;
}

View file

@ -0,0 +1,32 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '..';
import rap from '@prezzemolo/rap';
import { AbuseUserReport } from '../entities/abuse-user-report';
@EntityRepository(AbuseUserReport)
export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
public packMany(
reports: any[],
) {
return Promise.all(reports.map(x => this.pack(x)));
}
public async pack(
src: AbuseUserReport['id'] | AbuseUserReport,
) {
const report = typeof src === 'object' ? src : await this.findOne(src);
return await rap({
id: report.id,
createdAt: report.createdAt,
reporterId: report.reporterId,
userId: report.userId,
reporter: Users.pack(report.reporter || report.reporterId, null, {
detail: true
}),
user: Users.pack(report.user || report.userId, null, {
detail: true
}),
});
}
}

View file

@ -0,0 +1,36 @@
import { EntityRepository, Repository } from 'typeorm';
import { App } from '../entities/app';
import { AccessTokens } from '..';
@EntityRepository(App)
export class AppRepository extends Repository<App> {
public async pack(
src: App['id'] | App,
me?: any,
options?: {
detail?: boolean,
includeSecret?: boolean,
includeProfileImageIds?: boolean
}
) {
const opts = Object.assign({
detail: false,
includeSecret: false,
includeProfileImageIds: false
}, options);
const app = typeof src === 'object' ? src : await this.findOne(src);
return {
id: app.id,
name: app.name,
...(opts.includeSecret ? { secret: app.secret } : {}),
...(me ? {
isAuthorized: await AccessTokens.count({
appId: app.id,
userId: me,
}).then(count => count > 0)
} : {})
};
}
}

View file

@ -0,0 +1,19 @@
import { EntityRepository, Repository } from 'typeorm';
import { Apps } from '..';
import rap from '@prezzemolo/rap';
import { AuthSession } from '../entities/auth-session';
@EntityRepository(AuthSession)
export class AuthSessionRepository extends Repository<AuthSession> {
public async pack(
src: AuthSession['id'] | AuthSession,
me?: any
) {
const session = typeof src === 'object' ? src : await this.findOne(src);
return await rap({
id: session.id,
app: Apps.pack(session.appId, me)
});
}
}

View file

@ -0,0 +1,28 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '..';
import rap from '@prezzemolo/rap';
import { Blocking } from '../entities/blocking';
@EntityRepository(Blocking)
export class BlockingRepository extends Repository<Blocking> {
public packMany(
blockings: any[],
me: any
) {
return Promise.all(blockings.map(x => this.pack(x, me)));
}
public async pack(
src: Blocking['id'] | Blocking,
me?: any
) {
const blocking = typeof src === 'object' ? src : await this.findOne(src);
return await rap({
id: blocking.id,
blockee: Users.pack(blocking.blockeeId, me, {
detail: true
})
});
}
}

View file

@ -0,0 +1,113 @@
import { EntityRepository, Repository } from 'typeorm';
import { DriveFile } from '../entities/drive-file';
import { Users, DriveFolders } from '..';
import rap from '@prezzemolo/rap';
import { User } from '../entities/user';
@EntityRepository(DriveFile)
export class DriveFileRepository extends Repository<DriveFile> {
public validateFileName(name: string): boolean {
return (
(name.trim().length > 0) &&
(name.length <= 200) &&
(name.indexOf('\\') === -1) &&
(name.indexOf('/') === -1) &&
(name.indexOf('..') === -1)
);
}
public getPublicUrl(file: DriveFile, thumbnail = false): string {
if (thumbnail) {
return file.thumbnailUrl || file.webpublicUrl || file.url;
} else {
return file.webpublicUrl || file.thumbnailUrl || file.url;
}
}
public async clacDriveUsageOf(user: User['id'] | User): Promise<number> {
const id = typeof user === 'object' ? user.id : user;
const { sum } = await this
.createQueryBuilder('file')
.where('file.userId = :id', { id: id })
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) || 0;
}
public async clacDriveUsageOfHost(host: string): Promise<number> {
const { sum } = await this
.createQueryBuilder('file')
.where('file.userHost = :host', { host: host })
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) || 0;
}
public async clacDriveUsageOfLocal(): Promise<number> {
const { sum } = await this
.createQueryBuilder('file')
.where('file.userHost IS NULL')
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) || 0;
}
public async clacDriveUsageOfRemote(): Promise<number> {
const { sum } = await this
.createQueryBuilder('file')
.where('file.userHost IS NOT NULL')
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) || 0;
}
public packMany(
files: any[],
options?: {
detail?: boolean
self?: boolean,
withUser?: boolean,
}
) {
return Promise.all(files.map(f => this.pack(f, options)));
}
public async pack(
src: DriveFile['id'] | DriveFile,
options?: {
detail?: boolean,
self?: boolean,
withUser?: boolean,
}
) {
const opts = Object.assign({
detail: false,
self: false
}, options);
const file = typeof src === 'object' ? src : await this.findOne(src);
return await rap({
id: file.id,
createdAt: file.createdAt,
name: file.name,
type: file.type,
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
properties: file.properties,
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
folderId: file.folderId,
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
detail: true
}) : null,
user: opts.withUser ? Users.pack(file.userId) : null
});
}
}

View file

@ -0,0 +1,49 @@
import { EntityRepository, Repository } from 'typeorm';
import { DriveFolders, DriveFiles } from '..';
import rap from '@prezzemolo/rap';
import { DriveFolder } from '../entities/drive-folder';
@EntityRepository(DriveFolder)
export class DriveFolderRepository extends Repository<DriveFolder> {
public validateFolderName(name: string): boolean {
return (
(name.trim().length > 0) &&
(name.length <= 200)
);
}
public async pack(
src: DriveFolder['id'] | DriveFolder,
options?: {
detail: boolean
}
): Promise<Record<string, any>> {
const opts = Object.assign({
detail: false
}, options);
const folder = typeof src === 'object' ? src : await this.findOne(src);
return await rap({
id: folder.id,
createdAt: folder.createdAt,
name: folder.name,
parentId: folder.parentId,
...(opts.detail ? {
foldersCount: DriveFolders.count({
parentId: folder.id
}),
filesCount: DriveFiles.count({
folderId: folder.id
}),
...(folder.parentId ? {
parent: this.pack(folder.parentId, {
detail: true
})
} : {})
} : {})
});
}
}

View file

@ -0,0 +1,19 @@
import { EntityRepository, Repository } from 'typeorm';
import { FollowRequest } from '../entities/follow-request';
import { Users } from '..';
@EntityRepository(FollowRequest)
export class FollowRequestRepository extends Repository<FollowRequest> {
public async pack(
src: FollowRequest['id'] | FollowRequest,
me?: any
) {
const request = typeof src === 'object' ? src : await this.findOne(src);
return {
id: request.id,
follower: await Users.pack(request.followerId, me),
followee: await Users.pack(request.followeeId, me),
};
}
}

View file

@ -0,0 +1,44 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '..';
import rap from '@prezzemolo/rap';
import { Following } from '../entities/following';
@EntityRepository(Following)
export class FollowingRepository extends Repository<Following> {
public packMany(
followings: any[],
me?: any,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
}
) {
return Promise.all(followings.map(x => this.pack(x, me, opts)));
}
public async pack(
src: Following['id'] | Following,
me?: any,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
}
) {
const following = typeof src === 'object' ? src : await this.findOne(src);
if (opts == null) opts = {};
return await rap({
id: following.id,
createdAt: following.createdAt,
followeeId: following.followeeId,
followerId: following.followerId,
followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, {
detail: true
}) : null,
follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, {
detail: true
}) : null,
});
}
}

View file

@ -0,0 +1,49 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../../..';
import { ReversiGame } from '../../../entities/games/reversi/game';
@EntityRepository(ReversiGame)
export class ReversiGameRepository extends Repository<ReversiGame> {
public async pack(
src: ReversiGame['id'] | ReversiGame,
me?: any,
options?: {
detail?: boolean
}
) {
const opts = Object.assign({
detail: true
}, options);
const game = typeof src === 'object' ? src : await this.findOne(src);
const meId = me ? typeof me === 'string' ? me : me.id : null;
return {
id: game.id,
createdAt: game.createdAt,
startedAt: game.startedAt,
isStarted: game.isStarted,
isEnded: game.isEnded,
form1: game.form1,
form2: game.form2,
user1Accepted: game.user1Accepted,
user2Accepted: game.user2Accepted,
user1Id: game.user1Id,
user2Id: game.user2Id,
user1: await Users.pack(game.user1Id, meId),
user2: await Users.pack(game.user2Id, meId),
winnerId: game.winnerId,
winner: game.winnerId ? await Users.pack(game.winnerId, meId) : null,
surrendered: game.surrendered,
black: game.black,
bw: game.bw,
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
...(opts.detail ? {
logs: game.logs,
map: game.map,
} : {})
};
}
}

View file

@ -0,0 +1,27 @@
import { EntityRepository, Repository } from 'typeorm';
import rap from '@prezzemolo/rap';
import { ReversiMatching } from '../../../entities/games/reversi/matching';
import { Users } from '../../..';
@EntityRepository(ReversiMatching)
export class ReversiMatchingRepository extends Repository<ReversiMatching> {
public async pack(
src: ReversiMatching['id'] | ReversiMatching,
me: any
) {
const matching = typeof src === 'object' ? src : await this.findOne(src);
return await rap({
id: matching.id,
createdAt: matching.createdAt,
parentId: matching.parentId,
parent: Users.pack(matching.parentId, me, {
detail: true
}),
childId: matching.childId,
child: Users.pack(matching.childId, me, {
detail: true
})
});
}
}

View file

@ -0,0 +1,37 @@
import { EntityRepository, Repository } from 'typeorm';
import { MessagingMessage } from '../entities/messaging-message';
import { Users, DriveFiles } from '..';
@EntityRepository(MessagingMessage)
export class MessagingMessageRepository extends Repository<MessagingMessage> {
public isValidText(text: string): boolean {
return text.trim().length <= 1000 && text.trim() != '';
}
public async pack(
src: MessagingMessage['id'] | MessagingMessage,
me?: any,
options?: {
populateRecipient: boolean
}
) {
const opts = options || {
populateRecipient: true
};
const message = typeof src === 'object' ? src : await this.findOne(src);
return {
id: message.id,
createdAt: message.createdAt,
text: message.text,
userId: message.userId,
user: await Users.pack(message.user || message.userId, me),
recipientId: message.recipientId,
recipient: opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : null,
fileId: message.fileId,
file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
isRead: message.isRead
};
}
}

View file

@ -0,0 +1,28 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '..';
import rap from '@prezzemolo/rap';
import { Muting } from '../entities/muting';
@EntityRepository(Muting)
export class MutingRepository extends Repository<Muting> {
public packMany(
mutings: any[],
me: any
) {
return Promise.all(mutings.map(x => this.pack(x, me)));
}
public async pack(
src: Muting['id'] | Muting,
me?: any
) {
const muting = typeof src === 'object' ? src : await this.findOne(src);
return await rap({
id: muting.id,
mutee: Users.pack(muting.muteeId, me, {
detail: true
})
});
}
}

View file

@ -0,0 +1,25 @@
import { EntityRepository, Repository } from 'typeorm';
import { NoteFavorite } from '../entities/note-favorite';
import { Notes } from '..';
@EntityRepository(NoteFavorite)
export class NoteFavoriteRepository extends Repository<NoteFavorite> {
public packMany(
favorites: any[],
me: any
) {
return Promise.all(favorites.map(x => this.pack(x, me)));
}
public async pack(
src: NoteFavorite['id'] | NoteFavorite,
me?: any
) {
const favorite = typeof src === 'object' ? src : await this.findOne(src);
return {
id: favorite.id,
note: await Notes.pack(favorite.note || favorite.noteId, me),
};
}
}

View file

@ -0,0 +1,18 @@
import { EntityRepository, Repository } from 'typeorm';
import { NoteReaction } from '../entities/note-reaction';
import { Users } from '..';
@EntityRepository(NoteReaction)
export class NoteReactionRepository extends Repository<NoteReaction> {
public async pack(
src: NoteReaction['id'] | NoteReaction,
me?: any
) {
const reaction = typeof src === 'object' ? src : await this.findOne(src);
return {
id: reaction.id,
user: await Users.pack(reaction.userId, me),
};
}
}

View file

@ -0,0 +1,210 @@
import { EntityRepository, Repository, In } from 'typeorm';
import { Note } from '../entities/note';
import { User } from '../entities/user';
import { unique, concat } from '../../prelude/array';
import { nyaize } from '../../misc/nyaize';
import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
import rap from '@prezzemolo/rap';
@EntityRepository(Note)
export class NoteRepository extends Repository<Note> {
public validateCw(x: string) {
return x.trim().length <= 100;
}
private async hideNote(packedNote: any, meId: User['id']) {
let hide = false;
// visibility が specified かつ自分が指定されていなかったら非表示
if (packedNote.visibility == 'specified') {
if (meId == null) {
hide = true;
} else if (meId === packedNote.userId) {
hide = false;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds.some((id: any) => meId === id);
if (specified) {
hide = false;
} else {
hide = true;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (packedNote.visibility == 'followers') {
if (meId == null) {
hide = true;
} else if (meId === packedNote.userId) {
hide = false;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
hide = false;
} else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId === id)) {
// 自分へのメンション
hide = false;
} else {
// フォロワーかどうか
const following = await Followings.findOne({
followeeId: packedNote.userId,
followerId: meId
});
if (following == null) {
hide = true;
} else {
hide = false;
}
}
}
if (hide) {
packedNote.visibleUserIds = null;
packedNote.fileIds = [];
packedNote.files = [];
packedNote.text = null;
packedNote.poll = null;
packedNote.cw = null;
packedNote.tags = [];
packedNote.geo = null;
packedNote.isHidden = true;
}
}
public packMany(
notes: (Note['id'] | Note)[],
me?: User['id'] | User,
options?: {
detail?: boolean;
skipHide?: boolean;
}
) {
return Promise.all(notes.map(n => this.pack(n, me, options)));
}
public async pack(
src: Note['id'] | Note,
me?: User['id'] | User,
options?: {
detail?: boolean;
skipHide?: boolean;
}
): Promise<Record<string, any>> {
const opts = Object.assign({
detail: true,
skipHide: false
}, options);
const meId = me ? typeof me === 'string' ? me : me.id : null;
const note = typeof src === 'object' ? src : await this.findOne(src);
const host = note.userHost;
async function populatePoll() {
const poll = await Polls.findOne({ noteId: note.id });
const choices = poll.choices.map(c => ({
text: c,
votes: poll.votes[poll.choices.indexOf(c)],
isVoted: false
}));
if (poll.multiple) {
const votes = await PollVotes.find({
userId: meId,
noteId: note.id
});
const myChoices = votes.map(v => v.choice);
for (const myChoice of myChoices) {
choices[myChoice].isVoted = true;
}
} else {
const vote = await PollVotes.findOne({
userId: meId,
noteId: note.id
});
if (vote) {
choices[vote.choice].isVoted = true;
}
}
return {
multiple: poll.multiple,
expiresAt: poll.expiresAt,
choices
};
}
async function populateMyReaction() {
const reaction = await NoteReactions.findOne({
userId: meId,
noteId: note.id,
});
if (reaction) {
return reaction.reaction;
}
return null;
}
let text = note.text;
if (note.name) {
text = `${note.name}\n${note.text}`;
}
const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)]));
const packed = await rap({
id: note.id,
createdAt: note.createdAt,
app: note.appId ? Apps.pack(note.appId) : null,
userId: note.userId,
user: Users.pack(note.user || note.userId, meId),
text: text,
cw: note.cw,
visibility: note.visibility,
visibleUserIds: note.visibleUserIds,
viaMobile: note.viaMobile,
reactions: note.reactions,
emojis: reactionEmojis.length > 0 ? Emojis.find({
name: In(reactionEmojis),
host: host
}) : [],
tags: note.tags,
fileIds: note.fileIds,
files: DriveFiles.packMany(note.fileIds),
replyId: note.replyId,
renoteId: note.renoteId,
...(opts.detail ? {
reply: note.replyId ? this.pack(note.replyId, meId, {
detail: false
}) : null,
renote: note.renoteId ? this.pack(note.renoteId, meId, {
detail: false
}) : null,
poll: note.hasPoll ? populatePoll() : null,
...(meId ? {
myReaction: populateMyReaction()
} : {})
} : {})
});
if (packed.user.isCat && packed.text) {
packed.text = nyaize(packed.text);
}
if (!opts.skipHide) {
await this.hideNote(packed, meId);
}
return packed;
}
}

View file

@ -0,0 +1,47 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users, Notes } from '..';
import rap from '@prezzemolo/rap';
import { Notification } from '../entities/notification';
@EntityRepository(Notification)
export class NotificationRepository extends Repository<Notification> {
public packMany(
notifications: any[],
) {
return Promise.all(notifications.map(x => this.pack(x)));
}
public async pack(
src: Notification['id'] | Notification,
) {
const notification = typeof src === 'object' ? src : await this.findOne(src);
return await rap({
id: notification.id,
createdAt: notification.createdAt,
type: notification.type,
userId: notification.notifierId,
user: Users.pack(notification.notifier || notification.notifierId),
...(notification.type === 'mention' ? {
note: Notes.pack(notification.note || notification.noteId),
} : {}),
...(notification.type === 'reply' ? {
note: Notes.pack(notification.note || notification.noteId),
} : {}),
...(notification.type === 'renote' ? {
note: Notes.pack(notification.note || notification.noteId),
} : {}),
...(notification.type === 'quote' ? {
note: Notes.pack(notification.note || notification.noteId),
} : {}),
...(notification.type === 'reaction' ? {
note: Notes.pack(notification.note || notification.noteId),
reaction: notification.reaction
} : {}),
...(notification.type === 'pollVote' ? {
note: Notes.pack(notification.note || notification.noteId),
choice: notification.choice
} : {})
});
}
}

View file

@ -0,0 +1,11 @@
import { EntityRepository, Repository } from 'typeorm';
import { Signin } from '../entities/signin';
@EntityRepository(Signin)
export class SigninRepository extends Repository<Signin> {
public async pack(
src: any,
) {
return src;
}
}

View file

@ -0,0 +1,16 @@
import { EntityRepository, Repository } from 'typeorm';
import { UserList } from '../entities/user-list';
@EntityRepository(UserList)
export class UserListRepository extends Repository<UserList> {
public async pack(
src: any,
) {
const userList = typeof src === 'object' ? src : await this.findOne(src);
return {
id: userList.id,
name: userList.name
};
}
}

View file

@ -0,0 +1,198 @@
import { EntityRepository, Repository, In } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '../entities/user';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings } from '..';
import rap from '@prezzemolo/rap';
@EntityRepository(User)
export class UserRepository extends Repository<User> {
public async getRelation(me: User['id'], target: User['id']) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
Followings.findOne({
followerId: me,
followeeId: target
}),
Followings.findOne({
followerId: target,
followeeId: me
}),
FollowRequests.findOne({
followerId: me,
followeeId: target
}),
FollowRequests.findOne({
followerId: target,
followeeId: me
}),
Blockings.findOne({
blockerId: me,
blockeeId: target
}),
Blockings.findOne({
blockerId: target,
blockeeId: me
}),
Mutings.findOne({
muterId: me,
muteeId: target
})
]);
return {
id: target,
isFollowing: following1 != null,
hasPendingFollowRequestFromYou: followReq1 != null,
hasPendingFollowRequestToYou: followReq2 != null,
isFollowed: following2 != null,
isBlocking: toBlocking != null,
isBlocked: fromBlocked != null,
isMuted: mute != null
};
}
public packMany(
users: (User['id'] | User)[],
me?: User['id'] | User,
options?: {
detail?: boolean,
includeSecrets?: boolean,
includeHasUnreadNotes?: boolean
}
) {
return Promise.all(users.map(u => this.pack(u, me, options)));
}
public async pack(
src: User['id'] | User,
me?: User['id'] | User,
options?: {
detail?: boolean,
includeSecrets?: boolean,
includeHasUnreadNotes?: boolean
}
): Promise<Record<string, any>> {
const opts = Object.assign({
detail: false,
includeSecrets: false
}, options);
const user = typeof src === 'object' ? src : await this.findOne(src);
const meId = me ? typeof me === 'string' ? me : me.id : null;
const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
const pins = opts.detail ? await UserNotePinings.find({ userId: user.id }) : [];
return await rap({
id: user.id,
name: user.name,
username: user.username,
host: user.host,
avatarUrl: user.avatarUrl,
bannerUrl: user.bannerUrl,
avatarColor: user.avatarColor,
bannerColor: user.bannerColor,
isAdmin: user.isAdmin,
// カスタム絵文字添付
emojis: user.emojis.length > 0 ? Emojis.find({
where: {
name: In(user.emojis),
host: user.host
},
select: ['name', 'host', 'url', 'aliases']
}) : [],
...(opts.includeHasUnreadNotes ? {
hasUnreadSpecifiedNotes: NoteUnreads.count({
where: { userId: user.id, isSpecified: true },
take: 1
}).then(count => count > 0),
hasUnreadMentions: NoteUnreads.count({
where: { userId: user.id },
take: 1
}).then(count => count > 0),
} : {}),
...(opts.detail ? {
description: user.description,
location: user.location,
birthday: user.birthday,
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
pinnedNoteIds: pins.map(pin => pin.noteId),
pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, {
detail: true
}),
} : {}),
...(opts.detail && meId === user.id ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
autoWatch: user.autoWatch,
alwaysMarkNsfw: user.alwaysMarkNsfw,
carefulBot: user.carefulBot,
hasUnreadMessagingMessage: MessagingMessages.count({
where: {
recipientId: user.id,
isRead: false
},
take: 1
}).then(count => count > 0),
hasUnreadNotification: Notifications.count({
where: {
userId: user.id,
isRead: false
},
take: 1
}).then(count => count > 0),
pendingReceivedFollowRequestsCount: FollowRequests.count({
followeeId: user.id
}),
} : {}),
...(relation ? {
isFollowing: relation.isFollowing,
isFollowed: relation.isFollowed,
hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
isBlocking: relation.isBlocking,
isBlocked: relation.isBlocked,
isMuted: relation.isMuted,
} : {})
});
}
public isLocalUser(user: User): user is ILocalUser {
return user.host === null;
}
public isRemoteUser(user: User): user is IRemoteUser {
return !this.isLocalUser(user);
}
//#region Validators
public validateUsername(username: string, remote = false): boolean {
return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
}
public validatePassword(password: string): boolean {
return typeof password == 'string' && password != '';
}
public isValidName(name?: string): boolean {
return name === null || (typeof name == 'string' && name.length < 50 && name.trim() != '');
}
public isValidDescription(description: string): boolean {
return typeof description == 'string' && description.length < 500 && description.trim() != '';
}
public isValidLocation(location: string): boolean {
return typeof location == 'string' && location.length < 50 && location.trim() != '';
}
public isValidBirthday(birthday: string): boolean {
return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
}
//#endregion
}

View file

@ -1,34 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../db/mongodb';
const Signin = db.get<ISignin>('signin');
export default Signin;
export interface ISignin {
_id: mongo.ObjectID;
createdAt: Date;
userId: mongo.ObjectID;
ip: string;
headers: any;
success: boolean;
}
/**
* Pack a signin record for API response
*
* @param {any} record
* @return {Promise<any>}
*/
export const pack = (
record: any
) => new Promise<any>(async (resolve, reject) => {
const _record = deepcopy(record);
// Rename _id to id
_record.id = _record._id;
delete _record._id;
resolve(_record);
});

View file

@ -1,13 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const SwSubscription = db.get<ISwSubscription>('swSubscriptions');
export default SwSubscription;
export interface ISwSubscription {
_id: mongo.ObjectID;
userId: mongo.ObjectID;
endpoint: string;
auth: string;
publickey: string;
}

View file

@ -1,41 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
const UserList = db.get<IUserList>('userList');
export default UserList;
export interface IUserList {
_id: mongo.ObjectID;
createdAt: Date;
title: string;
userId: mongo.ObjectID;
userIds: mongo.ObjectID[];
}
export const pack = (
userList: string | mongo.ObjectID | IUserList
) => new Promise<any>(async (resolve, reject) => {
let _userList: any;
if (isObjectId(userList)) {
_userList = await UserList.findOne({
_id: userList
});
} else if (typeof userList === 'string') {
_userList = await UserList.findOne({
_id: new mongo.ObjectID(userList)
});
} else {
_userList = deepcopy(userList);
}
if (!_userList) throw `invalid userList arg ${userList}`;
// Rename _id to id
_userList.id = _userList._id;
delete _userList._id;
resolve(_userList);
});

View file

@ -1,438 +0,0 @@
import * as mongo from 'mongodb';
import * as deepcopy from 'deepcopy';
import rap from '@prezzemolo/rap';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import { packMany as packNoteMany } from './note';
import Following from './following';
import Blocking from './blocking';
import Mute from './mute';
import { getFriendIds } from '../server/api/common/get-friends';
import config from '../config';
import FollowRequest from './follow-request';
import fetchMeta from '../misc/fetch-meta';
import Emoji from './emoji';
import { dbLogger } from '../db/logger';
const User = db.get<IUser>('users');
User.createIndex('createdAt');
User.createIndex('updatedAt');
User.createIndex('followersCount');
User.createIndex('tags');
User.createIndex('isSuspended');
User.createIndex('username');
User.createIndex('usernameLower');
User.createIndex('host');
User.createIndex(['username', 'host'], { unique: true });
User.createIndex(['usernameLower', 'host'], { unique: true });
User.createIndex('token', { sparse: true, unique: true });
User.createIndex('uri', { sparse: true, unique: true });
export default User;
type IUserBase = {
_id: mongo.ObjectID;
createdAt: Date;
updatedAt?: Date;
deletedAt?: Date;
followersCount: number;
followingCount: number;
name?: string;
notesCount: number;
username: string;
usernameLower: string;
avatarId: mongo.ObjectID;
bannerId: mongo.ObjectID;
avatarUrl?: string;
bannerUrl?: string;
avatarColor?: any;
bannerColor?: any;
wallpaperId: mongo.ObjectID;
wallpaperUrl?: string;
data: any;
description: string;
lang?: string;
pinnedNoteIds: mongo.ObjectID[];
emojis?: string[];
tags?: string[];
isDeleted: boolean;
/**
*
*/
isSuspended: boolean;
/**
*
*/
isSilenced: boolean;
/**
*
*/
isLocked: boolean;
/**
* Botか否か
*/
isBot: boolean;
/**
* Botからのフォローを承認制にするか
*/
carefulBot: boolean;
/**
*
*/
autoAcceptFollowed: boolean;
/**
*
*/
pendingReceivedFollowRequestsCount: number;
host: string;
};
export interface ILocalUser extends IUserBase {
host: null;
keypair: string;
email: string;
emailVerified?: boolean;
emailVerifyCode?: string;
password: string;
token: string;
twitter: {
accessToken: string;
accessTokenSecret: string;
userId: string;
screenName: string;
};
github: {
accessToken: string;
id: string;
login: string;
};
discord: {
accessToken: string;
refreshToken: string;
expiresDate: number;
id: string;
username: string;
discriminator: string;
};
profile: {
location: string;
birthday: string; // 'YYYY-MM-DD'
tags: string[];
};
fields?: {
name: string;
value: string;
}[];
isCat: boolean;
isAdmin?: boolean;
isModerator?: boolean;
isVerified?: boolean;
twoFactorSecret: string;
twoFactorEnabled: boolean;
twoFactorTempSecret?: string;
clientSettings: any;
settings: {
autoWatch: boolean;
alwaysMarkNsfw?: boolean;
};
hasUnreadNotification: boolean;
hasUnreadMessagingMessage: boolean;
}
export interface IRemoteUser extends IUserBase {
inbox: string;
sharedInbox?: string;
featured?: string;
endpoints: string[];
uri: string;
url?: string;
publicKey: {
id: string;
publicKeyPem: string;
};
lastFetchedAt: Date;
isAdmin: false;
isModerator: false;
}
export type IUser = ILocalUser | IRemoteUser;
export const isLocalUser = (user: any): user is ILocalUser =>
user.host === null;
export const isRemoteUser = (user: any): user is IRemoteUser =>
!isLocalUser(user);
//#region Validators
export function validateUsername(username: string, remote = false): boolean {
return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
}
export function validatePassword(password: string): boolean {
return typeof password == 'string' && password != '';
}
export function isValidName(name?: string): boolean {
return name === null || (typeof name == 'string' && name.length < 50 && name.trim() != '');
}
export function isValidDescription(description: string): boolean {
return typeof description == 'string' && description.length < 500 && description.trim() != '';
}
export function isValidLocation(location: string): boolean {
return typeof location == 'string' && location.length < 50 && location.trim() != '';
}
export function isValidBirthday(birthday: string): boolean {
return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
}
//#endregion
export async function getRelation(me: mongo.ObjectId, target: mongo.ObjectId) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
Following.findOne({
followerId: me,
followeeId: target
}),
Following.findOne({
followerId: target,
followeeId: me
}),
FollowRequest.findOne({
followerId: me,
followeeId: target
}),
FollowRequest.findOne({
followerId: target,
followeeId: me
}),
Blocking.findOne({
blockerId: me,
blockeeId: target
}),
Blocking.findOne({
blockerId: target,
blockeeId: me
}),
Mute.findOne({
muterId: me,
muteeId: target
})
]);
return {
id: target,
isFollowing: following1 !== null,
hasPendingFollowRequestFromYou: followReq1 !== null,
hasPendingFollowRequestToYou: followReq2 !== null,
isFollowed: following2 !== null,
isBlocking: toBlocking !== null,
isBlocked: fromBlocked !== null,
isMuted: mute !== null
};
}
/**
* Pack a user for API response
*
* @param user target
* @param me? serializee
* @param options? serialize options
* @return Packed user
*/
export const pack = (
user: string | mongo.ObjectID | IUser,
me?: string | mongo.ObjectID | IUser,
options?: {
detail?: boolean,
includeSecrets?: boolean,
includeHasUnreadNotes?: boolean
}
) => new Promise<any>(async (resolve, reject) => {
const opts = Object.assign({
detail: false,
includeSecrets: false
}, options);
let _user: any;
const fields = opts.detail ? {} : {
name: true,
username: true,
host: true,
avatarColor: true,
avatarUrl: true,
emojis: true,
isCat: true,
isBot: true,
isAdmin: true,
isVerified: true
};
// Populate the user if 'user' is ID
if (isObjectId(user)) {
_user = await User.findOne({
_id: user
}, { fields });
} else if (typeof user === 'string') {
_user = await User.findOne({
_id: new mongo.ObjectID(user)
}, { fields });
} else {
_user = deepcopy(user);
}
// (データベースの欠損などで)ユーザーがデータベース上に見つからなかったとき
if (_user == null) {
dbLogger.warn(`user not found on database: ${user}`);
return resolve(null);
}
// Me
const meId: mongo.ObjectID = me
? isObjectId(me)
? me as mongo.ObjectID
: typeof me === 'string'
? new mongo.ObjectID(me)
: (me as IUser)._id
: null;
// Rename _id to id
_user.id = _user._id;
delete _user._id;
delete _user.usernameLower;
delete _user.emailVerifyCode;
if (_user.host == null) {
// Remove private properties
delete _user.keypair;
delete _user.password;
delete _user.token;
delete _user.twoFactorTempSecret;
delete _user.two_factor_temp_secret; // 後方互換性のため
delete _user.twoFactorSecret;
if (_user.twitter) {
delete _user.twitter.accessToken;
delete _user.twitter.accessTokenSecret;
}
if (_user.github) {
delete _user.github.accessToken;
}
if (_user.discord) {
delete _user.discord.accessToken;
delete _user.discord.refreshToken;
delete _user.discord.expiresDate;
}
// Visible via only the official client
if (!opts.includeSecrets) {
delete _user.email;
delete _user.emailVerified;
delete _user.settings;
delete _user.clientSettings;
}
if (!opts.detail) {
delete _user.twoFactorEnabled;
}
} else {
delete _user.publicKey;
}
if (_user.avatarUrl == null) {
_user.avatarUrl = `${config.driveUrl}/default-avatar.jpg`;
}
if (!meId || !meId.equals(_user.id) || !opts.detail) {
delete _user.avatarId;
delete _user.bannerId;
delete _user.hasUnreadMessagingMessage;
delete _user.hasUnreadNotification;
}
if (meId && !meId.equals(_user.id) && opts.detail) {
const relation = await getRelation(meId, _user.id);
_user.isFollowing = relation.isFollowing;
_user.isFollowed = relation.isFollowed;
_user.hasPendingFollowRequestFromYou = relation.hasPendingFollowRequestFromYou;
_user.hasPendingFollowRequestToYou = relation.hasPendingFollowRequestToYou;
_user.isBlocking = relation.isBlocking;
_user.isBlocked = relation.isBlocked;
_user.isMuted = relation.isMuted;
}
if (opts.detail) {
if (_user.pinnedNoteIds) {
// Populate pinned notes
_user.pinnedNotes = packNoteMany(_user.pinnedNoteIds, meId, {
detail: true
});
}
if (meId && !meId.equals(_user.id)) {
const myFollowingIds = await getFriendIds(meId);
// Get following you know count
_user.followingYouKnowCount = Following.count({
followeeId: { $in: myFollowingIds },
followerId: _user.id
});
// Get followers you know count
_user.followersYouKnowCount = Following.count({
followeeId: _user.id,
followerId: { $in: myFollowingIds }
});
}
}
if (!opts.includeHasUnreadNotes) {
delete _user.hasUnreadSpecifiedNotes;
delete _user.hasUnreadMentions;
}
// カスタム絵文字添付
if (_user.emojis) {
_user.emojis = Emoji.find({
name: { $in: _user.emojis },
host: _user.host
}, {
fields: { _id: false }
});
}
// resolve promises in _user object
_user = await rap(_user);
resolve(_user);
});
/*
function img(url) {
return {
thumbnail: {
large: `${url}`,
medium: '',
small: ''
}
};
}
*/
export async function fetchProxyAccount(): Promise<ILocalUser> {
const meta = await fetchMeta();
return await User.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser;
}