diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2018-03-29 20:32:18 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2018-03-29 20:32:18 +0900 |
| commit | cf33e483f7e6f40e8cbbbc0118a7df70bdaf651f (patch) | |
| tree | 318279530d3392ee40d91968477fc0e78d5cf0f7 /src/models | |
| parent | Update .travis.yml (diff) | |
| download | sharkey-cf33e483f7e6f40e8cbbbc0118a7df70bdaf651f.tar.gz sharkey-cf33e483f7e6f40e8cbbbc0118a7df70bdaf651f.tar.bz2 sharkey-cf33e483f7e6f40e8cbbbc0118a7df70bdaf651f.zip | |
整理した
Diffstat (limited to 'src/models')
| -rw-r--r-- | src/models/access-token.ts | 16 | ||||
| -rw-r--r-- | src/models/app.ts | 103 | ||||
| -rw-r--r-- | src/models/auth-session.ts | 48 | ||||
| -rw-r--r-- | src/models/channel-watching.ts | 13 | ||||
| -rw-r--r-- | src/models/channel.ts | 75 | ||||
| -rw-r--r-- | src/models/drive-file.ts | 113 | ||||
| -rw-r--r-- | src/models/drive-folder.ts | 77 | ||||
| -rw-r--r-- | src/models/favorite.ts | 12 | ||||
| -rw-r--r-- | src/models/following.ts | 13 | ||||
| -rw-r--r-- | src/models/messaging-history.ts | 13 | ||||
| -rw-r--r-- | src/models/messaging-message.ts | 82 | ||||
| -rw-r--r-- | src/models/meta.ts | 8 | ||||
| -rw-r--r-- | src/models/mute.ts | 13 | ||||
| -rw-r--r-- | src/models/notification.ts | 107 | ||||
| -rw-r--r-- | src/models/othello-game.ts | 109 | ||||
| -rw-r--r-- | src/models/othello-matching.ts | 44 | ||||
| -rw-r--r-- | src/models/poll-vote.ts | 13 | ||||
| -rw-r--r-- | src/models/post-reaction.ts | 53 | ||||
| -rw-r--r-- | src/models/post-watching.ts | 12 | ||||
| -rw-r--r-- | src/models/post.ts | 221 | ||||
| -rw-r--r-- | src/models/signin.ts | 34 | ||||
| -rw-r--r-- | src/models/sw-subscription.ts | 13 | ||||
| -rw-r--r-- | src/models/user.ts | 341 |
23 files changed, 1533 insertions, 0 deletions
diff --git a/src/models/access-token.ts b/src/models/access-token.ts new file mode 100644 index 0000000000..4451ca140d --- /dev/null +++ b/src/models/access-token.ts @@ -0,0 +1,16 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const AccessToken = db.get<IAccessTokens>('accessTokens'); +AccessToken.createIndex('token'); +AccessToken.createIndex('hash'); +export default AccessToken; + +export type IAccessTokens = { + _id: mongo.ObjectID; + createdAt: Date; + appId: mongo.ObjectID; + userId: mongo.ObjectID; + token: string; + hash: string; +}; diff --git a/src/models/app.ts b/src/models/app.ts new file mode 100644 index 0000000000..3b80a1602f --- /dev/null +++ b/src/models/app.ts @@ -0,0 +1,103 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import AccessToken from './access-token'; +import db from '../db/mongodb'; +import config from '../conf'; + +const App = db.get<IApp>('apps'); +App.createIndex('nameId'); +App.createIndex('nameIdLower'); +App.createIndex('secret'); +export default App; + +export type IApp = { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + secret: string; + name: string; + nameId: string; + nameIdLower: string; + description: string; + permission: string; + callbackUrl: string; +}; + +export function isValidNameId(nameId: string): boolean { + return typeof nameId == 'string' && /^[a-zA-Z0-9\-]{3,30}$/.test(nameId); +} + +/** + * Pack an app for API response + * + * @param {any} app + * @param {any} me? + * @param {any} options? + * @return {Promise<any>} + */ +export const pack = ( + app: any, + me?: any, + options?: { + includeSecret?: boolean, + includeProfileImageIds?: boolean + } +) => new Promise<any>(async (resolve, reject) => { + const opts = options || { + includeSecret: false, + includeProfileImageIds: false + }; + + let _app: any; + + // Populate the app if 'app' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(app)) { + _app = await App.findOne({ + _id: app + }); + } else if (typeof app === 'string') { + _app = await App.findOne({ + _id: new mongo.ObjectID(app) + }); + } else { + _app = deepcopy(app); + } + + // Me + if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) { + if (typeof me === 'string') { + me = new mongo.ObjectID(me); + } else { + me = me._id; + } + } + + // Rename _id to id + _app.id = _app._id; + delete _app._id; + + delete _app.nameIdLower; + + // Visible by only owner + if (!opts.includeSecret) { + delete _app.secret; + } + + _app.iconUrl = _app.icon != null + ? `${config.drive_url}/${_app.icon}` + : `${config.drive_url}/app-default.jpg`; + + if (me) { + // 既に連携しているか + const exist = await AccessToken.count({ + appId: _app.id, + userId: me, + }, { + limit: 1 + }); + + _app.isAuthorized = exist === 1; + } + + resolve(_app); +}); diff --git a/src/models/auth-session.ts b/src/models/auth-session.ts new file mode 100644 index 0000000000..6fe3468a7b --- /dev/null +++ b/src/models/auth-session.ts @@ -0,0 +1,48 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +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 && !mongo.ObjectID.prototype.isPrototypeOf(me)) { + if (typeof me === 'string') { + me = new mongo.ObjectID(me); + } else { + me = me._id; + } + } + + delete _session._id; + + // Populate app + _session.app = await packApp(_session.appId, me); + + resolve(_session); +}); diff --git a/src/models/channel-watching.ts b/src/models/channel-watching.ts new file mode 100644 index 0000000000..44ca06883f --- /dev/null +++ b/src/models/channel-watching.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const ChannelWatching = db.get<IChannelWatching>('channelWatching'); +export default ChannelWatching; + +export interface IChannelWatching { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + channelId: mongo.ObjectID; + userId: mongo.ObjectID; +} diff --git a/src/models/channel.ts b/src/models/channel.ts new file mode 100644 index 0000000000..67386ac072 --- /dev/null +++ b/src/models/channel.ts @@ -0,0 +1,75 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import { IUser } from './user'; +import Watching from './channel-watching'; +import db from '../db/mongodb'; + +const Channel = db.get<IChannel>('channels'); +export default Channel; + +export type IChannel = { + _id: mongo.ObjectID; + createdAt: Date; + title: string; + userId: mongo.ObjectID; + index: number; + watchingCount: number; +}; + +/** + * Pack a channel for API response + * + * @param channel target + * @param me? serializee + * @return response + */ +export const pack = ( + channel: string | mongo.ObjectID | IChannel, + me?: string | mongo.ObjectID | IUser +) => new Promise<any>(async (resolve, reject) => { + + let _channel: any; + + // Populate the channel if 'channel' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(channel)) { + _channel = await Channel.findOne({ + _id: channel + }); + } else if (typeof channel === 'string') { + _channel = await Channel.findOne({ + _id: new mongo.ObjectID(channel) + }); + } else { + _channel = deepcopy(channel); + } + + // Rename _id to id + _channel.id = _channel._id; + delete _channel._id; + + // Remove needless properties + delete _channel.userId; + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + if (me) { + //#region Watchしているかどうか + const watch = await Watching.findOne({ + userId: meId, + channelId: _channel.id, + deletedAt: { $exists: false } + }); + + _channel.isWatching = watch !== null; + //#endregion + } + + resolve(_channel); +}); diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts new file mode 100644 index 0000000000..9e0df58c45 --- /dev/null +++ b/src/models/drive-file.ts @@ -0,0 +1,113 @@ +import * as mongodb from 'mongodb'; +import deepcopy = require('deepcopy'); +import { pack as packFolder } from './drive-folder'; +import config from '../conf'; +import monkDb, { nativeDbConn } from '../db/mongodb'; + +const DriveFile = monkDb.get<IDriveFile>('driveFiles.files'); + +export default DriveFile; + +const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => { + const db = await nativeDbConn(); + const bucket = new mongodb.GridFSBucket(db, { + bucketName: 'driveFiles' + }); + return bucket; +}; + +export { getGridFSBucket }; + +export type IDriveFile = { + _id: mongodb.ObjectID; + uploadDate: Date; + md5: string; + filename: string; + contentType: string; + metadata: { + properties: any; + userId: mongodb.ObjectID; + folderId: mongodb.ObjectID; + } +}; + +export function validateFileName(name: string): boolean { + return ( + (name.trim().length > 0) && + (name.length <= 200) && + (name.indexOf('\\') === -1) && + (name.indexOf('/') === -1) && + (name.indexOf('..') === -1) + ); +} + +/** + * Pack a drive file for API response + * + * @param {any} file + * @param {any} options? + * @return {Promise<any>} + */ +export const pack = ( + file: any, + options?: { + detail: boolean + } +) => new Promise<any>(async (resolve, reject) => { + const opts = Object.assign({ + detail: false + }, options); + + let _file: any; + + // Populate the file if 'file' is ID + if (mongodb.ObjectID.prototype.isPrototypeOf(file)) { + _file = await DriveFile.findOne({ + _id: file + }); + } else if (typeof file === 'string') { + _file = await DriveFile.findOne({ + _id: new mongodb.ObjectID(file) + }); + } else { + _file = deepcopy(file); + } + + if (!_file) return reject('invalid file arg.'); + + // 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 = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`; + + 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) + ); + } + */ + } + + resolve(_target); +}); diff --git a/src/models/drive-folder.ts b/src/models/drive-folder.ts new file mode 100644 index 0000000000..ad27b151b1 --- /dev/null +++ b/src/models/drive-folder.ts @@ -0,0 +1,77 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +import DriveFile from './drive-file'; + +const DriveFolder = db.get<IDriveFolder>('drive_folders'); +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 + * + * @param {any} folder + * @param {any} options? + * @return {Promise<any>} + */ +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 (mongo.ObjectID.prototype.isPrototypeOf(folder)) { + _folder = await DriveFolder.findOne({ _id: folder }); + } else if (typeof folder === 'string') { + _folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) }); + } else { + _folder = deepcopy(folder); + } + + // Rename _id to id + _folder.id = _folder._id; + delete _folder._id; + + if (opts.detail) { + const childFoldersCount = await DriveFolder.count({ + parentId: _folder.id + }); + + const childFilesCount = await DriveFile.count({ + 'metadata.folderId': _folder.id + }); + + _folder.foldersCount = childFoldersCount; + _folder.filesCount = childFilesCount; + } + + if (opts.detail && _folder.parentId) { + // Populate parent folder + _folder.parent = await pack(_folder.parentId, { + detail: true + }); + } + + resolve(_folder); +}); diff --git a/src/models/favorite.ts b/src/models/favorite.ts new file mode 100644 index 0000000000..2fa00e99c4 --- /dev/null +++ b/src/models/favorite.ts @@ -0,0 +1,12 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const Favorites = db.get<IFavorite>('favorites'); +export default Favorites; + +export type IFavorite = { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + postId: mongo.ObjectID; +}; diff --git a/src/models/following.ts b/src/models/following.ts new file mode 100644 index 0000000000..3f8a9be50f --- /dev/null +++ b/src/models/following.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const Following = db.get<IFollowing>('following'); +export default Following; + +export type IFollowing = { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + followeeId: mongo.ObjectID; + followerId: mongo.ObjectID; +}; diff --git a/src/models/messaging-history.ts b/src/models/messaging-history.ts new file mode 100644 index 0000000000..6864e22d2f --- /dev/null +++ b/src/models/messaging-history.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const MessagingHistory = db.get<IMessagingHistory>('messagingHistories'); +export default MessagingHistory; + +export type IMessagingHistory = { + _id: mongo.ObjectID; + updatedAt: Date; + userId: mongo.ObjectID; + partnerId: mongo.ObjectID; + messageId: mongo.ObjectID; +}; diff --git a/src/models/messaging-message.ts b/src/models/messaging-message.ts new file mode 100644 index 0000000000..8bee657c34 --- /dev/null +++ b/src/models/messaging-message.ts @@ -0,0 +1,82 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import { pack as packUser } from './user'; +import { pack as packFile } from './drive-file'; +import db from '../db/mongodb'; +import parse from '../common/text'; + +const MessagingMessage = db.get<IMessagingMessage>('messagingMessages'); +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 text.length <= 1000 && text.trim() != ''; +} + +/** + * Pack a messaging message for API response + * + * @param {any} message + * @param {any} me? + * @param {any} options? + * @return {Promise<any>} + */ +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 (mongo.ObjectID.prototype.isPrototypeOf(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; + + // Parse text + if (_message.text) { + _message.ast = parse(_message.text); + } + + // Populate user + _message.user = await packUser(_message.userId, me); + + if (_message.fileId) { + // Populate file + _message.file = await packFile(_message.fileId); + } + + if (opts.populateRecipient) { + // Populate recipient + _message.recipient = await packUser(_message.recipientId, me); + } + + resolve(_message); +}); diff --git a/src/models/meta.ts b/src/models/meta.ts new file mode 100644 index 0000000000..710bb23382 --- /dev/null +++ b/src/models/meta.ts @@ -0,0 +1,8 @@ +import db from '../db/mongodb'; + +const Meta = db.get<IMeta>('meta'); +export default Meta; + +export type IMeta = { + broadcasts: any[]; +}; diff --git a/src/models/mute.ts b/src/models/mute.ts new file mode 100644 index 0000000000..8793615967 --- /dev/null +++ b/src/models/mute.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const Mute = db.get<IMute>('mute'); +export default Mute; + +export interface IMute { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + muterId: mongo.ObjectID; + muteeId: mongo.ObjectID; +} diff --git a/src/models/notification.ts b/src/models/notification.ts new file mode 100644 index 0000000000..078c8d5118 --- /dev/null +++ b/src/models/notification.ts @@ -0,0 +1,107 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +import { IUser, pack as packUser } from './user'; +import { pack as packPost } from './post'; + +const Notification = db.get<INotification>('notifications'); +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している)投稿が返信された + * repost - (自分または自分がWatchしている)投稿がRepostされた + * quote - (自分または自分がWatchしている)投稿が引用Repostされた + * reaction - (自分または自分がWatchしている)投稿にリアクションされた + * poll_vote - (自分または自分がWatchしている)投稿の投票に投票された + */ + type: 'follow' | 'mention' | 'reply' | 'repost' | 'quote' | 'reaction' | 'poll_vote'; + + /** + * 通知が読まれたかどうか + */ + isRead: Boolean; +} + +/** + * Pack a notification for API response + * + * @param {any} notification + * @return {Promise<any>} + */ +export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => { + let _notification: any; + + // Populate the notification if 'notification' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(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': + // nope + break; + case 'mention': + case 'reply': + case 'repost': + case 'quote': + case 'reaction': + case 'poll_vote': + // Populate post + _notification.post = await packPost(_notification.postId, me); + break; + default: + console.error(`Unknown type: ${_notification.type}`); + break; + } + + resolve(_notification); +}); diff --git a/src/models/othello-game.ts b/src/models/othello-game.ts new file mode 100644 index 0000000000..297aee3028 --- /dev/null +++ b/src/models/othello-game.ts @@ -0,0 +1,109 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +import { IUser, pack as packUser } from './user'; + +const OthelloGame = db.get<IOthelloGame>('othelloGames'); +export default OthelloGame; + +export interface IOthelloGame { + _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; + logs: Array<{ + 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 othello 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 (mongo.ObjectID.prototype.isPrototypeOf(game)) { + _game = await OthelloGame.findOne({ + _id: game + }); + } else if (typeof game === 'string') { + _game = await OthelloGame.findOne({ + _id: new mongo.ObjectID(game) + }); + } else { + _game = deepcopy(game); + } + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + // Rename _id to id + _game.id = _game._id; + delete _game._id; + + if (opts.detail === false) { + delete _game.logs; + delete _game.settings.map; + } else { + // 互換性のため + if (_game.settings.map.hasOwnProperty('size')) { + _game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g')); + } + } + + // Populate user + _game.user1 = await packUser(_game.user1Id, meId); + _game.user2 = await packUser(_game.user2Id, meId); + if (_game.winnerId) { + _game.winner = await packUser(_game.winnerId, meId); + } else { + _game.winner = null; + } + + resolve(_game); +}); diff --git a/src/models/othello-matching.ts b/src/models/othello-matching.ts new file mode 100644 index 0000000000..8082c258c8 --- /dev/null +++ b/src/models/othello-matching.ts @@ -0,0 +1,44 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +import { IUser, pack as packUser } from './user'; + +const Matching = db.get<IMatching>('othelloMatchings'); +export default Matching; + +export interface IMatching { + _id: mongo.ObjectID; + createdAt: Date; + parentId: mongo.ObjectID; + childId: mongo.ObjectID; +} + +/** + * Pack an othello 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 + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + const _matching = deepcopy(matching); + + // Rename _id to id + _matching.id = _matching._id; + delete _matching._id; + + // Populate user + _matching.parent = await packUser(_matching.parentId, meId); + _matching.child = await packUser(_matching.childId, meId); + + resolve(_matching); +}); diff --git a/src/models/poll-vote.ts b/src/models/poll-vote.ts new file mode 100644 index 0000000000..cd18ffd5f8 --- /dev/null +++ b/src/models/poll-vote.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const PollVote = db.get<IPollVote>('pollVotes'); +export default PollVote; + +export interface IPollVote { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + postId: mongo.ObjectID; + choice: number; +} diff --git a/src/models/post-reaction.ts b/src/models/post-reaction.ts new file mode 100644 index 0000000000..3fc33411fb --- /dev/null +++ b/src/models/post-reaction.ts @@ -0,0 +1,53 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +import Reaction from './post-reaction'; +import { pack as packUser } from './user'; + +const PostReaction = db.get<IPostReaction>('postReactions'); +export default PostReaction; + +export interface IPostReaction { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + postId: mongo.ObjectID; + userId: mongo.ObjectID; + reaction: string; +} + +/** + * Pack a reaction for API response + * + * @param {any} reaction + * @param {any} me? + * @return {Promise<any>} + */ +export const pack = ( + reaction: any, + me?: any +) => new Promise<any>(async (resolve, reject) => { + let _reaction: any; + + // Populate the reaction if 'reaction' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(reaction)) { + _reaction = await Reaction.findOne({ + _id: reaction + }); + } else if (typeof reaction === 'string') { + _reaction = await Reaction.findOne({ + _id: new mongo.ObjectID(reaction) + }); + } else { + _reaction = deepcopy(reaction); + } + + // Rename _id to id + _reaction.id = _reaction._id; + delete _reaction._id; + + // Populate user + _reaction.user = await packUser(_reaction.userId, me); + + resolve(_reaction); +}); diff --git a/src/models/post-watching.ts b/src/models/post-watching.ts new file mode 100644 index 0000000000..b4ddcaafa6 --- /dev/null +++ b/src/models/post-watching.ts @@ -0,0 +1,12 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const PostWatching = db.get<IPostWatching>('postWatching'); +export default PostWatching; + +export interface IPostWatching { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + postId: mongo.ObjectID; +} diff --git a/src/models/post.ts b/src/models/post.ts new file mode 100644 index 0000000000..833e599320 --- /dev/null +++ b/src/models/post.ts @@ -0,0 +1,221 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import rap from '@prezzemolo/rap'; +import db from '../db/mongodb'; +import { IUser, pack as packUser } from './user'; +import { pack as packApp } from './app'; +import { pack as packChannel } from './channel'; +import Vote from './poll-vote'; +import Reaction from './post-reaction'; +import { pack as packFile } from './drive-file'; +import parse from '../common/text'; + +const Post = db.get<IPost>('posts'); + +export default Post; + +export function isValidText(text: string): boolean { + return text.length <= 1000 && text.trim() != ''; +} + +export type IPost = { + _id: mongo.ObjectID; + channelId: mongo.ObjectID; + createdAt: Date; + mediaIds: mongo.ObjectID[]; + replyId: mongo.ObjectID; + repostId: mongo.ObjectID; + poll: any; // todo + text: string; + userId: mongo.ObjectID; + appId: mongo.ObjectID; + viaMobile: boolean; + repostCount: number; + repliesCount: number; + reactionCounts: any; + mentions: mongo.ObjectID[]; + geo: { + coordinates: number[]; + altitude: number; + accuracy: number; + altitudeAccuracy: number; + heading: number; + speed: number; + }; +}; + +/** + * Pack a post for API response + * + * @param post target + * @param me? serializee + * @param options? serialize options + * @return response + */ +export const pack = async ( + post: string | mongo.ObjectID | IPost, + me?: string | mongo.ObjectID | IUser, + options?: { + detail: boolean + } +) => { + const opts = options || { + detail: true, + }; + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + let _post: any; + + // Populate the post if 'post' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(post)) { + _post = await Post.findOne({ + _id: post + }); + } else if (typeof post === 'string') { + _post = await Post.findOne({ + _id: new mongo.ObjectID(post) + }); + } else { + _post = deepcopy(post); + } + + if (!_post) throw 'invalid post arg.'; + + const id = _post._id; + + // Rename _id to id + _post.id = _post._id; + delete _post._id; + + delete _post.mentions; + if (_post.geo) delete _post.geo.type; + + // Parse text + if (_post.text) { + _post.ast = parse(_post.text); + } + + // Populate user + _post.user = packUser(_post.userId, meId); + + // Populate app + if (_post.appId) { + _post.app = packApp(_post.appId); + } + + // Populate channel + if (_post.channelId) { + _post.channel = packChannel(_post.channelId); + } + + // Populate media + if (_post.mediaIds) { + _post.media = Promise.all(_post.mediaIds.map(fileId => + packFile(fileId) + )); + } + + // When requested a detailed post data + if (opts.detail) { + // Get previous post info + _post.prev = (async () => { + const prev = await Post.findOne({ + userId: _post.userId, + _id: { + $lt: id + } + }, { + fields: { + _id: true + }, + sort: { + _id: -1 + } + }); + return prev ? prev._id : null; + })(); + + // Get next post info + _post.next = (async () => { + const next = await Post.findOne({ + userId: _post.userId, + _id: { + $gt: id + } + }, { + fields: { + _id: true + }, + sort: { + _id: 1 + } + }); + return next ? next._id : null; + })(); + + if (_post.replyId) { + // Populate reply to post + _post.reply = pack(_post.replyId, meId, { + detail: false + }); + } + + if (_post.repostId) { + // Populate repost + _post.repost = pack(_post.repostId, meId, { + detail: _post.text == null + }); + } + + // Poll + if (meId && _post.poll) { + _post.poll = (async (poll) => { + const vote = await Vote + .findOne({ + userId: meId, + postId: id + }); + + if (vote != null) { + const myChoice = poll.choices + .filter(c => c.id == vote.choice)[0]; + + myChoice.isVoted = true; + } + + return poll; + })(_post.poll); + } + + // Fetch my reaction + if (meId) { + _post.myReaction = (async () => { + const reaction = await Reaction + .findOne({ + userId: meId, + postId: id, + deletedAt: { $exists: false } + }); + + if (reaction) { + return reaction.reaction; + } + + return null; + })(); + } + } + + // resolve promises in _post object + _post = await rap(_post); + + return _post; +}; diff --git a/src/models/signin.ts b/src/models/signin.ts new file mode 100644 index 0000000000..7f56e1a283 --- /dev/null +++ b/src/models/signin.ts @@ -0,0 +1,34 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('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); +}); diff --git a/src/models/sw-subscription.ts b/src/models/sw-subscription.ts new file mode 100644 index 0000000000..743d0d2dd9 --- /dev/null +++ b/src/models/sw-subscription.ts @@ -0,0 +1,13 @@ +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; +} diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000000..4f2872800f --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,341 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import rap from '@prezzemolo/rap'; +import db from '../db/mongodb'; +import { IPost, pack as packPost } from './post'; +import Following from './following'; +import Mute from './mute'; +import getFriends from '../server/api/common/get-friends'; +import config from '../conf'; + +const User = db.get<IUser>('users'); + +User.createIndex('username'); +User.createIndex('account.token'); + +export default User; + +export function validateUsername(username: string): boolean { + return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username); +} + +export function validatePassword(password: string): boolean { + return typeof password == 'string' && password != ''; +} + +export function isValidName(name: string): boolean { + return typeof name == 'string' && name.length < 30 && 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); +} + +export type ILocalAccount = { + keypair: string; + email: string; + links: string[]; + password: string; + token: string; + twitter: { + accessToken: string; + accessTokenSecret: string; + userId: string; + screenName: string; + }; + line: { + userId: string; + }; + profile: { + location: string; + birthday: string; // 'YYYY-MM-DD' + tags: string[]; + }; + lastUsedAt: Date; + isBot: boolean; + isPro: boolean; + twoFactorSecret: string; + twoFactorEnabled: boolean; + twoFactorTempSecret: string; + clientSettings: any; + settings: any; +}; + +export type IRemoteAccount = { + uri: string; +}; + +export type IUser = { + _id: mongo.ObjectID; + createdAt: Date; + deletedAt: Date; + followersCount: number; + followingCount: number; + name: string; + postsCount: number; + driveCapacity: number; + username: string; + usernameLower: string; + avatarId: mongo.ObjectID; + bannerId: mongo.ObjectID; + data: any; + description: string; + latestPost: IPost; + pinnedPostId: mongo.ObjectID; + isSuspended: boolean; + keywords: string[]; + host: string; + hostLower: string; + account: ILocalAccount | IRemoteAccount; +}; + +export function init(user): IUser { + user._id = new mongo.ObjectID(user._id); + user.avatarId = new mongo.ObjectID(user.avatarId); + user.bannerId = new mongo.ObjectID(user.bannerId); + user.pinnedPostId = new mongo.ObjectID(user.pinnedPostId); + return user; +} + +/** + * 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 + } +) => new Promise<any>(async (resolve, reject) => { + + const opts = Object.assign({ + detail: false, + includeSecrets: false + }, options); + + let _user: any; + + const fields = opts.detail ? { + } : { + 'account.settings': false, + 'account.clientSettings': false, + 'account.profile': false, + 'account.keywords': false, + 'account.domains': false + }; + + // Populate the user if 'user' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(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) return reject('invalid user arg.'); + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(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; + + // Remove needless properties + delete _user.latestPost; + + if (!_user.host) { + // Remove private properties + delete _user.account.keypair; + delete _user.account.password; + delete _user.account.token; + delete _user.account.twoFactorTempSecret; + delete _user.account.twoFactorSecret; + delete _user.usernameLower; + if (_user.account.twitter) { + delete _user.account.twitter.accessToken; + delete _user.account.twitter.accessTokenSecret; + } + delete _user.account.line; + + // Visible via only the official client + if (!opts.includeSecrets) { + delete _user.account.email; + delete _user.account.settings; + delete _user.account.clientSettings; + } + + if (!opts.detail) { + delete _user.account.twoFactorEnabled; + } + } + + _user.avatarUrl = _user.avatarId != null + ? `${config.drive_url}/${_user.avatarId}` + : `${config.drive_url}/default-avatar.jpg`; + + _user.bannerUrl = _user.bannerId != null + ? `${config.drive_url}/${_user.bannerId}` + : null; + + if (!meId || !meId.equals(_user.id) || !opts.detail) { + delete _user.avatarId; + delete _user.bannerId; + + delete _user.driveCapacity; + } + + if (meId && !meId.equals(_user.id)) { + // Whether the user is following + _user.isFollowing = (async () => { + const follow = await Following.findOne({ + followerId: meId, + followeeId: _user.id, + deletedAt: { $exists: false } + }); + return follow !== null; + })(); + + // Whether the user is followed + _user.isFollowed = (async () => { + const follow2 = await Following.findOne({ + followerId: _user.id, + followeeId: meId, + deletedAt: { $exists: false } + }); + return follow2 !== null; + })(); + + // Whether the user is muted + _user.isMuted = (async () => { + const mute = await Mute.findOne({ + muterId: meId, + muteeId: _user.id, + deletedAt: { $exists: false } + }); + return mute !== null; + })(); + } + + if (opts.detail) { + if (_user.pinnedPostId) { + // Populate pinned post + _user.pinnedPost = packPost(_user.pinnedPostId, meId, { + detail: true + }); + } + + if (meId && !meId.equals(_user.id)) { + const myFollowingIds = await getFriends(meId); + + // Get following you know count + _user.followingYouKnowCount = Following.count({ + followeeId: { $in: myFollowingIds }, + followerId: _user.id, + deletedAt: { $exists: false } + }); + + // Get followers you know count + _user.followersYouKnowCount = Following.count({ + followeeId: _user.id, + followerId: { $in: myFollowingIds }, + deletedAt: { $exists: false } + }); + } + } + + // resolve promises in _user object + _user = await rap(_user); + + resolve(_user); +}); + +/** + * Pack a user for ActivityPub + * + * @param user target + * @return Packed user + */ +export const packForAp = ( + user: string | mongo.ObjectID | IUser +) => new Promise<any>(async (resolve, reject) => { + + let _user: any; + + const fields = { + // something + }; + + // Populate the user if 'user' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(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) return reject('invalid user arg.'); + + const userUrl = `${config.url}/@${_user.username}`; + + resolve({ + "@context": ["https://www.w3.org/ns/activitystreams", { + "@language": "ja" + }], + "type": "Person", + "id": userUrl, + "following": `${userUrl}/following.json`, + "followers": `${userUrl}/followers.json`, + "liked": `${userUrl}/liked.json`, + "inbox": `${userUrl}/inbox.json`, + "outbox": `${userUrl}/outbox.json`, + "preferredUsername": _user.username, + "name": _user.name, + "summary": _user.description, + "icon": [ + `${config.drive_url}/${_user.avatarId}` + ] + }); +}); + +/* +function img(url) { + return { + thumbnail: { + large: `${url}`, + medium: '', + small: '' + } + }; +} +*/ |