diff --git a/.config/docker_example.env b/.config/docker_example.env new file mode 100644 index 0000000000000000000000000000000000000000..411d93659bf35981e21a86aa370a5eabe0ab1639 --- /dev/null +++ b/.config/docker_example.env @@ -0,0 +1,5 @@ +# db settings +POSTGRES_PASSWORD="example-misskey-pass" +POSTGRES_USER="example-misskey-user" +POSTGRES_DB="misskey" + diff --git a/.config/example.yml b/.config/example.yml index 70c096baa13706ef52bd938dfa74c1a7e2153128..10239f1a760bc538146449ab27ca0d9f01beafb3 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -1,8 +1,16 @@ +#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â” +# Misskey configuration +#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â” + +# ┌─────┠+#───┘ URL └───────────────────────────────────────────────────── + # Final accessible URL seen by a user. url: https://example.tld/ +# ┌───────────────────────┠+#───┘ Port and TLS settings └─────────────────────────────────── -### Port and TLS settings ###################################### # # Misskey supports two deployment options for public. # @@ -30,28 +38,51 @@ url: https://example.tld/ # You need to set Certificate in 'https' section. # To use option 1, uncomment below line. -# port: 3000 # A port that your Misskey server should listen. +#port: 3000 # A port that your Misskey server should listen. # To use option 2, uncomment below lines. -# port: 443 -# -# https: -# # path for certification -# key: /etc/letsencrypt/live/example.tld/privkey.pem -# cert: /etc/letsencrypt/live/example.tld/fullchain.pem +#port: 443 -################################################################ +#https: +# # path for certification +# key: /etc/letsencrypt/live/example.tld/privkey.pem +# cert: /etc/letsencrypt/live/example.tld/fullchain.pem +# ┌──────────────────────────┠+#───┘ PostgreSQL configuration └──────────────────────────────── -mongodb: +db: host: localhost - port: 27017 + port: 5432 + + # Database name db: misskey + + # Auth user: example-misskey-user pass: example-misskey-pass +# ┌─────────────────────┠+#───┘ Redis configuration └───────────────────────────────────── + +#redis: +# host: localhost +# port: 6379 +# pass: example-pass + +# ┌─────────────────────────────┠+#───┘ Elasticsearch configuration └───────────────────────────── + +#elasticsearch: +# host: localhost +# port: 9200 +# pass: null + +# ┌────────────────────────────────────┠+#───┘ File storage (Drive) configuration └────────────────────── + drive: - storage: 'db' + storage: 'fs' # OR @@ -88,25 +119,43 @@ drive: # accessKey: XXX # secretKey: YYY -# If enabled: -# The first account created is automatically marked as Admin. -autoAdmin: true +# ┌───────────────┠+#───┘ ID generation └─────────────────────────────────────────── -# -# Below settings are optional -# +# You can select the ID generation method. +# You don't usually need to change this setting, but you can +# change it according to your preferences. -# Redis -#redis: -# host: localhost -# port: 6379 -# pass: example-pass +# Available methods: +# aid1 ... Use AID for ID generation (with random 1 char) +# aid2 ... Use AID for ID generation (with random 2 chars) +# aid3 ... Use AID for ID generation (with random 3 chars) +# aid4 ... Use AID for ID generation (with random 4 chars) +# ulid ... Use ulid for ID generation +# objectid ... This is left for backward compatibility. -# Elasticsearch -#elasticsearch: -# host: localhost -# port: 9200 -# pass: null +# AID(n) is the original ID generation method. +# The trailing n represents the number of random characters that +# will be suffixed. +# The larger n is the safer. If n is small, the possibility of +# collision at the same time increases, but there are also +# advantages such as shortening of the URL. + +# ULID: Universally Unique Lexicographically Sortable Identifier. +# for more details: https://github.com/ulid/spec +# * Normally, AID should be sufficient. + +# ObjectID is the method used in previous versions of Misskey. +# * Choose this if you are migrating from a previous Misskey. + +id: 'aid2' + +# ┌─────────────────────┠+#───┘ Other configuration └───────────────────────────────────── + +# If enabled: +# The first account created is automatically marked as Admin. +autoAdmin: true # Whether disable HSTS #disableHsts: true diff --git a/.config/mongo_initdb_example.js b/.config/mongo_initdb_example.js deleted file mode 100644 index b7e7321f3531cd085fb36b5d126012912683cbbe..0000000000000000000000000000000000000000 --- a/.config/mongo_initdb_example.js +++ /dev/null @@ -1,13 +0,0 @@ -var user = { - user: 'example-misskey-user', - pwd: 'example-misskey-pass', - roles: [ - { - role: 'readWrite', - db: 'misskey' - } - ] -}; - -db.createUser(user); - diff --git a/.dockerignore b/.dockerignore old mode 100755 new mode 100644 index a25d4e571827fba23c37c8aa7a3fb250665da632..324c4bce58c3289c6add88e87151b9c7c9bf1b55 --- a/.dockerignore +++ b/.dockerignore @@ -5,8 +5,8 @@ .vscode Dockerfile build/ +db/ docker-compose.yml +elasticsearch/ node_modules/ -mongo/ redis/ -elasticsearch/ diff --git a/.gitignore b/.gitignore index 98ee82cd7e0e1d05fe31118c217285b130310345..650d4f6128fddda70ef35a4ca122fd80412bb892 100644 --- a/.gitignore +++ b/.gitignore @@ -8,14 +8,15 @@ built /data /.cache-loader +/db +/elasticsearch npm-debug.log *.pem run.bat api-docs.json *.log /redis -/mongo -/elasticsearch *.code-workspace yarn.lock .DS_Store +/files diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1ad1f8041d8efa4db10b65ce4ee1d760147b1cb..a45ed5cb5f3662e685d56b9db00909493c91a86d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,3 +75,61 @@ src ... Source code test ... Test code ``` + +## Notes +### placeholder +SQLをクエリビルダã§çµ„ã¿ç«‹ã¦ã‚‹éš›ã€ä½¿ç”¨ã™ã‚‹ãƒ—レースホルダã¯é‡è¤‡ã—ã¦ã¯ãªã‚‰ãªã„ +例ãˆã° +``` ts +query.andWhere(new Brackets(qb => { + for (const type of ps.fileType) { + qb.orWhere(`:type = ANY(note.attachedFileTypes)`, { type: type }); + } +})); +``` +ã¨æ›¸ãã¨ã€ãƒ«ãƒ¼ãƒ—ä¸ã§`type`ã¨ã„ã†ãƒ—レースホルダãŒè¤‡æ•°å›žä½¿ã‚ã‚Œã¦ã—ã¾ã„ãŠã‹ã—ããªã‚‹ +ã ã‹ã‚‰æ¬¡ã®ã‚ˆã†ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚‹ +```ts +query.andWhere(new Brackets(qb => { + for (const type of ps.fileType) { + const i = ps.fileType.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } +})); +``` + +### `null` in SQL +SQLを発行ã™ã‚‹éš›ã€ãƒ‘ラメータãŒ`null`ã«ãªã‚‹å¯èƒ½æ€§ã®ã‚ã‚‹å ´åˆã¯SQL文を出ã—分ã‘ãªã‘ã‚Œã°ãªã‚‰ãªã„ +例ãˆã° +``` ts +query.where('file.folderId = :folderId', { folderId: ps.folderId }); +``` +ã¨ã„ã†å‡¦ç†ã§ã€`ps.folderId`ãŒ`null`ã ã¨çµæžœçš„ã«`file.folderId = null`ã®ã‚ˆã†ãªã‚¯ã‚¨ãƒªãŒç™ºè¡Œã•ã‚Œã¦ã—ã¾ã„ã€ã“ã‚Œã¯æ£ã—ã„SQLã§ã¯ãªã„ã®ã§æœŸå¾…ã—ãŸçµæžœãŒå¾—られãªã„ +ã ã‹ã‚‰æ¬¡ã®ã‚ˆã†ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚‹ +``` ts +if (ps.folderId) { + query.where('file.folderId = :folderId', { folderId: ps.folderId }); +} else { + query.where('file.folderId IS NULL'); +} +``` + +### `[]` in SQL +SQLを発行ã™ã‚‹éš›ã€`IN`ã®ãƒ‘ラメータãŒ`[]`(空ã®é…列)ã«ãªã‚‹å¯èƒ½æ€§ã®ã‚ã‚‹å ´åˆã¯SQL文を出ã—分ã‘ãªã‘ã‚Œã°ãªã‚‰ãªã„ +例ãˆã° +``` ts +const users = await Users.find({ + id: In(userIds) +}); +``` +ã¨ã„ã†å‡¦ç†ã§ã€`userIds`ãŒ`[]`ã ã¨çµæžœçš„ã«`user.id IN ()`ã®ã‚ˆã†ãªã‚¯ã‚¨ãƒªãŒç™ºè¡Œã•ã‚Œã¦ã—ã¾ã„ã€ã“ã‚Œã¯æ£ã—ã„SQLã§ã¯ãªã„ã®ã§æœŸå¾…ã—ãŸçµæžœãŒå¾—られãªã„ +ã ã‹ã‚‰æ¬¡ã®ã‚ˆã†ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚‹ +``` ts +const users = userIds.length > 0 ? await Users.find({ + id: In(userIds) +}) : []; +``` + +### `undefined`ã«ã”用心 +MongoDBã®æ™‚ã¨ã¯é•ã„ã€findOneã§ãƒ¬ã‚³ãƒ¼ãƒ‰ã‚’å–å¾—ã™ã‚‹æ™‚ã«å¯¾è±¡ãƒ¬ã‚³ãƒ¼ãƒ‰ãŒå˜åœ¨ã—ãªã„å ´åˆ **`undefined`** ãŒè¿”ã£ã¦ãã‚‹ã®ã§æ³¨æ„。 +MongoDBã¯`null`ã§è¿”ã—ã¦ãã¦ãŸã®ã§ã€ãã®æ„Ÿè¦šã§`if (x === null)`ã¨ã‹æ›¸ãã¨ãƒã‚°ã‚‹ã€‚代ã‚ã‚Šã«`if (x == null)`ã¨æ›¸ã„ã¦ãã ã•ã„ diff --git a/Dockerfile b/Dockerfile index ad04fb33dc57b1c2b7ad6d5818b49fffddb065c7..ec7d8a6a2795b609ed38f956674c886e2d068222 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,8 +23,9 @@ RUN apk add --no-cache \ zlib-dev RUN npm i -g yarn -COPY . ./ +COPY package.json ./ RUN yarn install +COPY . ./ RUN yarn build FROM base AS runner diff --git a/binding.gyp b/binding.gyp deleted file mode 100644 index 0349526d52368e10ea34df96a8fc8eb4ef362903..0000000000000000000000000000000000000000 --- a/binding.gyp +++ /dev/null @@ -1,9 +0,0 @@ -{ - 'targets': [ - { - 'target_name': 'crypto_key', - 'sources': ['src/crypto_key.cc'], - 'include_dirs': ['<!(node -e "require(\'nan\')")'] - } - ] -} diff --git a/cli/migration/2.0.0.js b/cli/migration/2.0.0.js deleted file mode 100644 index f7298972e5a0d385655a47361e7698b4b73f6ba1..0000000000000000000000000000000000000000 --- a/cli/migration/2.0.0.js +++ /dev/null @@ -1,57 +0,0 @@ -// for Node.js interpret - -const chalk = require('chalk'); -const sequential = require('promise-sequential'); - -const { default: User } = require('../../built/models/user'); -const { default: DriveFile } = require('../../built/models/drive-file'); - -async function main() { - const promiseGens = []; - - const count = await DriveFile.count({}); - - let prev; - - for (let i = 0; i < count; i++) { - promiseGens.push(() => { - const promise = new Promise(async (res, rej) => { - const file = await DriveFile.findOne(prev ? { - _id: { $gt: prev._id } - } : {}, { - sort: { - _id: 1 - } - }); - - prev = file; - - const user = await User.findOne({ _id: file.metadata.userId }); - - DriveFile.update({ - _id: file._id - }, { - $set: { - 'metadata._user': { - host: user.host - } - } - }).then(() => { - res([i, file]); - }).catch(rej); - }); - - promise.then(([i, file]) => { - console.log(chalk`{gray ${i}} {green done: {bold ${file._id}} ${file.filename}}`); - }); - - return promise; - }); - } - - return await sequential(promiseGens); -} - -main().then(() => { - console.log('ALL DONE'); -}).catch(console.error); diff --git a/cli/migration/2.4.0.js b/cli/migration/2.4.0.js deleted file mode 100644 index aa37849aa1c48018f9b0cc0806b51136b896d060..0000000000000000000000000000000000000000 --- a/cli/migration/2.4.0.js +++ /dev/null @@ -1,71 +0,0 @@ -// for Node.js interpret - -const chalk = require('chalk'); -const sequential = require('promise-sequential'); - -const { default: User } = require('../../built/models/user'); -const { default: DriveFile } = require('../../built/models/drive-file'); - -async function main() { - const promiseGens = []; - - const count = await User.count({}); - - let prev; - - for (let i = 0; i < count; i++) { - promiseGens.push(() => { - const promise = new Promise(async (res, rej) => { - const user = await User.findOne(prev ? { - _id: { $gt: prev._id } - } : {}, { - sort: { - _id: 1 - } - }); - - prev = user; - - const set = {}; - - if (user.avatarId != null) { - const file = await DriveFile.findOne({ _id: user.avatarId }); - - if (file && file.metadata.properties.avgColor) { - set.avatarColor = file.metadata.properties.avgColor; - } - } - - if (user.bannerId != null) { - const file = await DriveFile.findOne({ _id: user.bannerId }); - - if (file && file.metadata.properties.avgColor) { - set.bannerColor = file.metadata.properties.avgColor; - } - } - - if (Object.keys(set).length === 0) return res([i, user]); - - User.update({ - _id: user._id - }, { - $set: set - }).then(() => { - res([i, user]); - }).catch(rej); - }); - - promise.then(([i, user]) => { - console.log(chalk`{gray ${i}} {green done: {bold ${user._id}} @${user.username}}`); - }); - - return promise; - }); - } - - return await sequential(promiseGens); -} - -main().then(() => { - console.log('ALL DONE'); -}).catch(console.error); diff --git a/cli/migration/5.0.0.js b/cli/migration/5.0.0.js deleted file mode 100644 index bef103fe4a84fbb6ea42074b63294bbbe70bc290..0000000000000000000000000000000000000000 --- a/cli/migration/5.0.0.js +++ /dev/null @@ -1,9 +0,0 @@ -const { default: DriveFile } = require('../../built/models/drive-file'); - -DriveFile.update({}, { - $rename: { - 'metadata.isMetaOnly': 'metadata.withoutChunks' - } -}, { - multi: true -}); diff --git a/cli/migration/7.0.0.js b/cli/migration/7.0.0.js deleted file mode 100644 index fa5e363db87d8f1a2e2b06f14b4aa0667ebfa67d..0000000000000000000000000000000000000000 --- a/cli/migration/7.0.0.js +++ /dev/null @@ -1,134 +0,0 @@ -const { default: Stats } = require('../../built/models/stats'); -const { default: User } = require('../../built/models/user'); -const { default: Note } = require('../../built/models/note'); -const { default: DriveFile } = require('../../built/models/drive-file'); - -const now = new Date(); -const y = now.getFullYear(); -const m = now.getMonth(); -const d = now.getDate(); -const today = new Date(y, m, d); - -async function main() { - const localUsersCount = await User.count({ - host: null - }); - - const remoteUsersCount = await User.count({ - host: { $ne: null } - }); - - const localNotesCount = await Note.count({ - '_user.host': null - }); - - const remoteNotesCount = await Note.count({ - '_user.host': { $ne: null } - }); - - const localDriveFilesCount = await DriveFile.count({ - 'metadata._user.host': null - }); - - const remoteDriveFilesCount = await DriveFile.count({ - 'metadata._user.host': { $ne: null } - }); - - const localDriveFilesSize = await DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': null, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(aggregates => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - const remoteDriveFilesSize = await DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': { $ne: null }, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(aggregates => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - await Stats.insert({ - date: today, - users: { - local: { - total: localUsersCount, - diff: 0 - }, - remote: { - total: remoteUsersCount, - diff: 0 - } - }, - notes: { - local: { - total: localNotesCount, - diff: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }, - remote: { - total: remoteNotesCount, - diff: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - } - }, - drive: { - local: { - totalCount: localDriveFilesCount, - totalSize: localDriveFilesSize, - diffCount: 0, - diffSize: 0 - }, - remote: { - totalCount: remoteDriveFilesCount, - totalSize: remoteDriveFilesSize, - diffCount: 0, - diffSize: 0 - } - } - }); - - console.log('done'); -} - -main(); diff --git a/cli/migration/8.0.0.js b/cli/migration/8.0.0.js deleted file mode 100644 index fd6cb245250268f7198051394d531ddf515c7410..0000000000000000000000000000000000000000 --- a/cli/migration/8.0.0.js +++ /dev/null @@ -1,144 +0,0 @@ -const { default: Stats } = require('../../built/models/stats'); -const { default: User } = require('../../built/models/user'); -const { default: Note } = require('../../built/models/note'); -const { default: DriveFile } = require('../../built/models/drive-file'); - -const now = new Date(); -const y = now.getFullYear(); -const m = now.getMonth(); -const d = now.getDate(); -const h = now.getHours(); -const date = new Date(y, m, d, h); - -async function main() { - await Stats.update({}, { - $set: { - span: 'day' - } - }, { - multi: true - }); - - const localUsersCount = await User.count({ - host: null - }); - - const remoteUsersCount = await User.count({ - host: { $ne: null } - }); - - const localNotesCount = await Note.count({ - '_user.host': null - }); - - const remoteNotesCount = await Note.count({ - '_user.host': { $ne: null } - }); - - const localDriveFilesCount = await DriveFile.count({ - 'metadata._user.host': null - }); - - const remoteDriveFilesCount = await DriveFile.count({ - 'metadata._user.host': { $ne: null } - }); - - const localDriveFilesSize = await DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': null, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(aggregates => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - const remoteDriveFilesSize = await DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': { $ne: null }, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(aggregates => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - await Stats.insert({ - date: date, - span: 'hour', - users: { - local: { - total: localUsersCount, - diff: 0 - }, - remote: { - total: remoteUsersCount, - diff: 0 - } - }, - notes: { - local: { - total: localNotesCount, - diff: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }, - remote: { - total: remoteNotesCount, - diff: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - } - }, - drive: { - local: { - totalCount: localDriveFilesCount, - totalSize: localDriveFilesSize, - diffCount: 0, - diffSize: 0 - }, - remote: { - totalCount: remoteDriveFilesCount, - totalSize: remoteDriveFilesSize, - diffCount: 0, - diffSize: 0 - } - } - }); - - console.log('done'); -} - -main(); diff --git a/docker-compose.yml b/docker-compose.yml index 7ff8f6a268bc75e13ffc2a717403b6b24637accc..184738aa8caf0dabff46e3c8c11197daaef61687 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: build: . restart: always links: - - mongo + - db # - redis # - es ports: @@ -19,21 +19,18 @@ services: # image: redis:4.0-alpine # networks: # - internal_network -### Uncomment to enable Redis persistance -## volumes: -## - ./redis:/data +# volumes: +# - ./redis:/data - mongo: + db: restart: always - image: mongo:4.1 + image: postgres:11.2-alpine networks: - internal_network - environment: - MONGO_INITDB_DATABASE: "misskey" + env_file: + - .config/docker.env volumes: - - ./.config/mongo_initdb.js:/docker-entrypoint-initdb.d/mongo_initdb.js:ro -### Uncomment to enable MongoDB persistance -# - ./mongo:/data + - ./db:/var/lib/postgresql/data # es: # restart: always @@ -42,9 +39,8 @@ services: # - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # networks: # - internal_network -#### Uncomment to enable ES persistence -## volumes: -## - ./elasticsearch:/usr/share/elasticsearch/data +# volumes: +# - ./elasticsearch:/usr/share/elasticsearch/data networks: internal_network: diff --git a/docs/backup.fr.md b/docs/backup.fr.md deleted file mode 100644 index 19e99068cec24a2616d34326cdac5253a45b21ee..0000000000000000000000000000000000000000 --- a/docs/backup.fr.md +++ /dev/null @@ -1,22 +0,0 @@ -Comment faire une sauvegarde de votre Misskey ? -========================== - -Assurez-vous d'avoir installé **mongodb-tools**. - ---- - -Dans votre terminal : -``` shell -$ mongodump --archive=db-backup -u <VotreNomdUtilisateur> -p <VotreMotDePasse> -``` - -Pour plus de détails, merci de consulter [la documentation de mongodump](https://docs.mongodb.com/manual/reference/program/mongodump/). - -Restauration -------- - -``` shell -$ mongorestore --archive=db-backup -``` - -Pour plus de détails, merci de consulter [la documentation de mongorestore](https://docs.mongodb.com/manual/reference/program/mongorestore/). diff --git a/docs/backup.md b/docs/backup.md deleted file mode 100644 index a69af0255b21d2c977c70552a73c78b45fe645a9..0000000000000000000000000000000000000000 --- a/docs/backup.md +++ /dev/null @@ -1,22 +0,0 @@ -How to backup your Misskey -========================== - -Make sure **mongodb-tools** installed. - ---- - -In your shell: -``` shell -$ mongodump --archive=db-backup -u <YourUserName> -p <YourPassword> -``` - -For details, please see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/). - -Restore -------- - -``` shell -$ mongorestore --archive=db-backup -``` - -For details, please see [mongorestore docs](https://docs.mongodb.com/manual/reference/program/mongorestore/). diff --git a/docs/docker.en.md b/docs/docker.en.md index f0fcdb66d570a65d0cffa59604626e3d66bcdbc9..ee69b6d7ae59d0a695c6ceaf4389b3b01a5ff806 100644 --- a/docs/docker.en.md +++ b/docs/docker.en.md @@ -15,9 +15,37 @@ This guide describes how to install and setup Misskey with Docker. *2.* Configure Misskey ---------------------------------------------------------------- -1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`. -2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` Copy the `.config/mongo_initdb_example.js` and rename it to `mongo_initdb.js`. -3. Edit `default.yml` and `mongo_initdb.js`. + +Create configuration files with following: + +```bash +cd .config +cp example.yml default.yml +cp docker_example.env docker.env +``` + +### `default.yml` + +Edit this file the same as non-Docker environment. +However hostname of Postgresql, Redis and Elasticsearch are not `localhost`, they are set in `docker-compose.yml`. +The following is default hostname: + +| Service | Hostname | +|---------------|----------| +| Postgresql | `db` | +| Redis | `redis` | +| Elasticsearch | `es` | + +### `docker.env` + +Configure Postgresql in this file. +The minimum required settings are: + +| name | Description | +|---------------------|---------------| +| `POSTGRES_PASSWORD` | Password | +| `POSTGRES_USER` | Username | +| `POSTGRES_DB` | Database name | *3.* Configure Docker ---------------------------------------------------------------- diff --git a/docs/docker.ja.md b/docs/docker.ja.md index 0baf2857284725c3631d959636da825f12d114fc..060d4e7bda3615e2df40ea2a7cc48f96d02805f3 100644 --- a/docs/docker.ja.md +++ b/docs/docker.ja.md @@ -13,11 +13,39 @@ Dockerを使ã£ãŸMisskey構築方法 2. `cd misskey` misskeyディレクトリã«ç§»å‹• 3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新ã®ãƒªãƒªãƒ¼ã‚¹](https://github.com/syuilo/misskey/releases/latest)ã‚’ç¢ºèª -*2.* è¨å®šãƒ•ã‚¡ã‚¤ãƒ«ã‚’作æˆã™ã‚‹ +*2.* è¨å®šãƒ•ã‚¡ã‚¤ãƒ«ã®ä½œæˆã¨ç·¨é›† ---------------------------------------------------------------- -1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーã—åå‰ã‚’`default.yml`ã«ã™ã‚‹ -2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` `.config/mongo_initdb_example.js`をコピーã—åå‰ã‚’`mongo_initdb.js`ã«ã™ã‚‹ -3. `default.yml`ã¨`mongo_initdb.js`を編集ã™ã‚‹ + +下記コマンドã§è¨å®šãƒ•ã‚¡ã‚¤ãƒ«ã‚’作æˆã—ã¦ãã ã•ã„。 + +```bash +cd .config +cp example.yml default.yml +cp docker_example.env docker.env +``` + +### `default.yml`ã®ç·¨é›† + +éžDocker環境ã¨åŒã˜æ§˜ã«ç·¨é›†ã—ã¦ãã ã•ã„。 +ãŸã ã—ã€Postgresqlã€Redisã¨Elasticsearchã®ãƒ›ã‚¹ãƒˆã¯`localhost`ã§ã¯ãªãã€`docker-compose.yml`ã§è¨å®šã•ã‚ŒãŸã‚µãƒ¼ãƒ“スåã«ãªã£ã¦ã„ã¾ã™ã€‚ +標準è¨å®šã§ã¯æ¬¡ã®é€šã‚Šã§ã™ã€‚ + +| サービス | ホストå | +|---------------|---------| +| Postgresql |`db` | +| Redis |`redis` | +| Elasticsearch |`es` | + +### `docker.env`ã®ç·¨é›† + +ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã¯Postgresqlã®è¨å®šã‚’記述ã—ã¾ã™ã€‚ +最低é™è¨˜è¿°ã™ã‚‹å¿…è¦ãŒã‚ã‚‹è¨å®šã¯æ¬¡ã®é€šã‚Šã§ã™ã€‚ + +| è¨å®š | 内容 | +|---------------------|--------------| +| `POSTGRES_PASSWORD` | パスワード | +| `POSTGRES_USER` | ユーザーå | +| `POSTGRES_DB` | データベースå | *3.* Dockerã®è¨å®š ---------------------------------------------------------------- diff --git a/docs/setup.en.md b/docs/setup.en.md index 1125081445dfd7f0ecb30f1c2cceca83f3fa788b..28de1f32f319dcad1e67f5d0cf5f77cf2de70a09 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey Please install and setup these softwares: #### Dependencies :package: -* **[Node.js](https://nodejs.org/en/)** >= 10.0.0 -* **[MongoDB](https://www.mongodb.com/)** >= 3.6 +* **[Node.js](https://nodejs.org/en/)** >= 11.7.0 +* **[PostgreSQL](https://www.postgresql.org/)** >= 10 ##### Optional * [Redis](https://redis.io/) @@ -31,13 +31,9 @@ Please install and setup these softwares: * [Elasticsearch](https://www.elastic.co/) - required to enable the search feature * [FFmpeg](https://www.ffmpeg.org/) -*3.* Setup MongoDB +*3.* Setup PostgreSQL ---------------------------------------------------------------- -As root: -1. `mongo` Go to the mongo shell -2. `use misskey` Use the misskey database -3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Create the misskey user. -4. `exit` You're done! +:) *4.* Install Misskey ---------------------------------------------------------------- @@ -68,7 +64,13 @@ If you're still encountering errors about some modules, use node-gyp: 3. `node-gyp build` 4. `NODE_ENV=production npm run build` -*7.* That is it. +*7.* Init DB +---------------------------------------------------------------- +``` shell +npm run init +``` + +*8.* That is it. ---------------------------------------------------------------- Well done! Now, you have an environment that run to Misskey. diff --git a/docs/setup.fr.md b/docs/setup.fr.md index 959ec3392ffb4ccc51476d156ef855557bbfefef..217a4c6a5b941bec77a5d2c8e89e3d89608155f5 100644 --- a/docs/setup.fr.md +++ b/docs/setup.fr.md @@ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey Installez les paquets suivants : #### Dépendences :package: -* **[Node.js](https://nodejs.org/en/)** >= 10.0.0 -* **[MongoDB](https://www.mongodb.com/)** >= 3.6 +* **[Node.js](https://nodejs.org/en/)** >= 11.7.0 +* **[PostgreSQL](https://www.postgresql.org/)** >= 10 ##### Optionnels * [Redis](https://redis.io/) @@ -31,13 +31,9 @@ Installez les paquets suivants : * [Elasticsearch](https://www.elastic.co/) - requis pour pouvoir activer la fonctionnalité de recherche * [FFmpeg](https://www.ffmpeg.org/) -*3.* Paramètrage de MongoDB +*3.* Paramètrage de PostgreSQL ---------------------------------------------------------------- -En root : -1. `mongo` Ouvrez le shell mongo -2. `use misskey` Utilisez la base de données misskey -3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Créez l'utilisateur misskey. -4. `exit` Vous avez terminé ! +:) *4.* Installation de Misskey ---------------------------------------------------------------- diff --git a/docs/setup.ja.md b/docs/setup.ja.md index 8a21e104d6349a4446a9269daa7425153ff913c3..1543541eee8188e70a46702406271fdc0ce50ecf 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey ã“れらã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚’インストール・è¨å®šã—ã¦ãã ã•ã„: #### ä¾å˜é–¢ä¿‚ :package: -* **[Node.js](https://nodejs.org/en/)** (10.0.0以上) -* **[MongoDB](https://www.mongodb.com/)** (3.6以上) +* **[Node.js](https://nodejs.org/en/)** (11.7.0以上) +* **[PostgreSQL](https://www.postgresql.org/)** (10以上) ##### オプション * [Redis](https://redis.io/) @@ -38,13 +38,9 @@ adduser --disabled-password --disabled-login misskey * 検索機能を有効ã«ã™ã‚‹ãŸã‚ã«ã¯ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå¿…è¦ã§ã™ã€‚ * [FFmpeg](https://www.ffmpeg.org/) -*3.* MongoDBã®è¨å®š +*3.* PostgreSQLã®è¨å®š ---------------------------------------------------------------- -ルートã§: -1. `mongo` mongoシェルを起動 -2. `use misskey` misskeyデータベースを使用 -3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` misskeyãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ä½œæˆ -4. `exit` mongoシェルを終了 +:) *4.* Misskeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ« ---------------------------------------------------------------- @@ -74,7 +70,13 @@ Debianã‚’ãŠä½¿ã„ã§ã‚ã‚Œã°ã€`build-essential`パッケージをインスト 3. `node-gyp build` 4. `NODE_ENV=production npm run build` -*7.* 以上ã§ã™ï¼ +*7.* データベースをåˆæœŸåŒ– +---------------------------------------------------------------- +``` shell +npm run init +``` + +*8.* 以上ã§ã™ï¼ ---------------------------------------------------------------- ãŠç–²ã‚Œæ§˜ã§ã—ãŸã€‚ã“ã‚Œã§Misskeyã‚’å‹•ã‹ã™æº–å‚™ã¯æ•´ã„ã¾ã—ãŸã€‚ diff --git a/gulpfile.ts b/gulpfile.ts index b2956c2403b7886c20fdacd4d5da2eae9599fb54..bf0b87ef20ed4c9ce337d70c3b0577cc9e6f15f3 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -49,7 +49,6 @@ gulp.task('build:copy:views', () => gulp.task('build:copy', gulp.parallel('build:copy:views', () => gulp.src([ - './build/Release/crypto_key.node', './src/const.json', './src/server/web/views/**/*', './src/**/assets/**/*', diff --git a/index.js b/index.js index 5b7d1347aa67fdec4cfbbc850b25e0d82ca169ab..bc7e8b2f3a31888a1dcb2bb4210e3c4eea35a8c7 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -require('./built'); +require('./built').default(); diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d4457b6594a7307e19f7686181c412bb5fab6d95..43d8cb309a2b7a38ddb28e4c90baea0b017226e2 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1238,11 +1238,6 @@ admin/views/instance.vue: save: "ä¿å˜" saved: "ä¿å˜ã—ã¾ã—ãŸ" user-recommendation-config: "ãŠã™ã™ã‚ユーザー" - enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効ã«ã™ã‚‹" - external-user-recommendation-engine: "エンジン" - external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}" - external-user-recommendation-timeout: "タイムアウト" - external-user-recommendation-timeout-desc: "ミリ秒å˜ä½ (例: 300000)" email-config: "メールサーãƒãƒ¼ã®è¨å®š" email-config-info: "メールアドレス確èªã‚„パスワードリセットã®éš›ã«ä½¿ã‚ã‚Œã¾ã™ã€‚" enable-email: "メールé…信を有効ã«ã™ã‚‹" diff --git a/package.json b/package.json index 39e6aa7deb26c38fe6e3a1cffb822df0266253cc..a7c12862ffffd626c5aa31048ddaf53bfd6d90e7 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "misskey", "author": "syuilo <i@syuilo.com>", - "version": "10.99.0", - "codename": "nighthike", + "version": "11.0.0", + "codename": "daybreak", "repository": { "type": "git", "url": "https://github.com/syuilo/misskey.git" @@ -11,6 +11,7 @@ "private": true, "scripts": { "start": "node ./index.js", + "init": "node ./built/init.js", "debug": "DEBUG=misskey:* node ./index.js", "build": "webpack && gulp build", "webpack": "webpack", @@ -62,10 +63,9 @@ "@types/koa-send": "4.1.1", "@types/koa-views": "2.0.3", "@types/koa__cors": "2.2.3", + "@types/lolex": "3.1.1", "@types/minio": "7.0.1", - "@types/mkdirp": "0.5.2", - "@types/mocha": "5.2.5", - "@types/mongodb": "3.1.20", + "@types/mocha": "5.2.6", "@types/node": "11.10.4", "@types/nodemailer": "4.6.6", "@types/nprogress": "0.0.29", @@ -107,6 +107,7 @@ "chai": "4.2.0", "chai-http": "4.2.1", "chalk": "2.4.2", + "cli-highlight": "2.1.0", "commander": "2.20.0", "content-disposition": "0.5.3", "crc-32": "1.2.0", @@ -114,12 +115,10 @@ "cssnano": "4.1.10", "dateformat": "3.0.3", "deep-equal": "1.0.1", - "deepcopy": "0.6.3", "diskusage": "1.0.0", "double-ended-queue": "2.1.0-0", "elasticsearch": "15.4.1", "emojilib": "2.4.0", - "escape-regexp": "0.0.1", "eslint": "5.15.1", "eslint-plugin-vue": "5.2.2", "eventemitter3": "3.1.0", @@ -163,23 +162,22 @@ "koa-views": "6.2.0", "langmap": "0.0.16", "loader-utils": "1.2.3", + "lolex": "3.1.0", "lookup-dns-cache": "2.1.0", "minio": "7.0.5", - "mkdirp": "0.5.1", - "mocha": "5.2.0", + "mocha": "6.0.2", "moji": "0.5.1", "moment": "2.24.0", - "mongodb": "3.2.2", - "monk": "6.0.6", "ms": "2.1.1", - "nan": "2.12.1", "nested-property": "0.0.7", + "node-fetch": "2.3.0", "nodemailer": "5.1.1", "nprogress": "0.2.0", "object-assign-deep": "0.4.0", "os-utils": "0.0.14", "parse5": "5.1.0", "parsimmon": "1.12.0", + "pg": "7.9.0", "portscanner": "2.2.0", "postcss-loader": "3.0.0", "prismjs": "1.16.0", @@ -195,10 +193,12 @@ "recaptcha-promise": "0.1.3", "reconnecting-websocket": "4.1.10", "redis": "2.8.0", + "reflect-metadata": "0.1.13", "rename": "1.0.4", "request": "2.88.0", "request-promise-native": "1.0.7", "request-stats": "3.0.0", + "require-all": "3.0.0", "rimraf": "2.6.3", "rndstr": "1.0.0", "s-age": "1.1.2", @@ -219,12 +219,14 @@ "tinycolor2": "1.4.1", "tmp": "0.0.33", "ts-loader": "5.3.3", - "ts-node": "8.0.3", + "ts-node": "7.0.1", "tslint": "5.13.1", "tslint-sonarts": "1.9.0", + "typeorm": "0.2.16-rc.1", "typescript": "3.3.3333", "typescript-eslint-parser": "22.0.0", "uglify-es": "3.3.9", + "ulid": "2.3.0", "url-loader": "1.1.2", "uuid": "3.3.2", "v-animate-css": "0.0.3", diff --git a/src/@types/deepcopy.d.ts b/src/@types/deepcopy.d.ts deleted file mode 100644 index f276b7e678b934f7d2e667c00921777e05209b41..0000000000000000000000000000000000000000 --- a/src/@types/deepcopy.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare module 'deepcopy' { - type DeepcopyCustomizerValueType = 'Object'; - - type DeepcopyCustomizer<T> = ( - value: T, - valueType: DeepcopyCustomizerValueType) => T; - - interface IDeepcopyOptions<T> { - customizer: DeepcopyCustomizer<T>; - } - - function deepcopy<T>( - value: T, - options?: IDeepcopyOptions<T> | DeepcopyCustomizer<T>): T; - - namespace deepcopy {} // Hack - - export = deepcopy; -} diff --git a/src/@types/escape-regexp.d.ts b/src/@types/escape-regexp.d.ts deleted file mode 100644 index d68e6048a14595021278f5f64d49728bddecf3f2..0000000000000000000000000000000000000000 --- a/src/@types/escape-regexp.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module 'escape-regexp' { - function escapeRegExp(str: string): string; - - namespace escapeRegExp {} // Hack - - export = escapeRegExp; -} diff --git a/src/argv.ts b/src/argv.ts index b5540441cca5a5f14bde2a3cf5de1ea4258cee7d..562852d17b421d8d8f53ca21a1aed62b5eeea28e 100644 --- a/src/argv.ts +++ b/src/argv.ts @@ -15,5 +15,8 @@ program .parse(process.argv); if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true; +if (process.env.NODE_ENV === 'test') program.disableClustering = true; +if (process.env.NODE_ENV === 'test') program.quiet = true; +if (process.env.NODE_ENV === 'test') program.noDaemons = true; export { program }; diff --git a/src/boot/index.ts b/src/boot/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c86d8ed8cc7b3ef6a090bf8fc2e4ab35ffe1375 --- /dev/null +++ b/src/boot/index.ts @@ -0,0 +1,77 @@ +import * as cluster from 'cluster'; +import chalk from 'chalk'; +import Xev from 'xev'; + +import Logger from '../services/logger'; +import { program } from '../argv'; + +// for typeorm +import 'reflect-metadata'; +import { masterMain } from './master'; +import { workerMain } from './worker'; + +const logger = new Logger('core', 'cyan'); +const clusterLogger = logger.createSubLogger('cluster', 'orange', false); +const ev = new Xev(); + +/** + * Init process + */ +export default async function() { + process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`; + + if (cluster.isMaster || program.disableClustering) { + await masterMain(); + + if (cluster.isMaster) { + ev.mount(); + } + } + + if (cluster.isWorker || program.disableClustering) { + await workerMain(); + } + + // ユニットテスト時ã«MisskeyãŒåプãƒã‚»ã‚¹ã§èµ·å‹•ã•ã‚ŒãŸæ™‚ã®ãŸã‚ + // ãれ以外ã®ã¨ã㯠process.send ã¯ä½¿ãˆãªã„ã®ã§å¼¾ã + if (process.send) { + process.send('ok'); + } +} + +//#region Events + +// Listen new workers +cluster.on('fork', worker => { + clusterLogger.debug(`Process forked: [${worker.id}]`); +}); + +// Listen online workers +cluster.on('online', worker => { + clusterLogger.debug(`Process is now online: [${worker.id}]`); +}); + +// Listen for dying workers +cluster.on('exit', worker => { + // Replace the dead worker, + // we're not sentimental + clusterLogger.error(chalk.red(`[${worker.id}] died :(`)); + cluster.fork(); +}); + +// Display detail of unhandled promise rejection +if (!program.quiet) { + process.on('unhandledRejection', console.dir); +} + +// Display detail of uncaught exception +process.on('uncaughtException', err => { + logger.error(err); +}); + +// Dying away... +process.on('exit', code => { + logger.info(`The process is going to exit with code ${code}`); +}); + +//#endregion diff --git a/src/boot/master.ts b/src/boot/master.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d4080fdb026db242bafb958b73e73cda6242e8e --- /dev/null +++ b/src/boot/master.ts @@ -0,0 +1,176 @@ +import * as os from 'os'; +import * as cluster from 'cluster'; +import chalk from 'chalk'; +import * as portscanner from 'portscanner'; +import * as isRoot from 'is-root'; + +import Logger from '../services/logger'; +import loadConfig from '../config/load'; +import { Config } from '../config/types'; +import { lessThan } from '../prelude/array'; +import * as pkg from '../../package.json'; +import { program } from '../argv'; +import { showMachineInfo } from '../misc/show-machine-info'; +import { initDb } from '../db/postgre'; + +const logger = new Logger('core', 'cyan'); +const bootLogger = logger.createSubLogger('boot', 'magenta', false); + +function greet() { + if (!program.quiet) { + //#region Misskey logo + const v = `v${pkg.version}`; + console.log(' _____ _ _ '); + console.log(' | |_|___ ___| |_ ___ _ _ '); + console.log(' | | | | |_ -|_ -| \'_| -_| | |'); + console.log(' |_|_|_|_|___|___|_,_|___|_ |'); + console.log(' ' + chalk.gray(v) + (' |___|\n'.substr(v.length))); + //#endregion + + console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.'); + console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo')); + + console.log(''); + console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`); + } + + bootLogger.info('Welcome to Misskey!'); + bootLogger.info(`Misskey v${pkg.version}`, null, true); +} + +/** + * Init master process + */ +export async function masterMain() { + greet(); + + let config: Config; + + try { + // initialize app + config = await init(); + + if (config.port == null) { + bootLogger.error('The port is not configured. Please configure port.', null, true); + process.exit(1); + } + + if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) { + bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true); + process.exit(1); + } + + if (!await isPortAvailable(config.port)) { + bootLogger.error(`Port ${config.port} is already in use`, null, true); + process.exit(1); + } + } catch (e) { + bootLogger.error('Fatal error occurred during initialization', null, true); + process.exit(1); + } + + bootLogger.succ('Misskey initialized'); + + if (!program.disableClustering) { + await spawnWorkers(config.clusterLimit); + } + + if (!program.noDaemons) { + require('../daemons/server-stats').default(); + require('../daemons/notes-stats').default(); + require('../daemons/queue-stats').default(); + } + + bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); +} + +const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10)); +const requiredNodejsVersion = [11, 7, 0]; +const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion); + +function isWellKnownPort(port: number): boolean { + return port < 1024; +} + +async function isPortAvailable(port: number): Promise<boolean> { + return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed'; +} + +function showEnvironment(): void { + const env = process.env.NODE_ENV; + const logger = bootLogger.createSubLogger('env'); + logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`); + + if (env !== 'production') { + logger.warn('The environment is not in production mode.'); + logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true); + } + + logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`); +} + +/** + * Init app + */ +async function init(): Promise<Config> { + showEnvironment(); + + const nodejsLogger = bootLogger.createSubLogger('nodejs'); + + nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`); + + if (!satisfyNodejsVersion) { + nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true); + process.exit(1); + } + + await showMachineInfo(bootLogger); + + const configLogger = bootLogger.createSubLogger('config'); + let config; + + try { + config = loadConfig(); + } catch (exception) { + if (typeof exception === 'string') { + configLogger.error(exception); + process.exit(1); + } + if (exception.code === 'ENOENT') { + configLogger.error('Configuration file not found', null, true); + process.exit(1); + } + throw exception; + } + + configLogger.succ('Loaded'); + + // Try to connect to DB + try { + bootLogger.info('Connecting database...'); + await initDb(); + } catch (e) { + bootLogger.error('Cannot connect to database', null, true); + bootLogger.error(e); + process.exit(1); + } + + return config; +} + +async function spawnWorkers(limit: number = Infinity) { + const workers = Math.min(limit, os.cpus().length); + bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); + await Promise.all([...Array(workers)].map(spawnWorker)); + bootLogger.succ('All workers started'); +} + +function spawnWorker(): Promise<void> { + return new Promise(res => { + const worker = cluster.fork(); + worker.on('message', message => { + if (message !== 'ready') return; + res(); + }); + }); +} diff --git a/src/boot/worker.ts b/src/boot/worker.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca3716972a169b293e8c931f273cafad6fa98447 --- /dev/null +++ b/src/boot/worker.ts @@ -0,0 +1,20 @@ +import * as cluster from 'cluster'; +import { initDb } from '../db/postgre'; + +/** + * Init worker process + */ +export async function workerMain() { + await initDb(); + + // start server + await require('../server').default(); + + // start job queue + require('../queue').default(); + + if (cluster.isWorker) { + // Send a 'ready' message to parent process + process.send('ready'); + } +} diff --git a/src/client/app/admin/views/drive.vue b/src/client/app/admin/views/drive.vue index 7812aadaaf152ca199a270ceaff866e486155592..491050b1f7aaa0dd9a3a69439ac980a9831bc069 100644 --- a/src/client/app/admin/views/drive.vue +++ b/src/client/app/admin/views/drive.vue @@ -48,7 +48,7 @@ <div> <div> <span style="margin-right:16px;">{{ file.type }}</span> - <span>{{ file.datasize | bytes }}</span> + <span>{{ file.size | bytes }}</span> </div> <div><mk-time :time="file.createdAt" mode="detail"/></div> </div> diff --git a/src/client/app/admin/views/hashtags.vue b/src/client/app/admin/views/hashtags.vue index b3190c29c415b70ef01b9ce06a5db0f9219c3aca..e1cc4b494d685e4f7c38d5a0871a08f93ffaf32c 100644 --- a/src/client/app/admin/views/hashtags.vue +++ b/src/client/app/admin/views/hashtags.vue @@ -3,7 +3,7 @@ <ui-card> <template #title>{{ $t('hided-tags') }}</template> <section> - <textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hidedTags"></textarea> + <textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hiddenTags"></textarea> <ui-button @click="save">{{ $t('save') }}</ui-button> </section> </ui-card> @@ -18,18 +18,18 @@ export default Vue.extend({ i18n: i18n('admin/views/hashtags.vue'), data() { return { - hidedTags: '', + hiddenTags: '', }; }, created() { this.$root.getMeta().then(meta => { - this.hidedTags = meta.hidedTags.join('\n'); + this.hiddenTags = meta.hiddenTags.join('\n'); }); }, methods: { save() { this.$root.api('admin/update-meta', { - hidedTags: this.hidedTags.split('\n') + hiddenTags: this.hiddenTags.split('\n') }).then(() => { //this.$root.os.apis.dialog({ text: `Saved` }); }).catch(e => { diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index 2d2a07784b9912908a33e3a8219b58594be83067..bc2a5fba85d253ac935482799993bebda0532226 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -77,12 +77,6 @@ <header>summaly Proxy</header> <ui-input v-model="summalyProxy">URL</ui-input> </section> - <section> - <header><fa :icon="faUserPlus"/> {{ $t('user-recommendation-config') }}</header> - <ui-switch v-model="enableExternalUserRecommendation">{{ $t('enable-external-user-recommendation') }}</ui-switch> - <ui-input v-model="externalUserRecommendationEngine" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-engine') }}<template #desc>{{ $t('external-user-recommendation-engine-desc') }}</template></ui-input> - <ui-input v-model="externalUserRecommendationTimeout" type="number" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-timeout') }}<template #suffix>ms</template><template #desc>{{ $t('external-user-recommendation-timeout-desc') }}</template></ui-input> - </section> <section> <ui-button @click="updateMeta">{{ $t('save') }}</ui-button> </section> @@ -184,9 +178,6 @@ export default Vue.extend({ discordClientSecret: null, proxyAccount: null, inviteCode: null, - enableExternalUserRecommendation: false, - externalUserRecommendationEngine: null, - externalUserRecommendationTimeout: null, summalyProxy: null, enableEmail: false, email: null, @@ -205,8 +196,8 @@ export default Vue.extend({ created() { this.$root.getMeta().then(meta => { - this.maintainerName = meta.maintainer.name; - this.maintainerEmail = meta.maintainer.email; + this.maintainerName = meta.maintainerName; + this.maintainerEmail = meta.maintainerEmail; this.disableRegistration = meta.disableRegistration; this.disableLocalTimeline = meta.disableLocalTimeline; this.disableGlobalTimeline = meta.disableGlobalTimeline; @@ -236,9 +227,6 @@ export default Vue.extend({ this.enableDiscordIntegration = meta.enableDiscordIntegration; this.discordClientId = meta.discordClientId; this.discordClientSecret = meta.discordClientSecret; - this.enableExternalUserRecommendation = meta.enableExternalUserRecommendation; - this.externalUserRecommendationEngine = meta.externalUserRecommendationEngine; - this.externalUserRecommendationTimeout = meta.externalUserRecommendationTimeout; this.summalyProxy = meta.summalyProxy; this.enableEmail = meta.enableEmail; this.email = meta.email; @@ -299,9 +287,6 @@ export default Vue.extend({ enableDiscordIntegration: this.enableDiscordIntegration, discordClientId: this.discordClientId, discordClientSecret: this.discordClientSecret, - enableExternalUserRecommendation: this.enableExternalUserRecommendation, - externalUserRecommendationEngine: this.externalUserRecommendationEngine, - externalUserRecommendationTimeout: parseInt(this.externalUserRecommendationTimeout, 10), summalyProxy: this.summalyProxy, enableEmail: this.enableEmail, email: this.email, diff --git a/src/client/app/admin/views/logs.vue b/src/client/app/admin/views/logs.vue index 4a2d957ed7e7c5e8ed45b27294ff0dd9e6ebdbbd..5c2cfdb3964730814f0eaa4859248c5783f62089 100644 --- a/src/client/app/admin/views/logs.vue +++ b/src/client/app/admin/views/logs.vue @@ -19,7 +19,7 @@ </ui-horizon-group> <div class="nqjzuvev"> - <code v-for="log in logs" :key="log._id" :class="log.level"> + <code v-for="log in logs" :key="log.id" :class="log.level"> <details> <summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary> <vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty> diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue index ff485cec868181bfb2a5e844dbda891b5a8538b9..0f46b564a93cb0ac7ef4a3a059603383388f2e58 100644 --- a/src/client/app/admin/views/users.vue +++ b/src/client/app/admin/views/users.vue @@ -165,7 +165,7 @@ export default Vue.extend({ /** 処ç†å¯¾è±¡ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æƒ…å ±ã‚’æ›´æ–°ã™ã‚‹ */ async refreshUser() { - this.$root.api('admin/show-user', { userId: this.user._id }).then(info => { + this.$root.api('admin/show-user', { userId: this.user.id }).then(info => { this.user = info; }); }, @@ -173,7 +173,7 @@ export default Vue.extend({ async resetPassword() { if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return; - this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => { + this.$root.api('admin/reset-password', { userId: this.user.id }).then(res => { this.$root.dialog({ type: 'success', text: this.$t('password-updated', { password: res.password }) @@ -187,7 +187,7 @@ export default Vue.extend({ this.verifying = true; const process = async () => { - await this.$root.api('admin/verify-user', { userId: this.user._id }); + await this.$root.api('admin/verify-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', text: this.$t('verified') @@ -212,7 +212,7 @@ export default Vue.extend({ this.unverifying = true; const process = async () => { - await this.$root.api('admin/unverify-user', { userId: this.user._id }); + await this.$root.api('admin/unverify-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', text: this.$t('unverified') @@ -233,7 +233,7 @@ export default Vue.extend({ async silenceUser() { const process = async () => { - await this.$root.api('admin/silence-user', { userId: this.user._id }); + await this.$root.api('admin/silence-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', splash: true @@ -252,7 +252,7 @@ export default Vue.extend({ async unsilenceUser() { const process = async () => { - await this.$root.api('admin/unsilence-user', { userId: this.user._id }); + await this.$root.api('admin/unsilence-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', splash: true @@ -275,7 +275,7 @@ export default Vue.extend({ this.suspending = true; const process = async () => { - await this.$root.api('admin/suspend-user', { userId: this.user._id }); + await this.$root.api('admin/suspend-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', text: this.$t('suspended') @@ -300,7 +300,7 @@ export default Vue.extend({ this.unsuspending = true; const process = async () => { - await this.$root.api('admin/unsuspend-user', { userId: this.user._id }); + await this.$root.api('admin/unsuspend-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', text: this.$t('unsuspended') @@ -320,7 +320,7 @@ export default Vue.extend({ }, async updateRemoteUser() { - this.$root.api('admin/update-remote-user', { userId: this.user._id }).then(res => { + this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => { this.$root.dialog({ type: 'success', text: this.$t('remote-user-updated') diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue index 105af375b6d0c34b4c2c1275c90d69e5484419b1..d5d6b25f009bc365572320f29e3ded88a4f29b77 100644 --- a/src/client/app/auth/views/form.vue +++ b/src/client/app/auth/views/form.vue @@ -14,15 +14,15 @@ <h2>{{ $t('permission-ask') }}</h2> <ul> <template v-for="p in app.permission"> - <li v-if="p == 'account-read'">{{ $t('account-read') }}</li> - <li v-if="p == 'account-write'">{{ $t('account-write') }}</li> - <li v-if="p == 'note-write'">{{ $t('note-write') }}</li> + <li v-if="p == 'read:account'">{{ $t('read:account') }}</li> + <li v-if="p == 'write:account'">{{ $t('write:account') }}</li> + <li v-if="p == 'write:notes'">{{ $t('write:notes') }}</li> <li v-if="p == 'like-write'">{{ $t('like-write') }}</li> - <li v-if="p == 'following-write'">{{ $t('following-write') }}</li> - <li v-if="p == 'drive-read'">{{ $t('drive-read') }}</li> - <li v-if="p == 'drive-write'">{{ $t('drive-write') }}</li> - <li v-if="p == 'notification-read'">{{ $t('notification-read') }}</li> - <li v-if="p == 'notification-write'">{{ $t('notification-write') }}</li> + <li v-if="p == 'write:following'">{{ $t('write:following') }}</li> + <li v-if="p == 'read:drive'">{{ $t('read:drive') }}</li> + <li v-if="p == 'write:drive'">{{ $t('write:drive') }}</li> + <li v-if="p == 'read:notifications'">{{ $t('read:notifications') }}</li> + <li v-if="p == 'write:notifications'">{{ $t('write:notifications') }}</li> </template> </ul> </section> diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts index 1efdbb1880b45f9fa5d50ba42b7313774de578a5..632ddf2ed6586c1188d3d3a9841410fadde8431d 100644 --- a/src/client/app/common/define-widget.ts +++ b/src/client/app/common/define-widget.ts @@ -45,15 +45,9 @@ export default function <T extends object>(data: { this.$watch('props', () => { this.mergeProps(); }); - - this.bakeProps(); }, methods: { - bakeProps() { - this.bakedOldProps = JSON.stringify(this.props); - }, - mergeProps() { if (data.props) { const defaultProps = data.props(); @@ -65,17 +59,10 @@ export default function <T extends object>(data: { }, save() { - if (this.bakedOldProps == JSON.stringify(this.props)) return; - - this.bakeProps(); - if (this.platform == 'deck') { this.$store.commit('device/updateDeckColumn', this.column); } else { - this.$root.api('i/update_widget', { - id: this.id, - data: this.props - }); + this.$store.commit('device/updateWidget', this.widget); } } } diff --git a/src/client/app/common/scripts/note-mixin.ts b/src/client/app/common/scripts/note-mixin.ts index 5707d1bb4145942252a2bbef4f5e33d3a07df593..67bbe8c0ae622f641249352007c48d6997ffdf2f 100644 --- a/src/client/app/common/scripts/note-mixin.ts +++ b/src/client/app/common/scripts/note-mixin.ts @@ -70,8 +70,8 @@ export default (opts: Opts = {}) => ({ }, reactionsCount(): number { - return this.appearNote.reactionCounts - ? sum(Object.values(this.appearNote.reactionCounts)) + return this.appearNote.reactions + ? sum(Object.values(this.appearNote.reactions)) : 0; }, diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts index c2b4dd6df9ac3217d7a5385b3f2f2e0ec34459fd..02d810ded982f7f6e97ff67be71e8038c09964a1 100644 --- a/src/client/app/common/scripts/note-subscriber.ts +++ b/src/client/app/common/scripts/note-subscriber.ts @@ -87,16 +87,16 @@ export default prop => ({ case 'reacted': { const reaction = body.reaction; - if (this.$_ns_target.reactionCounts == null) { - Vue.set(this.$_ns_target, 'reactionCounts', {}); + if (this.$_ns_target.reactions == null) { + Vue.set(this.$_ns_target, 'reactions', {}); } - if (this.$_ns_target.reactionCounts[reaction] == null) { - Vue.set(this.$_ns_target.reactionCounts, reaction, 0); + if (this.$_ns_target.reactions[reaction] == null) { + Vue.set(this.$_ns_target.reactions, reaction, 0); } // Increment the count - this.$_ns_target.reactionCounts[reaction]++; + this.$_ns_target.reactions[reaction]++; if (body.userId == this.$store.state.i.id) { Vue.set(this.$_ns_target, 'myReaction', reaction); @@ -107,16 +107,16 @@ export default prop => ({ case 'unreacted': { const reaction = body.reaction; - if (this.$_ns_target.reactionCounts == null) { + if (this.$_ns_target.reactions == null) { return; } - if (this.$_ns_target.reactionCounts[reaction] == null) { + if (this.$_ns_target.reactions[reaction] == null) { return; } // Decrement the count - if (this.$_ns_target.reactionCounts[reaction] > 0) this.$_ns_target.reactionCounts[reaction]--; + if (this.$_ns_target.reactions[reaction] > 0) this.$_ns_target.reactions[reaction]--; if (body.userId == this.$store.state.i.id) { Vue.set(this.$_ns_target, 'myReaction', null); @@ -125,9 +125,11 @@ export default prop => ({ } case 'pollVoted': { - if (body.userId == this.$store.state.i.id) return; const choice = body.choice; - this.$_ns_target.poll.choices.find(c => c.id === choice).votes++; + this.$_ns_target.poll.choices[choice].votes++; + if (body.userId == this.$store.state.i.id) { + Vue.set(this.$_ns_target.poll.choices[choice], 'isVoted', true); + } break; } diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue index dce594e7022557be9d45e6d68a7fd211217fdf6f..c074fb600fead3da11c90210b2caadec83b767b0 100644 --- a/src/client/app/common/views/components/avatar.vue +++ b/src/client/app/common/views/components/avatar.vue @@ -55,11 +55,12 @@ export default Vue.extend({ }, icon(): any { return { - backgroundColor: this.lightmode - ? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})` - : this.user.avatarColor && this.user.avatarColor.length == 3 - ? `rgb(${this.user.avatarColor.join(',')})` - : null, + backgroundColor: this.user.avatarColor ? this.lightmode + ? this.user.avatarColor + : this.user.avatarColor.startsWith('rgb(') + ? this.user.avatarColor + : null + : null, backgroundImage: this.lightmode ? null : `url(${this.url})`, borderRadius: this.$store.state.settings.circleIcons ? '100%' : null }; @@ -67,7 +68,7 @@ export default Vue.extend({ }, mounted() { if (this.user.avatarColor) { - this.$el.style.color = `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`; + this.$el.style.color = this.user.avatarColor; } }, methods: { diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue index c6fc36db33d1c7dd82584e17e6326ec8ec25ce07..bd0401f785661e00587624cc5f1e978c650f4969 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.game.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue @@ -24,11 +24,11 @@ <div class="board"> <div class="labels-x" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> - <span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span> + <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> </div> <div class="flex"> <div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> - <div v-for="i in game.settings.map.length">{{ i }}</div> + <div v-for="i in game.map.length">{{ i }}</div> </div> <div class="cells" :style="cellsStyle"> <div v-for="(stone, i) in o.board" @@ -46,11 +46,11 @@ </div> </div> <div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> - <div v-for="i in game.settings.map.length">{{ i }}</div> + <div v-for="i in game.map.length">{{ i }}</div> </div> </div> <div class="labels-x" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> - <span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span> + <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> </div> </div> @@ -71,9 +71,9 @@ </div> <div class="info"> - <p v-if="game.settings.isLlotheo">{{ $t('is-llotheo') }}</p> - <p v-if="game.settings.loopedBoard">{{ $t('looped-map') }}</p> - <p v-if="game.settings.canPutEverywhere">{{ $t('can-put-everywhere') }}</p> + <p v-if="game.isLlotheo">{{ $t('is-llotheo') }}</p> + <p v-if="game.loopedBoard">{{ $t('looped-map') }}</p> + <p v-if="game.canPutEverywhere">{{ $t('can-put-everywhere') }}</p> </div> </div> </template> @@ -160,8 +160,8 @@ export default Vue.extend({ cellsStyle(): any { return { - 'grid-template-rows': `repeat(${this.game.settings.map.length}, 1fr)`, - 'grid-template-columns': `repeat(${this.game.settings.map[0].length}, 1fr)` + 'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`, + 'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)` }; } }, @@ -169,10 +169,10 @@ export default Vue.extend({ watch: { logPos(v) { if (!this.game.isEnded) return; - this.o = new Reversi(this.game.settings.map, { - isLlotheo: this.game.settings.isLlotheo, - canPutEverywhere: this.game.settings.canPutEverywhere, - loopedBoard: this.game.settings.loopedBoard + this.o = new Reversi(this.game.map, { + isLlotheo: this.game.isLlotheo, + canPutEverywhere: this.game.canPutEverywhere, + loopedBoard: this.game.loopedBoard }); for (const log of this.logs.slice(0, v)) { this.o.put(log.color, log.pos); @@ -184,10 +184,10 @@ export default Vue.extend({ created() { this.game = this.initGame; - this.o = new Reversi(this.game.settings.map, { - isLlotheo: this.game.settings.isLlotheo, - canPutEverywhere: this.game.settings.canPutEverywhere, - loopedBoard: this.game.settings.loopedBoard + this.o = new Reversi(this.game.map, { + isLlotheo: this.game.isLlotheo, + canPutEverywhere: this.game.canPutEverywhere, + loopedBoard: this.game.loopedBoard }); for (const log of this.game.logs) { @@ -286,10 +286,10 @@ export default Vue.extend({ onRescue(game) { this.game = game; - this.o = new Reversi(this.game.settings.map, { - isLlotheo: this.game.settings.isLlotheo, - canPutEverywhere: this.game.settings.canPutEverywhere, - loopedBoard: this.game.settings.loopedBoard + this.o = new Reversi(this.game.map, { + isLlotheo: this.game.isLlotheo, + canPutEverywhere: this.game.canPutEverywhere, + loopedBoard: this.game.loopedBoard }); for (const log of this.game.logs) { diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue index d5d148790cc1ec0c5dc4d5882b83e05f56318a66..9ee1a78b869dfd211dc8269b114329ebd0f31203 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.room.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue @@ -17,9 +17,9 @@ </header> <div> - <div class="random" v-if="game.settings.map == null"><fa icon="dice"/></div> - <div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }"> - <div v-for="(x, i) in game.settings.map.join('')" + <div class="random" v-if="game.map == null"><fa icon="dice"/></div> + <div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }"> + <div v-for="(x, i) in game.map.join('')" :data-none="x == ' '" @click="onPixelClick(i, x)"> <fa v-if="x == 'b'" :icon="fasCircle"/> @@ -35,9 +35,9 @@ </header> <div> - <form-radio v-model="game.settings.bw" value="random" @change="updateSettings">{{ $t('random') }}</form-radio> - <form-radio v-model="game.settings.bw" :value="1" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> - <form-radio v-model="game.settings.bw" :value="2" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> + <form-radio v-model="game.bw" value="random" @change="updateSettings('bw')">{{ $t('random') }}</form-radio> + <form-radio v-model="game.bw" :value="1" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> + <form-radio v-model="game.bw" :value="2" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> </div> </div> @@ -47,9 +47,9 @@ </header> <div> - <ui-switch v-model="game.settings.isLlotheo" @change="updateSettings">{{ $t('is-llotheo') }}</ui-switch> - <ui-switch v-model="game.settings.loopedBoard" @change="updateSettings">{{ $t('looped-map') }}</ui-switch> - <ui-switch v-model="game.settings.canPutEverywhere" @change="updateSettings">{{ $t('can-put-everywhere') }}</ui-switch> + <ui-switch v-model="game.isLlotheo" @change="updateSettings('isLlotheo')">{{ $t('is-llotheo') }}</ui-switch> + <ui-switch v-model="game.loopedBoard" @change="updateSettings('loopedBoard')">{{ $t('looped-map') }}</ui-switch> + <ui-switch v-model="game.canPutEverywhere" @change="updateSettings('canPutEverywhere')">{{ $t('can-put-everywhere') }}</ui-switch> </div> </div> @@ -159,8 +159,8 @@ export default Vue.extend({ this.connection.on('initForm', this.onInitForm); this.connection.on('message', this.onMessage); - if (this.game.user1Id != this.$store.state.i.id && this.game.settings.form1) this.form = this.game.settings.form1; - if (this.game.user2Id != this.$store.state.i.id && this.game.settings.form2) this.form = this.game.settings.form2; + if (this.game.user1Id != this.$store.state.i.id && this.game.form1) this.form = this.game.form1; + if (this.game.user2Id != this.$store.state.i.id && this.game.form2) this.form = this.game.form2; }, beforeDestroy() { @@ -189,18 +189,19 @@ export default Vue.extend({ this.$forceUpdate(); }, - updateSettings() { + updateSettings(key: string) { this.connection.send('updateSettings', { - settings: this.game.settings + key: key, + value: this.game[key] }); }, - onUpdateSettings(settings) { - this.game.settings = settings; - if (this.game.settings.map == null) { + onUpdateSettings({ key, value }) { + this.game[key] = value; + if (this.game.map == null) { this.mapName = null; } else { - const found = Object.values(maps).find(x => x.data.join('') == this.game.settings.map.join('')); + const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join('')); this.mapName = found ? found.name : '-Custom-'; } }, @@ -224,27 +225,27 @@ export default Vue.extend({ onMapChange() { if (this.mapName == null) { - this.game.settings.map = null; + this.game.map = null; } else { - this.game.settings.map = Object.values(maps).find(x => x.name == this.mapName).data; + this.game.map = Object.values(maps).find(x => x.name == this.mapName).data; } this.$forceUpdate(); this.updateSettings(); }, onPixelClick(pos, pixel) { - const x = pos % this.game.settings.map[0].length; - const y = Math.floor(pos / this.game.settings.map[0].length); + const x = pos % this.game.map[0].length; + const y = Math.floor(pos / this.game.map[0].length); const newPixel = pixel == ' ' ? '-' : pixel == '-' ? 'b' : pixel == 'b' ? 'w' : ' '; - const line = this.game.settings.map[y].split(''); + const line = this.game.map[y].split(''); line[x] = newPixel; - this.$set(this.game.settings.map, y, line.join('')); + this.$set(this.game.map, y, line.join('')); this.$forceUpdate(); - this.updateSettings(); + this.updateSettings('map'); } } }); diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue index b6803cd7f7f2f01c903ce989fdc88f78cd3f2d29..d33471a04981bbeb1a3fd6178247cc323c468125 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.vue @@ -106,7 +106,7 @@ export default Vue.extend({ async nav(game, actualNav = true) { if (this.selfNav) { // å—ã‘å–ã£ãŸã‚²ãƒ¼ãƒ æƒ…å ±ãŒçœç•¥ã•ã‚ŒãŸã‚‚ã®ãªã‚‰å®Œå…¨ãªæƒ…å ±ã‚’å–å¾—ã™ã‚‹ - if (game != null && (game.settings == null || game.settings.map == null)) { + if (game != null && game.map == null) { game = await this.$root.api('games/reversi/games/show', { gameId: game.id }); diff --git a/src/client/app/common/views/components/instance.vue b/src/client/app/common/views/components/instance.vue index 7b8d4f8e0bc488d54a4c765659fcea91d30f05df..497e4976f59b80f48aa25487f1530fd57ad13cd1 100644 --- a/src/client/app/common/views/components/instance.vue +++ b/src/client/app/common/views/components/instance.vue @@ -2,7 +2,7 @@ <div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta"> <div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div> - <h1>{{ meta.name }}</h1> + <h1>{{ meta.name || 'Misskey' }}</h1> <p v-html="meta.description || this.$t('@.about')"></p> <router-link to="/">{{ $t('start') }}</router-link> </div> diff --git a/src/client/app/common/views/components/mention.vue b/src/client/app/common/views/components/mention.vue index 11dddbd52a126ffe13164208e8e03bf09ff453a0..e1f67282b6f1e0483105974524e2beda3e843a03 100644 --- a/src/client/app/common/views/components/mention.vue +++ b/src/client/app/common/views/components/mention.vue @@ -33,7 +33,7 @@ export default Vue.extend({ }, computed: { canonical(): string { - return `@${this.username}@${toUnicode(this.host)}`; + return this.host === localHost ? `@${this.username}` : `@${this.username}@${toUnicode(this.host)}`; }, isMe(): boolean { return this.$store.getters.isSignedIn && this.canonical.toLowerCase() === `@${this.$store.state.i.username}@${toUnicode(localHost)}`.toLowerCase(); diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue index ba14ba3a44703475da40bab4f95b15280994827b..dc3aaa34f38261ccc288be4037bc75a960c10063 100644 --- a/src/client/app/common/views/components/poll.vue +++ b/src/client/app/common/views/components/poll.vue @@ -1,7 +1,7 @@ <template> <div class="mk-poll" :data-done="closed || isVoted"> <ul> - <li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''"> + <li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''"> <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <span> <template v-if="choice.isVoted"><fa icon="check"/></template> @@ -82,12 +82,6 @@ export default Vue.extend({ noteId: this.note.id, choice: id }).then(() => { - for (const c of this.poll.choices) { - if (c.id == id) { - c.votes++; - Vue.set(c, 'isVoted', true); - } - } if (!this.showResult) this.showResult = !this.poll.multiple; }); } diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue index cf7f88b2f56af9d4571149f8ce5fa28693093cce..46668054b8a1fac44dbaf485aaf331adb4446e9a 100644 --- a/src/client/app/common/views/components/reactions-viewer.vue +++ b/src/client/app/common/views/components/reactions-viewer.vue @@ -20,7 +20,7 @@ export default Vue.extend({ }, computed: { reactions(): any { - return this.note.reactionCounts; + return this.note.reactions; }, isMe(): boolean { return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId; diff --git a/src/client/app/common/views/components/settings/notification.vue b/src/client/app/common/views/components/settings/notification.vue index b689544d69f91460dabb7011a0b183140a1b90b3..2554fe633137dcd589f264cdbf08059f049424ea 100644 --- a/src/client/app/common/views/components/settings/notification.vue +++ b/src/client/app/common/views/components/settings/notification.vue @@ -2,7 +2,7 @@ <ui-card> <template #title><fa :icon="['far', 'bell']"/> {{ $t('title') }}</template> <section> - <ui-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch"> + <ui-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch"> {{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template> </ui-switch> <section> diff --git a/src/client/app/common/views/components/settings/profile.vue b/src/client/app/common/views/components/settings/profile.vue index b9837a6966c4dc95d0b03c6c8ef94ec6483218ff..acfc1875a66038bb497ade1d73adab20ccdcc9a5 100644 --- a/src/client/app/common/views/components/settings/profile.vue +++ b/src/client/app/common/views/components/settings/profile.vue @@ -158,14 +158,14 @@ export default Vue.extend({ computed: { alwaysMarkNsfw: { - get() { return this.$store.state.i.settings.alwaysMarkNsfw; }, + get() { return this.$store.state.i.alwaysMarkNsfw; }, set(value) { this.$root.api('i/update', { alwaysMarkNsfw: value }); } }, bannerStyle(): any { if (this.$store.state.i.bannerUrl == null) return {}; return { - backgroundColor: this.$store.state.i.bannerColor && this.$store.state.i.bannerColor.length == 3 ? `rgb(${ this.$store.state.i.bannerColor.join(',') })` : null, + backgroundColor: this.$store.state.i.bannerColor ? this.$store.state.i.bannerColor : null, backgroundImage: `url(${ this.$store.state.i.bannerUrl })` }; }, @@ -178,10 +178,10 @@ export default Vue.extend({ this.email = this.$store.state.i.email; this.name = this.$store.state.i.name; this.username = this.$store.state.i.username; - this.location = this.$store.state.i.profile.location; + this.location = this.$store.state.i.location; this.description = this.$store.state.i.description; this.lang = this.$store.state.i.lang; - this.birthday = this.$store.state.i.profile.birthday; + this.birthday = this.$store.state.i.birthday; this.avatarId = this.$store.state.i.avatarId; this.bannerId = this.$store.state.i.bannerId; this.isCat = this.$store.state.i.isCat; diff --git a/src/client/app/common/views/components/settings/theme.vue b/src/client/app/common/views/components/settings/theme.vue index 1dff61e459bb0db63b304c2cc0f5a8c2f4dae30b..3440aacb28cc650bfec9fcb112013e242060c3ba 100644 --- a/src/client/app/common/views/components/settings/theme.vue +++ b/src/client/app/common/views/components/settings/theme.vue @@ -130,20 +130,6 @@ import * as tinycolor from 'tinycolor2'; import * as JSON5 from 'json5'; import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons'; -// 後方互æ›æ€§ã®ãŸã‚ -function convertOldThemedefinition(t) { - const t2 = { - id: t.meta.id, - name: t.meta.name, - author: t.meta.author, - base: t.meta.base, - vars: t.meta.vars, - props: t - }; - delete t2.props.meta; - return t2; -} - export default Vue.extend({ i18n: i18n('common/views/components/theme.vue'), components: { @@ -231,20 +217,6 @@ export default Vue.extend({ } }, - beforeCreate() { - // migrate old theme definitions - // 後方互æ›æ€§ã®ãŸã‚ - this.$store.commit('device/set', { - key: 'themes', value: this.$store.state.device.themes.map(t => { - if (t.id == null) { - return convertOldThemedefinition(t); - } else { - return t; - } - }) - }); - }, - methods: { install(code) { let theme; @@ -259,11 +231,6 @@ export default Vue.extend({ return; } - // 後方互æ›æ€§ã®ãŸã‚ - if (theme.id == null && theme.meta != null) { - theme = convertOldThemedefinition(theme); - } - if (theme.id == null) { this.$root.dialog({ type: 'error', diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue index 16e1afaa9431d40f92aeeaf489877f37512900fb..45c2eabd45d29adbedd3a4f7ae4eeed208b76d2c 100644 --- a/src/client/app/common/views/components/signup.vue +++ b/src/client/app/common/views/components/signup.vue @@ -4,7 +4,7 @@ <ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill"> <span>{{ $t('invitation-code') }}</span> <template #prefix><fa icon="id-card-alt"/></template> - <template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainer.email)"></template> + <template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainerEmail)"></template> </ui-input> <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill"> <span>{{ $t('username') }}</span> diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue index 536d55247cdd6721ff7cb815f678ca8308d9dfb4..cd67cc0092945b28d05643c26becf035069f170b 100644 --- a/src/client/app/common/views/components/trends.vue +++ b/src/client/app/common/views/components/trends.vue @@ -4,9 +4,9 @@ <p class="empty" v-else-if="stats.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p> <!-- トランジションを有効ã«ã™ã‚‹ã¨ãªãœã‹ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã™ã‚‹ --> <transition-group v-else tag="div" name="chart"> - <div v-for="stat in stats" :key="stat.tag"> + <div v-for="stat in stats" :key="stat.name"> <div class="tag"> - <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> + <router-link :to="`/tags/${ encodeURIComponent(stat.name) }`" :title="stat.name">#{{ stat.name }}</router-link> <p>{{ $t('count').replace('{}', stat.usersCount) }}</p> </div> <x-chart class="chart" :src="stat.chart"/> diff --git a/src/client/app/common/views/components/user-list-editor.vue b/src/client/app/common/views/components/user-list-editor.vue index 53c945ca0a362e1fb0e0ffba88e3761d7dca5547..8d2e04d045b30522f52c909f316d6983b61c2e04 100644 --- a/src/client/app/common/views/components/user-list-editor.vue +++ b/src/client/app/common/views/components/user-list-editor.vue @@ -1,7 +1,7 @@ <template> <div class="cudqjmnl"> <ui-card> - <template #title><fa :icon="faList"/> {{ list.title }}</template> + <template #title><fa :icon="faList"/> {{ list.name }}</template> <section> <ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> @@ -75,7 +75,7 @@ export default Vue.extend({ this.$root.dialog({ title: this.$t('rename'), input: { - default: this.list.title + default: this.list.name } }).then(({ canceled, result: title }) => { if (canceled) return; @@ -89,7 +89,7 @@ export default Vue.extend({ del() { this.$root.dialog({ type: 'warning', - text: this.$t('delete-are-you-sure').replace('$1', this.list.title), + text: this.$t('delete-are-you-sure').replace('$1', this.list.name), showCancelButton: true }).then(({ canceled }) => { if (canceled) return; diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue index 93fd759fd956ba861eaa4284039975d631d92a9a..a95f7a9225d1fb4cb858d1ca9ca41dea7f80a865 100644 --- a/src/client/app/common/views/components/user-menu.vue +++ b/src/client/app/common/views/components/user-menu.vue @@ -73,7 +73,7 @@ export default Vue.extend({ title: t, select: { items: lists.map(list => ({ - value: list.id, text: list.title + value: list.id, text: list.name })) }, showCancelButton: true diff --git a/src/client/app/common/views/deck/deck.column-core.vue b/src/client/app/common/views/deck/deck.column-core.vue index 974c58235df9e4957df8d6d751514f1466cdcf7b..e3f92dea16276d15603ee2c9a8e046808d362377 100644 --- a/src/client/app/common/views/deck/deck.column-core.vue +++ b/src/client/app/common/views/deck/deck.column-core.vue @@ -3,7 +3,7 @@ <x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> <x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> <x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-tl-column v-else-if="column.type == 'social'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> <x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> <x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> <x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> diff --git a/src/client/app/common/views/deck/deck.hashtag-tl.vue b/src/client/app/common/views/deck/deck.hashtag-tl.vue index 07d96f82c4a655e1e12045d5fad180711b2dd17d..b94267b74b77386d9891391b923c87f5431bdb7a 100644 --- a/src/client/app/common/views/deck/deck.hashtag-tl.vue +++ b/src/client/app/common/views/deck/deck.hashtag-tl.vue @@ -28,7 +28,7 @@ export default Vue.extend({ data() { return { connection: null, - makePromise: cursor => this.$root.api('notes/search_by_tag', { + makePromise: cursor => this.$root.api('notes/search-by-tag', { limit: fetchLimit + 1, untilId: cursor ? cursor : undefined, withFiles: this.mediaOnly, diff --git a/src/client/app/common/views/deck/deck.notification.vue b/src/client/app/common/views/deck/deck.notification.vue index 6a116260e5ef2a23045f6c5d4abf795ab511ad97..3ced7b7e23cb982e5f07ff5397b96643cecf8b1a 100644 --- a/src/client/app/common/views/deck/deck.notification.vue +++ b/src/client/app/common/views/deck/deck.notification.vue @@ -62,7 +62,7 @@ </div> </div> - <div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> + <div class="notification pollVote" v-if="notification.type == 'pollVote'"> <mk-avatar class="avatar" :user="notification.user"/> <div> <header> diff --git a/src/client/app/common/views/deck/deck.tl-column.vue b/src/client/app/common/views/deck/deck.tl-column.vue index d53aabaea5175952b5c986d45f35ce607a01149a..f6a9ee52862c56757a3426eb90b12e227182e6d5 100644 --- a/src/client/app/common/views/deck/deck.tl-column.vue +++ b/src/client/app/common/views/deck/deck.tl-column.vue @@ -3,7 +3,7 @@ <template #header> <fa v-if="column.type == 'home'" icon="home"/> <fa v-if="column.type == 'local'" :icon="['far', 'comments']"/> - <fa v-if="column.type == 'hybrid'" icon="share-alt"/> + <fa v-if="column.type == 'social'" icon="share-alt"/> <fa v-if="column.type == 'global'" icon="globe"/> <fa v-if="column.type == 'list'" icon="list"/> <fa v-if="column.type == 'hashtag'" icon="hashtag"/> @@ -80,9 +80,9 @@ export default Vue.extend({ switch (this.column.type) { case 'home': return this.$t('@deck.home'); case 'local': return this.$t('@deck.local'); - case 'hybrid': return this.$t('@deck.hybrid'); + case 'social': return this.$t('@deck.social'); case 'global': return this.$t('@deck.global'); - case 'list': return this.column.list.title; + case 'list': return this.column.list.name; case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title; } } diff --git a/src/client/app/common/views/deck/deck.tl.vue b/src/client/app/common/views/deck/deck.tl.vue index 35cdfa704f794c099d3b278b27e648b52b2730f5..5381cfbd5ead6cfc7e1c34575d159eb17c4a513a 100644 --- a/src/client/app/common/views/deck/deck.tl.vue +++ b/src/client/app/common/views/deck/deck.tl.vue @@ -51,7 +51,7 @@ export default Vue.extend({ switch (this.src) { case 'home': return this.$root.stream.useSharedConnection('homeTimeline'); case 'local': return this.$root.stream.useSharedConnection('localTimeline'); - case 'hybrid': return this.$root.stream.useSharedConnection('hybridTimeline'); + case 'social': return this.$root.stream.useSharedConnection('socialTimeline'); case 'global': return this.$root.stream.useSharedConnection('globalTimeline'); } }, @@ -60,7 +60,7 @@ export default Vue.extend({ switch (this.src) { case 'home': return 'notes/timeline'; case 'local': return 'notes/local-timeline'; - case 'hybrid': return 'notes/hybrid-timeline'; + case 'social': return 'notes/social-timeline'; case 'global': return 'notes/global-timeline'; } }, @@ -107,7 +107,7 @@ export default Vue.extend({ this.$root.getMeta().then(meta => { this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && ( - meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) || + meta.disableLocalTimeline && ['local', 'social'].includes(this.src) || meta.disableGlobalTimeline && ['global'].includes(this.src)); }); }, diff --git a/src/client/app/common/views/deck/deck.vue b/src/client/app/common/views/deck/deck.vue index 8ffb3223f9b709cd14cedd5879e40736121ed3f6..a1bef840082d8f77857bf978c788af5beab627b3 100644 --- a/src/client/app/common/views/deck/deck.vue +++ b/src/client/app/common/views/deck/deck.vue @@ -106,16 +106,6 @@ export default Vue.extend({ value: deck }); } - - // 互æ›æ€§ã®ãŸã‚ - if (this.$store.state.device.deck != null && this.$store.state.device.deck.layout == null) { - this.$store.commit('device/set', { - key: 'deck', - value: Object.assign({}, this.$store.state.device.deck, { - layout: this.$store.state.device.deck.columns.map(c => [c.id]) - }) - }); - } }, mounted() { @@ -155,11 +145,11 @@ export default Vue.extend({ } }, { icon: 'share-alt', - text: this.$t('@deck.hybrid'), + text: this.$t('@deck.social'), action: () => { this.$store.commit('device/addDeckColumn', { id: uuid(), - type: 'hybrid' + type: 'social' }); } }, { @@ -199,7 +189,7 @@ export default Vue.extend({ title: this.$t('@deck.select-list'), select: { items: lists.map(list => ({ - value: list.id, text: list.title + value: list.id, text: list.name })) }, showCancelButton: true @@ -312,7 +302,7 @@ export default Vue.extend({ isTlColumn(id) { const column = this.columns.find(c => c.id === id); - return ['home', 'local', 'hybrid', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type); + return ['home', 'local', 'social', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type); } } }); diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue index 098bf1f4c49aded31673542543ebf5de3973a12b..67e92af4452bd71b157045f415882b54b89549fe 100644 --- a/src/client/app/common/views/pages/explore.vue +++ b/src/client/app/common/views/pages/explore.vue @@ -3,7 +3,7 @@ <ui-container :show-header="false" v-if="meta && stats"> <div class="kpdsmpnk" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> <div> - <router-link to="/explore" class="title">{{ $t('explore', { host: meta.name }) }}</router-link> + <router-link to="/explore" class="title">{{ $t('explore', { host: meta.name || 'Misskey' }) }}</router-link> <span>{{ $t('users-info', { users: num(stats.originalUsersCount) }) }}</span> </div> </div> @@ -13,8 +13,8 @@ <template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popular-tags') }}</template> <div class="vxjfqztj"> - <router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link> - <router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link> + <router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.name}`" :key="'local:' + tag.name" class="local">{{ tag.name }}</router-link> + <router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.name}`" :key="'remote:' + tag.name">{{ tag.name }}</router-link> </div> </ui-container> diff --git a/src/client/app/common/views/pages/followers.vue b/src/client/app/common/views/pages/followers.vue index 94d9c9b13c8ef9ba553f3bba70e3effda1898627..67cfb8512f3b5ea8fb964d0d7dac26661aa3d4b5 100644 --- a/src/client/app/common/views/pages/followers.vue +++ b/src/client/app/common/views/pages/followers.vue @@ -9,20 +9,30 @@ import Vue from 'vue'; import parseAcct from '../../../../../misc/acct/parse'; import i18n from '../../../i18n'; +const fetchLimit = 30; + export default Vue.extend({ - i18n: i18n(''), + i18n: i18n(), data() { return { makePromise: cursor => this.$root.api('users/followers', { ...parseAcct(this.$route.params.user), - limit: 30, - cursor: cursor ? cursor : undefined - }).then(x => { - return { - users: x.users, - cursor: x.next - }; + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + }).then(followings => { + if (followings.length == fetchLimit + 1) { + followings.pop(); + return { + users: followings.map(following => following.follower), + cursor: followings[followings.length - 1].id + }; + } else { + return { + users: followings.map(following => following.follower), + cursor: null + }; + } }), }; }, diff --git a/src/client/app/common/views/pages/following.vue b/src/client/app/common/views/pages/following.vue index 39739fa3da6b7f92fe4a41d4664807ff25d2fab5..518a63ac1bb6b992a9493eb2e292817e18e6190c 100644 --- a/src/client/app/common/views/pages/following.vue +++ b/src/client/app/common/views/pages/following.vue @@ -7,19 +7,32 @@ <script lang="ts"> import Vue from 'vue'; import parseAcct from '../../../../../misc/acct/parse'; +import i18n from '../../../i18n'; + +const fetchLimit = 30; export default Vue.extend({ + i18n: i18n(), + data() { return { makePromise: cursor => this.$root.api('users/following', { ...parseAcct(this.$route.params.user), - limit: 30, - cursor: cursor ? cursor : undefined - }).then(x => { - return { - users: x.users, - cursor: x.next - }; + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + }).then(followings => { + if (followings.length == fetchLimit + 1) { + followings.pop(); + return { + users: followings.map(following => following.followee), + cursor: followings[followings.length - 1].id + }; + } else { + return { + users: followings.map(following => following.followee), + cursor: null + }; + } }), }; }, diff --git a/src/client/app/common/views/pages/share.vue b/src/client/app/common/views/pages/share.vue index 760350b9211be612383bb682a60fe4d555f62886..0452b25dfc5a6232c11806327d4655a895cb84af 100644 --- a/src/client/app/common/views/pages/share.vue +++ b/src/client/app/common/views/pages/share.vue @@ -42,7 +42,7 @@ export default Vue.extend({ }, mounted() { this.$root.getMeta().then(meta => { - this.name = meta.name; + this.name = meta.name || 'Misskey'; }); } }); diff --git a/src/client/app/common/views/widgets/server.info.vue b/src/client/app/common/views/widgets/server.info.vue index f7efb6fa2a700340e213d86f5ff0d285179c3ceb..a97b4ec49678b31b0a6d27ca43fc6ca2e7e7cc89 100644 --- a/src/client/app/common/views/widgets/server.info.vue +++ b/src/client/app/common/views/widgets/server.info.vue @@ -1,6 +1,6 @@ <template> <div class="info"> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> + <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> <p>Machine: {{ meta.machine }}</p> <p>Node: {{ meta.node }}</p> <p>Version: {{ meta.version }} </p> diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue index c560e6d97e39c97d57b531db3a7764e98213302b..5b9ff81c0d122658945b3bef34d9d6aed2a3795b 100644 --- a/src/client/app/desktop/views/components/drive.file.vue +++ b/src/client/app/desktop/views/components/drive.file.vue @@ -60,7 +60,7 @@ export default Vue.extend({ return this.browser.selectedFiles.some(f => f.id == this.file.id); }, title(): string { - return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`; + return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`; } }, methods: { diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue index 9b36716e8372d6f680a1513297c724fae139e371..585294fc89a37f89b97f3094e9fe9d0844649fc9 100644 --- a/src/client/app/desktop/views/components/note.vue +++ b/src/client/app/desktop/views/components/note.vue @@ -54,11 +54,11 @@ </button> <button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton button" @click="react()" ref="reactButton" :title="$t('add-reaction')"> <fa icon="plus"/> - <p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p> + <p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p> </button> <button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted button" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')"> <fa icon="minus"/> - <p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p> + <p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p> </button> <button @click="menu()" ref="menuButton" class="button"> <fa icon="ellipsis-h"/> diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index 24b6fc3eba2a2c65c58294e23ef0c0287f53bd07..0bf013292688781dafb03f3e557a86afd3cf02b0 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -110,7 +110,7 @@ </div> </template> - <template v-if="notification.type == 'poll_vote'"> + <template v-if="notification.type == 'pollVote'"> <mk-avatar class="avatar" :user="notification.user"/> <div class="text"> <p><fa icon="chart-pie"/><a :href="notification.user | userPage" v-user-preview="notification.user.id"> diff --git a/src/client/app/desktop/views/components/user-list-window.vue b/src/client/app/desktop/views/components/user-list-window.vue index afece9fe8687c03e5bb1728746183ffeb1254856..6764579b206a56f45794e56decfc8e61f942cd74 100644 --- a/src/client/app/desktop/views/components/user-list-window.vue +++ b/src/client/app/desktop/views/components/user-list-window.vue @@ -1,6 +1,6 @@ <template> <mk-window ref="window" width="450px" height="500px" @closed="destroyDom"> - <template #header><fa icon="list"/> {{ list.title }}</template> + <template #header><fa icon="list"/> {{ list.name }}</template> <x-editor :list="list"/> </mk-window> diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue index 4f0af4a278bcd43d7a61dcad0802aec48ad90976..7afcd6aa3bf7f9112cad3e6ca215c037cd7a9e11 100644 --- a/src/client/app/desktop/views/components/user-lists-window.vue +++ b/src/client/app/desktop/views/components/user-lists-window.vue @@ -4,7 +4,7 @@ <div class="xkxvokkjlptzyewouewmceqcxhpgzprp"> <button class="ui" @click="add">{{ $t('create-list') }}</button> - <a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a> + <a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a> </div> </mk-window> </template> diff --git a/src/client/app/desktop/views/home/home.vue b/src/client/app/desktop/views/home/home.vue index fb7af5a9ad2b2a888417e48f1a3e508f5eb45d32..d0b2fc10bc58a7b8c0ccb036c4ab2e0541f4cacf 100644 --- a/src/client/app/desktop/views/home/home.vue +++ b/src/client/app/desktop/views/home/home.vue @@ -101,7 +101,7 @@ export default Vue.extend({ computed: { home(): any[] { if (this.$store.getters.isSignedIn) { - return this.$store.state.settings.home || []; + return this.$store.state.device.home || []; } else { return [{ name: 'instance', @@ -182,12 +182,8 @@ export default Vue.extend({ } //#endregion - if (this.$store.state.settings.home == null) { - this.$root.api('i/update_home', { - home: _defaultDesktopHomeWidgets - }).then(() => { - this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets); - }); + if (this.$store.state.device.home == null) { + this.$store.commit('device/setHome', _defaultDesktopHomeWidgets); } } }, @@ -226,7 +222,7 @@ export default Vue.extend({ }, addWidget() { - this.$store.dispatch('settings/addHomeWidget', { + this.$store.commit('device/addHomeWidget', { name: this.widgetAdderSelected, id: uuid(), place: 'left', @@ -237,12 +233,9 @@ export default Vue.extend({ saveHome() { const left = this.widgets.left; const right = this.widgets.right; - this.$store.commit('settings/setHome', left.concat(right)); + this.$store.commit('device/setHome', left.concat(right)); for (const w of left) w.place = 'left'; for (const w of right) w.place = 'right'; - this.$root.api('i/update_home', { - home: this.home - }); }, done() { diff --git a/src/client/app/desktop/views/home/tag.vue b/src/client/app/desktop/views/home/tag.vue index 4f9bc66e7b8b2b14a5afb48a33eff950987419fc..98d89955b34c92a5ddeebed796f619e23f490fab 100644 --- a/src/client/app/desktop/views/home/tag.vue +++ b/src/client/app/desktop/views/home/tag.vue @@ -21,7 +21,7 @@ export default Vue.extend({ i18n: i18n('desktop/views/pages/tag.vue'), data() { return { - makePromise: cursor => this.$root.api('notes/search_by_tag', { + makePromise: cursor => this.$root.api('notes/search-by-tag', { limit: limit + 1, offset: cursor ? cursor : undefined, tag: this.$route.params.tag diff --git a/src/client/app/desktop/views/home/timeline.core.vue b/src/client/app/desktop/views/home/timeline.core.vue index e306ac873c8c0871ef07cf3d805d8e8581fe9be3..bf07b69dbf866495c38e810dd1ebdc54b17f5421 100644 --- a/src/client/app/desktop/views/home/timeline.core.vue +++ b/src/client/app/desktop/views/home/timeline.core.vue @@ -58,7 +58,7 @@ export default Vue.extend({ }; if (this.src == 'tag') { - this.endpoint = 'notes/search_by_tag'; + this.endpoint = 'notes/search-by-tag'; this.query = { query: this.tagTl.query }; @@ -77,9 +77,9 @@ export default Vue.extend({ this.endpoint = 'notes/local-timeline'; this.connection = this.$root.stream.useSharedConnection('localTimeline'); this.connection.on('note', prepend); - } else if (this.src == 'hybrid') { - this.endpoint = 'notes/hybrid-timeline'; - this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); + } else if (this.src == 'social') { + this.endpoint = 'notes/social-timeline'; + this.connection = this.$root.stream.useSharedConnection('socialTimeline'); this.connection.on('note', prepend); } else if (this.src == 'global') { this.endpoint = 'notes/global-timeline'; diff --git a/src/client/app/desktop/views/home/timeline.vue b/src/client/app/desktop/views/home/timeline.vue index 0b8ced479500ed237edae4e6b1b5a536cab90759..ccd55d1d7a1c362b6d6909a4241d5c68cab299ee 100644 --- a/src/client/app/desktop/views/home/timeline.vue +++ b/src/client/app/desktop/views/home/timeline.vue @@ -6,10 +6,10 @@ <header class="zahtxcqi"> <span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> - <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> + <span :data-active="src == 'social'" @click="src = 'social'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('social') }}</span> <span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> <span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span> - <span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span> + <span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</span> <div class="buttons"> <button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> <button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> @@ -78,7 +78,7 @@ export default Vue.extend({ ) && this.src === 'global') this.src = 'local'; if (!( this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; + ) && ['local', 'social'].includes(this.src)) this.src = 'home'; }); if (this.$store.state.device.tl) { @@ -89,7 +89,7 @@ export default Vue.extend({ this.tagTl = this.$store.state.device.tl.arg; } } else if (this.$store.state.i.followingCount == 0) { - this.src = 'hybrid'; + this.src = 'social'; } }, @@ -143,7 +143,7 @@ export default Vue.extend({ menu = menu.concat(lists.map(list => ({ icon: 'list', - text: list.title, + text: list.name, action: () => { this.list = list; this.src = 'list'; diff --git a/src/client/app/desktop/views/home/user/user.header.vue b/src/client/app/desktop/views/home/user/user.header.vue index 85dcd3ddae81222f2f567482b51ec5cef4ad94e4..61c3839c146b0584e04d62c34129404e8ce60960 100644 --- a/src/client/app/desktop/views/home/user/user.header.vue +++ b/src/client/app/desktop/views/home/user/user.header.vue @@ -36,8 +36,8 @@ </dl> </div> <div class="info"> - <span class="location" v-if="user.host === null && user.profile.location"><fa icon="map-marker"/> {{ user.profile.location }}</span> - <span class="birthday" v-if="user.host === null && user.profile.birthday"><fa icon="birthday-cake"/> {{ user.profile.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span> + <span class="location" v-if="user.host === null && user.location"><fa icon="map-marker"/> {{ user.location }}</span> + <span class="birthday" v-if="user.host === null && user.birthday"><fa icon="birthday-cake"/> {{ user.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span> </div> <div class="status"> <router-link :to="user | userPage()" class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</router-link> @@ -71,7 +71,7 @@ export default Vue.extend({ }, age(): number { - return age(this.user.profile.birthday); + return age(this.user.birthday); } }, mounted() { diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue index ddffeae4084785715d328e9d8e4ed5d2e6f22830..5a5cd9c8e679d310439261b0dbeefea36548d32e 100644 --- a/src/client/app/desktop/views/pages/welcome.vue +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -13,8 +13,8 @@ <div class="body"> <div class="main block"> <div> - <h1 v-if="name != 'Misskey'">{{ name }}</h1> - <h1 v-else><img svg-inline src="../../../../assets/title.svg" :alt="name"></h1> + <h1 v-if="name != null">{{ name }}</h1> + <h1 v-else><img svg-inline src="../../../../assets/title.svg" alt="Misskey"></h1> <div class="info"> <span><b>{{ host }}</b> - <span v-html="$t('powered-by-misskey')"></span></span> @@ -87,7 +87,7 @@ <div> <div v-if="meta" class="body"> <p>Version: <b>{{ meta.version }}</b></p> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> + <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> </div> </div> </div> @@ -162,7 +162,7 @@ export default Vue.extend({ banner: null, copyright, host: toUnicode(host), - name: 'Misskey', + name: null, description: '', announcements: [], photos: [] diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue index d8c128904a6330692eaeb2920c345b77c92ec379..00f2ed60d9edac5920209bc6a089d656fb4b1e02 100644 --- a/src/client/app/dev/views/new-app.vue +++ b/src/client/app/dev/views/new-app.vue @@ -15,15 +15,21 @@ <b-form-group :description="$t('description')"> <b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert> <b-form-checkbox-group v-model="permission" stacked> - <b-form-checkbox value="account-read">{{ $t('account-read') }}</b-form-checkbox> - <b-form-checkbox value="account-write">{{ $t('account-write') }}</b-form-checkbox> - <b-form-checkbox value="note-write">{{ $t('note-write') }}</b-form-checkbox> - <b-form-checkbox value="reaction-write">{{ $t('reaction-write') }}</b-form-checkbox> - <b-form-checkbox value="following-write">{{ $t('following-write') }}</b-form-checkbox> - <b-form-checkbox value="drive-read">{{ $t('drive-read') }}</b-form-checkbox> - <b-form-checkbox value="drive-write">{{ $t('drive-write') }}</b-form-checkbox> - <b-form-checkbox value="notification-read">{{ $t('notification-read') }}</b-form-checkbox> - <b-form-checkbox value="notification-write">{{ $t('notification-write') }}</b-form-checkbox> + <b-form-checkbox value="read:account">{{ $t('read:account') }}</b-form-checkbox> + <b-form-checkbox value="write:account">{{ $t('write:account') }}</b-form-checkbox> + <b-form-checkbox value="write:notes">{{ $t('write:notes') }}</b-form-checkbox> + <b-form-checkbox value="read:reactions">{{ $t('read:reactions') }}</b-form-checkbox> + <b-form-checkbox value="write:reactions">{{ $t('write:reactions') }}</b-form-checkbox> + <b-form-checkbox value="read:following">{{ $t('read:following') }}</b-form-checkbox> + <b-form-checkbox value="write:following">{{ $t('write:following') }}</b-form-checkbox> + <b-form-checkbox value="read:mutes">{{ $t('read:mutes') }}</b-form-checkbox> + <b-form-checkbox value="write:mutes">{{ $t('write:mutes') }}</b-form-checkbox> + <b-form-checkbox value="read:blocks">{{ $t('read:blocks') }}</b-form-checkbox> + <b-form-checkbox value="write:blocks">{{ $t('write:blocks') }}</b-form-checkbox> + <b-form-checkbox value="read:drive">{{ $t('read:drive') }}</b-form-checkbox> + <b-form-checkbox value="write:drive">{{ $t('write:drive') }}</b-form-checkbox> + <b-form-checkbox value="read:notifications">{{ $t('read:notifications') }}</b-form-checkbox> + <b-form-checkbox value="write:notifications">{{ $t('write:notifications') }}</b-form-checkbox> </b-form-checkbox-group> </b-form-group> </b-card> diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts index 9e191bf43cde84f2e3e5493a03a5cae180c3dd86..8f4b243623776d3b53e27a68a77c418c4e2df2a9 100644 --- a/src/client/app/mios.ts +++ b/src/client/app/mios.ts @@ -278,21 +278,6 @@ export default class MiOS extends EventEmitter { }); }); - main.on('homeUpdated', x => { - this.store.commit('settings/setHome', x); - }); - - main.on('mobileHomeUpdated', x => { - this.store.commit('settings/setMobileHome', x); - }); - - main.on('widgetUpdated', x => { - this.store.commit('settings/updateWidget', { - id: x.id, - data: x.data - }); - }); - // トークンãŒå†ç”Ÿæˆã•ã‚ŒãŸã¨ã // ã“ã®ã¾ã¾ã§ã¯MisskeyãŒåˆ©ç”¨ã§ããªã„ã®ã§å¼·åˆ¶çš„ã«ã‚µã‚¤ãƒ³ã‚¢ã‚¦ãƒˆã•ã›ã‚‹ main.on('myTokenRegenerated', () => { diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue index 92f5c1fd195b064fc3fa3f095e9c12748044bdb6..8f724b0f8e2fba3d60c938be7870dc4663f32970 100644 --- a/src/client/app/mobile/views/components/drive.file-detail.vue +++ b/src/client/app/mobile/views/components/drive.file-detail.vue @@ -22,7 +22,7 @@ <div> <span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span> <span class="separator"></span> - <span class="data-size">{{ file.datasize | bytes }}</span> + <span class="data-size">{{ file.size | bytes }}</span> <span class="separator"></span> <span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> <template v-if="file.isSensitive"> diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue index feca266ede6c0187f16f3be03a1757f32b852b51..ed95537f9cbf8be00309b1302129347bbc0ec24f 100644 --- a/src/client/app/mobile/views/components/drive.file.vue +++ b/src/client/app/mobile/views/components/drive.file.vue @@ -10,7 +10,7 @@ <footer> <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span> <span class="separator"></span> - <span class="data-size">{{ file.datasize | bytes }}</span> + <span class="data-size">{{ file.size | bytes }}</span> <span class="separator"></span> <span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> <template v-if="file.isSensitive"> diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue index 1b8eceaa6c044f87c6525e21daf573da9caf91e5..8422c7342064b87b1cd5729954becad705a82a2d 100644 --- a/src/client/app/mobile/views/components/notification-preview.vue +++ b/src/client/app/mobile/views/components/notification-preview.vue @@ -54,7 +54,7 @@ </div> </template> - <template v-if="notification.type == 'poll_vote'"> + <template v-if="notification.type == 'pollVote'"> <mk-avatar class="avatar" :user="notification.user"/> <div class="text"> <p><fa icon="chart-pie"/><mk-user-name :user="notification.user"/></p> diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue index 5308d96533b405fbe16f17b84b561aa4b1f203ac..1128a76000b16325741baee24870b6696adb72fe 100644 --- a/src/client/app/mobile/views/components/notification.vue +++ b/src/client/app/mobile/views/components/notification.vue @@ -54,7 +54,7 @@ </div> </div> - <div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> + <div class="notification pollVote" v-if="notification.type == 'pollVote'"> <mk-avatar class="avatar" :user="notification.user"/> <div> <header> diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index 4f9f5119ab00cb44a56ef2cf9648bceeb7fc9bcb..1eb7399979e20e1301ecaf4c24220cdddf5673ca 100644 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -59,7 +59,7 @@ export default Vue.extend({ }; if (this.src == 'tag') { - this.endpoint = 'notes/search_by_tag'; + this.endpoint = 'notes/search-by-tag'; this.query = { query: this.tagTl.query }; @@ -78,9 +78,9 @@ export default Vue.extend({ this.endpoint = 'notes/local-timeline'; this.connection = this.$root.stream.useSharedConnection('localTimeline'); this.connection.on('note', prepend); - } else if (this.src == 'hybrid') { - this.endpoint = 'notes/hybrid-timeline'; - this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); + } else if (this.src == 'social') { + this.endpoint = 'notes/social-timeline'; + this.connection = this.$root.stream.useSharedConnection('socialTimeline'); this.connection.on('note', prepend); } else if (this.src == 'global') { this.endpoint = 'notes/global-timeline'; diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 59fae2340b4f0d1a43a506906dc436a738ce8127..7e39441996cf009d6c6b0294f603a37c132972f3 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -5,11 +5,11 @@ <span :class="$style.title"> <span v-if="src == 'home'"><fa icon="home"/>{{ $t('home') }}</span> <span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>{{ $t('local') }}</span> - <span v-if="src == 'hybrid'"><fa icon="share-alt"/>{{ $t('hybrid') }}</span> + <span v-if="src == 'social'"><fa icon="share-alt"/>{{ $t('social') }}</span> <span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span> <span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span> <span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span> - <span v-if="src == 'list'"><fa icon="list"/>{{ list.title }}</span> + <span v-if="src == 'list'"><fa icon="list"/>{{ list.name }}</span> <span v-if="src == 'tag'"><fa icon="hashtag"/>{{ tagTl.title }}</span> </span> <span style="margin-left:8px"> @@ -32,7 +32,7 @@ <div> <span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> - <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> + <span :data-active="src == 'social'" @click="src = 'social'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('social') }}</span> <span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> <div class="hr"></div> <span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> {{ $t('mentions') }}<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span> @@ -50,7 +50,7 @@ <div class="tl"> <x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/> <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> - <x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> + <x-tl v-if="src == 'social'" ref="tl" key="social" src="social"/> <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> <x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> <x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> @@ -120,7 +120,7 @@ export default Vue.extend({ ) && this.src === 'global') this.src = 'local'; if (!( this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; + ) && ['local', 'social'].includes(this.src)) this.src = 'home'; }); if (this.$store.state.device.tl) { @@ -131,7 +131,7 @@ export default Vue.extend({ this.tagTl = this.$store.state.device.tl.arg; } } else if (this.$store.state.i.followingCount == 0) { - this.src = 'hybrid'; + this.src = 'social'; } }, diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue index 318e63a4735775b9e7edc05cf9c52db7228589b2..7a7b90dad003a14ba998882aeec54526d18be3a2 100644 --- a/src/client/app/mobile/views/pages/tag.vue +++ b/src/client/app/mobile/views/pages/tag.vue @@ -19,7 +19,7 @@ export default Vue.extend({ i18n: i18n('mobile/views/pages/tag.vue'), data() { return { - makePromise: cursor => this.$root.api('notes/search_by_tag', { + makePromise: cursor => this.$root.api('notes/search-by-tag', { limit: limit + 1, offset: cursor ? cursor : undefined, tag: this.$route.params.tag diff --git a/src/client/app/mobile/views/pages/user-list.vue b/src/client/app/mobile/views/pages/user-list.vue index 874bae5d1816e1d3cc1f613913312841da26fa21..68fd0358c47131270efa97baaae43a7fa281040f 100644 --- a/src/client/app/mobile/views/pages/user-list.vue +++ b/src/client/app/mobile/views/pages/user-list.vue @@ -1,6 +1,6 @@ <template> <mk-ui> - <template #header v-if="!fetching"><fa icon="list"/>{{ list.title }}</template> + <template #header v-if="!fetching"><fa icon="list"/>{{ list.name }}</template> <main v-if="!fetching"> <x-editor :list="list"/> diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue index fd129339fd85c43ee584c087eecee132a953ce34..49006f41f68270ce4473b96b111653600286a3c7 100644 --- a/src/client/app/mobile/views/pages/user-lists.vue +++ b/src/client/app/mobile/views/pages/user-lists.vue @@ -5,7 +5,7 @@ <main> <ul> - <li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.title }}</router-link></li> + <li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link></li> </ul> </main> </mk-ui> diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue index fe5ef057e7bfa40826d4b0fb3dcae8b470ccdef4..72f2998dbad8a4a56bc1909992bb905400737f3d 100644 --- a/src/client/app/mobile/views/pages/user/index.vue +++ b/src/client/app/mobile/views/pages/user/index.vue @@ -36,11 +36,11 @@ </dl> </div> <div class="info"> - <p class="location" v-if="user.host === null && user.profile.location"> - <fa icon="map-marker"/>{{ user.profile.location }} + <p class="location" v-if="user.host === null && user.location"> + <fa icon="map-marker"/>{{ user.location }} </p> - <p class="birthday" v-if="user.host === null && user.profile.birthday"> - <fa icon="birthday-cake"/>{{ user.profile.birthday.replace('-', 'å¹´').replace('-', '月') + 'æ—¥' }} ({{ $t('years-old', { age }) }}) + <p class="birthday" v-if="user.host === null && user.birthday"> + <fa icon="birthday-cake"/>{{ user.birthday.replace('-', 'å¹´').replace('-', '月') + 'æ—¥' }} ({{ $t('years-old', { age }) }}) </p> </div> <div class="status"> @@ -104,7 +104,7 @@ export default Vue.extend({ }, computed: { age(): number { - return age(this.user.profile.birthday); + return age(this.user.birthday); }, avator(): string { return this.$store.state.device.disableShowingAnimatedImages diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue index 1a2b0b6c128179dcf515ffdc508fda7cf1556d23..dd71a918dbe865b44f5383d19d65f109fd0758fd 100644 --- a/src/client/app/mobile/views/pages/welcome.vue +++ b/src/client/app/mobile/views/pages/welcome.vue @@ -3,10 +3,10 @@ <div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div> <div> - <img svg-inline src="../../../../assets/title.svg" :alt="name"> + <img svg-inline src="../../../../assets/title.svg" alt="Misskey"> <p class="host">{{ host }}</p> <div class="about"> - <h2>{{ name }}</h2> + <h2>{{ name || 'Misskey' }}</h2> <p v-html="description || this.$t('@.about')"></p> <router-link class="signup" to="/signup">{{ $t('@.signup') }}</router-link> </div> @@ -62,7 +62,7 @@ </article> <div class="info" v-if="meta"> <p>Version: <b>{{ meta.version }}</b></p> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> + <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> </div> <footer> <small>{{ copyright }}</small> @@ -87,7 +87,7 @@ export default Vue.extend({ stats: null, banner: null, host: toUnicode(host), - name: 'Misskey', + name: null, description: '', photos: [], announcements: [] diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue index 96dcb977fa38ba27e29f871952a3ebf3d46ca083..2647f96de2610e34e10df59ae361e70a2c4371a9 100644 --- a/src/client/app/mobile/views/pages/widgets.vue +++ b/src/client/app/mobile/views/pages/widgets.vue @@ -119,7 +119,7 @@ export default Vue.extend({ }, addWidget() { - this.$store.dispatch('settings/addMobileHomeWidget', { + this.$store.commit('settings/addMobileHomeWidget', { name: this.widgetAdderSelected, id: uuid(), data: {} @@ -127,14 +127,11 @@ export default Vue.extend({ }, removeWidget(widget) { - this.$store.dispatch('settings/removeMobileHomeWidget', widget); + this.$store.commit('settings/removeMobileHomeWidget', widget); }, saveHome() { this.$store.commit('settings/setMobileHome', this.widgets); - this.$root.api('i/update_mobile_home', { - home: this.widgets - }); } } }); diff --git a/src/client/app/store.ts b/src/client/app/store.ts index e49934fc1673d674f493112d34ed5b45ec9fe347..c82981ad245a0bfe3a89432ee5655d76db48d337 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -7,8 +7,6 @@ import { erase } from '../../prelude/array'; import getNoteSummary from '../../misc/get-note-summary'; const defaultSettings = { - home: null, - mobileHome: [], keepCw: false, tagTimelines: [], fetchOnScroll: true, @@ -41,6 +39,8 @@ const defaultSettings = { }; const defaultDeviceSettings = { + home: null, + mobileHome: [], deck: null, deckMode: false, deckColumnAlign: 'center', @@ -120,7 +120,7 @@ export default (os: MiOS) => new Vuex.Store({ actions: { login(ctx, i) { ctx.commit('updateI', i); - ctx.dispatch('settings/merge', i.clientSettings); + ctx.dispatch('settings/merge', i.clientData); }, logout(ctx) { @@ -134,8 +134,8 @@ export default (os: MiOS) => new Vuex.Store({ ctx.commit('updateIKeyValue', { key, value }); } - if (me.clientSettings) { - ctx.dispatch('settings/merge', me.clientSettings); + if (me.clientData) { + ctx.dispatch('settings/merge', me.clientData); } }, }, @@ -162,6 +162,48 @@ export default (os: MiOS) => new Vuex.Store({ state.visibility = visibility; }, + setHome(state, data) { + state.home = data; + }, + + addHomeWidget(state, widget) { + state.home.unshift(widget); + }, + + setMobileHome(state, data) { + state.mobileHome = data; + }, + + updateWidget(state, x) { + let w; + + //#region Desktop home + if (state.home) { + w = state.home.find(w => w.id == x.id); + if (w) { + w.data = x.data; + } + } + //#endregion + + //#region Mobile home + if (state.mobileHome) { + w = state.mobileHome.find(w => w.id == x.id); + if (w) { + w.data = x.data; + } + } + //#endregion + }, + + addMobileHomeWidget(state, widget) { + state.mobileHome.unshift(widget); + }, + + removeMobileHomeWidget(state, widget) { + state.mobileHome = state.mobileHome.filter(w => w.id != widget.id); + }, + addDeckColumn(state, column) { if (column.name == undefined) column.name = null; state.deck.columns.push(column); @@ -301,48 +343,6 @@ export default (os: MiOS) => new Vuex.Store({ set(state, x: { key: string; value: any }) { nestedProperty.set(state, x.key, x.value); }, - - setHome(state, data) { - state.home = data; - }, - - addHomeWidget(state, widget) { - state.home.unshift(widget); - }, - - setMobileHome(state, data) { - state.mobileHome = data; - }, - - updateWidget(state, x) { - let w; - - //#region Desktop home - if (state.home) { - w = state.home.find(w => w.id == x.id); - if (w) { - w.data = x.data; - } - } - //#endregion - - //#region Mobile home - if (state.mobileHome) { - w = state.mobileHome.find(w => w.id == x.id); - if (w) { - w.data = x.data; - } - } - //#endregion - }, - - addMobileHomeWidget(state, widget) { - state.mobileHome.unshift(widget); - }, - - removeMobileHomeWidget(state, widget) { - state.mobileHome = state.mobileHome.filter(w => w.id != widget.id); - }, }, actions: { @@ -363,30 +363,6 @@ export default (os: MiOS) => new Vuex.Store({ }); } }, - - addHomeWidget(ctx, widget) { - ctx.commit('addHomeWidget', widget); - - os.api('i/update_home', { - home: ctx.state.home - }); - }, - - addMobileHomeWidget(ctx, widget) { - ctx.commit('addMobileHomeWidget', widget); - - os.api('i/update_mobile_home', { - home: ctx.state.mobileHome - }); - }, - - removeMobileHomeWidget(ctx, widget) { - ctx.commit('removeMobileHomeWidget', widget); - - os.api('i/update_mobile_home', { - home: ctx.state.mobileHome.filter(w => w.id != widget.id) - }); - } } } } diff --git a/src/config/types.ts b/src/config/types.ts index 5f30d410c9f7379e9a92ff1794c723c39d1bb78c..d1749c52f721faf4bd39940a31c19065053b1877 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -8,7 +8,7 @@ export type Source = { port: number; https?: { [x: string]: string }; disableHsts?: boolean; - mongodb: { + db: { host: string; port: number; db: string; @@ -42,6 +42,8 @@ export type Source = { accesslog?: string; clusterLimit?: number; + + id: string; }; /** diff --git a/src/crypto_key.cc b/src/crypto_key.cc deleted file mode 100644 index 658586baedef4011e36a2168744013a109410cbd..0000000000000000000000000000000000000000 --- a/src/crypto_key.cc +++ /dev/null @@ -1,111 +0,0 @@ -#include <nan.h> -#include <openssl/bio.h> -#include <openssl/buffer.h> -#include <openssl/crypto.h> -#include <openssl/pem.h> -#include <openssl/rsa.h> -#include <openssl/x509.h> - -NAN_METHOD(extractPublic) -{ - const auto sourceString = info[0]->ToString(Nan::GetCurrentContext()).ToLocalChecked(); - if (!sourceString->IsOneByte()) { - Nan::ThrowError("Malformed character found"); - return; - } - - size_t sourceLength = sourceString->Length(); - const auto sourceBuf = new char[sourceLength]; - - Nan::DecodeWrite(sourceBuf, sourceLength, sourceString); - - const auto source = BIO_new_mem_buf(sourceBuf, sourceLength); - if (source == nullptr) { - Nan::ThrowError("Memory allocation failed"); - delete[] sourceBuf; - return; - } - - const auto rsa = PEM_read_bio_RSAPrivateKey(source, nullptr, nullptr, nullptr); - - BIO_free(source); - delete[] sourceBuf; - - if (rsa == nullptr) { - Nan::ThrowError("Decode failed"); - return; - } - - const auto destination = BIO_new(BIO_s_mem()); - if (destination == nullptr) { - Nan::ThrowError("Memory allocation failed"); - return; - } - - const auto result = PEM_write_bio_RSAPublicKey(destination, rsa); - - RSA_free(rsa); - - if (result != 1) { - Nan::ThrowError("Public key extraction failed"); - BIO_free(destination); - return; - } - - char *pem; - const auto pemLength = BIO_get_mem_data(destination, &pem); - - info.GetReturnValue().Set(Nan::Encode(pem, pemLength)); - BIO_free(destination); -} - -NAN_METHOD(generate) -{ - const auto exponent = BN_new(); - const auto mem = BIO_new(BIO_s_mem()); - const auto rsa = RSA_new(); - char *data; - long result; - - if (exponent == nullptr || mem == nullptr || rsa == nullptr) { - Nan::ThrowError("Memory allocation failed"); - goto done; - } - - result = BN_set_word(exponent, 65537); - if (result != 1) { - Nan::ThrowError("Exponent setting failed"); - goto done; - } - - result = RSA_generate_key_ex(rsa, 2048, exponent, nullptr); - if (result != 1) { - Nan::ThrowError("Key generation failed"); - goto done; - } - - result = PEM_write_bio_RSAPrivateKey(mem, rsa, NULL, NULL, 0, NULL, NULL); - if (result != 1) { - Nan::ThrowError("Key export failed"); - goto done; - } - - result = BIO_get_mem_data(mem, &data); - info.GetReturnValue().Set(Nan::Encode(data, result)); - -done: - RSA_free(rsa); - BIO_free(mem); - BN_free(exponent); -} - -NAN_MODULE_INIT(InitAll) -{ - Nan::Set(target, Nan::New<v8::String>("extractPublic").ToLocalChecked(), - Nan::GetFunction(Nan::New<v8::FunctionTemplate>(extractPublic)).ToLocalChecked()); - - Nan::Set(target, Nan::New<v8::String>("generate").ToLocalChecked(), - Nan::GetFunction(Nan::New<v8::FunctionTemplate>(generate)).ToLocalChecked()); -} - -NODE_MODULE(crypto_key, InitAll); diff --git a/src/crypto_key.d.ts b/src/crypto_key.d.ts deleted file mode 100644 index 9aa81a687ccdd19768cf798086f27c781c007a0b..0000000000000000000000000000000000000000 --- a/src/crypto_key.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export function extractPublic(keypair: string): string; -export function generate(): string; diff --git a/src/daemons/notes-stats-child.ts b/src/daemons/notes-stats-child.ts index 7f54a36bff7cc34d1c3af21bd5ebd5ce395156af..c491aed4cdc3e815e22223b3f26d28b78f440206 100644 --- a/src/daemons/notes-stats-child.ts +++ b/src/daemons/notes-stats-child.ts @@ -1,26 +1,28 @@ -import Note from '../models/note'; +import { MoreThanOrEqual, getRepository } from 'typeorm'; +import { Note } from '../models/entities/note'; +import { initDb } from '../db/postgre'; const interval = 5000; -async function tick() { - const [all, local] = await Promise.all([Note.count({ - createdAt: { - $gte: new Date(Date.now() - interval) - } - }), Note.count({ - createdAt: { - $gte: new Date(Date.now() - interval) - }, - '_user.host': null - })]); +initDb().then(() => { + const Notes = getRepository(Note); - const stats = { - all, local - }; + async function tick() { + const [all, local] = await Promise.all([Notes.count({ + createdAt: MoreThanOrEqual(new Date(Date.now() - interval)) + }), Notes.count({ + createdAt: MoreThanOrEqual(new Date(Date.now() - interval)), + userHost: null + })]); - process.send(stats); -} + const stats = { + all, local + }; -tick(); + process.send(stats); + } -setInterval(tick, interval); + tick(); + + setInterval(tick, interval); +}); diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts deleted file mode 100644 index f82ced176597c078f919aeea0703a54bb9f876b8..0000000000000000000000000000000000000000 --- a/src/db/mongodb.ts +++ /dev/null @@ -1,39 +0,0 @@ -import config from '../config'; - -const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null; -const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null; - -const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; - -/** - * monk - */ -import mongo from 'monk'; - -const db = mongo(uri); - -export default db; - -/** - * MongoDB native module (officialy) - */ -import * as mongodb from 'mongodb'; - -let mdb: mongodb.Db; - -const nativeDbConn = async (): Promise<mongodb.Db> => { - if (mdb) return mdb; - - const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => { - mongodb.MongoClient.connect(uri, { useNewUrlParser: true }, (e: Error, client: any) => { - if (e) return reject(e); - resolve(client.db(config.mongodb.db)); - }); - }))(); - - mdb = db; - - return db; -}; - -export { nativeDbConn }; diff --git a/src/db/postgre.ts b/src/db/postgre.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc5ee4ce8cb59ed7b8696e22121aa763f97b3ff7 --- /dev/null +++ b/src/db/postgre.ts @@ -0,0 +1,137 @@ +import { createConnection, Logger, getConnection } from 'typeorm'; +import config from '../config'; +import { entities as charts } from '../services/chart/entities'; +import { dbLogger } from './logger'; +import * as highlight from 'cli-highlight'; + +import { Log } from '../models/entities/log'; +import { User } from '../models/entities/user'; +import { DriveFile } from '../models/entities/drive-file'; +import { DriveFolder } from '../models/entities/drive-folder'; +import { AccessToken } from '../models/entities/access-token'; +import { App } from '../models/entities/app'; +import { PollVote } from '../models/entities/poll-vote'; +import { Note } from '../models/entities/note'; +import { NoteReaction } from '../models/entities/note-reaction'; +import { NoteWatching } from '../models/entities/note-watching'; +import { NoteUnread } from '../models/entities/note-unread'; +import { Notification } from '../models/entities/notification'; +import { Meta } from '../models/entities/meta'; +import { Following } from '../models/entities/following'; +import { Instance } from '../models/entities/instance'; +import { Muting } from '../models/entities/muting'; +import { SwSubscription } from '../models/entities/sw-subscription'; +import { Blocking } from '../models/entities/blocking'; +import { UserList } from '../models/entities/user-list'; +import { UserListJoining } from '../models/entities/user-list-joining'; +import { Hashtag } from '../models/entities/hashtag'; +import { NoteFavorite } from '../models/entities/note-favorite'; +import { AbuseUserReport } from '../models/entities/abuse-user-report'; +import { RegistrationTicket } from '../models/entities/registration-tickets'; +import { MessagingMessage } from '../models/entities/messaging-message'; +import { Signin } from '../models/entities/signin'; +import { AuthSession } from '../models/entities/auth-session'; +import { FollowRequest } from '../models/entities/follow-request'; +import { Emoji } from '../models/entities/emoji'; +import { ReversiGame } from '../models/entities/games/reversi/game'; +import { ReversiMatching } from '../models/entities/games/reversi/matching'; +import { UserNotePining } from '../models/entities/user-note-pinings'; +import { UserServiceLinking } from '../models/entities/user-service-linking'; +import { Poll } from '../models/entities/poll'; +import { UserKeypair } from '../models/entities/user-keypair'; +import { UserPublickey } from '../models/entities/user-publickey'; + +const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); + +class MyCustomLogger implements Logger { + private highlight(sql: string) { + return highlight.highlight(sql, { + language: 'sql', ignoreIllegals: true, + }); + } + + public logQuery(query: string, parameters?: any[]) { + sqlLogger.info(this.highlight(query)); + } + + public logQueryError(error: string, query: string, parameters?: any[]) { + sqlLogger.error(this.highlight(query)); + } + + public logQuerySlow(time: number, query: string, parameters?: any[]) { + sqlLogger.warn(this.highlight(query)); + } + + public logSchemaBuild(message: string) { + sqlLogger.info(message); + } + + public log(message: string) { + sqlLogger.info(message); + } + + public logMigration(message: string) { + sqlLogger.info(message); + } +} + +export function initDb(justBorrow = false, sync = false, log = false) { + const enableLogging = log || !['production', 'test'].includes(process.env.NODE_ENV); + + try { + const conn = getConnection(); + return Promise.resolve(conn); + } catch (e) {} + + return createConnection({ + type: 'postgres', + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, + synchronize: process.env.NODE_ENV === 'test' || sync, + dropSchema: process.env.NODE_ENV === 'test' && !justBorrow, + logging: enableLogging, + logger: enableLogging ? new MyCustomLogger() : null, + entities: [ + Meta, + Instance, + App, + AuthSession, + AccessToken, + User, + UserKeypair, + UserPublickey, + UserList, + UserListJoining, + UserNotePining, + UserServiceLinking, + Following, + FollowRequest, + Muting, + Blocking, + Note, + NoteFavorite, + NoteReaction, + NoteWatching, + NoteUnread, + Log, + DriveFile, + DriveFolder, + Poll, + PollVote, + Notification, + Emoji, + Hashtag, + SwSubscription, + AbuseUserReport, + RegistrationTicket, + MessagingMessage, + Signin, + ReversiGame, + ReversiMatching, + ...charts as any + ] + }); +} diff --git a/src/docs/reversi-bot.ja-JP.md b/src/docs/reversi-bot.ja-JP.md index a389ead5717b5a780f08fbbc6666ce9e4ebeec10..b1f759ade8abc6ad11e0b0fa698d3d3f707544f5 100644 --- a/src/docs/reversi-bot.ja-JP.md +++ b/src/docs/reversi-bot.ja-JP.md @@ -42,9 +42,9 @@ Misskeyã®ãƒªãƒãƒ¼ã‚·æ©Ÿèƒ½ã«å¯¾å¿œã—ãŸBotã®é–‹ç™ºæ–¹æ³•ã‚’ã“ã“ã«è¨˜ã— ``` pos = x + (y * mapWidth) ``` -`mapWidth`ã¯ã€ã‚²ãƒ¼ãƒ æƒ…å ±ã®`settings.map`ã‹ã‚‰ã€æ¬¡ã®ã‚ˆã†ã«ã—ã¦è¨ˆç®—ã§ãã¾ã™: +`mapWidth`ã¯ã€ã‚²ãƒ¼ãƒ æƒ…å ±ã®`map`ã‹ã‚‰ã€æ¬¡ã®ã‚ˆã†ã«ã—ã¦è¨ˆç®—ã§ãã¾ã™: ``` -mapWidth = settings.map[0].length +mapWidth = map[0].length ``` ### Pos ã‹ã‚‰ X,Y座標 ã«å¤‰æ›ã™ã‚‹ @@ -54,7 +54,7 @@ y = Math.floor(pos / mapWidth) ``` ## ãƒžãƒƒãƒ—æƒ…å ± -ãƒžãƒƒãƒ—æƒ…å ±ã¯ã€ã‚²ãƒ¼ãƒ æƒ…å ±ã®`settings.map`ã«å…¥ã£ã¦ã„ã¾ã™ã€‚ +ãƒžãƒƒãƒ—æƒ…å ±ã¯ã€ã‚²ãƒ¼ãƒ æƒ…å ±ã®`map`ã«å…¥ã£ã¦ã„ã¾ã™ã€‚ æ–‡å—列ã®é…列ã«ãªã£ã¦ãŠã‚Šã€ã²ã¨ã¤ã²ã¨ã¤ã®æ–‡å—ãŒãƒžã‚¹æƒ…å ±ã‚’è¡¨ã—ã¦ã„ã¾ã™ã€‚ ãれをもã¨ã«ãƒžãƒƒãƒ—ã®ãƒ‡ã‚¶ã‚¤ãƒ³ã‚’知る事ãŒå‡ºæ¥ã¾ã™: * `(スペース)` ... マス無㗠diff --git a/src/docs/stream.ja-JP.md b/src/docs/stream.ja-JP.md index 0e9afa73329e5911a0b9a468f02837a7d2162f9b..8a6bf634646ed6d90e22c765275727108ece3f0f 100644 --- a/src/docs/stream.ja-JP.md +++ b/src/docs/stream.ja-JP.md @@ -339,7 +339,7 @@ Misskeyã¯æŠ•ç¨¿ã®ã‚ャプãƒãƒ£ã¨å‘¼ã°ã‚Œã‚‹ä»•çµ„ã¿ã‚’æä¾›ã—ã¦ã„ã¾ #### `note` ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインã«æ–°ã—ã„投稿ãŒæµã‚Œã¦ããŸã¨ãã«ç™ºç”Ÿã™ã‚‹ã‚¤ãƒ™ãƒ³ãƒˆã§ã™ã€‚ -## `hybridTimeline` +## `socialTimeline` ソーシャルタイムラインã®æŠ•ç¨¿æƒ…å ±ãŒæµã‚Œã¦ãã¾ã™ã€‚ã“ã®ãƒãƒ£ãƒ³ãƒãƒ«ã«ãƒ‘ラメータã¯ã‚ã‚Šã¾ã›ã‚“。 ### æµã‚Œã¦ãるイベント一覧 diff --git a/src/index.ts b/src/index.ts index e55ba5115d54f8f245bf83af0aa919d1113525d2..c4a1088c2e238ec8c8872e7a26dd49c13cf79a22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,281 +6,8 @@ Error.stackTraceLimit = Infinity; require('events').EventEmitter.defaultMaxListeners = 128; -import * as os from 'os'; -import * as cluster from 'cluster'; -import chalk from 'chalk'; -import * as portscanner from 'portscanner'; -import * as isRoot from 'is-root'; -import Xev from 'xev'; +import boot from './boot'; -import Logger from './services/logger'; -import serverStats from './daemons/server-stats'; -import notesStats from './daemons/notes-stats'; -import queueStats from './daemons/queue-stats'; -import loadConfig from './config/load'; -import { Config } from './config/types'; -import { lessThan } from './prelude/array'; -import * as pkg from '../package.json'; -import { program } from './argv'; -import { checkMongoDB } from './misc/check-mongodb'; -import { showMachineInfo } from './misc/show-machine-info'; - -const logger = new Logger('core', 'cyan'); -const bootLogger = logger.createSubLogger('boot', 'magenta', false); -const clusterLogger = logger.createSubLogger('cluster', 'orange'); -const ev = new Xev(); - -/** - * Init process - */ -function main() { - process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`; - - if (program.onlyQueue) { - queueMain(); - return; - } - - if (cluster.isMaster || program.disableClustering) { - masterMain(); - - if (cluster.isMaster) { - ev.mount(); - } - - if (program.daemons) { - serverStats(); - notesStats(); - queueStats(); - } - } - - if (cluster.isWorker || program.disableClustering) { - workerMain(); - } -} - -function greet() { - if (!program.quiet) { - //#region Misskey logo - const v = `v${pkg.version}`; - console.log(' _____ _ _ '); - console.log(' | |_|___ ___| |_ ___ _ _ '); - console.log(' | | | | |_ -|_ -| \'_| -_| | |'); - console.log(' |_|_|_|_|___|___|_,_|___|_ |'); - console.log(' ' + chalk.gray(v) + (' |___|\n'.substr(v.length))); - //#endregion - - console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.'); - console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo')); - - console.log(''); - console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`); - } - - bootLogger.info('Welcome to Misskey!'); - bootLogger.info(`Misskey v${pkg.version}`, null, true); -} - -/** - * Init master process - */ -async function masterMain() { - greet(); - - let config: Config; - - try { - // initialize app - config = await init(); - - if (config.port == null) { - bootLogger.error('The port is not configured. Please configure port.', null, true); - process.exit(1); - } - - if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) { - bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true); - process.exit(1); - } - - if (!await isPortAvailable(config.port)) { - bootLogger.error(`Port ${config.port} is already in use`, null, true); - process.exit(1); - } - } catch (e) { - bootLogger.error('Fatal error occurred during initialization', null, true); - process.exit(1); - } - - bootLogger.succ('Misskey initialized'); - - if (!program.disableClustering) { - await spawnWorkers(config.clusterLimit); - } - - bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); -} - -/** - * Init worker process - */ -async function workerMain() { - // start server - await require('./server').default(); - - // start job queue - require('./queue').default(); - - if (cluster.isWorker) { - // Send a 'ready' message to parent process - process.send('ready'); - } -} - -async function queueMain() { - greet(); - - try { - // initialize app - await init(); - } catch (e) { - bootLogger.error('Fatal error occurred during initialization', null, true); - process.exit(1); - } - - bootLogger.succ('Misskey initialized'); - - // start processor - require('./queue').default(); - - bootLogger.succ('Queue started', null, true); -} - -const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10)); -const requiredNodejsVersion = [10, 0, 0]; -const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion); - -function isWellKnownPort(port: number): boolean { - return port < 1024; -} - -async function isPortAvailable(port: number): Promise<boolean> { - return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed'; -} - -function showEnvironment(): void { - const env = process.env.NODE_ENV; - const logger = bootLogger.createSubLogger('env'); - logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`); - - if (env !== 'production') { - logger.warn('The environment is not in production mode.'); - logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true); - } - - logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`); +export default function() { + return boot(); } - -/** - * Init app - */ -async function init(): Promise<Config> { - showEnvironment(); - - const nodejsLogger = bootLogger.createSubLogger('nodejs'); - - nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`); - - if (!satisfyNodejsVersion) { - nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true); - process.exit(1); - } - - await showMachineInfo(bootLogger); - - const configLogger = bootLogger.createSubLogger('config'); - let config; - - try { - config = loadConfig(); - } catch (exception) { - if (typeof exception === 'string') { - configLogger.error(exception); - process.exit(1); - } - if (exception.code === 'ENOENT') { - configLogger.error('Configuration file not found', null, true); - process.exit(1); - } - throw exception; - } - - configLogger.succ('Loaded'); - - // Try to connect to MongoDB - try { - await checkMongoDB(config, bootLogger); - } catch (e) { - bootLogger.error('Cannot connect to database', null, true); - process.exit(1); - } - - return config; -} - -async function spawnWorkers(limit: number = Infinity) { - const workers = Math.min(limit, os.cpus().length); - bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); - await Promise.all([...Array(workers)].map(spawnWorker)); - bootLogger.succ('All workers started'); -} - -function spawnWorker(): Promise<void> { - return new Promise(res => { - const worker = cluster.fork(); - worker.on('message', message => { - if (message !== 'ready') return; - res(); - }); - }); -} - -//#region Events - -// Listen new workers -cluster.on('fork', worker => { - clusterLogger.debug(`Process forked: [${worker.id}]`); -}); - -// Listen online workers -cluster.on('online', worker => { - clusterLogger.debug(`Process is now online: [${worker.id}]`); -}); - -// Listen for dying workers -cluster.on('exit', worker => { - // Replace the dead worker, - // we're not sentimental - clusterLogger.error(chalk.red(`[${worker.id}] died :(`)); - cluster.fork(); -}); - -// Display detail of unhandled promise rejection -if (!program.quiet) { - process.on('unhandledRejection', console.dir); -} - -// Display detail of uncaught exception -process.on('uncaughtException', err => { - logger.error(err); -}); - -// Dying away... -process.on('exit', code => { - logger.info(`The process is going to exit with code ${code}`); -}); - -//#endregion - -main(); diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 0000000000000000000000000000000000000000..69c117c140b2cd3120e8b43594df0e9d55480bf5 --- /dev/null +++ b/src/init.ts @@ -0,0 +1,16 @@ +import { initDb } from './db/postgre'; + +async function main() { + try { + console.log('Connecting database...'); + await initDb(false, true, true); + } catch (e) { + console.error('Cannot connect to database', null, true); + console.error(e); + process.exit(1); + } + + console.log('Done :)'); +} + +main(); diff --git a/src/mfm/toHtml.ts b/src/mfm/toHtml.ts index c676ae6ffcd16739b868cda715ff27106fde2550..3cd79876247e37a414cda802c93a623206af6fcd 100644 --- a/src/mfm/toHtml.ts +++ b/src/mfm/toHtml.ts @@ -1,10 +1,10 @@ import { JSDOM } from 'jsdom'; import config from '../config'; -import { INote } from '../models/note'; import { intersperse } from '../prelude/array'; import { MfmForest, MfmTree } from './prelude'; +import { IMentionedRemoteUsers } from '../models/entities/note'; -export function toHtml(tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) { +export function toHtml(tokens: MfmForest, mentionedRemoteUsers: IMentionedRemoteUsers = []) { if (tokens == null) { return null; } diff --git a/src/misc/aid.ts b/src/misc/aid.ts new file mode 100644 index 0000000000000000000000000000000000000000..aba53ed5fba533ab8edf1d9898d0ba95eb4f8d5d --- /dev/null +++ b/src/misc/aid.ts @@ -0,0 +1,26 @@ +// AID +// é•·ã•8ã®[2000å¹´1月1æ—¥ã‹ã‚‰ã®çµŒéŽãƒŸãƒªç§’ã‚’base36ã§ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã—ãŸã‚‚ã®] + é•·ã•nã®[ランダムãªæ–‡å—列] + +const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; +const TIME2000 = 946684800000; + +function getTime(time: number) { + time = time - TIME2000; + if (time < 0) time = 0; + + return time.toString(36); +} + +function getRandom(length: number) { + let str = ''; + + for (let i = 0; i < length; i++) { + str += CHARS[Math.floor(Math.random() * CHARS.length)]; + } + + return str; +} + +export function genAid(date: Date, rand: number): string { + return getTime(date.getTime()).padStart(8, CHARS[0]) + getRandom(rand); +} diff --git a/src/misc/aidc.ts b/src/misc/aidc.ts new file mode 100644 index 0000000000000000000000000000000000000000..75168ac3072c61a3cf8ee2db14497a13204daca0 --- /dev/null +++ b/src/misc/aidc.ts @@ -0,0 +1,26 @@ +// AID(Cheep) +// é•·ã•6ã®[2000å¹´1月1æ—¥ã‹ã‚‰ã®çµŒéŽç§’ã‚’base36ã§ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã—ãŸã‚‚ã®] + é•·ã•3ã®[ランダムãªæ–‡å—列] + +const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; +const TIME2000 = 946684800000; + +function getTime(time: number) { + time = time - TIME2000; + if (time < 0) time = 0; + time = Math.floor(time / 1000); + return time.toString(36); +} + +function getRandom() { + let str = ''; + + for (let i = 0; i < 3; i++) { + str += CHARS[Math.floor(Math.random() * CHARS.length)]; + } + + return str; +} + +export function genAidc(date: Date): string { + return getTime(date.getTime()).padStart(6, CHARS[0]) + getRandom(); +} diff --git a/src/misc/cafy-id.ts b/src/misc/cafy-id.ts index bc8fe4ea2bf8cfde3b59503b1296f211de6dd36a..39886611e16921bdbef2fef4c2d1575b0dad0b60 100644 --- a/src/misc/cafy-id.ts +++ b/src/misc/cafy-id.ts @@ -1,38 +1,13 @@ -import * as mongo from 'mongodb'; import { Context } from 'cafy'; -import isObjectId from './is-objectid'; -export const isAnId = (x: any) => mongo.ObjectID.isValid(x); -export const isNotAnId = (x: any) => !isAnId(x); -export const transform = (x: string | mongo.ObjectID): mongo.ObjectID => { - if (x === undefined) return undefined; - if (x === null) return null; - - if (isAnId(x) && !isObjectId(x)) { - return new mongo.ObjectID(x); - } else { - return x as mongo.ObjectID; - } -}; -export const transformMany = (xs: (string | mongo.ObjectID)[]): mongo.ObjectID[] => { - if (xs == null) return null; - - return xs.map(x => transform(x)); -}; - -export type ObjectId = mongo.ObjectID; - -/** - * ID - */ -export default class ID<Maybe = string> extends Context<string | Maybe> { +export class ID<Maybe = string> extends Context<string | (Maybe extends {} ? string : Maybe)> { public readonly name = 'ID'; constructor(optional = false, nullable = false) { super(optional, nullable); this.push((v: any) => { - if (!isObjectId(v) && isNotAnId(v)) { + if (typeof v !== 'string') { return new Error('must-be-an-id'); } return true; diff --git a/src/misc/check-mongodb.ts b/src/misc/check-mongodb.ts deleted file mode 100644 index 8e03db5d4205de35866f12169f0b75ccc2ea3f25..0000000000000000000000000000000000000000 --- a/src/misc/check-mongodb.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { nativeDbConn } from '../db/mongodb'; -import { Config } from '../config/types'; -import Logger from '../services/logger'; -import { lessThan } from '../prelude/array'; - -const requiredMongoDBVersion = [3, 6]; - -export function checkMongoDB(config: Config, logger: Logger) { - return new Promise((res, rej) => { - const mongoDBLogger = logger.createSubLogger('db'); - const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null; - const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null; - const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; - mongoDBLogger.info(`Connecting to ${uri} ...`); - - nativeDbConn().then(db => { - mongoDBLogger.succ('Connectivity confirmed'); - - db.admin().serverInfo().then(x => { - const version = x.version as string; - mongoDBLogger.info(`Version: ${version}`); - if (lessThan(version.split('.').map(x => parseInt(x, 10)), requiredMongoDBVersion)) { - mongoDBLogger.error(`MongoDB version is less than ${requiredMongoDBVersion.join('.')}. Please upgrade it.`); - rej('outdated version'); - } else { - res(); - } - }).catch(err => { - mongoDBLogger.error(`Failed to fetch server info: ${err.message}`); - rej(err); - }); - }).catch(err => { - mongoDBLogger.error(err.message); - rej(err); - }); - }); -} diff --git a/src/misc/fetch-meta.ts b/src/misc/fetch-meta.ts index 3584a819bf20d8c8ce4a58158ecb071772990293..d1483e9edbf94b7d0a2c1e9dfba080517f67f541 100644 --- a/src/misc/fetch-meta.ts +++ b/src/misc/fetch-meta.ts @@ -1,32 +1,15 @@ -import Meta, { IMeta } from '../models/meta'; +import { Meta } from '../models/entities/meta'; +import { Metas } from '../models'; +import { genId } from './gen-id'; -const defaultMeta: any = { - name: 'Misskey', - maintainer: {}, - langs: [], - cacheRemoteFiles: true, - localDriveCapacityMb: 256, - remoteDriveCapacityMb: 8, - hidedTags: [], - stats: { - originalNotesCount: 0, - originalUsersCount: 0 - }, - maxNoteTextLength: 1000, - enableEmojiReaction: true, - enableTwitterIntegration: false, - enableGithubIntegration: false, - enableDiscordIntegration: false, - enableExternalUserRecommendation: false, - externalUserRecommendationEngine: 'https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}', - externalUserRecommendationTimeout: 300000, - mascotImageUrl: '/assets/ai.png', - errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png', - enableServiceWorker: false -}; - -export default async function(): Promise<IMeta> { - const meta = await Meta.findOne({}); - - return Object.assign({}, defaultMeta, meta); +export default async function(): Promise<Meta> { + const meta = await Metas.findOne(); + if (meta) { + return meta; + } else { + return Metas.save({ + id: genId(), + hiddenTags: [] + } as Meta); + } } diff --git a/src/misc/fetch-proxy-account.ts b/src/misc/fetch-proxy-account.ts new file mode 100644 index 0000000000000000000000000000000000000000..d60fa9b313c1d10dd0bba7c232285133327628f5 --- /dev/null +++ b/src/misc/fetch-proxy-account.ts @@ -0,0 +1,8 @@ +import fetchMeta from './fetch-meta'; +import { ILocalUser } from '../models/entities/user'; +import { Users } from '../models'; + +export async function fetchProxyAccount(): Promise<ILocalUser> { + const meta = await fetchMeta(); + return await Users.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser; +} diff --git a/src/misc/gen-id.ts b/src/misc/gen-id.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe901b1fe7bddc876d6ecf85e3e931b1ef086083 --- /dev/null +++ b/src/misc/gen-id.ts @@ -0,0 +1,22 @@ +import { ulid } from 'ulid'; +import { genAid } from './aid'; +import { genAidc } from './aidc'; +import { genObjectId } from './object-id'; +import config from '../config'; + +const metohd = config.id.toLowerCase(); + +export function genId(date?: Date): string { + if (!date || (date > new Date())) date = new Date(); + + switch (metohd) { + case 'aidc': return genAidc(date); + case 'aid1': return genAid(date, 1); + case 'aid2': return genAid(date, 2); + case 'aid3': return genAid(date, 3); + case 'aid4': return genAid(date, 4); + case 'ulid': return ulid(date.getTime()); + case 'objectid': return genObjectId(date); + default: throw 'unknown id generation method'; + } +} diff --git a/src/misc/get-drive-file-url.ts b/src/misc/get-drive-file-url.ts deleted file mode 100644 index 067db8a5d03429d5a275f68e8799a4953877e342..0000000000000000000000000000000000000000 --- a/src/misc/get-drive-file-url.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { IDriveFile } from '../models/drive-file'; -import config from '../config'; - -export default function(file: IDriveFile, thumbnail = false): string { - if (file == null) return null; - - const isImage = file.contentType && file.contentType.startsWith('image/'); - - if (file.metadata.withoutChunks) { - if (thumbnail) { - return file.metadata.thumbnailUrl || file.metadata.webpublicUrl || (isImage ? file.metadata.url : null); - } else { - return file.metadata.webpublicUrl || file.metadata.url; - } - } else { - if (thumbnail) { - return `${config.driveUrl}/${file._id}?thumbnail`; - } else { - return `${config.driveUrl}/${file._id}?web`; - } - } -} - -export function getOriginalUrl(file: IDriveFile) { - if (file.metadata && file.metadata.url) { - return file.metadata.url; - } - - const accessKey = file.metadata ? file.metadata.accessKey : null; - return `${config.driveUrl}/${file._id}${accessKey ? '?original=' + accessKey : ''}`; -} diff --git a/src/misc/get-notification-summary.ts b/src/misc/get-notification-summary.ts index 71d4973ce9e53be2c5fefd0442785ddf04877e09..b20711c60580c4a9d02559971c03fe0e3871af4b 100644 --- a/src/misc/get-notification-summary.ts +++ b/src/misc/get-notification-summary.ts @@ -20,7 +20,7 @@ export default function(notification: any): string { return `引用ã•ã‚Œã¾ã—ãŸ:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}ã€`; case 'reaction': return `リアクションã•ã‚Œã¾ã—ãŸ:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getNoteSummary(notification.note)}ã€`; - case 'poll_vote': + case 'pollVote': return `投票ã•ã‚Œã¾ã—ãŸ:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}ã€`; default: return `<ä¸æ˜Žãªé€šçŸ¥ã‚¿ã‚¤ãƒ—: ${notification.type}>`; diff --git a/src/misc/get-user-name.ts b/src/misc/get-user-name.ts index eab9f87ef03a207acd6db93cff266f7b9d86620a..b6b45118b0cd89f4ec1249561e6c587b05c08960 100644 --- a/src/misc/get-user-name.ts +++ b/src/misc/get-user-name.ts @@ -1,5 +1,5 @@ -import { IUser } from '../models/user'; +import { User } from '../models/entities/user'; -export default function(user: IUser): string { +export default function(user: User): string { return user.name || user.username; } diff --git a/src/misc/get-user-summary.ts b/src/misc/get-user-summary.ts index 09cf5ebadc008cc8409d49a4a846b6e089222eea..9cb06f43ce015ee1b44e38d402af4a2c43766dd7 100644 --- a/src/misc/get-user-summary.ts +++ b/src/misc/get-user-summary.ts @@ -1,17 +1,18 @@ -import { IUser, isLocalUser } from '../models/user'; import getAcct from './acct/render'; import getUserName from './get-user-name'; +import { User } from '../models/entities/user'; +import { Users } from '../models'; /** * ユーザーを表ã™æ–‡å—列をå–å¾—ã—ã¾ã™ã€‚ * @param user ユーザー */ -export default function(user: IUser): string { +export default function(user: User): string { let string = `${getUserName(user)} (@${getAcct(user)})\n` + `${user.notesCount}投稿ã€${user.followingCount}フォãƒãƒ¼ã€${user.followersCount}フォãƒãƒ¯ãƒ¼\n`; - if (isLocalUser(user)) { - string += `å ´æ‰€: ${user.profile.location}ã€èª•ç”Ÿæ—¥: ${user.profile.birthday}\n`; + if (Users.isLocalUser(user)) { + string += `å ´æ‰€: ${user.location}ã€èª•ç”Ÿæ—¥: ${user.birthday}\n`; } return string + `「${user.description}ã€`; diff --git a/src/misc/is-duplicate-key-value-error.ts b/src/misc/is-duplicate-key-value-error.ts new file mode 100644 index 0000000000000000000000000000000000000000..23d8ceb1b7592ed722080f64b3c33c190395c039 --- /dev/null +++ b/src/misc/is-duplicate-key-value-error.ts @@ -0,0 +1,3 @@ +export function isDuplicateKeyValueError(e: Error): boolean { + return e.message.startsWith('duplicate key value'); +} diff --git a/src/misc/is-objectid.ts b/src/misc/is-objectid.ts deleted file mode 100644 index a77c4ee2d578c6c960cae2bf4c1a804a58238842..0000000000000000000000000000000000000000 --- a/src/misc/is-objectid.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ObjectID } from 'mongodb'; - -export default function(x: any): x is ObjectID { - return x && typeof x === 'object' && (x.hasOwnProperty('toHexString') || x.hasOwnProperty('_bsontype')); -} diff --git a/src/misc/is-quote.ts b/src/misc/is-quote.ts index a99b8f64348541131eeca0f3b7d3138e94ee234a..0a2a72f4a0ebcc08d17df2fe36bc33f51a5fa347 100644 --- a/src/misc/is-quote.ts +++ b/src/misc/is-quote.ts @@ -1,5 +1,5 @@ -import { INote } from '../models/note'; +import { Note } from '../models/entities/note'; -export default function(note: INote): boolean { - return note.renoteId != null && (note.text != null || note.poll != null || (note.fileIds != null && note.fileIds.length > 0)); +export default function(note: Note): boolean { + return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); } diff --git a/src/misc/nyaize.ts b/src/misc/nyaize.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b06300eab5dfa58d65f8df5f5937341f418c778 --- /dev/null +++ b/src/misc/nyaize.ts @@ -0,0 +1,9 @@ +export function nyaize(text: string): string { + return text + // ja-JP + .replace(/ãª/g, 'ã«ã‚ƒ').replace(/ナ/g, 'ニャ').replace(/ï¾…/g, 'ニャ') + // ko-KR + .replace(/[나-낳]/g, (match: string) => String.fromCharCode( + match.codePointAt(0) + 'ëƒ'.charCodeAt(0) - '나'.charCodeAt(0) + )); +} diff --git a/src/misc/object-id.ts b/src/misc/object-id.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f6422e5e43d1bc4686b88437c6653e095baee33 --- /dev/null +++ b/src/misc/object-id.ts @@ -0,0 +1,26 @@ +const CHARS = '0123456789abcdef'; + +function getTime(time: number) { + if (time < 0) time = 0; + if (time === 0) { + return CHARS[0]; + } + + time = Math.floor(time / 1000); + + return time.toString(16); +} + +function getRandom() { + let str = ''; + + for (let i = 0; i < 16; i++) { + str += CHARS[Math.floor(Math.random() * CHARS.length)]; + } + + return str; +} + +export function genObjectId(date: Date): string { + return getTime(date.getTime()) + getRandom(); +} diff --git a/src/misc/reaction-lib.ts b/src/misc/reaction-lib.ts index 7e5a1b0bc0b6a971bf3d355255ed71e431f52f59..20051f028002156948c3cc269ce4b20876221529 100644 --- a/src/misc/reaction-lib.ts +++ b/src/misc/reaction-lib.ts @@ -1,6 +1,6 @@ -import Emoji from '../models/emoji'; import { emojiRegex } from './emoji-regex'; import fetchMeta from './fetch-meta'; +import { Emojis } from '../models'; const basic10: Record<string, string> = { 'ðŸ‘': 'like', @@ -49,7 +49,7 @@ export async function toDbReaction(reaction: string, enableEmoji = true): Promis const custom = reaction.match(/^:([\w+-]+):$/); if (custom) { - const emoji = await Emoji.findOne({ + const emoji = await Emojis.findOne({ host: null, name: custom[1], }); diff --git a/src/misc/should-mute-this-note.ts b/src/misc/should-mute-this-note.ts index b1d29c6a28d558ffda5c7ebfe1bfc782316a0475..8f606a2943937522ad1e95964530d0a9d2f2de5e 100644 --- a/src/misc/should-mute-this-note.ts +++ b/src/misc/should-mute-this-note.ts @@ -1,20 +1,13 @@ -import * as mongo from 'mongodb'; -import isObjectId from './is-objectid'; - -function toString(id: any) { - return isObjectId(id) ? (id as mongo.ObjectID).toHexString() : id; -} - export default function(note: any, mutedUserIds: string[]): boolean { - if (mutedUserIds.includes(toString(note.userId))) { + if (mutedUserIds.includes(note.userId)) { return true; } - if (note.reply != null && mutedUserIds.includes(toString(note.reply.userId))) { + if (note.reply != null && mutedUserIds.includes(note.reply.userId)) { return true; } - if (note.renote != null && mutedUserIds.includes(toString(note.renote.userId))) { + if (note.renote != null && mutedUserIds.includes(note.renote.userId)) { return true; } diff --git a/src/models/abuse-user-report.ts b/src/models/abuse-user-report.ts deleted file mode 100644 index f3900d348dc1d647d6af190a0af8d1255e9d38f1..0000000000000000000000000000000000000000 --- a/src/models/abuse-user-report.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { pack as packUser } from './user'; - -const AbuseUserReport = db.get<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); -}); diff --git a/src/models/access-token.ts b/src/models/access-token.ts deleted file mode 100644 index 66c5c91c0bb81df0970dea8e314c943e87a7a571..0000000000000000000000000000000000000000 --- a/src/models/access-token.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const AccessToken = db.get<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; -}; diff --git a/src/models/app.ts b/src/models/app.ts deleted file mode 100644 index 45d50bccda87b444b14830172b6eff537eff1f11..0000000000000000000000000000000000000000 --- a/src/models/app.ts +++ /dev/null @@ -1,102 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import AccessToken from './access-token'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import config from '../config'; -import { dbLogger } from '../db/logger'; - -const App = db.get<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); -}); diff --git a/src/models/auth-session.ts b/src/models/auth-session.ts deleted file mode 100644 index 428c707470d1bd812024ca9828d110a9f293ca7d..0000000000000000000000000000000000000000 --- a/src/models/auth-session.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { pack as packApp } from './app'; - -const AuthSession = db.get<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); -}); diff --git a/src/models/blocking.ts b/src/models/blocking.ts deleted file mode 100644 index 4bdaa741e95828e872a0097ba4bf2f90f0e0e06b..0000000000000000000000000000000000000000 --- a/src/models/blocking.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import * as deepcopy from 'deepcopy'; -import { pack as packUser, IUser } from './user'; - -const Blocking = db.get<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); -}); diff --git a/src/models/drive-file-thumbnail.ts b/src/models/drive-file-thumbnail.ts deleted file mode 100644 index bdb3d010e641d3cec856e667922512ebf1c58cbf..0000000000000000000000000000000000000000 --- a/src/models/drive-file-thumbnail.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as mongo from 'mongodb'; -import monkDb, { nativeDbConn } from '../db/mongodb'; - -const DriveFileThumbnail = monkDb.get<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; -}; diff --git a/src/models/drive-file-webpublic.ts b/src/models/drive-file-webpublic.ts deleted file mode 100644 index d087c355d39a8bb38ceb5d37a4d375a502d26bb4..0000000000000000000000000000000000000000 --- a/src/models/drive-file-webpublic.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as mongo from 'mongodb'; -import monkDb, { nativeDbConn } from '../db/mongodb'; - -const DriveFileWebpublic = monkDb.get<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; -}; diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts deleted file mode 100644 index c31e9a709f2ceed94f042d71a42474d03982f877..0000000000000000000000000000000000000000 --- a/src/models/drive-file.ts +++ /dev/null @@ -1,232 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import { pack as packFolder } from './drive-folder'; -import { pack as packUser } from './user'; -import monkDb, { nativeDbConn } from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import getDriveFileUrl, { getOriginalUrl } from '../misc/get-drive-file-url'; -import { dbLogger } from '../db/logger'; - -const DriveFile = monkDb.get<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); -}); diff --git a/src/models/drive-folder.ts b/src/models/drive-folder.ts deleted file mode 100644 index b0f6e4273e2c6372a03bfb9b7460fece32366439..0000000000000000000000000000000000000000 --- a/src/models/drive-folder.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import DriveFile from './drive-file'; - -const DriveFolder = db.get<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); -}); diff --git a/src/models/emoji.ts b/src/models/emoji.ts deleted file mode 100644 index cbf939222efb96ceade1a6b643b8fc52256f105e..0000000000000000000000000000000000000000 --- a/src/models/emoji.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const Emoji = db.get<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; -}; diff --git a/src/models/entities/abuse-user-report.ts b/src/models/entities/abuse-user-report.ts new file mode 100644 index 0000000000000000000000000000000000000000..43ab56023a2e80bd85224e21085e50ed320d23c4 --- /dev/null +++ b/src/models/entities/abuse-user-report.ts @@ -0,0 +1,41 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'reporterId'], { unique: true }) +export class AbuseUserReport { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the AbuseUserReport.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public reporterId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public reporter: User | null; + + @Column('varchar', { + length: 512, + }) + public comment: string; +} diff --git a/src/models/entities/access-token.ts b/src/models/entities/access-token.ts new file mode 100644 index 0000000000000000000000000000000000000000..d08930cf5ab6c6282c28ded52f0f5682609eb804 --- /dev/null +++ b/src/models/entities/access-token.ts @@ -0,0 +1,45 @@ +import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn, RelationId } from 'typeorm'; +import { User } from './user'; +import { App } from './app'; +import { id } from '../id'; + +@Entity() +export class AccessToken { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the AccessToken.' + }) + public createdAt: Date; + + @Index() + @Column('varchar', { + length: 128 + }) + public token: string; + + @Index() + @Column('varchar', { + length: 128 + }) + public hash: string; + + @RelationId((self: AccessToken) => self.user) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public appId: App['id']; + + @ManyToOne(type => App, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public app: App | null; +} diff --git a/src/models/entities/app.ts b/src/models/entities/app.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0c89000fce2d3b317227d8b636d108a5164e26b --- /dev/null +++ b/src/models/entities/app.ts @@ -0,0 +1,60 @@ +import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class App { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the App.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The owner ID.' + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL', + nullable: true, + }) + public user: User | null; + + @Index() + @Column('varchar', { + length: 64, + comment: 'The secret key of the App.' + }) + public secret: string; + + @Column('varchar', { + length: 128, + comment: 'The name of the App.' + }) + public name: string; + + @Column('varchar', { + length: 512, + comment: 'The description of the App.' + }) + public description: string; + + @Column('varchar', { + length: 64, array: true, + comment: 'The permission of the App.' + }) + public permission: string[]; + + @Column('varchar', { + length: 256, nullable: true, + comment: 'The callbackUrl of the App.' + }) + public callbackUrl: string | null; +} diff --git a/src/models/entities/auth-session.ts b/src/models/entities/auth-session.ts new file mode 100644 index 0000000000000000000000000000000000000000..83f83656301381d4adac9a9b3872db0f759252df --- /dev/null +++ b/src/models/entities/auth-session.ts @@ -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; +} diff --git a/src/models/entities/blocking.ts b/src/models/entities/blocking.ts new file mode 100644 index 0000000000000000000000000000000000000000..48487cb086bfccec019ca17860349197abbc308a --- /dev/null +++ b/src/models/entities/blocking.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['blockerId', 'blockeeId'], { unique: true }) +export class Blocking { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Blocking.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The blockee user ID.' + }) + public blockeeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public blockee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The blocker user ID.' + }) + public blockerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public blocker: User | null; +} diff --git a/src/models/entities/drive-file.ts b/src/models/entities/drive-file.ts new file mode 100644 index 0000000000000000000000000000000000000000..a8f8c69e56bc251913e5aabaa97214f883206ce9 --- /dev/null +++ b/src/models/entities/drive-file.ts @@ -0,0 +1,154 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { DriveFolder } from './drive-folder'; +import { id } from '../id'; + +@Entity() +export class DriveFile { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the DriveFile.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The owner ID.' + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: 'The host of owner. It will be null if the user in local.' + }) + public userHost: string | null; + + @Index() + @Column('varchar', { + length: 32, + comment: 'The MD5 hash of the DriveFile.' + }) + public md5: string; + + @Column('varchar', { + length: 256, + comment: 'The file name of the DriveFile.' + }) + public name: string; + + @Index() + @Column('varchar', { + length: 128, + comment: 'The content type (MIME) of the DriveFile.' + }) + public type: string; + + @Column('integer', { + comment: 'The file size (bytes) of the DriveFile.' + }) + public size: number; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The comment of the DriveFile.' + }) + public comment: string | null; + + @Column('jsonb', { + default: {}, + comment: 'The any properties of the DriveFile. For example, it includes image width/height.' + }) + public properties: Record<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; +} diff --git a/src/models/entities/drive-folder.ts b/src/models/entities/drive-folder.ts new file mode 100644 index 0000000000000000000000000000000000000000..a80d075855a24861241a4b3f958e7c13db19ce52 --- /dev/null +++ b/src/models/entities/drive-folder.ts @@ -0,0 +1,49 @@ +import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class DriveFolder { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the DriveFolder.' + }) + public createdAt: Date; + + @Column('varchar', { + length: 128, + comment: 'The name of the DriveFolder.' + }) + public name: string; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The owner ID.' + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.' + }) + public parentId: DriveFolder['id'] | null; + + @ManyToOne(type => DriveFolder, { + onDelete: 'SET NULL' + }) + @JoinColumn() + public parent: DriveFolder | null; +} diff --git a/src/models/entities/emoji.ts b/src/models/entities/emoji.ts new file mode 100644 index 0000000000000000000000000000000000000000..da04da897eb52c4d57aa92a6d4feaaed82a8a7d9 --- /dev/null +++ b/src/models/entities/emoji.ts @@ -0,0 +1,46 @@ +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from '../id'; + +@Entity() +@Index(['name', 'host'], { unique: true }) +export class Emoji { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + nullable: true + }) + public updatedAt: Date | null; + + @Index() + @Column('varchar', { + length: 128 + }) + public name: string; + + @Index() + @Column('varchar', { + length: 128, nullable: true + }) + public host: string | null; + + @Column('varchar', { + length: 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[]; +} diff --git a/src/models/entities/follow-request.ts b/src/models/entities/follow-request.ts new file mode 100644 index 0000000000000000000000000000000000000000..80a71fe482ec48a6a819c68ae399cdf0003b4958 --- /dev/null +++ b/src/models/entities/follow-request.ts @@ -0,0 +1,85 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['followerId', 'followeeId'], { unique: true }) +export class FollowRequest { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the FollowRequest.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The followee user ID.' + }) + public followeeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public followee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The follower user ID.' + }) + public followerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public follower: User | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'id of Follow Activity.' + }) + public requestId: string | null; + + //#region Denormalized fields + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public followerHost: string | null; + + @Column('varchar', { + length: 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 +} diff --git a/src/models/entities/following.ts b/src/models/entities/following.ts new file mode 100644 index 0000000000000000000000000000000000000000..963873d112d6d66476291fb5452a365aa5738196 --- /dev/null +++ b/src/models/entities/following.ts @@ -0,0 +1,80 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['followerId', 'followeeId'], { unique: true }) +export class Following { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Following.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The followee user ID.' + }) + public followeeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public followee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The follower user ID.' + }) + public followerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public follower: User | null; + + //#region Denormalized fields + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public followerHost: string | null; + + @Column('varchar', { + length: 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 +} diff --git a/src/models/entities/games/reversi/game.ts b/src/models/entities/games/reversi/game.ts new file mode 100644 index 0000000000000000000000000000000000000000..9deacaf5c6dc3f90ef54457cf708c03af9ff52dc --- /dev/null +++ b/src/models/entities/games/reversi/game.ts @@ -0,0 +1,133 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from '../../user'; +import { id } from '../../../id'; + +@Entity() +export class ReversiGame { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the ReversiGame.' + }) + public createdAt: Date; + + @Column('timestamp with time zone', { + nullable: true, + comment: 'The started date of the ReversiGame.' + }) + public startedAt: Date | null; + + @Column(id()) + public user1Id: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user1: User | null; + + @Column(id()) + public user2Id: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user2: User | null; + + @Column('boolean', { + default: false, + }) + public user1Accepted: boolean; + + @Column('boolean', { + default: false, + }) + public user2Accepted: boolean; + + /** + * ã©ã¡ã‚‰ã®ãƒ—レイヤーãŒå…ˆè¡Œ(é»’)ã‹ + * 1 ... user1 + * 2 ... user2 + */ + @Column('integer', { + nullable: true, + }) + public black: number | null; + + @Column('boolean', { + default: false, + }) + public isStarted: boolean; + + @Column('boolean', { + default: false, + }) + public isEnded: boolean; + + @Column({ + ...id(), + nullable: true + }) + public winnerId: User['id'] | null; + + @Column({ + ...id(), + nullable: true + }) + public surrendered: User['id'] | null; + + @Column('jsonb', { + default: [], + }) + public logs: { + at: Date; + color: boolean; + pos: number; + }[]; + + @Column('varchar', { + array: true, length: 64, + }) + public map: string[]; + + @Column('varchar', { + length: 32 + }) + public bw: string; + + @Column('boolean', { + default: false, + }) + public isLlotheo: boolean; + + @Column('boolean', { + default: false, + }) + public canPutEverywhere: boolean; + + @Column('boolean', { + default: false, + }) + public loopedBoard: boolean; + + @Column('jsonb', { + nullable: true, default: null, + }) + public form1: any | null; + + @Column('jsonb', { + nullable: true, default: null, + }) + public form2: any | null; + + /** + * ãƒã‚°ã®posã‚’æ–‡å—列ã¨ã—ã¦ã™ã¹ã¦é€£çµã—ãŸã‚‚ã®ã®CRC32値 + */ + @Column('varchar', { + length: 32, nullable: true + }) + public crc32: string | null; +} diff --git a/src/models/entities/games/reversi/matching.ts b/src/models/entities/games/reversi/matching.ts new file mode 100644 index 0000000000000000000000000000000000000000..477a29316e95eca708c014e03585aa4eb421ea00 --- /dev/null +++ b/src/models/entities/games/reversi/matching.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from '../../user'; +import { id } from '../../../id'; + +@Entity() +export class ReversiMatching { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the ReversiMatching.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public parentId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public parent: User | null; + + @Index() + @Column(id()) + public childId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public child: User | null; +} diff --git a/src/models/entities/hashtag.ts b/src/models/entities/hashtag.ts new file mode 100644 index 0000000000000000000000000000000000000000..842cdaa5626ee748b787db671aa540548c08aadc --- /dev/null +++ b/src/models/entities/hashtag.ts @@ -0,0 +1,87 @@ +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class Hashtag { + @PrimaryColumn(id()) + public id: string; + + @Index({ unique: true }) + @Column('varchar', { + length: 128 + }) + public name: string; + + @Column({ + ...id(), + array: true, + }) + public mentionedUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public mentionedUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public mentionedLocalUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public mentionedLocalUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public mentionedRemoteUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public mentionedRemoteUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public attachedUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public attachedUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public attachedLocalUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public attachedLocalUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public attachedRemoteUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public attachedRemoteUsersCount: number; +} diff --git a/src/models/entities/instance.ts b/src/models/entities/instance.ts new file mode 100644 index 0000000000000000000000000000000000000000..977054263c9e32ea08d458b1f72c42fabda4391f --- /dev/null +++ b/src/models/entities/instance.ts @@ -0,0 +1,132 @@ +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class Instance { + @PrimaryColumn(id()) + public id: string; + + /** + * ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’æ•æ‰ã—ãŸæ—¥æ™‚ + */ + @Index() + @Column('timestamp with time zone', { + comment: 'The caught date of the Instance.' + }) + public caughtAt: Date; + + /** + * ホスト + */ + @Index({ unique: true }) + @Column('varchar', { + length: 128, + comment: 'The host of the Instance.' + }) + public host: string; + + /** + * インスタンスã®ã‚·ã‚¹ãƒ†ãƒ (Mastodonã¨ã‹Misskeyã¨ã‹Pleromaã¨ã‹) + */ + @Column('varchar', { + length: 64, nullable: true, + comment: 'The system of the Instance.' + }) + public system: string | null; + + /** + * インスタンスã®ãƒ¦ãƒ¼ã‚¶ãƒ¼æ•° + */ + @Column('integer', { + default: 0, + comment: 'The count of the users of the Instance.' + }) + public usersCount: number; + + /** + * インスタンスã®æŠ•ç¨¿æ•° + */ + @Column('integer', { + default: 0, + comment: 'The count of the notes of the Instance.' + }) + public notesCount: number; + + /** + * ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ãƒ•ã‚©ãƒãƒ¼ã•ã‚Œã¦ã„ã‚‹ã€è‡ªã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æ•° + */ + @Column('integer', { + default: 0, + }) + public followingCount: number; + + /** + * ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’フォãƒãƒ¼ã—ã¦ã„ã‚‹ã€è‡ªã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æ•° + */ + @Column('integer', { + default: 0, + }) + public followersCount: number; + + /** + * ãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ + */ + @Column('integer', { + default: 0, + }) + public driveUsage: number; + + /** + * ドライブã®ãƒ•ã‚¡ã‚¤ãƒ«æ•° + */ + @Column('integer', { + default: 0, + }) + public driveFiles: number; + + /** + * ç›´è¿‘ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆé€ä¿¡æ—¥æ™‚ + */ + @Column('timestamp with time zone', { + nullable: true, + }) + public latestRequestSentAt: Date | null; + + /** + * ç›´è¿‘ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆé€ä¿¡æ™‚ã®HTTPステータスコード + */ + @Column('integer', { + nullable: true, + }) + public latestStatus: number | null; + + /** + * ç›´è¿‘ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆå—信日時 + */ + @Column('timestamp with time zone', { + nullable: true, + }) + public latestRequestReceivedAt: Date | null; + + /** + * ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¨æœ€å¾Œã«ã‚„ã‚Šå–ã‚Šã—ãŸæ—¥æ™‚ + */ + @Column('timestamp with time zone') + public lastCommunicatedAt: Date; + + /** + * ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¨ä¸é€šã‹ã©ã†ã‹ + */ + @Column('boolean', { + default: false + }) + public isNotResponding: boolean; + + /** + * ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ãŒé–‰éŽ–済ã¿ã¨ã—ã¦ãƒžãƒ¼ã‚¯ã•ã‚Œã¦ã„ã‚‹ã‹ + */ + @Column('boolean', { + default: false + }) + public isMarkedAsClosed: boolean; +} diff --git a/src/models/entities/log.ts b/src/models/entities/log.ts new file mode 100644 index 0000000000000000000000000000000000000000..99e1e8947ec7f2c36d14e0bdeaeaa72143087c5d --- /dev/null +++ b/src/models/entities/log.ts @@ -0,0 +1,46 @@ +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class Log { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Log.' + }) + public createdAt: Date; + + @Index() + @Column('varchar', { + length: 64, array: true, default: '{}' + }) + public domain: string[]; + + @Index() + @Column('enum', { + enum: ['error', 'warning', 'info', 'success', 'debug'] + }) + public level: string; + + @Column('varchar', { + length: 8 + }) + public worker: string; + + @Column('varchar', { + length: 128 + }) + public machine: string; + + @Column('varchar', { + length: 1024 + }) + public message: string; + + @Column('jsonb', { + default: {} + }) + public data: Record<string, any>; +} diff --git a/src/models/entities/messaging-message.ts b/src/models/entities/messaging-message.ts new file mode 100644 index 0000000000000000000000000000000000000000..d3c3eab3a22f1f411226fc83f35f5c1a05dbe238 --- /dev/null +++ b/src/models/entities/messaging-message.ts @@ -0,0 +1,64 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { DriveFile } from './drive-file'; +import { id } from '../id'; + +@Entity() +export class MessagingMessage { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the MessagingMessage.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The sender user ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The recipient user ID.' + }) + public recipientId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public recipient: User | null; + + @Column('varchar', { + length: 4096, nullable: true + }) + public text: string | null; + + @Column('boolean', { + default: false, + }) + public isRead: boolean; + + @Column({ + ...id(), + nullable: true, + }) + public fileId: DriveFile['id'] | null; + + @ManyToOne(type => DriveFile, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public file: DriveFile | null; +} diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts new file mode 100644 index 0000000000000000000000000000000000000000..c34f5b690438a56e0d4d9c3ec1056eb4e6838313 --- /dev/null +++ b/src/models/entities/meta.ts @@ -0,0 +1,264 @@ +import { Entity, Column, PrimaryColumn } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class Meta { + @PrimaryColumn(id()) + public id: string; + + @Column('varchar', { + length: 128, nullable: true + }) + public name: string | null; + + @Column('varchar', { + length: 1024, nullable: true + }) + public description: string | null; + + /** + * メンテナã®åå‰ + */ + @Column('varchar', { + length: 128, nullable: true + }) + public maintainerName: string | null; + + /** + * メンテナã®é€£çµ¡å…ˆ + */ + @Column('varchar', { + length: 128, nullable: true + }) + public maintainerEmail: string | null; + + @Column('jsonb', { + default: [], + }) + public announcements: Record<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; +} diff --git a/src/models/entities/muting.ts b/src/models/entities/muting.ts new file mode 100644 index 0000000000000000000000000000000000000000..0084213bcc06093146aaaf5b676aa6e49f5ee569 --- /dev/null +++ b/src/models/entities/muting.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['muterId', 'muteeId'], { unique: true }) +export class Muting { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Muting.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The mutee user ID.' + }) + public muteeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public mutee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The muter user ID.' + }) + public muterId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public muter: User | null; +} diff --git a/src/models/entities/note-favorite.ts b/src/models/entities/note-favorite.ts new file mode 100644 index 0000000000000000000000000000000000000000..0713c3ae56a18436d54fbb9e24d18b430bf6c0ff --- /dev/null +++ b/src/models/entities/note-favorite.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { Note } from './note'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the NoteFavorite.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; +} diff --git a/src/models/entities/note-reaction.ts b/src/models/entities/note-reaction.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ce5d841fb7caa7d5bcd9b7bd213258a1bb44e38 --- /dev/null +++ b/src/models/entities/note-reaction.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Note } from './note'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteReaction { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the NoteReaction.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Column('varchar', { + length: 32 + }) + public reaction: string; +} diff --git a/src/models/entities/note-unread.ts b/src/models/entities/note-unread.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d18728256780125ee8cdfc08b84ab5514bee5f8 --- /dev/null +++ b/src/models/entities/note-unread.ts @@ -0,0 +1,43 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Note } from './note'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteUnread { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Column({ + ...id(), + comment: '[Denormalized]' + }) + public noteUserId: User['id']; + + /** + * ダイレクト投稿㋠+ */ + @Column('boolean') + public isSpecified: boolean; +} diff --git a/src/models/entities/note-watching.ts b/src/models/entities/note-watching.ts new file mode 100644 index 0000000000000000000000000000000000000000..741a1c0c8bcd2e1108a9a1d4da96ae078b05a476 --- /dev/null +++ b/src/models/entities/note-watching.ts @@ -0,0 +1,52 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Note } from './note'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteWatching { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the NoteWatching.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The watcher ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The target Note ID.' + }) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + //#region Denormalized fields + @Index() + @Column({ + ...id(), + comment: '[Denormalized]' + }) + public noteUserId: Note['userId']; + //#endregion +} diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts new file mode 100644 index 0000000000000000000000000000000000000000..0bcb9b4a447f8ab0c4f72c0ad7addde74fea91d7 --- /dev/null +++ b/src/models/entities/note.ts @@ -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; +}[]; diff --git a/src/models/entities/notification.ts b/src/models/entities/notification.ts new file mode 100644 index 0000000000000000000000000000000000000000..627a57bececeedd972c6de0c9405ca435ce386e6 --- /dev/null +++ b/src/models/entities/notification.ts @@ -0,0 +1,94 @@ +import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; +import { Note } from './note'; + +@Entity() +export class Notification { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Notification.' + }) + public createdAt: Date; + + /** + * 通知ã®å—信者 + */ + @Index() + @Column({ + ...id(), + comment: 'The ID of recipient user of the Notification.' + }) + public notifieeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public notifiee: User | null; + + /** + * 通知ã®é€ä¿¡è€…(initiator) + */ + @Column({ + ...id(), + comment: 'The ID of sender user of the Notification.' + }) + public notifierId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public notifier: User | null; + + /** + * 通知ã®ç¨®é¡žã€‚ + * follow - フォãƒãƒ¼ã•ã‚ŒãŸ + * mention - 投稿ã§è‡ªåˆ†ãŒè¨€åŠã•ã‚ŒãŸ + * reply - (自分ã¾ãŸã¯è‡ªåˆ†ãŒWatchã—ã¦ã„ã‚‹)投稿ãŒè¿”ä¿¡ã•ã‚ŒãŸ + * renote - (自分ã¾ãŸã¯è‡ªåˆ†ãŒWatchã—ã¦ã„ã‚‹)投稿ãŒRenoteã•ã‚ŒãŸ + * quote - (自分ã¾ãŸã¯è‡ªåˆ†ãŒWatchã—ã¦ã„ã‚‹)投稿ãŒå¼•ç”¨Renoteã•ã‚ŒãŸ + * reaction - (自分ã¾ãŸã¯è‡ªåˆ†ãŒWatchã—ã¦ã„ã‚‹)投稿ã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã•ã‚ŒãŸ + * pollVote - (自分ã¾ãŸã¯è‡ªåˆ†ãŒWatchã—ã¦ã„ã‚‹)投稿ã®æŠ•ç¥¨ã«æŠ•ç¥¨ã•ã‚ŒãŸ + */ + @Column('varchar', { + length: 32, + comment: 'The type of the Notification.' + }) + public type: string; + + /** + * 通知ãŒèªã¾ã‚ŒãŸã‹ã©ã†ã‹ + */ + @Column('boolean', { + default: false, + comment: 'Whether the Notification is read.' + }) + public isRead: boolean; + + @Column({ + ...id(), + nullable: true + }) + public noteId: Note['id'] | null; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Column('varchar', { + length: 128, nullable: true + }) + public reaction: string; + + @Column('integer', { + nullable: true + }) + public choice: number; +} diff --git a/src/models/entities/poll-vote.ts b/src/models/entities/poll-vote.ts new file mode 100644 index 0000000000000000000000000000000000000000..709376f909cee1fded3c11e43c52d54e2799f3ac --- /dev/null +++ b/src/models/entities/poll-vote.ts @@ -0,0 +1,40 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Note } from './note'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId', 'choice'], { unique: true }) +export class PollVote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the PollVote.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Column('integer') + public choice: number; +} diff --git a/src/models/entities/poll.ts b/src/models/entities/poll.ts new file mode 100644 index 0000000000000000000000000000000000000000..204f102f5103f14b80a284f0d4290f42f476100a --- /dev/null +++ b/src/models/entities/poll.ts @@ -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; +}; diff --git a/src/models/entities/registration-tickets.ts b/src/models/entities/registration-tickets.ts new file mode 100644 index 0000000000000000000000000000000000000000..d962f78a78c9ef9f17c068e3ffe1d2dbd2947ff5 --- /dev/null +++ b/src/models/entities/registration-tickets.ts @@ -0,0 +1,17 @@ +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class RegistrationTicket { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index({ unique: true }) + @Column('varchar', { + length: 64, + }) + public code: string; +} diff --git a/src/models/entities/signin.ts b/src/models/entities/signin.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e047084b10a2b8789502196ae6ef08d4434740f --- /dev/null +++ b/src/models/entities/signin.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class Signin { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the Signin.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + }) + public ip: string; + + @Column('jsonb') + public headers: Record<string, any>; + + @Column('boolean') + public success: boolean; +} diff --git a/src/models/entities/sw-subscription.ts b/src/models/entities/sw-subscription.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0f2a69f1b0fa14dd574d0cda0a031d42922c2f0 --- /dev/null +++ b/src/models/entities/sw-subscription.ts @@ -0,0 +1,37 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class SwSubscription { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 256, + }) + public endpoint: string; + + @Column('varchar', { + length: 256, + }) + public auth: string; + + @Column('varchar', { + length: 128, + }) + public publickey: string; +} diff --git a/src/models/entities/user-keypair.ts b/src/models/entities/user-keypair.ts new file mode 100644 index 0000000000000000000000000000000000000000..06b98d2536ddc19ebe91ca8d621ead4e32a03202 --- /dev/null +++ b/src/models/entities/user-keypair.ts @@ -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; +} diff --git a/src/models/entities/user-list-joining.ts b/src/models/entities/user-list-joining.ts new file mode 100644 index 0000000000000000000000000000000000000000..8af4efb6a73937fc45ff88462c073ef68e022a74 --- /dev/null +++ b/src/models/entities/user-list-joining.ts @@ -0,0 +1,41 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { UserList } from './user-list'; +import { id } from '../id'; + +@Entity() +export class UserListJoining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserListJoining.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The user ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The list ID.' + }) + public userListId: UserList['id']; + + @ManyToOne(type => UserList, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public userList: UserList | null; +} diff --git a/src/models/entities/user-list.ts b/src/models/entities/user-list.ts new file mode 100644 index 0000000000000000000000000000000000000000..35a83ef8c318b656b6caf9a15dc545261068cf65 --- /dev/null +++ b/src/models/entities/user-list.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class UserList { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserList.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The owner ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + comment: 'The name of the UserList.' + }) + public name: string; +} diff --git a/src/models/entities/user-note-pinings.ts b/src/models/entities/user-note-pinings.ts new file mode 100644 index 0000000000000000000000000000000000000000..04a6f8f6452fbd126fc6f14104fd02320ce33c27 --- /dev/null +++ b/src/models/entities/user-note-pinings.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { Note } from './note'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class UserNotePining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserNotePinings.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; +} diff --git a/src/models/entities/user-publickey.ts b/src/models/entities/user-publickey.ts new file mode 100644 index 0000000000000000000000000000000000000000..6c019f331327c0166835a033dd8b4ca811d43a26 --- /dev/null +++ b/src/models/entities/user-publickey.ts @@ -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; +} diff --git a/src/models/entities/user-service-linking.ts b/src/models/entities/user-service-linking.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d99554e1e5bd4823300a182e4c47f946b97279d --- /dev/null +++ b/src/models/entities/user-service-linking.ts @@ -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 +} diff --git a/src/models/entities/user.ts b/src/models/entities/user.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ef98cadc2a6c8a9f3285a294e3de11c3d977cc0 --- /dev/null +++ b/src/models/entities/user.ts @@ -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; +} diff --git a/src/models/favorite.ts b/src/models/favorite.ts deleted file mode 100644 index 2008edbfaf47625ccd38200fdd1ed272d07e8cd1..0000000000000000000000000000000000000000 --- a/src/models/favorite.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { pack as packNote } from './note'; -import { dbLogger } from '../db/logger'; - -const Favorite = db.get<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); -}); diff --git a/src/models/follow-request.ts b/src/models/follow-request.ts deleted file mode 100644 index 4f75c63a32285da131504c57b665441df5218680..0000000000000000000000000000000000000000 --- a/src/models/follow-request.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { pack as packUser } from './user'; - -const FollowRequest = db.get<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); -}); diff --git a/src/models/following.ts b/src/models/following.ts deleted file mode 100644 index 12cc27211bc87d267021bd55c08574e818c37a0b..0000000000000000000000000000000000000000 --- a/src/models/following.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const Following = db.get<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; - } -}; diff --git a/src/models/games/reversi/game.ts b/src/models/games/reversi/game.ts deleted file mode 100644 index 57c493cff50687b9307f2b9a2faaf979dfa95955..0000000000000000000000000000000000000000 --- a/src/models/games/reversi/game.ts +++ /dev/null @@ -1,111 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../../../db/mongodb'; -import isObjectId from '../../../misc/is-objectid'; -import { IUser, pack as packUser } from '../../user'; - -const ReversiGame = db.get<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); -}); diff --git a/src/models/games/reversi/matching.ts b/src/models/games/reversi/matching.ts deleted file mode 100644 index ba2ac1bc051e55bd359a9f2c484cf700b1836894..0000000000000000000000000000000000000000 --- a/src/models/games/reversi/matching.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../../../db/mongodb'; -import isObjectId from '../../../misc/is-objectid'; -import { IUser, pack as packUser } from '../../user'; - -const Matching = db.get<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); -}); diff --git a/src/models/hashtag.ts b/src/models/hashtag.ts deleted file mode 100644 index c1de42086e168bbcd109c17a7afa2b0c2b482531..0000000000000000000000000000000000000000 --- a/src/models/hashtag.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const Hashtag = db.get<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; -} diff --git a/src/models/id.ts b/src/models/id.ts new file mode 100644 index 0000000000000000000000000000000000000000..be2cccfe3b415e37cf1df00bf9e7d4b10275155a --- /dev/null +++ b/src/models/id.ts @@ -0,0 +1,4 @@ +export const id = () => ({ + type: 'varchar' as 'varchar', + length: 32 +}); diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f88bb8d6366137396329962ab328154ce0a811d3 --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,74 @@ +import { getRepository, getCustomRepository } from 'typeorm'; +import { Instance } from './entities/instance'; +import { Emoji } from './entities/emoji'; +import { Poll } from './entities/poll'; +import { PollVote } from './entities/poll-vote'; +import { Meta } from './entities/meta'; +import { SwSubscription } from './entities/sw-subscription'; +import { NoteWatching } from './entities/note-watching'; +import { UserListJoining } from './entities/user-list-joining'; +import { Hashtag } from './entities/hashtag'; +import { NoteUnread } from './entities/note-unread'; +import { RegistrationTicket } from './entities/registration-tickets'; +import { UserRepository } from './repositories/user'; +import { NoteRepository } from './repositories/note'; +import { DriveFileRepository } from './repositories/drive-file'; +import { DriveFolderRepository } from './repositories/drive-folder'; +import { Log } from './entities/log'; +import { AccessToken } from './entities/access-token'; +import { UserNotePining } from './entities/user-note-pinings'; +import { SigninRepository } from './repositories/signin'; +import { MessagingMessageRepository } from './repositories/messaging-message'; +import { ReversiGameRepository } from './repositories/games/reversi/game'; +import { UserListRepository } from './repositories/user-list'; +import { FollowRequestRepository } from './repositories/follow-request'; +import { MutingRepository } from './repositories/muting'; +import { BlockingRepository } from './repositories/blocking'; +import { NoteReactionRepository } from './repositories/note-reaction'; +import { 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); diff --git a/src/models/instance.ts b/src/models/instance.ts deleted file mode 100644 index cdce570a4be13b782272add1d3f2164b949e0c59..0000000000000000000000000000000000000000 --- a/src/models/instance.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const Instance = db.get<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; -} diff --git a/src/models/log.ts b/src/models/log.ts deleted file mode 100644 index 6f79e83c78c2f3df6709672354a79943bad2f9d4..0000000000000000000000000000000000000000 --- a/src/models/log.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const Log = db.get<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; -} diff --git a/src/models/messaging-message.ts b/src/models/messaging-message.ts deleted file mode 100644 index 67abb4d111f0ef7cfc00ab57e358dddaae37fe2e..0000000000000000000000000000000000000000 --- a/src/models/messaging-message.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import { pack as packUser } from './user'; -import { pack as packFile } from './drive-file'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { length } from 'stringz'; - -const MessagingMessage = db.get<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); -}); diff --git a/src/models/meta.ts b/src/models/meta.ts deleted file mode 100644 index 5ca0f01236449c5bf3d546bccb350096ee5bc8a8..0000000000000000000000000000000000000000 --- a/src/models/meta.ts +++ /dev/null @@ -1,257 +0,0 @@ -import db from '../db/mongodb'; -import config from '../config'; -import User from './user'; -import { transform } from '../misc/cafy-id'; - -const Meta = db.get<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; -}; diff --git a/src/models/mute.ts b/src/models/mute.ts deleted file mode 100644 index 52775e13ca5fd5147861dcf352c8c8548c3fed06..0000000000000000000000000000000000000000 --- a/src/models/mute.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import * as deepcopy from 'deepcopy'; -import { pack as packUser, IUser } from './user'; - -const Mute = db.get<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); -}); diff --git a/src/models/note-reaction.ts b/src/models/note-reaction.ts deleted file mode 100644 index 89b7529350f4a204bc5e9394b2a00c95271c70a4..0000000000000000000000000000000000000000 --- a/src/models/note-reaction.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { pack as packUser } from './user'; - -const NoteReaction = db.get<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); -}); diff --git a/src/models/note-unread.ts b/src/models/note-unread.ts deleted file mode 100644 index dd08640d85f4332efa2c6a63fbcf7e500fbf491e..0000000000000000000000000000000000000000 --- a/src/models/note-unread.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const NoteUnread = db.get<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; - }; -} diff --git a/src/models/note-watching.ts b/src/models/note-watching.ts deleted file mode 100644 index 83aaf8ad0661141a617fad414d2d31abed72e087..0000000000000000000000000000000000000000 --- a/src/models/note-watching.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const NoteWatching = db.get<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; -} diff --git a/src/models/note.ts b/src/models/note.ts deleted file mode 100644 index 8c71c1940c3f58a2d61bee3c6b3d0d5788e30016..0000000000000000000000000000000000000000 --- a/src/models/note.ts +++ /dev/null @@ -1,418 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import rap from '@prezzemolo/rap'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { length } from 'stringz'; -import { IUser, pack as packUser } from './user'; -import { pack as packApp } from './app'; -import PollVote from './poll-vote'; -import NoteReaction from './note-reaction'; -import { packMany as packFileMany, IDriveFile } from './drive-file'; -import Following from './following'; -import Emoji from './emoji'; -import { dbLogger } from '../db/logger'; -import { unique, concat } from '../prelude/array'; - -const Note = db.get<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; -}; diff --git a/src/models/notification.ts b/src/models/notification.ts deleted file mode 100644 index 75456af57b34036abad50d771a1e1216a8b81dcf..0000000000000000000000000000000000000000 --- a/src/models/notification.ts +++ /dev/null @@ -1,120 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { IUser, pack as packUser } from './user'; -import { pack as packNote } from './note'; -import { dbLogger } from '../db/logger'; - -const Notification = db.get<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); -}); diff --git a/src/models/poll-vote.ts b/src/models/poll-vote.ts deleted file mode 100644 index e6178cbc262064481dfe7906efde1e2db7009c11..0000000000000000000000000000000000000000 --- a/src/models/poll-vote.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const PollVote = db.get<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; -} diff --git a/src/models/registration-tickets.ts b/src/models/registration-tickets.ts deleted file mode 100644 index 846acefedf132df42a4bab6edd51dd830de2d682..0000000000000000000000000000000000000000 --- a/src/models/registration-tickets.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const RegistrationTicket = db.get<IRegistrationTicket>('registrationTickets'); -RegistrationTicket.createIndex('code', { unique: true }); -export default RegistrationTicket; - -export interface IRegistrationTicket { - _id: mongo.ObjectID; - createdAt: Date; - code: string; -} diff --git a/src/models/repositories/abuse-user-report.ts b/src/models/repositories/abuse-user-report.ts new file mode 100644 index 0000000000000000000000000000000000000000..c72a582c04613ce2422ff1ebd40d0b653e4990ef --- /dev/null +++ b/src/models/repositories/abuse-user-report.ts @@ -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 + }), + }); + } +} diff --git a/src/models/repositories/app.ts b/src/models/repositories/app.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e3323baf8ad55e286bc405d643adfc50f183400 --- /dev/null +++ b/src/models/repositories/app.ts @@ -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) + } : {}) + }; + } +} diff --git a/src/models/repositories/auth-session.ts b/src/models/repositories/auth-session.ts new file mode 100644 index 0000000000000000000000000000000000000000..76e3ddf9ab11cf47e9a709a3f19a91d91e54c3e6 --- /dev/null +++ b/src/models/repositories/auth-session.ts @@ -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) + }); + } +} diff --git a/src/models/repositories/blocking.ts b/src/models/repositories/blocking.ts new file mode 100644 index 0000000000000000000000000000000000000000..81f3866131e326c14859296107ff3a4197308a8a --- /dev/null +++ b/src/models/repositories/blocking.ts @@ -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 + }) + }); + } +} diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe0ca72bfbb3759cbc80e4a0a6f6cf288f0f7774 --- /dev/null +++ b/src/models/repositories/drive-file.ts @@ -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 + }); + } +} diff --git a/src/models/repositories/drive-folder.ts b/src/models/repositories/drive-folder.ts new file mode 100644 index 0000000000000000000000000000000000000000..faf0f353aa795c80f04ce6bc03c00b4d80d78c65 --- /dev/null +++ b/src/models/repositories/drive-folder.ts @@ -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 + }) + } : {}) + } : {}) + }); + } +} diff --git a/src/models/repositories/follow-request.ts b/src/models/repositories/follow-request.ts new file mode 100644 index 0000000000000000000000000000000000000000..bead093b21850f2b1e269f311d4793d50fd7f5a4 --- /dev/null +++ b/src/models/repositories/follow-request.ts @@ -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), + }; + } +} diff --git a/src/models/repositories/following.ts b/src/models/repositories/following.ts new file mode 100644 index 0000000000000000000000000000000000000000..02253d272d9e4386795241c5e02cf56f4d397393 --- /dev/null +++ b/src/models/repositories/following.ts @@ -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, + }); + } +} diff --git a/src/models/repositories/games/reversi/game.ts b/src/models/repositories/games/reversi/game.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0cb6ff905d7b556e8dae794eaa522bd454fe9b5 --- /dev/null +++ b/src/models/repositories/games/reversi/game.ts @@ -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, + } : {}) + }; + } +} diff --git a/src/models/repositories/games/reversi/matching.ts b/src/models/repositories/games/reversi/matching.ts new file mode 100644 index 0000000000000000000000000000000000000000..3612ac5c47f83edf9f88f4c54d0b27b731b166bc --- /dev/null +++ b/src/models/repositories/games/reversi/matching.ts @@ -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 + }) + }); + } +} diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts new file mode 100644 index 0000000000000000000000000000000000000000..b87b30388a6e8685fca0a7915cbb0137f25bebdf --- /dev/null +++ b/src/models/repositories/messaging-message.ts @@ -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 + }; + } +} diff --git a/src/models/repositories/muting.ts b/src/models/repositories/muting.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd98cb4fece28ab24d0aaff7d6fd54b16894c3e9 --- /dev/null +++ b/src/models/repositories/muting.ts @@ -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 + }) + }); + } +} diff --git a/src/models/repositories/note-favorite.ts b/src/models/repositories/note-favorite.ts new file mode 100644 index 0000000000000000000000000000000000000000..4526461e6937cee64ce46183df095e52f39efca8 --- /dev/null +++ b/src/models/repositories/note-favorite.ts @@ -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), + }; + } +} diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts new file mode 100644 index 0000000000000000000000000000000000000000..7189da8e202e8103442472d9e63786795e837e22 --- /dev/null +++ b/src/models/repositories/note-reaction.ts @@ -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), + }; + } +} diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts new file mode 100644 index 0000000000000000000000000000000000000000..4df0135115384168a7a47f0080f3f63b7ab0445a --- /dev/null +++ b/src/models/repositories/note.ts @@ -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; + } +} diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts new file mode 100644 index 0000000000000000000000000000000000000000..9bc569cd3f716f9ae479030e04122ef644556852 --- /dev/null +++ b/src/models/repositories/notification.ts @@ -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 + } : {}) + }); + } +} diff --git a/src/models/repositories/signin.ts b/src/models/repositories/signin.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5b90c0e9ebbf86dc46dc918051b928f369a50ad --- /dev/null +++ b/src/models/repositories/signin.ts @@ -0,0 +1,11 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Signin } from '../entities/signin'; + +@EntityRepository(Signin) +export class SigninRepository extends Repository<Signin> { + public async pack( + src: any, + ) { + return src; + } +} diff --git a/src/models/repositories/user-list.ts b/src/models/repositories/user-list.ts new file mode 100644 index 0000000000000000000000000000000000000000..921c18ca7aa5027d53f0f398e02444bc7f2698f0 --- /dev/null +++ b/src/models/repositories/user-list.ts @@ -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 + }; + } +} diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c4cc545cf518b8899a47a4383cd8caec5d3a919 --- /dev/null +++ b/src/models/repositories/user.ts @@ -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 +} diff --git a/src/models/signin.ts b/src/models/signin.ts deleted file mode 100644 index d8b05c0e3060dcc91ea16616c15e9dfb67a1f331..0000000000000000000000000000000000000000 --- a/src/models/signin.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; - -const Signin = db.get<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 deleted file mode 100644 index 743d0d2dd9a94dacf3c14032102b91bfb11fa59b..0000000000000000000000000000000000000000 --- a/src/models/sw-subscription.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const SwSubscription = db.get<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-list.ts b/src/models/user-list.ts deleted file mode 100644 index e7dd74bdd176cfab1ce15e7c544384e2c3cb0331..0000000000000000000000000000000000000000 --- a/src/models/user-list.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; - -const UserList = db.get<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); -}); diff --git a/src/models/user.ts b/src/models/user.ts deleted file mode 100644 index 0c3f7b5508792e4282f3dffb6955875c2fb68f6c..0000000000000000000000000000000000000000 --- a/src/models/user.ts +++ /dev/null @@ -1,438 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import rap from '@prezzemolo/rap'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { packMany as packNoteMany } from './note'; -import Following from './following'; -import Blocking from './blocking'; -import Mute from './mute'; -import { getFriendIds } from '../server/api/common/get-friends'; -import config from '../config'; -import FollowRequest from './follow-request'; -import fetchMeta from '../misc/fetch-meta'; -import Emoji from './emoji'; -import { dbLogger } from '../db/logger'; - -const User = db.get<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; -} diff --git a/src/queue/index.ts b/src/queue/index.ts index d8328a1d5758c92ca2dfe215c176a25b0aedcf41..728c43c6aced18a7493e0bf9d8c6d9fa0236efdc 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -2,14 +2,14 @@ import * as Queue from 'bull'; import * as httpSignature from 'http-signature'; import config from '../config'; -import { ILocalUser } from '../models/user'; +import { ILocalUser } from '../models/entities/user'; import { program } from '../argv'; import processDeliver from './processors/deliver'; import processInbox from './processors/inbox'; import processDb from './processors/db'; import { queueLogger } from './logger'; -import { IDriveFile } from '../models/drive-file'; +import { DriveFile } from '../models/entities/drive-file'; function initializeQueue(name: string) { return new Queue(name, config.redis != null ? { @@ -83,15 +83,6 @@ export function inbox(activity: any, signature: httpSignature.IParsedSignature) }); } -export function createDeleteNotesJob(user: ILocalUser) { - return dbQueue.add('deleteNotes', { - user: user - }, { - removeOnComplete: true, - removeOnFail: true - }); -} - export function createDeleteDriveFilesJob(user: ILocalUser) { return dbQueue.add('deleteDriveFiles', { user: user @@ -146,7 +137,7 @@ export function createExportUserListsJob(user: ILocalUser) { }); } -export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_id']) { +export function createImportFollowingJob(user: ILocalUser, fileId: DriveFile['id']) { return dbQueue.add('importFollowing', { user: user, fileId: fileId @@ -156,7 +147,7 @@ export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_ }); } -export function createImportUserListsJob(user: ILocalUser, fileId: IDriveFile['_id']) { +export function createImportUserListsJob(user: ILocalUser, fileId: DriveFile['id']) { return dbQueue.add('importUserLists', { user: user, fileId: fileId diff --git a/src/queue/processors/db/delete-drive-files.ts b/src/queue/processors/db/delete-drive-files.ts index 3de960a25e3ab425d9cf4df70c27602bed6854bd..5f347fb58818732ead7bb9392110eb759ccdb1d2 100644 --- a/src/queue/processors/db/delete-drive-files.ts +++ b/src/queue/processors/db/delete-drive-files.ts @@ -1,18 +1,17 @@ import * as Bull from 'bull'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; -import User from '../../../models/user'; -import DriveFile from '../../../models/drive-file'; import deleteFile from '../../../services/drive/delete-file'; +import { Users, DriveFiles } from '../../../models'; +import { MoreThan } from 'typeorm'; const logger = queueLogger.createSubLogger('delete-drive-files'); export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void> { - logger.info(`Deleting drive files of ${job.data.user._id} ...`); + logger.info(`Deleting drive files of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) + const user = await Users.findOne({ + id: job.data.user.id }); let deletedCount = 0; @@ -20,13 +19,14 @@ export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void> let cursor: any = null; while (!ended) { - const files = await DriveFile.find({ - userId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 + const files = await DriveFiles.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}) + }, + take: 100, + order: { + id: 1 } }); @@ -36,20 +36,20 @@ export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void> break; } - cursor = files[files.length - 1]._id; + cursor = files[files.length - 1].id; for (const file of files) { await deleteFile(file); deletedCount++; } - const total = await DriveFile.count({ - userId: user._id, + const total = await DriveFiles.count({ + userId: user.id, }); job.progress(deletedCount / total); } - logger.succ(`All drive files (${deletedCount}) of ${user._id} has been deleted.`); + logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`); done(); } diff --git a/src/queue/processors/db/delete-notes.ts b/src/queue/processors/db/delete-notes.ts deleted file mode 100644 index 021db8062e7e40b9549b08f50d23c37050262989..0000000000000000000000000000000000000000 --- a/src/queue/processors/db/delete-notes.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as Bull from 'bull'; -import * as mongo from 'mongodb'; - -import { queueLogger } from '../../logger'; -import Note from '../../../models/note'; -import deleteNote from '../../../services/note/delete'; -import User from '../../../models/user'; - -const logger = queueLogger.createSubLogger('delete-notes'); - -export async function deleteNotes(job: Bull.Job, done: any): Promise<void> { - logger.info(`Deleting notes of ${job.data.user._id} ...`); - - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) - }); - - let deletedCount = 0; - let ended = false; - let cursor: any = null; - - while (!ended) { - const notes = await Note.find({ - userId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 - } - }); - - if (notes.length === 0) { - ended = true; - job.progress(100); - break; - } - - cursor = notes[notes.length - 1]._id; - - for (const note of notes) { - await deleteNote(user, note, true); - deletedCount++; - } - - const total = await Note.count({ - userId: user._id, - }); - - job.progress(deletedCount / total); - } - - logger.succ(`All notes (${deletedCount}) of ${user._id} has been deleted.`); - done(); -} diff --git a/src/queue/processors/db/export-blocking.ts b/src/queue/processors/db/export-blocking.ts index 7f32c06472ebc5909afb3ce8cac217df52961975..c12aa4fca30839234c49efa5d2dc652a3d2ba8fe 100644 --- a/src/queue/processors/db/export-blocking.ts +++ b/src/queue/processors/db/export-blocking.ts @@ -1,22 +1,21 @@ import * as Bull from 'bull'; import * as tmp from 'tmp'; import * as fs from 'fs'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; import addFile from '../../../services/drive/add-file'; -import User from '../../../models/user'; import dateFormat = require('dateformat'); -import Blocking from '../../../models/blocking'; import { getFullApAccount } from '../../../misc/convert-host'; +import { Users, Blockings } from '../../../models'; +import { MoreThan } from 'typeorm'; const logger = queueLogger.createSubLogger('export-blocking'); export async function exportBlocking(job: Bull.Job, done: any): Promise<void> { - logger.info(`Exporting blocking of ${job.data.user._id} ...`); + logger.info(`Exporting blocking of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) + const user = await Users.findOne({ + id: job.data.user.id }); // Create temp file @@ -36,13 +35,14 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> { let cursor: any = null; while (!ended) { - const blockings = await Blocking.find({ - blockerId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 + const blockings = await Blockings.find({ + where: { + blockerId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}) + }, + take: 100, + order: { + id: 1 } }); @@ -52,10 +52,10 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> { break; } - cursor = blockings[blockings.length - 1]._id; + cursor = blockings[blockings.length - 1].id; for (const block of blockings) { - const u = await User.findOne({ _id: block.blockeeId }, { fields: { username: true, host: true } }); + const u = await Users.findOne({ id: block.blockeeId }); const content = getFullApAccount(u.username, u.host); await new Promise((res, rej) => { stream.write(content + '\n', err => { @@ -70,8 +70,8 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> { exportedCount++; } - const total = await Blocking.count({ - blockerId: user._id, + const total = await Blockings.count({ + blockerId: user.id, }); job.progress(exportedCount / total); @@ -83,7 +83,7 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> { const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv'; const driveFile = await addFile(user, path, fileName); - logger.succ(`Exported to: ${driveFile._id}`); + logger.succ(`Exported to: ${driveFile.id}`); cleanup(); done(); } diff --git a/src/queue/processors/db/export-following.ts b/src/queue/processors/db/export-following.ts index 019414072a01a6cde5666cbde8bbc3037b5d0c6c..fb30df79fe67ddee5ba88bd5e6f6dcaf0539bd8d 100644 --- a/src/queue/processors/db/export-following.ts +++ b/src/queue/processors/db/export-following.ts @@ -1,22 +1,21 @@ import * as Bull from 'bull'; import * as tmp from 'tmp'; import * as fs from 'fs'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; import addFile from '../../../services/drive/add-file'; -import User from '../../../models/user'; import dateFormat = require('dateformat'); -import Following from '../../../models/following'; import { getFullApAccount } from '../../../misc/convert-host'; +import { Users, Followings } from '../../../models'; +import { MoreThan } from 'typeorm'; const logger = queueLogger.createSubLogger('export-following'); export async function exportFollowing(job: Bull.Job, done: any): Promise<void> { - logger.info(`Exporting following of ${job.data.user._id} ...`); + logger.info(`Exporting following of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) + const user = await Users.findOne({ + id: job.data.user.id }); // Create temp file @@ -36,13 +35,14 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> { let cursor: any = null; while (!ended) { - const followings = await Following.find({ - followerId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 + const followings = await Followings.find({ + where: { + followerId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}) + }, + take: 100, + order: { + id: 1 } }); @@ -52,10 +52,10 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> { break; } - cursor = followings[followings.length - 1]._id; + cursor = followings[followings.length - 1].id; for (const following of followings) { - const u = await User.findOne({ _id: following.followeeId }, { fields: { username: true, host: true } }); + const u = await Users.findOne({ id: following.followeeId }); const content = getFullApAccount(u.username, u.host); await new Promise((res, rej) => { stream.write(content + '\n', err => { @@ -70,8 +70,8 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> { exportedCount++; } - const total = await Following.count({ - followerId: user._id, + const total = await Followings.count({ + followerId: user.id, }); job.progress(exportedCount / total); @@ -83,7 +83,7 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> { const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv'; const driveFile = await addFile(user, path, fileName); - logger.succ(`Exported to: ${driveFile._id}`); + logger.succ(`Exported to: ${driveFile.id}`); cleanup(); done(); } diff --git a/src/queue/processors/db/export-mute.ts b/src/queue/processors/db/export-mute.ts index 5ded7cf651985f396c69f1ed816e422d830b96a4..3aed526dc571e3367099d1cd8794bf43c97fab31 100644 --- a/src/queue/processors/db/export-mute.ts +++ b/src/queue/processors/db/export-mute.ts @@ -1,22 +1,21 @@ import * as Bull from 'bull'; import * as tmp from 'tmp'; import * as fs from 'fs'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; import addFile from '../../../services/drive/add-file'; -import User from '../../../models/user'; import dateFormat = require('dateformat'); -import Mute from '../../../models/mute'; import { getFullApAccount } from '../../../misc/convert-host'; +import { Users, Mutings } from '../../../models'; +import { MoreThan } from 'typeorm'; const logger = queueLogger.createSubLogger('export-mute'); export async function exportMute(job: Bull.Job, done: any): Promise<void> { - logger.info(`Exporting mute of ${job.data.user._id} ...`); + logger.info(`Exporting mute of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) + const user = await Users.findOne({ + id: job.data.user.id }); // Create temp file @@ -36,13 +35,14 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> { let cursor: any = null; while (!ended) { - const mutes = await Mute.find({ - muterId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 + const mutes = await Mutings.find({ + where: { + muterId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}) + }, + take: 100, + order: { + id: 1 } }); @@ -52,10 +52,10 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> { break; } - cursor = mutes[mutes.length - 1]._id; + cursor = mutes[mutes.length - 1].id; for (const mute of mutes) { - const u = await User.findOne({ _id: mute.muteeId }, { fields: { username: true, host: true } }); + const u = await Users.findOne({ id: mute.muteeId }); const content = getFullApAccount(u.username, u.host); await new Promise((res, rej) => { stream.write(content + '\n', err => { @@ -70,8 +70,8 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> { exportedCount++; } - const total = await Mute.count({ - muterId: user._id, + const total = await Mutings.count({ + muterId: user.id, }); job.progress(exportedCount / total); @@ -83,7 +83,7 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> { const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv'; const driveFile = await addFile(user, path, fileName); - logger.succ(`Exported to: ${driveFile._id}`); + logger.succ(`Exported to: ${driveFile.id}`); cleanup(); done(); } diff --git a/src/queue/processors/db/export-notes.ts b/src/queue/processors/db/export-notes.ts index 8f3cdc5b997f5e1592a5bf06bb4a2ce1757d4601..92867ad82e38009c587ffcc753f7f8ee087b81f6 100644 --- a/src/queue/processors/db/export-notes.ts +++ b/src/queue/processors/db/export-notes.ts @@ -1,21 +1,22 @@ import * as Bull from 'bull'; import * as tmp from 'tmp'; import * as fs from 'fs'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; -import Note, { INote } from '../../../models/note'; import addFile from '../../../services/drive/add-file'; -import User from '../../../models/user'; import dateFormat = require('dateformat'); +import { Users, Notes, Polls } from '../../../models'; +import { MoreThan } from 'typeorm'; +import { Note } from '../../../models/entities/note'; +import { Poll } from '../../../models/entities/poll'; const logger = queueLogger.createSubLogger('export-notes'); export async function exportNotes(job: Bull.Job, done: any): Promise<void> { - logger.info(`Exporting notes of ${job.data.user._id} ...`); + logger.info(`Exporting notes of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) + const user = await Users.findOne({ + id: job.data.user.id }); // Create temp file @@ -46,13 +47,14 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> { let cursor: any = null; while (!ended) { - const notes = await Note.find({ - userId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 + const notes = await Notes.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}) + }, + take: 100, + order: { + id: 1 } }); @@ -62,10 +64,14 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> { break; } - cursor = notes[notes.length - 1]._id; + cursor = notes[notes.length - 1].id; for (const note of notes) { - const content = JSON.stringify(serialize(note)); + let poll: Poll; + if (note.hasPoll) { + poll = await Polls.findOne({ noteId: note.id }); + } + const content = JSON.stringify(serialize(note, poll)); await new Promise((res, rej) => { stream.write(exportedNotesCount === 0 ? content : ',\n' + content, err => { if (err) { @@ -79,8 +85,8 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> { exportedNotesCount++; } - const total = await Note.count({ - userId: user._id, + const total = await Notes.count({ + userId: user.id, }); job.progress(exportedNotesCount / total); @@ -103,20 +109,20 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> { const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json'; const driveFile = await addFile(user, path, fileName); - logger.succ(`Exported to: ${driveFile._id}`); + logger.succ(`Exported to: ${driveFile.id}`); cleanup(); done(); } -function serialize(note: INote): any { +function serialize(note: Note, poll: Poll): any { return { - id: note._id, + id: note.id, text: note.text, createdAt: note.createdAt, fileIds: note.fileIds, replyId: note.replyId, renoteId: note.renoteId, - poll: note.poll, + poll: poll, cw: note.cw, viaMobile: note.viaMobile, visibility: note.visibility, diff --git a/src/queue/processors/db/export-user-lists.ts b/src/queue/processors/db/export-user-lists.ts index dfbf152ec061c432a8eb656f34eb6bf035d80946..f3987cb0d2c4a86f76e705983233590b8eaa1693 100644 --- a/src/queue/processors/db/export-user-lists.ts +++ b/src/queue/processors/db/export-user-lists.ts @@ -1,26 +1,25 @@ import * as Bull from 'bull'; import * as tmp from 'tmp'; import * as fs from 'fs'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; import addFile from '../../../services/drive/add-file'; -import User from '../../../models/user'; import dateFormat = require('dateformat'); -import UserList from '../../../models/user-list'; import { getFullApAccount } from '../../../misc/convert-host'; +import { Users, UserLists, UserListJoinings } from '../../../models'; +import { In } from 'typeorm'; const logger = queueLogger.createSubLogger('export-user-lists'); export async function exportUserLists(job: Bull.Job, done: any): Promise<void> { - logger.info(`Exporting user lists of ${job.data.user._id} ...`); + logger.info(`Exporting user lists of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) + const user = await Users.findOne({ + id: job.data.user.id }); - const lists = await UserList.find({ - userId: user._id + const lists = await UserLists.find({ + userId: user.id }); // Create temp file @@ -36,18 +35,14 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> { const stream = fs.createWriteStream(path, { flags: 'a' }); for (const list of lists) { - const users = await User.find({ - _id: { $in: list.userIds } - }, { - fields: { - username: true, - host: true - } + const joinings = await UserListJoinings.find({ userListId: list.id }); + const users = await Users.find({ + id: In(joinings.map(j => j.userId)) }); for (const u of users) { const acct = getFullApAccount(u.username, u.host); - const content = `${list.title},${acct}`; + const content = `${list.name},${acct}`; await new Promise((res, rej) => { stream.write(content + '\n', err => { if (err) { @@ -67,7 +62,7 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> { const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv'; const driveFile = await addFile(user, path, fileName); - logger.succ(`Exported to: ${driveFile._id}`); + logger.succ(`Exported to: ${driveFile.id}`); cleanup(); done(); } diff --git a/src/queue/processors/db/import-following.ts b/src/queue/processors/db/import-following.ts index 069afa74c466ac736a3e91d09c6951e75dc0e299..2e646c1869445bbfe44e356f9542d2a08596c8ec 100644 --- a/src/queue/processors/db/import-following.ts +++ b/src/queue/processors/db/import-following.ts @@ -1,32 +1,27 @@ import * as Bull from 'bull'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; -import User from '../../../models/user'; import follow from '../../../services/following/create'; -import DriveFile from '../../../models/drive-file'; -import { getOriginalUrl } from '../../../misc/get-drive-file-url'; import parseAcct from '../../../misc/acct/parse'; import resolveUser from '../../../remote/resolve-user'; import { downloadTextFile } from '../../../misc/download-text-file'; import { isSelfHost, toDbHost } from '../../../misc/convert-host'; +import { Users, DriveFiles } from '../../../models'; const logger = queueLogger.createSubLogger('import-following'); export async function importFollowing(job: Bull.Job, done: any): Promise<void> { - logger.info(`Importing following of ${job.data.user._id} ...`); + logger.info(`Importing following of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) + const user = await Users.findOne({ + id: job.data.user.id }); - const file = await DriveFile.findOne({ - _id: new mongo.ObjectID(job.data.fileId.toString()) + const file = await DriveFiles.findOne({ + id: job.data.fileId }); - const url = getOriginalUrl(file); - - const csv = await downloadTextFile(url); + const csv = await downloadTextFile(file.url); let linenum = 0; @@ -36,10 +31,10 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> { try { const { username, host } = parseAcct(line.trim()); - let target = isSelfHost(host) ? await User.findOne({ + let target = isSelfHost(host) ? await Users.findOne({ host: null, usernameLower: username.toLowerCase() - }) : await User.findOne({ + }) : await Users.findOne({ host: toDbHost(host), usernameLower: username.toLowerCase() }); @@ -55,9 +50,9 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> { } // skip myself - if (target._id.equals(job.data.user._id)) continue; + if (target.id === job.data.user.id) continue; - logger.info(`Follow[${linenum}] ${target._id} ...`); + logger.info(`Follow[${linenum}] ${target.id} ...`); follow(user, target); } catch (e) { diff --git a/src/queue/processors/db/import-user-lists.ts b/src/queue/processors/db/import-user-lists.ts index 50d3c649d4003a8cb424387b43814164f7331b32..8be578589621679031c055ac841647a8738889d4 100644 --- a/src/queue/processors/db/import-user-lists.ts +++ b/src/queue/processors/db/import-user-lists.ts @@ -1,62 +1,59 @@ import * as Bull from 'bull'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; -import User from '../../../models/user'; -import UserList from '../../../models/user-list'; -import DriveFile from '../../../models/drive-file'; -import { getOriginalUrl } from '../../../misc/get-drive-file-url'; import parseAcct from '../../../misc/acct/parse'; import resolveUser from '../../../remote/resolve-user'; import { pushUserToUserList } from '../../../services/user-list/push'; import { downloadTextFile } from '../../../misc/download-text-file'; import { isSelfHost, toDbHost } from '../../../misc/convert-host'; +import { DriveFiles, Users, UserLists, UserListJoinings } from '../../../models'; +import { genId } from '../../../misc/gen-id'; const logger = queueLogger.createSubLogger('import-user-lists'); export async function importUserLists(job: Bull.Job, done: any): Promise<void> { - logger.info(`Importing user lists of ${job.data.user._id} ...`); + logger.info(`Importing user lists of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) + const user = await Users.findOne({ + id: job.data.user.id }); - const file = await DriveFile.findOne({ - _id: new mongo.ObjectID(job.data.fileId.toString()) + const file = await DriveFiles.findOne({ + id: job.data.fileId }); - const url = getOriginalUrl(file); - - const csv = await downloadTextFile(url); + const csv = await downloadTextFile(file.url); for (const line of csv.trim().split('\n')) { const listName = line.split(',')[0].trim(); const { username, host } = parseAcct(line.split(',')[1].trim()); - let list = await UserList.findOne({ - userId: user._id, - title: listName + let list = await UserLists.findOne({ + userId: user.id, + name: listName }); if (list == null) { - list = await UserList.insert({ + list = await UserLists.save({ + id: genId(), createdAt: new Date(), - userId: user._id, - title: listName, + userId: user.id, + name: listName, userIds: [] }); } - let target = isSelfHost(host) ? await User.findOne({ + let target = isSelfHost(host) ? await Users.findOne({ host: null, usernameLower: username.toLowerCase() - }) : await User.findOne({ + }) : await Users.findOne({ host: toDbHost(host), usernameLower: username.toLowerCase() }); if (host == null && target == null) continue; - if (list.userIds.some(id => id.equals(target._id))) continue; + + if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue; if (target == null) { target = await resolveUser(username, host); diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts index 1bc9a9af7ce19bfe7c648aa761dafbfe756e4d86..921cdf7ab14e51f93bfa42e450db6242ee9679d8 100644 --- a/src/queue/processors/db/index.ts +++ b/src/queue/processors/db/index.ts @@ -1,5 +1,4 @@ import * as Bull from 'bull'; -import { deleteNotes } from './delete-notes'; import { deleteDriveFiles } from './delete-drive-files'; import { exportNotes } from './export-notes'; import { exportFollowing } from './export-following'; @@ -10,7 +9,6 @@ import { importFollowing } from './import-following'; import { importUserLists } from './import-user-lists'; const jobs = { - deleteNotes, deleteDriveFiles, exportNotes, exportFollowing, diff --git a/src/queue/processors/deliver.ts b/src/queue/processors/deliver.ts index 28d3a17f6b5091501f6889c49b6536a5e41ab9d5..b9701c0c65ac1e8f66852c81a7c090d7d42636f7 100644 --- a/src/queue/processors/deliver.ts +++ b/src/queue/processors/deliver.ts @@ -1,9 +1,9 @@ import * as Bull from 'bull'; import request from '../../remote/activitypub/request'; import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; import Logger from '../../services/logger'; +import { Instances } from '../../models'; +import { instanceChart } from '../../services/chart'; const logger = new Logger('deliver'); @@ -21,13 +21,11 @@ export default async (job: Bull.Job) => { // Update stats registerOrFetchInstanceDoc(host).then(i => { - Instance.update({ _id: i._id }, { - $set: { - latestRequestSentAt: new Date(), - latestStatus: 200, - lastCommunicatedAt: new Date(), - isNotResponding: false - } + Instances.update(i.id, { + latestRequestSentAt: new Date(), + latestStatus: 200, + lastCommunicatedAt: new Date(), + isNotResponding: false }); instanceChart.requestSent(i.host, true); @@ -37,12 +35,10 @@ export default async (job: Bull.Job) => { } catch (res) { // Update stats registerOrFetchInstanceDoc(host).then(i => { - Instance.update({ _id: i._id }, { - $set: { - latestRequestSentAt: new Date(), - latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null, - isNotResponding: true - } + Instances.update(i.id, { + latestRequestSentAt: new Date(), + latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null, + isNotResponding: true }); instanceChart.requestSent(i.host, false); diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts index 436f3335c8422fb257b4a12c2c3f0e06a4ee70b4..16badabcf7eeb32445f6cf6802e9cf0bfac6ddde 100644 --- a/src/queue/processors/inbox.ts +++ b/src/queue/processors/inbox.ts @@ -1,7 +1,7 @@ import * as Bull from 'bull'; import * as httpSignature from 'http-signature'; import parseAcct from '../../misc/acct/parse'; -import User, { IRemoteUser } from '../../models/user'; +import { IRemoteUser } from '../../models/entities/user'; import perform from '../../remote/activitypub/perform'; import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person'; import { toUnicode } from 'punycode'; @@ -9,8 +9,10 @@ import { URL } from 'url'; import { publishApLogStream } from '../../services/stream'; import Logger from '../../services/logger'; import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; +import { Instances, Users, UserPublickeys } from '../../models'; +import { instanceChart } from '../../services/chart'; +import { UserPublickey } from '../../models/entities/user-publickey'; +import fetchMeta from '../../misc/fetch-meta'; const logger = new Logger('inbox'); @@ -28,6 +30,7 @@ export default async (job: Bull.Job): Promise<void> => { const keyIdLower = signature.keyId.toLowerCase(); let user: IRemoteUser; + let key: UserPublickey; if (keyIdLower.startsWith('acct:')) { const { username, host } = parseAcct(keyIdLower.slice('acct:'.length)); @@ -46,13 +49,17 @@ export default async (job: Bull.Job): Promise<void> => { // ブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– // TODO: ã„ã¡ã„ã¡ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã®ã¯ã‚³ã‚¹ãƒˆé«˜ãã†ãªã®ã§ã©ã£ã‹ã«ã‚ャッシュã—ã¦ãŠã - const instance = await Instance.findOne({ host: host.toLowerCase() }); - if (instance && instance.isBlocked) { + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(host.toLowerCase())) { logger.info(`Blocked request: ${host}`); return; } - user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser; + user = await Users.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser; + + key = await UserPublickeys.findOne({ + userId: user.id + }); } else { // アクティビティ内ã®ãƒ›ã‚¹ãƒˆã®æ¤œè¨¼ const host = toUnicode(new URL(signature.keyId).hostname.toLowerCase()); @@ -65,16 +72,17 @@ export default async (job: Bull.Job): Promise<void> => { // ブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– // TODO: ã„ã¡ã„ã¡ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã®ã¯ã‚³ã‚¹ãƒˆé«˜ãã†ãªã®ã§ã©ã£ã‹ã«ã‚ャッシュã—ã¦ãŠã - const instance = await Instance.findOne({ host: host.toLowerCase() }); - if (instance && instance.isBlocked) { - logger.warn(`Blocked request: ${host}`); + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(host.toLowerCase())) { + logger.info(`Blocked request: ${host}`); return; } - user = await User.findOne({ - host: { $ne: null }, - 'publicKey.id': signature.keyId - }) as IRemoteUser; + key = await UserPublickeys.findOne({ + keyId: signature.keyId + }); + + user = await Users.findOne(key.userId) as IRemoteUser; } // Update Person activityã®å ´åˆã¯ã€ã“ã“ã§ç½²å検証/更新処ç†ã¾ã§å®Ÿæ–½ã—ã¦çµ‚了 @@ -82,7 +90,7 @@ export default async (job: Bull.Job): Promise<void> => { if (activity.object && activity.object.type === 'Person') { if (user == null) { logger.warn('Update activity received, but user not registed.'); - } else if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) { + } else if (!httpSignature.verifySignature(signature, key.keyPem)) { logger.warn('Update activity received, but signature verification failed.'); } else { updatePerson(activity.actor, null, activity.object); @@ -92,15 +100,15 @@ export default async (job: Bull.Job): Promise<void> => { } // アクティビティをé€ä¿¡ã—ã¦ããŸãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã¾ã Misskeyサーãƒãƒ¼ã«ç™»éŒ²ã•ã‚Œã¦ã„ãªã‹ã£ãŸã‚‰ç™»éŒ²ã™ã‚‹ - if (user === null) { + if (user == null) { user = await resolvePerson(activity.actor) as IRemoteUser; } - if (user === null) { + if (user == null) { throw new Error('failed to resolve user'); } - if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) { + if (!httpSignature.verifySignature(signature, key.keyPem)) { logger.error('signature verification failed'); return; } @@ -116,12 +124,10 @@ export default async (job: Bull.Job): Promise<void> => { // Update stats registerOrFetchInstanceDoc(user.host).then(i => { - Instance.update({ _id: i._id }, { - $set: { - latestRequestReceivedAt: new Date(), - lastCommunicatedAt: new Date(), - isNotResponding: false - } + Instances.update(i.id, { + latestRequestReceivedAt: new Date(), + lastCommunicatedAt: new Date(), + isNotResponding: false }); instanceChart.requestReceived(i.host); diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts index 07c820c28a2f6d5e10a799d271f76c572d147929..816fcbadbfe5b95a733679e52f395f029fce8f00 100644 --- a/src/remote/activitypub/kernel/accept/follow.ts +++ b/src/remote/activitypub/kernel/accept/follow.ts @@ -1,8 +1,8 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import config from '../../../../config'; import accept from '../../../../services/following/requests/accept'; import { IFollow } from '../../type'; +import { Users } from '../../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id; @@ -11,11 +11,11 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { return null; } - const follower = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) + const follower = await Users.findOne({ + id: id.split('/').pop() }); - if (follower === null) { + if (follower == null) { throw new Error('follower not found'); } diff --git a/src/remote/activitypub/kernel/accept/index.ts b/src/remote/activitypub/kernel/accept/index.ts index 443c1935d68a210c1407a299ae022ec1656b87fa..5a27ce1d4d193a58a0e0fee63b44fc2fcb63272b 100644 --- a/src/remote/activitypub/kernel/accept/index.ts +++ b/src/remote/activitypub/kernel/accept/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import acceptFollow from './follow'; import { IAccept, IFollow } from '../../type'; import { apLogger } from '../../logger'; diff --git a/src/remote/activitypub/kernel/add/index.ts b/src/remote/activitypub/kernel/add/index.ts index eb2dba5b21f2f241f19e43472c8d090a65e8b16f..d16f0a4a0d33239b7a663c55beb7a7949a0023af 100644 --- a/src/remote/activitypub/kernel/add/index.ts +++ b/src/remote/activitypub/kernel/add/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IAdd } from '../../type'; import { resolveNote } from '../../models/note'; import { addPinned } from '../../../../services/i/pin'; @@ -14,7 +14,7 @@ export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => { if (activity.target === actor.featured) { const note = await resolveNote(activity.object); - await addPinned(actor, note._id); + await addPinned(actor, note.id); return; } diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts index 5f738da6c76104bdd1170d39ef64dd23ed320d16..ebd5a27b929fd7670d35ebf870706449e83d65e5 100644 --- a/src/remote/activitypub/kernel/announce/index.ts +++ b/src/remote/activitypub/kernel/announce/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import announceNote from './note'; import { IAnnounce, INote } from '../../type'; import { apLogger } from '../../logger'; diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts index 912936bef823dc1b00546b761f15f1bf84e5dbeb..403fc66bedcc76d715baff62d0b6c455b921b9c3 100644 --- a/src/remote/activitypub/kernel/announce/note.ts +++ b/src/remote/activitypub/kernel/announce/note.ts @@ -1,12 +1,12 @@ import Resolver from '../../resolver'; import post from '../../../../services/note/create'; -import { IRemoteUser, IUser } from '../../../../models/user'; +import { IRemoteUser, User } from '../../../../models/entities/user'; import { IAnnounce, INote } from '../../type'; import { fetchNote, resolveNote } from '../../models/note'; import { resolvePerson } from '../../models/person'; import { apLogger } from '../../logger'; import { extractDbHost } from '../../../../misc/convert-host'; -import Instance from '../../../../models/instance'; +import fetchMeta from '../../../../misc/fetch-meta'; const logger = apLogger; @@ -27,8 +27,8 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity: // アナウンス先をブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– // TODO: ã„ã¡ã„ã¡ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã®ã¯ã‚³ã‚¹ãƒˆé«˜ãã†ãªã®ã§ã©ã£ã‹ã«ã‚ャッシュã—ã¦ãŠã - const instance = await Instance.findOne({ host: extractDbHost(uri) }); - if (instance && instance.isBlocked) return; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(extractDbHost(uri))) return; // æ—¢ã«åŒã˜URIã‚’æŒã¤ã‚‚ã®ãŒç™»éŒ²ã•ã‚Œã¦ã„ãªã„ã‹ãƒã‚§ãƒƒã‚¯ const exist = await fetchNote(uri); @@ -55,7 +55,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity: //#region Visibility const visibility = getVisibility(activity.to, activity.cc, actor); - let visibleUsers: IUser[] = []; + let visibleUsers: User[] = []; if (visibility == 'specified') { visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri))); } diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts index a10163016cab022a552fc9e66303713677b14512..48e251dd9b4adfcc004684f75eb8434f2e8189f6 100644 --- a/src/remote/activitypub/kernel/block/index.ts +++ b/src/remote/activitypub/kernel/block/index.ts @@ -1,9 +1,9 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; import config from '../../../../config'; import { IBlock } from '../../type'; import block from '../../../../services/blocking/create'; import { apLogger } from '../../logger'; +import { Users } from '../../../../models'; +import { IRemoteUser } from '../../../../models/entities/user'; const logger = apLogger; @@ -18,11 +18,9 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => { return null; } - const blockee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const blockee = await Users.findOne(id.split('/').pop()); - if (blockee === null) { + if (blockee == null) { throw new Error('blockee not found'); } diff --git a/src/remote/activitypub/kernel/create/image.ts b/src/remote/activitypub/kernel/create/image.ts index 9c19abbcc41ee75b39b316ff9085e0386a88f246..7720e8f1bd37d121bdbc0abe2d92968dafa4f8e6 100644 --- a/src/remote/activitypub/kernel/create/image.ts +++ b/src/remote/activitypub/kernel/create/image.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { createImage } from '../../models/image'; export default async function(actor: IRemoteUser, image: any): Promise<void> { diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts index 6e314d0b821ec7558e32939f778e49c56af1772f..0326b591f8f6cb53b8df9342cef19cc5e3843ef7 100644 --- a/src/remote/activitypub/kernel/create/index.ts +++ b/src/remote/activitypub/kernel/create/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import createImage from './image'; import createNote from './note'; import { ICreate } from '../../type'; diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts index 0f874b9fbf9ae7c3fd93b3152b410f6128d6589e..70e61bdf1b7b4be159bb44a41b421387fd3b37e6 100644 --- a/src/remote/activitypub/kernel/create/note.ts +++ b/src/remote/activitypub/kernel/create/note.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { createNote, fetchNote } from '../../models/note'; /** diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts index c9c385b1fa5157476b318cd066c0cb491e8ec5c0..fab5e7ab64e722ab1df8055598091f5978d86358 100644 --- a/src/remote/activitypub/kernel/delete/index.ts +++ b/src/remote/activitypub/kernel/delete/index.ts @@ -1,9 +1,9 @@ import Resolver from '../../resolver'; import deleteNote from './note'; -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IDelete } from '../../type'; import { apLogger } from '../../logger'; +import { Notes } from '../../../../models'; /** * 削除アクティビティをæŒãã¾ã™ @@ -27,7 +27,7 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => { break; case 'Tombstone': - const note = await Note.findOne({ uri }); + const note = await Notes.findOne({ uri }); if (note != null) { deleteNote(actor, uri); } diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts index f67919c56bcc4524749ccddd30cf03bf0f37e8df..b146e68a071e09c063a43bb7c312042cb9c5aec5 100644 --- a/src/remote/activitypub/kernel/delete/note.ts +++ b/src/remote/activitypub/kernel/delete/note.ts @@ -1,20 +1,20 @@ -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import deleteNode from '../../../../services/note/delete'; import { apLogger } from '../../logger'; +import { Notes } from '../../../../models'; const logger = apLogger; export default async function(actor: IRemoteUser, uri: string): Promise<void> { logger.info(`Deleting the Note: ${uri}`); - const note = await Note.findOne({ uri }); + const note = await Notes.findOne({ uri }); if (note == null) { throw new Error('note not found'); } - if (!note.userId.equals(actor._id)) { + if (note.userId !== actor.id) { throw new Error('投稿を削除ã—よã†ã¨ã—ã¦ã„るユーザーã¯æŠ•ç¨¿ã®ä½œæˆè€…ã§ã¯ã‚ã‚Šã¾ã›ã‚“'); } diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts index e2db70b20d30d598719811a0a8ddebcc94f8859a..e6c8833f3a2acbdde30ffbc68271d18ef45e0d98 100644 --- a/src/remote/activitypub/kernel/follow.ts +++ b/src/remote/activitypub/kernel/follow.ts @@ -1,8 +1,8 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../models/user'; +import { IRemoteUser } from '../../../models/entities/user'; import config from '../../../config'; import follow from '../../../services/following/create'; import { IFollow } from '../type'; +import { Users } from '../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; @@ -11,11 +11,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { return null; } - const followee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const followee = await Users.findOne(id.split('/').pop()); - if (followee === null) { + if (followee == null) { throw new Error('followee not found'); } diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts index 4f7a5c91fdf54af35a6aa99dd41eb4d735971924..4a57d0675ed0e307c75056a81ed4a8d64adf4498 100644 --- a/src/remote/activitypub/kernel/index.ts +++ b/src/remote/activitypub/kernel/index.ts @@ -1,5 +1,5 @@ import { Object } from '../type'; -import { IRemoteUser } from '../../../models/user'; +import { IRemoteUser } from '../../../models/entities/user'; import create from './create'; import performDeleteActivity from './delete'; import performUpdateActivity from './update'; diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts index ed35da81332f430ed11d9860d93972c31ee85f93..86dd8fb33d0d718981e04ac6be7a81a07ba8c48d 100644 --- a/src/remote/activitypub/kernel/like.ts +++ b/src/remote/activitypub/kernel/like.ts @@ -1,8 +1,7 @@ -import * as mongo from 'mongodb'; -import Note from '../../../models/note'; -import { IRemoteUser } from '../../../models/user'; +import { IRemoteUser } from '../../../models/entities/user'; import { ILike } from '../type'; import create from '../../../services/note/reaction/create'; +import { Notes } from '../../../models'; export default async (actor: IRemoteUser, activity: ILike) => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; @@ -10,10 +9,10 @@ export default async (actor: IRemoteUser, activity: ILike) => { // Transform: // https://misskey.ex/notes/xxxx to // xxxx - const noteId = new mongo.ObjectID(id.split('/').pop()); + const noteId = id.split('/').pop(); - const note = await Note.findOne({ _id: noteId }); - if (note === null) { + const note = await Notes.findOne(noteId); + if (note == null) { throw new Error(); } diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts index 35cd2ec0c9d31b537d183e311e3ff8d9d93f3fad..b06ae6fb9690a61e989a230c50676f54bdc85f3e 100644 --- a/src/remote/activitypub/kernel/reject/follow.ts +++ b/src/remote/activitypub/kernel/reject/follow.ts @@ -1,8 +1,8 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import config from '../../../../config'; import reject from '../../../../services/following/requests/reject'; import { IFollow } from '../../type'; +import { Users } from '../../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id; @@ -11,11 +11,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { return null; } - const follower = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const follower = await Users.findOne(id.split('/').pop()); - if (follower === null) { + if (follower == null) { throw new Error('follower not found'); } diff --git a/src/remote/activitypub/kernel/reject/index.ts b/src/remote/activitypub/kernel/reject/index.ts index c3585abbb6a689a4c37cf0917085515633b8dfca..8ece5cf174bf9aa6cfa9883ca607f4c3368c7e13 100644 --- a/src/remote/activitypub/kernel/reject/index.ts +++ b/src/remote/activitypub/kernel/reject/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import rejectFollow from './follow'; import { IReject, IFollow } from '../../type'; import { apLogger } from '../../logger'; diff --git a/src/remote/activitypub/kernel/remove/index.ts b/src/remote/activitypub/kernel/remove/index.ts index 91b207c80dcfbb226d5633d87e40ae95bfcf55f5..ae33be59dc28bf2b5d1376e1f66506d08ecdf17d 100644 --- a/src/remote/activitypub/kernel/remove/index.ts +++ b/src/remote/activitypub/kernel/remove/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IRemove } from '../../type'; import { resolveNote } from '../../models/note'; import { removePinned } from '../../../../services/i/pin'; @@ -14,7 +14,7 @@ export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => { if (activity.target === actor.featured) { const note = await resolveNote(activity.object); - await removePinned(actor, note._id); + await removePinned(actor, note.id); return; } diff --git a/src/remote/activitypub/kernel/undo/block.ts b/src/remote/activitypub/kernel/undo/block.ts index 4a22ac79241b5532ca870d7bdd92b75ba739652c..c916a0073790be80607a7a24ce6b83d0517fcbd4 100644 --- a/src/remote/activitypub/kernel/undo/block.ts +++ b/src/remote/activitypub/kernel/undo/block.ts @@ -1,9 +1,9 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; import config from '../../../../config'; import { IBlock } from '../../type'; import unblock from '../../../../services/blocking/delete'; import { apLogger } from '../../logger'; +import { IRemoteUser } from '../../../../models/entities/user'; +import { Users } from '../../../../models'; const logger = apLogger; @@ -18,11 +18,9 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => { return null; } - const blockee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const blockee = await Users.findOne(id.split('/').pop()); - if (blockee === null) { + if (blockee == null) { throw new Error('blockee not found'); } diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts index af06aa5b31ecbed78ade3264128eb7a70b11bc28..cc63a740b14eb76adb35019d14d1bd8fe174bb8e 100644 --- a/src/remote/activitypub/kernel/undo/follow.ts +++ b/src/remote/activitypub/kernel/undo/follow.ts @@ -1,11 +1,9 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; import config from '../../../../config'; import unfollow from '../../../../services/following/delete'; import cancelRequest from '../../../../services/following/requests/cancel'; import { IFollow } from '../../type'; -import FollowRequest from '../../../../models/follow-request'; -import Following from '../../../../models/following'; +import { IRemoteUser } from '../../../../models/entities/user'; +import { Users, FollowRequests, Followings } from '../../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; @@ -14,11 +12,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { return null; } - const followee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const followee = await Users.findOne(id.split('/').pop()); - if (followee === null) { + if (followee == null) { throw new Error('followee not found'); } @@ -26,14 +22,14 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { throw new Error('フォãƒãƒ¼è§£é™¤ã—よã†ã¨ã—ã¦ã„るユーザーã¯ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã§ã¯ã‚ã‚Šã¾ã›ã‚“'); } - const req = await FollowRequest.findOne({ - followerId: actor._id, - followeeId: followee._id + const req = await FollowRequests.findOne({ + followerId: actor.id, + followeeId: followee.id }); - const following = await Following.findOne({ - followerId: actor._id, - followeeId: followee._id + const following = await Followings.findOne({ + followerId: actor.id, + followeeId: followee.id }); if (req) { diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts index 80b44fae04ea51a3173969d2329b847db3f1513b..6376ab93a82c8f8ad39c1592eacf66ec284162bc 100644 --- a/src/remote/activitypub/kernel/undo/index.ts +++ b/src/remote/activitypub/kernel/undo/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IUndo, IFollow, IBlock, ILike } from '../../type'; import unfollow from './follow'; import unblock from './block'; diff --git a/src/remote/activitypub/kernel/undo/like.ts b/src/remote/activitypub/kernel/undo/like.ts index b324ec854cb46f94ac4756b61e74b56be35dc773..f337a0173e4fa9454f460c9dc02831be2a64c9d9 100644 --- a/src/remote/activitypub/kernel/undo/like.ts +++ b/src/remote/activitypub/kernel/undo/like.ts @@ -1,8 +1,7 @@ -import * as mongo from 'mongodb'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { ILike } from '../../type'; -import Note from '../../../../models/note'; import deleteReaction from '../../../../services/note/reaction/delete'; +import { Notes } from '../../../../models'; /** * Process Undo.Like activity @@ -10,10 +9,10 @@ import deleteReaction from '../../../../services/note/reaction/delete'; export default async (actor: IRemoteUser, activity: ILike): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; - const noteId = new mongo.ObjectID(id.split('/').pop()); + const noteId = id.split('/').pop(); - const note = await Note.findOne({ _id: noteId }); - if (note === null) { + const note = await Notes.findOne(noteId); + if (note == null) { throw 'note not found'; } diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts index 49b730391a0a9d45f4e39fba491cb68984a493d3..b8dff733952f47fe233efbcb96dfaed947c63b5b 100644 --- a/src/remote/activitypub/kernel/update/index.ts +++ b/src/remote/activitypub/kernel/update/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IUpdate, IObject } from '../../type'; import { apLogger } from '../../logger'; import { updateQuestion } from '../../models/question'; diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts index 967ee655440ea5913833620277e684349dccf29a..dba915fee9cb3644e3c45209c7084ac8acbfbffa 100644 --- a/src/remote/activitypub/misc/get-note-html.ts +++ b/src/remote/activitypub/misc/get-note-html.ts @@ -1,9 +1,9 @@ -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; import { toHtml } from '../../../mfm/toHtml'; import { parse } from '../../../mfm/parse'; -export default function(note: INote) { - let html = toHtml(parse(note.text), note.mentionedRemoteUsers); +export default function(note: Note) { + let html = toHtml(parse(note.text), JSON.parse(note.mentionedRemoteUsers)); if (html == null) html = '<p>.</p>'; return html; diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts index bd97d13d27d81ab4ee9cf81354608a39cd306bd6..87095acd886aff7bc1d8a2c53c0b648585dc1247 100644 --- a/src/remote/activitypub/models/image.ts +++ b/src/remote/activitypub/models/image.ts @@ -1,16 +1,17 @@ import uploadFromUrl from '../../../services/drive/upload-from-url'; -import { IRemoteUser } from '../../../models/user'; -import DriveFile, { IDriveFile } from '../../../models/drive-file'; +import { IRemoteUser } from '../../../models/entities/user'; import Resolver from '../resolver'; import fetchMeta from '../../../misc/fetch-meta'; import { apLogger } from '../logger'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles } from '../../../models'; const logger = apLogger; /** * Imageを作æˆã—ã¾ã™ã€‚ */ -export async function createImage(actor: IRemoteUser, value: any): Promise<IDriveFile> { +export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> { // 投稿者ãŒå‡çµã•ã‚Œã¦ã„ãŸã‚‰ã‚¹ã‚ップ if (actor.isSuspended) { return null; @@ -39,18 +40,16 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv throw e; } - if (file.metadata.isRemote) { + if (file.isRemote) { // URLãŒç•°ãªã£ã¦ã„ã‚‹å ´åˆã€åŒã˜ç”»åƒãŒä»¥å‰ã«ç•°ãªã‚‹URLã§ç™»éŒ²ã•ã‚Œã¦ã„ãŸã¨ã„ã†ã“ã¨ãªã®ã§ã€ // URLã‚’æ›´æ–°ã™ã‚‹ - if (file.metadata.url !== image.url) { - file = await DriveFile.findOneAndUpdate({ _id: file._id }, { - $set: { - 'metadata.url': image.url, - 'metadata.uri': image.url - } - }, { - returnNewDocument: true + if (file.url !== image.url) { + await DriveFiles.update({ id: file.id }, { + url: image.url, + uri: image.url }); + + file = DriveFiles.findOne(file.id); } } @@ -63,7 +62,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv * Misskeyã«å¯¾è±¡ã®ImageãŒç™»éŒ²ã•ã‚Œã¦ã„ã‚Œã°ãれを返ã—ã€ãã†ã§ãªã‘れ㰠* リモートサーãƒãƒ¼ã‹ã‚‰ãƒ•ã‚§ãƒƒãƒã—ã¦Misskeyã«ç™»éŒ²ã—ãれを返ã—ã¾ã™ã€‚ */ -export async function resolveImage(actor: IRemoteUser, value: any): Promise<IDriveFile> { +export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> { // TODO // リモートサーãƒãƒ¼ã‹ã‚‰ãƒ•ã‚§ãƒƒãƒã—ã¦ãã¦ç™»éŒ² diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 6251621527cb0a891f73233670b7001cad95700a..cd587c51cf4ff1757ed31e0608fa61d3a60991ed 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -1,26 +1,27 @@ -import * as mongo from 'mongodb'; import * as promiseLimit from 'promise-limit'; import config from '../../../config'; import Resolver from '../resolver'; -import Note, { INote } from '../../../models/note'; import post from '../../../services/note/create'; -import { INote as INoteActivityStreamsObject, IObject } from '../type'; import { resolvePerson, updatePerson } from './person'; import { resolveImage } from './image'; -import { IRemoteUser, IUser } from '../../../models/user'; +import { IRemoteUser, User } from '../../../models/entities/user'; import { fromHtml } from '../../../mfm/fromHtml'; -import Emoji, { IEmoji } from '../../../models/emoji'; import { ITag, extractHashtags } from './tag'; import { toUnicode } from 'punycode'; import { unique, concat, difference } from '../../../prelude/array'; import { extractPollFromQuestion } from './question'; import vote from '../../../services/note/polls/vote'; import { apLogger } from '../logger'; -import { IDriveFile } from '../../../models/drive-file'; +import { DriveFile } from '../../../models/entities/drive-file'; import { deliverQuestionUpdate } from '../../../services/note/polls/update'; -import Instance from '../../../models/instance'; import { extractDbHost } from '../../../misc/convert-host'; +import { Notes, Emojis, Polls } from '../../../models'; +import { Note } from '../../../models/entities/note'; +import { IObject, INote } from '../type'; +import { Emoji } from '../../../models/entities/emoji'; +import { genId } from '../../../misc/gen-id'; +import fetchMeta from '../../../misc/fetch-meta'; const logger = apLogger; @@ -29,17 +30,17 @@ const logger = apLogger; * * Misskeyã«å¯¾è±¡ã®NoteãŒç™»éŒ²ã•ã‚Œã¦ã„ã‚Œã°ãれを返ã—ã¾ã™ã€‚ */ -export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<INote> { +export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note> { const uri = typeof value == 'string' ? value : value.id; // URIãŒã“ã®ã‚µãƒ¼ãƒãƒ¼ã‚’指ã—ã¦ã„ã‚‹ãªã‚‰ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã‹ã‚‰ãƒ•ã‚§ãƒƒãƒ if (uri.startsWith(config.url + '/')) { - const id = new mongo.ObjectID(uri.split('/').pop()); - return await Note.findOne({ _id: id }); + const id = uri.split('/').pop(); + return await Notes.findOne(id); } //#region ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«æ—¢ã«ç™»éŒ²ã•ã‚Œã¦ã„ãŸã‚‰ãれを返㙠- const exist = await Note.findOne({ uri }); + const exist = await Notes.findOne({ uri }); if (exist) { return exist; @@ -52,7 +53,7 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P /** * Noteを作æˆã—ã¾ã™ã€‚ */ -export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> { +export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note> { if (resolver == null) resolver = new Resolver(); const object: any = await resolver.resolve(value); @@ -68,7 +69,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false return null; } - const note: INoteActivityStreamsObject = object; + const note: INote = object; logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); @@ -87,7 +88,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc; let visibility = 'public'; - let visibleUsers: IUser[] = []; + let visibleUsers: User[] = []; if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) { if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) { visibility = 'home'; @@ -113,12 +114,12 @@ export async function createNote(value: any, resolver?: Resolver, silent = false note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; const files = note.attachment .map(attach => attach.sensitive = note.sensitive) - ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>))) + ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<DriveFile>))) .filter(image => image != null) : []; // リプライ - const reply: INote = note.inReplyTo + const reply: Note = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver).catch(e => { // 4xxã®å ´åˆã¯ãƒªãƒ—ライã—ã¦ãªã„ã“ã¨ã«ã™ã‚‹ if (e.statusCode >= 400 && e.statusCode < 500) { @@ -131,7 +132,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false : null; // 引用 - let quote: INote; + let quote: Note; if (note._misskey_quote && typeof note._misskey_quote == 'string') { quote = await resolveNote(note._misskey_quote).catch(e => { @@ -151,22 +152,23 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const text = note._misskey_content || fromHtml(note.content); // vote - if (reply && reply.poll) { + if (reply && reply.hasPoll) { + const poll = await Polls.findOne({ noteId: reply.id }); const tryCreateVote = async (name: string, index: number): Promise<null> => { - if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) { + if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); } else if (index >= 0) { logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); await vote(actor, reply, index); // リモートフォãƒãƒ¯ãƒ¼ã«Updateé…ä¿¡ - deliverQuestionUpdate(reply._id); + deliverQuestionUpdate(reply.id); } return null; }; if (note.name) { - return await tryCreateVote(note.name, reply.poll.choices.findIndex(x => x.text === note.name)); + return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); } // 後方互æ›æ€§ã®ãŸã‚ @@ -181,7 +183,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const emojis = await extractEmojis(note.tag, actor.host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const apEmojis = emojis.map(emoji => emoji.name); @@ -222,13 +224,13 @@ export async function createNote(value: any, resolver?: Resolver, silent = false * Misskeyã«å¯¾è±¡ã®NoteãŒç™»éŒ²ã•ã‚Œã¦ã„ã‚Œã°ãれを返ã—ã€ãã†ã§ãªã‘れ㰠* リモートサーãƒãƒ¼ã‹ã‚‰ãƒ•ã‚§ãƒƒãƒã—ã¦Misskeyã«ç™»éŒ²ã—ãれを返ã—ã¾ã™ã€‚ */ -export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> { +export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note> { const uri = typeof value == 'string' ? value : value.id; // ブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– // TODO: ã„ã¡ã„ã¡ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã®ã¯ã‚³ã‚¹ãƒˆé«˜ãã†ãªã®ã§ã©ã£ã‹ã«ã‚ャッシュã—ã¦ãŠã - const instance = await Instance.findOne({ host: extractDbHost(uri) }); - if (instance && instance.isBlocked) throw { statusCode: 451 }; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 }; //#region ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«æ—¢ã«ç™»éŒ²ã•ã‚Œã¦ã„ãŸã‚‰ãれを返㙠const exist = await fetchNote(uri); @@ -255,7 +257,7 @@ export async function extractEmojis(tags: ITag[], host_: string) { eomjiTags.map(async tag => { const name = tag.name.replace(/^:/, '').replace(/:$/, ''); - const exists = await Emoji.findOne({ + const exists = await Emojis.findOne({ host, name }); @@ -263,31 +265,37 @@ export async function extractEmojis(tags: ITag[], host_: string) { if (exists) { if ((tag.updated != null && exists.updatedAt == null) || (tag.id != null && exists.uri == null) - || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)) { - return await Emoji.findOneAndUpdate({ - host, - name, - }, { - $set: { - uri: tag.id, - url: tag.icon.url, - updatedAt: new Date(tag.updated), - } - }); + || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) + ) { + await Emojis.update({ + host, + name, + }, { + uri: tag.id, + url: tag.icon.url, + updatedAt: new Date(tag.updated), + }); + + return await Emojis.findOne({ + host, + name + }); } + return exists; } logger.info(`register emoji host=${host}, name=${name}`); - return await Emoji.insert({ + return await Emojis.save({ + id: genId(), host, name, uri: tag.id, url: tag.icon.url, updatedAt: tag.updated ? new Date(tag.updated) : undefined, aliases: [] - }); + } as Emoji); }) ); } @@ -298,7 +306,7 @@ async function extractMentionedUsers(actor: IRemoteUser, to: string[], cc: strin const limit = promiseLimit(2); const users = await Promise.all( - uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<IUser>) + uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<User>) ); return users.filter(x => x != null); diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index d27c9379886aaf91925f504c65388c53ac3f272a..51a9efa10b091e1b2174f9b0ec5375c79fb755e1 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -1,29 +1,29 @@ -import * as mongo from 'mongodb'; import * as promiseLimit from 'promise-limit'; import { toUnicode } from 'punycode'; import config from '../../../config'; -import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user'; import Resolver from '../resolver'; import { resolveImage } from './image'; import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type'; -import { IDriveFile } from '../../../models/drive-file'; -import Meta from '../../../models/meta'; +import { DriveFile } from '../../../models/entities/drive-file'; import { fromHtml } from '../../../mfm/fromHtml'; -import usersChart from '../../../services/chart/users'; -import instanceChart from '../../../services/chart/instance'; import { URL } from 'url'; import { resolveNote, extractEmojis } from './note'; import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc'; -import Instance from '../../../models/instance'; -import getDriveFileUrl from '../../../misc/get-drive-file-url'; -import { IEmoji } from '../../../models/emoji'; import { ITag, extractHashtags } from './tag'; -import Following from '../../../models/following'; import { IIdentifier } from './identifier'; import { apLogger } from '../logger'; -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; import { updateHashtag } from '../../../services/update-hashtag'; +import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserServiceLinkings, UserPublickeys } from '../../../models'; +import { User, IRemoteUser } from '../../../models/entities/user'; +import { Emoji } from '../../../models/entities/emoji'; +import { UserNotePining } from '../../../models/entities/user-note-pinings'; +import { genId } from '../../../misc/gen-id'; +import { UserServiceLinking } from '../../../models/entities/user-service-linking'; +import { instanceChart, usersChart } from '../../../services/chart'; +import { UserPublickey } from '../../../models/entities/user-publickey'; +import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; const logger = apLogger; /** @@ -50,11 +50,11 @@ function validatePerson(x: any, uri: string) { return new Error('invalid person: inbox is not a string'); } - if (!validateUsername(x.preferredUsername, true)) { + if (!Users.validateUsername(x.preferredUsername, true)) { return new Error('invalid person: invalid username'); } - if (!isValidName(x.name == '' ? null : x.name)) { + if (!Users.isValidName(x.name == '' ? null : x.name)) { return new Error('invalid person: invalid name'); } @@ -84,17 +84,17 @@ function validatePerson(x: any, uri: string) { * * Misskeyã«å¯¾è±¡ã®PersonãŒç™»éŒ²ã•ã‚Œã¦ã„ã‚Œã°ãれを返ã—ã¾ã™ã€‚ */ -export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> { +export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User> { if (typeof uri !== 'string') throw 'uri is not string'; // URIãŒã“ã®ã‚µãƒ¼ãƒãƒ¼ã‚’指ã—ã¦ã„ã‚‹ãªã‚‰ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã‹ã‚‰ãƒ•ã‚§ãƒƒãƒ if (uri.startsWith(config.url + '/')) { - const id = new mongo.ObjectID(uri.split('/').pop()); - return await User.findOne({ _id: id }); + const id = uri.split('/').pop(); + return await Users.findOne(id); } //#region ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«æ—¢ã«ç™»éŒ²ã•ã‚Œã¦ã„ãŸã‚‰ãれを返㙠- const exist = await User.findOne({ uri }); + const exist = await Users.findOne({ uri }); if (exist) { return exist; @@ -107,7 +107,7 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUs /** * Personを作æˆã—ã¾ã™ã€‚ */ -export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> { +export async function createPerson(uri: string, resolver?: Resolver): Promise<User> { if (typeof uri !== 'string') throw 'uri is not string'; if (resolver == null) resolver = new Resolver(); @@ -124,21 +124,6 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU logger.info(`Creating the Person: ${person.id}`); - const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([ - resolver.resolve(person.followers).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.following).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.outbox).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ) - ]); - const host = toUnicode(new URL(object.id).hostname.toLowerCase()); const { fields, services } = analyzeAttachments(person.attachment); @@ -150,24 +135,18 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU // Create user let user: IRemoteUser; try { - user = await User.insert({ + user = await Users.save({ + id: genId(), avatarId: null, bannerId: null, - createdAt: Date.parse(person.published) || null, + createdAt: Date.parse(person.published) || new Date(), lastFetchedAt: new Date(), description: fromHtml(person.summary), - followersCount, - followingCount, - notesCount, name: person.name, isLocked: person.manuallyApprovesFollowers, username: person.preferredUsername, usernameLower: person.preferredUsername.toLowerCase(), host, - publicKey: { - id: person.publicKey.id, - publicKeyPem: person.publicKey.publicKeyPem - }, inbox: person.inbox, sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), featured: person.featured, @@ -179,10 +158,22 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU tags, isBot, isCat: (person as any).isCat === true - }) as IRemoteUser; + } as Partial<User>) as IRemoteUser; + + await UserPublickeys.save({ + id: genId(), + userId: user.id, + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem + } as UserPublickey); + + await UserServiceLinkings.save({ + id: genId(), + userId: user.id, + } as UserServiceLinking); } catch (e) { // duplicate key error - if (e.code === 11000) { + if (isDuplicateKeyValueError(e)) { throw new Error('already registered'); } @@ -190,33 +181,25 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU throw e; } + await UserServiceLinkings.save({ + id: genId(), + userId: user.id + } as UserServiceLinking); + // Register host registerOrFetchInstanceDoc(host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - usersCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'usersCount', 1); instanceChart.newUser(i.host); }); - //#region Increment users count - Meta.update({}, { - $inc: { - 'stats.usersCount': 1 - } - }, { upsert: true }); - usersChart.update(user, true); - //#endregion // ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°æ›´æ–° for (const tag of tags) updateHashtag(user, tag, true, true); for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false); //#region アイコンã¨ãƒ˜ãƒƒãƒ€ãƒ¼ç”»åƒã‚’フェッム- const [avatar, banner] = (await Promise.all<IDriveFile>([ + const [avatar, banner] = (await Promise.all<DriveFile>([ person.icon, person.image ].map(img => @@ -225,22 +208,20 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU : resolveImage(user, img).catch(() => null) ))); - const avatarId = avatar ? avatar._id : null; - const bannerId = banner ? banner._id : null; - const avatarUrl = getDriveFileUrl(avatar, true); - const bannerUrl = getDriveFileUrl(banner, false); - const avatarColor = avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null; - const bannerColor = banner && avatar.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null; - - await User.update({ _id: user._id }, { - $set: { - avatarId, - bannerId, - avatarUrl, - bannerUrl, - avatarColor, - bannerColor - } + const avatarId = avatar ? avatar.id : null; + const bannerId = banner ? banner.id : null; + const avatarUrl = DriveFiles.getPublicUrl(avatar); + const bannerUrl = DriveFiles.getPublicUrl(banner); + const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null; + const bannerColor = banner && avatar.properties.avgColor ? banner.properties.avgColor : null; + + await Users.update(user.id, { + avatarId, + bannerId, + avatarUrl, + bannerUrl, + avatarColor, + bannerColor }); user.avatarId = avatarId; @@ -254,19 +235,17 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU //#region カスタム絵文å—å–å¾— const emojis = await extractEmojis(person.tag, host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const emojiNames = emojis.map(emoji => emoji.name); - await User.update({ _id: user._id }, { - $set: { - emojis: emojiNames - } + await Users.update(user.id, { + emojis: emojiNames }); //#endregion - await updateFeatured(user._id).catch(err => logger.error(err)); + await updateFeatured(user.id).catch(err => logger.error(err)); return user; } @@ -287,7 +266,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje } //#region ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«æ—¢ã«ç™»éŒ²ã•ã‚Œã¦ã„ã‚‹ã‹ - const exist = await User.findOne({ uri }) as IRemoteUser; + const exist = await Users.findOne({ uri }) as IRemoteUser; if (exist == null) { return; @@ -295,10 +274,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje //#endregion // 繋ãŒã‚‰ãªã„インスタンスã«ä½•å›žã‚‚試行ã™ã‚‹ã®ã‚’防ã, 後続ã®åŒæ§˜å‡¦ç†ã®é€£ç¶šè©¦è¡Œã‚’防ã ãŸã‚ 試行å‰ã«ã‚‚æ›´æ–°ã™ã‚‹ - await User.update({ _id: exist._id }, { - $set: { - lastFetchedAt: new Date(), - }, + await Users.update(exist.id, { + lastFetchedAt: new Date(), }); if (resolver == null) resolver = new Resolver(); @@ -315,23 +292,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje logger.info(`Updating the Person: ${person.id}`); - const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([ - resolver.resolve(person.followers).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.following).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.outbox).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ) - ]); - // アイコンã¨ãƒ˜ãƒƒãƒ€ãƒ¼ç”»åƒã‚’フェッム- const [avatar, banner] = (await Promise.all<IDriveFile>([ + const [avatar, banner] = (await Promise.all<DriveFile>([ person.icon, person.image ].map(img => @@ -343,7 +305,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje // カスタム絵文å—å–å¾— const emojis = await extractEmojis(person.tag, exist.host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const emojiNames = emojis.map(emoji => emoji.name); @@ -359,40 +321,45 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje featured: person.featured, emojis: emojiNames, description: fromHtml(person.summary), - followersCount, - followingCount, - notesCount, name: person.name, url: person.url, endpoints: person.endpoints, fields, - ...services, tags, isBot: object.type == 'Service', isCat: (person as any).isCat === true, isLocked: person.manuallyApprovesFollowers, - createdAt: Date.parse(person.published) || null, - publicKey: { - id: person.publicKey.id, - publicKeyPem: person.publicKey.publicKeyPem - }, - } as any; + createdAt: new Date(Date.parse(person.published)) || null, + } as Partial<User>; if (avatar) { - updates.avatarId = avatar._id; - updates.avatarUrl = getDriveFileUrl(avatar, true); - updates.avatarColor = avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null; + updates.avatarId = avatar.id; + updates.avatarUrl = DriveFiles.getPublicUrl(avatar); + updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null; } if (banner) { - updates.bannerId = banner._id; - updates.bannerUrl = getDriveFileUrl(banner, true); - updates.bannerColor = banner.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null; + updates.bannerId = banner.id; + updates.bannerUrl = DriveFiles.getPublicUrl(banner); + updates.bannerColor = banner.properties.avgColor ? banner.properties.avgColor : null; } // Update user - await User.update({ _id: exist._id }, { - $set: updates + await Users.update(exist.id, updates); + + await UserPublickeys.update({ userId: exist.id }, { + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem + }); + + await UserServiceLinkings.update({ userId: exist.id }, { + twitterUserId: services.twitter.userId, + twitterScreenName: services.twitter.screenName, + githubId: services.github.id, + githubLogin: services.github.login, + discordId: services.discord.id, + discordUsername: services.discord.username, + discordDiscriminator: services.discord.discriminator, }); // ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°æ›´æ–° @@ -400,17 +367,13 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje for (const tag of (exist.tags || []).filter(x => !tags.includes(x))) updateHashtag(exist, tag, true, false); // 該当ユーザーãŒæ—¢ã«ãƒ•ã‚©ãƒãƒ¯ãƒ¼ã«ãªã£ã¦ã„ãŸå ´åˆã¯Followingもアップデートã™ã‚‹ - await Following.update({ - followerId: exist._id - }, { - $set: { - '_follower.sharedInbox': person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined) - } + await Followings.update({ + followerId: exist.id }, { - multi: true + followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined) }); - await updateFeatured(exist._id).catch(err => logger.error(err)); + await updateFeatured(exist.id).catch(err => logger.error(err)); } /** @@ -419,7 +382,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje * Misskeyã«å¯¾è±¡ã®PersonãŒç™»éŒ²ã•ã‚Œã¦ã„ã‚Œã°ãれを返ã—ã€ãã†ã§ãªã‘れ㰠* リモートサーãƒãƒ¼ã‹ã‚‰ãƒ•ã‚§ãƒƒãƒã—ã¦Misskeyã«ç™»éŒ²ã—ãれを返ã—ã¾ã™ã€‚ */ -export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> { +export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<User> { if (typeof uri !== 'string') throw 'uri is not string'; //#region ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«æ—¢ã«ç™»éŒ²ã•ã‚Œã¦ã„ãŸã‚‰ãれを返㙠@@ -492,9 +455,9 @@ export function analyzeAttachments(attachments: ITag[]) { return { fields, services }; } -export async function updateFeatured(userId: mongo.ObjectID) { - const user = await User.findOne({ _id: userId }); - if (!isRemoteUser(user)) return; +export async function updateFeatured(userId: User['id']) { + const user = await Users.findOne(userId); + if (!Users.isRemoteUser(user)) return; if (!user.featured) return; logger.info(`Updating the featured: ${user.uri}`); @@ -515,11 +478,14 @@ export async function updateFeatured(userId: mongo.ObjectID) { const featuredNotes = await Promise.all(items .filter(item => item.type === 'Note') .slice(0, 5) - .map(item => limit(() => resolveNote(item, resolver)) as Promise<INote>)); - - await User.update({ _id: user._id }, { - $set: { - pinnedNoteIds: featuredNotes.filter(note => note != null).map(note => note._id) - } - }); + .map(item => limit(() => resolveNote(item, resolver)) as Promise<Note>)); + + for (const note of featuredNotes.filter(note => note != null)) { + UserNotePinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + noteId: note.id + } as UserNotePining); + } } diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts index c073684349ccadde6777c3be9152240fba191c08..a5091a6d96d3cca6dd99f55c25a4aa3c9f8fa761 100644 --- a/src/remote/activitypub/models/question.ts +++ b/src/remote/activitypub/models/question.ts @@ -1,8 +1,9 @@ import config from '../../../config'; -import Note, { IChoice, IPoll } from '../../../models/note'; import Resolver from '../resolver'; import { IQuestion } from '../type'; import { apLogger } from '../logger'; +import { Notes, Polls } from '../../../models'; +import { IPoll } from '../../../models/entities/poll'; export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> { const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source; @@ -14,14 +15,14 @@ export async function extractPollFromQuestion(source: string | IQuestion): Promi } const choices = question[multiple ? 'anyOf' : 'oneOf'] - .map((x, i) => ({ - id: i, - text: x.name, - votes: x.replies && x.replies.totalItems || x._misskey_votes || 0, - } as IChoice)); + .map((x, i) => x.name); + + const votes = question[multiple ? 'anyOf' : 'oneOf'] + .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0); return { choices, + votes, multiple, expiresAt }; @@ -39,9 +40,11 @@ export async function updateQuestion(value: any) { if (uri.startsWith(config.url + '/')) throw 'uri points local'; //#region ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«æ—¢ã«ç™»éŒ²ã•ã‚Œã¦ã„ã‚‹ã‹ - const note = await Note.findOne({ uri }); - + const note = await Notes.findOne({ uri }); if (note == null) throw 'Question is not registed'; + + const poll = await Polls.findOne({ noteId: note.id }); + if (poll == null) throw 'Question is not registed'; //#endregion // resolve new Question object @@ -52,27 +55,25 @@ export async function updateQuestion(value: any) { if (question.type !== 'Question') throw 'object is not a Question'; const apChoices = question.oneOf || question.anyOf; - const dbChoices = note.poll.choices; let changed = false; - for (const db of dbChoices) { - const oldCount = db.votes; - const newCount = apChoices.filter(ap => ap.name === db.text)[0].replies.totalItems; + for (const choice of poll.choices) { + const oldCount = poll.votes[poll.choices.indexOf(choice)]; + const newCount = apChoices.filter(ap => ap.name === choice)[0].replies.totalItems; if (oldCount != newCount) { changed = true; - db.votes = newCount; + poll.votes[poll.choices.indexOf(choice)] = newCount; } } - await Note.update({ - _id: note._id - }, { - $set: { - 'poll.choices': dbChoices, - updatedAt: new Date(), - } + await Notes.update(note.id, { + updatedAt: new Date(), + }); + + await Polls.update(poll.id, { + votes: poll.votes }); return changed; diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts index 2e4f53adf5bb62156f6e163d24c8bcf74768862e..425adaec9686aa5769145d6c44b0fddf7a655a8c 100644 --- a/src/remote/activitypub/perform.ts +++ b/src/remote/activitypub/perform.ts @@ -1,5 +1,5 @@ import { Object } from './type'; -import { IRemoteUser } from '../../models/user'; +import { IRemoteUser } from '../../models/entities/user'; import kernel from './kernel'; export default async (actor: IRemoteUser, activity: Object): Promise<void> => { diff --git a/src/remote/activitypub/renderer/accept.ts b/src/remote/activitypub/renderer/accept.ts index fdbdff3f12b5ce861a72b475ef5353ad5f451840..21b462907455a4c4f8d0d78db9240056df192684 100644 --- a/src/remote/activitypub/renderer/accept.ts +++ b/src/remote/activitypub/renderer/accept.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => ({ type: 'Accept', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/add.ts b/src/remote/activitypub/renderer/add.ts index 4d6fe392aa3c051a21f449e4a384154f504fbc53..46f937f61dbe6d017c2dc45fb13757bea0b70576 100644 --- a/src/remote/activitypub/renderer/add.ts +++ b/src/remote/activitypub/renderer/add.ts @@ -1,9 +1,9 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (user: ILocalUser, target: any, object: any) => ({ type: 'Add', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, target, object }); diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts index f6f2f9bdcd39b886dccbdf1b100f3d595e6525ac..11e7be449b9261a1e3c61ae597c5d2ba173914cb 100644 --- a/src/remote/activitypub/renderer/announce.ts +++ b/src/remote/activitypub/renderer/announce.ts @@ -1,7 +1,7 @@ import config from '../../../config'; -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; -export default (object: any, note: INote) => { +export default (object: any, note: Note) => { const attributedTo = `${config.url}/users/${note.userId}`; let to: string[] = []; @@ -18,7 +18,7 @@ export default (object: any, note: INote) => { } return { - id: `${config.url}/notes/${note._id}/activity`, + id: `${config.url}/notes/${note.id}/activity`, actor: `${config.url}/users/${note.userId}`, type: 'Announce', published: note.createdAt.toISOString(), diff --git a/src/remote/activitypub/renderer/block.ts b/src/remote/activitypub/renderer/block.ts index 694f3a1418e8cb7ec6b1e3766884fb597bfa3875..946c45a813d8171846fd833a4e25240d6012425e 100644 --- a/src/remote/activitypub/renderer/block.ts +++ b/src/remote/activitypub/renderer/block.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser, IRemoteUser } from '../../../models/user'; +import { ILocalUser, IRemoteUser } from '../../../models/entities/user'; export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({ type: 'Block', - actor: `${config.url}/users/${blocker._id}`, + actor: `${config.url}/users/${blocker.id}`, object: blockee.uri }); diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts index 1ee1418fce317c61d7becdeb21ef51e3bd58bf92..e1fc0515c8e34292f5e8f89617c0935b349a33aa 100644 --- a/src/remote/activitypub/renderer/create.ts +++ b/src/remote/activitypub/renderer/create.ts @@ -1,9 +1,9 @@ import config from '../../../config'; -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; -export default (object: any, note: INote) => { +export default (object: any, note: Note) => { const activity = { - id: `${config.url}/notes/${note._id}/activity`, + id: `${config.url}/notes/${note.id}/activity`, actor: `${config.url}/users/${note.userId}`, type: 'Create', published: note.createdAt.toISOString(), diff --git a/src/remote/activitypub/renderer/delete.ts b/src/remote/activitypub/renderer/delete.ts index e090e1c88618298aee55e4398bf87cb1c732a6ae..a98c97e6e915cecd41aef734d781a4f2d9ebec96 100644 --- a/src/remote/activitypub/renderer/delete.ts +++ b/src/remote/activitypub/renderer/delete.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => ({ type: 'Delete', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts index 17721e9417308e76c96dbc090e8a5f3b61447e55..4f6ea8c4eea05b70c3a60257e5e1892e185ee3a9 100644 --- a/src/remote/activitypub/renderer/document.ts +++ b/src/remote/activitypub/renderer/document.ts @@ -1,8 +1,8 @@ -import { IDriveFile } from '../../../models/drive-file'; -import getDriveFileUrl from '../../../misc/get-drive-file-url'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles } from '../../../models'; -export default (file: IDriveFile) => ({ +export default (file: DriveFile) => ({ type: 'Document', - mediaType: file.contentType, - url: getDriveFileUrl(file) + mediaType: file.type, + url: DriveFiles.getPublicUrl(file) }); diff --git a/src/remote/activitypub/renderer/emoji.ts b/src/remote/activitypub/renderer/emoji.ts index 1a05b4e89e78589c57c25336bd6f7ab5d31c4216..947a96df37c8333c227fe426e8d2bb05c08e882b 100644 --- a/src/remote/activitypub/renderer/emoji.ts +++ b/src/remote/activitypub/renderer/emoji.ts @@ -1,7 +1,7 @@ -import { IEmoji } from '../../../models/emoji'; import config from '../../../config'; +import { Emoji } from '../../../models/entities/emoji'; -export default (emoji: IEmoji) => ({ +export default (emoji: Emoji) => ({ id: `${config.url}/emojis/${emoji.name}`, type: 'Emoji', name: `:${emoji.name}:`, diff --git a/src/remote/activitypub/renderer/follow-user.ts b/src/remote/activitypub/renderer/follow-user.ts index 9a488d392b4fb053d74b2ae60ee42baa902cc775..9446be3c8618f5d92a510d472d2ff28ac4b7af32 100644 --- a/src/remote/activitypub/renderer/follow-user.ts +++ b/src/remote/activitypub/renderer/follow-user.ts @@ -1,16 +1,12 @@ import config from '../../../config'; -import * as mongo from 'mongodb'; -import User, { isLocalUser } from '../../../models/user'; +import { Users } from '../../../models'; +import { User } from '../../../models/entities/user'; /** * Convert (local|remote)(Follower|Followee)ID to URL * @param id Follower|Followee ID */ -export default async function renderFollowUser(id: mongo.ObjectID): Promise<any> { - - const user = await User.findOne({ - _id: id - }); - - return isLocalUser(user) ? `${config.url}/users/${user._id}` : user.uri; +export default async function renderFollowUser(id: User['id']): Promise<any> { + const user = await Users.findOne(id); + return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri; } diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts index 98d4cdd020aac11062ca4bed78bed1e6f3e52b04..400b15ec7bedba180bbccd5821232ad3c41f62b6 100644 --- a/src/remote/activitypub/renderer/follow.ts +++ b/src/remote/activitypub/renderer/follow.ts @@ -1,11 +1,12 @@ import config from '../../../config'; -import { IUser, isLocalUser } from '../../../models/user'; +import { User } from '../../../models/entities/user'; +import { Users } from '../../../models'; -export default (follower: IUser, followee: IUser, requestId?: string) => { +export default (follower: User, followee: User, requestId?: string) => { const follow = { type: 'Follow', - actor: isLocalUser(follower) ? `${config.url}/users/${follower._id}` : follower.uri, - object: isLocalUser(followee) ? `${config.url}/users/${followee._id}` : followee.uri + actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri, + object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri } as any; if (requestId) follow.id = requestId; diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts index ec637b9521dde0ac304d535a0d89ce842c268272..ce98f98c629da4914894f821f206e6e817c7b44c 100644 --- a/src/remote/activitypub/renderer/image.ts +++ b/src/remote/activitypub/renderer/image.ts @@ -1,8 +1,8 @@ -import { IDriveFile } from '../../../models/drive-file'; -import getDriveFileUrl from '../../../misc/get-drive-file-url'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles } from '../../../models'; -export default (file: IDriveFile) => ({ +export default (file: DriveFile) => ({ type: 'Image', - url: getDriveFileUrl(file), - sensitive: file.metadata.isSensitive + url: DriveFiles.getPublicUrl(file), + sensitive: file.isSensitive }); diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts index 0d5e52557c2da519eeed2fb01044382fd4d24c52..fb5975a6c42832ca5cdb44feb4a38e38066356df 100644 --- a/src/remote/activitypub/renderer/key.ts +++ b/src/remote/activitypub/renderer/key.ts @@ -1,10 +1,11 @@ +import { createPublicKey } from 'crypto'; import config from '../../../config'; -import { extractPublic } from '../../../crypto_key'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; +import { UserKeypair } from '../../../models/entities/user-keypair'; -export default (user: ILocalUser) => ({ - id: `${config.url}/users/${user._id}/publickey`, +export default (user: ILocalUser, key: UserKeypair) => ({ + id: `${config.url}/users/${user.id}/publickey`, type: 'Key', - owner: `${config.url}/users/${user._id}`, - publicKeyPem: extractPublic(user.keypair) + owner: `${config.url}/users/${user.id}`, + publicKeyPem: createPublicKey(key.keyPem) }); diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts index 523cb4f1ad3a64d931525ac34c43a90df1b09ae4..01f10ec0a905632f183186442a70d1b9e1293aec 100644 --- a/src/remote/activitypub/renderer/like.ts +++ b/src/remote/activitypub/renderer/like.ts @@ -1,10 +1,10 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; -import { INote } from '../../../models/note'; +import { ILocalUser } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; -export default (user: ILocalUser, note: INote, reaction: string) => ({ +export default (user: ILocalUser, note: Note, reaction: string) => ({ type: 'Like', - actor: `${config.url}/users/${user._id}`, - object: note.uri ? note.uri : `${config.url}/notes/${note._id}`, + actor: `${config.url}/users/${user.id}`, + object: note.uri ? note.uri : `${config.url}/notes/${note.id}`, _misskey_reaction: reaction }); diff --git a/src/remote/activitypub/renderer/mention.ts b/src/remote/activitypub/renderer/mention.ts index 8d12e6d8bf230b7d38b196747e0363e4af0e4179..889be5d85d1dccf2d219bdde5a67aa901e45a605 100644 --- a/src/remote/activitypub/renderer/mention.ts +++ b/src/remote/activitypub/renderer/mention.ts @@ -1,8 +1,9 @@ -import { IUser, isRemoteUser } from '../../../models/user'; import config from '../../../config'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { Users } from '../../../models'; -export default (mention: IUser) => ({ +export default (mention: User) => ({ type: 'Mention', - href: isRemoteUser(mention) ? mention.uri : `${config.url}/@${mention.username}`, - name: isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${mention.username}`, + href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/@${(mention as ILocalUser).username}`, + name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`, }); diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index 8b349526e174e2e155e9508b271e6c2e3368d086..5b36366b289f7cb4d14f1344f8727ab1591a5642 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -3,29 +3,27 @@ import renderHashtag from './hashtag'; import renderMention from './mention'; import renderEmoji from './emoji'; import config from '../../../config'; -import DriveFile, { IDriveFile } from '../../../models/drive-file'; -import Note, { INote } from '../../../models/note'; -import User from '../../../models/user'; import toHtml from '../misc/get-note-html'; -import Emoji, { IEmoji } from '../../../models/emoji'; - -export default async function renderNote(note: INote, dive = true): Promise<any> { - const promisedFiles: Promise<IDriveFile[]> = note.fileIds - ? DriveFile.find({ _id: { $in: note.fileIds } }) +import { Note, IMentionedRemoteUsers } from '../../../models/entities/note'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles, Notes, Users, Emojis, Polls } from '../../../models'; +import { In } from 'typeorm'; +import { Emoji } from '../../../models/entities/emoji'; +import { Poll } from '../../../models/entities/poll'; + +export default async function renderNote(note: Note, dive = true): Promise<any> { + const promisedFiles: Promise<DriveFile[]> = note.fileIds.length > 1 + ? DriveFiles.find({ id: In(note.fileIds) }) : Promise.resolve([]); let inReplyTo; - let inReplyToNote: INote; + let inReplyToNote: Note; if (note.replyId) { - inReplyToNote = await Note.findOne({ - _id: note.replyId, - }); + inReplyToNote = await Notes.findOne(note.replyId); if (inReplyToNote !== null) { - const inReplyToUser = await User.findOne({ - _id: inReplyToNote.userId, - }); + const inReplyToUser = await Users.findOne(inReplyToNote.userId); if (inReplyToUser !== null) { if (inReplyToNote.uri) { @@ -34,7 +32,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any> if (dive) { inReplyTo = await renderNote(inReplyToNote, false); } else { - inReplyTo = `${config.url}/notes/${inReplyToNote._id}`; + inReplyTo = `${config.url}/notes/${inReplyToNote.id}`; } } } @@ -46,24 +44,20 @@ export default async function renderNote(note: INote, dive = true): Promise<any> let quote; if (note.renoteId) { - const renote = await Note.findOne({ - _id: note.renoteId, - }); + const renote = await Notes.findOne(note.renoteId); if (renote) { - quote = renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`; + quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`; } } - const user = await User.findOne({ - _id: note.userId + const user = await Users.findOne({ + id: note.userId }); - const attributedTo = `${config.url}/users/${user._id}`; + const attributedTo = `${config.url}/users/${user.id}`; - const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0 - ? note.mentionedRemoteUsers.map(x => x.uri) - : []; + const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); let to: string[] = []; let cc: string[] = []; @@ -81,10 +75,8 @@ export default async function renderNote(note: INote, dive = true): Promise<any> to = mentions; } - const mentionedUsers = note.mentions ? await User.find({ - _id: { - $in: note.mentions - } + const mentionedUsers = note.mentions.length > 0 ? await Users.find({ + id: In(note.mentions) }) : []; const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag)); @@ -93,23 +85,28 @@ export default async function renderNote(note: INote, dive = true): Promise<any> const files = await promisedFiles; let text = note.text; + let poll: Poll; + + if (note.hasPoll) { + poll = await Polls.findOne({ noteId: note.id }); + } let question: string; - if (note.poll != null) { + if (poll) { if (text == null) text = ''; - const url = `${config.url}/notes/${note._id}`; + const url = `${config.url}/notes/${note.id}`; // TODO: i18n text += `\n[リモートã§çµæžœã‚’表示](${url})`; - question = `${config.url}/questions/${note._id}`; + question = `${config.url}/questions/${note.id}`; } let apText = text; if (apText == null) apText = ''; // Provides choices as text for AP - if (note.poll != null) { - const cs = note.poll.choices.map(c => `${c.id}: ${c.text}`); + if (poll) { + const cs = poll.choices.map((c, i) => `${i}: ${c}`); apText += '\n----------------------------------------\n'; apText += cs.join('\n'); apText += '\n----------------------------------------\n'; @@ -135,31 +132,25 @@ export default async function renderNote(note: INote, dive = true): Promise<any> ...apemojis, ]; - const { - choices = [], - expiresAt = null, - multiple = false - } = note.poll || {}; - - const asPoll = note.poll ? { + const asPoll = poll ? { type: 'Question', content: toHtml(Object.assign({}, note, { text: text })), _misskey_fallback_content: content, - [expiresAt && expiresAt < new Date() ? 'closed' : 'endTime']: expiresAt, - [multiple ? 'anyOf' : 'oneOf']: choices.map(({ text, votes }) => ({ + [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, + [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ type: 'Note', name: text, replies: { type: 'Collection', - totalItems: votes + totalItems: poll.votes[i] } })) } : {}; return { - id: `${config.url}/notes/${note._id}`, + id: `${config.url}/notes/${note.id}`, type: 'Note', attributedTo, summary, @@ -172,17 +163,17 @@ export default async function renderNote(note: INote, dive = true): Promise<any> cc, inReplyTo, attachment: files.map(renderDocument), - sensitive: files.some(file => file.metadata.isSensitive), + sensitive: files.some(file => file.isSensitive), tag, ...asPoll }; } -export async function getEmojis(names: string[]): Promise<IEmoji[]> { - if (names == null || names.length < 1) return []; +export async function getEmojis(names: string[]): Promise<Emoji[]> { + if (names == null || names.length === 0) return []; const emojis = await Promise.all( - names.map(name => Emoji.findOne({ + names.map(name => Emojis.findOne({ name, host: null })) diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index 77e60cd61ae5c48f66bef70681616ba4c372f980..4c6b518eb608a218b12bf94c6573223b7eed6e98 100644 --- a/src/remote/activitypub/renderer/person.ts +++ b/src/remote/activitypub/renderer/person.ts @@ -1,21 +1,22 @@ import renderImage from './image'; import renderKey from './key'; import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; import { toHtml } from '../../../mfm/toHtml'; import { parse } from '../../../mfm/parse'; -import DriveFile from '../../../models/drive-file'; import { getEmojis } from './note'; import renderEmoji from './emoji'; import { IIdentifier } from '../models/identifier'; import renderHashtag from './hashtag'; +import { DriveFiles, UserServiceLinkings, UserKeypairs } from '../../../models'; -export default async (user: ILocalUser) => { - const id = `${config.url}/users/${user._id}`; +export async function renderPerson(user: ILocalUser) { + const id = `${config.url}/users/${user.id}`; - const [avatar, banner] = await Promise.all([ - DriveFile.findOne({ _id: user.avatarId }), - DriveFile.findOne({ _id: user.bannerId }) + const [avatar, banner, links] = await Promise.all([ + DriveFiles.findOne(user.avatarId), + DriveFiles.findOne(user.bannerId), + UserServiceLinkings.findOne({ userId: user.id }) ]); const attachment: { @@ -26,41 +27,41 @@ export default async (user: ILocalUser) => { identifier?: IIdentifier }[] = []; - if (user.twitter) { + if (links.twitter) { attachment.push({ type: 'PropertyValue', name: 'Twitter', - value: `<a href="https://twitter.com/intent/user?user_id=${user.twitter.userId}" rel="me nofollow noopener" target="_blank"><span>@${user.twitter.screenName}</span></a>`, + value: `<a href="https://twitter.com/intent/user?user_id=${links.twitterUserId}" rel="me nofollow noopener" target="_blank"><span>@${links.twitterScreenName}</span></a>`, identifier: { type: 'PropertyValue', name: 'misskey:authentication:twitter', - value: `${user.twitter.userId}@${user.twitter.screenName}` + value: `${links.twitterUserId}@${links.twitterScreenName}` } }); } - if (user.github) { + if (links.github) { attachment.push({ type: 'PropertyValue', name: 'GitHub', - value: `<a href="https://github.com/${user.github.login}" rel="me nofollow noopener" target="_blank"><span>@${user.github.login}</span></a>`, + value: `<a href="https://github.com/${links.githubLogin}" rel="me nofollow noopener" target="_blank"><span>@${links.githubLogin}</span></a>`, identifier: { type: 'PropertyValue', name: 'misskey:authentication:github', - value: `${user.github.id}@${user.github.login}` + value: `${links.githubId}@${links.githubLogin}` } }); } - if (user.discord) { + if (links.discord) { attachment.push({ type: 'PropertyValue', name: 'Discord', - value: `<a href="https://discordapp.com/users/${user.discord.id}" rel="me nofollow noopener" target="_blank"><span>${user.discord.username}#${user.discord.discriminator}</span></a>`, + value: `<a href="https://discordapp.com/users/${links.discordId}" rel="me nofollow noopener" target="_blank"><span>${links.discordUsername}#${links.discordDiscriminator}</span></a>`, identifier: { type: 'PropertyValue', name: 'misskey:authentication:discord', - value: `${user.discord.id}@${user.discord.username}#${user.discord.discriminator}` + value: `${links.discordId}@${links.discordUsername}#${links.discordDiscriminator}` } }); } @@ -75,6 +76,10 @@ export default async (user: ILocalUser) => { ...hashtagTags, ]; + const keypair = await UserKeypairs.findOne({ + userId: user.id + }); + return { type: user.isBot ? 'Service' : 'Person', id, @@ -93,8 +98,8 @@ export default async (user: ILocalUser) => { image: user.bannerId && renderImage(banner), tag, manuallyApprovesFollowers: user.isLocked, - publicKey: renderKey(user), + publicKey: renderKey(user, keypair), isCat: user.isCat, attachment: attachment.length ? attachment : undefined }; -}; +} diff --git a/src/remote/activitypub/renderer/question.ts b/src/remote/activitypub/renderer/question.ts index cf0bf387c8b2a87aac66ea675a5a9f23e6d34929..6ade10d1bfa48153d40a3d7c954889d6cdc6ca94 100644 --- a/src/remote/activitypub/renderer/question.ts +++ b/src/remote/activitypub/renderer/question.ts @@ -1,19 +1,20 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; -import { INote } from '../../../models/note'; +import { ILocalUser } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { Poll } from '../../../models/entities/poll'; -export default async function renderQuestion(user: ILocalUser, note: INote) { +export default async function renderQuestion(user: ILocalUser, note: Note, poll: Poll) { const question = { type: 'Question', - id: `${config.url}/questions/${note._id}`, - actor: `${config.url}/users/${user._id}`, + id: `${config.url}/questions/${note.id}`, + actor: `${config.url}/users/${user.id}`, content: note.text || '', - [note.poll.multiple ? 'anyOf' : 'oneOf']: note.poll.choices.map(c => ({ - name: c.text, - _misskey_votes: c.votes, + [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ + name: text, + _misskey_votes: poll.votes[i], replies: { type: 'Collection', - totalItems: c.votes + totalItems: poll.votes[i] } })) }; diff --git a/src/remote/activitypub/renderer/reject.ts b/src/remote/activitypub/renderer/reject.ts index 6d7d23708ac8d92952be644dabed36dbcb2b0514..c4e0ba0d0adc6db72c69320de208414c9751dccf 100644 --- a/src/remote/activitypub/renderer/reject.ts +++ b/src/remote/activitypub/renderer/reject.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => ({ type: 'Reject', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/remove.ts b/src/remote/activitypub/renderer/remove.ts index ed840be751be48b5b03a9f77b90b1e1f70c24b92..1b9a6b8c05fda5f1f50ac1d06ba58329551b60c4 100644 --- a/src/remote/activitypub/renderer/remove.ts +++ b/src/remote/activitypub/renderer/remove.ts @@ -1,9 +1,9 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (user: ILocalUser, target: any, object: any) => ({ type: 'Remove', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, target, object }); diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts index dbcf5732be01feca9ec77373c98c6eaa5603a22d..2ff6b61b905c743b08205e0d08be976f4310abe5 100644 --- a/src/remote/activitypub/renderer/undo.ts +++ b/src/remote/activitypub/renderer/undo.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser, IUser } from '../../../models/user'; +import { ILocalUser, User } from '../../../models/entities/user'; -export default (object: any, user: ILocalUser | IUser) => ({ +export default (object: any, user: ILocalUser | User) => ({ type: 'Undo', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts index cf9acc9acb9a64dc5871d92ac33e039e915cfe31..c1d5ba29b243d262aa5fc106ed5fccb65945dd66 100644 --- a/src/remote/activitypub/renderer/update.ts +++ b/src/remote/activitypub/renderer/update.ts @@ -1,10 +1,10 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => { const activity = { - id: `${config.url}/users/${user._id}#updates/${new Date().getTime()}`, - actor: `${config.url}/users/${user._id}`, + id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`, + actor: `${config.url}/users/${user.id}`, type: 'Update', to: [ 'https://www.w3.org/ns/activitystreams#Public' ], object diff --git a/src/remote/activitypub/renderer/vote.ts b/src/remote/activitypub/renderer/vote.ts index 014b76765b8958e29d091f40600f688285b56555..8929c03460bac78865e088423db6df709ff60616 100644 --- a/src/remote/activitypub/renderer/vote.ts +++ b/src/remote/activitypub/renderer/vote.ts @@ -1,22 +1,23 @@ import config from '../../../config'; -import { INote } from '../../../models/note'; -import { IRemoteUser, ILocalUser } from '../../../models/user'; -import { IPollVote } from '../../../models/poll-vote'; +import { Note } from '../../../models/entities/note'; +import { IRemoteUser, ILocalUser } from '../../../models/entities/user'; +import { PollVote } from '../../../models/entities/poll-vote'; +import { Poll } from '../../../models/entities/poll'; -export default async function renderVote(user: ILocalUser, vote: IPollVote, pollNote: INote, pollOwner: IRemoteUser): Promise<any> { +export default async function renderVote(user: ILocalUser, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser): Promise<any> { return { - id: `${config.url}/users/${user._id}#votes/${vote._id}/activity`, - actor: `${config.url}/users/${user._id}`, + id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`, + actor: `${config.url}/users/${user.id}`, type: 'Create', to: [pollOwner.uri], published: new Date().toISOString(), object: { - id: `${config.url}/users/${user._id}#votes/${vote._id}`, + id: `${config.url}/users/${user.id}#votes/${vote.id}`, type: 'Note', - attributedTo: `${config.url}/users/${user._id}`, + attributedTo: `${config.url}/users/${user.id}`, to: [pollOwner.uri], - inReplyTo: pollNote.uri, - name: pollNote.poll.choices.find(x => x.id === vote.choice).text + inReplyTo: note.uri, + name: poll.choices[vote.choice] } }; } diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index 08dd7a6ba9b33e4112518c29fcca59c65825afa7..a089ed371c7705df753b4dada410dbe5f496628a 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -7,10 +7,11 @@ import * as promiseAny from 'promise-any'; import { toUnicode } from 'punycode'; import config from '../../config'; -import { ILocalUser } from '../../models/user'; +import { ILocalUser } from '../../models/entities/user'; import { publishApLogStream } from '../../services/stream'; import { apLogger } from './logger'; -import Instance from '../../models/instance'; +import { UserKeypairs } from '../../models'; +import fetchMeta from '../../misc/fetch-meta'; export const logger = apLogger.createSubLogger('deliver'); @@ -23,8 +24,8 @@ export default async (user: ILocalUser, url: string, object: any) => { // ブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– // TODO: ã„ã¡ã„ã¡ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã®ã¯ã‚³ã‚¹ãƒˆé«˜ãã†ãªã®ã§ã©ã£ã‹ã«ã‚ャッシュã—ã¦ãŠã - const instance = await Instance.findOne({ host: toUnicode(host) }); - if (instance && instance.isBlocked) return; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(toUnicode(host))) return; const data = JSON.stringify(object); @@ -35,6 +36,10 @@ export default async (user: ILocalUser, url: string, object: any) => { const addr = await resolveAddr(hostname); if (!addr) return; + const keypair = await UserKeypairs.findOne({ + userId: user.id + }); + const _ = new Promise((resolve, reject) => { const req = request({ protocol, @@ -62,8 +67,8 @@ export default async (user: ILocalUser, url: string, object: any) => { sign(req, { authorizationHeaderName: 'Signature', - key: user.keypair, - keyId: `${config.url}/users/${user._id}/publickey`, + key: keypair.keyPem, + keyId: `${config.url}/users/${user.id}/publickey`, headers: ['date', 'host', 'digest'] }); diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 05152993e4d4cde034a9cf482543b1d76572f26b..e8d0be638a4fc461c9cc497859d82e60ccd7a763 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -64,7 +64,7 @@ export default class Resolver { json: true }); - if (object === null || ( + if (object == null || ( Array.isArray(object['@context']) ? !object['@context'].includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts index 400293da89f52ad1edf77e1dc7dadb2c4ed0e55d..be846ab2799adb907788794a9030ce765f207959 100644 --- a/src/remote/resolve-user.ts +++ b/src/remote/resolve-user.ts @@ -1,20 +1,21 @@ import { toUnicode, toASCII } from 'punycode'; -import User, { IUser, IRemoteUser } from '../models/user'; import webFinger from './webfinger'; import config from '../config'; import { createPerson, updatePerson } from './activitypub/models/person'; import { URL } from 'url'; import { remoteLogger } from './logger'; import chalk from 'chalk'; +import { User, IRemoteUser } from '../models/entities/user'; +import { Users } from '../models'; const logger = remoteLogger.createSubLogger('resolve-user'); -export default async (username: string, _host: string, option?: any, resync?: boolean): Promise<IUser> => { +export default async (username: string, _host: string, option?: any, resync = false): Promise<User> => { const usernameLower = username.toLowerCase(); if (_host == null) { logger.info(`return local user: ${usernameLower}`); - return await User.findOne({ usernameLower, host: null }); + return await Users.findOne({ usernameLower, host: null }); } const configHostAscii = toASCII(config.host).toLowerCase(); @@ -25,14 +26,14 @@ export default async (username: string, _host: string, option?: any, resync?: bo if (configHost == host) { logger.info(`return local user: ${usernameLower}`); - return await User.findOne({ usernameLower, host: null }); + return await Users.findOne({ usernameLower, host: null }); } - const user = await User.findOne({ usernameLower, host }, option); + const user = await Users.findOne({ usernameLower, host }, option); const acctLower = `${usernameLower}@${hostAscii}`; - if (user === null) { + if (user == null) { const self = await resolveSelf(acctLower); logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); @@ -54,13 +55,11 @@ export default async (username: string, _host: string, option?: any, resync?: bo throw new Error(`Invalied uri`); } - await User.update({ + await Users.update({ usernameLower, host: host }, { - $set: { - uri: self.href - } + uri: self.href }); } else { logger.info(`uri is fine: ${acctLower}`); @@ -69,7 +68,7 @@ export default async (username: string, _host: string, option?: any, resync?: bo await updatePerson(self.href); logger.info(`return resynced remote user: ${acctLower}`); - return await User.findOne({ uri: self.href }); + return await Users.findOne({ uri: self.href }); } logger.info(`return existing remote user: ${acctLower}`); diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index df5f5b141daed3ca51a3e47f677269f9672c9dfd..3b39977d47b63e0521648518929155d1d8e7c1fe 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -1,15 +1,11 @@ -import { ObjectID } from 'mongodb'; import * as Router from 'koa-router'; import * as json from 'koa-json-body'; import * as httpSignature from 'http-signature'; import { renderActivity } from '../remote/activitypub/renderer'; -import Note from '../models/note'; -import User, { isLocalUser, ILocalUser, IUser } from '../models/user'; -import Emoji from '../models/emoji'; import renderNote from '../remote/activitypub/renderer/note'; import renderKey from '../remote/activitypub/renderer/key'; -import renderPerson from '../remote/activitypub/renderer/person'; +import { renderPerson } from '../remote/activitypub/renderer/person'; import renderEmoji from '../remote/activitypub/renderer/emoji'; import Outbox, { packActivity } from './activitypub/outbox'; import Followers from './activitypub/followers'; @@ -18,6 +14,9 @@ import Featured from './activitypub/featured'; import renderQuestion from '../remote/activitypub/renderer/question'; import { inbox as processInbox } from '../queue'; import { isSelfHost } from '../misc/convert-host'; +import { Notes, Users, Emojis, UserKeypairs, Polls } from '../models'; +import { ILocalUser, User } from '../models/entities/user'; +import { In } from 'typeorm'; // Init router const router = new Router(); @@ -64,25 +63,20 @@ router.post('/users/:user/inbox', json(), inbox); router.get('/notes/:note', async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); - if (!ObjectID.isValid(ctx.params.note)) { - ctx.status = 404; - return; - } - - const note = await Note.findOne({ - _id: new ObjectID(ctx.params.note), - visibility: { $in: ['public', 'home'] }, - localOnly: { $ne: true } + const note = await Notes.findOne({ + id: ctx.params.note, + visibility: In(['public', 'home']), + localOnly: false }); - if (note === null) { + if (note == null) { ctx.status = 404; return; } // リモートã ã£ãŸã‚‰ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆ - if (note._user.host != null) { - if (note.uri == null || isSelfHost(note._user.host)) { + if (note.userHost != null) { + if (note.uri == null || isSelfHost(note.userHost)) { ctx.status = 500; return; } @@ -97,19 +91,14 @@ router.get('/notes/:note', async (ctx, next) => { // note activity router.get('/notes/:note/activity', async ctx => { - if (!ObjectID.isValid(ctx.params.note)) { - ctx.status = 404; - return; - } - - const note = await Note.findOne({ - _id: new ObjectID(ctx.params.note), - '_user.host': null, - visibility: { $in: ['public', 'home'] }, - localOnly: { $ne: true } + const note = await Notes.findOne({ + id: ctx.params.note, + userHost: null, + visibility: In(['public', 'home']), + localOnly: false }); - if (note === null) { + if (note == null) { ctx.status = 404; return; } @@ -121,32 +110,23 @@ router.get('/notes/:note/activity', async ctx => { // question router.get('/questions/:question', async (ctx, next) => { - if (!ObjectID.isValid(ctx.params.question)) { - ctx.status = 404; - return; - } - - const poll = await Note.findOne({ - _id: new ObjectID(ctx.params.question), - '_user.host': null, - visibility: { $in: ['public', 'home'] }, - localOnly: { $ne: true }, - poll: { - $exists: true, - $ne: null - }, + const pollNote = await Notes.findOne({ + id: ctx.params.question, + userHost: null, + visibility: In(['public', 'home']), + localOnly: false, + hasPoll: true }); - if (poll === null) { + if (pollNote == null) { ctx.status = 404; return; } - const user = await User.findOne({ - _id: poll.userId - }); + const user = await Users.findOne(pollNote.userId); + const poll = await Polls.findOne({ noteId: pollNote.id }); - ctx.body = renderActivity(await renderQuestion(user as ILocalUser, poll)); + ctx.body = renderActivity(await renderQuestion(user as ILocalUser, pollNote, poll)); setResponseType(ctx); }); @@ -164,25 +144,24 @@ router.get('/users/:user/collections/featured', Featured); // publickey router.get('/users/:user/publickey', async ctx => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } - if (isLocalUser(user)) { - ctx.body = renderActivity(renderKey(user)); + const keypair = await UserKeypairs.findOne({ + userId: user.id + }); + + if (Users.isLocalUser(user)) { + ctx.body = renderActivity(renderKey(user, keypair)); ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); } else { @@ -191,8 +170,8 @@ router.get('/users/:user/publickey', async ctx => { }); // user -async function userInfo(ctx: Router.IRouterContext, user: IUser) { - if (user === null) { +async function userInfo(ctx: Router.IRouterContext, user: User) { + if (user == null) { ctx.status = 404; return; } @@ -205,15 +184,10 @@ async function userInfo(ctx: Router.IRouterContext, user: IUser) { router.get('/users/:user', async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); @@ -223,7 +197,7 @@ router.get('/users/:user', async (ctx, next) => { router.get('/@:user', async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); - const user = await User.findOne({ + const user = await Users.findOne({ usernameLower: ctx.params.user.toLowerCase(), host: null }); @@ -234,12 +208,12 @@ router.get('/@:user', async (ctx, next) => { // emoji router.get('/emojis/:emoji', async ctx => { - const emoji = await Emoji.findOne({ + const emoji = await Emojis.findOne({ host: null, name: ctx.params.emoji }); - if (emoji === null) { + if (emoji == null) { ctx.status = 404; return; } diff --git a/src/server/activitypub/featured.ts b/src/server/activitypub/featured.ts index fc6150902bd818ee63b942947474e0caa7232e90..f43312d79a59229ed7a8b5caf6a2e54bc5128900 100644 --- a/src/server/activitypub/featured.ts +++ b/src/server/activitypub/featured.ts @@ -1,35 +1,28 @@ -import { ObjectID } from 'mongodb'; import * as Router from 'koa-router'; import config from '../../config'; -import User from '../../models/user'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import { setResponseType } from '../activitypub'; -import Note from '../../models/note'; import renderNote from '../../remote/activitypub/renderer/note'; +import { Users, Notes, UserNotePinings } from '../../models'; export default async (ctx: Router.IRouterContext) => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; // Verify user - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } - const pinnedNoteIds = user.pinnedNoteIds || []; + const pinings = await UserNotePinings.find({ userId: user.id }); - const pinnedNotes = await Promise.all(pinnedNoteIds.filter(ObjectID.isValid).map(id => Note.findOne({ _id: id }))); + const pinnedNotes = await Promise.all(pinings.map(pining => Notes.findOne(pining.noteId))); const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note))); diff --git a/src/server/activitypub/followers.ts b/src/server/activitypub/followers.ts index 002576b2e7a22fef068a935f1950c81dbc788553..62c54399edacc197e77e1dd35f8dc10cd6836696 100644 --- a/src/server/activitypub/followers.ts +++ b/src/server/activitypub/followers.ts @@ -1,24 +1,18 @@ -import { ObjectID } from 'mongodb'; import * as Router from 'koa-router'; import config from '../../config'; import $ from 'cafy'; -import ID, { transform } from '../../misc/cafy-id'; -import User from '../../models/user'; -import Following from '../../models/following'; +import { ID } from '../../misc/cafy-id'; import * as url from '../../prelude/url'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; import renderFollowUser from '../../remote/activitypub/renderer/follow-user'; import { setResponseType } from '../activitypub'; +import { Users, Followings } from '../../models'; +import { LessThan } from 'typeorm'; export default async (ctx: Router.IRouterContext) => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; // Get 'cursor' parameter const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor); @@ -34,12 +28,12 @@ export default async (ctx: Router.IRouterContext) => { } // Verify user - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } @@ -49,22 +43,20 @@ export default async (ctx: Router.IRouterContext) => { if (page) { const query = { - followeeId: user._id + followeeId: user.id } as any; // カーソルãŒæŒ‡å®šã•ã‚Œã¦ã„ã‚‹å ´åˆ if (cursor) { - query._id = { - $lt: transform(cursor) - }; + query.id = LessThan(cursor); } // Get followers - const followings = await Following - .find(query, { - limit: limit + 1, - sort: { _id: -1 } - }); + const followings = await Followings.find({ + where: query, + take: limit + 1, + order: { id: -1 } + }); // 「次ã®ãƒšãƒ¼ã‚¸ã€ãŒã‚ã‚‹ã‹ã©ã†ã‹ const inStock = followings.length === limit + 1; @@ -80,7 +72,7 @@ export default async (ctx: Router.IRouterContext) => { null, inStock ? `${partOf}?${url.query({ page: 'true', - cursor: followings[followings.length - 1]._id.toHexString() + cursor: followings[followings.length - 1].id })}` : null ); diff --git a/src/server/activitypub/following.ts b/src/server/activitypub/following.ts index 0d7486f68ad567c395350aa3c0ba7939747ec02d..4894aac1f8c3acb7d5bb2dccbaf224632690e9de 100644 --- a/src/server/activitypub/following.ts +++ b/src/server/activitypub/following.ts @@ -1,24 +1,19 @@ -import { ObjectID } from 'mongodb'; import * as Router from 'koa-router'; import config from '../../config'; import $ from 'cafy'; -import ID, { transform } from '../../misc/cafy-id'; -import User from '../../models/user'; -import Following from '../../models/following'; +import { ID } from '../../misc/cafy-id'; import * as url from '../../prelude/url'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; import renderFollowUser from '../../remote/activitypub/renderer/follow-user'; import { setResponseType } from '../activitypub'; +import { Users, Followings } from '../../models'; +import { LessThan, FindConditions } from 'typeorm'; +import { Following } from '../../models/entities/following'; export default async (ctx: Router.IRouterContext) => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; // Get 'cursor' parameter const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor); @@ -34,12 +29,12 @@ export default async (ctx: Router.IRouterContext) => { } // Verify user - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } @@ -49,22 +44,20 @@ export default async (ctx: Router.IRouterContext) => { if (page) { const query = { - followerId: user._id - } as any; + followerId: user.id + } as FindConditions<Following>; // カーソルãŒæŒ‡å®šã•ã‚Œã¦ã„ã‚‹å ´åˆ if (cursor) { - query._id = { - $lt: transform(cursor) - }; + query.id = LessThan(cursor); } // Get followings - const followings = await Following - .find(query, { - limit: limit + 1, - sort: { _id: -1 } - }); + const followings = await Followings.find({ + where: query, + take: limit + 1, + order: { id: -1 } + }); // 「次ã®ãƒšãƒ¼ã‚¸ã€ãŒã‚ã‚‹ã‹ã©ã†ã‹ const inStock = followings.length === limit + 1; @@ -80,7 +73,7 @@ export default async (ctx: Router.IRouterContext) => { null, inStock ? `${partOf}?${url.query({ page: 'true', - cursor: followings[followings.length - 1]._id.toHexString() + cursor: followings[followings.length - 1].id })}` : null ); diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts index ff8f884b190cee9916b951330fe6c1437c2ce949..377f43c98616e0496575fe2619d74eee88d9ca09 100644 --- a/src/server/activitypub/outbox.ts +++ b/src/server/activitypub/outbox.ts @@ -1,28 +1,23 @@ -import { ObjectID } from 'mongodb'; import * as Router from 'koa-router'; import config from '../../config'; import $ from 'cafy'; -import ID, { transform } from '../../misc/cafy-id'; -import User from '../../models/user'; +import { ID } from '../../misc/cafy-id'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; import { setResponseType } from '../activitypub'; - -import Note, { INote } from '../../models/note'; import renderNote from '../../remote/activitypub/renderer/note'; import renderCreate from '../../remote/activitypub/renderer/create'; import renderAnnounce from '../../remote/activitypub/renderer/announce'; import { countIf } from '../../prelude/array'; import * as url from '../../prelude/url'; +import { Users, Notes } from '../../models'; +import { makePaginationQuery } from '../api/common/make-pagination-query'; +import { Brackets } from 'typeorm'; +import { Note } from '../../models/entities/note'; export default async (ctx: Router.IRouterContext) => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; // Get 'sinceId' parameter const [sinceId, sinceIdErr] = $.optional.type(ID).get(ctx.request.query.since_id); @@ -41,12 +36,12 @@ export default async (ctx: Router.IRouterContext) => { } // Verify user - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } @@ -55,34 +50,15 @@ export default async (ctx: Router.IRouterContext) => { const partOf = `${config.url}/users/${userId}/outbox`; if (page) { - //#region Construct query - const sort = { - _id: -1 - }; - - const query = { - userId: user._id, - visibility: { $in: ['public', 'home'] }, - localOnly: { $ne: true } - } as any; - - if (sinceId) { - sort._id = 1; - query._id = { - $gt: transform(sinceId) - }; - } else if (untilId) { - query._id = { - $lt: transform(untilId) - }; - } - //#endregion + const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId) + .andWhere('note.userId = :userId', { userId: user.id }) + .andWhere(new Brackets(qb => { qb + .where(`note.visibility = 'public'`) + .orWhere(`note.visibility = 'home'`); + })) + .andWhere('note.localOnly = FALSE'); - const notes = await Note - .find(query, { - limit: limit, - sort: sort - }); + const notes = await query.take(limit).getMany(); if (sinceId) notes.reverse(); @@ -96,11 +72,11 @@ export default async (ctx: Router.IRouterContext) => { user.notesCount, activities, partOf, notes.length ? `${partOf}?${url.query({ page: 'true', - since_id: notes[0]._id.toHexString() + since_id: notes[0].id })}` : null, notes.length ? `${partOf}?${url.query({ page: 'true', - until_id: notes[notes.length - 1]._id.toHexString() + until_id: notes[notes.length - 1].id })}` : null ); @@ -123,10 +99,10 @@ export default async (ctx: Router.IRouterContext) => { * Pack Create<Note> or Announce Activity * @param note Note */ -export async function packActivity(note: INote): Promise<object> { - if (note.renoteId && note.text == null && note.poll == null && (note.fileIds == null || note.fileIds.length == 0)) { - const renote = await Note.findOne(note.renoteId); - return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`, note); +export async function packActivity(note: Note): Promise<object> { + if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) { + const renote = await Notes.findOne(note.renoteId); + return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note); } return renderCreate(await renderNote(note, false), note); diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts index 7781b87c88249c9858b77af933894109e92e6ace..e293e3fed06071013d5d081db37414f3e08f2df6 100644 --- a/src/server/api/authenticate.ts +++ b/src/server/api/authenticate.ts @@ -1,37 +1,37 @@ -import App, { IApp } from '../../models/app'; -import { default as User, IUser } from '../../models/user'; -import AccessToken from '../../models/access-token'; import isNativeToken from './common/is-native-token'; +import { User } from '../../models/entities/user'; +import { App } from '../../models/entities/app'; +import { Users, AccessTokens, Apps } from '../../models'; -export default async (token: string): Promise<[IUser, IApp]> => { +export default async (token: string): Promise<[User, App]> => { if (token == null) { return [null, null]; } if (isNativeToken(token)) { // Fetch user - const user: IUser = await User + const user = await Users .findOne({ token }); - if (user === null) { + if (user == null) { throw 'user not found'; } return [user, null]; } else { - const accessToken = await AccessToken.findOne({ + const accessToken = await AccessTokens.findOne({ hash: token.toLowerCase() }); - if (accessToken === null) { + if (accessToken == null) { throw 'invalid signature'; } - const app = await App - .findOne({ _id: accessToken.appId }); + const app = await Apps + .findOne(accessToken.appId); - const user = await User - .findOne({ _id: accessToken.userId }); + const user = await Users + .findOne(accessToken.userId); return [user, app]; } diff --git a/src/server/api/call.ts b/src/server/api/call.ts index 89a44b3c65a0ec7c0df30bbd7df8de0b4fc512ea..885c6226676f5258ae3080f01e47134c7127c534 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -1,10 +1,10 @@ import { performance } from 'perf_hooks'; import limiter from './limiter'; -import { IUser } from '../../models/user'; -import { IApp } from '../../models/app'; +import { User } from '../../models/entities/user'; import endpoints from './endpoints'; import { ApiError } from './error'; import { apiLogger } from './logger'; +import { App } from '../../models/entities/app'; const accessDenied = { message: 'Access denied.', @@ -12,7 +12,7 @@ const accessDenied = { id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e' }; -export default async (endpoint: string, user: IUser, app: IApp, data: any, file?: any) => { +export default async (endpoint: string, user: User, app: App, data: any, file?: any) => { const isSecure = user != null && app == null; const ep = endpoints.find(e => e.name === endpoint); diff --git a/src/server/api/common/generate-mute-query.ts b/src/server/api/common/generate-mute-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..090c14eb833f4d383ffb315a5a6b98bd5cf6fe1a --- /dev/null +++ b/src/server/api/common/generate-mute-query.ts @@ -0,0 +1,36 @@ +import { User } from '../../../models/entities/user'; +import { Mutings } from '../../../models'; +import { SelectQueryBuilder, Brackets } from 'typeorm'; + +export function generateMuteQuery(q: SelectQueryBuilder<any>, me: User) { + const mutingQuery = Mutings.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: me.id }); + + // 投稿ã®ä½œè€…をミュートã—ã¦ã„ãªã„ ã‹ã¤ + // 投稿ã®è¿”ä¿¡å…ˆã®ä½œè€…をミュートã—ã¦ã„ãªã„ ã‹ã¤ + // 投稿ã®å¼•ç”¨å…ƒã®ä½œè€…をミュートã—ã¦ã„ãªã„ + q + .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`) + .andWhere(new Brackets(qb => { qb + .where(`note.replyUserId IS NULL`) + .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); + })) + .andWhere(new Brackets(qb => { qb + .where(`note.renoteUserId IS NULL`) + .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); + })); + + q.setParameters(mutingQuery.getParameters()); +} + +export function generateMuteQueryForUsers(q: SelectQueryBuilder<any>, me: User) { + const mutingQuery = Mutings.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: me.id }); + + q + .andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); + + q.setParameters(mutingQuery.getParameters()); +} diff --git a/src/server/api/common/generate-native-user-token.ts b/src/server/api/common/generate-native-user-token.ts index 2082b89a5a6b1f59806e6cf1acdbcd51fecb55eb..92f8a3a0e8811648fcd0a2414ac174a17141f4b8 100644 --- a/src/server/api/common/generate-native-user-token.ts +++ b/src/server/api/common/generate-native-user-token.ts @@ -1,3 +1,3 @@ import rndstr from 'rndstr'; -export default () => `!${rndstr('a-zA-Z0-9', 32)}`; +export default () => `!${rndstr('a-zA-Z0-9', 31)}`; diff --git a/src/server/api/common/generate-visibility-query.ts b/src/server/api/common/generate-visibility-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..2807dc99dcf01b60af042e39a95430823549a40f --- /dev/null +++ b/src/server/api/common/generate-visibility-query.ts @@ -0,0 +1,40 @@ +import { User } from '../../../models/entities/user'; +import { Followings } from '../../../models'; +import { Brackets, SelectQueryBuilder } from 'typeorm'; + +export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: User) { + if (me == null) { + q.andWhere(new Brackets(qb => { qb + .where(`note.visibility = 'public'`) + .orWhere(`note.visibility = 'home'`); + })); + } else { + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + q.andWhere(new Brackets(qb => { qb + // 公開投稿ã§ã‚ã‚‹ + .where(new Brackets(qb => { qb + .where(`note.visibility = 'public'`) + .orWhere(`note.visibility = 'home'`); + })) + // ã¾ãŸã¯ 自分自身 + .orWhere('note.userId = :userId1', { userId1: me.id }) + // ã¾ãŸã¯ 自分宛㦠+ .orWhere(':userId2 = ANY(note.visibleUserIds)', { userId2: me.id }) + .orWhere(new Brackets(qb => { qb + // ã¾ãŸã¯ フォãƒãƒ¯ãƒ¼å®›ã¦ã®æŠ•ç¨¿ã§ã‚り〠+ .where('note.visibility = \'followers\'') + .andWhere(new Brackets(qb => { qb + // 自分ãŒãƒ•ã‚©ãƒãƒ¯ãƒ¼ã§ã‚ã‚‹ + .where(`note.userId IN (${ followingQuery.getQuery() })`) + // ã¾ãŸã¯ 自分ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライ + .orWhere('note.replyUserId = :userId3', { userId3: me.id }); + })); + })); + })); + + q.setParameters(followingQuery.getParameters()); + } +} diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts deleted file mode 100644 index 876aa399f746bdffd64aa0aa68c1ae1d04fd302d..0000000000000000000000000000000000000000 --- a/src/server/api/common/get-friends.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as mongodb from 'mongodb'; -import Following from '../../../models/following'; - -export const getFriendIds = async (me: mongodb.ObjectID, includeMe = true) => { - // Fetch relation to other users who the I follows - // SELECT followee - const followings = await Following - .find({ - followerId: me - }, { - fields: { - followeeId: true - } - }); - - // ID list of other users who the I follows - const myfollowingIds = followings.map(following => following.followeeId); - - if (includeMe) { - myfollowingIds.push(me); - } - - return myfollowingIds; -}; - -export const getFriends = async (me: mongodb.ObjectID, includeMe = true, remoteOnly = false) => { - const q: any = remoteOnly ? { - followerId: me, - '_followee.host': { $ne: null } - } : { - followerId: me - }; - // Fetch relation to other users who the I follows - const followings = await Following - .find(q); - - // ID list of other users who the I follows - const myfollowings = followings.map(following => ({ - id: following.followeeId - })); - - if (includeMe) { - myfollowings.push({ - id: me - }); - } - - return myfollowings; -}; diff --git a/src/server/api/common/get-hide-users.ts b/src/server/api/common/get-hide-users.ts deleted file mode 100644 index 3cdf80675140be822021f3253b8f50dc5d8c501b..0000000000000000000000000000000000000000 --- a/src/server/api/common/get-hide-users.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as mongo from 'mongodb'; -import Mute from '../../../models/mute'; -import User, { IUser } from '../../../models/user'; -import { unique } from '../../../prelude/array'; - -export async function getHideUserIds(me: IUser) { - return await getHideUserIdsById(me ? me._id : null); -} - -export async function getHideUserIdsById(meId?: mongo.ObjectID) { - const [suspended, muted] = await Promise.all([ - User.find({ - isSuspended: true - }, { - fields: { - _id: true - } - }), - meId ? Mute.find({ - muterId: meId - }) : Promise.resolve([]) - ]); - - return unique(suspended.map(user => user._id).concat(muted.map(mute => mute.muteeId))); -} diff --git a/src/server/api/common/getters.ts b/src/server/api/common/getters.ts index 7a72e6489a42aaf3d1d1c0194cc16525002b8ec2..b720840ebba7fb3c81800f002c9341d40a93f58c 100644 --- a/src/server/api/common/getters.ts +++ b/src/server/api/common/getters.ts @@ -1,18 +1,15 @@ -import * as mongo from 'mongodb'; -import Note from '../../../models/note'; -import User, { isRemoteUser, isLocalUser } from '../../../models/user'; import { IdentifiableError } from '../../../misc/identifiable-error'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { Notes, Users } from '../../../models'; /** * Get note for API processing */ -export async function getNote(noteId: mongo.ObjectID) { - const note = await Note.findOne({ - _id: noteId, - deletedAt: { $exists: false } - }); +export async function getNote(noteId: Note['id']) { + const note = await Notes.findOne(noteId); - if (note === null) { + if (note == null) { throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); } @@ -22,23 +19,10 @@ export async function getNote(noteId: mongo.ObjectID) { /** * Get user for API processing */ -export async function getUser(userId: mongo.ObjectID) { - const user = await User.findOne({ - _id: userId, - $or: [{ - isDeleted: { $exists: false } - }, { - isDeleted: false - }] - }, { - fields: { - data: false, - profile: false, - clientSettings: false - } - }); +export async function getUser(userId: User['id']) { + const user = await Users.findOne(userId); - if (user === null) { + if (user == null) { throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); } @@ -48,10 +32,10 @@ export async function getUser(userId: mongo.ObjectID) { /** * Get remote user for API processing */ -export async function getRemoteUser(userId: mongo.ObjectID) { +export async function getRemoteUser(userId: User['id']) { const user = await getUser(userId); - if (!isRemoteUser(user)) { + if (!Users.isRemoteUser(user)) { throw 'user is not a remote user'; } @@ -61,10 +45,10 @@ export async function getRemoteUser(userId: mongo.ObjectID) { /** * Get local user for API processing */ -export async function getLocalUser(userId: mongo.ObjectID) { +export async function getLocalUser(userId: User['id']) { const user = await getUser(userId); - if (!isLocalUser(user)) { + if (!Users.isLocalUser(user)) { throw 'user is not a local user'; } diff --git a/src/server/api/common/make-pagination-query.ts b/src/server/api/common/make-pagination-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..0c859a4f8db235baf7bf22b51ebec6b36ea267fb --- /dev/null +++ b/src/server/api/common/make-pagination-query.ts @@ -0,0 +1,28 @@ +import { SelectQueryBuilder } from 'typeorm'; + +export function makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId: string, untilId: string, sinceDate?: number, untilDate?: number) { + if (sinceId && untilId) { + q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); + q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); + q.orderBy(`${q.alias}.id`, 'DESC'); + } else if (sinceId) { + q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); + q.orderBy(`${q.alias}.id`, 'ASC'); + } else if (untilId) { + q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); + q.orderBy(`${q.alias}.id`, 'DESC'); + } else if (sinceDate && untilDate) { + q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); + q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); + q.orderBy(`${q.alias}.createdAt`, 'DESC'); + } else if (sinceDate) { + q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); + q.orderBy(`${q.alias}.createdAt`, 'ASC'); + } else if (untilDate) { + q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); + q.orderBy(`${q.alias}.createdAt`, 'DESC'); + } else { + q.orderBy(`${q.alias}.id`, 'DESC'); + } + return q; +} diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts index 9f1e7e6ab4ce810f26a53aa63b7f5ff1cd1f7de8..2cb5a1f87f406845f040ebbb7b6d4d0192ec8f93 100644 --- a/src/server/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -1,77 +1,43 @@ -import * as mongo from 'mongodb'; -import isObjectId from '../../../misc/is-objectid'; -import Message from '../../../models/messaging-message'; -import { IMessagingMessage as IMessage } from '../../../models/messaging-message'; import { publishMainStream } from '../../../services/stream'; import { publishMessagingStream } from '../../../services/stream'; import { publishMessagingIndexStream } from '../../../services/stream'; -import User from '../../../models/user'; +import { User } from '../../../models/entities/user'; +import { MessagingMessage } from '../../../models/entities/messaging-message'; +import { MessagingMessages } from '../../../models'; +import { In } from 'typeorm'; /** * Mark messages as read */ -export default ( - user: string | mongo.ObjectID, - otherparty: string | mongo.ObjectID, - message: string | string[] | IMessage | IMessage[] | mongo.ObjectID | mongo.ObjectID[] -) => new Promise<any>(async (resolve, reject) => { - - const userId = isObjectId(user) - ? user - : new mongo.ObjectID(user); - - const otherpartyId = isObjectId(otherparty) - ? otherparty - : new mongo.ObjectID(otherparty); - - const ids: mongo.ObjectID[] = Array.isArray(message) - ? isObjectId(message[0]) - ? (message as mongo.ObjectID[]) - : typeof message[0] === 'string' - ? (message as string[]).map(m => new mongo.ObjectID(m)) - : (message as IMessage[]).map(m => m._id) - : isObjectId(message) - ? [(message as mongo.ObjectID)] - : typeof message === 'string' - ? [new mongo.ObjectID(message)] - : [(message as IMessage)._id]; +export default async ( + userId: User['id'], + otherpartyId: User['id'], + messageIds: MessagingMessage['id'][] +) => { + if (messageIds.length === 0) return; // Update documents - await Message.update({ - _id: { $in: ids }, + await MessagingMessages.update({ + id: In(messageIds), userId: otherpartyId, recipientId: userId, isRead: false }, { - $set: { - isRead: true - } - }, { - multi: true - }); + isRead: true + }); // Publish event - publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString())); - publishMessagingIndexStream(userId, 'read', ids.map(id => id.toString())); + publishMessagingStream(otherpartyId, userId, 'read', messageIds); + publishMessagingIndexStream(userId, 'read', messageIds); // Calc count of my unread messages - const count = await Message - .count({ - recipientId: userId, - isRead: false - }, { - limit: 1 - }); + const count = await MessagingMessages.count({ + recipientId: userId, + isRead: false + }); if (count == 0) { - // Update flag - User.update({ _id: userId }, { - $set: { - hasUnreadMessagingMessage: false - } - }); - // å…¨ã¦ã®(ã„ã¾ã¾ã§æœªèªã ã£ãŸ)自分宛ã¦ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’(ã“ã‚Œã§)èªã¿ã¾ã—ãŸã‚ˆã¨ã„ã†ã‚¤ãƒ™ãƒ³ãƒˆã‚’発行 publishMainStream(userId, 'readAllMessagingMessages'); } -}); +}; diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts index 436130511983871145acb667397b6b256b865229..c8d43ba286266cfe3ee7e2c1d34a60f7989b89f7 100644 --- a/src/server/api/common/read-notification.ts +++ b/src/server/api/common/read-notification.ts @@ -1,72 +1,38 @@ -import * as mongo from 'mongodb'; -import isObjectId from '../../../misc/is-objectid'; -import { default as Notification, INotification } from '../../../models/notification'; import { publishMainStream } from '../../../services/stream'; -import Mute from '../../../models/mute'; -import User from '../../../models/user'; +import { User } from '../../../models/entities/user'; +import { Notification } from '../../../models/entities/notification'; +import { Mutings, Notifications } from '../../../models'; +import { In, Not } from 'typeorm'; /** * Mark notifications as read */ -export default ( - user: string | mongo.ObjectID, - message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[] -) => new Promise<any>(async (resolve, reject) => { - - const userId = isObjectId(user) - ? user - : new mongo.ObjectID(user); - - const ids: mongo.ObjectID[] = Array.isArray(message) - ? isObjectId(message[0]) - ? (message as mongo.ObjectID[]) - : typeof message[0] === 'string' - ? (message as string[]).map(m => new mongo.ObjectID(m)) - : (message as INotification[]).map(m => m._id) - : isObjectId(message) - ? [(message as mongo.ObjectID)] - : typeof message === 'string' - ? [new mongo.ObjectID(message)] - : [(message as INotification)._id]; - - const mute = await Mute.find({ +export async function readNotification( + userId: User['id'], + notificationIds: Notification['id'][] +) { + const mute = await Mutings.find({ muterId: userId }); const mutedUserIds = mute.map(m => m.muteeId); // Update documents - await Notification.update({ - _id: { $in: ids }, + await Notifications.update({ + id: In(notificationIds), isRead: false }, { - $set: { - isRead: true - } - }, { - multi: true - }); + isRead: true + }); // Calc count of my unread notifications - const count = await Notification - .count({ - notifieeId: userId, - notifierId: { - $nin: mutedUserIds - }, - isRead: false - }, { - limit: 1 - }); - - if (count == 0) { - // Update flag - User.update({ _id: userId }, { - $set: { - hasUnreadNotification: false - } - }); + const count = await Notifications.count({ + notifieeId: userId, + ...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}), + isRead: false + }); + if (count === 0) { // å…¨ã¦ã®(ã„ã¾ã¾ã§æœªèªã ã£ãŸ)通知を(ã“ã‚Œã§)èªã¿ã¾ã—ãŸã‚ˆã¨ã„ã†ã‚¤ãƒ™ãƒ³ãƒˆã‚’発行 publishMainStream(userId, 'readAllNotifications'); } -}); +} diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts index 84cad3a9359bf9670b5401b7af777f1968c5835f..0f4ee4ca1135808246ec36535442e9ef0c893226 100644 --- a/src/server/api/common/signin.ts +++ b/src/server/api/common/signin.ts @@ -1,7 +1,7 @@ import * as Koa from 'koa'; import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default function(ctx: Koa.BaseContext, user: ILocalUser, redirect = false) { if (redirect) { diff --git a/src/server/api/define.ts b/src/server/api/define.ts index f2fababc32b631008136842b04b4356eefa98bab..a18419bcf6585f340e4aedf926b03b3b819b1ebb 100644 --- a/src/server/api/define.ts +++ b/src/server/api/define.ts @@ -1,8 +1,8 @@ import * as fs from 'fs'; -import { ILocalUser } from '../../models/user'; -import { IApp } from '../../models/app'; +import { ILocalUser } from '../../models/entities/user'; import { IEndpointMeta } from './endpoints'; import { ApiError } from './error'; +import { App } from '../../models/entities/app'; type Params<T extends IEndpointMeta> = { [P in keyof T['params']]: T['params'][P]['transform'] extends Function @@ -12,8 +12,8 @@ type Params<T extends IEndpointMeta> = { export type Response = Record<string, any> | void; -export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T>, user: ILocalUser, app: IApp, file?: any, cleanup?: Function) => Promise<Response>): (params: any, user: ILocalUser, app: IApp, file?: any) => Promise<any> { - return (params: any, user: ILocalUser, app: IApp, file?: any) => { +export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T>, user: ILocalUser, app: App, file?: any, cleanup?: Function) => Promise<Response>): (params: any, user: ILocalUser, app: App, file?: any) => Promise<any> { + return (params: any, user: ILocalUser, app: App, file?: any) => { function cleanup() { fs.unlink(file.path, () => {}); } diff --git a/src/server/api/endpoints/admin/abuse-user-reports.ts b/src/server/api/endpoints/admin/abuse-user-reports.ts index d9fe3429cedd0613c6859193205207d1a41cada1..5c5a734c1d587dff149971fb5c6933fa58b23c27 100644 --- a/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Report, { packMany } from '../../../../models/abuse-user-report'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { AbuseUserReports } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { tags: ['admin'], @@ -17,37 +18,18 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, } }; export default define(meta, async (ps) => { - const sort = { - _id: -1 - }; - const query = {} as any; - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } + const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId); - const reports = await Report - .find(query, { - limit: ps.limit, - sort: sort - }); + const reports = await query.take(ps.limit).getMany(); - return await packMany(reports); + return await AbuseUserReports.packMany(reports); }); diff --git a/src/server/api/endpoints/admin/drive/files.ts b/src/server/api/endpoints/admin/drive/files.ts index 8ed417a429c58040ccab2ba99d8266369b541d4a..1ccabc92d91ee1edd5c707858afaabc1aa58a1ca 100644 --- a/src/server/api/endpoints/admin/drive/files.ts +++ b/src/server/api/endpoints/admin/drive/files.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import File, { packMany } from '../../../../../models/drive-file'; import define from '../../../define'; import { fallback } from '../../../../../prelude/symbol'; +import { DriveFiles } from '../../../../../models'; export const meta = { tags: ['admin'], @@ -41,27 +41,25 @@ export const meta = { }; const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - '+createdAt': { uploadDate: -1 }, - '-createdAt': { uploadDate: 1 }, - '+size': { length: -1 }, - '-size': { length: 1 }, - [fallback]: { _id: -1 } + '+createdAt': { createdAt: -1 }, + '-createdAt': { createdAt: 1 }, + '+size': { size: -1 }, + '-size': { size: 1 }, + [fallback]: { id: -1 } }; export default define(meta, async (ps, me) => { - const q = { - 'metadata.deletedAt': { $exists: false }, - } as any; + const q = {} as any; - if (ps.origin == 'local') q['metadata._user.host'] = null; - if (ps.origin == 'remote') q['metadata._user.host'] = { $ne: null }; + if (ps.origin == 'local') q['userHost'] = null; + if (ps.origin == 'remote') q['userHost'] = { $ne: null }; - const files = await File - .find(q, { - limit: ps.limit, - sort: sort[ps.sort] || sort[fallback], - skip: ps.offset - }); + const files = await DriveFiles.find({ + where: q, + take: ps.limit, + order: sort[ps.sort] || sort[fallback], + skip: ps.offset + }); - return await packMany(files, { detail: true, withUser: true, self: true }); + return await DriveFiles.packMany(files, { detail: true, withUser: true, self: true }); }); diff --git a/src/server/api/endpoints/admin/drive/show-file.ts b/src/server/api/endpoints/admin/drive/show-file.ts index 405b6d44ceb18a4b8d8ef8c7b5f313b01bc974e7..a2b6c158f0b3464da83b5e0719da0c9ca9fa2693 100644 --- a/src/server/api/endpoints/admin/drive/show-file.ts +++ b/src/server/api/endpoints/admin/drive/show-file.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import DriveFile from '../../../../../models/drive-file'; import { ApiError } from '../../../error'; +import { DriveFiles } from '../../../../../models'; export const meta = { tags: ['admin'], @@ -13,7 +13,6 @@ export const meta = { params: { fileId: { validator: $.type(ID), - transform: transform, }, }, @@ -27,9 +26,7 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const file = await DriveFile.findOne({ - _id: ps.fileId - }); + const file = await DriveFiles.findOne(ps.fileId); if (file == null) { throw new ApiError(meta.errors.noSuchFile); diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts index c126c8380f116183e6072a7776fe1ccb3a90ba3a..c26e8dd04d7b4d51a89ee1aa7d49a6b005ff1341 100644 --- a/src/server/api/endpoints/admin/emoji/add.ts +++ b/src/server/api/endpoints/admin/emoji/add.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import Emoji from '../../../../../models/emoji'; import define from '../../../define'; import { detectUrlMine } from '../../../../../misc/detect-url-mine'; +import { Emojis } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { desc: { @@ -32,7 +33,8 @@ export const meta = { export default define(meta, async (ps) => { const type = await detectUrlMine(ps.url); - const emoji = await Emoji.insert({ + const emoji = await Emojis.save({ + id: genId(), updatedAt: new Date(), name: ps.name, host: null, @@ -42,6 +44,6 @@ export default define(meta, async (ps) => { }); return { - id: emoji._id + id: emoji.id }; }); diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts index 954f8f96c6098476ff531aa39fb8e819bb14697a..07174723b9a1677665a432424e248fd6e41b8174 100644 --- a/src/server/api/endpoints/admin/emoji/list.ts +++ b/src/server/api/endpoints/admin/emoji/list.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; -import Emoji from '../../../../../models/emoji'; import define from '../../../define'; +import { Emojis } from '../../../../../models'; export const meta = { desc: { @@ -21,12 +21,12 @@ export const meta = { }; export default define(meta, async (ps) => { - const emojis = await Emoji.find({ + const emojis = await Emojis.find({ host: ps.host }); return emojis.map(e => ({ - id: e._id, + id: e.id, name: e.name, aliases: e.aliases, host: e.host, diff --git a/src/server/api/endpoints/admin/emoji/remove.ts b/src/server/api/endpoints/admin/emoji/remove.ts index 4c69dffbaee8e809f3abd0076e6997d960cf4f47..316834b884ae74f7a18b23d7a19a1e706571c041 100644 --- a/src/server/api/endpoints/admin/emoji/remove.ts +++ b/src/server/api/endpoints/admin/emoji/remove.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import Emoji from '../../../../../models/emoji'; import define from '../../../define'; -import ID from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; +import { Emojis } from '../../../../../models'; export const meta = { desc: { @@ -21,13 +21,9 @@ export const meta = { }; export default define(meta, async (ps) => { - const emoji = await Emoji.findOne({ - _id: ps.id - }); + const emoji = await Emojis.findOne(ps.id); if (emoji == null) throw new Error('emoji not found'); - await Emoji.remove({ _id: emoji._id }); - - return; + await Emojis.delete(emoji.id); }); diff --git a/src/server/api/endpoints/admin/emoji/update.ts b/src/server/api/endpoints/admin/emoji/update.ts index 8b1c07be9ea3eb8899c7d82fa92a6e7ee0a5349c..48b4a4ee23cf7f0dc5285b307cbf1f59f36ed1c5 100644 --- a/src/server/api/endpoints/admin/emoji/update.ts +++ b/src/server/api/endpoints/admin/emoji/update.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import Emoji from '../../../../../models/emoji'; import define from '../../../define'; -import ID from '../../../../../misc/cafy-id'; import { detectUrlMine } from '../../../../../misc/detect-url-mine'; +import { ID } from '../../../../../misc/cafy-id'; +import { Emojis } from '../../../../../models'; export const meta = { desc: { @@ -34,23 +34,17 @@ export const meta = { }; export default define(meta, async (ps) => { - const emoji = await Emoji.findOne({ - _id: ps.id - }); + const emoji = await Emojis.findOne(ps.id); if (emoji == null) throw new Error('emoji not found'); const type = await detectUrlMine(ps.url); - await Emoji.update({ _id: emoji._id }, { - $set: { - updatedAt: new Date(), - name: ps.name, - aliases: ps.aliases, - url: ps.url, - type, - } + await Emojis.update(emoji.id, { + updatedAt: new Date(), + name: ps.name, + aliases: ps.aliases, + url: ps.url, + type, }); - - return; }); diff --git a/src/server/api/endpoints/admin/federation/remove-all-following.ts b/src/server/api/endpoints/admin/federation/remove-all-following.ts index 98afdfc2a5d806bc75e7d8fd20ff2eb3f0f6c717..fca76e70864bc66aa6f7850d9a5dca6720fc7256 100644 --- a/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -1,8 +1,7 @@ import $ from 'cafy'; import define from '../../../define'; -import Following from '../../../../../models/following'; -import User from '../../../../../models/user'; import deleteFollowing from '../../../../../services/following/delete'; +import { Followings, Users } from '../../../../../models'; export const meta = { tags: ['admin'], @@ -18,13 +17,13 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const followings = await Following.find({ - '_follower.host': ps.host + const followings = await Followings.find({ + followerHost: ps.host }); const pairs = await Promise.all(followings.map(f => Promise.all([ - User.findOne({ _id: f.followerId }), - User.findOne({ _id: f.followeeId }) + Users.findOne(f.followerId), + Users.findOne(f.followeeId) ]))); for (const pair of pairs) { diff --git a/src/server/api/endpoints/admin/federation/update-instance.ts b/src/server/api/endpoints/admin/federation/update-instance.ts index 0d127b53b30632d9623845941a470aac94c0277d..d1abe95a5b2b26362d0bd7f93945ee025a2d9d0f 100644 --- a/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/src/server/api/endpoints/admin/federation/update-instance.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import define from '../../../define'; -import Instance from '../../../../../models/instance'; +import { Instances } from '../../../../../models'; export const meta = { tags: ['admin'], @@ -13,10 +13,6 @@ export const meta = { validator: $.str }, - isBlocked: { - validator: $.bool - }, - isClosed: { validator: $.bool }, @@ -24,18 +20,13 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const instance = await Instance.findOne({ host: ps.host }); + const instance = await Instances.findOne({ host: ps.host }); if (instance == null) { throw new Error('instance not found'); } - Instance.update({ host: ps.host }, { - $set: { - isBlocked: ps.isBlocked, - isMarkedAsClosed: ps.isClosed - } + Instances.update({ host: ps.host }, { + isMarkedAsClosed: ps.isClosed }); - - return; }); diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts index 28aa30195747b1d42ff891570551c7e717b9f406..4e264feef677e3024ef4c076097c99531e3312ed 100644 --- a/src/server/api/endpoints/admin/invite.ts +++ b/src/server/api/endpoints/admin/invite.ts @@ -1,6 +1,7 @@ import rndstr from 'rndstr'; -import RegistrationTicket from '../../../../models/registration-tickets'; import define from '../../define'; +import { RegistrationTickets } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; export const meta = { desc: { @@ -18,7 +19,8 @@ export const meta = { export default define(meta, async (ps) => { const code = rndstr({ length: 5, chars: '0-9' }); - await RegistrationTicket.insert({ + await RegistrationTickets.save({ + id: genId(), createdAt: new Date(), code: code }); diff --git a/src/server/api/endpoints/admin/logs.ts b/src/server/api/endpoints/admin/logs.ts index 805a42b9e0a3febed9e5385b90b6fd2d4dce1a3d..eee56a39395a9f546d4f39858362a82459c0a192 100644 --- a/src/server/api/endpoints/admin/logs.ts +++ b/src/server/api/endpoints/admin/logs.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import Log from '../../../../models/log'; +import { Logs } from '../../../../models'; +import { Brackets } from 'typeorm'; export const meta = { tags: ['admin'], @@ -27,41 +28,44 @@ export const meta = { }; export default define(meta, async (ps) => { - const sort = { - _id: -1 - }; - const query = {} as any; + const query = Logs.createQueryBuilder('log'); + + if (ps.level) query.andWhere('log.level = :level', { level: ps.level }); - if (ps.level) query.level = ps.level; if (ps.domain) { - for (const d of ps.domain.split(' ')) { - const qs: any[] = []; - let i = 0; - for (const sd of (d.startsWith('-') ? d.substr(1) : d).split('.')) { - qs.push({ - [`domain.${i}`]: d.startsWith('-') ? { $ne: sd } : sd - }); - i++; - } - if (d.startsWith('-')) { - if (query['$and'] == null) query['$and'] = []; - query['$and'].push({ - $and: qs - }); - } else { - if (query['$or'] == null) query['$or'] = []; - query['$or'].push({ - $and: qs - }); - } + const whiteDomains = ps.domain.split(' ').filter(x => !x.startsWith('-')); + const blackDomains = ps.domain.split(' ').filter(x => x.startsWith('-')); + + if (whiteDomains.length > 0) { + query.andWhere(new Brackets(qb => { + for (const whiteDomain of whiteDomains) { + let i = 0; + for (const subDomain of whiteDomain.split('.')) { + const p = `whiteSubDomain_${subDomain}_${i}`; + // SQL is 1 based, so we need '+ 1' + qb.orWhere(`log.domain[${i + 1}] = :${p}`, { [p]: subDomain }); + i++; + } + } + })); + } + + if (blackDomains.length > 0) { + query.andWhere(new Brackets(qb => { + for (const blackDomain of blackDomains) { + let i = 0; + for (const subDomain of blackDomain.split('.')) { + const p = `blackSubDomain_${subDomain}_${i}`; + // SQL is 1 based, so we need '+ 1' + qb.andWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain }); + i++; + } + } + })); } } - const logs = await Log - .find(query, { - limit: ps.limit, - sort: sort - }); + const logs = await query.take(ps.limit).getMany(); return logs; }); diff --git a/src/server/api/endpoints/admin/moderators/add.ts b/src/server/api/endpoints/admin/moderators/add.ts index 2271bcd1a919b40dc27b7a81254ec5700f7586c9..a15f0a17a2ea9bcfa3ed3669238a2f5dbcd4c01d 100644 --- a/src/server/api/endpoints/admin/moderators/add.ts +++ b/src/server/api/endpoints/admin/moderators/add.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import User from '../../../../../models/user'; +import { Users } from '../../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.update({ - _id: user._id - }, { - $set: { - isModerator: true - } + await Users.update(user.id, { + isModerator: true }); - - return; }); diff --git a/src/server/api/endpoints/admin/moderators/remove.ts b/src/server/api/endpoints/admin/moderators/remove.ts index 84143d3e359fdce0ff8447a7d7a05618fc583c4c..209cf0814f2010804c41e4ebf74c750a8d1a7131 100644 --- a/src/server/api/endpoints/admin/moderators/remove.ts +++ b/src/server/api/endpoints/admin/moderators/remove.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import User from '../../../../../models/user'; +import { Users } from '../../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.update({ - _id: user._id - }, { - $set: { - isModerator: false - } + await Users.update(user.id, { + isModerator: false }); - - return; }); diff --git a/src/server/api/endpoints/admin/remove-abuse-user-report.ts b/src/server/api/endpoints/admin/remove-abuse-user-report.ts index fa17e2c9379e8ae8c8ab46136a5ab146e5507485..f293c007183153cbb198bfebcb077880a755ec37 100644 --- a/src/server/api/endpoints/admin/remove-abuse-user-report.ts +++ b/src/server/api/endpoints/admin/remove-abuse-user-report.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import AbuseUserReport from '../../../../models/abuse-user-report'; +import { AbuseUserReports } from '../../../../models'; export const meta = { tags: ['admin'], @@ -12,23 +12,16 @@ export const meta = { params: { reportId: { validator: $.type(ID), - transform: transform }, } }; export default define(meta, async (ps) => { - const report = await AbuseUserReport.findOne({ - _id: ps.reportId - }); + const report = await AbuseUserReports.findOne(ps.reportId); if (report == null) { throw new Error('report not found'); } - await AbuseUserReport.remove({ - _id: report._id - }); - - return; + await AbuseUserReports.delete(report.id); }); diff --git a/src/server/api/endpoints/admin/reset-password.ts b/src/server/api/endpoints/admin/reset-password.ts index 73901d83584e75623bdfa625691147e3f008bc62..07b8b6d93846088ee1c748b8f6484c790c07d630 100644 --- a/src/server/api/endpoints/admin/reset-password.ts +++ b/src/server/api/endpoints/admin/reset-password.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; import * as bcrypt from 'bcryptjs'; import rndstr from 'rndstr'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -18,7 +18,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID which you want to suspend' @@ -28,9 +27,7 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); @@ -45,12 +42,8 @@ export default define(meta, async (ps) => { // Generate hash of password const hash = bcrypt.hashSync(passwd); - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - password: hash - } + await Users.update(user.id, { + password: hash }); return { diff --git a/src/server/api/endpoints/admin/show-user.ts b/src/server/api/endpoints/admin/show-user.ts index 985f71a8739b264dc540e2d9f866f4e3aa398ba0..452125dea0668e5fa5e36372b041badab671b6f9 100644 --- a/src/server/api/endpoints/admin/show-user.ts +++ b/src/server/api/endpoints/admin/show-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -16,7 +16,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID which you want to suspend' @@ -26,9 +25,7 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); diff --git a/src/server/api/endpoints/admin/show-users.ts b/src/server/api/endpoints/admin/show-users.ts index 5feb1b4fd85a10704ed25832e7536c4fb277a4e3..73976b98725fbd278cff0cb0012959f85e413b2c 100644 --- a/src/server/api/endpoints/admin/show-users.ts +++ b/src/server/api/endpoints/admin/show-users.ts @@ -1,7 +1,6 @@ import $ from 'cafy'; -import User, { pack } from '../../../../models/user'; import define from '../../define'; -import { fallback } from '../../../../prelude/symbol'; +import { Users } from '../../../../models'; export const meta = { tags: ['admin'], @@ -55,51 +54,38 @@ export const meta = { } }; -const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - '+follower': { followersCount: -1 }, - '-follower': { followersCount: 1 }, - '+createdAt': { createdAt: -1 }, - '-createdAt': { createdAt: 1 }, - '+updatedAt': { updatedAt: -1 }, - '-updatedAt': { updatedAt: 1 }, - [fallback]: { _id: -1 } -}; - export default define(meta, async (ps, me) => { - const q = { - $and: [] - } as any; + const query = Users.createQueryBuilder('user'); + + switch (ps.state) { + case 'admin': query.where('user.isAdmin = TRUE'); break; + case 'moderator': query.where('user.isModerator = TRUE'); break; + case 'adminOrModerator': query.where('user.isAdmin = TRUE OR isModerator = TRUE'); break; + case 'verified': query.where('user.isVerified = TRUE'); break; + case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; + case 'silenced': query.where('user.isSilenced = TRUE'); break; + case 'suspended': query.where('user.isSuspended = TRUE'); break; + } - // state - q.$and.push( - ps.state == 'admin' ? { isAdmin: true } : - ps.state == 'moderator' ? { isModerator: true } : - ps.state == 'adminOrModerator' ? { - $or: [{ - isAdmin: true - }, { - isModerator: true - }] - } : - ps.state == 'verified' ? { isVerified: true } : - ps.state == 'silenced' ? { isSilenced: true } : - ps.state == 'suspended' ? { isSuspended: true } : - {} - ); + switch (ps.origin) { + case 'local': query.andWhere('user.host IS NULL'); break; + case 'remote': query.andWhere('user.host IS NOT NULL'); break; + } + + switch (ps.sort) { + case '+follower': query.orderBy('user.followersCount', 'DESC'); break; + case '-follower': query.orderBy('user.followersCount', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; + case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break; + case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; + default: query.orderBy('user.id', 'ASC'); break; + } - // origin - q.$and.push( - ps.origin == 'local' ? { host: null } : - ps.origin == 'remote' ? { host: { $ne: null } } : - {} - ); + query.take(ps.limit); + query.skip(ps.offset); - const users = await User - .find(q, { - limit: ps.limit, - sort: sort[ps.sort] || sort[fallback], - skip: ps.offset - }); + const users = await query.getMany(); - return await Promise.all(users.map(user => pack(user, me, { detail: true }))); + return await Users.packMany(users, me, { detail: true }); }); diff --git a/src/server/api/endpoints/admin/silence-user.ts b/src/server/api/endpoints/admin/silence-user.ts index 2557d8de6a7edbc47b6b5fd7f51f279ae759e623..83aa88012a568eb8984bba177f0e7415a2156e6b 100644 --- a/src/server/api/endpoints/admin/silence-user.ts +++ b/src/server/api/endpoints/admin/silence-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID which you want to make silence' @@ -27,9 +26,7 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); @@ -39,13 +36,7 @@ export default define(meta, async (ps) => { throw new Error('cannot silence admin'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isSilenced: true - } + await Users.update(user.id, { + isSilenced: true }); - - return; }); diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts index 0a2d30953078dc4a1cd4daa844b3b0b7426bd1cf..fa4d378708a07ce7b2576ab3ac549ad2e10f69b4 100644 --- a/src/server/api/endpoints/admin/suspend-user.ts +++ b/src/server/api/endpoints/admin/suspend-user.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User, { IUser } from '../../../../models/user'; -import Following from '../../../../models/following'; import deleteFollowing from '../../../../services/following/delete'; +import { Users, Followings } from '../../../../models'; +import { User } from '../../../../models/entities/user'; export const meta = { desc: { @@ -19,7 +19,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID which you want to suspend' @@ -29,9 +28,7 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); @@ -45,27 +42,21 @@ export default define(meta, async (ps) => { throw new Error('cannot suspend moderator'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isSuspended: true - } + await Users.update(user.id, { + isSuspended: true }); unFollowAll(user); - - return; }); -async function unFollowAll(follower: IUser) { - const followings = await Following.find({ - followerId: follower._id +async function unFollowAll(follower: User) { + const followings = await Followings.find({ + followerId: follower.id }); for (const following of followings) { - const followee = await User.findOne({ - _id: following.followeeId + const followee = await Users.findOne({ + id: following.followeeId }); if (followee == null) { diff --git a/src/server/api/endpoints/admin/unsilence-user.ts b/src/server/api/endpoints/admin/unsilence-user.ts index 01bf41aaefd2b2308281ddd421bc6a79b80f43d0..f9b173366b52bd3578f54512b2dd3797d2a65f00 100644 --- a/src/server/api/endpoints/admin/unsilence-user.ts +++ b/src/server/api/endpoints/admin/unsilence-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID which you want to unsilence' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isSilenced: false - } + await Users.update(user.id, { + isSilenced: false }); - - return; }); diff --git a/src/server/api/endpoints/admin/unsuspend-user.ts b/src/server/api/endpoints/admin/unsuspend-user.ts index 5da35f28e644118fe54ea93f3b08e0ddfb147c2c..08dae034d3ab41723807e7079a211527d883317b 100644 --- a/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/src/server/api/endpoints/admin/unsuspend-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID which you want to unsuspend' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isSuspended: false - } + await Users.update(user.id, { + isSuspended: false }); - - return; }); diff --git a/src/server/api/endpoints/admin/unverify-user.ts b/src/server/api/endpoints/admin/unverify-user.ts index d3ca05cb39e8948a258c9365a85071ace78ac21d..b215dbf10d912cf71d39f7193ac1de55f1abd4d2 100644 --- a/src/server/api/endpoints/admin/unverify-user.ts +++ b/src/server/api/endpoints/admin/unverify-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID which you want to unverify' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isVerified: false - } + await Users.update(user.id, { + isVerified: false }); - - return; }); diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index f8f7cb5d9aeab10f2b9f01213952b9e6c342f84b..e242ac71a1c1cd3659e1f95ccffcf1acd3a1c2fa 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; -import Meta from '../../../../models/meta'; import define from '../../define'; +import { Metas } from '../../../../models'; +import { Meta } from '../../../../models/entities/meta'; export const meta = { desc: { @@ -55,7 +56,7 @@ export const meta = { } }, - hidedTags: { + hiddenTags: { validator: $.optional.nullable.arr($.str), desc: { 'ja-JP': '統計ãªã©ã§ç„¡è¦–ã™ã‚‹ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°' @@ -253,27 +254,6 @@ export const meta = { } }, - enableExternalUserRecommendation: { - validator: $.optional.bool, - desc: { - 'ja-JP': '外部ユーザーレコメンデーションを有効ã«ã™ã‚‹' - } - }, - - externalUserRecommendationEngine: { - validator: $.optional.nullable.str, - desc: { - 'ja-JP': '外部ユーザーレコメンデーションã®ã‚µãƒ¼ãƒ‰ãƒ‘ーティエンジン' - } - }, - - externalUserRecommendationTimeout: { - validator: $.optional.nullable.num.min(0), - desc: { - 'ja-JP': '外部ユーザーレコメンデーションã®ã‚¿ã‚¤ãƒ アウト (ミリ秒)' - } - }, - enableEmail: { validator: $.optional.bool, desc: { @@ -347,7 +327,7 @@ export const meta = { }; export default define(meta, async (ps) => { - const set = {} as any; + const set = {} as Partial<Meta>; if (ps.announcements) { set.announcements = ps.announcements; @@ -373,8 +353,8 @@ export default define(meta, async (ps) => { set.useStarForReactionFallback = ps.useStarForReactionFallback; } - if (Array.isArray(ps.hidedTags)) { - set.hidedTags = ps.hidedTags; + if (Array.isArray(ps.hiddenTags)) { + set.hiddenTags = ps.hiddenTags; } if (ps.mascotImageUrl !== undefined) { @@ -430,11 +410,11 @@ export default define(meta, async (ps) => { } if (ps.maintainerName !== undefined) { - set['maintainer.name'] = ps.maintainerName; + set.maintainerName = ps.maintainerName; } if (ps.maintainerEmail !== undefined) { - set['maintainer.email'] = ps.maintainerEmail; + set.maintainerEmail = ps.maintainerEmail; } if (ps.langs !== undefined) { @@ -481,18 +461,6 @@ export default define(meta, async (ps) => { set.discordClientSecret = ps.discordClientSecret; } - if (ps.enableExternalUserRecommendation !== undefined) { - set.enableExternalUserRecommendation = ps.enableExternalUserRecommendation; - } - - if (ps.externalUserRecommendationEngine !== undefined) { - set.externalUserRecommendationEngine = ps.externalUserRecommendationEngine; - } - - if (ps.externalUserRecommendationTimeout !== undefined) { - set.externalUserRecommendationTimeout = ps.externalUserRecommendationTimeout; - } - if (ps.enableEmail !== undefined) { set.enableEmail = ps.enableEmail; } @@ -537,9 +505,11 @@ export default define(meta, async (ps) => { set.swPrivateKey = ps.swPrivateKey; } - await Meta.update({}, { - $set: set - }, { upsert: true }); + const meta = await Metas.findOne(); - return; + if (meta) { + await Metas.update(meta.id, set); + } else { + await Metas.save(set); + } }); diff --git a/src/server/api/endpoints/admin/update-remote-user.ts b/src/server/api/endpoints/admin/update-remote-user.ts index a74685912c771b0286b4dbfc6a708fa8b4e1c7b8..0be9047d5a77e2b921b7cbc7e0871b180740ad0e 100644 --- a/src/server/api/endpoints/admin/update-remote-user.ts +++ b/src/server/api/endpoints/admin/update-remote-user.ts @@ -1,6 +1,5 @@ -import * as mongo from 'mongodb'; import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { getRemoteUser } from '../../common/getters'; import { updatePerson } from '../../../../remote/activitypub/models/person'; @@ -19,7 +18,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID which you want to update' @@ -29,11 +27,6 @@ export const meta = { }; export default define(meta, async (ps) => { - await updatePersonById(ps.userId); - return; -}); - -async function updatePersonById(userId: mongo.ObjectID) { - const user = await getRemoteUser(userId); + const user = await getRemoteUser(ps.userId); await updatePerson(user.uri); -} +}); diff --git a/src/server/api/endpoints/admin/verify-user.ts b/src/server/api/endpoints/admin/verify-user.ts index f67b6c3bf0fa8a56bc6926a2903ab124d2ff35a9..c1b447a92b500f222e6ab72ae6815f5b4929eca1 100644 --- a/src/server/api/endpoints/admin/verify-user.ts +++ b/src/server/api/endpoints/admin/verify-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ID', 'en-US': 'The user ID which you want to verify' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isVerified: true - } + await Users.update(user.id, { + isVerified: true }); - - return; }); diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts deleted file mode 100644 index 978e9f64b7df29b0fedd08bc1526904c29d94c0d..0000000000000000000000000000000000000000 --- a/src/server/api/endpoints/aggregation/hashtags.ts +++ /dev/null @@ -1,72 +0,0 @@ -import Note from '../../../../models/note'; -import define from '../../define'; -import fetchMeta from '../../../../misc/fetch-meta'; - -export const meta = { - tags: ['hashtags'], - - requireCredential: false, -}; - -export default define(meta, async (ps) => { - const instance = await fetchMeta(); - const hidedTags = instance.hidedTags.map(t => t.toLowerCase()); - - // é‡ã„ - //const span = 1000 * 60 * 60 * 24 * 7; // 1週間 - const span = 1000 * 60 * 60 * 24; // 1æ—¥ - - //#region 1. 指定期間ã®å†…ã«æŠ•ç¨¿ã•ã‚ŒãŸãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°(ã¨ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒšã‚¢)を集計 - const data = await Note.aggregate([{ - $match: { - createdAt: { - $gt: new Date(Date.now() - span) - }, - tagsLower: { - $exists: true, - $ne: [] - } - } - }, { - $unwind: '$tagsLower' - }, { - $group: { - _id: { tag: '$tagsLower', userId: '$userId' } - } - }]) as { - _id: { - tag: string; - userId: any; - } - }[]; - //#endregion - - if (data.length == 0) { - return []; - } - - let tags: { - name: string; - count: number; - }[] = []; - - // カウント - for (const x of data.map(x => x._id).filter(x => !hidedTags.includes(x.tag))) { - const i = tags.findIndex(tag => tag.name == x.tag); - if (i != -1) { - tags[i].count++; - } else { - tags.push({ - name: x.tag, - count: 1 - }); - } - } - - // ã‚¿ã‚°ã‚’äººæ°—é †ã«ä¸¦ã¹æ›¿ãˆ - tags.sort((a, b) => b.count - a.count); - - tags = tags.slice(0, 30); - - return tags; -}); diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts index 7f4afa1f6e4f00c5f2b76e78a04741cfd69ab2da..5b2aaeadbb09dea25039cff70839cb4e49a64be2 100644 --- a/src/server/api/endpoints/ap/show.ts +++ b/src/server/api/endpoints/ap/show.ts @@ -1,15 +1,15 @@ import $ from 'cafy'; import define from '../../define'; import config from '../../../../config'; -import * as mongo from 'mongodb'; -import User, { pack as packUser, IUser } from '../../../../models/user'; import { createPerson } from '../../../../remote/activitypub/models/person'; -import Note, { pack as packNote, INote } from '../../../../models/note'; import { createNote } from '../../../../remote/activitypub/models/note'; import Resolver from '../../../../remote/activitypub/resolver'; import { ApiError } from '../../error'; -import Instance from '../../../../models/instance'; import { extractDbHost } from '../../../../misc/convert-host'; +import { Users, Notes } from '../../../../models'; +import { Note } from '../../../../models/entities/note'; +import { User } from '../../../../models/entities/user'; +import fetchMeta from '../../../../misc/fetch-meta'; export const meta = { tags: ['federation'], @@ -53,25 +53,40 @@ export default define(meta, async (ps) => { async function fetchAny(uri: string) { // URIãŒã“ã®ã‚µãƒ¼ãƒãƒ¼ã‚’指ã—ã¦ã„ã‚‹ãªã‚‰ã€ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼IDã¨ã—ã¦DBã‹ã‚‰ãƒ•ã‚§ãƒƒãƒ if (uri.startsWith(config.url + '/')) { - const id = new mongo.ObjectID(uri.split('/').pop()); - const [user, note] = await Promise.all([ - User.findOne({ _id: id }), - Note.findOne({ _id: id }) - ]); - - const packed = await mergePack(user, note); - if (packed !== null) return packed; + const parts = uri.split('/'); + const id = parts.pop(); + const type = parts.pop(); + + if (type === 'notes') { + const note = await Notes.findOne(id); + + if (note) { + return { + type: 'Note', + object: await Notes.pack(note, null, { detail: true }) + }; + } + } else if (type === 'users') { + const user = await Users.findOne(id); + + if (user) { + return { + type: 'User', + object: await Users.pack(user, null, { detail: true }) + }; + } + } } // ブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– - const instance = await Instance.findOne({ host: extractDbHost(uri) }); - if (instance && instance.isBlocked) return null; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(extractDbHost(uri))) return null; // URI(AP Object id)ã¨ã—ã¦DB検索 { const [user, note] = await Promise.all([ - User.findOne({ uri: uri }), - Note.findOne({ uri: uri }) + Users.findOne({ uri: uri }), + Notes.findOne({ uri: uri }) ]); const packed = await mergePack(user, note); @@ -86,8 +101,8 @@ async function fetchAny(uri: string) { // ã“ã‚Œã¯DBã«å˜åœ¨ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ãŸã‚å†åº¦DB検索 if (uri !== object.id) { const [user, note] = await Promise.all([ - User.findOne({ uri: object.id }), - Note.findOne({ uri: object.id }) + Users.findOne({ uri: object.id }), + Notes.findOne({ uri: object.id }) ]); const packed = await mergePack(user, note); @@ -99,7 +114,7 @@ async function fetchAny(uri: string) { const user = await createPerson(object.id); return { type: 'User', - object: await packUser(user, null, { detail: true }) + object: await Users.pack(user, null, { detail: true }) }; } @@ -107,25 +122,25 @@ async function fetchAny(uri: string) { const note = await createNote(object.id); return { type: 'Note', - object: await packNote(note, null, { detail: true }) + object: await Notes.pack(note, null, { detail: true }) }; } return null; } -async function mergePack(user: IUser, note: INote) { +async function mergePack(user: User, note: Note) { if (user !== null) { return { type: 'User', - object: await packUser(user, null, { detail: true }) + object: await Users.pack(user, null, { detail: true }) }; } if (note !== null) { return { type: 'Note', - object: await packNote(note, null, { detail: true }) + object: await Notes.pack(note, null, { detail: true }) }; } diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts index 67b1b8150a0865ce94f5caad030dac26554d99d2..c7e7e516ad5e2d07758989e1ea0f9d188a8d7773 100644 --- a/src/server/api/endpoints/app/create.ts +++ b/src/server/api/endpoints/app/create.ts @@ -1,7 +1,8 @@ import rndstr from 'rndstr'; import $ from 'cafy'; -import App, { pack } from '../../../../models/app'; import define from '../../define'; +import { Apps } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; export const meta = { tags: ['app'], @@ -34,9 +35,10 @@ export default define(meta, async (ps, user) => { const secret = rndstr('a-zA-Z0-9', 32); // Create account - const app = await App.insert({ + const app = await Apps.save({ + id: genId(), createdAt: new Date(), - userId: user && user._id, + userId: user && user.id, name: ps.name, description: ps.description, permission: ps.permission, @@ -44,7 +46,7 @@ export default define(meta, async (ps, user) => { secret: secret }); - return await pack(app, null, { + return await Apps.pack(app, null, { detail: true, includeSecret: true }); diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts index f3f5b843b370227a4ced39c44c7811a677082cdf..ce9baed2aebc33a5dda8cc5ab0b5a9fcbb0037fe 100644 --- a/src/server/api/endpoints/app/show.ts +++ b/src/server/api/endpoints/app/show.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import App, { pack } from '../../../../models/app'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; +import { Apps } from '../../../../models'; export const meta = { tags: ['app'], @@ -10,7 +10,6 @@ export const meta = { params: { appId: { validator: $.type(ID), - transform: transform }, }, @@ -27,14 +26,14 @@ export default define(meta, async (ps, user, app) => { const isSecure = user != null && app == null; // Lookup app - const ap = await App.findOne({ _id: ps.appId }); + const ap = await Apps.findOne(ps.appId); - if (ap === null) { + if (ap == null) { throw new ApiError(meta.errors.noSuchApp); } - return await pack(ap, user, { + return await Apps.pack(ap, user, { detail: true, - includeSecret: isSecure && ap.userId.equals(user._id) + includeSecret: isSecure && (ap.userId === user.id) }); }); diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts index cedf7821fe069c999e11ff3ff6a8d2e5634b3dbd..21a78011dc0b2b9928782ab4126b44d21f267087 100644 --- a/src/server/api/endpoints/auth/accept.ts +++ b/src/server/api/endpoints/auth/accept.ts @@ -1,11 +1,10 @@ import rndstr from 'rndstr'; import * as crypto from 'crypto'; import $ from 'cafy'; -import App from '../../../../models/app'; -import AuthSess from '../../../../models/auth-session'; -import AccessToken from '../../../../models/access-token'; import define from '../../define'; import { ApiError } from '../../error'; +import { AuthSessions, AccessTokens, Apps } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; export const meta = { tags: ['auth'], @@ -31,10 +30,10 @@ export const meta = { export default define(meta, async (ps, user) => { // Fetch token - const session = await AuthSess + const session = await AuthSessions .findOne({ token: ps.token }); - if (session === null) { + if (session == null) { throw new ApiError(meta.errors.noSuchSession); } @@ -42,16 +41,14 @@ export default define(meta, async (ps, user) => { const accessToken = rndstr('a-zA-Z0-9', 32); // Fetch exist access token - const exist = await AccessToken.findOne({ + const exist = await AccessTokens.findOne({ appId: session.appId, - userId: user._id, + userId: user.id, }); - if (exist === null) { + if (exist == null) { // Lookup app - const app = await App.findOne({ - _id: session.appId - }); + const app = await Apps.findOne(session.appId); // Generate Hash const sha256 = crypto.createHash('sha256'); @@ -59,20 +56,19 @@ export default define(meta, async (ps, user) => { const hash = sha256.digest('hex'); // Insert access token doc - await AccessToken.insert({ + await AccessTokens.save({ + id: genId(), createdAt: new Date(), appId: session.appId, - userId: user._id, + userId: user.id, token: accessToken, hash: hash }); } // Update session - await AuthSess.update(session._id, { - $set: { - userId: user._id - } + await AuthSessions.update(session.id, { + userId: user.id }); return; diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts index e12bea768114a8826933de3c65e288dc3a7fd8b9..5a9bfe6451cdfe278f9a754daa4b278a087bc77f 100644 --- a/src/server/api/endpoints/auth/session/generate.ts +++ b/src/server/api/endpoints/auth/session/generate.ts @@ -1,10 +1,10 @@ import * as uuid from 'uuid'; import $ from 'cafy'; -import App from '../../../../../models/app'; -import AuthSess from '../../../../../models/auth-session'; import config from '../../../../../config'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { Apps, AuthSessions } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { tags: ['auth'], @@ -46,7 +46,7 @@ export const meta = { export default define(meta, async (ps) => { // Lookup app - const app = await App.findOne({ + const app = await Apps.findOne({ secret: ps.appSecret }); @@ -58,9 +58,10 @@ export default define(meta, async (ps) => { const token = uuid.v4(); // Create session token document - const doc = await AuthSess.insert({ + const doc = await AuthSessions.save({ + id: genId(), createdAt: new Date(), - appId: app._id, + appId: app.id, token: token }); diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts index 992e0a499e1078cf140674e787ee5ef63721d886..e6ecd8b8390d43d62a717858cd988949a83846bf 100644 --- a/src/server/api/endpoints/auth/session/show.ts +++ b/src/server/api/endpoints/auth/session/show.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import AuthSess, { pack } from '../../../../../models/auth-session'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { AuthSessions } from '../../../../../models'; export const meta = { tags: ['auth'], @@ -29,7 +29,7 @@ export const meta = { export default define(meta, async (ps, user) => { // Lookup session - const session = await AuthSess.findOne({ + const session = await AuthSessions.findOne({ token: ps.token }); @@ -37,5 +37,5 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.noSuchSession); } - return await pack(session, user); + return await AuthSessions.pack(session, user); }); diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts index e09e16e658e6d9368086564045ec0b2e3551d2b0..8524b96f94d8a5837c16307a1bfb4b9cee324064 100644 --- a/src/server/api/endpoints/auth/session/userkey.ts +++ b/src/server/api/endpoints/auth/session/userkey.ts @@ -1,10 +1,7 @@ import $ from 'cafy'; -import App from '../../../../../models/app'; -import AuthSess from '../../../../../models/auth-session'; -import AccessToken from '../../../../../models/access-token'; -import { pack } from '../../../../../models/user'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { Apps, AuthSessions, AccessTokens, Users } from '../../../../../models'; export const meta = { tags: ['auth'], @@ -67,7 +64,7 @@ export const meta = { export default define(meta, async (ps) => { // Lookup app - const app = await App.findOne({ + const app = await Apps.findOne({ secret: ps.appSecret }); @@ -76,13 +73,12 @@ export default define(meta, async (ps) => { } // Fetch token - const session = await AuthSess - .findOne({ - token: ps.token, - appId: app._id - }); + const session = await AuthSessions.findOne({ + token: ps.token, + appId: app.id + }); - if (session === null) { + if (session == null) { throw new ApiError(meta.errors.noSuchSession); } @@ -91,25 +87,17 @@ export default define(meta, async (ps) => { } // Lookup access token - const accessToken = await AccessToken.findOne({ - appId: app._id, + const accessToken = await AccessTokens.findOne({ + appId: app.id, userId: session.userId }); // Delete session - - /* https://github.com/Automattic/monk/issues/178 - AuthSess.deleteOne({ - _id: session._id - }); - */ - AuthSess.remove({ - _id: session._id - }); + AuthSessions.delete(session.id); return { accessToken: accessToken.token, - user: await pack(session.userId, null, { + user: await Users.pack(session.userId, null, { detail: true }) }; diff --git a/src/server/api/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts index e723cb03867f86a460263fcd2e64b08847a3a856..0d6626b2d567cf2c27cbdb647184dba95fddbaed 100644 --- a/src/server/api/endpoints/blocking/create.ts +++ b/src/server/api/endpoints/blocking/create.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import * as ms from 'ms'; -import { pack } from '../../../../models/user'; -import Blocking from '../../../../models/blocking'; import create from '../../../../services/blocking/create'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Blockings, NoteWatchings } from '../../../../models'; export const meta = { stability: 'stable', @@ -25,12 +24,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:blocks', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => { const blocker = user; // 自分自身 - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.blockeeIsYourself); } @@ -74,19 +72,22 @@ export default define(meta, async (ps, user) => { }); // Check if already blocking - const exist = await Blocking.findOne({ - blockerId: blocker._id, - blockeeId: blockee._id + const exist = await Blockings.findOne({ + blockerId: blocker.id, + blockeeId: blockee.id }); - if (exist !== null) { + if (exist != null) { throw new ApiError(meta.errors.alreadyBlocking); } // Create blocking await create(blocker, blockee); - return await pack(blockee._id, user, { - detail: true + NoteWatchings.delete({ + userId: blocker.id, + noteUserId: blockee.id }); + + return await Blockings.pack(blockee.id, user); }); diff --git a/src/server/api/endpoints/blocking/delete.ts b/src/server/api/endpoints/blocking/delete.ts index 2a9fdc5e2448665f5b31d354b121e3c963016824..e304dca8110624fdd601afa957fa9d4ae7b8e028 100644 --- a/src/server/api/endpoints/blocking/delete.ts +++ b/src/server/api/endpoints/blocking/delete.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import * as ms from 'ms'; -import { pack } from '../../../../models/user'; -import Blocking from '../../../../models/blocking'; import deleteBlocking from '../../../../services/blocking/delete'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Blockings } from '../../../../models'; export const meta = { stability: 'stable', @@ -25,12 +24,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:blocks', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => { const blocker = user; // Check if the blockee is yourself - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.blockeeIsYourself); } @@ -74,19 +72,17 @@ export default define(meta, async (ps, user) => { }); // Check not blocking - const exist = await Blocking.findOne({ - blockerId: blocker._id, - blockeeId: blockee._id + const exist = await Blockings.findOne({ + blockerId: blocker.id, + blockeeId: blockee.id }); - if (exist === null) { + if (exist == null) { throw new ApiError(meta.errors.notBlocking); } // Delete blocking await deleteBlocking(blocker, blockee); - return await pack(blockee._id, user, { - detail: true - }); + return await Blockings.pack(blockee.id, user); }); diff --git a/src/server/api/endpoints/blocking/list.ts b/src/server/api/endpoints/blocking/list.ts index b9ad6e8a3f7996d3340ddda7dc2b36c9f33872f2..a078891ab0912e62e06ca95ed28d77462c9bbc52 100644 --- a/src/server/api/endpoints/blocking/list.ts +++ b/src/server/api/endpoints/blocking/list.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Blocking, { packMany } from '../../../../models/blocking'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { Blockings } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -13,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'following-read', + kind: 'read:blocks', params: { limit: { @@ -23,12 +24,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, }, @@ -41,30 +40,12 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const query = { - blockerId: me._id - } as any; + const query = makePaginationQuery(Blockings.createQueryBuilder('blocking'), ps.sinceId, ps.untilId) + .andWhere(`blocking.blockerId = :meId`, { meId: me.id }); - const sort = { - _id: -1 - }; + const blockings = await query + .take(ps.limit) + .getMany(); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } - - const blockings = await Blocking - .find(query, { - limit: ps.limit, - sort: sort - }); - - return await packMany(blockings, me); + return await Blockings.packMany(blockings, me); }); diff --git a/src/server/api/endpoints/charts/active-users.ts b/src/server/api/endpoints/charts/active-users.ts index 9dad942e06d24150f92c540eacac296db2523e50..60fa72c5c766c8881425ece01c3f66f7635453b0 100644 --- a/src/server/api/endpoints/charts/active-users.ts +++ b/src/server/api/endpoints/charts/active-users.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import activeUsersChart from '../../../../services/chart/active-users'; +import { convertLog } from '../../../../services/chart/core'; +import { activeUsersChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -28,12 +29,7 @@ export const meta = { }, }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(activeUsersChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/charts/drive.ts b/src/server/api/endpoints/charts/drive.ts index 6bbb266f96eb0a9285ae388a645837162aec0ac3..a9676e15864075320af9a62d256f7ed94d580f60 100644 --- a/src/server/api/endpoints/charts/drive.ts +++ b/src/server/api/endpoints/charts/drive.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import driveChart, { driveLogSchema } from '../../../../services/chart/drive'; -import { convertLog } from '../../../../services/chart'; +import { convertLog } from '../../../../services/chart/core'; +import { driveChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -29,7 +29,7 @@ export const meta = { }, }, - res: convertLog(driveLogSchema), + res: convertLog(driveChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/charts/federation.ts b/src/server/api/endpoints/charts/federation.ts index c7b34f10158a5495d1685ac3ba25904f6d644df6..95f16c76a71827e444b2698f4656056d6ac4abaa 100644 --- a/src/server/api/endpoints/charts/federation.ts +++ b/src/server/api/endpoints/charts/federation.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import federationChart from '../../../../services/chart/federation'; +import { convertLog } from '../../../../services/chart/core'; +import { federationChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -28,12 +29,7 @@ export const meta = { }, }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(federationChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/charts/hashtag.ts b/src/server/api/endpoints/charts/hashtag.ts index 4db6e624080de26f965a9d2b66422339157fa419..a7ec12707e3a95372a8704ebb3f0acb5e054b796 100644 --- a/src/server/api/endpoints/charts/hashtag.ts +++ b/src/server/api/endpoints/charts/hashtag.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import hashtagChart from '../../../../services/chart/hashtag'; +import { convertLog } from '../../../../services/chart/core'; +import { hashtagChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -35,12 +36,7 @@ export const meta = { }, }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(hashtagChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/charts/instance.ts b/src/server/api/endpoints/charts/instance.ts index 3fe85f086a7d439529f6548fe1123811af45c4e7..cf3094f7e1249daa631d3f858f9a21a6c61dc6b6 100644 --- a/src/server/api/endpoints/charts/instance.ts +++ b/src/server/api/endpoints/charts/instance.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import instanceChart from '../../../../services/chart/instance'; +import { convertLog } from '../../../../services/chart/core'; +import { instanceChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -36,12 +37,7 @@ export const meta = { } }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(instanceChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/charts/network.ts b/src/server/api/endpoints/charts/network.ts index 48b1d0f66f7c7f53ba3f4c3ee0da6d26a0aa85a8..c0fcd95fe9be1ce40bf2117eb0efcda4d47ba5b5 100644 --- a/src/server/api/endpoints/charts/network.ts +++ b/src/server/api/endpoints/charts/network.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import networkChart from '../../../../services/chart/network'; +import { convertLog } from '../../../../services/chart/core'; +import { networkChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -28,12 +29,7 @@ export const meta = { }, }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(networkChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts index cc0ca8bef7a4ad63e557355705daec071def4bcb..86f30e4b898a045904dd68d44d9779d1cc1ee819 100644 --- a/src/server/api/endpoints/charts/notes.ts +++ b/src/server/api/endpoints/charts/notes.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import notesChart, { notesLogSchema } from '../../../../services/chart/notes'; -import { convertLog } from '../../../../services/chart'; +import { convertLog } from '../../../../services/chart/core'; +import { notesChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -29,7 +29,7 @@ export const meta = { }, }, - res: convertLog(notesLogSchema), + res: convertLog(notesChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/charts/user/drive.ts b/src/server/api/endpoints/charts/user/drive.ts index 064c7c7b721ca955f4b27428f21a63bbad71dd2f..e3696dfda1adb2b78d6bf0cf9216183b5d419ccc 100644 --- a/src/server/api/endpoints/charts/user/drive.ts +++ b/src/server/api/endpoints/charts/user/drive.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; -import perUserDriveChart, { perUserDriveLogSchema } from '../../../../../services/chart/per-user-drive'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import { convertLog } from '../../../../../services/chart'; +import { ID } from '../../../../../misc/cafy-id'; +import { convertLog } from '../../../../../services/chart/core'; +import { perUserDriveChart } from '../../../../../services/chart'; export const meta = { stability: 'stable', @@ -31,7 +31,6 @@ export const meta = { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -39,7 +38,7 @@ export const meta = { } }, - res: convertLog(perUserDriveLogSchema), + res: convertLog(perUserDriveChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/charts/user/following.ts b/src/server/api/endpoints/charts/user/following.ts index f5b1355038518f1f5c9d8c4aed9a7e800c54404a..8feba0bd169a16103ee94803fe7fa4220e498f7b 100644 --- a/src/server/api/endpoints/charts/user/following.ts +++ b/src/server/api/endpoints/charts/user/following.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; -import perUserFollowingChart, { perUserFollowingLogSchema } from '../../../../../services/chart/per-user-following'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import { convertLog } from '../../../../../services/chart'; +import { ID } from '../../../../../misc/cafy-id'; +import { convertLog } from '../../../../../services/chart/core'; +import { perUserFollowingChart } from '../../../../../services/chart'; export const meta = { stability: 'stable', @@ -31,7 +31,6 @@ export const meta = { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -39,9 +38,9 @@ export const meta = { } }, - res: convertLog(perUserFollowingLogSchema), + res: convertLog(perUserFollowingChart.schema), }; export default define(meta, async (ps) => { - return await perUserFollowingChart.getChart(ps.span as any, ps.limit, ps.userId); + return await perUserFollowingChart.getChart(ps.span as any, ps.limit, ps.userId); }); diff --git a/src/server/api/endpoints/charts/user/notes.ts b/src/server/api/endpoints/charts/user/notes.ts index 7e31978bf3e741e9b7df443d9483c2fa46f4bd77..8c1db54f76843c4ee4a4554907b4cee7811a3086 100644 --- a/src/server/api/endpoints/charts/user/notes.ts +++ b/src/server/api/endpoints/charts/user/notes.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; -import perUserNotesChart, { perUserNotesLogSchema } from '../../../../../services/chart/per-user-notes'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import { convertLog } from '../../../../../services/chart'; +import { ID } from '../../../../../misc/cafy-id'; +import { convertLog } from '../../../../../services/chart/core'; +import { perUserNotesChart } from '../../../../../services/chart'; export const meta = { stability: 'stable', @@ -31,7 +31,6 @@ export const meta = { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -39,7 +38,7 @@ export const meta = { } }, - res: convertLog(perUserNotesLogSchema), + res: convertLog(perUserNotesChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/charts/user/reactions.ts b/src/server/api/endpoints/charts/user/reactions.ts index 51ff83f20ebd3c39a031b7372e02ee67b67779a7..7c9b2508ae1ad45607e62ecb81e73607c0bc94e5 100644 --- a/src/server/api/endpoints/charts/user/reactions.ts +++ b/src/server/api/endpoints/charts/user/reactions.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; -import perUserReactionsChart from '../../../../../services/chart/per-user-reactions'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; +import { convertLog } from '../../../../../services/chart/core'; +import { perUserReactionsChart } from '../../../../../services/chart'; export const meta = { stability: 'stable', @@ -30,7 +31,6 @@ export const meta = { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -38,12 +38,7 @@ export const meta = { } }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(perUserReactionsChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/charts/users.ts b/src/server/api/endpoints/charts/users.ts index 9de54a630e63887ec38e7658a7d1b96e61673b7f..3ed5e093493907dae4ac9b89ab54b0e783476539 100644 --- a/src/server/api/endpoints/charts/users.ts +++ b/src/server/api/endpoints/charts/users.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import usersChart, { usersLogSchema } from '../../../../services/chart/users'; -import { convertLog } from '../../../../services/chart'; +import { convertLog } from '../../../../services/chart/core'; +import { usersChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -29,7 +29,7 @@ export const meta = { }, }, - res: convertLog(usersLogSchema), + res: convertLog(usersChart.schema), }; export default define(meta, async (ps) => { diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts index 138adffad2dc2ca19d632136009e12dc63299709..adf780301bc89e82a5e6b170f2c162bb4c5b19ae 100644 --- a/src/server/api/endpoints/drive.ts +++ b/src/server/api/endpoints/drive.ts @@ -1,6 +1,6 @@ -import DriveFile from '../../../models/drive-file'; import define from '../define'; import fetchMeta from '../../../misc/fetch-meta'; +import { DriveFiles } from '../../../models'; export const meta = { desc: { @@ -12,7 +12,7 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', res: { type: 'object', @@ -31,27 +31,7 @@ export default define(meta, async (ps, user) => { const instance = await fetchMeta(); // Calculate drive usage - const usage = await DriveFile.aggregate([{ - $match: { - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then((aggregates: any[]) => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); + const usage = await DriveFiles.clacDriveUsageOf(user); return { capacity: 1024 * 1024 * instance.localDriveCapacityMb, diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts index f108e820e7def81b4caed6de3835268d29b71e17..400b73d3b73edf9b79b49504c19cffc060660986 100644 --- a/src/server/api/endpoints/drive/files.ts +++ b/src/server/api/endpoints/drive/files.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import DriveFile, { packMany } from '../../../../models/drive-file'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { DriveFiles } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -13,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { limit: { @@ -23,18 +24,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, folderId: { validator: $.optional.nullable.type(ID), default: null as any, - transform: transform, }, type: { @@ -51,36 +49,24 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const sort = { - _id: -1 - }; + const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) + .andWhere('file.userId = :userId', { userId: user.id }); - const query = { - 'metadata.userId': user._id, - 'metadata.folderId': ps.folderId, - 'metadata.deletedAt': { $exists: false } - } as any; - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; + if (ps.folderId) { + query.andWhere('file.folderId = :folderId', { folderId: ps.folderId }); + } else { + query.andWhere('file.folderId IS NULL'); } if (ps.type) { - query.contentType = new RegExp(`^${ps.type.replace(/\*/g, '.+?')}$`); + if (ps.type.endsWith('/*')) { + query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); + } else { + query.andWhere('file.type = :type', { type: ps.type }); + } } - const files = await DriveFile - .find(query, { - limit: ps.limit, - sort: sort - }); + const files = await query.take(ps.limit).getMany(); - return await packMany(files, { detail: false, self: true }); + return await DriveFiles.packMany(files, { detail: false, self: true }); }); diff --git a/src/server/api/endpoints/drive/files/attached-notes.ts b/src/server/api/endpoints/drive/files/attached-notes.ts index c9eeab58c597443f9eeab522e03a49f2e98565d4..7214463dde1bbd85682d3b3b2bb04c30e5fb4053 100644 --- a/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/src/server/api/endpoints/drive/files/attached-notes.ts @@ -1,9 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFile from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import { packMany } from '../../../../../models/note'; import { ApiError } from '../../../error'; +import { DriveFiles } from '../../../../../models'; export const meta = { stability: 'stable', @@ -17,12 +16,11 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { fileId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ•ã‚¡ã‚¤ãƒ«ID', 'en-US': 'Target file ID' @@ -48,18 +46,17 @@ export const meta = { export default define(meta, async (ps, user) => { // Fetch file - const file = await DriveFile - .findOne({ - _id: ps.fileId, - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - }); + const file = await DriveFiles.findOne({ + id: ps.fileId, + userId: user.id, + }); - if (file === null) { + if (file == null) { throw new ApiError(meta.errors.noSuchFile); } + /* v11 TODO return await packMany(file.metadata.attachedNoteIds || [], user, { detail: true - }); + });*/ }); diff --git a/src/server/api/endpoints/drive/files/check-existence.ts b/src/server/api/endpoints/drive/files/check-existence.ts index 926411c83accbfb6dba18950c74b1c82c79dba46..3a87a9497fa1d723ef71df06801d6480113c06f7 100644 --- a/src/server/api/endpoints/drive/files/check-existence.ts +++ b/src/server/api/endpoints/drive/files/check-existence.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; -import DriveFile, { pack } from '../../../../../models/drive-file'; import define from '../../../define'; +import { DriveFiles } from '../../../../../models'; export const meta = { desc: { @@ -12,7 +12,7 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { md5: { @@ -29,11 +29,12 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const file = await DriveFile.findOne({ + const file = await DriveFiles.findOne({ md5: ps.md5, - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } + userId: user.id, }); - return { file: file ? await pack(file, { self: true }) : null }; + return { + file: file ? await DriveFiles.pack(file, { self: true }) : null + }; }); diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index b2979c4888b86201cab637381cd90917c000eb5d..5702c70fc0e89318c8f70fc9e493b2760ba2fee5 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -1,11 +1,11 @@ import * as ms from 'ms'; import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import { validateFileName, pack } from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import create from '../../../../../services/drive/add-file'; import define from '../../../define'; import { apiLogger } from '../../../logger'; import { ApiError } from '../../../error'; +import { DriveFiles } from '../../../../../models'; export const meta = { desc: { @@ -24,12 +24,11 @@ export const meta = { requireFile: true, - kind: 'drive-write', + kind: 'write:drive', params: { folderId: { validator: $.optional.nullable.type(ID), - transform: transform, default: null as any, desc: { 'ja-JP': 'フォルダID' @@ -78,7 +77,7 @@ export default define(meta, async (ps, user, app, file, cleanup) => { name = null; } else if (name === 'blob') { name = null; - } else if (!validateFileName(name)) { + } else if (!DriveFiles.validateFileName(name)) { throw new ApiError(meta.errors.invalidFileName); } } else { @@ -88,7 +87,7 @@ export default define(meta, async (ps, user, app, file, cleanup) => { try { // Create file const driveFile = await create(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive); - return pack(driveFile, { self: true }); + return DriveFiles.pack(driveFile, { self: true }); } catch (e) { apiLogger.error(e); throw new ApiError(); diff --git a/src/server/api/endpoints/drive/files/delete.ts b/src/server/api/endpoints/drive/files/delete.ts index dd4e187fcd7544edfb01737a9505627fe7b0d1a3..d8cc5ec0a19aea50f1b70d44b45cad047550517e 100644 --- a/src/server/api/endpoints/drive/files/delete.ts +++ b/src/server/api/endpoints/drive/files/delete.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFile from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import del from '../../../../../services/drive/delete-file'; import { publishDriveStream } from '../../../../../services/stream'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { DriveFiles } from '../../../../../models'; export const meta = { stability: 'stable', @@ -18,12 +18,11 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { fileId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ•ã‚¡ã‚¤ãƒ«ID', 'en-US': 'Target file ID' @@ -47,17 +46,13 @@ export const meta = { }; export default define(meta, async (ps, user) => { - // Fetch file - const file = await DriveFile - .findOne({ - _id: ps.fileId - }); + const file = await DriveFiles.findOne(ps.fileId); - if (file === null) { + if (file == null) { throw new ApiError(meta.errors.noSuchFile); } - if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) { + if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } @@ -65,7 +60,5 @@ export default define(meta, async (ps, user) => { await del(file); // Publish fileDeleted event - publishDriveStream(user._id, 'fileDeleted', file._id); - - return; + publishDriveStream(user.id, 'fileDeleted', file.id); }); diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts index 0d4102a48fc87e33196c0b8de1e163d1e83669f6..265850f84c82f912552c200ee7654d8c80dffd08 100644 --- a/src/server/api/endpoints/drive/files/find.ts +++ b/src/server/api/endpoints/drive/files/find.ts @@ -1,14 +1,14 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFile, { pack } from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; +import { DriveFiles } from '../../../../../models'; export const meta = { requireCredential: true, tags: ['drive'], - kind: 'drive-read', + kind: 'read:drive', params: { name: { @@ -17,7 +17,6 @@ export const meta = { folderId: { validator: $.optional.nullable.type(ID), - transform: transform, default: null as any, desc: { 'ja-JP': 'フォルダID' @@ -27,12 +26,11 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const files = await DriveFile - .find({ - filename: ps.name, - 'metadata.userId': user._id, - 'metadata.folderId': ps.folderId - }); + const files = await DriveFiles.find({ + name: ps.name, + userId: user.id, + folderId: ps.folderId + }); - return await Promise.all(files.map(file => pack(file, { self: true }))); + return await Promise.all(files.map(file => DriveFiles.pack(file, { self: true }))); }); diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts index 6d63a8605c5e059074bddde69b8ac74062bd1dee..b516ec2df67a5d7660f3c0aa4cbcc05f607d2bbf 100644 --- a/src/server/api/endpoints/drive/files/show.ts +++ b/src/server/api/endpoints/drive/files/show.ts @@ -1,10 +1,9 @@ import $ from 'cafy'; -import * as mongo from 'mongodb'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFile, { pack, IDriveFile } from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import config from '../../../../../config'; import { ApiError } from '../../../error'; +import { DriveFile } from '../../../../../models/entities/drive-file'; +import { DriveFiles } from '../../../../../models'; export const meta = { stability: 'stable', @@ -18,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { fileId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ•ã‚¡ã‚¤ãƒ«ID', 'en-US': 'Target file ID' @@ -65,49 +63,33 @@ export const meta = { }; export default define(meta, async (ps, user) => { - let file: IDriveFile; + let file: DriveFile; if (ps.fileId) { - file = await DriveFile.findOne({ - _id: ps.fileId, - 'metadata.deletedAt': { $exists: false } - }); + file = await DriveFiles.findOne(ps.fileId); } else if (ps.url) { - const isInternalStorageUrl = ps.url.startsWith(config.driveUrl); - if (isInternalStorageUrl) { - // Extract file ID from url - // e.g. - // http://misskey.local/files/foo?original=bar --> foo - const fileId = new mongo.ObjectID(ps.url.replace(config.driveUrl, '').replace(/\?(.*)$/, '').replace(/\//g, '')); - file = await DriveFile.findOne({ - _id: fileId, - 'metadata.deletedAt': { $exists: false } - }); - } else { - file = await DriveFile.findOne({ - $or: [{ - 'metadata.url': ps.url - }, { - 'metadata.webpublicUrl': ps.url - }, { - 'metadata.thumbnailUrl': ps.url - }], - 'metadata.deletedAt': { $exists: false } - }); - } + file = await DriveFiles.findOne({ + where: [{ + url: ps.url + }, { + webpublicUrl: ps.url + }, { + thumbnailUrl: ps.url + }], + }); } else { throw new ApiError(meta.errors.fileIdOrUrlRequired); } - if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) { + if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } - if (file === null) { + if (file == null) { throw new ApiError(meta.errors.noSuchFile); } - return await pack(file, { + return await DriveFiles.pack(file, { detail: true, self: true }); diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts index c8803bec3a1a265e0ce32efda50129dc434b69c5..81e86a27345c235bb4121a90207e9271f1614e0b 100644 --- a/src/server/api/endpoints/drive/files/update.ts +++ b/src/server/api/endpoints/drive/files/update.ts @@ -1,11 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder from '../../../../../models/drive-folder'; -import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import { publishDriveStream } from '../../../../../services/stream'; import define from '../../../define'; -import Note from '../../../../../models/note'; import { ApiError } from '../../../error'; +import { DriveFiles, DriveFolders } from '../../../../../models'; export const meta = { desc: { @@ -17,12 +15,11 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { fileId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ•ã‚¡ã‚¤ãƒ«ID' } @@ -30,7 +27,6 @@ export const meta = { folderId: { validator: $.optional.nullable.type(ID), - transform: transform, default: undefined as any, desc: { 'ja-JP': 'フォルダID' @@ -38,7 +34,7 @@ export const meta = { }, name: { - validator: $.optional.str.pipe(validateFileName), + validator: $.optional.str.pipe(DriveFiles.validateFileName), default: undefined as any, desc: { 'ja-JP': 'ファイルå', @@ -78,69 +74,47 @@ export const meta = { }; export default define(meta, async (ps, user) => { - // Fetch file - const file = await DriveFile - .findOne({ - _id: ps.fileId - }); + const file = await DriveFiles.findOne(ps.fileId); - if (file === null) { + if (file == null) { throw new ApiError(meta.errors.noSuchFile); } - if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) { + if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } - if (ps.name) file.filename = ps.name; + if (ps.name) file.name = ps.name; - if (ps.isSensitive !== undefined) file.metadata.isSensitive = ps.isSensitive; + if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive; if (ps.folderId !== undefined) { if (ps.folderId === null) { - file.metadata.folderId = null; + file.folderId = null; } else { - // Fetch folder - const folder = await DriveFolder - .findOne({ - _id: ps.folderId, - userId: user._id - }); - - if (folder === null) { + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); + + if (folder == null) { throw new ApiError(meta.errors.noSuchFolder); } - file.metadata.folderId = folder._id; + file.folderId = folder.id; } } - await DriveFile.update(file._id, { - $set: { - filename: file.filename, - 'metadata.folderId': file.metadata.folderId, - 'metadata.isSensitive': file.metadata.isSensitive - } - }); - - // ドライブã®ãƒ•ã‚¡ã‚¤ãƒ«ãŒéžæ£è¦åŒ–ã•ã‚Œã¦ã„るドã‚ュメントも更新 - Note.find({ - '_files._id': file._id - }).then(notes => { - for (const note of notes) { - note._files[note._files.findIndex(f => f._id.equals(file._id))] = file; - Note.update({ _id: note._id }, { - $set: { - _files: note._files - } - }); - } + await DriveFiles.update(file.id, { + name: file.name, + folderId: file.folderId, + isSensitive: file.isSensitive }); - const fileObj = await pack(file, { self: true }); + const fileObj = await DriveFiles.pack(file, { self: true }); // Publish fileUpdated event - publishDriveStream(user._id, 'fileUpdated', fileObj); + publishDriveStream(user.id, 'fileUpdated', fileObj); return fileObj; }); diff --git a/src/server/api/endpoints/drive/files/upload-from-url.ts b/src/server/api/endpoints/drive/files/upload-from-url.ts index 93a9fa62fae8b5c9284b644042f63016397650a7..034ab10f19e5eb3e23b9de087dfdf5f15f4f93aa 100644 --- a/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import * as ms from 'ms'; -import { pack } from '../../../../../models/drive-file'; import uploadFromUrl from '../../../../../services/drive/upload-from-url'; import define from '../../../define'; +import { DriveFiles } from '../../../../../models'; export const meta = { desc: { @@ -19,7 +19,7 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { url: { @@ -30,7 +30,6 @@ export const meta = { folderId: { validator: $.optional.nullable.type(ID), default: null as any, - transform: transform }, isSensitive: { @@ -53,5 +52,5 @@ export const meta = { }; export default define(meta, async (ps, user) => { - return await pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true }); + return await DriveFiles.pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true }); }); diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts index 73c179f7be2f805190514f99d51e63ce5dd866d4..f5c38164073681b4add40726bd81174a49f439d1 100644 --- a/src/server/api/endpoints/drive/folders.ts +++ b/src/server/api/endpoints/drive/folders.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import DriveFolder, { pack } from '../../../../models/drive-folder'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { DriveFolders } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -13,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { limit: { @@ -23,18 +24,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, folderId: { validator: $.optional.nullable.type(ID), default: null as any, - transform: transform, } }, @@ -47,29 +45,16 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const sort = { - _id: -1 - }; - const query = { - userId: user._id, - parentId: ps.folderId - } as any; - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; + const query = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId) + .andWhere('folder.userId = :userId', { userId: user.id }); + + if (ps.folderId) { + query.andWhere('folder.parentId = :parentId', { parentId: ps.folderId }); + } else { + query.andWhere('folder.parentId IS NULL'); } - const folders = await DriveFolder - .find(query, { - limit: ps.limit, - sort: sort - }); + const folders = await query.take(ps.limit).getMany(); - return await Promise.all(folders.map(folder => pack(folder))); + return await Promise.all(folders.map(folder => DriveFolders.pack(folder))); }); diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts index 5fab0b91a1295f9ca6034eb3709a4da5e3f8f181..5530abf9dc310fae5002a06602f5817da206746a 100644 --- a/src/server/api/endpoints/drive/folders/create.ts +++ b/src/server/api/endpoints/drive/folders/create.ts @@ -1,9 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder'; +import { ID } from '../../../../../misc/cafy-id'; import { publishDriveStream } from '../../../../../services/stream'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { DriveFolders } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { stability: 'stable', @@ -17,11 +18,11 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { name: { - validator: $.optional.str.pipe(isValidFolderName), + validator: $.optional.str.pipe(DriveFolders.validateFolderName), default: 'Untitled', desc: { 'ja-JP': 'フォルダå', @@ -31,7 +32,6 @@ export const meta = { parentId: { validator: $.optional.nullable.type(ID), - transform: transform, desc: { 'ja-JP': '親フォルダID', 'en-US': 'Parent folder ID' @@ -53,29 +53,29 @@ export default define(meta, async (ps, user) => { let parent = null; if (ps.parentId) { // Fetch parent folder - parent = await DriveFolder - .findOne({ - _id: ps.parentId, - userId: user._id - }); + parent = await DriveFolders.findOne({ + id: ps.parentId, + userId: user.id + }); - if (parent === null) { + if (parent == null) { throw new ApiError(meta.errors.noSuchFolder); } } // Create folder - const folder = await DriveFolder.insert({ + const folder = await DriveFolders.save({ + id: genId(), createdAt: new Date(), name: ps.name, - parentId: parent !== null ? parent._id : null, - userId: user._id + parentId: parent !== null ? parent.id : null, + userId: user.id }); - const folderObj = await pack(folder); + const folderObj = await DriveFolders.pack(folder); // Publish folderCreated event - publishDriveStream(user._id, 'folderCreated', folderObj); + publishDriveStream(user.id, 'folderCreated', folderObj); return folderObj; }); diff --git a/src/server/api/endpoints/drive/folders/delete.ts b/src/server/api/endpoints/drive/folders/delete.ts index 9f22bf9ea7e9f9fe6b73d1d1733fadb722f5806a..fe6c05ad07d65543154af96c3e007d1cb88720eb 100644 --- a/src/server/api/endpoints/drive/folders/delete.ts +++ b/src/server/api/endpoints/drive/folders/delete.ts @@ -1,10 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder from '../../../../../models/drive-folder'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { publishDriveStream } from '../../../../../services/stream'; -import DriveFile from '../../../../../models/drive-file'; import { ApiError } from '../../../error'; +import { DriveFolders, DriveFiles } from '../../../../../models'; export const meta = { stability: 'stable', @@ -18,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { folderId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ•ã‚©ãƒ«ãƒ€ID', 'en-US': 'Target folder ID' @@ -48,29 +46,26 @@ export const meta = { export default define(meta, async (ps, user) => { // Get folder - const folder = await DriveFolder - .findOne({ - _id: ps.folderId, - userId: user._id - }); + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); - if (folder === null) { + if (folder == null) { throw new ApiError(meta.errors.noSuchFolder); } const [childFoldersCount, childFilesCount] = await Promise.all([ - DriveFolder.count({ parentId: folder._id }), - DriveFile.count({ 'metadata.folderId': folder._id }) + DriveFolders.count({ parentId: folder.id }), + DriveFiles.count({ folderId: folder.id }) ]); if (childFoldersCount !== 0 || childFilesCount !== 0) { throw new ApiError(meta.errors.hasChildFilesOrFolders); } - await DriveFolder.remove({ _id: folder._id }); + await DriveFolders.delete(folder.id); // Publish folderCreated event - publishDriveStream(user._id, 'folderDeleted', folder._id); - - return; + publishDriveStream(user.id, 'folderDeleted', folder.id); }); diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts index 16b6c10633a3e753a51d5fc87cd41f9d1a18672a..f0989ec5ae474574233b79f61d643bc6292ad2e3 100644 --- a/src/server/api/endpoints/drive/folders/find.ts +++ b/src/server/api/endpoints/drive/folders/find.ts @@ -1,14 +1,14 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder, { pack } from '../../../../../models/drive-folder'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; +import { DriveFolders } from '../../../../../models'; export const meta = { tags: ['drive'], requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { name: { @@ -17,7 +17,6 @@ export const meta = { parentId: { validator: $.optional.nullable.type(ID), - transform: transform, default: null as any, desc: { 'ja-JP': 'フォルダID' @@ -34,12 +33,11 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const folders = await DriveFolder - .find({ - name: ps.name, - userId: user._id, - parentId: ps.parentId - }); - - return await Promise.all(folders.map(folder => pack(folder))); + const folders = await DriveFolders.find({ + name: ps.name, + userId: user.id, + parentId: ps.parentId + }); + + return await Promise.all(folders.map(folder => DriveFolders.pack(folder))); }); diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts index bbfcbed51f995183caebecbe616e3fec39db047b..60507e7d7f15aacb9e7806dbae9d11ed2595f70d 100644 --- a/src/server/api/endpoints/drive/folders/show.ts +++ b/src/server/api/endpoints/drive/folders/show.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder, { pack } from '../../../../../models/drive-folder'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { DriveFolders } from '../../../../../models'; export const meta = { stability: 'stable', @@ -16,12 +16,11 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { folderId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ•ã‚©ãƒ«ãƒ€ID', 'en-US': 'Target folder ID' @@ -44,17 +43,16 @@ export const meta = { export default define(meta, async (ps, user) => { // Get folder - const folder = await DriveFolder - .findOne({ - _id: ps.folderId, - userId: user._id - }); + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); - if (folder === null) { + if (folder == null) { throw new ApiError(meta.errors.noSuchFolder); } - return await pack(folder, { + return await DriveFolders.pack(folder, { detail: true }); }); diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts index a1ee2669f013527c5fa3dfbcdd4c033ed46d2ceb..90129bed631c6a27c03c684ce0727930261b55d3 100644 --- a/src/server/api/endpoints/drive/folders/update.ts +++ b/src/server/api/endpoints/drive/folders/update.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder'; +import { ID } from '../../../../../misc/cafy-id'; import { publishDriveStream } from '../../../../../services/stream'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { DriveFolders } from '../../../../../models'; export const meta = { stability: 'stable', @@ -17,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { folderId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ•ã‚©ãƒ«ãƒ€ID', 'en-US': 'Target folder ID' @@ -30,7 +29,7 @@ export const meta = { }, name: { - validator: $.optional.str.pipe(isValidFolderName), + validator: $.optional.str.pipe(DriveFolders.validateFolderName), desc: { 'ja-JP': 'フォルダå', 'en-US': 'Folder name' @@ -39,7 +38,6 @@ export const meta = { parentId: { validator: $.optional.nullable.type(ID), - transform: transform, desc: { 'ja-JP': '親フォルダID', 'en-US': 'Parent folder ID' @@ -70,46 +68,41 @@ export const meta = { export default define(meta, async (ps, user) => { // Fetch folder - const folder = await DriveFolder - .findOne({ - _id: ps.folderId, - userId: user._id - }); + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); - if (folder === null) { + if (folder == null) { throw new ApiError(meta.errors.noSuchFolder); } if (ps.name) folder.name = ps.name; if (ps.parentId !== undefined) { - if (ps.parentId.equals(folder._id)) { + if (ps.parentId === folder.id) { throw new ApiError(meta.errors.recursiveNesting); } else if (ps.parentId === null) { folder.parentId = null; } else { // Get parent folder - const parent = await DriveFolder - .findOne({ - _id: ps.parentId, - userId: user._id - }); + const parent = await DriveFolders.findOne({ + id: ps.parentId, + userId: user.id + }); - if (parent === null) { + if (parent == null) { throw new ApiError(meta.errors.noSuchParentFolder); } // Check if the circular reference will occur async function checkCircle(folderId: any): Promise<boolean> { // Fetch folder - const folder2 = await DriveFolder.findOne({ - _id: folderId - }, { - _id: true, - parentId: true + const folder2 = await DriveFolders.findOne({ + id: folderId }); - if (folder2._id.equals(folder._id)) { + if (folder2.id === folder.id) { return true; } else if (folder2.parentId) { return await checkCircle(folder2.parentId); @@ -124,22 +117,20 @@ export default define(meta, async (ps, user) => { } } - folder.parentId = parent._id; + folder.parentId = parent.id; } } // Update - DriveFolder.update(folder._id, { - $set: { - name: folder.name, - parentId: folder.parentId - } + DriveFolders.update(folder.id, { + name: folder.name, + parentId: folder.parentId }); - const folderObj = await pack(folder); + const folderObj = await DriveFolders.pack(folder); // Publish folderUpdated event - publishDriveStream(user._id, 'folderUpdated', folderObj); + publishDriveStream(user.id, 'folderUpdated', folderObj); return folderObj; }); diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts index 916482be4d76d6665001d4b0bfd6555ee1fd1905..9a84627767cb91fefdc6a3933877f5b887aaac94 100644 --- a/src/server/api/endpoints/drive/stream.ts +++ b/src/server/api/endpoints/drive/stream.ts @@ -1,14 +1,15 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import DriveFile, { packMany } from '../../../../models/drive-file'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { DriveFiles } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { tags: ['drive'], requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { limit: { @@ -18,12 +19,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, type: { @@ -40,35 +39,18 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const sort = { - _id: -1 - }; - - const query = { - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - } as any; - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } + const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) + .andWhere('file.userId = :userId', { userId: user.id }); if (ps.type) { - query.contentType = new RegExp(`^${ps.type.replace(/\*/g, '.+?')}$`); + if (ps.type.endsWith('/*')) { + query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); + } else { + query.andWhere('file.type = :type', { type: ps.type }); + } } - const files = await DriveFile - .find(query, { - limit: ps.limit, - sort: sort - }); + const files = await query.take(ps.limit).getMany(); - return await packMany(files, { self: true }); + return await DriveFiles.packMany(files, { detail: false, self: true }); }); diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts index f81f81822e9b0835fa1a2ec66831628c55e9137a..1946d26dece6fb1944c126a2fecd34aa8daa81cc 100644 --- a/src/server/api/endpoints/federation/instances.ts +++ b/src/server/api/endpoints/federation/instances.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import Instance from '../../../../models/instance'; +import { Instances } from '../../../../models'; +import fetchMeta from '../../../../misc/fetch-meta'; export const meta = { tags: ['federation'], @@ -37,92 +38,55 @@ export const meta = { }; export default define(meta, async (ps, me) => { - let sort; - - if (ps.sort) { - if (ps.sort == '+notes') { - sort = { - notesCount: -1 - }; - } else if (ps.sort == '-notes') { - sort = { - notesCount: 1 - }; - } else if (ps.sort == '+users') { - sort = { - usersCount: -1 - }; - } else if (ps.sort == '-users') { - sort = { - usersCount: 1 - }; - } else if (ps.sort == '+following') { - sort = { - followingCount: -1 - }; - } else if (ps.sort == '-following') { - sort = { - followingCount: 1 - }; - } else if (ps.sort == '+followers') { - sort = { - followersCount: -1 - }; - } else if (ps.sort == '-followers') { - sort = { - followersCount: 1 - }; - } else if (ps.sort == '+caughtAt') { - sort = { - caughtAt: -1 - }; - } else if (ps.sort == '-caughtAt') { - sort = { - caughtAt: 1 - }; - } else if (ps.sort == '+lastCommunicatedAt') { - sort = { - lastCommunicatedAt: -1 - }; - } else if (ps.sort == '-lastCommunicatedAt') { - sort = { - lastCommunicatedAt: 1 - }; - } else if (ps.sort == '+driveUsage') { - sort = { - driveUsage: -1 - }; - } else if (ps.sort == '-driveUsage') { - sort = { - driveUsage: 1 - }; - } else if (ps.sort == '+driveFiles') { - sort = { - driveFiles: -1 - }; - } else if (ps.sort == '-driveFiles') { - sort = { - driveFiles: 1 - }; + const query = Instances.createQueryBuilder('instance'); + + switch (ps.sort) { + case '+notes': query.orderBy('instance.notesCount', 'DESC'); break; + case '-notes': query.orderBy('instance.notesCount', 'ASC'); break; + case '+usersCount': query.orderBy('instance.usersCount', 'DESC'); break; + case '-usersCount': query.orderBy('instance.usersCount', 'ASC'); break; + case '+followingCount': query.orderBy('instance.followingCount', 'DESC'); break; + case '-followingCount': query.orderBy('instance.followingCount', 'ASC'); break; + case '+followersCount': query.orderBy('instance.followersCount', 'DESC'); break; + case '-followersCount': query.orderBy('instance.followersCount', 'ASC'); break; + case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break; + case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break; + case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break; + case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break; + case '+driveUsage': query.orderBy('instance.driveUsage', 'DESC'); break; + case '-driveUsage': query.orderBy('instance.driveUsage', 'ASC'); break; + case '+driveFiles': query.orderBy('instance.driveFiles', 'DESC'); break; + case '-driveFiles': query.orderBy('instance.driveFiles', 'ASC'); break; + + default: query.orderBy('instance.id', 'DESC'); break; + } + + if (typeof ps.blocked === 'boolean') { + const meta = await fetchMeta(); + if (ps.blocked) { + query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts }); + } else { + query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts }); } - } else { - sort = { - _id: -1 - }; } - const q = {} as any; + if (typeof ps.notResponding === 'boolean') { + if (ps.notResponding) { + query.andWhere('instance.isNotResponding = TRUE'); + } else { + query.andWhere('instance.isNotResponding = FALSE'); + } + } - if (typeof ps.blocked === 'boolean') q.isBlocked = ps.blocked; - if (typeof ps.notResponding === 'boolean') q.isNotResponding = ps.notResponding; - if (typeof ps.markedAsClosed === 'boolean') q.isMarkedAsClosed = ps.markedAsClosed; + if (typeof ps.markedAsClosed === 'boolean') { + if (ps.markedAsClosed) { + query.andWhere('instance.isMarkedAsClosed = TRUE'); + } else { + query.andWhere('instance.isMarkedAsClosed = FALSE'); + } + } - const instances = await Instance - .find(q, { - limit: ps.limit, - sort: sort, - skip: ps.offset - }); + const instances = await query.take(ps.limit).skip(ps.offset).getMany(); return instances; }); diff --git a/src/server/api/endpoints/federation/show-instance.ts b/src/server/api/endpoints/federation/show-instance.ts index e7f68620af642ef5446600d8400fc654ea8a9a48..875afa05b2f7f1f4c6ca2c51a745cad0d8bcfbf3 100644 --- a/src/server/api/endpoints/federation/show-instance.ts +++ b/src/server/api/endpoints/federation/show-instance.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import define from '../../define'; -import Instance from '../../../../models/instance'; +import { Instances } from '../../../../models'; export const meta = { tags: ['federation'], @@ -15,7 +15,7 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const instance = await Instance + const instance = await Instances .findOne({ host: ps.host }); return instance; diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index 81b239955140d07d521b3eedbac0b8a3c8200361..5b43815a5e0f663f69885dcbc971df7f618a4cfc 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import * as ms from 'ms'; -import { pack } from '../../../../models/user'; -import Following from '../../../../models/following'; import create from '../../../../services/following/create'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Followings, Users } from '../../../../models'; export const meta = { stability: 'stable', @@ -25,12 +24,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:following', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -75,7 +73,7 @@ export default define(meta, async (ps, user) => { const follower = user; // 自分自身 - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.followeeIsYourself); } @@ -86,12 +84,12 @@ export default define(meta, async (ps, user) => { }); // Check if already following - const exist = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id + const exist = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id }); - if (exist !== null) { + if (exist != null) { throw new ApiError(meta.errors.alreadyFollowing); } @@ -103,5 +101,5 @@ export default define(meta, async (ps, user) => { throw e; } - return await pack(followee._id, user); + return await Users.pack(followee.id, user); }); diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts index 8f8249b1e849558ac2d68d7e28e2d99a9a7e81b7..240a037c9e30dc6eaeeab21f82923a92de2eb5a7 100644 --- a/src/server/api/endpoints/following/delete.ts +++ b/src/server/api/endpoints/following/delete.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import * as ms from 'ms'; -import { pack } from '../../../../models/user'; -import Following from '../../../../models/following'; import deleteFollowing from '../../../../services/following/delete'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Followings, Users } from '../../../../models'; export const meta = { stability: 'stable', @@ -25,12 +24,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:following', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => { const follower = user; // Check if the followee is yourself - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.followeeIsYourself); } @@ -74,16 +72,16 @@ export default define(meta, async (ps, user) => { }); // Check not following - const exist = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id + const exist = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id }); - if (exist === null) { + if (exist == null) { throw new ApiError(meta.errors.notFollowing); } await deleteFollowing(follower, followee); - return await pack(followee._id, user); + return await Users.pack(followee.id, user); }); diff --git a/src/server/api/endpoints/following/requests/accept.ts b/src/server/api/endpoints/following/requests/accept.ts index 0975990c0224c0471dcfc2745a7f5f51cc31996d..65c24f7be90c99c3867d32dbd1aebfd84292c36a 100644 --- a/src/server/api/endpoints/following/requests/accept.ts +++ b/src/server/api/endpoints/following/requests/accept.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import acceptFollowRequest from '../../../../../services/following/requests/accept'; import define from '../../../define'; import { ApiError } from '../../../error'; @@ -15,12 +15,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:following', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' diff --git a/src/server/api/endpoints/following/requests/cancel.ts b/src/server/api/endpoints/following/requests/cancel.ts index 371f9f0ed38452290ab3ee99dc8ac3d853f57c9f..79cdb776f28c3a69768b5b67c73ec51990304434 100644 --- a/src/server/api/endpoints/following/requests/cancel.ts +++ b/src/server/api/endpoints/following/requests/cancel.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import cancelFollowRequest from '../../../../../services/following/requests/cancel'; -import { pack } from '../../../../../models/user'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; +import { Users } from '../../../../../models'; export const meta = { desc: { @@ -16,12 +16,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:following', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -58,5 +57,5 @@ export default define(meta, async (ps, user) => { throw e; } - return await pack(followee._id, user); + return await Users.pack(followee.id, user); }); diff --git a/src/server/api/endpoints/following/requests/list.ts b/src/server/api/endpoints/following/requests/list.ts index c9bcedf9292f2e5f39fe07ba86b0bcdd6454fc8c..13e4a39388ec3537d03105800ef906f099d800e7 100644 --- a/src/server/api/endpoints/following/requests/list.ts +++ b/src/server/api/endpoints/following/requests/list.ts @@ -1,5 +1,5 @@ -import FollowRequest, { pack } from '../../../../../models/follow-request'; import define from '../../../define'; +import { FollowRequests } from '../../../../../models'; export const meta = { desc: { @@ -11,13 +11,13 @@ export const meta = { requireCredential: true, - kind: 'following-read' + kind: 'read:following' }; export default define(meta, async (ps, user) => { - const reqs = await FollowRequest.find({ - followeeId: user._id + const reqs = await FollowRequests.find({ + followeeId: user.id }); - return await Promise.all(reqs.map(req => pack(req))); + return await Promise.all(reqs.map(req => FollowRequests.pack(req))); }); diff --git a/src/server/api/endpoints/following/requests/reject.ts b/src/server/api/endpoints/following/requests/reject.ts index 5e59d4bc97cc30694493646bf25c32b1c7642cbb..cccb60b24317d47d6823492f856468b437a42b13 100644 --- a/src/server/api/endpoints/following/requests/reject.ts +++ b/src/server/api/endpoints/following/requests/reject.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import rejectFollowRequest from '../../../../../services/following/requests/reject'; import define from '../../../define'; import { ApiError } from '../../../error'; @@ -15,12 +15,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:following', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' diff --git a/src/server/api/endpoints/games/reversi/games.ts b/src/server/api/endpoints/games/reversi/games.ts index e3c22c7611b4dbcf9bf5722947fbebddc765dbef..07736e04245d09c713ab322481d4d9a5d377fcb2 100644 --- a/src/server/api/endpoints/games/reversi/games.ts +++ b/src/server/api/endpoints/games/reversi/games.ts @@ -1,7 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import ReversiGame, { pack } from '../../../../../models/games/reversi/game'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; +import { ReversiGames } from '../../../../../models'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; +import { Brackets } from 'typeorm'; export const meta = { tags: ['games'], @@ -14,12 +16,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, my: { @@ -30,39 +30,20 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const q: any = ps.my ? { - isStarted: true, - $or: [{ - user1Id: user._id - }, { - user2Id: user._id - }] - } : { - isStarted: true - }; - - const sort = { - _id: -1 - }; - - if (ps.sinceId) { - sort._id = 1; - q._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - q._id = { - $lt: ps.untilId - }; + const query = makePaginationQuery(ReversiGames.createQueryBuilder('game'), ps.sinceId, ps.untilId) + .andWhere('game.isStarted = TRUE'); + + if (ps.my) { + query.andWhere(new Brackets(qb => { qb + .where('game.user1Id = :userId', { userId: user.id }) + .orWhere('game.user2Id = :userId', { userId: user.id }); + })); } // Fetch games - const games = await ReversiGame.find(q, { - sort: sort, - limit: ps.limit - }); + const games = await query.take(ps.limit).getMany(); - return await Promise.all(games.map((g) => pack(g, user, { + return await Promise.all(games.map((g) => ReversiGames.pack(g, user, { detail: false }))); }); diff --git a/src/server/api/endpoints/games/reversi/games/show.ts b/src/server/api/endpoints/games/reversi/games/show.ts index 766ca90119d58d0649d2568b140294fe23d58281..ea2776b16f5573a1fb30d3c5927a93714b8d608f 100644 --- a/src/server/api/endpoints/games/reversi/games/show.ts +++ b/src/server/api/endpoints/games/reversi/games/show.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../../misc/cafy-id'; -import ReversiGame, { pack } from '../../../../../../models/games/reversi/game'; +import { ID } from '../../../../../../misc/cafy-id'; import Reversi from '../../../../../../games/reversi/core'; import define from '../../../../define'; import { ApiError } from '../../../../error'; +import { ReversiGames } from '../../../../../../models'; export const meta = { tags: ['games'], @@ -11,7 +11,6 @@ export const meta = { params: { gameId: { validator: $.type(ID), - transform: transform, }, }, @@ -25,22 +24,23 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const game = await ReversiGame.findOne({ _id: ps.gameId }); + const game = await ReversiGames.findOne(ps.gameId); if (game == null) { throw new ApiError(meta.errors.noSuchGame); } - const o = new Reversi(game.settings.map, { - isLlotheo: game.settings.isLlotheo, - canPutEverywhere: game.settings.canPutEverywhere, - loopedBoard: game.settings.loopedBoard + const o = new Reversi(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard }); - for (const log of game.logs) + for (const log of game.logs) { o.put(log.color, log.pos); + } - const packed = await pack(game, user); + const packed = await ReversiGames.pack(game, user); return Object.assign({ board: o.board, diff --git a/src/server/api/endpoints/games/reversi/games/surrender.ts b/src/server/api/endpoints/games/reversi/games/surrender.ts index 446210894d093f4f60be8ffe609ffc02f9037ae8..56d66fb20535a37fc4e07db37d1cfa5295e40438 100644 --- a/src/server/api/endpoints/games/reversi/games/surrender.ts +++ b/src/server/api/endpoints/games/reversi/games/surrender.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../../misc/cafy-id'; -import ReversiGame, { pack } from '../../../../../../models/games/reversi/game'; +import { ID } from '../../../../../../misc/cafy-id'; import { publishReversiGameStream } from '../../../../../../services/stream'; import define from '../../../../define'; import { ApiError } from '../../../../error'; +import { ReversiGames } from '../../../../../../models'; export const meta = { tags: ['games'], @@ -17,7 +17,6 @@ export const meta = { params: { gameId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '投了ã—ãŸã„対局' } @@ -46,7 +45,7 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const game = await ReversiGame.findOne({ _id: ps.gameId }); + const game = await ReversiGames.findOne(ps.gameId); if (game == null) { throw new ApiError(meta.errors.noSuchGame); @@ -56,26 +55,20 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.alreadyEnded); } - if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) { + if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) { throw new ApiError(meta.errors.accessDenied); } - const winnerId = game.user1Id.equals(user._id) ? game.user2Id : game.user1Id; + const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id; - await ReversiGame.update({ - _id: game._id - }, { - $set: { - surrendered: user._id, - isEnded: true, - winnerId: winnerId - } + await ReversiGames.update(game.id, { + surrendered: user.id, + isEnded: true, + winnerId: winnerId }); - publishReversiGameStream(game._id, 'ended', { + publishReversiGameStream(game.id, 'ended', { winnerId: winnerId, - game: await pack(game._id, user) + game: await ReversiGames.pack(game.id, user) }); - - return; }); diff --git a/src/server/api/endpoints/games/reversi/invitations.ts b/src/server/api/endpoints/games/reversi/invitations.ts index c20477057878ab0eefa4e2610bbf348160461728..71f5aca1d1f337026032e72ea4d58f961a9002a3 100644 --- a/src/server/api/endpoints/games/reversi/invitations.ts +++ b/src/server/api/endpoints/games/reversi/invitations.ts @@ -1,5 +1,5 @@ -import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching'; import define from '../../../define'; +import { ReversiMatchings } from '../../../../../models'; export const meta = { tags: ['games'], @@ -9,13 +9,9 @@ export const meta = { export default define(meta, async (ps, user) => { // Find session - const invitations = await Matching.find({ - childId: user._id - }, { - sort: { - _id: -1 - } + const invitations = await ReversiMatchings.find({ + childId: user.id }); - return await Promise.all(invitations.map((i) => packMatching(i, user))); + return await Promise.all(invitations.map((i) => ReversiMatchings.pack(i, user))); }); diff --git a/src/server/api/endpoints/games/reversi/match.ts b/src/server/api/endpoints/games/reversi/match.ts index e66765944d30edfeb8c525cf66af94a756103423..e34d3c67f4e9344b847ffde88b21bdf9ad423f15 100644 --- a/src/server/api/endpoints/games/reversi/match.ts +++ b/src/server/api/endpoints/games/reversi/match.ts @@ -1,12 +1,14 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching'; -import ReversiGame, { pack as packGame } from '../../../../../models/games/reversi/game'; +import { ID } from '../../../../../misc/cafy-id'; import { publishMainStream, publishReversiStream } from '../../../../../services/stream'; import { eighteight } from '../../../../../games/reversi/maps'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; +import { genId } from '../../../../../misc/gen-id'; +import { ReversiMatchings, ReversiGames } from '../../../../../models'; +import { ReversiGame } from '../../../../../models/entities/games/reversi/game'; +import { ReversiMatching } from '../../../../../models/entities/games/reversi/matching'; export const meta = { tags: ['games'], @@ -16,7 +18,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -41,50 +42,47 @@ export const meta = { export default define(meta, async (ps, user) => { // Myself - if (ps.userId.equals(user._id)) { + if (ps.userId === user.id) { throw new ApiError(meta.errors.isYourself); } // Find session - const exist = await Matching.findOne({ + const exist = await ReversiMatchings.findOne({ parentId: ps.userId, - childId: user._id + childId: user.id }); if (exist) { // Destroy session - Matching.remove({ - _id: exist._id - }); + ReversiMatchings.delete(exist.id); // Create game - const game = await ReversiGame.insert({ + const game = await ReversiGames.save({ + id: genId(), createdAt: new Date(), user1Id: exist.parentId, - user2Id: user._id, + user2Id: user.id, user1Accepted: false, user2Accepted: false, isStarted: false, isEnded: false, logs: [], - settings: { - map: eighteight.data, - bw: 'random', - isLlotheo: false - } - }); + map: eighteight.data, + bw: 'random', + isLlotheo: false + } as ReversiGame); - publishReversiStream(exist.parentId, 'matched', await packGame(game, exist.parentId)); + publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, exist.parentId)); - const other = await Matching.count({ - childId: user._id + const other = await ReversiMatchings.count({ + childId: user.id }); if (other == 0) { - publishMainStream(user._id, 'reversiNoInvites'); + publishMainStream(user.id, 'reversiNoInvites'); } - return await packGame(game, user); + return await ReversiGames.pack(game, user); } else { // Fetch child const child = await getUser(ps.userId).catch(e => { @@ -93,21 +91,22 @@ export default define(meta, async (ps, user) => { }); // 以å‰ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã¯ã™ã¹ã¦å‰Šé™¤ã—ã¦ãŠã - await Matching.remove({ - parentId: user._id + await ReversiMatchings.delete({ + parentId: user.id }); // ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚’ä½œæˆ - const matching = await Matching.insert({ + const matching = await ReversiMatchings.save({ + id: genId(), createdAt: new Date(), - parentId: user._id, - childId: child._id - }); + parentId: user.id, + childId: child.id + } as ReversiMatching); - const packed = await packMatching(matching, child); - publishReversiStream(child._id, 'invited', packed); - publishMainStream(child._id, 'reversiInvited', packed); + const packed = await ReversiMatchings.pack(matching, child); + publishReversiStream(child.id, 'invited', packed); + publishMainStream(child.id, 'reversiInvited', packed); - return; + return null; } }); diff --git a/src/server/api/endpoints/games/reversi/match/cancel.ts b/src/server/api/endpoints/games/reversi/match/cancel.ts index fb230032d8a4d98e0401fd3d317886f4b5a2c72f..71aaae5ee1c52628c93aaec9bc8539aad41fb34c 100644 --- a/src/server/api/endpoints/games/reversi/match/cancel.ts +++ b/src/server/api/endpoints/games/reversi/match/cancel.ts @@ -1,5 +1,5 @@ -import Matching from '../../../../../../models/games/reversi/matching'; import define from '../../../../define'; +import { ReversiMatchings } from '../../../../../../models'; export const meta = { tags: ['games'], @@ -8,9 +8,7 @@ export const meta = { }; export default define(meta, async (ps, user) => { - await Matching.remove({ - parentId: user._id + await ReversiMatchings.delete({ + parentId: user.id }); - - return; }); diff --git a/src/server/api/endpoints/hashtags/list.ts b/src/server/api/endpoints/hashtags/list.ts index f454d47fedf71a45b0993f80f99bfdc666b5e1d2..7996c816694933b0f75fa39a6e80cf293bd5698c 100644 --- a/src/server/api/endpoints/hashtags/list.ts +++ b/src/server/api/endpoints/hashtags/list.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import define from '../../define'; -import Hashtag from '../../../../models/hashtag'; +import { Hashtags } from '../../../../models'; export const meta = { tags: ['hashtags'], @@ -54,40 +54,39 @@ export const meta = { }, }; -const sort: any = { - '+mentionedUsers': { mentionedUsersCount: -1 }, - '-mentionedUsers': { mentionedUsersCount: 1 }, - '+mentionedLocalUsers': { mentionedLocalUsersCount: -1 }, - '-mentionedLocalUsers': { mentionedLocalUsersCount: 1 }, - '+mentionedRemoteUsers': { mentionedRemoteUsersCount: -1 }, - '-mentionedRemoteUsers': { mentionedRemoteUsersCount: 1 }, - '+attachedUsers': { attachedUsersCount: -1 }, - '-attachedUsers': { attachedUsersCount: 1 }, - '+attachedLocalUsers': { attachedLocalUsersCount: -1 }, - '-attachedLocalUsers': { attachedLocalUsersCount: 1 }, - '+attachedRemoteUsers': { attachedRemoteUsersCount: -1 }, - '-attachedRemoteUsers': { attachedRemoteUsersCount: 1 }, -}; - export default define(meta, async (ps, me) => { - const q = {} as any; - if (ps.attachedToUserOnly) q.attachedUsersCount = { $ne: 0 }; - if (ps.attachedToLocalUserOnly) q.attachedLocalUsersCount = { $ne: 0 }; - if (ps.attachedToRemoteUserOnly) q.attachedRemoteUsersCount = { $ne: 0 }; - const tags = await Hashtag - .find(q, { - limit: ps.limit, - sort: sort[ps.sort], - fields: { - tag: true, - mentionedUsersCount: true, - mentionedLocalUsersCount: true, - mentionedRemoteUsersCount: true, - attachedUsersCount: true, - attachedLocalUsersCount: true, - attachedRemoteUsersCount: true - } - }); + const query = Hashtags.createQueryBuilder('tag'); + + if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0'); + if (ps.attachedToLocalUserOnly) query.andWhere('tag.attachedLocalUsersCount != 0'); + if (ps.attachedToRemoteUserOnly) query.andWhere('tag.attachedRemoteUsersCount != 0'); + + switch (ps.sort) { + case '+mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'DESC'); break; + case '-mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'ASC'); break; + case '+mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'DESC'); break; + case '-mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'ASC'); break; + case '+mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'DESC'); break; + case '-mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'ASC'); break; + case '+attachedUsers': query.orderBy('tag.attachedUsersCount', 'DESC'); break; + case '-attachedUsers': query.orderBy('tag.attachedUsersCount', 'ASC'); break; + case '+attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'DESC'); break; + case '-attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'ASC'); break; + case '+attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'DESC'); break; + case '-attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'ASC'); break; + } + + query.select([ + 'tag.name', + 'tag.mentionedUsersCount', + 'tag.mentionedLocalUsersCount', + 'tag.mentionedRemoteUsersCount', + 'tag.attachedUsersCount', + 'tag.attachedLocalUsersCount', + 'tag.attachedRemoteUsersCount', + ]); + + const tags = await query.take(ps.limit).getMany(); return tags; }); diff --git a/src/server/api/endpoints/hashtags/search.ts b/src/server/api/endpoints/hashtags/search.ts index 19b2975adff5d7fabd1b5dab55ff86c12da88685..fd91a2ebcc4cdd9e74fcf0feddfc423449c6cd79 100644 --- a/src/server/api/endpoints/hashtags/search.ts +++ b/src/server/api/endpoints/hashtags/search.ts @@ -1,7 +1,6 @@ import $ from 'cafy'; -import Hashtag from '../../../../models/hashtag'; import define from '../../define'; -import * as escapeRegexp from 'escape-regexp'; +import { Hashtags } from '../../../../models'; export const meta = { desc: { @@ -46,16 +45,12 @@ export const meta = { }; export default define(meta, async (ps) => { - const hashtags = await Hashtag - .find({ - tag: new RegExp('^' + escapeRegexp(ps.query.toLowerCase())) - }, { - sort: { - count: -1 - }, - limit: ps.limit, - skip: ps.offset - }); - - return hashtags.map(tag => tag.tag); + const hashtags = await Hashtags.createQueryBuilder('tag') + .where('tag.name like :q', { q: ps.query.toLowerCase() + '%' }) + .orderBy('tag.count', 'DESC') + .take(ps.limit) + .skip(ps.offset) + .getMany(); + + return hashtags.map(tag => tag.name); }); diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts index 8b8dd70245ab6c86f28af547e810e4b459e8befd..c750e72a1534fa8b0a52538aac13a36137f9c334 100644 --- a/src/server/api/endpoints/hashtags/trend.ts +++ b/src/server/api/endpoints/hashtags/trend.ts @@ -1,17 +1,19 @@ -import Note from '../../../../models/note'; -import { erase } from '../../../../prelude/array'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; +import { Notes } from '../../../../models'; +import { Note } from '../../../../models/entities/note'; /* トレンドã«è¼‰ã‚‹ãŸã‚ã«ã¯ã€Œã€Žç›´è¿‘a分間ã®ãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ãŒä»Šã‹ã‚‰a分å‰ï½žä»Šã‹ã‚‰b分å‰ã®é–“ã®ãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ã®nå€ä»¥ä¸Šã€ã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ä¸Šä½5ä½ä»¥å†…ã«å…¥ã‚‹ã€ã“ã¨ãŒå¿…è¦ ãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ã¨ã¯ãã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã¨æŠ•ç¨¿ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒšã‚¢ã®ã‚«ã‚¦ãƒ³ãƒˆã§ã€ä¾‹ãˆã°åŒã˜ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒè¤‡æ•°å›žåŒã˜ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’投稿ã—ã¦ã‚‚ãã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ã¯1ã¨ã‚«ã‚¦ãƒ³ãƒˆã•ã‚Œã‚‹ + +..ãŒç†æƒ³ã ã‘ã©PostgreSQLã§ã©ã†ã™ã‚‹ã®ã‹åˆ†ã‹ã‚‰ãªã„ã®ã§å˜ã«ã€Œç›´è¿‘Aã®å†…ã«æŠ•ç¨¿ã•ã‚ŒãŸãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ãŒå¤šã„ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€ã§å¦¥å”ã™ã‚‹ */ const rangeA = 1000 * 60 * 30; // 30分 -const rangeB = 1000 * 60 * 120; // 2時間 -const coefficient = 1.25; // 「nå€ã€ã®éƒ¨åˆ† -const requiredUsers = 3; // 最低何人ãŒãã®ã‚¿ã‚°ã‚’投稿ã—ã¦ã„ã‚‹å¿…è¦ãŒã‚ã‚‹ã‹ +//const rangeB = 1000 * 60 * 120; // 2時間 +//const coefficient = 1.25; // 「nå€ã€ã®éƒ¨åˆ† +//const requiredUsers = 3; // 最低何人ãŒãã®ã‚¿ã‚°ã‚’投稿ã—ã¦ã„ã‚‹å¿…è¦ãŒã‚ã‚‹ã‹ const max = 5; @@ -23,92 +25,47 @@ export const meta = { export default define(meta, async () => { const instance = await fetchMeta(); - const hidedTags = instance.hidedTags.map(t => t.toLowerCase()); - - //#region 1. ç›´è¿‘Aã®å†…ã«æŠ•ç¨¿ã•ã‚ŒãŸãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°(ã¨ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒšã‚¢)を集計 - const data = await Note.aggregate([{ - $match: { - createdAt: { - $gt: new Date(Date.now() - rangeA) - }, - tagsLower: { - $exists: true, - $ne: [] - } - } - }, { - $unwind: '$tagsLower' - }, { - $group: { - _id: { tag: '$tagsLower', userId: '$userId' } - } - }]) as { - _id: { - tag: string; - userId: any; - } - }[]; - //#endregion + const hiddenTags = instance.hiddenTags.map(t => t.toLowerCase()); + + const tagNotes = await Notes.createQueryBuilder('note') + .where(`note.createdAt > :date`, { date: new Date(Date.now() - rangeA) }) + .andWhere(`note.tags != '{}'`) + .select(['note.tags', 'note.userId']) + .getMany(); - if (data.length == 0) { + if (tagNotes.length === 0) { return []; } const tags: { name: string; - count: number; + users: Note['userId'][]; }[] = []; - // カウント - for (const x of data.map(x => x._id).filter(x => !hidedTags.includes(x.tag))) { - const i = tags.findIndex(tag => tag.name == x.tag); - if (i != -1) { - tags[i].count++; - } else { - tags.push({ - name: x.tag, - count: 1 - }); - } - } - - // 最低è¦æ±‚投稿者数を下回るãªã‚‰ã‚«ãƒƒãƒˆã™ã‚‹ - const limitedTags = tags.filter(tag => tag.count >= requiredUsers); - - //#region 2. 1ã§å–å¾—ã—ãŸãã‚Œãžã‚Œã®ã‚¿ã‚°ã«ã¤ã„ã¦ã€ã€Œç›´è¿‘a分間ã®ãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ãŒä»Šã‹ã‚‰a分å‰ï½žä»Šã‹ã‚‰b分å‰ã®é–“ã®ãƒ¦ãƒ‹ãƒ¼ã‚¯æŠ•ç¨¿æ•°ã®nå€ä»¥ä¸Šã€ã‹ã©ã†ã‹ã‚’判定ã™ã‚‹ - const hotsPromises = limitedTags.map(async tag => { - const passedCount = (await Note.distinct('userId', { - tagsLower: tag.name, - createdAt: { - $lt: new Date(Date.now() - rangeA), - $gt: new Date(Date.now() - rangeB) + for (const note of tagNotes) { + for (const tag of note.tags) { + if (hiddenTags.includes(tag)) continue; + + const x = tags.find(x => x.name === tag); + if (x) { + if (!x.users.includes(note.userId)) { + x.users.push(note.userId); + } + } else { + tags.push({ + name: tag, + users: [note.userId] + }); } - }) as any).length; - - if (tag.count >= (passedCount * coefficient)) { - return tag; - } else { - return null; } - }); - //#endregion + } // ã‚¿ã‚°ã‚’äººæ°—é †ã«ä¸¦ã¹æ›¿ãˆ - let hots = erase(null, await Promise.all(hotsPromises)) - .sort((a, b) => b.count - a.count) + const hots = tags + .sort((a, b) => b.users.length - a.users.length) .map(tag => tag.name) .slice(0, max); - //#region 3. ã‚‚ã—上記ã®æ–¹æ³•ã§ã®ãƒˆãƒ¬ãƒ³ãƒ‰æŠ½å‡ºã®çµæžœã€æ±‚ã‚られã¦ã„ã‚‹ã‚¿ã‚°æ•°ã«é”ã—ãªã‘ã‚Œã°ã€ŒãŸã å˜ã«ç¾åœ¨æŠ•ç¨¿æ•°ãŒå¤šã„ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€ã«åˆ‡ã‚Šæ›¿ãˆã‚‹ - if (hots.length < max) { - hots = hots.concat(tags - .filter(tag => hots.indexOf(tag.name) == -1) - .sort((a, b) => b.count - a.count) - .map(tag => tag.name) - .slice(0, max - hots.length)); - } - //#endregion - //#region 2(ã¾ãŸã¯3)ã§è©±é¡Œã¨åˆ¤å®šã•ã‚ŒãŸã‚¿ã‚°ãã‚Œãžã‚Œã«ã¤ã„ã¦éŽåŽ»ã®æŠ•ç¨¿æ•°ã‚°ãƒ©ãƒ•ã‚’å–å¾—ã™ã‚‹ const countPromises: Promise<any[]>[] = []; @@ -118,23 +75,25 @@ export default define(meta, async () => { const interval = 1000 * 60 * 10; for (let i = 0; i < range; i++) { - countPromises.push(Promise.all(hots.map(tag => Note.distinct('userId', { - tagsLower: tag, - createdAt: { - $lt: new Date(Date.now() - (interval * i)), - $gt: new Date(Date.now() - (interval * (i + 1))) - } - })))); + countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note') + .select('count(distinct note.userId)') + .where(':tag = ANY(note.tags)', { tag: tag }) + .andWhere('note.createdAt < :lt', { lt: new Date(Date.now() - (interval * i)) }) + .andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * (i + 1))) }) + .getRawOne() + .then(x => parseInt(x.count, 10)) + ))); } const countsLog = await Promise.all(countPromises); - const totalCounts: any = await Promise.all(hots.map(tag => Note.distinct('userId', { - tagsLower: tag, - createdAt: { - $gt: new Date(Date.now() - (interval * range)) - } - }))); + const totalCounts: any = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note') + .select('count(distinct note.userId)') + .where(':tag = ANY(note.tags)', { tag: tag }) + .andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * range)) }) + .getRawOne() + .then(x => parseInt(x.count, 10)) + )); //#endregion const stats = hots.map((tag, i) => ({ diff --git a/src/server/api/endpoints/hashtags/users.ts b/src/server/api/endpoints/hashtags/users.ts index 4b047aee95891e06c2bf24bc3874057c849251be..20cba96d0e1fa2c78656c1b20ad1699f7cfeaced 100644 --- a/src/server/api/endpoints/hashtags/users.ts +++ b/src/server/api/endpoints/hashtags/users.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; -import User, { pack } from '../../../../models/user'; import define from '../../define'; +import { Users } from '../../../../models'; export const meta = { requireCredential: false, @@ -54,39 +54,32 @@ export const meta = { }, }; -const sort: any = { - '+follower': { followersCount: -1 }, - '-follower': { followersCount: 1 }, - '+createdAt': { createdAt: -1 }, - '-createdAt': { createdAt: 1 }, - '+updatedAt': { updatedAt: -1 }, - '-updatedAt': { updatedAt: 1 }, -}; - export default define(meta, async (ps, me) => { - const q = { - tags: ps.tag, - $and: [] - } as any; + const query = Users.createQueryBuilder('user') + .where(':tag = ANY(user.tags)', { tag: ps.tag }); + + const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)); + + if (ps.state === 'alive') { + query.andWhere('user.updatedAt > :date', { date: recent }); + } - // state - q.$and.push( - ps.state == 'alive' ? { updatedAt: { $gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)) } } : - {} - ); + if (ps.origin === 'local') { + query.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + query.andWhere('user.host IS NOT NULL'); + } - // origin - q.$and.push( - ps.origin == 'local' ? { host: null } : - ps.origin == 'remote' ? { host: { $ne: null } } : - {} - ); + switch (ps.sort) { + case '+follower': query.orderBy('user.followersCount', 'DESC'); break; + case '-follower': query.orderBy('user.followersCount', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; + case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break; + case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; + } - const users = await User - .find(q, { - limit: ps.limit, - sort: sort[ps.sort], - }); + const users = await query.take(ps.limit).getMany(); - return await Promise.all(users.map(user => pack(user, me, { detail: true }))); + return await Users.packMany(users, me, { detail: true }); }); diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts index 7b50cc76c2600bd4de9ce447ec0ec1d261b5d0a6..afad38c4694e3d0560ae8a764961002dac026d4a 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -1,5 +1,5 @@ -import { pack } from '../../../models/user'; import define from '../define'; +import { Users } from '../../../models'; export const meta = { stability: 'stable', @@ -22,7 +22,7 @@ export const meta = { export default define(meta, async (ps, user, app) => { const isSecure = user != null && app == null; - return await pack(user, user, { + return await Users.pack(user, user, { detail: true, includeHasUnreadNotes: true, includeSecrets: isSecure diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts index 556354c386f331fcd4b203275f0e70750f046870..8ccb09b8b7c20a07e92676b9fa4ccbd261d5aa6a 100644 --- a/src/server/api/endpoints/i/2fa/done.ts +++ b/src/server/api/endpoints/i/2fa/done.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import * as speakeasy from 'speakeasy'; -import User from '../../../../../models/user'; import define from '../../../define'; +import { Users } from '../../../../../models'; export const meta = { requireCredential: true, @@ -32,12 +32,8 @@ export default define(meta, async (ps, user) => { throw new Error('not verified'); } - await User.update(user._id, { - $set: { - 'twoFactorSecret': user.twoFactorTempSecret, - 'twoFactorEnabled': true - } + await Users.update(user.id, { + twoFactorSecret: user.twoFactorTempSecret, + twoFactorEnabled: true }); - - return; }); diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts index 302b51ec0be7b6fdee364a9f8119a13734b126cf..5efe77900af22b84e890da706e37e063bcaf850b 100644 --- a/src/server/api/endpoints/i/2fa/register.ts +++ b/src/server/api/endpoints/i/2fa/register.ts @@ -2,9 +2,9 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import * as QRCode from 'qrcode'; -import User from '../../../../../models/user'; import config from '../../../../../config'; import define from '../../../define'; +import { Users } from '../../../../../models'; export const meta = { requireCredential: true, @@ -31,10 +31,8 @@ export default define(meta, async (ps, user) => { length: 32 }); - await User.update(user._id, { - $set: { - twoFactorTempSecret: secret.base32 - } + await Users.update(user.id, { + twoFactorTempSecret: secret.base32 }); // Get the data URL of the authenticator URL diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts index 37b26391985ff333fd0f54527994884fd519660c..fb3ecd40435dc48eca38961454a65e1b4a87486d 100644 --- a/src/server/api/endpoints/i/2fa/unregister.ts +++ b/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../../../../models/user'; import define from '../../../define'; +import { Users } from '../../../../../models'; export const meta = { requireCredential: true, @@ -23,11 +23,9 @@ export default define(meta, async (ps, user) => { throw new Error('incorrect password'); } - await User.update(user._id, { - $set: { - 'twoFactorSecret': null, - 'twoFactorEnabled': false - } + await Users.update(user.id, { + twoFactorSecret: null, + twoFactorEnabled: false }); return; diff --git a/src/server/api/endpoints/i/authorized-apps.ts b/src/server/api/endpoints/i/authorized-apps.ts index cb8be9ed97c0538ebc62d0911d0f214cbc39d670..ebf04fcb5872fc55ae726adffa7cc1ef39fe0a40 100644 --- a/src/server/api/endpoints/i/authorized-apps.ts +++ b/src/server/api/endpoints/i/authorized-apps.ts @@ -1,7 +1,6 @@ import $ from 'cafy'; -import AccessToken from '../../../../models/access-token'; -import { pack } from '../../../../models/app'; import define from '../../define'; +import { AccessTokens, Apps } from '../../../../models'; export const meta = { requireCredential: true, @@ -28,18 +27,18 @@ export const meta = { export default define(meta, async (ps, user) => { // Get tokens - const tokens = await AccessToken - .find({ - userId: user._id - }, { - limit: ps.limit, - skip: ps.offset, - sort: { - _id: ps.sort == 'asc' ? 1 : -1 - } - }); + const tokens = await AccessTokens.find({ + where: { + userId: user.id + }, + take: ps.limit, + skip: ps.offset, + order: { + id: ps.sort == 'asc' ? 1 : -1 + } + }); - return await Promise.all(tokens.map(token => pack(token.appId, user, { + return await Promise.all(tokens.map(token => Apps.pack(token.appId, user, { detail: true }))); }); diff --git a/src/server/api/endpoints/i/change-password.ts b/src/server/api/endpoints/i/change-password.ts index 8ab286b4bffeebc49fb862512c682fe50e8103c6..f8f977200f512da8e8a12d58bf5ee6c3fd9fff2f 100644 --- a/src/server/api/endpoints/i/change-password.ts +++ b/src/server/api/endpoints/i/change-password.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../../../models/user'; import define from '../../define'; +import { Users } from '../../../../models'; export const meta = { requireCredential: true, @@ -31,11 +31,7 @@ export default define(meta, async (ps, user) => { const salt = await bcrypt.genSalt(8); const hash = await bcrypt.hash(ps.newPassword, salt); - await User.update(user._id, { - $set: { - 'password': hash - } + await Users.update(user.id, { + password: hash }); - - return; }); diff --git a/src/server/api/endpoints/i/clear-follow-request-notification.ts b/src/server/api/endpoints/i/clear-follow-request-notification.ts deleted file mode 100644 index 38c6ec1cefe4cbc81b6893fde0c4470b77c67a59..0000000000000000000000000000000000000000 --- a/src/server/api/endpoints/i/clear-follow-request-notification.ts +++ /dev/null @@ -1,23 +0,0 @@ -import User from '../../../../models/user'; -import define from '../../define'; - -export const meta = { - tags: ['account', 'following'], - - requireCredential: true, - - kind: 'account-write', - - params: { - } -}; - -export default define(meta, async (ps, user) => { - await User.update({ _id: user._id }, { - $set: { - pendingReceivedFollowRequestsCount: 0 - } - }); - - return; -}); diff --git a/src/server/api/endpoints/i/delete-account.ts b/src/server/api/endpoints/i/delete-account.ts index fed38eab5a266585a175a0e05f6e76dfd939e2fc..5aff74e0cc631bec232a30546b2e3aac6b81d9b6 100644 --- a/src/server/api/endpoints/i/delete-account.ts +++ b/src/server/api/endpoints/i/delete-account.ts @@ -1,10 +1,7 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../../../models/user'; import define from '../../define'; -import { createDeleteNotesJob, createDeleteDriveFilesJob } from '../../../../queue'; -import Message from '../../../../models/messaging-message'; -import Signin from '../../../../models/signin'; +import { Users } from '../../../../models'; export const meta = { requireCredential: true, @@ -26,27 +23,5 @@ export default define(meta, async (ps, user) => { throw new Error('incorrect password'); } - await User.update({ _id: user._id }, { - $set: { - isDeleted: true, - name: null, - description: null, - pinnedNoteIds: [], - password: null, - email: null, - twitter: null, - github: null, - discord: null, - profile: {}, - fields: [], - clientSettings: {}, - } - }); - - Message.remove({ userId: user._id }); - Signin.remove({ userId: user._id }); - createDeleteNotesJob(user); - createDeleteDriveFilesJob(user); - - return; + await Users.delete(user.id); }); diff --git a/src/server/api/endpoints/i/export-blocking.ts b/src/server/api/endpoints/i/export-blocking.ts index 346b29c79dc16ee05ec732ee9bda08aab649e5a2..14d49487e8417f753ec3f166e6c54d2e039a4f3c 100644 --- a/src/server/api/endpoints/i/export-blocking.ts +++ b/src/server/api/endpoints/i/export-blocking.ts @@ -13,6 +13,4 @@ export const meta = { export default define(meta, async (ps, user) => { createExportBlockingJob(user); - - return; }); diff --git a/src/server/api/endpoints/i/export-following.ts b/src/server/api/endpoints/i/export-following.ts index 5977b0310513c51dec2eb67a4b88b5b640f0080a..50dd28837fcb48faa5ed449777604c4c7138cd4d 100644 --- a/src/server/api/endpoints/i/export-following.ts +++ b/src/server/api/endpoints/i/export-following.ts @@ -13,6 +13,4 @@ export const meta = { export default define(meta, async (ps, user) => { createExportFollowingJob(user); - - return; }); diff --git a/src/server/api/endpoints/i/export-mute.ts b/src/server/api/endpoints/i/export-mute.ts index 22ceff363111111fc61c2e0a10006eedbc9b9eb6..1eb51cd77ecc8bca3a77f7fbdd8e022a8a047a01 100644 --- a/src/server/api/endpoints/i/export-mute.ts +++ b/src/server/api/endpoints/i/export-mute.ts @@ -13,6 +13,4 @@ export const meta = { export default define(meta, async (ps, user) => { createExportMuteJob(user); - - return; }); diff --git a/src/server/api/endpoints/i/export-notes.ts b/src/server/api/endpoints/i/export-notes.ts index 2881aa26976f375403d67923aa9b5d50769c8bdd..dd32c18d11cab4f2d8197f6a253a551afc67515a 100644 --- a/src/server/api/endpoints/i/export-notes.ts +++ b/src/server/api/endpoints/i/export-notes.ts @@ -13,6 +13,4 @@ export const meta = { export default define(meta, async (ps, user) => { createExportNotesJob(user); - - return; }); diff --git a/src/server/api/endpoints/i/export-user-lists.ts b/src/server/api/endpoints/i/export-user-lists.ts index 9d7424ad89e2737677787aef86dbefcf22af3163..7650ca721081ec738a57c290b820b556df2b9c2c 100644 --- a/src/server/api/endpoints/i/export-user-lists.ts +++ b/src/server/api/endpoints/i/export-user-lists.ts @@ -13,6 +13,4 @@ export const meta = { export default define(meta, async (ps, user) => { createExportUserListsJob(user); - - return; }); diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts index 7ea6f7b966a1f54a0890ef3ff8378163e5a3d0f8..d2d149b2d1983b3294e2d5a15d1ffc042b64e95f 100644 --- a/src/server/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Favorite, { packMany } from '../../../../models/favorite'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { NoteFavorites } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -23,42 +24,22 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, - } + }, } }; export default define(meta, async (ps, user) => { - const query = { - userId: user._id - } as any; - - const sort = { - _id: -1 - }; - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } + const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId) + .andWhere(`favorite.userId = :meId`, { meId: user.id }) + .leftJoinAndSelect('favorite.note', 'note'); - // Get favorites - const favorites = await Favorite - .find(query, { - limit: ps.limit, - sort: sort - }); + const favorites = await query + .take(ps.limit) + .getMany(); - return await packMany(favorites, user); + return await NoteFavorites.packMany(favorites, user); }); diff --git a/src/server/api/endpoints/i/import-following.ts b/src/server/api/endpoints/i/import-following.ts index f188291bc2d14671de1c986dd721a75775ec6869..deafec18ecd9a549fef23491219bae1f948a8494 100644 --- a/src/server/api/endpoints/i/import-following.ts +++ b/src/server/api/endpoints/i/import-following.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { createImportFollowingJob } from '../../../../queue'; import ms = require('ms'); -import DriveFile from '../../../../models/drive-file'; import { ApiError } from '../../error'; +import { DriveFiles } from '../../../../models'; export const meta = { secure: true, @@ -17,7 +17,6 @@ export const meta = { params: { fileId: { validator: $.type(ID), - transform: transform, } }, @@ -49,16 +48,12 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const file = await DriveFile.findOne({ - _id: ps.fileId - }); + const file = await DriveFiles.findOne(ps.fileId); if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.length > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.length === 0) throw new ApiError(meta.errors.emptyFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); - createImportFollowingJob(user, file._id); - - return; + createImportFollowingJob(user, file.id); }); diff --git a/src/server/api/endpoints/i/import-user-lists.ts b/src/server/api/endpoints/i/import-user-lists.ts index ed3085e5f8b2e38d95d7ef50b393392954590d7d..b7d9d029b7ab6d2f0df94e62d67c40040c1ef602 100644 --- a/src/server/api/endpoints/i/import-user-lists.ts +++ b/src/server/api/endpoints/i/import-user-lists.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { createImportUserListsJob } from '../../../../queue'; import ms = require('ms'); -import DriveFile from '../../../../models/drive-file'; import { ApiError } from '../../error'; +import { DriveFiles } from '../../../../models'; export const meta = { secure: true, @@ -17,7 +17,6 @@ export const meta = { params: { fileId: { validator: $.type(ID), - transform: transform, } }, @@ -49,16 +48,12 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const file = await DriveFile.findOne({ - _id: ps.fileId - }); + const file = await DriveFiles.findOne(ps.fileId); if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.length > 30000) throw new ApiError(meta.errors.tooBigFile); - if (file.length === 0) throw new ApiError(meta.errors.emptyFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); - createImportUserListsJob(user, file._id); - - return; + createImportUserListsJob(user, file.id); }); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index d3e3064abd3ae5f81b8f580588c07cb20ae47e71..9b016e0a2daf65ac67e69a3131beda03809c6fef 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -1,11 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Notification from '../../../../models/notification'; -import { packMany } from '../../../../models/notification'; -import { getFriendIds } from '../../common/get-friends'; -import read from '../../common/read-notification'; +import { ID } from '../../../../misc/cafy-id'; +import { readNotification } from '../../common/read-notification'; import define from '../../define'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Notifications, Followings, Mutings } from '../../../../models'; export const meta = { desc: { @@ -17,7 +15,7 @@ export const meta = { requireCredential: true, - kind: 'account-read', + kind: 'read:notifications', params: { limit: { @@ -27,12 +25,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, following: { @@ -46,12 +42,12 @@ export const meta = { }, includeTypes: { - validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote', 'receiveFollowRequest'])), + validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])), default: [] as string[] }, excludeTypes: { - validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote', 'receiveFollowRequest'])), + validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])), default: [] as string[] } }, @@ -65,63 +61,38 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const hideUserIds = await getHideUserIds(user); + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: user.id }); - const query = { - notifieeId: user._id, - $and: [{ - notifierId: { - $nin: hideUserIds - } - }] - } as any; + const mutingQuery = Mutings.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: user.id }); - const sort = { - _id: -1 - }; + const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId) + .andWhere(`notification.notifieeId = :meId`, { meId: user.id }) + .leftJoinAndSelect('notification.notifier', 'notifier'); - if (ps.following) { - // ID list of the user itself and other users who the user follows - const followingIds = await getFriendIds(user._id); - - query.$and.push({ - notifierId: { - $in: followingIds - } - }); - } + query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`); + query.setParameters(mutingQuery.getParameters()); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; + if (ps.following) { + query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id }); + query.setParameters(followingQuery.getParameters()); } if (ps.includeTypes.length > 0) { - query.type = { - $in: ps.includeTypes - }; + query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes }); } else if (ps.excludeTypes.length > 0) { - query.type = { - $nin: ps.excludeTypes - }; + query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes }); } - const notifications = await Notification - .find(query, { - limit: ps.limit, - sort: sort - }); + const notifications = await query.take(ps.limit).getMany(); // Mark all as read if (notifications.length > 0 && ps.markAsRead) { - read(user._id, notifications); + readNotification(user.id, notifications.map(x => x.id)); } - return await packMany(notifications); + return await Notifications.packMany(notifications); }); diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts index 8d853d45c879a06eef5d14dab43cf0086839f3a4..ac104b19f9b945a7b7feb86306bd0a843b8be281 100644 --- a/src/server/api/endpoints/i/pin.ts +++ b/src/server/api/endpoints/i/pin.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import { pack } from '../../../../models/user'; +import { ID } from '../../../../misc/cafy-id'; import { addPinned } from '../../../../services/i/pin'; import define from '../../define'; import { ApiError } from '../../error'; +import { Users } from '../../../../models'; export const meta = { stability: 'stable', @@ -16,12 +16,11 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID' @@ -58,7 +57,7 @@ export default define(meta, async (ps, user) => { throw e; }); - return await pack(user, user, { + return await Users.pack(user, user, { detail: true }); }); diff --git a/src/server/api/endpoints/i/read-all-messaging-messages.ts b/src/server/api/endpoints/i/read-all-messaging-messages.ts index bbbfa0d7b3d4fc3126f86a99cc9005c129b3e199..e8ada277e976b8fa477450957abe9ae2fa9a0447 100644 --- a/src/server/api/endpoints/i/read-all-messaging-messages.ts +++ b/src/server/api/endpoints/i/read-all-messaging-messages.ts @@ -1,7 +1,6 @@ -import User from '../../../../models/user'; import { publishMainStream } from '../../../../services/stream'; -import Message from '../../../../models/messaging-message'; import define from '../../define'; +import { MessagingMessages } from '../../../../models'; export const meta = { desc: { @@ -13,7 +12,7 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { } @@ -21,24 +20,12 @@ export const meta = { export default define(meta, async (ps, user) => { // Update documents - await Message.update({ - recipientId: user._id, + await MessagingMessages.update({ + recipientId: user.id, isRead: false }, { - $set: { - isRead: true - } - }, { - multi: true - }); - - User.update({ _id: user._id }, { - $set: { - hasUnreadMessagingMessage: false - } + isRead: true }); - publishMainStream(user._id, 'readAllMessagingMessages'); - - return; + publishMainStream(user.id, 'readAllMessagingMessages'); }); diff --git a/src/server/api/endpoints/i/read-all-unread-notes.ts b/src/server/api/endpoints/i/read-all-unread-notes.ts index 742c2d99087c4d740be21255da3829fa5f6a9923..cc8ebf58ec5b3a245e0dfb85b27f9baf9f2a97af 100644 --- a/src/server/api/endpoints/i/read-all-unread-notes.ts +++ b/src/server/api/endpoints/i/read-all-unread-notes.ts @@ -1,7 +1,6 @@ -import User from '../../../../models/user'; import { publishMainStream } from '../../../../services/stream'; -import NoteUnread from '../../../../models/note-unread'; import define from '../../define'; +import { NoteUnreads } from '../../../../models'; export const meta = { desc: { @@ -13,7 +12,7 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { } @@ -21,20 +20,11 @@ export const meta = { export default define(meta, async (ps, user) => { // Remove documents - await NoteUnread.remove({ - userId: user._id - }); - - User.update({ _id: user._id }, { - $set: { - hasUnreadMentions: false, - hasUnreadSpecifiedNotes: false - } + await NoteUnreads.delete({ + userId: user.id }); // å…¨ã¦æ—¢èªã«ãªã£ãŸã‚¤ãƒ™ãƒ³ãƒˆã‚’発行 - publishMainStream(user._id, 'readAllUnreadMentions'); - publishMainStream(user._id, 'readAllUnreadSpecifiedNotes'); - - return; + publishMainStream(user.id, 'readAllUnreadMentions'); + publishMainStream(user.id, 'readAllUnreadSpecifiedNotes'); }); diff --git a/src/server/api/endpoints/i/regenerate-token.ts b/src/server/api/endpoints/i/regenerate-token.ts index ad10b99b369bcd47ac3df5ecc3385493042a97dd..729c1a300a4b3ee675131cc4f5ad0b4c6c37af24 100644 --- a/src/server/api/endpoints/i/regenerate-token.ts +++ b/src/server/api/endpoints/i/regenerate-token.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../../../models/user'; import { publishMainStream } from '../../../../services/stream'; import generateUserToken from '../../common/generate-native-user-token'; import define from '../../define'; +import { Users } from '../../../../models'; export const meta = { requireCredential: true, @@ -28,14 +28,10 @@ export default define(meta, async (ps, user) => { // Generate secret const secret = generateUserToken(); - await User.update(user._id, { - $set: { - 'token': secret - } + await Users.update(user.id, { + token: secret }); // Publish event - publishMainStream(user._id, 'myTokenRegenerated'); - - return; + publishMainStream(user.id, 'myTokenRegenerated'); }); diff --git a/src/server/api/endpoints/i/signin-history.ts b/src/server/api/endpoints/i/signin-history.ts index 87160a9f912b3c2b6f764511e24de526262a6ca9..e9ae19d734901762e1b3e030fee87d7c06373933 100644 --- a/src/server/api/endpoints/i/signin-history.ts +++ b/src/server/api/endpoints/i/signin-history.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Signin, { pack } from '../../../../models/signin'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { Signins } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { requireCredential: true, @@ -16,41 +17,19 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, } } }; export default define(meta, async (ps, user) => { - const query = { - userId: user._id - } as any; - - const sort = { - _id: -1 - }; - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } + const query = makePaginationQuery(Signins.createQueryBuilder('signin'), ps.sinceId, ps.untilId) + .andWhere(`signin.userId = :meId`, { meId: user.id }); - const history = await Signin - .find(query, { - limit: ps.limit, - sort: sort - }); + const history = await query.take(ps.limit).getMany(); - return await Promise.all(history.map(record => pack(record))); + return await Promise.all(history.map(record => Signins.pack(record))); }); diff --git a/src/server/api/endpoints/i/unpin.ts b/src/server/api/endpoints/i/unpin.ts index 184d46f2c361f5891f9ff8fce3cbbf33ec982f4d..4688533578d5a9613bdb2dce30c71a01143fda61 100644 --- a/src/server/api/endpoints/i/unpin.ts +++ b/src/server/api/endpoints/i/unpin.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import { pack } from '../../../../models/user'; +import { ID } from '../../../../misc/cafy-id'; import { removePinned } from '../../../../services/i/pin'; import define from '../../define'; import { ApiError } from '../../error'; +import { Users } from '../../../../models'; export const meta = { stability: 'stable', @@ -16,12 +16,11 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID' @@ -44,7 +43,7 @@ export default define(meta, async (ps, user) => { throw e; }); - return await pack(user, user, { + return await Users.pack(user, user, { detail: true }); }); diff --git a/src/server/api/endpoints/i/update-client-setting.ts b/src/server/api/endpoints/i/update-client-setting.ts index 79cd04e169840f87a6df46685256fe00cda0e20d..edbfe28f35c416f504473f45354f415ae75a2052 100644 --- a/src/server/api/endpoints/i/update-client-setting.ts +++ b/src/server/api/endpoints/i/update-client-setting.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import User from '../../../../models/user'; import { publishMainStream } from '../../../../services/stream'; import define from '../../define'; +import { Users } from '../../../../models'; export const meta = { requireCredential: true, @@ -10,7 +10,7 @@ export const meta = { params: { name: { - validator: $.str + validator: $.str.match(/^[a-zA-Z]+$/) }, value: { @@ -20,18 +20,18 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const x: any = {}; - x[`clientSettings.${ps.name}`] = ps.value; - - await User.update(user._id, { - $set: x - }); + await Users.createQueryBuilder().update() + .set({ + clientData: { + [ps.name]: ps.value + }, + }) + .where('id = :id', { id: user.id }) + .execute(); // Publish event - publishMainStream(user._id, 'clientSettingUpdated', { + publishMainStream(user.id, 'clientSettingUpdated', { key: ps.name, value: ps.value }); - - return; }); diff --git a/src/server/api/endpoints/i/update-email.ts b/src/server/api/endpoints/i/update-email.ts index c90462d85041495b068bf3dd99e9c76df87dff50..253017535ff9a5e9cd643d904cad42064861978f 100644 --- a/src/server/api/endpoints/i/update-email.ts +++ b/src/server/api/endpoints/i/update-email.ts @@ -1,5 +1,4 @@ import $ from 'cafy'; -import User, { pack } from '../../../../models/user'; import { publishMainStream } from '../../../../services/stream'; import define from '../../define'; import * as nodemailer from 'nodemailer'; @@ -9,6 +8,7 @@ import config from '../../../../config'; import * as ms from 'ms'; import * as bcrypt from 'bcryptjs'; import { apiLogger } from '../../logger'; +import { Users } from '../../../../models'; export const meta = { requireCredential: true, @@ -39,29 +39,25 @@ export default define(meta, async (ps, user) => { throw new Error('incorrect password'); } - await User.update(user._id, { - $set: { - email: ps.email, - emailVerified: false, - emailVerifyCode: null - } + await Users.update(user.id, { + email: ps.email, + emailVerified: false, + emailVerifyCode: null }); - const iObj = await pack(user._id, user, { + const iObj = await Users.pack(user.id, user, { detail: true, includeSecrets: true }); // Publish meUpdated event - publishMainStream(user._id, 'meUpdated', iObj); + publishMainStream(user.id, 'meUpdated', iObj); if (ps.email != null) { const code = rndstr('a-z0-9', 16); - await User.update(user._id, { - $set: { - emailVerifyCode: code - } + await Users.update(user.id, { + emailVerifyCode: code }); const meta = await fetchMeta(); @@ -84,7 +80,7 @@ export default define(meta, async (ps, user) => { transporter.sendMail({ from: meta.email, to: ps.email, - subject: meta.name, + subject: meta.name || 'Misskey', text: `To verify email, please click this link: ${link}` }, (error, info) => { if (error) { diff --git a/src/server/api/endpoints/i/update-home.ts b/src/server/api/endpoints/i/update-home.ts deleted file mode 100644 index e2c319887f628d436c17232fed1b477e6bbf0e43..0000000000000000000000000000000000000000 --- a/src/server/api/endpoints/i/update-home.ts +++ /dev/null @@ -1,33 +0,0 @@ -import $ from 'cafy'; -import User from '../../../../models/user'; -import { publishMainStream } from '../../../../services/stream'; -import define from '../../define'; - -export const meta = { - requireCredential: true, - - secure: true, - - params: { - home: { - validator: $.arr($.obj({ - name: $.str, - id: $.str, - place: $.str, - data: $.obj() - }).strict()) - } - } -}; - -export default define(meta, async (ps, user) => { - await User.update(user._id, { - $set: { - 'clientSettings.home': ps.home - } - }); - - publishMainStream(user._id, 'homeUpdated', ps.home); - - return; -}); diff --git a/src/server/api/endpoints/i/update-mobile-home.ts b/src/server/api/endpoints/i/update-mobile-home.ts deleted file mode 100644 index 642e2b3e09f38c2a60e62039e710c037a12921db..0000000000000000000000000000000000000000 --- a/src/server/api/endpoints/i/update-mobile-home.ts +++ /dev/null @@ -1,32 +0,0 @@ -import $ from 'cafy'; -import User from '../../../../models/user'; -import { publishMainStream } from '../../../../services/stream'; -import define from '../../define'; - -export const meta = { - requireCredential: true, - - secure: true, - - params: { - home: { - validator: $.arr($.obj({ - name: $.str, - id: $.str, - data: $.obj() - }).strict()) - } - } -}; - -export default define(meta, async (ps, user) => { - await User.update(user._id, { - $set: { - 'clientSettings.mobileHome': ps.home - } - }); - - publishMainStream(user._id, 'mobileHomeUpdated', ps.home); - - return; -}); diff --git a/src/server/api/endpoints/i/update-widget.ts b/src/server/api/endpoints/i/update-widget.ts deleted file mode 100644 index 67d342278d7928182b2a3d49d55e16f39cf0063e..0000000000000000000000000000000000000000 --- a/src/server/api/endpoints/i/update-widget.ts +++ /dev/null @@ -1,88 +0,0 @@ -import $ from 'cafy'; -import User from '../../../../models/user'; -import { publishMainStream } from '../../../../services/stream'; -import define from '../../define'; - -export const meta = { - requireCredential: true, - - secure: true, - - params: { - id: { - validator: $.str - }, - - data: { - validator: $.obj() - } - } -}; - -export default define(meta, async (ps, user) => { - if (ps.id == null && ps.data == null) throw new Error('you need to set id and data params if home param unset'); - - let widget; - - //#region Desktop home - if (widget == null && user.clientSettings.home) { - const desktopHome = user.clientSettings.home; - widget = desktopHome.find((w: any) => w.id == ps.id); - if (widget) { - widget.data = ps.data; - - await User.update(user._id, { - $set: { - 'clientSettings.home': desktopHome - } - }); - } - } - //#endregion - - //#region Mobile home - if (widget == null && user.clientSettings.mobileHome) { - const mobileHome = user.clientSettings.mobileHome; - widget = mobileHome.find((w: any) => w.id == ps.id); - if (widget) { - widget.data = ps.data; - - await User.update(user._id, { - $set: { - 'clientSettings.mobileHome': mobileHome - } - }); - } - } - //#endregion - - //#region Deck - if (widget == null && user.clientSettings.deck && user.clientSettings.deck.columns) { - const deck = user.clientSettings.deck; - for (const c of deck.columns.filter((c: any) => c.type == 'widgets')) { - for (const w of c.widgets.filter((w: any) => w.id == ps.id)) { - widget = w; - } - } - if (widget) { - widget.data = ps.data; - - await User.update(user._id, { - $set: { - 'clientSettings.deck': deck - } - }); - } - } - //#endregion - - if (widget) { - publishMainStream(user._id, 'widgetUpdated', { - id: ps.id, data: ps.data - }); - - return; - } else { - throw new Error('widget not found'); - } -}); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 099ef3399087881f5c214d1737e4a10de3561100..f3e5d41021313528ba892ab837584b2acc694ecf 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -1,18 +1,16 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user'; +import { ID } from '../../../../misc/cafy-id'; import { publishMainStream } from '../../../../services/stream'; -import DriveFile from '../../../../models/drive-file'; import acceptAllFollowRequests from '../../../../services/following/requests/accept-all'; import { publishToFollowers } from '../../../../services/i/update'; import define from '../../define'; -import getDriveFileUrl from '../../../../misc/get-drive-file-url'; import { parse, parsePlain } from '../../../../mfm/parse'; import extractEmojis from '../../../../misc/extract-emojis'; import extractHashtags from '../../../../misc/extract-hashtags'; import * as langmap from 'langmap'; import { updateHashtag } from '../../../../services/update-hashtag'; import { ApiError } from '../../error'; +import { Users, DriveFiles } from '../../../../models'; export const meta = { desc: { @@ -24,18 +22,18 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { name: { - validator: $.optional.nullable.str.pipe(isValidName), + validator: $.optional.nullable.str.pipe(Users.isValidName), desc: { 'ja-JP': 'åå‰(ãƒãƒ³ãƒ‰ãƒ«ãƒãƒ¼ãƒ やニックãƒãƒ¼ãƒ )' } }, description: { - validator: $.optional.nullable.str.pipe(isValidDescription), + validator: $.optional.nullable.str.pipe(Users.isValidDescription), desc: { 'ja-JP': 'アカウントã®èª¬æ˜Žã‚„自己紹介' } @@ -49,14 +47,14 @@ export const meta = { }, location: { - validator: $.optional.nullable.str.pipe(isValidLocation), + validator: $.optional.nullable.str.pipe(Users.isValidLocation), desc: { 'ja-JP': 'ä½ã‚“ã§ã„る地域ã€æ‰€åœ¨' } }, birthday: { - validator: $.optional.nullable.str.pipe(isValidBirthday), + validator: $.optional.nullable.str.pipe(Users.isValidBirthday), desc: { 'ja-JP': '誕生日 (YYYY-MM-DDå½¢å¼)' } @@ -64,7 +62,6 @@ export const meta = { avatarId: { validator: $.optional.nullable.type(ID), - transform: transform, desc: { 'ja-JP': 'アイコンã«è¨å®šã™ã‚‹ç”»åƒã®ãƒ‰ãƒ©ã‚¤ãƒ–ファイルID' } @@ -72,20 +69,11 @@ export const meta = { bannerId: { validator: $.optional.nullable.type(ID), - transform: transform, desc: { 'ja-JP': 'ãƒãƒŠãƒ¼ã«è¨å®šã™ã‚‹ç”»åƒã®ãƒ‰ãƒ©ã‚¤ãƒ–ファイルID' } }, - wallpaperId: { - validator: $.optional.nullable.type(ID), - transform: transform, - desc: { - 'ja-JP': 'å£ç´™ã«è¨å®šã™ã‚‹ç”»åƒã®ãƒ‰ãƒ©ã‚¤ãƒ–ファイルID' - } - }, - isLocked: { validator: $.optional.bool, desc: { @@ -171,116 +159,76 @@ export default define(meta, async (ps, user, app) => { if (ps.name !== undefined) updates.name = ps.name; if (ps.description !== undefined) updates.description = ps.description; if (ps.lang !== undefined) updates.lang = ps.lang; - if (ps.location !== undefined) updates['profile.location'] = ps.location; - if (ps.birthday !== undefined) updates['profile.birthday'] = ps.birthday; + if (ps.location !== undefined) updates.location = ps.location; + if (ps.birthday !== undefined) updates.birthday = ps.birthday; if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; - if (ps.wallpaperId !== undefined) updates.wallpaperId = ps.wallpaperId; if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot; if (typeof ps.carefulBot == 'boolean') updates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed == 'boolean') updates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat; - if (typeof ps.autoWatch == 'boolean') updates['settings.autoWatch'] = ps.autoWatch; - if (typeof ps.alwaysMarkNsfw == 'boolean') updates['settings.alwaysMarkNsfw'] = ps.alwaysMarkNsfw; + if (typeof ps.autoWatch == 'boolean') updates.autoWatch = ps.autoWatch; + if (typeof ps.alwaysMarkNsfw == 'boolean') updates.alwaysMarkNsfw = ps.alwaysMarkNsfw; if (ps.avatarId) { - const avatar = await DriveFile.findOne({ - _id: ps.avatarId - }); + const avatar = await DriveFiles.findOne(ps.avatarId); - if (avatar == null) throw new ApiError(meta.errors.noSuchAvatar); - if (!avatar.contentType.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage); + if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); + if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage); - if (avatar.metadata.deletedAt) { - updates.avatarUrl = null; - } else { - updates.avatarUrl = getDriveFileUrl(avatar, true); + updates.avatarUrl = avatar.thumbnailUrl; - if (avatar.metadata.properties.avgColor) { - updates.avatarColor = avatar.metadata.properties.avgColor; - } + if (avatar.properties.avgColor) { + updates.avatarColor = avatar.properties.avgColor; } } if (ps.bannerId) { - const banner = await DriveFile.findOne({ - _id: ps.bannerId - }); + const banner = await DriveFiles.findOne(ps.bannerId); - if (banner == null) throw new ApiError(meta.errors.noSuchBanner); - if (!banner.contentType.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage); + if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); + if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage); - if (banner.metadata.deletedAt) { - updates.bannerUrl = null; - } else { - updates.bannerUrl = getDriveFileUrl(banner, false); + updates.bannerUrl = banner.webpublicUrl; - if (banner.metadata.properties.avgColor) { - updates.bannerColor = banner.metadata.properties.avgColor; - } - } - } - - if (ps.wallpaperId !== undefined) { - if (ps.wallpaperId === null) { - updates.wallpaperUrl = null; - updates.wallpaperColor = null; - } else { - const wallpaper = await DriveFile.findOne({ - _id: ps.wallpaperId - }); - - if (wallpaper == null) throw new Error('wallpaper not found'); - - if (wallpaper.metadata.deletedAt) { - updates.wallpaperUrl = null; - } else { - updates.wallpaperUrl = getDriveFileUrl(wallpaper); - - if (wallpaper.metadata.properties.avgColor) { - updates.wallpaperColor = wallpaper.metadata.properties.avgColor; - } - } + if (banner.properties.avgColor) { + updates.bannerColor = banner.properties.avgColor; } } //#region emojis/tags - if (updates.name != null || updates.description != null) { - let emojis = [] as string[]; - let tags = [] as string[]; + let emojis = [] as string[]; + let tags = [] as string[]; - if (updates.name != null) { - const tokens = parsePlain(updates.name); - emojis = emojis.concat(extractEmojis(tokens)); - } + if (updates.name != null) { + const tokens = parsePlain(updates.name); + emojis = emojis.concat(extractEmojis(tokens)); + } - if (updates.description != null) { - const tokens = parse(updates.description); - emojis = emojis.concat(extractEmojis(tokens)); - tags = extractHashtags(tokens).map(tag => tag.toLowerCase()); - } + if (updates.description != null) { + const tokens = parse(updates.description); + emojis = emojis.concat(extractEmojis(tokens)); + tags = extractHashtags(tokens).map(tag => tag.toLowerCase()); + } - updates.emojis = emojis; - updates.tags = tags; + updates.emojis = emojis; + updates.tags = tags; - // ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°æ›´æ–° - for (const tag of tags) updateHashtag(user, tag, true, true); - for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false); - } + // ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°æ›´æ–° + for (const tag of tags) updateHashtag(user, tag, true, true); + for (const tag of user.tags.filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false); //#endregion - await User.update(user._id, { - $set: updates - }); + await Users.update(user.id, updates); - const iObj = await pack(user._id, user, { + const iObj = await Users.pack(user.id, user, { detail: true, includeSecrets: isSecure }); // Publish meUpdated event - publishMainStream(user._id, 'meUpdated', iObj); + publishMainStream(user.id, 'meUpdated', iObj); // éµåž¢ã‚’解除ã—ãŸã¨ãã€æºœã¾ã£ã¦ã„ãŸãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒã‚ã‚‹ãªã‚‰ã™ã¹ã¦æ‰¿èª if (user.isLocked && ps.isLocked === false) { @@ -288,7 +236,7 @@ export default define(meta, async (ps, user, app) => { } // フォãƒãƒ¯ãƒ¼ã«Updateã‚’é…ä¿¡ - publishToFollowers(user._id); + publishToFollowers(user.id); return iObj; }); diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts index 699dc7c253bb78835a0745fb210f8d605aefbaf7..c0aec61212d0474c8f97b295c2cf1f13ff057303 100644 --- a/src/server/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import Mute from '../../../../models/mute'; -import Message, { pack, IMessagingMessage } from '../../../../models/messaging-message'; import define from '../../define'; +import { MessagingMessage } from '../../../../models/entities/messaging-message'; +import { MessagingMessages, Mutings } from '../../../../models'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -31,34 +32,33 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const mute = await Mute.find({ - muterId: user._id, - deletedAt: { $exists: false } + const mute = await Mutings.find({ + muterId: user.id, }); - const history: IMessagingMessage[] = []; + const history: MessagingMessage[] = []; for (let i = 0; i < ps.limit; i++) { - const found = history.map(m => m.userId.equals(user._id) ? m.recipientId : m.userId); + const found = history.map(m => (m.userId === user.id) ? m.recipientId : m.userId); - const message = await Message.findOne({ - $or: [{ - userId: user._id - }, { - recipientId: user._id - }], - $and: [{ - userId: { $nin: found }, - recipientId: { $nin: found } - }, { - userId: { $nin: mute.map(m => m.muteeId) }, - recipientId: { $nin: mute.map(m => m.muteeId) } - }] - }, { - sort: { - createdAt: -1 - } - }); + const query = MessagingMessages.createQueryBuilder('message') + .where(new Brackets(qb => { qb + .where(`message.userId = :userId`, { userId: user.id }) + .orWhere(`message.recipientId = :userId`, { userId: user.id }); + })) + .orderBy('message.createdAt', 'DESC'); + + if (found.length > 0) { + query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); + query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); + } + + if (mute.length > 0) { + query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + } + + const message = await query.getOne(); if (message) { history.push(message); @@ -67,5 +67,5 @@ export default define(meta, async (ps, user) => { } } - return await Promise.all(history.map(h => pack(h._id, user))); + return await Promise.all(history.map(h => MessagingMessages.pack(h.id, user))); }); diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts index c19db45f1f1fef0d6dc813d4e71d26f82c691974..02c57b8d03b5f0567a65d4a831266db15a5a61ed 100644 --- a/src/server/api/endpoints/messaging/messages.ts +++ b/src/server/api/endpoints/messaging/messages.ts @@ -1,11 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Message from '../../../../models/messaging-message'; -import { pack } from '../../../../models/messaging-message'; +import { ID } from '../../../../misc/cafy-id'; import read from '../../common/read-messaging-message'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { MessagingMessages } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -22,7 +22,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -36,12 +35,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, markAsRead: { @@ -73,43 +70,17 @@ export default define(meta, async (ps, user) => { throw e; }); - const query = { - $or: [{ - userId: user._id, - recipientId: recipient._id - }, { - userId: recipient._id, - recipientId: user._id - }] - } as any; - - const sort = { - _id: -1 - }; - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } + const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) + .andWhere(`(message.userId = :meId AND message.recipientId = :recipientId) OR (message.userId = :recipientId AND message.recipientId = :meId)`, { meId: user.id, recipientId: recipient.id }); - const messages = await Message - .find(query, { - limit: ps.limit, - sort: sort - }); + const messages = await query.getMany(); // Mark all as read if (ps.markAsRead) { - read(user._id, recipient._id, messages); + read(user.id, recipient.id, messages.map(x => x.id)); } - return await Promise.all(messages.map(message => pack(message, user, { + return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { populateRecipient: false }))); }); diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index fc048e6edd8d93462ebd8e66d42c12568f22c9d1..2c7e5ad2d958f015bb1065c267827a06f616f536 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -1,17 +1,14 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Message from '../../../../../models/messaging-message'; -import { isValidText } from '../../../../../models/messaging-message'; -import User from '../../../../../models/user'; -import Mute from '../../../../../models/mute'; -import DriveFile from '../../../../../models/drive-file'; -import { pack } from '../../../../../models/messaging-message'; +import { ID } from '../../../../../misc/cafy-id'; import { publishMainStream } from '../../../../../services/stream'; import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../services/stream'; import pushSw from '../../../../../services/push-notification'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; +import { MessagingMessages, DriveFiles, Mutings } from '../../../../../models'; +import { MessagingMessage } from '../../../../../models/entities/messaging-message'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { desc: { @@ -28,7 +25,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -36,12 +32,11 @@ export const meta = { }, text: { - validator: $.optional.str.pipe(isValidText) + validator: $.optional.str.pipe(MessagingMessages.isValidText) }, fileId: { validator: $.optional.type(ID), - transform: transform, } }, @@ -78,7 +73,7 @@ export const meta = { export default define(meta, async (ps, user) => { // Myself - if (ps.userId.equals(user._id)) { + if (ps.userId === user.id) { throw new ApiError(meta.errors.recipientIsYourself); } @@ -90,12 +85,12 @@ export default define(meta, async (ps, user) => { let file = null; if (ps.fileId != null) { - file = await DriveFile.findOne({ - _id: ps.fileId, - 'metadata.userId': user._id + file = await DriveFiles.findOne({ + id: ps.fileId, + userId: user.id }); - if (file === null) { + if (file == null) { throw new ApiError(meta.errors.noSuchFile); } } @@ -105,16 +100,17 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.contentRequired); } - const message = await Message.insert({ + const message = await MessagingMessages.save({ + id: genId(), createdAt: new Date(), - fileId: file ? file._id : undefined, - recipientId: recipient._id, - text: ps.text ? ps.text.trim() : undefined, - userId: user._id, + fileId: file ? file.id : null, + recipientId: recipient.id, + text: ps.text ? ps.text.trim() : null, + userId: user.id, isRead: false - }); + } as MessagingMessage); - const messageObj = await pack(message); + const messageObj = await MessagingMessages.pack(message); // 自分ã®ã‚¹ãƒˆãƒªãƒ¼ãƒ publishMessagingStream(message.userId, message.recipientId, 'message', messageObj); @@ -126,25 +122,17 @@ export default define(meta, async (ps, user) => { publishMessagingIndexStream(message.recipientId, 'message', messageObj); publishMainStream(message.recipientId, 'messagingMessage', messageObj); - // Update flag - User.update({ _id: recipient._id }, { - $set: { - hasUnreadMessagingMessage: true - } - }); - // 2秒経ã£ã¦ã‚‚(今回作æˆã—ãŸ)メッセージãŒæ—¢èªã«ãªã‚‰ãªã‹ã£ãŸã‚‰ã€Œæœªèªã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãŒã‚ã‚Šã¾ã™ã‚ˆã€ã‚¤ãƒ™ãƒ³ãƒˆã‚’発行ã™ã‚‹ setTimeout(async () => { - const freshMessage = await Message.findOne({ _id: message._id }, { isRead: true }); + const freshMessage = await MessagingMessages.findOne({ id: message.id }); if (freshMessage == null) return; // メッセージãŒå‰Šé™¤ã•ã‚Œã¦ã„ã‚‹å ´åˆã‚‚ã‚ã‚‹ if (!freshMessage.isRead) { //#region ãŸã ã—ミュートã•ã‚Œã¦ã„ã‚‹ãªã‚‰ç™ºè¡Œã—ãªã„ - const mute = await Mute.find({ - muterId: recipient._id, - deletedAt: { $exists: false } + const mute = await Mutings.find({ + muterId: recipient.id, }); const mutedUserIds = mute.map(m => m.muteeId.toString()); - if (mutedUserIds.indexOf(user._id.toString()) != -1) { + if (mutedUserIds.indexOf(user.id) != -1) { return; } //#endregion diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts index 0ca12846c118262e50efac3d5f7e4ab98af2ac12..9f55caba624be903671c0f3f2ec5068baf12d585 100644 --- a/src/server/api/endpoints/messaging/messages/delete.ts +++ b/src/server/api/endpoints/messaging/messages/delete.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Message from '../../../../../models/messaging-message'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { publishMessagingStream } from '../../../../../services/stream'; import * as ms from 'ms'; import { ApiError } from '../../../error'; +import { MessagingMessages } from '../../../../../models'; export const meta = { stability: 'stable', @@ -29,7 +29,6 @@ export const meta = { params: { messageId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®ID', 'en-US': 'Target message ID.' @@ -47,19 +46,17 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const message = await Message.findOne({ - _id: ps.messageId, - userId: user._id + const message = await MessagingMessages.findOne({ + id: ps.messageId, + userId: user.id }); - if (message === null) { + if (message == null) { throw new ApiError(meta.errors.noSuchMessage); } - await Message.remove({ _id: message._id }); + await MessagingMessages.delete(message.id); - publishMessagingStream(message.userId, message.recipientId, 'deleted', message._id); - publishMessagingStream(message.recipientId, message.userId, 'deleted', message._id); - - return; + publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); + publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); }); diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts index aa8ecdc4ff3ce3440448c2e8c889264051250c38..24a28285bf228d03537bdc89e8445b8ba7847fdc 100644 --- a/src/server/api/endpoints/messaging/messages/read.ts +++ b/src/server/api/endpoints/messaging/messages/read.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Message from '../../../../../models/messaging-message'; +import { ID } from '../../../../../misc/cafy-id'; import read from '../../../common/read-messaging-message'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { MessagingMessages } from '../../../../../models'; export const meta = { desc: { @@ -20,7 +20,6 @@ export const meta = { params: { messageId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': 'æ—¢èªã«ã™ã‚‹ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®ID', 'en-US': 'The ID of a message that you want to mark as read' @@ -38,16 +37,14 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const message = await Message.findOne({ - _id: ps.messageId, - recipientId: user._id + const message = await MessagingMessages.findOne({ + id: ps.messageId, + recipientId: user.id }); if (message == null) { throw new ApiError(meta.errors.noSuchMessage); } - read(user._id, message.userId, message); - - return; + read(user.id, message.userId, [message.id]); }); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index a297f47e0ef1df48d8f219d5fa08595bf168e0d7..785f21f22bac726f68228cf42fd7032da2af1508 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; import * as os from 'os'; import config from '../../../config'; -import Emoji from '../../../models/emoji'; import define from '../define'; import fetchMeta from '../../../misc/fetch-meta'; import * as pkg from '../../../../package.json'; +import { Emojis } from '../../../models'; export const meta = { stability: 'stable', @@ -81,14 +81,11 @@ export const meta = { export default define(meta, async (ps, me) => { const instance = await fetchMeta(); - const emojis = await Emoji.find({ host: null }, { - fields: { - _id: false - } - }); + const emojis = await Emojis.find({ host: null }); const response: any = { - maintainer: instance.maintainer, + maintainerName: instance.maintainerName, + maintainerEmail: instance.maintainerEmail, version: pkg.version, @@ -145,17 +142,12 @@ export default define(meta, async (ps, me) => { github: instance.enableGithubIntegration, discord: instance.enableDiscordIntegration, serviceWorker: instance.enableServiceWorker, - userRecommendation: { - external: instance.enableExternalUserRecommendation, - engine: instance.externalUserRecommendationEngine, - timeout: instance.externalUserRecommendationTimeout - } }; } if (me && (me.isAdmin || me.isModerator)) { response.useStarForReactionFallback = instance.useStarForReactionFallback; - response.hidedTags = instance.hidedTags; + response.hiddenTags = instance.hiddenTags; response.recaptchaSecretKey = instance.recaptchaSecretKey; response.proxyAccount = instance.proxyAccount; response.twitterConsumerKey = instance.twitterConsumerKey; @@ -164,9 +156,6 @@ export default define(meta, async (ps, me) => { response.githubClientSecret = instance.githubClientSecret; response.discordClientId = instance.discordClientId; response.discordClientSecret = instance.discordClientSecret; - response.enableExternalUserRecommendation = instance.enableExternalUserRecommendation; - response.externalUserRecommendationEngine = instance.externalUserRecommendationEngine; - response.externalUserRecommendationTimeout = instance.externalUserRecommendationTimeout; response.summalyProxy = instance.summalyProxy; response.email = instance.email; response.smtpSecure = instance.smtpSecure; diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts index 7eaee90a05b28f8ea640e8d3e330af7a1b5ab579..d13c546fdc6f82e9845e9087befb91a94852c4f1 100644 --- a/src/server/api/endpoints/mute/create.ts +++ b/src/server/api/endpoints/mute/create.ts @@ -1,9 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Mute from '../../../../models/mute'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { genId } from '../../../../misc/gen-id'; +import { Mutings, NoteWatchings } from '../../../../models'; +import { Muting } from '../../../../models/entities/muting'; export const meta = { desc: { @@ -15,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'account/write', + kind: 'write:mutes', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -53,7 +54,7 @@ export default define(meta, async (ps, user) => { const muter = user; // 自分自身 - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.muteeIsYourself); } @@ -64,21 +65,25 @@ export default define(meta, async (ps, user) => { }); // Check if already muting - const exist = await Mute.findOne({ - muterId: muter._id, - muteeId: mutee._id + const exist = await Mutings.findOne({ + muterId: muter.id, + muteeId: mutee.id }); - if (exist !== null) { + if (exist != null) { throw new ApiError(meta.errors.alreadyMuting); } // Create mute - await Mute.insert({ + await Mutings.save({ + id: genId(), createdAt: new Date(), - muterId: muter._id, - muteeId: mutee._id, - }); + muterId: muter.id, + muteeId: mutee.id, + } as Muting); - return; + NoteWatchings.delete({ + userId: muter.id, + noteUserId: mutee.id + }); }); diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts index 1a03f6371b81a7a06504ac0013a15979d8864000..1aae15af910804d6053b76865a4815561a93e17c 100644 --- a/src/server/api/endpoints/mute/delete.ts +++ b/src/server/api/endpoints/mute/delete.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Mute from '../../../../models/mute'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Mutings } from '../../../../models'; export const meta = { desc: { @@ -15,12 +15,11 @@ export const meta = { requireCredential: true, - kind: 'account/write', + kind: 'write:mutes', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -53,7 +52,7 @@ export default define(meta, async (ps, user) => { const muter = user; // Check if the mutee is yourself - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.muteeIsYourself); } @@ -64,19 +63,17 @@ export default define(meta, async (ps, user) => { }); // Check not muting - const exist = await Mute.findOne({ - muterId: muter._id, - muteeId: mutee._id + const exist = await Mutings.findOne({ + muterId: muter.id, + muteeId: mutee.id }); - if (exist === null) { + if (exist == null) { throw new ApiError(meta.errors.notMuting); } // Delete mute - await Mute.remove({ - _id: exist._id + await Mutings.delete({ + id: exist.id }); - - return; }); diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts index 1b8f7594968aa51bd6ef0fe417bf72bf04398452..5f2d1844724cfe2c5918b50845f17294cd560efb 100644 --- a/src/server/api/endpoints/mute/list.ts +++ b/src/server/api/endpoints/mute/list.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Mute, { packMany } from '../../../../models/mute'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Mutings } from '../../../../models'; export const meta = { desc: { @@ -13,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'account/read', + kind: 'read:mutes', params: { limit: { @@ -23,12 +24,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, }, @@ -41,30 +40,12 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const query = { - muterId: me._id - } as any; + const query = makePaginationQuery(Mutings.createQueryBuilder('muting'), ps.sinceId, ps.untilId) + .andWhere(`muting.muterId = :meId`, { meId: me.id }); - const sort = { - _id: -1 - }; + const mutings = await query + .take(ps.limit) + .getMany(); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } - - const mutes = await Mute - .find(query, { - limit: ps.limit, - sort: sort - }); - - return await packMany(mutes, me); + return await Mutings.packMany(mutings, me); }); diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts index 1a936c918b14c01babc31efb94b90b794b0163fb..d205d1674c0057e46b6ed62a00342702b97feb6b 100644 --- a/src/server/api/endpoints/my/apps.ts +++ b/src/server/api/endpoints/my/apps.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; -import App, { pack } from '../../../../models/app'; import define from '../../define'; +import { Apps } from '../../../../models'; export const meta = { tags: ['account', 'app'], @@ -27,19 +27,16 @@ export const meta = { export default define(meta, async (ps, user) => { const query = { - userId: user._id + userId: user.id }; - const apps = await App - .find(query, { - limit: ps.limit, - skip: ps.offset, - sort: { - _id: -1 - } - }); + const apps = await Apps.find({ + where: query, + take: ps.limit, + skip: ps.offset, + }); - return await Promise.all(apps.map(app => pack(app, user, { + return await Promise.all(apps.map(app => Apps.pack(app, user, { detail: true }))); }); diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts index 835c515cfe67bf4cb9d017e93be7bb1960622d65..10f6e398450ec4847010c938f5e153439526e0ad 100644 --- a/src/server/api/endpoints/notes.ts +++ b/src/server/api/endpoints/notes.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../misc/cafy-id'; -import Note, { packMany } from '../../../models/note'; +import { ID } from '../../../misc/cafy-id'; import define from '../define'; +import { makePaginationQuery } from '../common/make-pagination-query'; +import { Notes } from '../../../models'; export const meta = { desc: { @@ -39,14 +40,6 @@ export const meta = { } }, - media: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'ファイルãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã«é™å®šã™ã‚‹ã‹å¦ã‹ (ã“ã®ãƒ‘ラメータã¯å»ƒæ¢äºˆå®šã§ã™ã€‚代ã‚ã‚Šã« withFiles を使ã£ã¦ãã ã•ã„。)' - } - }, - poll: { validator: $.optional.bool, desc: { @@ -61,12 +54,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, }, @@ -79,43 +70,29 @@ export const meta = { }; export default define(meta, async (ps) => { - const sort = { - _id: -1 - }; - const query = { - deletedAt: null, - visibility: 'public', - localOnly: { $ne: true }, - } as any; - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(`note.visibility = 'public'`) + .andWhere(`note.localOnly = FALSE`) + .leftJoinAndSelect('note.user', 'user'); if (ps.local) { - query['_user.host'] = null; + query.andWhere('note.userHost IS NULL'); } if (ps.reply != undefined) { - query.replyId = ps.reply ? { $exists: true, $ne: null } : null; + query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL'); } if (ps.renote != undefined) { - query.renoteId = ps.renote ? { $exists: true, $ne: null } : null; + query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL'); } - const withFiles = ps.withFiles != undefined ? ps.withFiles : ps.media; - - if (withFiles) query.fileIds = { $exists: true, $ne: null }; + if (ps.withFiles != undefined) { + query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`); + } if (ps.poll != undefined) { - query.poll = ps.poll ? { $exists: true, $ne: null } : null; + query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE'); } // TODO @@ -123,10 +100,7 @@ export default define(meta, async (ps) => { // query.isBot = bot; //} - const notes = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const notes = await query.take(ps.limit).getMany(); - return await packMany(notes); + return await Notes.packMany(notes); }); diff --git a/src/server/api/endpoints/notes/children.ts b/src/server/api/endpoints/notes/children.ts index 3738459b7109e12ec95eefd47001000ebfc3bced..72f2c39d6ab25920387150673c6bde941a8e7970 100644 --- a/src/server/api/endpoints/notes/children.ts +++ b/src/server/api/endpoints/notes/children.ts @@ -1,9 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note, { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import { getFriends } from '../../common/get-friends'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { Brackets } from 'typeorm'; +import { Notes } from '../../../../models'; export const meta = { desc: { @@ -18,7 +20,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID' @@ -32,12 +33,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, }, @@ -50,83 +49,24 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const [followings, hideUserIds] = await Promise.all([ - // フォãƒãƒ¼ã‚’å–å¾— - // Fetch following - user ? getFriends(user._id) : [], - - // éš ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å–å¾— - getHideUserIds(user) - ]); - - const visibleQuery = user == null ? [{ - visibility: { $in: [ 'public', 'home' ] } - }] : [{ - visibility: { $in: [ 'public', 'home' ] } - }, { - // myself (for followers/specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ user._id ] } - }, { - visibility: 'followers', - $or: [{ - // フォãƒãƒ¯ãƒ¼ã®æŠ•ç¨¿ - userId: { $in: followings.map(f => f.id) }, - }, { - // 自分ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライ - '_reply.userId': user._id, - }, { - // 自分ã¸ã®ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ãŒå«ã¾ã‚Œã¦ã„ã‚‹ - mentions: { $in: [ user._id ] } - }] - }]; - - const q = { - $and: [{ - $or: [{ - replyId: ps.noteId, - }, { - renoteId: ps.noteId, - $or: [{ - text: { $ne: null } - }, { - fileIds: { $ne: [] } - }, { - poll: { $ne: null } - }] - }] - }, { - $or: visibleQuery - }] - } as any; - - if (hideUserIds && hideUserIds.length > 0) { - q['userId'] = { - $nin: hideUserIds - }; - } - - const sort = { - _id: -1 - }; - - if (ps.sinceId) { - sort._id = 1; - q._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - q._id = { - $lt: ps.untilId - }; - } - - const notes = await Note.find(q, { - limit: ps.limit, - sort: sort - }); - - return await packMany(notes, user); + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(new Brackets(qb => { qb + .where(`note.replyId = :noteId`, { noteId: ps.noteId }) + .orWhere(new Brackets(qb => { qb + .where(`note.renoteId = :noteId`, { noteId: ps.noteId }) + .andWhere(new Brackets(qb => { qb + .where(`note.text IS NOT NULL`) + .orWhere(`note.fileIds != '{}'`) + .orWhere(`note.hasPoll = TRUE`); + })); + })); + })) + .leftJoinAndSelect('note.user', 'user'); + + if (user) generateVisibilityQuery(query, user); + if (user) generateMuteQuery(query, user); + + const notes = await query.take(ps.limit).getMany(); + + return await Notes.packMany(notes, user); }); diff --git a/src/server/api/endpoints/notes/conversation.ts b/src/server/api/endpoints/notes/conversation.ts index 702d8dc430a558db19cc289f48b7ffd1b02d5f6c..6defd790423506f5a868392f1218ef47b69634aa 100644 --- a/src/server/api/endpoints/notes/conversation.ts +++ b/src/server/api/endpoints/notes/conversation.ts @@ -1,9 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note, { packMany, INote } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; import { getNote } from '../../common/getters'; +import { Note } from '../../../../models/entities/note'; +import { Notes } from '../../../../models'; export const meta = { desc: { @@ -18,7 +19,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID' @@ -58,12 +58,12 @@ export default define(meta, async (ps, user) => { throw e; }); - const conversation: INote[] = []; + const conversation: Note[] = []; let i = 0; async function get(id: any) { i++; - const p = await Note.findOne({ _id: id }); + const p = await Notes.findOne(id); if (i > ps.offset) { conversation.push(p); @@ -82,5 +82,5 @@ export default define(meta, async (ps, user) => { await get(note.replyId); } - return await packMany(conversation, user); + return await Notes.packMany(conversation, user); }); diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 8cc5e4b81502cdaa64fb3df8300ada3a3932b693..138f05fb3b9cf472914e0b9342b3503bba847a5f 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -1,14 +1,15 @@ import $ from 'cafy'; -import ID, { transform, transformMany } from '../../../../misc/cafy-id'; import * as ms from 'ms'; import { length } from 'stringz'; -import Note, { INote, isValidCw, pack } from '../../../../models/note'; -import User, { IUser } from '../../../../models/user'; -import DriveFile, { IDriveFile } from '../../../../models/drive-file'; import create from '../../../../services/note/create'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; import { ApiError } from '../../error'; +import { ID } from '../../../../misc/cafy-id'; +import { User } from '../../../../models/entities/user'; +import { Users, DriveFiles, Notes } from '../../../../models'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { Note } from '../../../../models/entities/note'; let maxNoteTextLength = 1000; @@ -34,7 +35,7 @@ export const meta = { max: 300 }, - kind: 'note-write', + kind: 'write:notes', params: { visibility: { @@ -47,7 +48,6 @@ export const meta = { visibleUserIds: { validator: $.optional.arr($.type(ID)).unique().min(0), - transform: transformMany, desc: { 'ja-JP': '(投稿ã®å…¬é–‹ç¯„囲㌠specified ã®å ´åˆ)投稿を閲覧ã§ãるユーザー' } @@ -64,7 +64,7 @@ export const meta = { }, cw: { - validator: $.optional.nullable.str.pipe(isValidCw), + validator: $.optional.nullable.str.pipe(Notes.validateCw), desc: { 'ja-JP': 'コンテンツã®è¦å‘Šã€‚ã“ã®ãƒ‘ラメータを指定ã™ã‚‹ã¨è¨å®šã—ãŸãƒ†ã‚ストã§æŠ•ç¨¿ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„ã‚’éš ã™äº‹ãŒå‡ºæ¥ã¾ã™ã€‚' } @@ -129,7 +129,6 @@ export const meta = { fileIds: { validator: $.optional.arr($.type(ID)).unique().range(1, 4), - transform: transformMany, desc: { 'ja-JP': '添付ã™ã‚‹ãƒ•ã‚¡ã‚¤ãƒ«' } @@ -137,7 +136,6 @@ export const meta = { mediaIds: { validator: $.optional.arr($.type(ID)).unique().range(1, 4), - transform: transformMany, deprecated: true, desc: { 'ja-JP': '添付ã™ã‚‹ãƒ•ã‚¡ã‚¤ãƒ« (ã“ã®ãƒ‘ラメータã¯å»ƒæ¢äºˆå®šã§ã™ã€‚代ã‚ã‚Šã« fileIds を使ã£ã¦ãã ã•ã„。)' @@ -146,7 +144,6 @@ export const meta = { replyId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': '返信対象' } @@ -154,7 +151,6 @@ export const meta = { renoteId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': 'Renote対象' } @@ -227,32 +223,28 @@ export const meta = { }; export default define(meta, async (ps, user, app) => { - let visibleUsers: IUser[] = []; + let visibleUsers: User[] = []; if (ps.visibleUserIds) { - visibleUsers = await Promise.all(ps.visibleUserIds.map(id => User.findOne({ - _id: id - }))); + visibleUsers = await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id))); } - let files: IDriveFile[] = []; + let files: DriveFile[] = []; const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; if (fileIds != null) { files = await Promise.all(fileIds.map(fileId => { - return DriveFile.findOne({ - _id: fileId, - 'metadata.userId': user._id + return DriveFiles.findOne({ + id: fileId, + userId: user.id }); })); files = files.filter(file => file != null); } - let renote: INote = null; + let renote: Note = null; if (ps.renoteId != null) { // Fetch renote to note - renote = await Note.findOne({ - _id: ps.renoteId - }); + renote = await Notes.findOne(ps.renoteId); if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); @@ -261,14 +253,12 @@ export default define(meta, async (ps, user, app) => { } } - let reply: INote = null; + let reply: Note = null; if (ps.replyId != null) { // Fetch reply - reply = await Note.findOne({ - _id: ps.replyId - }); + reply = await Notes.findOne(ps.replyId); - if (reply === null) { + if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); } @@ -279,12 +269,6 @@ export default define(meta, async (ps, user, app) => { } if (ps.poll) { - (ps.poll as any).choices = (ps.poll as any).choices.map((choice: string, i: number) => ({ - id: i, // IDを付与 - text: choice.trim(), - votes: 0 - })); - if (typeof ps.poll.expiresAt === 'number') { if (ps.poll.expiresAt < Date.now()) throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); @@ -298,11 +282,6 @@ export default define(meta, async (ps, user, app) => { throw new ApiError(meta.errors.contentRequired); } - // 後方互æ›æ€§ã®ãŸã‚ - if (ps.visibility == 'private') { - ps.visibility = 'specified'; - } - // æŠ•ç¨¿ã‚’ä½œæˆ const note = await create(user, { createdAt: new Date(), @@ -311,7 +290,7 @@ export default define(meta, async (ps, user, app) => { choices: ps.poll.choices, multiple: ps.poll.multiple || false, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null - } : undefined, + } : null, text: ps.text, reply, renote, @@ -321,13 +300,13 @@ export default define(meta, async (ps, user, app) => { localOnly: ps.localOnly, visibility: ps.visibility, visibleUsers, - apMentions: ps.noExtractMentions ? [] : undefined, - apHashtags: ps.noExtractHashtags ? [] : undefined, - apEmojis: ps.noExtractEmojis ? [] : undefined, + apMentions: ps.noExtractMentions ? [] : null, + apHashtags: ps.noExtractHashtags ? [] : null, + apEmojis: ps.noExtractEmojis ? [] : null, geo: ps.geo }); return { - createdNote: await pack(note, user) + createdNote: await Notes.pack(note, user) }; }); diff --git a/src/server/api/endpoints/notes/delete.ts b/src/server/api/endpoints/notes/delete.ts index 399f9288d640f555e831edfc4e94cd88018783ac..dbaf91bca37555dc77a065e9ee3357df6b27edf3 100644 --- a/src/server/api/endpoints/notes/delete.ts +++ b/src/server/api/endpoints/notes/delete.ts @@ -1,11 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import deleteNote from '../../../../services/note/delete'; -import User from '../../../../models/user'; import define from '../../define'; import * as ms from 'ms'; import { getNote } from '../../common/getters'; import { ApiError } from '../../error'; +import { Users } from '../../../../models'; export const meta = { stability: 'stable', @@ -19,7 +19,7 @@ export const meta = { requireCredential: true, - kind: 'note-write', + kind: 'write:notes', limit: { duration: ms('1hour'), @@ -30,7 +30,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID.' @@ -59,9 +58,10 @@ export default define(meta, async (ps, user) => { throw e; }); - if (!user.isAdmin && !user.isModerator && !note.userId.equals(user._id)) { + if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } - await deleteNote(await User.findOne({ _id: note.userId }), note); + // ã“ã®æ“作を行ã†ã®ãŒæŠ•ç¨¿è€…ã¨ã¯é™ã‚‰ãªã„(例ãˆã°ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼)ãŸã‚ + await deleteNote(await Users.findOne(note.userId), note); }); diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts index 9cde1a7dcf7cd73cc553fe0da33ba346efea48d8..7e046377588930bb26bdf2072147ff78cec1ed6e 100644 --- a/src/server/api/endpoints/notes/favorites/create.ts +++ b/src/server/api/endpoints/notes/favorites/create.ts @@ -1,9 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Favorite from '../../../../../models/favorite'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getNote } from '../../../common/getters'; +import { NoteFavorites } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { stability: 'stable', @@ -22,7 +23,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID.' @@ -53,21 +53,20 @@ export default define(meta, async (ps, user) => { }); // if already favorited - const exist = await Favorite.findOne({ - noteId: note._id, - userId: user._id + const exist = await NoteFavorites.findOne({ + noteId: note.id, + userId: user.id }); - if (exist !== null) { + if (exist != null) { throw new ApiError(meta.errors.alreadyFavorited); } // Create favorite - await Favorite.insert({ + await NoteFavorites.save({ + id: genId(), createdAt: new Date(), - noteId: note._id, - userId: user._id + noteId: note.id, + userId: user.id }); - - return; }); diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts index e2c787f3b5aa0409a6e3ca91fc65496aa2e456f4..a889c84d4d8fd8f89e847acb564c273ed72d7442 100644 --- a/src/server/api/endpoints/notes/favorites/delete.ts +++ b/src/server/api/endpoints/notes/favorites/delete.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Favorite from '../../../../../models/favorite'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getNote } from '../../../common/getters'; +import { NoteFavorites } from '../../../../../models'; export const meta = { stability: 'stable', @@ -22,7 +22,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID.' @@ -53,19 +52,15 @@ export default define(meta, async (ps, user) => { }); // if already favorited - const exist = await Favorite.findOne({ - noteId: note._id, - userId: user._id + const exist = await NoteFavorites.findOne({ + noteId: note.id, + userId: user.id }); - if (exist === null) { + if (exist == null) { throw new ApiError(meta.errors.notFavorited); } // Delete favorite - await Favorite.remove({ - _id: exist._id - }); - - return; + await NoteFavorites.delete(exist.id); }); diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts index 3648b307d73d0c66c5f431b27ee24d8db8982faa..c44a5275bbe9d629009299f3b262615492474aa9 100644 --- a/src/server/api/endpoints/notes/featured.ts +++ b/src/server/api/endpoints/notes/featured.ts @@ -1,8 +1,7 @@ import $ from 'cafy'; -import Note from '../../../../models/note'; -import { packMany } from '../../../../models/note'; import define from '../../define'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { Notes } from '../../../../models'; export const meta = { desc: { @@ -35,25 +34,14 @@ export const meta = { export default define(meta, async (ps, user) => { const day = 1000 * 60 * 60 * 24 * 3; // 3æ—¥å‰ã¾ã§ - const hideUserIds = await getHideUserIds(user); + const query = Notes.createQueryBuilder('note') + .andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) }) + .andWhere(`note.visibility = 'public'`) + .leftJoinAndSelect('note.user', 'user'); - const notes = await Note.find({ - createdAt: { - $gt: new Date(Date.now() - day) - }, - deletedAt: null, - visibility: 'public', - '_user.host': null, - ...(hideUserIds && hideUserIds.length > 0 ? { userId: { $nin: hideUserIds } } : {}) - }, { - limit: ps.limit, - sort: { - score: -1 - }, - hint: { - score: -1 - } - }); + if (user) generateMuteQuery(query, user); + + const notes = await query.orderBy('note.score', 'DESC').take(ps.limit).getMany(); - return await packMany(notes, user); + return await Notes.packMany(notes, user); }); diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts index 0eb761cdb6930aa513a1bce8c919519af2604853..7bf62f366bfe29ab37746a22b60d8d03ffa41574 100644 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ b/src/server/api/endpoints/notes/global-timeline.ts @@ -1,11 +1,12 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; -import { getHideUserIds } from '../../common/get-hide-users'; import { ApiError } from '../../error'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Notes } from '../../../../models'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { activeUsersChart } from '../../../../services/chart'; export const meta = { desc: { @@ -22,14 +23,6 @@ export const meta = { } }, - mediaOnly: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'ファイルãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã«é™å®šã™ã‚‹ã‹å¦ã‹ (ã“ã®ãƒ‘ラメータã¯å»ƒæ¢äºˆå®šã§ã™ã€‚代ã‚ã‚Šã« withFiles を使ã£ã¦ãã ã•ã„。)' - } - }, - limit: { validator: $.optional.num.range(1, 100), default: 10 @@ -37,12 +30,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, sinceDate: { @@ -71,6 +62,7 @@ export const meta = { }; export default define(meta, async (ps, user) => { + // TODO ã©ã£ã‹ã«ã‚ャッシュ const m = await fetchMeta(); if (m.disableGlobalTimeline) { if (user == null || (!user.isAdmin && !user.isModerator)) { @@ -78,68 +70,25 @@ export default define(meta, async (ps, user) => { } } - // éš ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å–å¾— - const hideUserIds = await getHideUserIds(user); - //#region Construct query - const sort = { - _id: -1 - }; - - const query = { - deletedAt: null, - - // public only - visibility: 'public', + const query = makePaginationQuery(Notes.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.visibility = \'public\'') + .andWhere('note.replyId IS NULL') + .leftJoinAndSelect('note.user', 'user'); - replyId: null - } as any; + if (user) generateMuteQuery(query, user); - if (hideUserIds && hideUserIds.length > 0) { - query.userId = { - $nin: hideUserIds - }; - - query['_reply.userId'] = { - $nin: hideUserIds - }; - - query['_renote.userId'] = { - $nin: hideUserIds - }; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } + //#endregion - const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + const timeline = await query.take(ps.limit).getMany(); - if (withFiles) { - query.fileIds = { $exists: true, $ne: [] }; + if (user) { + activeUsersChart.update(user); } - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - query.createdAt = { - $lt: new Date(ps.untilDate) - }; - } - //#endregion - - const timeline = await Note.find(query, { - limit: ps.limit, - sort: sort - }); - - return await packMany(timeline, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index 57ef4c3e15fcc45986dbe9a653b6ce3f43a41192..cd07341342b3f85afd03479334a49f17b2ec2388 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -1,12 +1,14 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; -import activeUsersChart from '../../../../services/chart/active-users'; -import { getHideUserIds } from '../../common/get-hide-users'; import { ApiError } from '../../error'; +import { Notes } from '../../../../models'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { activeUsersChart } from '../../../../services/chart'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -23,14 +25,6 @@ export const meta = { } }, - mediaOnly: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'ファイルãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã«é™å®šã™ã‚‹ã‹å¦ã‹ (ã“ã®ãƒ‘ラメータã¯å»ƒæ¢äºˆå®šã§ã™ã€‚代ã‚ã‚Šã« withFiles を使ã£ã¦ãã ã•ã„。)' - } - }, - fileType: { validator: $.optional.arr($.str), desc: { @@ -53,12 +47,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, sinceDate: { @@ -87,6 +79,7 @@ export const meta = { }; export default define(meta, async (ps, user) => { + // TODO ã©ã£ã‹ã«ã‚ャッシュ const m = await fetchMeta(); if (m.disableLocalTimeline) { if (user == null || (!user.isAdmin && !user.isModerator)) { @@ -94,90 +87,44 @@ export default define(meta, async (ps, user) => { } } - // éš ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å–å¾— - const hideUserIds = await getHideUserIds(user); - //#region Construct query - const sort = { - _id: -1 - }; - - const query = { - deletedAt: null, - - // public only - visibility: 'public', - - // リプライã§ãªã„ - //replyId: null, + const query = makePaginationQuery(Notes.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') + .leftJoinAndSelect('note.user', 'user'); - // local - '_user.host': null - } as any; + if (user) generateVisibilityQuery(query, user); + if (user) generateMuteQuery(query, user); - if (hideUserIds && hideUserIds.length > 0) { - query.userId = { - $nin: hideUserIds - }; - - query['_reply.userId'] = { - $nin: hideUserIds - }; - - query['_renote.userId'] = { - $nin: hideUserIds - }; - } - - const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; - - if (withFiles) { - query.fileIds = { $exists: true, $ne: [] }; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } if (ps.fileType) { - query.fileIds = { $exists: true, $ne: [] }; - - query['_files.contentType'] = { - $in: ps.fileType - }; + query.andWhere('note.fileIds != \'{}\''); + query.andWhere(new Brackets(qb => { + for (const type of ps.fileType) { + const i = ps.fileType.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } + })); if (ps.excludeNsfw) { - query['_files.metadata.isSensitive'] = { + // v11 TODO + /* + query['_files.isSensitive'] = { $ne: true }; + */ } } - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - query.createdAt = { - $lt: new Date(ps.untilDate) - }; - } //#endregion - const timeline = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const timeline = await query.take(ps.limit).getMany(); if (user) { activeUsersChart.update(user); } - return await packMany(timeline, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index 91333174edeb4edc2000a951bcd49c119230b771..0bbe7d3327016bc61b68d3ebe82f20e1d73f1b2c 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -1,11 +1,12 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { getFriendIds, getFriends } from '../../common/get-friends'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import read from '../../../../services/note/read'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { Notes, Followings } from '../../../../models'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -30,12 +31,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, visibility: { @@ -52,97 +51,34 @@ export const meta = { }; export default define(meta, async (ps, user) => { - // フォãƒãƒ¼ã‚’å–å¾— - const followings = await getFriends(user._id); - - const visibleQuery = [{ - visibility: { $in: [ 'public', 'home' ] } - }, { - // myself (for followers/specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ user._id ] } - }, { - visibility: 'followers', - $or: [{ - // フォãƒãƒ¯ãƒ¼ã®æŠ•ç¨¿ - userId: { $in: followings.map(f => f.id) }, - }, { - // 自分ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライ - '_reply.userId': user._id, - }, { - // 自分ã¸ã®ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ãŒå«ã¾ã‚Œã¦ã„ã‚‹ - mentions: { $in: [ user._id ] } - }] - }]; - - const query = { - $and: [{ - deletedAt: null, - }, { - $or: visibleQuery, - }], - - $or: [{ - mentions: user._id - }, { - visibleUserIds: user._id - }] - } as any; - - // éš ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å–å¾— - const hideUserIds = await getHideUserIds(user); - - if (hideUserIds && hideUserIds.length > 0) { - query.userId = { - $nin: hideUserIds - }; - - query['_reply.userId'] = { - $nin: hideUserIds - }; - - query['_renote.userId'] = { - $nin: hideUserIds - }; - } + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: user.id }); + + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(new Brackets(qb => { qb + .where(`:meId = ANY(note.mentions)`, { meId: user.id }) + .orWhere(`:meId = ANY(note.visibleUserIds)`, { meId: user.id }); + })) + .leftJoinAndSelect('note.user', 'user'); - const sort = { - _id: -1 - }; + generateVisibilityQuery(query, user); + generateMuteQuery(query, user); if (ps.visibility) { - query.visibility = ps.visibility; + query.andWhere('note.visibility = :visibility', { visibility: ps.visibility }); } if (ps.following) { - const followingIds = await getFriendIds(user._id); - - query.userId = { - $in: followingIds - }; - } - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; + query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id }); + query.setParameters(followingQuery.getParameters()); } - const mentions = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const mentions = await query.take(ps.limit).getMany(); for (const note of mentions) { - read(user._id, note._id); + read(user.id, note.id); } - return await packMany(mentions, user); + return await Notes.packMany(mentions, user); }); diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts index 9adabdf0e917875a84e2a5470591f6c4ead9a8bf..ff838d4f4fddff0718e1f9fb849daed977be1a62 100644 --- a/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/src/server/api/endpoints/notes/polls/recommendation.ts @@ -1,8 +1,7 @@ import $ from 'cafy'; -import Vote from '../../../../../models/poll-vote'; -import Note, { pack } from '../../../../../models/note'; import define from '../../../define'; -import { getHideUserIds } from '../../../common/get-hide-users'; +import { Polls, Mutings, Notes, PollVotes } from '../../../../../models'; +import { Brackets, In } from 'typeorm'; export const meta = { desc: { @@ -28,51 +27,46 @@ export const meta = { }; export default define(meta, async (ps, user) => { - // Get votes - const votes = await Vote.find({ - userId: user._id - }, { - fields: { - _id: false, - noteId: true - } - }); + const query = Polls.createQueryBuilder('poll') + .where('poll.userHost IS NULL') + .andWhere(`poll.userId != :meId`, { meId: user.id }) + .andWhere(`poll.noteVisibility = 'public'`) + .andWhere(new Brackets(qb => { qb + .where('poll.expiresAt IS NULL') + .orWhere('poll.expiresAt > :now', { now: new Date() }); + })); - const nin = votes && votes.length != 0 ? votes.map(v => v.noteId) : []; + //#region exclude arleady voted polls + const votedQuery = PollVotes.createQueryBuilder('vote') + .select('vote.noteId') + .where('vote.userId = :meId', { meId: user.id }); - // éš ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å–å¾— - const hideUserIds = await getHideUserIds(user); + query + .andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`); - const notes = await Note.find({ - '_user.host': null, - _id: { - $nin: nin - }, - userId: { - $ne: user._id, - $nin: hideUserIds - }, - visibility: 'public', - poll: { - $exists: true, - $ne: null - }, - $or: [{ - 'poll.expiresAt': null - }, { - 'poll.expiresAt': { - $gt: new Date() - } - }], - }, { - limit: ps.limit, - skip: ps.offset, - sort: { - _id: -1 - } + query.setParameters(votedQuery.getParameters()); + //#endregion + + //#region mute + const mutingQuery = Mutings.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: user.id }); + + query + .andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`); + + query.setParameters(mutingQuery.getParameters()); + //#endregion + + const polls = await query.take(ps.limit).skip(ps.offset).getMany(); + + if (polls.length === 0) return []; + + const notes = await Notes.find({ + id: In(polls.map(poll => poll.noteId)) }); - return await Promise.all(notes.map(note => pack(note, user, { + return await Notes.packMany(notes, user, { detail: true - }))); + }); }); diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts index ed20e0221f420db4e39c83876e73c22a8f2385a1..7d0ed6e4f9cd6652ab690dd55880ae00e2c7567a 100644 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -1,19 +1,19 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Vote from '../../../../../models/poll-vote'; -import Note from '../../../../../models/note'; -import Watching from '../../../../../models/note-watching'; +import { ID } from '../../../../../misc/cafy-id'; import watch from '../../../../../services/note/watch'; import { publishNoteStream } from '../../../../../services/stream'; -import notify from '../../../../../services/create-notification'; +import { createNotification } from '../../../../../services/create-notification'; import define from '../../../define'; -import User, { IRemoteUser } from '../../../../../models/user'; import { ApiError } from '../../../error'; import { getNote } from '../../../common/getters'; import { deliver } from '../../../../../queue'; import { renderActivity } from '../../../../../remote/activitypub/renderer'; import renderVote from '../../../../../remote/activitypub/renderer/vote'; import { deliverQuestionUpdate } from '../../../../../services/note/polls/update'; +import { PollVotes, NoteWatchings, Users, Polls } from '../../../../../models'; +import { Not } from 'typeorm'; +import { IRemoteUser } from '../../../../../models/entities/user'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { desc: { @@ -30,7 +30,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID' @@ -84,26 +83,28 @@ export default define(meta, async (ps, user) => { throw e; }); - if (note.poll == null) { + if (!note.hasPoll) { throw new ApiError(meta.errors.noPoll); } - if (note.poll.expiresAt && note.poll.expiresAt < createdAt) { + const poll = await Polls.findOne({ noteId: note.id }); + + if (poll.expiresAt && poll.expiresAt < createdAt) { throw new ApiError(meta.errors.alreadyExpired); } - if (!note.poll.choices.some(x => x.id == ps.choice)) { + if (poll.choices[ps.choice] == null) { throw new ApiError(meta.errors.invalidChoice); } // if already voted - const exist = await Vote.find({ - noteId: note._id, - userId: user._id + const exist = await PollVotes.find({ + noteId: note.id, + userId: user.id }); if (exist.length) { - if (note.poll.multiple) { + if (poll.multiple) { if (exist.some(x => x.choice == ps.choice)) throw new ApiError(meta.errors.alreadyVoted); } else { @@ -112,69 +113,54 @@ export default define(meta, async (ps, user) => { } // Create vote - const vote = await Vote.insert({ + const vote = await PollVotes.save({ + id: genId(), createdAt, - noteId: note._id, - userId: user._id, + noteId: note.id, + userId: user.id, choice: ps.choice }); - const inc: any = {}; - inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == ps.choice)}.votes`] = 1; - // Increment votes count - await Note.update({ _id: note._id }, { - $inc: inc - }); + const index = ps.choice + 1; // In SQL, array index is 1 based + await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE id = '${poll.id}'`); - publishNoteStream(note._id, 'pollVoted', { + publishNoteStream(note.id, 'pollVoted', { choice: ps.choice, - userId: user._id.toHexString() + userId: user.id }); // Notify - notify(note.userId, user._id, 'poll_vote', { - noteId: note._id, + createNotification(note.userId, user.id, 'pollVote', { + noteId: note.id, choice: ps.choice }); // Fetch watchers - Watching - .find({ - noteId: note._id, - userId: { $ne: user._id }, - // 削除ã•ã‚ŒãŸãƒ‰ã‚ュメントã¯é™¤ã - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - for (const watcher of watchers) { - notify(watcher.userId, user._id, 'poll_vote', { - noteId: note._id, - choice: ps.choice - }); - } - }); + NoteWatchings.find({ + noteId: note.id, + userId: Not(user.id), + }).then(watchers => { + for (const watcher of watchers) { + createNotification(watcher.userId, user.id, 'pollVote', { + noteId: note.id, + choice: ps.choice + }); + } + }); // ã“ã®æŠ•ç¨¿ã‚’Watchã™ã‚‹ - if (user.settings.autoWatch !== false) { - watch(user._id, note); + if (user.autoWatch !== false) { + watch(user.id, note); } // リモート投票ã®å ´åˆãƒªãƒ—ライé€ä¿¡ - if (note._user.host != null) { - const pollOwner: IRemoteUser = await User.findOne({ - _id: note.userId - }); + if (note.userHost != null) { + const pollOwner: IRemoteUser = await Users.findOne(note.userId); - deliver(user, renderActivity(await renderVote(user, vote, note, pollOwner)), pollOwner.inbox); + deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox); } // リモートフォãƒãƒ¯ãƒ¼ã«Updateé…ä¿¡ - deliverQuestionUpdate(note._id); - - return; + deliverQuestionUpdate(note.id); }); diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts index 7d977154f2b7bb8a38444302921424ba28a9c365..b1b5ca9d339f4b90ba08cabf7674d5116620c266 100644 --- a/src/server/api/endpoints/notes/reactions.ts +++ b/src/server/api/endpoints/notes/reactions.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import NoteReaction, { pack } from '../../../../models/note-reaction'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { getNote } from '../../common/getters'; import { ApiError } from '../../error'; +import { NoteReactions } from '../../../../models'; export const meta = { desc: { @@ -18,7 +18,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'The ID of the target note' @@ -37,12 +36,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, }, @@ -69,29 +66,17 @@ export default define(meta, async (ps, user) => { }); const query = { - noteId: note._id - } as any; - - const sort = { - _id: -1 + noteId: note.id }; - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } - - const reactions = await NoteReaction.find(query, { - limit: ps.limit, + const reactions = await NoteReactions.find({ + where: query, + take: ps.limit, skip: ps.offset, - sort: sort + order: { + id: -1 + } }); - return await Promise.all(reactions.map(reaction => pack(reaction, user))); + return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user))); }); diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts index 299ed302783a3d5a8583f61571af326ad165ca86..b6aa4c58f3a9d21938943d2d196fb44356604ada 100644 --- a/src/server/api/endpoints/notes/reactions/create.ts +++ b/src/server/api/endpoints/notes/reactions/create.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import createReaction from '../../../../../services/note/reaction/create'; import define from '../../../define'; import { getNote } from '../../../common/getters'; @@ -17,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'reaction-write', + kind: 'write:reactions', params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿' } diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts index 08442226c5dfaa3d37fb06ae6aa82f39c48103fd..0bdea58027926d9865767a29ab38934968c13a77 100644 --- a/src/server/api/endpoints/notes/reactions/delete.ts +++ b/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import * as ms from 'ms'; import deleteReaction from '../../../../../services/note/reaction/delete'; @@ -16,7 +16,7 @@ export const meta = { requireCredential: true, - kind: 'reaction-write', + kind: 'write:reactions', limit: { duration: ms('1hour'), @@ -27,7 +27,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID' diff --git a/src/server/api/endpoints/notes/renotes.ts b/src/server/api/endpoints/notes/renotes.ts index 15dcf55dcee245be79fc16deb4b29323f14ec506..81b899836de8fc7d539fb049d804babc911b11b9 100644 --- a/src/server/api/endpoints/notes/renotes.ts +++ b/src/server/api/endpoints/notes/renotes.ts @@ -1,9 +1,12 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note, { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { getNote } from '../../common/getters'; import { ApiError } from '../../error'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Notes } from '../../../../models'; export const meta = { desc: { @@ -18,7 +21,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID' @@ -32,12 +34,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, } }, @@ -63,29 +63,14 @@ export default define(meta, async (ps, user) => { throw e; }); - const sort = { - _id: -1 - }; + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(`note.renoteId = :renoteId`, { renoteId: note.id }) + .leftJoinAndSelect('note.user', 'user'); - const query = { - renoteId: note._id - } as any; + if (user) generateVisibilityQuery(query, user); + if (user) generateMuteQuery(query, user); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } - - const renotes = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const renotes = await query.take(ps.limit).getMany(); - return await packMany(renotes, user); + return await Notes.packMany(renotes, user); }); diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts index c80fd732056d5c131ab0f684483ee126be4051b5..09b0f17164de6ad5345df92589854bb5afbd5c97 100644 --- a/src/server/api/endpoints/notes/replies.ts +++ b/src/server/api/endpoints/notes/replies.ts @@ -1,9 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note, { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import { getFriends } from '../../common/get-friends'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { Notes } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; export const meta = { desc: { @@ -18,22 +19,30 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID' } }, + sinceId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šæ–°ã—ã„投稿をå–å¾—ã—ã¾ã™' + } + }, + + untilId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' + } + }, + limit: { validator: $.optional.num.range(1, 100), default: 10 }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - }, }, res: { @@ -45,54 +54,14 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const [followings, hideUserIds] = await Promise.all([ - // フォãƒãƒ¼ã‚’å–å¾— - // Fetch following - user ? getFriends(user._id) : [], - - // éš ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å–å¾— - getHideUserIds(user) - ]); - - const visibleQuery = user == null ? [{ - visibility: { $in: [ 'public', 'home' ] } - }] : [{ - visibility: { $in: [ 'public', 'home' ] } - }, { - // myself (for followers/specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ user._id ] } - }, { - visibility: 'followers', - $or: [{ - // フォãƒãƒ¯ãƒ¼ã®æŠ•ç¨¿ - userId: { $in: followings.map(f => f.id) }, - }, { - // 自分ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライ - '_reply.userId': user._id, - }, { - // 自分ã¸ã®ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ãŒå«ã¾ã‚Œã¦ã„ã‚‹ - mentions: { $in: [ user._id ] } - }] - }]; - - const q = { - replyId: ps.noteId, - $or: visibleQuery - } as any; + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.replyId = :replyId', { replyId: ps.noteId }) + .leftJoinAndSelect('note.user', 'user'); - if (hideUserIds && hideUserIds.length > 0) { - q['userId'] = { - $nin: hideUserIds - }; - } + if (user) generateVisibilityQuery(query, user); + if (user) generateMuteQuery(query, user); - const notes = await Note.find(q, { - limit: ps.limit, - skip: ps.offset - }); + const timeline = await query.take(ps.limit).getMany(); - return await packMany(notes, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/search-by-tag.ts b/src/server/api/endpoints/notes/search-by-tag.ts index b33c884049032f43da4a3b8991d4735887077a74..48de88d36e9e489176b57f500a6c3b6551db4e36 100644 --- a/src/server/api/endpoints/notes/search-by-tag.ts +++ b/src/server/api/endpoints/notes/search-by-tag.ts @@ -1,10 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { getFriendIds } from '../../common/get-friends'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Notes } from '../../../../models'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -28,16 +29,6 @@ export const meta = { } }, - following: { - validator: $.optional.nullable.bool, - default: null as any - }, - - mute: { - validator: $.optional.str, - default: 'mute_all' - }, - reply: { validator: $.optional.nullable.bool, default: null as any, @@ -61,44 +52,28 @@ export const meta = { } }, - media: { + poll: { validator: $.optional.nullable.bool, default: null as any, - deprecated: true, desc: { - 'ja-JP': 'ファイルãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã«é™å®šã™ã‚‹ã‹å¦ã‹ (ã“ã®ãƒ‘ラメータã¯å»ƒæ¢äºˆå®šã§ã™ã€‚代ã‚ã‚Šã« withFiles を使ã£ã¦ãã ã•ã„。)' + 'ja-JP': 'アンケートãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã«é™å®šã™ã‚‹ã‹å¦ã‹' } }, - poll: { - validator: $.optional.nullable.bool, - default: null as any, + sinceId: { + validator: $.optional.type(ID), desc: { - 'ja-JP': 'アンケートãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã«é™å®šã™ã‚‹ã‹å¦ã‹' + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šæ–°ã—ã„投稿をå–å¾—ã—ã¾ã™' } }, untilId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定ã™ã‚‹ã¨ã€ã“ã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' } }, - sinceDate: { - validator: $.optional.num, - }, - - untilDate: { - validator: $.optional.num, - }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - }, - limit: { validator: $.optional.num.range(1, 30), default: 10 @@ -114,226 +89,58 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const visibleQuery = me == null ? [{ - visibility: { $in: [ 'public', 'home' ] } - }] : [{ - visibility: { $in: [ 'public', 'home' ] } - }, { - // myself (for specified/private) - userId: me._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ me._id ] } - }]; - - const q: any = { - $and: [ps.tag ? { - tagsLower: ps.tag.toLowerCase() - } : { - $or: ps.query.map(tags => ({ - $and: tags.map(t => ({ - tagsLower: t.toLowerCase() - })) - })) - }], - deletedAt: { $exists: false }, - $or: visibleQuery - }; - - const push = (x: any) => q.$and.push(x); - - if (ps.following != null && me != null) { - const ids = await getFriendIds(me._id, false); - push({ - userId: ps.following ? { - $in: ids - } : { - $nin: ids.concat(me._id) - } - }); - } - - if (me != null) { - const hideUserIds = await getHideUserIds(me); - - switch (ps.mute) { - case 'mute_all': - push({ - userId: { - $nin: hideUserIds - }, - '_reply.userId': { - $nin: hideUserIds - }, - '_renote.userId': { - $nin: hideUserIds - } - }); - break; - case 'mute_related': - push({ - '_reply.userId': { - $nin: hideUserIds - }, - '_renote.userId': { - $nin: hideUserIds + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .leftJoinAndSelect('note.user', 'user'); + + if (me) generateVisibilityQuery(query, me); + if (me) generateMuteQuery(query, me); + + if (ps.tag) { + query.andWhere(':tag = ANY(note.tags)', { tag: ps.tag }); + } else { + let i = 0; + query.andWhere(new Brackets(qb => { + for (const tags of ps.query) { + qb.orWhere(new Brackets(qb => { + for (const tag of tags) { + qb.andWhere(`:tag${i} = ANY(note.tags)`, { [`tag${i}`]: tag }); + i++; } - }); - break; - case 'mute_direct': - push({ - userId: { - $nin: hideUserIds - } - }); - break; - case 'direct_only': - push({ - userId: { - $in: hideUserIds - } - }); - break; - case 'related_only': - push({ - $or: [{ - '_reply.userId': { - $in: hideUserIds - } - }, { - '_renote.userId': { - $in: hideUserIds - } - }] - }); - break; - case 'all_only': - push({ - $or: [{ - userId: { - $in: hideUserIds - } - }, { - '_reply.userId': { - $in: hideUserIds - } - }, { - '_renote.userId': { - $in: hideUserIds - } - }] - }); - break; - } + })); + } + })); } if (ps.reply != null) { if (ps.reply) { - push({ - replyId: { - $exists: true, - $ne: null - } - }); + query.andWhere('note.replyId IS NOT NULL'); } else { - push({ - $or: [{ - replyId: { - $exists: false - } - }, { - replyId: null - }] - }); + query.andWhere('note.replyId IS NULL'); } } if (ps.renote != null) { if (ps.renote) { - push({ - renoteId: { - $exists: true, - $ne: null - } - }); + query.andWhere('note.renoteId IS NOT NULL'); } else { - push({ - $or: [{ - renoteId: { - $exists: false - } - }, { - renoteId: null - }] - }); + query.andWhere('note.renoteId IS NULL'); } } - const withFiles = ps.withFiles != null ? ps.withFiles : ps.media; - - if (withFiles) { - push({ - fileIds: { $exists: true, $ne: [] } - }); + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } if (ps.poll != null) { if (ps.poll) { - push({ - poll: { - $exists: true, - $ne: null - } - }); + query.andWhere('note.hasPoll = TRUE'); } else { - push({ - $or: [{ - poll: { - $exists: false - } - }, { - poll: null - }] - }); + query.andWhere('note.hasPoll = FALSE'); } } - if (ps.untilId) { - push({ - _id: { - $lt: ps.untilId - } - }); - } - - if (ps.sinceDate) { - push({ - createdAt: { - $gt: new Date(ps.sinceDate) - } - }); - } - - if (ps.untilDate) { - push({ - createdAt: { - $lt: new Date(ps.untilDate) - } - }); - } - - if (q.$and.length == 0) { - delete q.$and; - } - // Search notes - const notes = await Note.find(q, { - sort: { - _id: -1 - }, - limit: ps.limit, - skip: ps.offset - }); + const notes = await query.take(ps.limit).getMany(); - return await packMany(notes, me); + return await Notes.packMany(notes, me); }); diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts index edc8a1456041c975b081254fbf9e015bb6419430..cc88fb9380ac7cc154b3c6fd7c3e47ab38f35cc1 100644 --- a/src/server/api/endpoints/notes/search.ts +++ b/src/server/api/endpoints/notes/search.ts @@ -1,10 +1,9 @@ import $ from 'cafy'; -import * as mongo from 'mongodb'; -import Note from '../../../../models/note'; -import { packMany } from '../../../../models/note'; import es from '../../../../db/elasticsearch'; import define from '../../define'; import { ApiError } from '../../error'; +import { Notes } from '../../../../models'; +import { In } from 'typeorm'; export const meta = { desc: { @@ -74,18 +73,19 @@ export default define(meta, async (ps, me) => { return []; } - const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id)); + const hits = response.hits.hits.map((hit: any) => hit.id); + + if (hits.length === 0) return []; // Fetch found notes - const notes = await Note.find({ - _id: { - $in: hits - } - }, { - sort: { - _id: -1 + const notes = await Notes.find({ + where: { + id: In(hits) + }, + order: { + id: -1 } }); - return await packMany(notes, me); + return await Notes.packMany(notes, me); }); diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts index 6d8dc73ff297955eed425b290584cc705f8ccabc..d41dc20c5498fe3e0194bf72f311f948bf05a685 100644 --- a/src/server/api/endpoints/notes/show.ts +++ b/src/server/api/endpoints/notes/show.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import { pack } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { getNote } from '../../common/getters'; import { ApiError } from '../../error'; +import { Notes } from '../../../../models'; export const meta = { stability: 'stable', @@ -20,7 +20,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID.' @@ -47,7 +46,7 @@ export default define(meta, async (ps, user) => { throw e; }); - return await pack(note, user, { + return await Notes.pack(note, user, { detail: true }); }); diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/social-timeline.ts similarity index 50% rename from src/server/api/endpoints/notes/hybrid-timeline.ts rename to src/server/api/endpoints/notes/social-timeline.ts index 9695547f0466b1002299f3558ef072af601a919d..10e215d6c4793c74654f86b285a5dc3f7d19fc53 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/social-timeline.ts @@ -1,17 +1,18 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { getFriends } from '../../common/get-friends'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; -import activeUsersChart from '../../../../services/chart/active-users'; -import { getHideUserIds } from '../../common/get-hide-users'; import { ApiError } from '../../error'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Followings, Notes } from '../../../../models'; +import { Brackets } from 'typeorm'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { activeUsersChart } from '../../../../services/chart'; export const meta = { desc: { - 'ja-JP': 'ãƒã‚¤ãƒ–リッドタイムラインをå–å¾—ã—ã¾ã™ã€‚' + 'ja-JP': 'ソーシャルタイムラインをå–å¾—ã—ã¾ã™ã€‚' }, tags: ['notes'], @@ -27,17 +28,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定ã™ã‚‹ã¨ã€ã“ã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šæ–°ã—ã„投稿をå–å¾—ã—ã¾ã™' + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šæ–°ã—ã„投稿をå–å¾—ã—ã¾ã™' } }, untilId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定ã™ã‚‹ã¨ã€ã“ã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' } }, @@ -85,14 +84,6 @@ export const meta = { 'ja-JP': 'true ã«ã™ã‚‹ã¨ã€ãƒ•ã‚¡ã‚¤ãƒ«ãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã ã‘å–å¾—ã—ã¾ã™' } }, - - mediaOnly: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'true ã«ã™ã‚‹ã¨ã€ãƒ•ã‚¡ã‚¤ãƒ«ãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã ã‘å–å¾—ã—ã¾ã™ (ã“ã®ãƒ‘ラメータã¯å»ƒæ¢äºˆå®šã§ã™ã€‚代ã‚ã‚Šã« withFiles を使ã£ã¦ãã ã•ã„。)' - } - }, }, res: { @@ -112,94 +103,30 @@ export const meta = { }; export default define(meta, async (ps, user) => { + // TODO ã©ã£ã‹ã«ã‚ャッシュ const m = await fetchMeta(); if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) { throw new ApiError(meta.errors.stlDisabled); } - const [followings, hideUserIds] = await Promise.all([ - // フォãƒãƒ¼ã‚’å–å¾— - // Fetch following - getFriends(user._id, true, false), - - // éš ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å–å¾— - getHideUserIds(user) - ]); - //#region Construct query - const sort = { - _id: -1 - }; - - const followQuery = followings.map(f => ({ - userId: f.id, - - /*// リプライã¯å«ã‚ãªã„(ãŸã ã—投稿者自身ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライã€è‡ªåˆ†ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライã€è‡ªåˆ†ã®ãƒªãƒ—ライã¯å«ã‚ã‚‹) - $or: [{ - // リプライã§ãªã„ - replyId: null - }, { // ã¾ãŸã¯ - // リプライã ãŒè¿”ä¿¡å…ˆãŒæŠ•ç¨¿è€…自身ã®æŠ•ç¨¿ - $expr: { - $eq: ['$_reply.userId', '$userId'] - } - }, { // ã¾ãŸã¯ - // リプライã ãŒè¿”ä¿¡å…ˆãŒè‡ªåˆ†(フォãƒãƒ¯ãƒ¼)ã®æŠ•ç¨¿ - '_reply.userId': user._id - }, { // ã¾ãŸã¯ - // 自分(フォãƒãƒ¯ãƒ¼)ãŒé€ä¿¡ã—ãŸãƒªãƒ—ライ - userId: user._id - }]*/ - })); - - const visibleQuery = user == null ? [{ - visibility: { $in: ['public', 'home'] } - }] : [{ - visibility: { $in: ['public', 'home', 'followers'] } - }, { - // myself (for specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ user._id ] } - }]; - - const query = { - $and: [{ - deletedAt: null, - - $or: [{ - $and: [{ - // フォãƒãƒ¼ã—ã¦ã„る人ã®æŠ•ç¨¿ - $or: followQuery - }, { - // visible for me - $or: visibleQuery - }] - }, { - // public only - visibility: 'public', - - // リプライã§ãªã„ - //replyId: null, - - // local - '_user.host': null - }], - - // hide - userId: { - $nin: hideUserIds - }, - '_reply.userId': { - $nin: hideUserIds - }, - '_renote.userId': { - $nin: hideUserIds - }, - }] - } as any; - + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: user.id }); + + const query = makePaginationQuery(Notes.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere(new Brackets(qb => { + qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id }) + .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); + })) + .leftJoinAndSelect('note.user', 'user') + .setParameters(followingQuery.getParameters()); + + generateVisibilityQuery(query, user); + generateMuteQuery(query, user); + + /* TODO // MongoDBã§ã¯ãƒˆãƒƒãƒ—レベルã§å¦å®šãŒã§ããªã„ãŸã‚ã€De Morganã®æ³•å‰‡ã‚’利用ã—ã¦ã‚¯ã‚¨ãƒªã—ã¾ã™ã€‚ // ã¤ã¾ã‚Šã€ã€Œã€Žè‡ªåˆ†ã®æŠ•ç¨¿ã‹ã¤Renoteã€ã§ã¯ãªã„ã€ã‚’「『自分ã®æŠ•ç¨¿ã§ã¯ãªã„ã€ã¾ãŸã¯ã€ŽRenoteã§ã¯ãªã„ã€ã€ã¨è¡¨ç¾ã—ã¾ã™ã€‚ // for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws @@ -207,7 +134,7 @@ export default define(meta, async (ps, user) => { if (ps.includeMyRenotes === false) { query.$and.push({ $or: [{ - userId: { $ne: user._id } + userId: { $ne: user.id } }, { renoteId: null }, { @@ -223,7 +150,7 @@ export default define(meta, async (ps, user) => { if (ps.includeRenotedMyNotes === false) { query.$and.push({ $or: [{ - '_renote.userId': { $ne: user._id } + '_renote.userId': { $ne: user.id } }, { renoteId: null }, { @@ -251,40 +178,18 @@ export default define(meta, async (ps, user) => { }] }); } + */ - if (ps.withFiles || ps.mediaOnly) { - query.$and.push({ - fileIds: { $exists: true, $ne: [] } - }); - } - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - query.createdAt = { - $lt: new Date(ps.untilDate) - }; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } //#endregion - const timeline = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const timeline = await query.take(ps.limit).getMany(); - activeUsersChart.update(user); + if (user) { + activeUsersChart.update(user); + } - return await packMany(timeline, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/state.ts b/src/server/api/endpoints/notes/state.ts index 4944802849c0b5cbac04bf69edb5448991f84b3a..df1d9d9fb0d42c136763dc040d1d0bf9e1a069f3 100644 --- a/src/server/api/endpoints/notes/state.ts +++ b/src/server/api/endpoints/notes/state.ts @@ -1,8 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import Favorite from '../../../../models/favorite'; -import NoteWatching from '../../../../models/note-watching'; +import { NoteFavorites, NoteWatchings } from '../../../../models'; export const meta = { stability: 'stable', @@ -19,7 +18,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID.' @@ -30,17 +28,19 @@ export const meta = { export default define(meta, async (ps, user) => { const [favorite, watching] = await Promise.all([ - Favorite.count({ - userId: user._id, + NoteFavorites.count({ + where: { + userId: user.id, noteId: ps.noteId - }, { - limit: 1 + }, + take: 1 }), - NoteWatching.count({ - userId: user._id, + NoteWatchings.count({ + where: { + userId: user.id, noteId: ps.noteId - }, { - limit: 1 + }, + take: 1 }) ]); diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 6ff7690c74303e7dcdd9bea15b114d276a35bf1e..e22db4d1b0ee889c042129349c4f8f872c3a005d 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -1,11 +1,12 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { getFriends } from '../../common/get-friends'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import activeUsersChart from '../../../../services/chart/active-users'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Notes, Followings } from '../../../../models'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { activeUsersChart } from '../../../../services/chart'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -28,17 +29,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定ã™ã‚‹ã¨ã€ã“ã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šæ–°ã—ã„投稿をå–å¾—ã—ã¾ã™' + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šæ–°ã—ã„投稿をå–å¾—ã—ã¾ã™' } }, untilId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定ã™ã‚‹ã¨ã€ã“ã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' } }, @@ -86,14 +85,6 @@ export const meta = { 'ja-JP': 'true ã«ã™ã‚‹ã¨ã€ãƒ•ã‚¡ã‚¤ãƒ«ãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã ã‘å–å¾—ã—ã¾ã™' } }, - - mediaOnly: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'true ã«ã™ã‚‹ã¨ã€ãƒ•ã‚¡ã‚¤ãƒ«ãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã ã‘å–å¾—ã—ã¾ã™ (ã“ã®ãƒ‘ラメータã¯å»ƒæ¢äºˆå®šã§ã™ã€‚代ã‚ã‚Šã« withFiles を使ã£ã¦ãã ã•ã„。)' - } - }, }, res: { @@ -105,78 +96,24 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const [followings, hideUserIds] = await Promise.all([ - // フォãƒãƒ¼ã‚’å–å¾— - // Fetch following - getFriends(user._id), - - // éš ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å–å¾— - getHideUserIds(user) - ]); - //#region Construct query - const sort = { - _id: -1 - }; - - const followQuery = followings.map(f => ({ - userId: f.id, - - /*// リプライã¯å«ã‚ãªã„(ãŸã ã—投稿者自身ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライã€è‡ªåˆ†ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライã€è‡ªåˆ†ã®ãƒªãƒ—ライã¯å«ã‚ã‚‹) - $or: [{ - // リプライã§ãªã„ - replyId: null - }, { // ã¾ãŸã¯ - // リプライã ãŒè¿”ä¿¡å…ˆãŒæŠ•ç¨¿è€…自身ã®æŠ•ç¨¿ - $expr: { - $eq: ['$_reply.userId', '$userId'] - } - }, { // ã¾ãŸã¯ - // リプライã ãŒè¿”ä¿¡å…ˆãŒè‡ªåˆ†(フォãƒãƒ¯ãƒ¼)ã®æŠ•ç¨¿ - '_reply.userId': user._id - }, { // ã¾ãŸã¯ - // 自分(フォãƒãƒ¯ãƒ¼)ãŒé€ä¿¡ã—ãŸãƒªãƒ—ライ - userId: user._id - }]*/ - })); - - const visibleQuery = user == null ? [{ - visibility: { $in: [ 'public', 'home' ] } - }] : [{ - visibility: { $in: [ 'public', 'home', 'followers' ] } - }, { - // myself (for specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ user._id ] } - }]; - - const query = { - $and: [{ - deletedAt: null, - - $and: [{ - // フォãƒãƒ¼ã—ã¦ã„る人ã®æŠ•ç¨¿ - $or: followQuery - }, { - // visible for me - $or: visibleQuery - }], - - // mute - userId: { - $nin: hideUserIds - }, - '_reply.userId': { - $nin: hideUserIds - }, - '_renote.userId': { - $nin: hideUserIds - }, - }] - } as any; - + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: user.id }); + + const query = makePaginationQuery(Notes.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere(new Brackets(qb => { qb + .where(`note.userId IN (${ followingQuery.getQuery() })`) + .orWhere('note.userId = :meId', { meId: user.id }); + })) + .leftJoinAndSelect('note.user', 'user') + .setParameters(followingQuery.getParameters()); + + generateVisibilityQuery(query, user); + generateMuteQuery(query, user); + + /* v11 TODO // MongoDBã§ã¯ãƒˆãƒƒãƒ—レベルã§å¦å®šãŒã§ããªã„ãŸã‚ã€De Morganã®æ³•å‰‡ã‚’利用ã—ã¦ã‚¯ã‚¨ãƒªã—ã¾ã™ã€‚ // ã¤ã¾ã‚Šã€ã€Œã€Žè‡ªåˆ†ã®æŠ•ç¨¿ã‹ã¤Renoteã€ã§ã¯ãªã„ã€ã‚’「『自分ã®æŠ•ç¨¿ã§ã¯ãªã„ã€ã¾ãŸã¯ã€ŽRenoteã§ã¯ãªã„ã€ã€ã¨è¡¨ç¾ã—ã¾ã™ã€‚ // for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws @@ -184,7 +121,7 @@ export default define(meta, async (ps, user) => { if (ps.includeMyRenotes === false) { query.$and.push({ $or: [{ - userId: { $ne: user._id } + userId: { $ne: user.id } }, { renoteId: null }, { @@ -200,7 +137,7 @@ export default define(meta, async (ps, user) => { if (ps.includeRenotedMyNotes === false) { query.$and.push({ $or: [{ - '_renote.userId': { $ne: user._id } + '_renote.userId': { $ne: user.id } }, { renoteId: null }, { @@ -227,43 +164,16 @@ export default define(meta, async (ps, user) => { poll: { $ne: null } }] }); - } - - const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; - - if (withFiles) { - query.$and.push({ - fileIds: { $exists: true, $ne: [] } - }); - } + }*/ - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - query.createdAt = { - $lt: new Date(ps.untilDate) - }; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } //#endregion - const timeline = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const timeline = await query.take(ps.limit).getMany(); activeUsersChart.update(user); - return await packMany(timeline, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts index 17c24ab11969b2131a609642a3d642ce5bc99fe7..deda04acb468db53f15289ecd1eaae4c9b0387df 100644 --- a/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/src/server/api/endpoints/notes/user-list-timeline.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { packMany } from '../../../../models/note'; -import UserList from '../../../../models/user-list'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import { getFriends } from '../../common/get-friends'; -import { getHideUserIds } from '../../common/get-hide-users'; import { ApiError } from '../../error'; +import { UserLists, UserListJoinings, Notes } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { activeUsersChart } from '../../../../services/chart'; export const meta = { desc: { @@ -21,7 +20,6 @@ export const meta = { params: { listId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': 'リストã®ID' } @@ -37,17 +35,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定ã™ã‚‹ã¨ã€ã“ã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šæ–°ã—ã„投稿をå–å¾—ã—ã¾ã™' + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šæ–°ã—ã„投稿をå–å¾—ã—ã¾ã™' } }, untilId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定ã™ã‚‹ã¨ã€ã“ã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' } }, @@ -95,14 +91,6 @@ export const meta = { 'ja-JP': 'true ã«ã™ã‚‹ã¨ã€ãƒ•ã‚¡ã‚¤ãƒ«ãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã ã‘å–å¾—ã—ã¾ã™' } }, - - mediaOnly: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'true ã«ã™ã‚‹ã¨ã€ãƒ•ã‚¡ã‚¤ãƒ«ãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã ã‘å–å¾—ã—ã¾ã™ (ã“ã®ãƒ‘ラメータã¯å»ƒæ¢äºˆå®šã§ã™ã€‚代ã‚ã‚Šã« withFiles を使ã£ã¦ãã ã•ã„。)' - } - }, }, res: { @@ -122,94 +110,28 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const [list, followings, hideUserIds] = await Promise.all([ - // リストをå–å¾— - // Fetch the list - UserList.findOne({ - _id: ps.listId, - userId: user._id - }), - - // フォãƒãƒ¼ã‚’å–å¾— - // Fetch following - getFriends(user._id, true, false), - - // éš ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å–å¾— - getHideUserIds(user) - ]); + const list = await UserLists.findOne({ + id: ps.listId, + userId: user.id + }); if (list == null) { throw new ApiError(meta.errors.noSuchList); } - if (list.userIds.length == 0) { - return []; - } - //#region Construct query - const sort = { - _id: -1 - }; - - const listQuery = list.userIds.map(u => ({ - userId: u, - - /*// リプライã¯å«ã‚ãªã„(ãŸã ã—投稿者自身ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライã€è‡ªåˆ†ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライã€è‡ªåˆ†ã®ãƒªãƒ—ライã¯å«ã‚ã‚‹) - $or: [{ - // リプライã§ãªã„ - replyId: null - }, { // ã¾ãŸã¯ - // リプライã ãŒè¿”ä¿¡å…ˆãŒæŠ•ç¨¿è€…自身ã®æŠ•ç¨¿ - $expr: { - $eq: ['$_reply.userId', '$userId'] - } - }, { // ã¾ãŸã¯ - // リプライã ãŒè¿”ä¿¡å…ˆãŒè‡ªåˆ†(フォãƒãƒ¯ãƒ¼)ã®æŠ•ç¨¿ - '_reply.userId': user._id - }, { // ã¾ãŸã¯ - // 自分(フォãƒãƒ¯ãƒ¼)ãŒé€ä¿¡ã—ãŸãƒªãƒ—ライ - userId: user._id - }]*/ - })); - - const visibleQuery = [{ - visibility: { $in: ['public', 'home'] } - }, { - // myself (for specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [user._id] } - }, { - visibility: 'followers', - userId: { $in: followings.map(f => f.id) } - }]; - - const query = { - $and: [{ - deletedAt: null, + const listQuery = UserListJoinings.createQueryBuilder('joining') + .select('joining.userId') + .where('joining.userListId = :userListId', { userListId: list.id }); - $and: [{ - // リストã«å…¥ã£ã¦ã„る人ã®ã‚¿ã‚¤ãƒ ラインã¸ã®æŠ•ç¨¿ - $or: listQuery - }, { - // visible for me - $or: visibleQuery - }], + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(`note.userId IN (${ listQuery.getQuery() })`) + .leftJoinAndSelect('note.user', 'user') + .setParameters(listQuery.getParameters()); - // mute - userId: { - $nin: hideUserIds - }, - '_reply.userId': { - $nin: hideUserIds - }, - '_renote.userId': { - $nin: hideUserIds - }, - }] - } as any; + generateVisibilityQuery(query, user); + /* TODO // MongoDBã§ã¯ãƒˆãƒƒãƒ—レベルã§å¦å®šãŒã§ããªã„ãŸã‚ã€De Morganã®æ³•å‰‡ã‚’利用ã—ã¦ã‚¯ã‚¨ãƒªã—ã¾ã™ã€‚ // ã¤ã¾ã‚Šã€ã€Œã€Žè‡ªåˆ†ã®æŠ•ç¨¿ã‹ã¤Renoteã€ã§ã¯ãªã„ã€ã‚’「『自分ã®æŠ•ç¨¿ã§ã¯ãªã„ã€ã¾ãŸã¯ã€ŽRenoteã§ã¯ãªã„ã€ã€ã¨è¡¨ç¾ã—ã¾ã™ã€‚ // for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws @@ -217,7 +139,7 @@ export default define(meta, async (ps, user) => { if (ps.includeMyRenotes === false) { query.$and.push({ $or: [{ - userId: { $ne: user._id } + userId: { $ne: user.id } }, { renoteId: null }, { @@ -233,7 +155,7 @@ export default define(meta, async (ps, user) => { if (ps.includeRenotedMyNotes === false) { query.$and.push({ $or: [{ - '_renote.userId': { $ne: user._id } + '_renote.userId': { $ne: user.id } }, { renoteId: null }, { @@ -260,41 +182,16 @@ export default define(meta, async (ps, user) => { poll: { $ne: null } }] }); - } - - const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; - - if (withFiles) { - query.$and.push({ - fileIds: { $exists: true, $ne: [] } - }); - } + }*/ - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - query.createdAt = { - $lt: new Date(ps.untilDate) - }; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } //#endregion - const timeline = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const timeline = await query.take(ps.limit).getMany(); + + activeUsersChart.update(user); - return await packMany(timeline, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/watching/create.ts b/src/server/api/endpoints/notes/watching/create.ts index 2b2de1bd3b76439e7338afb085469d0a31943ca1..b4045fe93c07ccf86161bc34a4b3b5539578f087 100644 --- a/src/server/api/endpoints/notes/watching/create.ts +++ b/src/server/api/endpoints/notes/watching/create.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import watch from '../../../../../services/note/watch'; import { getNote } from '../../../common/getters'; @@ -17,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID.' @@ -45,5 +44,5 @@ export default define(meta, async (ps, user) => { throw e; }); - await watch(user._id, note); + await watch(user.id, note); }); diff --git a/src/server/api/endpoints/notes/watching/delete.ts b/src/server/api/endpoints/notes/watching/delete.ts index 512db793ea85250939fdc93dc4077d565f05e102..a272ecc37d29e8d54ccd217b952187b914a383c8 100644 --- a/src/server/api/endpoints/notes/watching/delete.ts +++ b/src/server/api/endpoints/notes/watching/delete.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import unwatch from '../../../../../services/note/unwatch'; import { getNote } from '../../../common/getters'; @@ -17,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®æŠ•ç¨¿ã®ID', 'en-US': 'Target note ID.' @@ -45,5 +44,5 @@ export default define(meta, async (ps, user) => { throw e; }); - await unwatch(user._id, note); + await unwatch(user.id, note); }); diff --git a/src/server/api/endpoints/notifications/mark-all-as-read.ts b/src/server/api/endpoints/notifications/mark-all-as-read.ts index e5df648285006cd9ec04d0a47aa9e376caedbfb6..9f34a32e80acc9c7a900d6eb99257e933328a75b 100644 --- a/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,7 +1,6 @@ -import Notification from '../../../../models/notification'; import { publishMainStream } from '../../../../services/stream'; -import User from '../../../../models/user'; import define from '../../define'; +import { Notifications } from '../../../../models'; export const meta = { desc: { @@ -13,29 +12,18 @@ export const meta = { requireCredential: true, - kind: 'notification-write' + kind: 'write:notifications' }; export default define(meta, async (ps, user) => { // Update documents - await Notification.update({ - notifieeId: user._id, - isRead: false + await Notifications.update({ + notifieeId: user.id, + isRead: false, }, { - $set: { - isRead: true - } - }, { - multi: true - }); - - // Update flag - User.update({ _id: user._id }, { - $set: { - hasUnreadNotification: false - } + isRead: true }); // å…¨ã¦ã®é€šçŸ¥ã‚’èªã¿ã¾ã—ãŸã‚ˆã¨ã„ã†ã‚¤ãƒ™ãƒ³ãƒˆã‚’発行 - publishMainStream(user._id, 'readAllNotifications'); + publishMainStream(user.id, 'readAllNotifications'); }); diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts index 30c49cdd8601780c1c3510ec7c04fc94bbd5d7aa..f3ebaa16ad88cf8874c8cf65ed8727708f3be96e 100644 --- a/src/server/api/endpoints/stats.ts +++ b/src/server/api/endpoints/stats.ts @@ -1,7 +1,6 @@ import define from '../define'; -import driveChart from '../../../services/chart/drive'; -import federationChart from '../../../services/chart/federation'; -import fetchMeta from '../../../misc/fetch-meta'; +import { Notes, Users } from '../../../models'; +import { federationChart, driveChart } from '../../../services/chart'; export const meta = { requireCredential: false, @@ -43,16 +42,17 @@ export const meta = { }; export default define(meta, async () => { - const instance = await fetchMeta(); - - const stats: any = instance.stats; - - const driveStats = await driveChart.getChart('hour', 1); - stats.driveUsageLocal = driveStats.local.totalSize[0]; - stats.driveUsageRemote = driveStats.remote.totalSize[0]; - - const federationStats = await federationChart.getChart('hour', 1); - stats.instances = federationStats.instance.total[0]; - - return stats; + const [notesCount, originalNotesCount, usersCount, originalUsersCount, instances, driveUsageLocal, driveUsageRemote] = await Promise.all([ + Notes.count(), + Notes.count({ userHost: null }), + Users.count(), + Users.count({ host: null }), + federationChart.getChart('hour', 1).then(chart => chart.instance.total[0]), + driveChart.getChart('hour', 1).then(chart => chart.local.totalSize[0]), + driveChart.getChart('hour', 1).then(chart => chart.remote.totalSize[0]), + ]); + + return { + notesCount, originalNotesCount, usersCount, originalUsersCount, instances, driveUsageLocal, driveUsageRemote + }; }); diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts index 0b81b06abe4496de94fa3f2e5e091711c3dc726b..cb0572aa902b60002f2d9bd918de494f1bbce5c8 100644 --- a/src/server/api/endpoints/sw/register.ts +++ b/src/server/api/endpoints/sw/register.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import Subscription from '../../../../models/sw-subscription'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; +import { genId } from '../../../../misc/gen-id'; +import { SwSubscriptions } from '../../../../models'; export const meta = { tags: ['account'], @@ -25,12 +26,11 @@ export const meta = { export default define(meta, async (ps, user) => { // if already subscribed - const exist = await Subscription.findOne({ - userId: user._id, + const exist = await SwSubscriptions.findOne({ + userId: user.id, endpoint: ps.endpoint, auth: ps.auth, publickey: ps.publickey, - deletedAt: { $exists: false } }); const instance = await fetchMeta(); @@ -42,8 +42,9 @@ export default define(meta, async (ps, user) => { }; } - await Subscription.insert({ - userId: user._id, + await SwSubscriptions.save({ + id: genId(), + userId: user.id, endpoint: ps.endpoint, auth: ps.auth, publickey: ps.publickey diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts index 1d098eb399ca9e3777650e66768a732f0fcf6e36..42ab1766524b8d4b8b170bd18e783457441f138c 100644 --- a/src/server/api/endpoints/username/available.ts +++ b/src/server/api/endpoints/username/available.ts @@ -1,7 +1,6 @@ import $ from 'cafy'; -import User from '../../../../models/user'; -import { validateUsername } from '../../../../models/user'; import define from '../../define'; +import { Users } from '../../../../models'; export const meta = { tags: ['users'], @@ -10,18 +9,16 @@ export const meta = { params: { username: { - validator: $.str.pipe(validateUsername) + validator: $.str.pipe(Users.validateUsername) } } }; export default define(meta, async (ps) => { // Get exist - const exist = await User.count({ + const exist = await Users.count({ host: null, usernameLower: ps.username.toLowerCase() - }, { - limit: 1 }); return { diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts index be83dcd9cc0b65c8ae11d625f3986d54d8123c7e..f99165f3d58dd1d0dd94afa637cec398e9b7f327 100644 --- a/src/server/api/endpoints/users.ts +++ b/src/server/api/endpoints/users.ts @@ -1,10 +1,7 @@ import $ from 'cafy'; -import User, { pack } from '../../../models/user'; import define from '../define'; -import { fallback } from '../../../prelude/symbol'; -import { getHideUserIds } from '../common/get-hide-users'; - -const nonnull = { $ne: null as any }; +import { Users } from '../../../models'; +import { generateMuteQueryForUsers } from '../common/generate-mute-query'; export const meta = { tags: ['users'], @@ -63,53 +60,38 @@ export const meta = { }, }; -const state: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - 'admin': { isAdmin: true }, - 'moderator': { isModerator: true }, - 'adminOrModerator': { - $or: [ - { isAdmin: true }, - { isModerator: true } - ] - }, - 'verified': { isVerified: true }, - 'alive': { - updatedAt: { $gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) } - }, - [fallback]: {} -}; +export default define(meta, async (ps, me) => { + const query = Users.createQueryBuilder('user'); -const origin: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - 'local': { host: null }, - 'remote': { host: nonnull }, - [fallback]: {} -}; + switch (ps.state) { + case 'admin': query.where('user.isAdmin = TRUE'); break; + case 'moderator': query.where('user.isModerator = TRUE'); break; + case 'adminOrModerator': query.where('user.isAdmin = TRUE OR isModerator = TRUE'); break; + case 'verified': query.where('user.isVerified = TRUE'); break; + case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; + } -const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - '+follower': { followersCount: -1 }, - '-follower': { followersCount: 1 }, - '+createdAt': { createdAt: -1 }, - '-createdAt': { createdAt: 1 }, - '+updatedAt': { updatedAt: -1 }, - '-updatedAt': { updatedAt: 1 }, - [fallback]: { _id: -1 } -}; + switch (ps.origin) { + case 'local': query.andWhere('user.host IS NULL'); break; + case 'remote': query.andWhere('user.host IS NOT NULL'); break; + } -export default define(meta, async (ps, me) => { - const hideUserIds = await getHideUserIds(me); - - const users = await User - .find({ - $and: [ - state[ps.state] || state[fallback], - origin[ps.origin] || origin[fallback] - ], - ...(hideUserIds && hideUserIds.length > 0 ? { _id: { $nin: hideUserIds } } : {}) - }, { - limit: ps.limit, - sort: sort[ps.sort] || sort[fallback], - skip: ps.offset - }); - - return await Promise.all(users.map(user => pack(user, me, { detail: true }))); + switch (ps.sort) { + case '+follower': query.orderBy('user.followersCount', 'DESC'); break; + case '-follower': query.orderBy('user.followersCount', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; + case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break; + case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; + default: query.orderBy('user.id', 'ASC'); break; + } + + if (me) generateMuteQueryForUsers(query, me); + + query.take(ps.limit); + query.skip(ps.offset); + + const users = await query.getMany(); + + return await Users.packMany(users, me, { detail: true }); }); diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts index 3c8290a8b15c9d6d4757416f80d9181cb4550cb1..51b007ddaa3f16a0be19f2be206ca14b5b3c1eb3 100644 --- a/src/server/api/endpoints/users/followers.ts +++ b/src/server/api/endpoints/users/followers.ts @@ -1,11 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import User from '../../../../models/user'; -import Following from '../../../../models/following'; -import { pack } from '../../../../models/user'; -import { getFriendIds } from '../../common/get-friends'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; +import { Users, Followings } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -20,7 +18,6 @@ export const meta = { params: { userId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -35,38 +32,25 @@ export const meta = { validator: $.optional.nullable.str }, - limit: { - validator: $.optional.num.range(1, 100), - default: 10 + sinceId: { + validator: $.optional.type(ID), }, - cursor: { + untilId: { validator: $.optional.type(ID), - default: null as any, - transform: transform, }, - iknow: { - validator: $.optional.bool, - default: false, - } + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, }, res: { - type: 'object', - properties: { - users: { - type: 'array', - items: { - type: 'User', - } - }, - next: { - type: 'string', - format: 'id', - nullable: true - } - } + type: 'array', + items: { + type: 'Following', + }, }, errors: { @@ -79,54 +63,20 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const q: any = ps.userId != null - ? { _id: ps.userId } - : { usernameLower: ps.username.toLowerCase(), host: ps.host }; - - const user = await User.findOne(q); + const user = await Users.findOne(ps.userId != null + ? { id: ps.userId } + : { usernameLower: ps.username.toLowerCase(), host: ps.host }); - if (user === null) { + if (user == null) { throw new ApiError(meta.errors.noSuchUser); } - const query = { - followeeId: user._id - } as any; - - // ãƒã‚°ã‚¤ãƒ³ã—ã¦ã„ã¦ã‹ã¤ iknow フラグãŒã‚ã‚‹ã¨ã - if (me && ps.iknow) { - // Get my friends - const myFriends = await getFriendIds(me._id); - - query.followerId = { - $in: myFriends - }; - } - - // カーソルãŒæŒ‡å®šã•ã‚Œã¦ã„ã‚‹å ´åˆ - if (ps.cursor) { - query._id = { - $lt: ps.cursor - }; - } - - // Get followers - const following = await Following - .find(query, { - limit: ps.limit + 1, - sort: { _id: -1 } - }); - - // 「次ã®ãƒšãƒ¼ã‚¸ã€ãŒã‚ã‚‹ã‹ã©ã†ã‹ - const inStock = following.length === ps.limit + 1; - if (inStock) { - following.pop(); - } + const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere(`following.followeeId = :userId`, { userId: user.id }); - const users = await Promise.all(following.map(f => pack(f.followerId, me, { detail: true }))); + const followings = await query + .take(ps.limit) + .getMany(); - return { - users: users, - next: inStock ? following[following.length - 1]._id : null, - }; + return await Followings.packMany(followings, me, { populateFollower: true }); }); diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts index 4bc740cad984a15a8b9470525c9596e825f7254f..46550f0f77a5dd28d612cb39a2e98607c8de1424 100644 --- a/src/server/api/endpoints/users/following.ts +++ b/src/server/api/endpoints/users/following.ts @@ -1,11 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import User from '../../../../models/user'; -import Following from '../../../../models/following'; -import { pack } from '../../../../models/user'; -import { getFriendIds } from '../../common/get-friends'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; +import { Users, Followings } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -20,7 +18,6 @@ export const meta = { params: { userId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -35,38 +32,25 @@ export const meta = { validator: $.optional.nullable.str }, - limit: { - validator: $.optional.num.range(1, 100), - default: 10 + sinceId: { + validator: $.optional.type(ID), }, - cursor: { + untilId: { validator: $.optional.type(ID), - default: null as any, - transform: transform, }, - iknow: { - validator: $.optional.bool, - default: false, - } + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, }, res: { - type: 'object', - properties: { - users: { - type: 'array', - items: { - type: 'User', - } - }, - next: { - type: 'string', - format: 'id', - nullable: true - } - } + type: 'array', + items: { + type: 'Following', + }, }, errors: { @@ -79,54 +63,20 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const q: any = ps.userId != null - ? { _id: ps.userId } - : { usernameLower: ps.username.toLowerCase(), host: ps.host }; - - const user = await User.findOne(q); + const user = await Users.findOne(ps.userId != null + ? { id: ps.userId } + : { usernameLower: ps.username.toLowerCase(), host: ps.host }); - if (user === null) { + if (user == null) { throw new ApiError(meta.errors.noSuchUser); } - const query = { - followerId: user._id - } as any; - - // ãƒã‚°ã‚¤ãƒ³ã—ã¦ã„ã¦ã‹ã¤ iknow フラグãŒã‚ã‚‹ã¨ã - if (me && ps.iknow) { - // Get my friends - const myFriends = await getFriendIds(me._id); - - query.followeeId = { - $in: myFriends - }; - } - - // カーソルãŒæŒ‡å®šã•ã‚Œã¦ã„ã‚‹å ´åˆ - if (ps.cursor) { - query._id = { - $lt: ps.cursor - }; - } - - // Get followers - const following = await Following - .find(query, { - limit: ps.limit + 1, - sort: { _id: -1 } - }); - - // 「次ã®ãƒšãƒ¼ã‚¸ã€ãŒã‚ã‚‹ã‹ã©ã†ã‹ - const inStock = following.length === ps.limit + 1; - if (inStock) { - following.pop(); - } + const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere(`following.followerId = :userId`, { userId: user.id }); - const users = await Promise.all(following.map(f => pack(f.followeeId, me, { detail: true }))); + const followings = await query + .take(ps.limit) + .getMany(); - return { - users: users, - next: inStock ? following[following.length - 1]._id : null, - }; + return await Followings.packMany(followings, me, { populateFollowee: true }); }); diff --git a/src/server/api/endpoints/users/get-frequently-replied-users.ts b/src/server/api/endpoints/users/get-frequently-replied-users.ts index 46c7fba2f61f8325b6ba499d0d59220df39b79f3..f82f437629b8d89c59b73cbd91c895a4656bcbff 100644 --- a/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { pack } from '../../../../models/user'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { maximum } from '../../../../prelude/array'; -import { getHideUserIds } from '../../common/get-hide-users'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Not, In } from 'typeorm'; +import { Notes, Users } from '../../../../models'; export const meta = { tags: ['users'], @@ -16,7 +15,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -53,21 +51,16 @@ export default define(meta, async (ps, me) => { }); // Fetch recent notes - const recentNotes = await Note.find({ - userId: user._id, - replyId: { - $exists: true, - $ne: null - } - }, { - sort: { - _id: -1 + const recentNotes = await Notes.find({ + where: { + userId: user.id, + replyId: Not(null) }, - limit: 1000, - fields: { - _id: false, - replyId: true - } + order: { + id: -1 + }, + take: 1000, + select: ['replyId'] }); // 投稿ãŒå°‘ãªã‹ã£ãŸã‚‰ä¸æ– @@ -75,21 +68,12 @@ export default define(meta, async (ps, me) => { return []; } - const hideUserIds = await getHideUserIds(me); - hideUserIds.push(user._id); - - const replyTargetNotes = await Note.find({ - _id: { - $in: recentNotes.map(p => p.replyId) + // TODO ミュートを考慮 + const replyTargetNotes = await Notes.find({ + where: { + id: In(recentNotes.map(p => p.replyId)), }, - userId: { - $nin: hideUserIds - } - }, { - fields: { - _id: false, - userId: true - } + select: ['userId'] }); const repliedUsers: any = {}; @@ -114,7 +98,7 @@ export default define(meta, async (ps, me) => { // Make replies object (includes weights) const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ - user: await pack(user, me, { detail: true }), + user: await Users.pack(user, me, { detail: true }), weight: repliedUsers[user] / peak }))); diff --git a/src/server/api/endpoints/users/lists/create.ts b/src/server/api/endpoints/users/lists/create.ts index 00d2538c9f67fb3d26ac186dc1e1dcac7960e53a..21dc6d331df11fe63a79e12265e5e1571ff899c0 100644 --- a/src/server/api/endpoints/users/lists/create.ts +++ b/src/server/api/endpoints/users/lists/create.ts @@ -1,6 +1,8 @@ import $ from 'cafy'; -import UserList, { pack } from '../../../../../models/user-list'; import define from '../../../define'; +import { UserLists } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; +import { UserList } from '../../../../../models/entities/user-list'; export const meta = { desc: { @@ -12,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { title: { @@ -22,12 +24,12 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const userList = await UserList.insert({ + const userList = await UserLists.save({ + id: genId(), createdAt: new Date(), - userId: user._id, - title: ps.title, - userIds: [] - }); + userId: user.id, + name: ps.title, + } as UserList); - return await pack(userList); + return await UserLists.pack(userList); }); diff --git a/src/server/api/endpoints/users/lists/delete.ts b/src/server/api/endpoints/users/lists/delete.ts index d8faaa928cf483cd4d7b382bef0c0cfa061ad739..0634bca4e3b04a4345bd535f4ec8a97cbda9907f 100644 --- a/src/server/api/endpoints/users/lists/delete.ts +++ b/src/server/api/endpoints/users/lists/delete.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import UserList from '../../../../../models/user-list'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { UserLists } from '../../../../../models'; export const meta = { desc: { @@ -14,12 +14,11 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { listId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã¨ãªã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒªã‚¹ãƒˆã®ID', 'en-US': 'ID of target user list' @@ -37,16 +36,14 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const userList = await UserList.findOne({ - _id: ps.listId, - userId: user._id + const userList = await UserLists.findOne({ + id: ps.listId, + userId: user.id }); if (userList == null) { throw new ApiError(meta.errors.noSuchList); } - await UserList.remove({ - _id: userList._id - }); + await UserLists.delete(userList.id); }); diff --git a/src/server/api/endpoints/users/lists/list.ts b/src/server/api/endpoints/users/lists/list.ts index ece2af5603c096f05a713e1be4a3c8a08766b30f..b05fc45527f9bcc6fd6183e522c2b0179b2f8830 100644 --- a/src/server/api/endpoints/users/lists/list.ts +++ b/src/server/api/endpoints/users/lists/list.ts @@ -1,5 +1,5 @@ -import UserList, { pack } from '../../../../../models/user-list'; import define from '../../../define'; +import { UserLists } from '../../../../../models'; export const meta = { desc: { @@ -10,7 +10,7 @@ export const meta = { requireCredential: true, - kind: 'account-read', + kind: 'read:account', res: { type: 'array', @@ -21,9 +21,9 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const userLists = await UserList.find({ - userId: me._id, + const userLists = await UserLists.find({ + userId: me.id, }); - return await Promise.all(userLists.map(x => pack(x))); + return await Promise.all(userLists.map(x => UserLists.pack(x))); }); diff --git a/src/server/api/endpoints/users/lists/pull.ts b/src/server/api/endpoints/users/lists/pull.ts index 0eee1975db44c7c5db8fd5bb983465a688e99bdb..524670b341703f5d3595339b656c16800c87adc6 100644 --- a/src/server/api/endpoints/users/lists/pull.ts +++ b/src/server/api/endpoints/users/lists/pull.ts @@ -1,11 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import UserList from '../../../../../models/user-list'; -import { pack as packUser } from '../../../../../models/user'; +import { ID } from '../../../../../misc/cafy-id'; import { publishUserListStream } from '../../../../../services/stream'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; +import { UserLists, UserListJoinings, Users } from '../../../../../models'; export const meta = { desc: { @@ -17,17 +16,15 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { listId: { validator: $.type(ID), - transform: transform, }, userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -52,9 +49,9 @@ export const meta = { export default define(meta, async (ps, me) => { // Fetch the list - const userList = await UserList.findOne({ - _id: ps.listId, - userId: me._id, + const userList = await UserLists.findOne({ + id: ps.listId, + userId: me.id, }); if (userList == null) { @@ -68,11 +65,7 @@ export default define(meta, async (ps, me) => { }); // Pull the user - await UserList.update({ _id: userList._id }, { - $pull: { - userIds: user._id - } - }); + await UserListJoinings.delete({ userId: user.id }); - publishUserListStream(userList._id, 'userRemoved', await packUser(user)); + publishUserListStream(userList.id, 'userRemoved', await Users.pack(user)); }); diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts index eea2f39a8cf7e8f54205318280acaa59200b765a..2763b3a19c62b648412f3498b6236066c2c36adc 100644 --- a/src/server/api/endpoints/users/lists/push.ts +++ b/src/server/api/endpoints/users/lists/push.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import UserList from '../../../../../models/user-list'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; import { pushUserToUserList } from '../../../../../services/user-list/push'; +import { UserLists, UserListJoinings } from '../../../../../models'; export const meta = { desc: { @@ -16,17 +16,15 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { listId: { validator: $.type(ID), - transform: transform, }, userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -57,9 +55,9 @@ export const meta = { export default define(meta, async (ps, me) => { // Fetch the list - const userList = await UserList.findOne({ - _id: ps.listId, - userId: me._id, + const userList = await UserLists.findOne({ + id: ps.listId, + userId: me.id, }); if (userList == null) { @@ -72,7 +70,12 @@ export default define(meta, async (ps, me) => { throw e; }); - if (userList.userIds.map(id => id.toHexString()).includes(user._id.toHexString())) { + const exist = await UserListJoinings.findOne({ + userListId: userList.id, + userId: user.id + }); + + if (exist) { throw new ApiError(meta.errors.alreadyAdded); } diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts index 0fab2fa4998700a756fb9a64acce3754b740b9c8..1a997ec7c5a52d8c9c27ba40c3c0b9319eb260c2 100644 --- a/src/server/api/endpoints/users/lists/show.ts +++ b/src/server/api/endpoints/users/lists/show.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import UserList, { pack } from '../../../../../models/user-list'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { UserLists } from '../../../../../models'; export const meta = { desc: { @@ -14,12 +14,11 @@ export const meta = { requireCredential: true, - kind: 'account-read', + kind: 'read:account', params: { listId: { validator: $.type(ID), - transform: transform, }, }, @@ -38,14 +37,14 @@ export const meta = { export default define(meta, async (ps, me) => { // Fetch the list - const userList = await UserList.findOne({ - _id: ps.listId, - userId: me._id, + const userList = await UserLists.findOne({ + id: ps.listId, + userId: me.id, }); if (userList == null) { throw new ApiError(meta.errors.noSuchList); } - return await pack(userList); + return await UserLists.pack(userList); }); diff --git a/src/server/api/endpoints/users/lists/update.ts b/src/server/api/endpoints/users/lists/update.ts index 58976931442dfc69cf22851260d6f25a9f8fc2cd..dc08d59f6a4a128777f89d6970e7be01cc17d1d9 100644 --- a/src/server/api/endpoints/users/lists/update.ts +++ b/src/server/api/endpoints/users/lists/update.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import UserList, { pack } from '../../../../../models/user-list'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { UserLists } from '../../../../../models'; export const meta = { desc: { @@ -14,19 +14,18 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { listId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã¨ãªã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒªã‚¹ãƒˆã®ID', 'en-US': 'ID of target user list' } }, - title: { + name: { validator: $.str.range(1, 100), desc: { 'ja-JP': 'ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒªã‚¹ãƒˆã®åå‰', @@ -46,20 +45,18 @@ export const meta = { export default define(meta, async (ps, user) => { // Fetch the list - const userList = await UserList.findOne({ - _id: ps.listId, - userId: user._id + const userList = await UserLists.findOne({ + id: ps.listId, + userId: user.id }); if (userList == null) { throw new ApiError(meta.errors.noSuchList); } - await UserList.update({ _id: userList._id }, { - $set: { - title: ps.title - } + await UserLists.update(userList.id, { + name: ps.name }); - return await pack(userList._id); + return await UserLists.pack(userList.id); }); diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts index 10d2f37fc2ea11c2facaff496e2427b4f5401d70..6df394cbb11ece42e5acbc2b5aaf17cb9be4dcc5 100644 --- a/src/server/api/endpoints/users/notes.ts +++ b/src/server/api/endpoints/users/notes.ts @@ -1,10 +1,13 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note, { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import Following from '../../../../models/following'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { Notes } from '../../../../models'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -16,7 +19,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -42,17 +44,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定ã™ã‚‹ã¨ã€ã“ã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šæ–°ã—ã„投稿をå–å¾—ã—ã¾ã™' + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šæ–°ã—ã„投稿をå–å¾—ã—ã¾ã™' } }, untilId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定ã™ã‚‹ã¨ã€ã“ã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' + 'ja-JP': '指定ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã‚’基点ã¨ã—ã¦ã‚ˆã‚Šå¤ã„投稿をå–å¾—ã—ã¾ã™' } }, @@ -102,15 +102,6 @@ export const meta = { } }, - mediaOnly: { - validator: $.optional.bool, - default: false, - deprecated: true, - desc: { - 'ja-JP': 'true ã«ã™ã‚‹ã¨ã€ãƒ•ã‚¡ã‚¤ãƒ«ãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã ã‘å–å¾—ã—ã¾ã™ (ã“ã®ãƒ‘ラメータã¯å»ƒæ¢äºˆå®šã§ã™ã€‚代ã‚ã‚Šã« withFiles を使ã£ã¦ãã ã•ã„。)' - } - }, - fileType: { validator: $.optional.arr($.str), desc: { @@ -150,67 +141,44 @@ export default define(meta, async (ps, me) => { throw e; }); - const isFollowing = me == null ? false : ((await Following.findOne({ - followerId: me._id, - followeeId: user._id - })) != null); - //#region Construct query - const sort = { } as any; + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.userId = :userId', { userId: user.id }) + .leftJoinAndSelect('note.user', 'user'); - const visibleQuery = me == null ? [{ - visibility: { $in: ['public', 'home'] } - }] : [{ - visibility: { - $in: isFollowing ? ['public', 'home', 'followers'] : ['public', 'home'] - } - }, { - // myself (for specified/private) - userId: me._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ me._id ] } - }]; + if (me) generateVisibilityQuery(query, me); + if (me) generateMuteQuery(query, me); + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } - const query = { - $and: [ {} ], - deletedAt: null, - userId: user._id, - $or: visibleQuery - } as any; + if (ps.fileType) { + query.andWhere('note.fileIds != \'{}\''); + query.andWhere(new Brackets(qb => { + for (const type of ps.fileType) { + const i = ps.fileType.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } + })); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - sort._id = -1; - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort.createdAt = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - sort.createdAt = -1; - query.createdAt = { - $lt: new Date(ps.untilDate) - }; - } else { - sort._id = -1; + if (ps.excludeNsfw) { + // v11 TODO + /*query['_files.isSensitive'] = { + $ne: true + };*/ + } } if (!ps.includeReplies) { - query.replyId = null; + query.andWhere('note.replyId IS NULL'); } + /* TODO if (ps.includeMyRenotes === false) { query.$and.push({ $or: [{ - userId: { $ne: user._id } + userId: { $ne: user.id } }, { renoteId: null }, { @@ -222,35 +190,11 @@ export default define(meta, async (ps, me) => { }] }); } + */ - const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; - - if (withFiles) { - query.fileIds = { - $exists: true, - $ne: [] - }; - } - - if (ps.fileType) { - query.fileIds = { $exists: true, $ne: [] }; - - query['_files.contentType'] = { - $in: ps.fileType - }; - - if (ps.excludeNsfw) { - query['_files.metadata.isSensitive'] = { - $ne: true - }; - } - } //#endregion - const notes = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const timeline = await query.take(ps.limit).getMany(); - return await packMany(notes, me); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index 60710fffca0e939d39c28d936263f06cbbbd6213..2c82d6613e915b584b2b0dd06c6da085b2e255e1 100644 --- a/src/server/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -1,14 +1,8 @@ import * as ms from 'ms'; import $ from 'cafy'; -import User, { pack, ILocalUser } from '../../../../models/user'; -import { getFriendIds } from '../../common/get-friends'; -import * as request from 'request-promise-native'; -import config from '../../../../config'; import define from '../../define'; -import fetchMeta from '../../../../misc/fetch-meta'; -import resolveUser from '../../../../remote/resolve-user'; -import { getHideUserIds } from '../../common/get-hide-users'; -import { apiLogger } from '../../logger'; +import { Users, Followings } from '../../../../models'; +import { generateMuteQueryForUsers } from '../../common/generate-mute-query'; export const meta = { desc: { @@ -19,7 +13,7 @@ export const meta = { requireCredential: true, - kind: 'account-read', + kind: 'read:account', params: { limit: { @@ -42,83 +36,24 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const instance = await fetchMeta(); + const query = Users.createQueryBuilder('user') + .where('user.isLocked = FALSE') + .where('user.host IS NULL') + .where('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) }) + .orderBy('user.followersCount', 'DESC'); - if (instance.enableExternalUserRecommendation) { - const userName = me.username; - const hostName = config.hostname; - const limit = ps.limit; - const offset = ps.offset; - const timeout = instance.externalUserRecommendationTimeout; - const engine = instance.externalUserRecommendationEngine; - const url = engine - .replace('{{host}}', hostName) - .replace('{{user}}', userName) - .replace('{{limit}}', limit.toString()) - .replace('{{offset}}', offset.toString()); + generateMuteQueryForUsers(query, me); - const users = await request({ - url: url, - proxy: config.proxy, - timeout: timeout, - json: true, - followRedirect: true, - followAllRedirects: true - }) - .then(body => convertUsers(body, me)); + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); - return users; - } else { - // ID list of the user itself and other users who the user follows - const followingIds = await getFriendIds(me._id); + query + .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`); - // éš ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å–å¾— - const hideUserIds = await getHideUserIds(me); + query.setParameters(followingQuery.getParameters()); - const users = await User.find({ - _id: { - $nin: followingIds.concat(hideUserIds) - }, - isLocked: { $ne: true }, - updatedAt: { - $gte: new Date(Date.now() - ms('7days')) - }, - host: null - }, { - limit: ps.limit, - skip: ps.offset, - sort: { - followersCount: -1 - } - }); + const users = await query.take(ps.limit).skip(ps.offset).getMany(); - return await Promise.all(users.map(user => pack(user, me, { detail: true }))); - } + return await Users.packMany(users, me, { detail: true }); }); - -type IRecommendUser = { - name: string; - username: string; - host: string; - description: string; - avatarUrl: string; -}; - -/** - * Resolve/Pack dummy users - */ -async function convertUsers(src: IRecommendUser[], me: ILocalUser) { - const packed = await Promise.all(src.map(async x => { - const user = await resolveUser(x.username, x.host) - .catch(() => { - apiLogger.warn(`Can't resolve ${x.username}@${x.host}`); - return null; - }); - - if (user == null) return x; - - return await pack(user, me, { detail: true }); - })); - - return packed; -} diff --git a/src/server/api/endpoints/users/relation.ts b/src/server/api/endpoints/users/relation.ts index f4121aa0d0b46dee99aad510ba3ca68116e2c1d3..4971738d32038b5b69f5a3c8c25ed31b61141f27 100644 --- a/src/server/api/endpoints/users/relation.ts +++ b/src/server/api/endpoints/users/relation.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform, ObjectId } from '../../../../misc/cafy-id'; -import { getRelation } from '../../../../models/user'; import define from '../../define'; +import { ID } from '../../../../misc/cafy-id'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -15,7 +15,6 @@ export const meta = { params: { userId: { validator: $.either($.type(ID), $.arr($.type(ID)).unique()), - transform: (v: any): ObjectId | ObjectId[] => Array.isArray(v) ? v.map(x => transform(x)) : transform(v), desc: { 'ja-JP': 'ユーザーID (é…列ã§ã‚‚å¯)' } @@ -26,7 +25,7 @@ export const meta = { export default define(meta, async (ps, me) => { const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; - const relations = await Promise.all(ids.map(id => getRelation(me._id, id))); + const relations = await Promise.all(ids.map(id => Users.getRelation(me.id, id))); return Array.isArray(ps.userId) ? relations : relations[0]; }); diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts index 0f23f8f0c3886ba65ba3b0c74f2e1cfa188bb8d6..2ee28c90023e4dcc1f0cc6fe3166b3b1dedaea3b 100644 --- a/src/server/api/endpoints/users/report-abuse.ts +++ b/src/server/api/endpoints/users/report-abuse.ts @@ -1,11 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; -import AbuseUserReport from '../../../../models/abuse-user-report'; import { publishAdminStream } from '../../../../services/stream'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { AbuseUserReports, Users } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; export const meta = { desc: { @@ -19,7 +19,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -62,7 +61,7 @@ export default define(meta, async (ps, me) => { throw e; }); - if (user._id.equals(me._id)) { + if (user.id === me.id) { throw new ApiError(meta.errors.cannotReportYourself); } @@ -70,17 +69,18 @@ export default define(meta, async (ps, me) => { throw new ApiError(meta.errors.cannotReportAdmin); } - const report = await AbuseUserReport.insert({ + const report = await AbuseUserReports.save({ + id: genId(), createdAt: new Date(), - userId: user._id, - reporterId: me._id, + userId: user.id, + reporterId: me.id, comment: ps.comment }); // Publish event to moderators setTimeout(async () => { - const moderators = await User.find({ - $or: [{ + const moderators = await Users.find({ + where: [{ isAdmin: true }, { isModerator: true @@ -88,8 +88,8 @@ export default define(meta, async (ps, me) => { }); for (const moderator of moderators) { - publishAdminStream(moderator._id, 'newAbuseUserReport', { - id: report._id, + publishAdminStream(moderator.id, 'newAbuseUserReport', { + id: report.id, userId: report.userId, reporterId: report.reporterId, comment: report.comment diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts index a95f6df6de195b0802e6a091c556392e382fb2d6..2e76546ade6297da32257e4abf25883fc10fe4d8 100644 --- a/src/server/api/endpoints/users/search.ts +++ b/src/server/api/endpoints/users/search.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import * as escapeRegexp from 'escape-regexp'; -import User, { pack, validateUsername, IUser } from '../../../../models/user'; import define from '../../define'; +import { Users } from '../../../../models'; +import { User } from '../../../../models/entities/user'; export const meta = { desc: { @@ -62,34 +62,30 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const isUsername = validateUsername(ps.query.replace('@', ''), !ps.localOnly); + const isUsername = Users.validateUsername(ps.query.replace('@', ''), !ps.localOnly); - let users: IUser[] = []; + let users: User[] = []; if (isUsername) { - users = await User - .find({ - host: null, - usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())), - isSuspended: { $ne: true } - }, { - limit: ps.limit, - skip: ps.offset - }); + users = await Users.createQueryBuilder('user') + .where('user.host IS NULL') + .where('user.isSuspended = FALSE') + .where('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) + .take(ps.limit) + .skip(ps.offset) + .getMany(); if (users.length < ps.limit && !ps.localOnly) { - const otherUsers = await User - .find({ - host: { $ne: null }, - usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())), - isSuspended: { $ne: true } - }, { - limit: ps.limit - users.length - }); + const otherUsers = await Users.createQueryBuilder('user') + .where('user.host IS NOT NULL') + .where('user.isSuspended = FALSE') + .where('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) + .take(ps.limit - users.length) + .getMany(); users = users.concat(otherUsers); } } - return await Promise.all(users.map(user => pack(user, me, { detail: ps.detail }))); + return await Users.packMany(users, me, { detail: ps.detail }); }); diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts index 4e59945eba19bf1d72108b1e0994dfadae6a6a71..a605eaf30aa35eb27e1ead99131d27effe5ca771 100644 --- a/src/server/api/endpoints/users/show.ts +++ b/src/server/api/endpoints/users/show.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform, transformMany } from '../../../../misc/cafy-id'; -import User, { pack, isRemoteUser } from '../../../../models/user'; import resolveRemoteUser from '../../../../remote/resolve-user'; import define from '../../define'; import { apiLogger } from '../../logger'; import { ApiError } from '../../error'; - -const cursorOption = { fields: { data: false } }; +import { ID } from '../../../../misc/cafy-id'; +import { Users } from '../../../../models'; +import { In } from 'typeorm'; export const meta = { desc: { @@ -20,7 +19,6 @@ export const meta = { params: { userId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': '対象ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ID', 'en-US': 'Target user ID' @@ -29,7 +27,6 @@ export const meta = { userIds: { validator: $.optional.arr($.type(ID)).unique(), - transform: transformMany, desc: { 'ja-JP': 'ユーザーID (é…列)' } @@ -68,42 +65,40 @@ export default define(meta, async (ps, me) => { let user; if (ps.userIds) { - const users = await User.find({ - _id: { - $in: ps.userIds - } + const users = await Users.find({ + id: In(ps.userIds) }); - return await Promise.all(users.map(u => pack(u, me, { + return await Promise.all(users.map(u => Users.pack(u, me, { detail: true }))); } else { // Lookup user if (typeof ps.host === 'string') { - user = await resolveRemoteUser(ps.username, ps.host, cursorOption).catch(e => { + user = await resolveRemoteUser(ps.username, ps.host).catch(e => { apiLogger.warn(`failed to resolve remote user: ${e}`); throw new ApiError(meta.errors.failedToResolveRemoteUser); }); } else { const q: any = ps.userId != null - ? { _id: ps.userId } + ? { id: ps.userId } : { usernameLower: ps.username.toLowerCase(), host: null }; - user = await User.findOne(q, cursorOption); + user = await Users.findOne(q); } - if (user === null) { + if (user == null) { throw new ApiError(meta.errors.noSuchUser); } // ãƒ¦ãƒ¼ã‚¶ãƒ¼æƒ…å ±æ›´æ–° - if (isRemoteUser(user)) { + if (Users.isRemoteUser(user)) { if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { resolveRemoteUser(ps.username, ps.host, { }, true); } } - return await pack(user, me, { + return await Users.pack(user, me, { detail: true }); } diff --git a/src/server/api/index.ts b/src/server/api/index.ts index fac57ca06ee9c59ece703ae548b18ea4b2bd3545..7858efd92793a7cde498f78330f1086e658636b4 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -15,8 +15,8 @@ import signin from './private/signin'; import discord from './service/discord'; import github from './service/github'; import twitter from './service/twitter'; -import Instance from '../../models/instance'; import { toASCII } from 'punycode'; +import { Instances } from '../../models'; // Init app const app = new Koa(); @@ -67,10 +67,9 @@ router.use(github.routes()); router.use(twitter.routes()); router.get('/v1/instance/peers', async ctx => { - const instances = await Instance.find({ - }, { - host: 1 - }); + const instances = await Instances.find({ + select: ['host'] + }); const punyCodes = instances.map(instance => toASCII(instance.host)); diff --git a/src/server/api/limiter.ts b/src/server/api/limiter.ts index 3d66172fd8bc316772ac1878226117b812d73232..e29c061337dcb9daea0512a603a47d2c879c7124 100644 --- a/src/server/api/limiter.ts +++ b/src/server/api/limiter.ts @@ -2,12 +2,12 @@ import * as Limiter from 'ratelimiter'; import limiterDB from '../../db/redis'; import { IEndpoint } from './endpoints'; import getAcct from '../../misc/acct/render'; -import { IUser } from '../../models/user'; +import { User } from '../../models/entities/user'; import Logger from '../../services/logger'; const logger = new Logger('limiter'); -export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) => { +export default (endpoint: IEndpoint, user: User) => new Promise((ok, reject) => { // RedisãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ãªã„å ´åˆã¯å¸¸ã«è¨±å¯ if (limiterDB == null) { ok(); @@ -38,7 +38,7 @@ export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) => // Short-term limit function min() { const minIntervalLimiter = new Limiter({ - id: `${user._id}:${key}:min`, + id: `${user.id}:${key}:min`, duration: limitation.minInterval, max: 1, db: limiterDB @@ -66,7 +66,7 @@ export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) => // Long term limit function max() { const limiter = new Limiter({ - id: `${user._id}:${key}`, + id: `${user.id}:${key}`, duration: limitation.duration, max: limitation.max, db: limiterDB diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts index 70a0d6faf008d590e0c71bf52262ee0eb1b1c780..5992fee835968a4ed732e1d6b355ec8782e914a6 100644 --- a/src/server/api/openapi/schemas.ts +++ b/src/server/api/openapi/schemas.ts @@ -221,7 +221,7 @@ export const schemas = { }, type: { type: 'string', - enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote'], + enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'], description: 'The type of the notification.' }, }, @@ -258,7 +258,7 @@ export const schemas = { description: 'The MD5 hash of this Drive file.', example: '15eca7fba0480996e2245f5185bf39f2' }, - datasize: { + size: { type: 'number', description: 'The size of this Drive file. (bytes)', example: 51469 @@ -275,7 +275,7 @@ export const schemas = { description: 'Whether this Drive file is sensitive.', }, }, - required: ['id', 'createdAt', 'name', 'type', 'datasize', 'md5'] + required: ['id', 'createdAt', 'name', 'type', 'size', 'md5'] }, DriveFolder: { @@ -318,6 +318,40 @@ export const schemas = { required: ['id', 'createdAt', 'name'] }, + Following: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'id', + description: 'The unique identifier for this following.', + example: 'xxxxxxxxxxxxxxxxxxxxxxxx', + }, + createdAt: { + type: 'string', + format: 'date-time', + description: 'The date that the following was created.' + }, + followeeId: { + type: 'string', + format: 'id', + }, + followee: { + $ref: '#/components/schemas/User', + description: 'The followee.' + }, + followerId: { + type: 'string', + format: 'id', + }, + follower: { + $ref: '#/components/schemas/User', + description: 'The follower.' + }, + }, + required: ['id', 'createdAt', 'followeeId', 'followerId'] + }, + Muting: { type: 'object', properties: { diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 40bcd2c5d6653dbf8b5bb4b90e67c5c4410812a1..c1fd908d8a402ad7af729ec09d89b5a8e7303051 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -1,11 +1,12 @@ import * as Koa from 'koa'; import * as bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; -import User, { ILocalUser } from '../../../models/user'; -import Signin, { pack } from '../../../models/signin'; import { publishMainStream } from '../../../services/stream'; import signin from '../common/signin'; import config from '../../../config'; +import { Users, Signins } from '../../../models'; +import { ILocalUser } from '../../../models/entities/user'; +import { genId } from '../../../misc/gen-id'; export default async (ctx: Koa.BaseContext) => { ctx.set('Access-Control-Allow-Origin', config.url); @@ -32,17 +33,12 @@ export default async (ctx: Koa.BaseContext) => { } // Fetch user - const user = await User.findOne({ + const user = await Users.findOne({ usernameLower: username.toLowerCase(), host: null - }, { - fields: { - data: false, - profile: false - } - }) as ILocalUser; + }) as ILocalUser; - if (user === null) { + if (user == null) { ctx.throw(404, { error: 'user not found' }); @@ -77,14 +73,15 @@ export default async (ctx: Koa.BaseContext) => { } // Append signin history - const record = await Signin.insert({ + const record = await Signins.save({ + id: genId(), createdAt: new Date(), - userId: user._id, + userId: user.id, ip: ctx.ip, headers: ctx.headers, success: same }); // Publish signin event - publishMainStream(user._id, 'signin', await pack(record)); + publishMainStream(user.id, 'signin', await Signins.pack(record)); }; diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index 89b7b330d21512fee4bd4e3f75a7c18502a02db7..1d304b8e11c996a3fe055f07bf9cf2c9d005a0d6 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -1,14 +1,15 @@ import * as Koa from 'koa'; import * as bcrypt from 'bcryptjs'; -import { generate as generateKeypair } from '../../../crypto_key'; -import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user'; +import { generateKeyPair } from 'crypto'; import generateUserToken from '../common/generate-native-user-token'; import config from '../../../config'; -import Meta from '../../../models/meta'; -import RegistrationTicket from '../../../models/registration-tickets'; -import usersChart from '../../../services/chart/users'; import fetchMeta from '../../../misc/fetch-meta'; import * as recaptcha from 'recaptcha-promise'; +import { Users, RegistrationTickets, UserServiceLinkings, UserKeypairs } from '../../../models'; +import { genId } from '../../../misc/gen-id'; +import { usersChart } from '../../../services/chart'; +import { UserServiceLinking } from '../../../models/entities/user-service-linking'; +import { User } from '../../../models/entities/user'; export default async (ctx: Koa.BaseContext) => { const body = ctx.request.body as any; @@ -32,6 +33,7 @@ export default async (ctx: Koa.BaseContext) => { const username = body['username']; const password = body['password']; + const host = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null; const invitationCode = body['invitationCode']; if (instance && instance.disableRegistration) { @@ -40,7 +42,7 @@ export default async (ctx: Koa.BaseContext) => { return; } - const ticket = await RegistrationTicket.findOne({ + const ticket = await RegistrationTickets.findOne({ code: invitationCode }); @@ -49,39 +51,22 @@ export default async (ctx: Koa.BaseContext) => { return; } - RegistrationTicket.remove({ - _id: ticket._id - }); + RegistrationTickets.delete(ticket.id); } // Validate username - if (!validateUsername(username)) { + if (!Users.validateUsername(username)) { ctx.status = 400; return; } // Validate password - if (!validatePassword(password)) { + if (!Users.validatePassword(password)) { ctx.status = 400; return; } - const usersCount = await User.count({}); - - // Fetch exist user that same username - const usernameExist = await User - .count({ - usernameLower: username.toLowerCase(), - host: null - }, { - limit: 1 - }); - - // Check username already used - if (usernameExist !== 0) { - ctx.status = 400; - return; - } + const usersCount = await Users.count({}); // Generate hash of password const salt = await bcrypt.genSalt(8); @@ -90,46 +75,50 @@ export default async (ctx: Koa.BaseContext) => { // Generate secret const secret = generateUserToken(); - // Create account - const account: IUser = await User.insert({ - avatarId: null, - bannerId: null, + if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) { + ctx.status = 400; + return; + } + + const account = await Users.save({ + id: genId(), createdAt: new Date(), - description: null, - followersCount: 0, - followingCount: 0, - name: null, - notesCount: 0, username: username, usernameLower: username.toLowerCase(), - host: null, - keypair: generateKeypair(), + host: host, token: secret, password: hash, isAdmin: config.autoAdmin && usersCount === 0, autoAcceptFollowed: true, - profile: { - bio: null, - birthday: null, - location: null - }, - settings: { - autoWatch: false - } + autoWatch: false + } as User); + + await UserKeypairs.save({ + id: genId(), + keyPem: await new Promise<string>((s, j) => generateKeyPair('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem', + cipher: undefined, + passphrase: undefined + } + }, (e, _, x) => e ? j(e) : s(x))), + userId: account.id }); - //#region Increment users count - Meta.update({}, { - $inc: { - 'stats.usersCount': 1, - 'stats.originalUsersCount': 1 - } - }, { upsert: true }); - //#endregion + await UserServiceLinkings.save({ + id: genId(), + userId: account.id + } as UserServiceLinking); usersChart.update(account, true); - const res = await pack(account, account, { + const res = await Users.pack(account, account, { detail: true, includeSecrets: true }); diff --git a/src/server/api/service/discord.ts b/src/server/api/service/discord.ts index 92f5bbf72d9e994be6378f2a493c41ad7f0547ea..4290e1ff9d9a3d60e8df705a1e59e9137c226c58 100644 --- a/src/server/api/service/discord.ts +++ b/src/server/api/service/discord.ts @@ -2,13 +2,14 @@ import * as Koa from 'koa'; import * as Router from 'koa-router'; import * as request from 'request'; import { OAuth2 } from 'oauth'; -import User, { pack, ILocalUser } from '../../../models/user'; import config from '../../../config'; import { publishMainStream } from '../../../services/stream'; import redis from '../../../db/redis'; import * as uuid from 'uuid'; import signin from '../common/signin'; import fetchMeta from '../../../misc/fetch-meta'; +import { Users, UserServiceLinkings } from '../../../models'; +import { ILocalUser } from '../../../models/entities/user'; function getUserToken(ctx: Koa.BaseContext) { return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; @@ -39,19 +40,27 @@ router.get('/disconnect/discord', async ctx => { return; } - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, - 'token': userToken + token: userToken + }); + + await UserServiceLinkings.update({ + userId: user.id }, { - $set: { - 'discord': null - } + discord: false, + discordAccessToken: null, + discordRefreshToken: null, + discordExpiresDate: null, + discordId: null, + discordUsername: null, + discordDiscriminator: null, }); ctx.body = `Discordã®é€£æºã‚’解除ã—ã¾ã—㟠:v:`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); @@ -193,32 +202,30 @@ router.get('/dc/cb', async ctx => { return; } - let user = await User.findOne({ - host: null, - 'discord.id': id - }) as ILocalUser; + const link = await UserServiceLinkings.createQueryBuilder() + .where('discord @> :discord', { + discord: { + id: id, + }, + }) + .andWhere('userHost IS NULL') + .getOne(); - if (!user) { + if (link == null) { ctx.throw(404, `@${username}#${discriminator}ã¨é€£æºã—ã¦ã„ã‚‹Misskeyアカウントã¯ã‚ã‚Šã¾ã›ã‚“ã§ã—ãŸ...`); return; } - user = await User.findOneAndUpdate({ - host: null, - 'discord.id': id - }, { - $set: { - discord: { - accessToken, - refreshToken, - expiresDate, - username, - discriminator - } - } - }) as ILocalUser; + await UserServiceLinkings.update(link.id, { + discord: true, + discordAccessToken: accessToken, + discordRefreshToken: refreshToken, + discordExpiresDate: expiresDate, + discordUsername: username, + discordDiscriminator: discriminator + }); - signin(ctx, user, true); + signin(ctx, await Users.findOne(link.userId) as ILocalUser, true); } else { const code = ctx.query.code; @@ -277,26 +284,25 @@ router.get('/dc/cb', async ctx => { return; } - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, token: userToken - }, { - $set: { - discord: { - accessToken, - refreshToken, - expiresDate, - id, - username, - discriminator - } - } + }); + + await UserServiceLinkings.update({ userId: user.id }, { + discord: true, + discordAccessToken: accessToken, + discordRefreshToken: refreshToken, + discordExpiresDate: expiresDate, + discordId: id, + discordUsername: username, + discordDiscriminator: discriminator }); ctx.body = `Discord: @${username}#${discriminator} ã‚’ã€Misskey: @${user.username} ã«æŽ¥ç¶šã—ã¾ã—ãŸï¼`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index cf3589a4b7136edf6f9d5c764b235995a15e9442..e59b149d19b1432ad4851d8a5f19d95b9569738a 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -2,13 +2,14 @@ import * as Koa from 'koa'; import * as Router from 'koa-router'; import * as request from 'request'; import { OAuth2 } from 'oauth'; -import User, { pack, ILocalUser } from '../../../models/user'; import config from '../../../config'; import { publishMainStream } from '../../../services/stream'; import redis from '../../../db/redis'; import * as uuid from 'uuid'; import signin from '../common/signin'; import fetchMeta from '../../../misc/fetch-meta'; +import { Users, UserServiceLinkings } from '../../../models'; +import { ILocalUser } from '../../../models/entities/user'; function getUserToken(ctx: Koa.BaseContext) { return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; @@ -39,19 +40,24 @@ router.get('/disconnect/github', async ctx => { return; } - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, - 'token': userToken + token: userToken + }); + + await UserServiceLinkings.update({ + userId: user.id }, { - $set: { - 'github': null - } + github: false, + githubAccessToken: null, + githubId: null, + githubLogin: null, }); ctx.body = `GitHubã®é€£æºã‚’解除ã—ã¾ã—㟠:v:`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); @@ -185,17 +191,21 @@ router.get('/gh/cb', async ctx => { return; } - const user = await User.findOne({ - host: null, - 'github.id': id - }) as ILocalUser; + const link = await UserServiceLinkings.createQueryBuilder() + .where('github @> :github', { + github: { + id: id, + }, + }) + .andWhere('userHost IS NULL') + .getOne(); - if (!user) { + if (link == null) { ctx.throw(404, `@${login}ã¨é€£æºã—ã¦ã„ã‚‹Misskeyアカウントã¯ã‚ã‚Šã¾ã›ã‚“ã§ã—ãŸ...`); return; } - signin(ctx, user, true); + signin(ctx, await Users.findOne(link.userId) as ILocalUser, true); } else { const code = ctx.query.code; @@ -248,23 +258,22 @@ router.get('/gh/cb', async ctx => { return; } - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, token: userToken - }, { - $set: { - github: { - accessToken, - id, - login - } - } + }); + + await UserServiceLinkings.update({ userId: user.id }, { + github: true, + githubAccessToken: accessToken, + githubId: id, + githubLogin: login, }); ctx.body = `GitHub: @${login} ã‚’ã€Misskey: @${user.username} ã«æŽ¥ç¶šã—ã¾ã—ãŸï¼`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts index fc23808e2156b81a8c0539b2f48d3d3d8ba052a8..77cf71395bf941868ed537231e7a330c37abdb73 100644 --- a/src/server/api/service/twitter.ts +++ b/src/server/api/service/twitter.ts @@ -3,11 +3,12 @@ import * as Router from 'koa-router'; import * as uuid from 'uuid'; import autwh from 'autwh'; import redis from '../../../db/redis'; -import User, { pack, ILocalUser } from '../../../models/user'; import { publishMainStream } from '../../../services/stream'; import config from '../../../config'; import signin from '../common/signin'; import fetchMeta from '../../../misc/fetch-meta'; +import { Users, UserServiceLinkings } from '../../../models'; +import { ILocalUser } from '../../../models/entities/user'; function getUserToken(ctx: Koa.BaseContext) { return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; @@ -38,19 +39,25 @@ router.get('/disconnect/twitter', async ctx => { return; } - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, - 'token': userToken + token: userToken + }); + + await UserServiceLinkings.update({ + userId: user.id }, { - $set: { - 'twitter': null - } + twitter: false, + twitterAccessToken: null, + twitterAccessTokenSecret: null, + twitterUserId: null, + twitterScreenName: null, }); ctx.body = `Twitterã®é€£æºã‚’解除ã—ã¾ã—㟠:v:`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); @@ -132,17 +139,21 @@ router.get('/tw/cb', async ctx => { const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier); - const user = await User.findOne({ - host: null, - 'twitter.userId': result.userId - }) as ILocalUser; + const link = await UserServiceLinkings.createQueryBuilder() + .where('twitter @> :twitter', { + twitter: { + userId: result.userId, + }, + }) + .andWhere('userHost IS NULL') + .getOne(); - if (user == null) { + if (link == null) { ctx.throw(404, `@${result.screenName}ã¨é€£æºã—ã¦ã„ã‚‹Misskeyアカウントã¯ã‚ã‚Šã¾ã›ã‚“ã§ã—ãŸ...`); return; } - signin(ctx, user, true); + signin(ctx, await Users.findOne(link.userId) as ILocalUser, true); } else { const verifier = ctx.query.oauth_verifier; @@ -161,24 +172,23 @@ router.get('/tw/cb', async ctx => { const result = await twAuth.done(JSON.parse(twCtx), verifier); - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, token: userToken - }, { - $set: { - twitter: { - accessToken: result.accessToken, - accessTokenSecret: result.accessTokenSecret, - userId: result.userId, - screenName: result.screenName - } - } + }); + + await UserServiceLinkings.update({ userId: user.id }, { + twitter: true, + twitterAccessToken: result.accessToken, + twitterAccessTokenSecret: result.accessTokenSecret, + twitterUserId: result.userId, + twitterScreenName: result.screenName, }); ctx.body = `Twitter: @${result.screenName} ã‚’ã€Misskey: @${user.username} ã«æŽ¥ç¶šã—ã¾ã—ãŸï¼`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts index bdbe4605cf8852b3a70ad931b9ff96a1845dc9b3..18fa6518201bb098b195a063475698c23610d811 100644 --- a/src/server/api/stream/channel.ts +++ b/src/server/api/stream/channel.ts @@ -15,6 +15,14 @@ export default abstract class Channel { return this.connection.user; } + protected get following() { + return this.connection.following; + } + + protected get muting() { + return this.connection.muting; + } + protected get subscriber() { return this.connection.subscriber; } diff --git a/src/server/api/stream/channels/admin.ts b/src/server/api/stream/channels/admin.ts index 6bcd1a7e0baf3db95d6361af28a51763c7a9fdce..e2eba10f7867fe5073d67369a2ad8e7b0801774e 100644 --- a/src/server/api/stream/channels/admin.ts +++ b/src/server/api/stream/channels/admin.ts @@ -9,7 +9,7 @@ export default class extends Channel { @autobind public async init(params: any) { // Subscribe admin stream - this.subscriber.on(`adminStream:${this.user._id}`, data => { + this.subscriber.on(`adminStream:${this.user.id}`, data => { this.send(data); }); } diff --git a/src/server/api/stream/channels/drive.ts b/src/server/api/stream/channels/drive.ts index 391c4b5c325a093369a5018a936de38dc1f2153a..671aad43669f96655f1bc22003ecef0d3af129b6 100644 --- a/src/server/api/stream/channels/drive.ts +++ b/src/server/api/stream/channels/drive.ts @@ -9,7 +9,7 @@ export default class extends Channel { @autobind public async init(params: any) { // Subscribe drive stream - this.subscriber.on(`driveStream:${this.user._id}`, data => { + this.subscriber.on(`driveStream:${this.user.id}`, data => { this.send(data); }); } diff --git a/src/server/api/stream/channels/games/reversi-game.ts b/src/server/api/stream/channels/games/reversi-game.ts index 87df9e194c6936cd84bd63ff491f59649c5d5700..158f108c4e1d90248584ee006568182000f5ad77 100644 --- a/src/server/api/stream/channels/games/reversi-game.ts +++ b/src/server/api/stream/channels/games/reversi-game.ts @@ -1,22 +1,22 @@ import autobind from 'autobind-decorator'; import * as CRC32 from 'crc-32'; -import * as mongo from 'mongodb'; -import ReversiGame, { pack } from '../../../../../models/games/reversi/game'; import { publishReversiGameStream } from '../../../../../services/stream'; import Reversi from '../../../../../games/reversi/core'; import * as maps from '../../../../../games/reversi/maps'; import Channel from '../../channel'; +import { ReversiGame } from '../../../../../models/entities/games/reversi/game'; +import { ReversiGames } from '../../../../../models'; export default class extends Channel { public readonly chName = 'gamesReversiGame'; public static shouldShare = false; public static requireCredential = false; - private gameId: mongo.ObjectID; + private gameId: ReversiGame['id']; @autobind public async init(params: any) { - this.gameId = new mongo.ObjectID(params.gameId as string); + this.gameId = params.gameId; // Subscribe game stream this.subscriber.on(`reversiGameStream:${this.gameId}`, data => { @@ -29,7 +29,7 @@ export default class extends Channel { switch (type) { case 'accept': this.accept(true); break; case 'cancelAccept': this.accept(false); break; - case 'updateSettings': this.updateSettings(body.settings); break; + case 'updateSettings': this.updateSettings(body.key, body.value); break; case 'initForm': this.initForm(body); break; case 'updateForm': this.updateForm(body.id, body.value); break; case 'message': this.message(body); break; @@ -39,54 +39,55 @@ export default class extends Channel { } @autobind - private async updateSettings(settings: any) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + private async updateSettings(key: string, value: any) { + const game = await ReversiGames.findOne(this.gameId); if (game.isStarted) return; - if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; - if (game.user1Id.equals(this.user._id) && game.user1Accepted) return; - if (game.user2Id.equals(this.user._id) && game.user2Accepted) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; + if ((game.user1Id === this.user.id) && game.user1Accepted) return; + if ((game.user2Id === this.user.id) && game.user2Accepted) return; - await ReversiGame.update({ _id: this.gameId }, { - $set: { - settings - } + if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return; + + await ReversiGames.update({ id: this.gameId }, { + [key]: value }); - publishReversiGameStream(this.gameId, 'updateSettings', settings); + publishReversiGameStream(this.gameId, 'updateSettings', { + key: key, + value: value + }); } @autobind private async initForm(form: any) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + const game = await ReversiGames.findOne(this.gameId); if (game.isStarted) return; - if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - const set = game.user1Id.equals(this.user._id) ? { + const set = game.user1Id === this.user.id ? { form1: form } : { - form2: form - }; + form2: form + }; - await ReversiGame.update({ _id: this.gameId }, { - $set: set - }); + await ReversiGames.update({ id: this.gameId }, set); publishReversiGameStream(this.gameId, 'initForm', { - userId: this.user._id, + userId: this.user.id, form }); } @autobind private async updateForm(id: string, value: any) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + const game = await ReversiGames.findOne({ id: this.gameId }); if (game.isStarted) return; - if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - const form = game.user1Id.equals(this.user._id) ? game.form2 : game.form1; + const form = game.user1Id === this.user.id ? game.form2 : game.form1; const item = form.find((i: any) => i.id == id); @@ -94,18 +95,16 @@ export default class extends Channel { item.value = value; - const set = game.user1Id.equals(this.user._id) ? { + const set = game.user1Id === this.user.id ? { form2: form } : { form1: form }; - await ReversiGame.update({ _id: this.gameId }, { - $set: set - }); + await ReversiGames.update({ id: this.gameId }, set); publishReversiGameStream(this.gameId, 'updateForm', { - userId: this.user._id, + userId: this.user.id, id, value }); @@ -115,24 +114,22 @@ export default class extends Channel { private async message(message: any) { message.id = Math.random(); publishReversiGameStream(this.gameId, 'message', { - userId: this.user._id, + userId: this.user.id, message }); } @autobind private async accept(accept: boolean) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + const game = await ReversiGames.findOne(this.gameId); if (game.isStarted) return; let bothAccepted = false; - if (game.user1Id.equals(this.user._id)) { - await ReversiGame.update({ _id: this.gameId }, { - $set: { - user1Accepted: accept - } + if (game.user1Id === this.user.id) { + await ReversiGames.update({ id: this.gameId }, { + user1Accepted: accept }); publishReversiGameStream(this.gameId, 'changeAccepts', { @@ -141,11 +138,9 @@ export default class extends Channel { }); if (accept && game.user2Accepted) bothAccepted = true; - } else if (game.user2Id.equals(this.user._id)) { - await ReversiGame.update({ _id: this.gameId }, { - $set: { - user2Accepted: accept - } + } else if (game.user2Id === this.user.id) { + await ReversiGames.update({ id: this.gameId }, { + user2Accepted: accept }); publishReversiGameStream(this.gameId, 'changeAccepts', { @@ -161,15 +156,15 @@ export default class extends Channel { if (bothAccepted) { // 3秒後ã€ã¾ã acceptã•ã‚Œã¦ã„ãŸã‚‰ã‚²ãƒ¼ãƒ 開始 setTimeout(async () => { - const freshGame = await ReversiGame.findOne({ _id: this.gameId }); + const freshGame = await ReversiGames.findOne(this.gameId); if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; let bw: number; - if (freshGame.settings.bw == 'random') { + if (freshGame.bw == 'random') { bw = Math.random() > 0.5 ? 1 : 2; } else { - bw = freshGame.settings.bw as number; + bw = parseInt(freshGame.bw, 10); } function getRandomMap() { @@ -178,22 +173,20 @@ export default class extends Channel { return Object.values(maps)[rnd].data; } - const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); + const map = freshGame.map != null ? freshGame.map : getRandomMap(); - await ReversiGame.update({ _id: this.gameId }, { - $set: { - startedAt: new Date(), - isStarted: true, - black: bw, - 'settings.map': map - } + await ReversiGames.update({ id: this.gameId }, { + startedAt: new Date(), + isStarted: true, + black: bw, + map: map }); //#region 盤é¢ã«æœ€åˆã‹ã‚‰çŸ³ãŒãªã„ãªã©ã—ã¦å§‹ã¾ã£ãŸçž¬é–“ã«å‹æ•—ãŒæ±ºå®šã™ã‚‹å ´åˆãŒã‚ã‚‹ã®ã§ãã®å‡¦ç† const o = new Reversi(map, { - isLlotheo: freshGame.settings.isLlotheo, - canPutEverywhere: freshGame.settings.canPutEverywhere, - loopedBoard: freshGame.settings.loopedBoard + isLlotheo: freshGame.isLlotheo, + canPutEverywhere: freshGame.canPutEverywhere, + loopedBoard: freshGame.loopedBoard }); if (o.isEnded) { @@ -206,23 +199,22 @@ export default class extends Channel { winner = null; } - await ReversiGame.update({ - _id: this.gameId + await ReversiGames.update({ + id: this.gameId }, { - $set: { - isEnded: true, - winnerId: winner - } - }); + isEnded: true, + winnerId: winner + }); publishReversiGameStream(this.gameId, 'ended', { winnerId: winner, - game: await pack(this.gameId, this.user) + game: await ReversiGames.pack(this.gameId, this.user) }); } //#endregion - publishReversiGameStream(this.gameId, 'started', await pack(this.gameId, this.user)); + publishReversiGameStream(this.gameId, 'started', + await ReversiGames.pack(this.gameId, this.user)); }, 3000); } } @@ -230,16 +222,16 @@ export default class extends Channel { // 石を打㤠@autobind private async set(pos: number) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + const game = await ReversiGames.findOne(this.gameId); if (!game.isStarted) return; if (game.isEnded) return; - if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - const o = new Reversi(game.settings.map, { - isLlotheo: game.settings.isLlotheo, - canPutEverywhere: game.settings.canPutEverywhere, - loopedBoard: game.settings.loopedBoard + const o = new Reversi(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard }); for (const log of game.logs) { @@ -247,7 +239,7 @@ export default class extends Channel { } const myColor = - (game.user1Id.equals(this.user._id) && game.black == 1) || (game.user2Id.equals(this.user._id) && game.black == 2) + ((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2) ? true : false; @@ -271,20 +263,18 @@ export default class extends Channel { pos }; - const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); + const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString(); - await ReversiGame.update({ - _id: this.gameId + game.logs.push(log); + + await ReversiGames.update({ + id: this.gameId }, { - $set: { - crc32, - isEnded: o.isEnded, - winnerId: winner - }, - $push: { - logs: log - } - }); + crc32, + isEnded: o.isEnded, + winnerId: winner, + logs: game.logs + }); publishReversiGameStream(this.gameId, 'set', Object.assign(log, { next: o.turn @@ -293,14 +283,14 @@ export default class extends Channel { if (o.isEnded) { publishReversiGameStream(this.gameId, 'ended', { winnerId: winner, - game: await pack(this.gameId, this.user) + game: await ReversiGames.pack(this.gameId, this.user) }); } } @autobind private async check(crc32: string) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + const game = await ReversiGames.findOne(this.gameId); if (!game.isStarted) return; @@ -308,7 +298,7 @@ export default class extends Channel { if (game.crc32 == null) return; if (crc32 !== game.crc32) { - this.send('rescue', await pack(game, this.user)); + this.send('rescue', await ReversiGames.pack(game, this.user)); } } } diff --git a/src/server/api/stream/channels/games/reversi.ts b/src/server/api/stream/channels/games/reversi.ts index 1b1ad187a35f916607bb252281ae85795e46c3c2..0498e5e0177d9476df21440c4b818c63db5a5ff4 100644 --- a/src/server/api/stream/channels/games/reversi.ts +++ b/src/server/api/stream/channels/games/reversi.ts @@ -1,8 +1,7 @@ import autobind from 'autobind-decorator'; -import * as mongo from 'mongodb'; -import Matching, { pack } from '../../../../../models/games/reversi/matching'; import { publishMainStream } from '../../../../../services/stream'; import Channel from '../../channel'; +import { ReversiMatchings } from '../../../../../models'; export default class extends Channel { public readonly chName = 'gamesReversi'; @@ -12,7 +11,7 @@ export default class extends Channel { @autobind public async init(params: any) { // Subscribe reversi stream - this.subscriber.on(`reversiStream:${this.user._id}`, data => { + this.subscriber.on(`reversiStream:${this.user.id}`, data => { this.send(data); }); } @@ -22,12 +21,12 @@ export default class extends Channel { switch (type) { case 'ping': if (body.id == null) return; - const matching = await Matching.findOne({ - parentId: this.user._id, - childId: new mongo.ObjectID(body.id) + const matching = await ReversiMatchings.findOne({ + parentId: this.user.id, + childId: body.id }); if (matching == null) return; - publishMainStream(matching.childId, 'reversiInvited', await pack(matching, matching.childId)); + publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, matching.childId)); break; } } diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts index b3689d47f57f843abfd047c26501263c553d43b0..bfb7697ba7689d41a77d3893a45e220584e0cf68 100644 --- a/src/server/api/stream/channels/global-timeline.ts +++ b/src/server/api/stream/channels/global-timeline.ts @@ -1,17 +1,14 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; import Channel from '../channel'; import fetchMeta from '../../../../misc/fetch-meta'; +import { Notes } from '../../../../models'; export default class extends Channel { public readonly chName = 'globalTimeline'; public static shouldShare = true; public static requireCredential = false; - private mutedUserIds: string[] = []; - @autobind public async init(params: any) { const meta = await fetchMeta(); @@ -20,29 +17,26 @@ export default class extends Channel { } // Subscribe events - this.subscriber.on('globalTimeline', this.onNote); - - const mute = await Mute.find({ muterId: this.user._id }); - this.mutedUserIds = mute.map(m => m.muteeId.toString()); + this.subscriber.on('notesStream', this.onNote); } @autobind private async onNote(note: any) { // リプライãªã‚‰å†pack if (note.replyId != null) { - note.reply = await pack(note.replyId, this.user, { + note.reply = await Notes.pack(note.replyId, this.user, { detail: true }); } // Renoteãªã‚‰å†pack if (note.renoteId != null) { - note.renote = await pack(note.renoteId, this.user, { + note.renote = await Notes.pack(note.renoteId, this.user, { detail: true }); } // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ - if (shouldMuteThisNote(note, this.mutedUserIds)) return; + if (shouldMuteThisNote(note, this.muting)) return; this.send('note', note); } @@ -50,6 +44,6 @@ export default class extends Channel { @autobind public dispose() { // Unsubscribe events - this.subscriber.off('globalTimeline', this.onNote); + this.subscriber.off('notesStream', this.onNote); } } diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts index 586ce02f0666c98d1c11c94b8c59f60cc2aada74..36c56c7ab6bee77071d270dbd48774a8abb7aecc 100644 --- a/src/server/api/stream/channels/hashtag.ts +++ b/src/server/api/stream/channels/hashtag.ts @@ -1,40 +1,46 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; import Channel from '../channel'; +import { Notes } from '../../../../models'; export default class extends Channel { public readonly chName = 'hashtag'; public static shouldShare = false; public static requireCredential = false; + private q: string[][]; @autobind public async init(params: any) { - const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null; - const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; + this.q = params.q; - const q: string[][] = params.q; - - if (q == null) return; + if (this.q == null) return; // Subscribe stream - this.subscriber.on('hashtag', async note => { - const noteTags = note.tags.map((t: string) => t.toLowerCase()); - const matched = q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase()))); - if (!matched) return; - - // Renoteãªã‚‰å†pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, this.user, { - detail: true - }); - } - - // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ - if (shouldMuteThisNote(note, mutedUserIds)) return; - - this.send('note', note); - }); + this.subscriber.on('notesStream', this.onNote); + } + + @autobind + private async onNote(note: any) { + const noteTags = note.tags.map((t: string) => t.toLowerCase()); + const matched = this.q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase()))); + if (!matched) return; + + // Renoteãªã‚‰å†pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user, { + detail: true + }); + } + + // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ + if (shouldMuteThisNote(note, this.muting)) return; + + this.send('note', note); + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off('notesStream', this.onNote); } } diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts index 3c0b23872047bbdfc73c6d031bfcabe1ec34d317..2cece0947f376f8ccafe32a718c9f267916a3a08 100644 --- a/src/server/api/stream/channels/home-timeline.ts +++ b/src/server/api/stream/channels/home-timeline.ts @@ -1,42 +1,49 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; import Channel from '../channel'; +import { Notes } from '../../../../models'; export default class extends Channel { public readonly chName = 'homeTimeline'; public static shouldShare = true; public static requireCredential = true; - private mutedUserIds: string[] = []; - @autobind public async init(params: any) { // Subscribe events - this.subscriber.on(`homeTimeline:${this.user._id}`, this.onNote); - - const mute = await Mute.find({ muterId: this.user._id }); - this.mutedUserIds = mute.map(m => m.muteeId.toString()); + this.subscriber.on('notesStream', this.onNote); } @autobind private async onNote(note: any) { - // リプライãªã‚‰å†pack - if (note.replyId != null) { - note.reply = await pack(note.replyId, this.user, { - detail: true - }); - } - // Renoteãªã‚‰å†pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, this.user, { + // ãã®æŠ•ç¨¿ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’フォãƒãƒ¼ã—ã¦ã„ãªã‹ã£ãŸã‚‰å¼¾ã + if (this.user.id !== note.userId && !this.following.includes(note.userId)) return; + + if (['followers', 'specified'].includes(note.visibility)) { + note = await Notes.pack(note.id, this.user, { detail: true }); + + if (note.isHidden) { + return; + } + } else { + // リプライãªã‚‰å†pack + if (note.replyId != null) { + note.reply = await Notes.pack(note.replyId, this.user, { + detail: true + }); + } + // Renoteãªã‚‰å†pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user, { + detail: true + }); + } } // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ - if (shouldMuteThisNote(note, this.mutedUserIds)) return; + if (shouldMuteThisNote(note, this.muting)) return; this.send('note', note); } @@ -44,6 +51,6 @@ export default class extends Channel { @autobind public dispose() { // Unsubscribe events - this.subscriber.off(`homeTimeline:${this.user._id}`, this.onNote); + this.subscriber.off('notesStream', this.onNote); } } diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts deleted file mode 100644 index 35ef17b56baa1d22916b98d5c1e76f05a1f03154..0000000000000000000000000000000000000000 --- a/src/server/api/stream/channels/hybrid-timeline.ts +++ /dev/null @@ -1,55 +0,0 @@ -import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; -import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; -import Channel from '../channel'; -import fetchMeta from '../../../../misc/fetch-meta'; - -export default class extends Channel { - public readonly chName = 'hybridTimeline'; - public static shouldShare = true; - public static requireCredential = true; - - private mutedUserIds: string[] = []; - - @autobind - public async init(params: any) { - const meta = await fetchMeta(); - if (meta.disableLocalTimeline && !this.user.isAdmin && !this.user.isModerator) return; - - // Subscribe events - this.subscriber.on('hybridTimeline', this.onNewNote); - this.subscriber.on(`hybridTimeline:${this.user._id}`, this.onNewNote); - - const mute = await Mute.find({ muterId: this.user._id }); - this.mutedUserIds = mute.map(m => m.muteeId.toString()); - } - - @autobind - private async onNewNote(note: any) { - // リプライãªã‚‰å†pack - if (note.replyId != null) { - note.reply = await pack(note.replyId, this.user, { - detail: true - }); - } - // Renoteãªã‚‰å†pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, this.user, { - detail: true - }); - } - - // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ - if (shouldMuteThisNote(note, this.mutedUserIds)) return; - - this.send('note', note); - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off('hybridTimeline', this.onNewNote); - this.subscriber.off(`hybridTimeline:${this.user._id}`, this.onNewNote); - } -} diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts index 4527fb1e46dd0ecfb456b892a193e4a9096aad83..199ab0a80986ede310e31133a509f73148e3c547 100644 --- a/src/server/api/stream/channels/index.ts +++ b/src/server/api/stream/channels/index.ts @@ -1,7 +1,7 @@ import main from './main'; import homeTimeline from './home-timeline'; import localTimeline from './local-timeline'; -import hybridTimeline from './hybrid-timeline'; +import socialTimeline from './social-timeline'; import globalTimeline from './global-timeline'; import notesStats from './notes-stats'; import serverStats from './server-stats'; @@ -20,7 +20,7 @@ export default { main, homeTimeline, localTimeline, - hybridTimeline, + socialTimeline, globalTimeline, notesStats, serverStats, diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts index 34020231922bca90a43d457e6b7f9b9fc3f91848..4aec2d66b4028cab2234e127a792ef967ec54578 100644 --- a/src/server/api/stream/channels/local-timeline.ts +++ b/src/server/api/stream/channels/local-timeline.ts @@ -1,17 +1,14 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; import Channel from '../channel'; import fetchMeta from '../../../../misc/fetch-meta'; +import { Notes } from '../../../../models'; export default class extends Channel { public readonly chName = 'localTimeline'; public static shouldShare = true; public static requireCredential = false; - private mutedUserIds: string[] = []; - @autobind public async init(params: any) { const meta = await fetchMeta(); @@ -20,29 +17,39 @@ export default class extends Channel { } // Subscribe events - this.subscriber.on('localTimeline', this.onNote); - - const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null; - this.mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; + this.subscriber.on('notesStream', this.onNote); } @autobind private async onNote(note: any) { - // リプライãªã‚‰å†pack - if (note.replyId != null) { - note.reply = await pack(note.replyId, this.user, { - detail: true - }); - } - // Renoteãªã‚‰å†pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, this.user, { + if (note.user.host !== null) return; + if (note.visibility === 'home') return; + + if (['followers', 'specified'].includes(note.visibility)) { + note = await Notes.pack(note.id, this.user, { detail: true }); + + if (note.isHidden) { + return; + } + } else { + // リプライãªã‚‰å†pack + if (note.replyId != null) { + note.reply = await Notes.pack(note.replyId, this.user, { + detail: true + }); + } + // Renoteãªã‚‰å†pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user, { + detail: true + }); + } } // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ - if (shouldMuteThisNote(note, this.mutedUserIds)) return; + if (shouldMuteThisNote(note, this.muting)) return; this.send('note', note); } @@ -50,6 +57,6 @@ export default class extends Channel { @autobind public dispose() { // Unsubscribe events - this.subscriber.off('localTimeline', this.onNote); + this.subscriber.off('notesStream', this.onNote); } } diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts index 175d914fa54ca4258e8c5cdc651efce51901257f..0d9bf3149d44a70fd0cdf50aef1a7471dc855262 100644 --- a/src/server/api/stream/channels/main.ts +++ b/src/server/api/stream/channels/main.ts @@ -1,6 +1,6 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; import Channel from '../channel'; +import { Mutings } from '../../../../models'; export default class extends Channel { public readonly chName = 'main'; @@ -9,16 +9,15 @@ export default class extends Channel { @autobind public async init(params: any) { - const mute = await Mute.find({ muterId: this.user._id }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); + const mute = await Mutings.find({ muterId: this.user.id }); // Subscribe main stream channel - this.subscriber.on(`mainStream:${this.user._id}`, async data => { + this.subscriber.on(`mainStream:${this.user.id}`, async data => { const { type, body } = data; switch (type) { case 'notification': { - if (mutedUserIds.includes(body.userId)) return; + if (mute.map(m => m.muteeId).includes(body.userId)) return; if (body.note && body.note.isHidden) return; break; } diff --git a/src/server/api/stream/channels/messaging-index.ts b/src/server/api/stream/channels/messaging-index.ts index 148ff7f93508e59fa89b834e0fc74817daca8a43..648badc1dc169598f1ac3ba399e3857d3cb99e6b 100644 --- a/src/server/api/stream/channels/messaging-index.ts +++ b/src/server/api/stream/channels/messaging-index.ts @@ -9,7 +9,7 @@ export default class extends Channel { @autobind public async init(params: any) { // Subscribe messaging index stream - this.subscriber.on(`messagingIndexStream:${this.user._id}`, data => { + this.subscriber.on(`messagingIndexStream:${this.user.id}`, data => { this.send(data); }); } diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts index 0d81b4e45c12facf680213876bcf2e79e763a349..b81fbb9d4c857e462a3d88d1ceb67830a50d0d44 100644 --- a/src/server/api/stream/channels/messaging.ts +++ b/src/server/api/stream/channels/messaging.ts @@ -14,7 +14,7 @@ export default class extends Channel { this.otherpartyId = params.otherparty as string; // Subscribe messaging stream - this.subscriber.on(`messagingStream:${this.user._id}-${this.otherpartyId}`, data => { + this.subscriber.on(`messagingStream:${this.user.id}-${this.otherpartyId}`, data => { this.send(data); }); } @@ -23,7 +23,7 @@ export default class extends Channel { public onMessage(type: string, body: any) { switch (type) { case 'read': - read(this.user._id, this.otherpartyId, body.id); + read(this.user.id, this.otherpartyId, body.id); break; } } diff --git a/src/server/api/stream/channels/social-timeline.ts b/src/server/api/stream/channels/social-timeline.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d76eed297c4a3bb4fc86af12cff1a54723e6084 --- /dev/null +++ b/src/server/api/stream/channels/social-timeline.ts @@ -0,0 +1,64 @@ +import autobind from 'autobind-decorator'; +import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; +import Channel from '../channel'; +import fetchMeta from '../../../../misc/fetch-meta'; +import { Notes } from '../../../../models'; + +export default class extends Channel { + public readonly chName = 'socialTimeline'; + public static shouldShare = true; + public static requireCredential = true; + + @autobind + public async init(params: any) { + const meta = await fetchMeta(); + if (meta.disableLocalTimeline && !this.user.isAdmin && !this.user.isModerator) return; + + // Subscribe events + this.subscriber.on('notesStream', this.onNote); + } + + @autobind + private async onNote(note: any) { + // 自分自身ã®æŠ•ç¨¿ ã¾ãŸã¯ ãã®æŠ•ç¨¿ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’フォãƒãƒ¼ã—ã¦ã„ã‚‹ ã¾ãŸã¯ ãƒãƒ¼ã‚«ãƒ«ã®æŠ•ç¨¿ ã®å ´åˆã ã‘ + if (!( + this.user.id === note.userId || + this.following.includes(note.userId) || + note.user.host === null + )) return; + + if (['followers', 'specified'].includes(note.visibility)) { + note = await Notes.pack(note.id, this.user, { + detail: true + }); + + if (note.isHidden) { + return; + } + } else { + // リプライãªã‚‰å†pack + if (note.replyId != null) { + note.reply = await Notes.pack(note.replyId, this.user, { + detail: true + }); + } + // Renoteãªã‚‰å†pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user, { + detail: true + }); + } + } + + // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ + if (shouldMuteThisNote(note, this.muting)) return; + + this.send('note', note); + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off('notesStream', this.onNote); + } +} diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts index 5debf4177048dea96c2001705564d1ce048220c7..f5434b8f0862485f239c7d2f72e4b9c56df04334 100644 --- a/src/server/api/stream/channels/user-list.ts +++ b/src/server/api/stream/channels/user-list.ts @@ -1,23 +1,81 @@ import autobind from 'autobind-decorator'; import Channel from '../channel'; -import { pack } from '../../../../models/note'; +import { Notes, UserListJoinings } from '../../../../models'; +import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; +import { User } from '../../../../models/entities/user'; export default class extends Channel { public readonly chName = 'userList'; public static shouldShare = false; public static requireCredential = false; + private listId: string; + public listUsers: User['id'][] = []; + private listUsersClock: NodeJS.Timer; @autobind public async init(params: any) { - const listId = params.listId as string; + this.listId = params.listId as string; // Subscribe stream - this.subscriber.on(`userListStream:${listId}`, async data => { - // å†ãƒ‘ック - if (data.type == 'note') data.body = await pack(data.body.id, this.user, { + this.subscriber.on(`userListStream:${this.listId}`, this.send); + + this.subscriber.on('notesStream', this.onNote); + + this.updateListUsers(); + this.listUsersClock = setInterval(this.updateListUsers, 5000); + } + + @autobind + private async updateListUsers() { + const users = await UserListJoinings.find({ + where: { + userListId: this.listId, + }, + select: ['userId'] + }); + + this.listUsers = users.map(x => x.userId); + } + + @autobind + private async onNote(note: any) { + if (!this.listUsers.includes(note.userId)) return; + + if (['followers', 'specified'].includes(note.visibility)) { + note = await Notes.pack(note.id, this.user, { detail: true }); - this.send(data); - }); + + if (note.isHidden) { + return; + } + } else { + // リプライãªã‚‰å†pack + if (note.replyId != null) { + note.reply = await Notes.pack(note.replyId, this.user, { + detail: true + }); + } + // Renoteãªã‚‰å†pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user, { + detail: true + }); + } + } + + // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ + if (shouldMuteThisNote(note, this.muting)) return; + + this.send('note', note); + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off(`userListStream:${this.listId}`, this.send); + this.subscriber.off('notesStream', this.onNote); + + clearInterval(this.listUsersClock); } } diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index 22f7646cb9ee5f19e9bbea0c2f46c8bc7f11cdb3..abbd91ec811a89389fedc08e3d950ae71a1352c2 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -1,33 +1,35 @@ import autobind from 'autobind-decorator'; import * as websocket from 'websocket'; - -import User, { IUser } from '../../../models/user'; -import readNotification from '../common/read-notification'; +import { readNotification } from '../common/read-notification'; import call from '../call'; -import { IApp } from '../../../models/app'; import readNote from '../../../services/note/read'; - import Channel from './channel'; import channels from './channels'; import { EventEmitter } from 'events'; +import { User } from '../../../models/entities/user'; +import { App } from '../../../models/entities/app'; +import { Users, Followings, Mutings } from '../../../models'; /** * Main stream connection */ export default class Connection { - public user?: IUser; - public app: IApp; + public user?: User; + public following: User['id'][] = []; + public muting: User['id'][] = []; + public app: App; private wsConnection: websocket.connection; public subscriber: EventEmitter; private channels: Channel[] = []; private subscribingNotes: any = {}; - public sendMessageToWsOverride: any = null; // 後方互æ›æ€§ã®ãŸã‚ + private followingClock: NodeJS.Timer; + private mutingClock: NodeJS.Timer; constructor( wsConnection: websocket.connection, subscriber: EventEmitter, - user: IUser, - app: IApp + user: User, + app: App ) { this.wsConnection = wsConnection; this.user = user; @@ -35,6 +37,14 @@ export default class Connection { this.subscriber = subscriber; this.wsConnection.on('message', this.onWsConnectionMessage); + + if (this.user) { + this.updateFollowing(); + this.followingClock = setInterval(this.updateFollowing, 5000); + + this.updateMuting(); + this.mutingClock = setInterval(this.updateMuting, 5000); + } } /** @@ -64,7 +74,7 @@ export default class Connection { @autobind private async onApiRequest(payload: any) { // æ–°é®®ãªãƒ‡ãƒ¼ã‚¿ã‚’利用ã™ã‚‹ãŸã‚ã«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’フェッム- const user = this.user ? await User.findOne({ _id: this.user._id }) : null; + const user = this.user ? await Users.findOne(this.user.id) : null; const endpoint = payload.endpoint || payload.ep; // alias @@ -79,7 +89,7 @@ export default class Connection { @autobind private onReadNotification(payload: any) { if (!payload.id) return; - readNotification(this.user._id, payload.id); + readNotification(this.user.id, [payload.id]); } /** @@ -100,7 +110,7 @@ export default class Connection { } if (payload.read) { - readNote(this.user._id, payload.id); + readNote(this.user.id, payload.id); } } @@ -150,7 +160,6 @@ export default class Connection { */ @autobind public sendMessageToWs(type: string, payload: any) { - if (this.sendMessageToWsOverride) return this.sendMessageToWsOverride(type, payload); // 後方互æ›æ€§ã®ãŸã‚ this.wsConnection.send(JSON.stringify({ type: type, body: payload @@ -208,6 +217,30 @@ export default class Connection { } } + @autobind + private async updateFollowing() { + const followings = await Followings.find({ + where: { + followerId: this.user.id + }, + select: ['followeeId'] + }); + + this.following = followings.map(x => x.followeeId); + } + + @autobind + private async updateMuting() { + const mutings = await Mutings.find({ + where: { + muterId: this.user.id + }, + select: ['muteeId'] + }); + + this.muting = mutings.map(x => x.muteeId); + } + /** * ストリームãŒåˆ‡ã‚ŒãŸã¨ã */ @@ -216,5 +249,8 @@ export default class Connection { for (const c of this.channels.filter(c => c.dispose)) { c.dispose(); } + + if (this.followingClock) clearInterval(this.followingClock); + if (this.mutingClock) clearInterval(this.mutingClock); } } diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index f8f3c0ff4aeeed398abf33697b0060441c3b0109..ab66f2b6d965a01d092be01c10be170e3f1371f7 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -48,33 +48,6 @@ module.exports = (server: http.Server) => { const main = new MainStreamConnection(connection, ev, user, app); - // 後方互æ›æ€§ã®ãŸã‚ - if (request.resourceURL.pathname !== '/streaming') { - main.sendMessageToWsOverride = (type: string, payload: any) => { - if (type == 'channel') { - type = payload.type; - payload = payload.body; - } - if (type.startsWith('api:')) { - type = type.replace('api:', 'api-res:'); - } - connection.send(JSON.stringify({ - type: type, - body: payload - })); - }; - - main.connectChannel(Math.random().toString().substr(2, 8), null, - request.resourceURL.pathname === '/' ? 'homeTimeline' : - request.resourceURL.pathname === '/local-timeline' ? 'localTimeline' : - request.resourceURL.pathname === '/hybrid-timeline' ? 'hybridTimeline' : - request.resourceURL.pathname === '/global-timeline' ? 'globalTimeline' : null); - - if (request.resourceURL.pathname === '/') { - main.connectChannel(Math.random().toString().substr(2, 8), null, 'main'); - } - } - connection.once('close', () => { ev.removeAllListeners(); main.dispose(); diff --git a/src/server/file/index.ts b/src/server/file/index.ts index 973528da33d8d528bd9e1536dc4bd2fe74338328..e3487a2636319ba73173386a6f1c5ab48a3b906e 100644 --- a/src/server/file/index.ts +++ b/src/server/file/index.ts @@ -33,8 +33,8 @@ router.get('/app-default.jpg', ctx => { ctx.body = file; }); -router.get('/:id', sendDriveFile); -router.get('/:id/*', sendDriveFile); +router.get('/:key', sendDriveFile); +router.get('/:key/*', sendDriveFile); // Register router app.use(router.routes()); diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts index e0208f3fab8d595547cf43dbbbfd5112ec8df79a..f9b067b79ccb926398168389cece25ad92c2edf1 100644 --- a/src/server/file/send-drive-file.ts +++ b/src/server/file/send-drive-file.ts @@ -1,12 +1,10 @@ import * as Koa from 'koa'; import * as send from 'koa-send'; -import * as mongodb from 'mongodb'; import * as rename from 'rename'; -import DriveFile, { getDriveFileBucket } from '../../models/drive-file'; -import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; -import DriveFileWebpublic, { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic'; import { serverLogger } from '..'; import { contentDisposition } from '../../misc/content-disposition'; +import { DriveFiles } from '../../models'; +import { InternalStorage } from '../../services/drive/internal-storage'; const assets = `${__dirname}/../../server/file/assets/`; @@ -16,16 +14,14 @@ const commonReadableHandlerGenerator = (ctx: Koa.BaseContext) => (e: Error): voi }; export default async function(ctx: Koa.BaseContext) { - // Validate id - if (!mongodb.ObjectID.isValid(ctx.params.id)) { - ctx.throw(400, 'incorrect id'); - return; - } - - const fileId = new mongodb.ObjectID(ctx.params.id); + const key = ctx.params.key; // Fetch drive file - const file = await DriveFile.findOne({ _id: fileId }); + const file = await DriveFiles.createQueryBuilder('file') + .where('file.accessKey = :accessKey', { accessKey: key }) + .orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key }) + .orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key }) + .getOne(); if (file == null) { ctx.status = 404; @@ -33,69 +29,30 @@ export default async function(ctx: Koa.BaseContext) { return; } - if (file.metadata.deletedAt) { - ctx.status = 410; - await send(ctx as any, '/tombstone.png', { root: assets }); - return; - } - - if (file.metadata.withoutChunks) { + if (!file.storedInternal) { ctx.status = 204; return; } - const sendRaw = async () => { - if (file.metadata && file.metadata.accessKey && file.metadata.accessKey != ctx.query['original']) { - ctx.status = 403; - return; - } - - const bucket = await getDriveFileBucket(); - const readable = bucket.openDownloadStream(fileId); - readable.on('error', commonReadableHandlerGenerator(ctx)); - ctx.set('Content-Type', file.contentType); - ctx.body = readable; - }; - - if ('thumbnail' in ctx.query) { - const thumb = await DriveFileThumbnail.findOne({ - 'metadata.originalId': fileId - }); - - if (thumb != null) { - ctx.set('Content-Type', 'image/jpeg'); - ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.filename, { suffix: '-thumb', extname: '.jpeg' })}`)); - const bucket = await getDriveFileThumbnailBucket(); - ctx.body = bucket.openDownloadStream(thumb._id); - } else { - if (file.contentType.startsWith('image/')) { - ctx.set('Content-Disposition', contentDisposition('inline', `${file.filename}`)); - await sendRaw(); - } else { - ctx.status = 404; - await send(ctx as any, '/dummy.png', { root: assets }); - } - } - } else if ('web' in ctx.query) { - const web = await DriveFileWebpublic.findOne({ - 'metadata.originalId': fileId - }); - - if (web != null) { - ctx.set('Content-Type', file.contentType); - ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.filename, { suffix: '-web' })}`)); - - const bucket = await getDriveFileWebpublicBucket(); - ctx.body = bucket.openDownloadStream(web._id); - } else { - ctx.set('Content-Disposition', contentDisposition('inline', `${file.filename}`)); - await sendRaw(); - } + const isThumbnail = file.thumbnailAccessKey === key; + const isWebpublic = file.webpublicAccessKey === key; + + if (isThumbnail) { + ctx.set('Content-Type', 'image/jpeg'); + ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-thumb', extname: '.jpeg' })}`)); + ctx.body = InternalStorage.read(key); + } else if (isWebpublic) { + ctx.set('Content-Type', file.type); + ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-web' })}`)); + ctx.body = InternalStorage.read(key); } else { if ('download' in ctx.query) { - ctx.set('Content-Disposition', contentDisposition('attachment', `${file.filename}`)); + ctx.set('Content-Disposition', contentDisposition('attachment', `${file.name}`)); } - await sendRaw(); + const readable = InternalStorage.read(file.accessKey); + readable.on('error', commonReadableHandlerGenerator(ctx)); + ctx.set('Content-Type', file.type); + ctx.body = readable; } } diff --git a/src/server/index.ts b/src/server/index.ts index 7c51923f9eb75216050248f2666286eb9fe86b94..563117773e971d0f0068de2163661ae448de25c6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -19,12 +19,12 @@ import activityPub from './activitypub'; import nodeinfo from './nodeinfo'; import wellKnown from './well-known'; import config from '../config'; -import networkChart from '../services/chart/network'; import apiServer from './api'; import { sum } from '../prelude/array'; -import User from '../models/user'; import Logger from '../services/logger'; import { program } from '../argv'; +import { Users } from '../models'; +import { networkChart } from '../services/chart'; export const serverLogger = new Logger('server', 'gray', false); @@ -73,17 +73,17 @@ router.use(nodeinfo.routes()); router.use(wellKnown.routes()); router.get('/verify-email/:code', async ctx => { - const user = await User.findOne({ emailVerifyCode: ctx.params.code }); + const user = await Users.findOne({ + emailVerifyCode: ctx.params.code + }); if (user != null) { ctx.body = 'Verify succeeded!'; ctx.status = 200; - User.update({ _id: user._id }, { - $set: { - emailVerified: true, - emailVerifyCode: null - } + Users.update(user.id, { + emailVerified: true, + emailVerifyCode: null }); } else { ctx.status = 404; diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts index a783eea90b73b37d21a18d9d5484c66a38c166e3..686412383e4c7bc4eb7aab5099c4fcb9442e8c6f 100644 --- a/src/server/nodeinfo.ts +++ b/src/server/nodeinfo.ts @@ -20,7 +20,24 @@ export const links = [/* (awaiting release) { const nodeinfo2 = async () => { const [ - { name, description, maintainer, langs, announcements, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker }, + { + name, + description, + maintainerName, + maintainerEmail, + langs, + announcements, + disableRegistration, + disableLocalTimeline, + disableGlobalTimeline, + enableRecaptcha, + maxNoteTextLength, + enableTwitterIntegration, + enableGithubIntegration, + enableDiscordIntegration, + enableEmail, + enableServiceWorker + }, // total, // activeHalfyear, // activeMonth, @@ -52,7 +69,26 @@ const nodeinfo2 = async () => { // localPosts, // localComments }, - metadata: { name, description, maintainer, langs, announcements, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker } + metadata: { + name, + description, + maintainer: { + name: maintainerName, + email: maintainerEmail + }, + langs, + announcements, + disableRegistration, + disableLocalTimeline, + disableGlobalTimeline, + enableRecaptcha, + maxNoteTextLength, + enableTwitterIntegration, + enableGithubIntegration, + enableDiscordIntegration, + enableEmail, + enableServiceWorker + } }; }; diff --git a/src/server/web/feed.ts b/src/server/web/feed.ts index 09ac10c576446900e989c090e84f68faf71f8ba9..4b4ea87973b2db089c35d42e669069f00940b165 100644 --- a/src/server/web/feed.ts +++ b/src/server/web/feed.ts @@ -1,25 +1,23 @@ import { Feed } from 'feed'; import config from '../../config'; -import Note from '../../models/note'; -import { IUser } from '../../models/user'; -import { getOriginalUrl } from '../../misc/get-drive-file-url'; +import { User } from '../../models/entities/user'; +import { Notes, DriveFiles } from '../../models'; +import { In } from 'typeorm'; -export default async function(user: IUser) { +export default async function(user: User) { const author: Author = { link: `${config.url}/@${user.username}`, name: user.name || user.username }; - const notes = await Note.find({ - userId: user._id, - renoteId: null, - $or: [ - { visibility: 'public' }, - { visibility: 'home' } - ] - }, { - sort: { createdAt: -1 }, - limit: 20 + const notes = await Notes.find({ + where: { + userId: user.id, + renoteId: null, + visibility: In(['public', 'home']) + }, + order: { createdAt: -1 }, + take: 20 }); const feed = new Feed({ @@ -38,15 +36,18 @@ export default async function(user: IUser) { } as FeedOptions); for (const note of notes) { - const file = note._files && note._files.find(file => file.contentType.startsWith('image/')); + const files = note.fileIds.length > 0 ? await DriveFiles.find({ + id: In(note.fileIds) + }) : []; + const file = files.find(file => file.type.startsWith('image/')); feed.addItem({ title: `New note by ${author.name}`, - link: `${config.url}/notes/${note._id}`, + link: `${config.url}/notes/${note.id}`, date: note.createdAt, description: note.cw, content: note.text, - image: file && getOriginalUrl(file) + image: file ? DriveFiles.getPublicUrl(file) : null }); } diff --git a/src/server/web/index.ts b/src/server/web/index.ts index d8525ba1145fd027f438114093b81902606e4d9c..de0d65cf33551d3116b6c0adb56b92e05a11bd75 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -9,19 +9,16 @@ import * as Router from 'koa-router'; import * as send from 'koa-send'; import * as favicon from 'koa-favicon'; import * as views from 'koa-views'; -import { ObjectID } from 'mongodb'; import docs from './docs'; import packFeed from './feed'; -import User from '../../models/user'; -import parseAcct from '../../misc/acct/parse'; -import config from '../../config'; -import Note, { pack as packNote } from '../../models/note'; -import getNoteSummary from '../../misc/get-note-summary'; import fetchMeta from '../../misc/fetch-meta'; -import Emoji from '../../models/emoji'; import * as pkg from '../../../package.json'; import { genOpenapiSpec } from '../api/openapi/gen-spec'; +import config from '../../config'; +import { Users, Notes, Emojis } from '../../models'; +import parseAcct from '../../misc/acct/parse'; +import getNoteSummary from '../../misc/get-note-summary'; const client = `${__dirname}/../../client/`; @@ -100,7 +97,7 @@ router.get('/api.json', async ctx => { const getFeed = async (acct: string) => { const { username, host } = parseAcct(acct); - const user = await User.findOne({ + const user = await Users.findOne({ usernameLower: username.toLowerCase(), host }); @@ -148,7 +145,7 @@ router.get('/@:user.json', async ctx => { // User router.get('/@:user', async (ctx, next) => { const { username, host } = parseAcct(ctx.params.user); - const user = await User.findOne({ + const user = await Users.findOne({ usernameLower: username.toLowerCase(), host }); @@ -157,7 +154,7 @@ router.get('/@:user', async (ctx, next) => { const meta = await fetchMeta(); await ctx.render('user', { user, - instanceName: meta.name + instanceName: meta.name || 'Misskey' }); ctx.set('Cache-Control', 'public, max-age=180'); } else { @@ -167,19 +164,12 @@ router.get('/@:user', async (ctx, next) => { }); router.get('/users/:user', async ctx => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); - - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: ctx.params.user, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } @@ -189,26 +179,24 @@ router.get('/users/:user', async ctx => { // Note router.get('/notes/:note', async ctx => { - if (ObjectID.isValid(ctx.params.note)) { - const note = await Note.findOne({ _id: ctx.params.note }); - - if (note) { - const _note = await packNote(note); - const meta = await fetchMeta(); - await ctx.render('note', { - note: _note, - summary: getNoteSummary(_note), - instanceName: meta.name - }); - - if (['public', 'home'].includes(note.visibility)) { - ctx.set('Cache-Control', 'public, max-age=180'); - } else { - ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); - } - - return; + const note = await Notes.findOne(ctx.params.note); + + if (note) { + const _note = await Notes.pack(note); + const meta = await fetchMeta(); + await ctx.render('note', { + note: _note, + summary: getNoteSummary(_note), + instanceName: meta.name || 'Misskey' + }); + + if (['public', 'home'].includes(note.visibility)) { + ctx.set('Cache-Control', 'public, max-age=180'); + } else { + ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); } + + return; } ctx.status = 404; @@ -217,10 +205,8 @@ router.get('/notes/:note', async ctx => { router.get('/info', async ctx => { const meta = await fetchMeta(); - const emojis = await Emoji.find({ host: null }, { - fields: { - _id: false - } + const emojis = await Emojis.find({ + where: { host: null } }); await ctx.render('info', { version: pkg.version, @@ -232,7 +218,9 @@ router.get('/info', async ctx => { cores: os.cpus().length }, emojis: emojis, - meta: meta + meta: meta, + originalUsersCount: await Users.count({ host: null }), + originalNotesCount: await Notes.count({ userHost: null }) }); }); @@ -247,7 +235,7 @@ router.get('*', async ctx => { const meta = await fetchMeta(); await ctx.render('base', { img: meta.bannerUrl, - title: meta.name, + title: meta.name || 'Misskey', desc: meta.description, icon: meta.iconUrl }); diff --git a/src/server/web/manifest.ts b/src/server/web/manifest.ts index 35d3d1b666cef090de68c744838698fab116fa27..4acfb22de5dfb8b7746396f607f21c8442adc8c5 100644 --- a/src/server/web/manifest.ts +++ b/src/server/web/manifest.ts @@ -1,10 +1,9 @@ import * as Koa from 'koa'; import * as manifest from '../../client/assets/manifest.json'; -import * as deepcopy from 'deepcopy'; import fetchMeta from '../../misc/fetch-meta'; module.exports = async (ctx: Koa.BaseContext) => { - const json = deepcopy(manifest); + const json = JSON.parse(JSON.stringify(manifest)); const instance = await fetchMeta(); diff --git a/src/server/web/views/info.pug b/src/server/web/views/info.pug index 1c4b272a627f8e2f9616d6e200ff014cb008eaf5..c8b0bd939afae54b99fe007878355a44990d6e34 100644 --- a/src/server/web/views/info.pug +++ b/src/server/web/views/info.pug @@ -70,15 +70,15 @@ html table tr th Instance - td= meta.name + td= meta.name || 'Misskey' tr th Description td= meta.description tr th Maintainer td - = meta.maintainer.name - | <#{meta.maintainer.email}> + = meta.maintainerName + | <#{meta.maintainerEmail}> tr th System td= os @@ -93,10 +93,10 @@ html td= cpu.model tr th Original users - td= meta.stats.originalUsersCount + td= originalUsersCount tr th Original notes - td= meta.stats.originalNotesCount + td= originalNotesCount tr th Registration td= !meta.disableRegistration ? 'yes' : 'no' diff --git a/src/server/well-known.ts b/src/server/well-known.ts index 18c080acc7df302ec1e9e83d61319ba027c2f31e..7c5684d2cefc4e1ba71abe4e36eaab88c5619bb5 100644 --- a/src/server/well-known.ts +++ b/src/server/well-known.ts @@ -1,12 +1,12 @@ -import * as mongo from 'mongodb'; import * as Router from 'koa-router'; import config from '../config'; import parseAcct from '../misc/acct/parse'; -import User from '../models/user'; import Acct from '../misc/acct/type'; import { links } from './nodeinfo'; import { escapeAttribute, escapeValue } from '../prelude/xml'; +import { Users } from '../models'; +import { User } from '../models/entities/user'; // Init router const router = new Router(); @@ -47,19 +47,19 @@ router.get('/.well-known/nodeinfo', async ctx => { }); router.get(webFingerPath, async ctx => { + const fromId = (id: User['id']): Record<string, any> => ({ + id, + host: null + }); + const generateQuery = (resource: string) => resource.startsWith(`${config.url.toLowerCase()}/users/`) ? - fromId(new mongo.ObjectID(resource.split('/').pop())) : + fromId(resource.split('/').pop()) : fromAcct(parseAcct( resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop() : resource.startsWith('acct:') ? resource.slice('acct:'.length) : resource)); - const fromId = (_id: mongo.ObjectID): Record<string, any> => ({ - _id, - host: null - }); - const fromAcct = (acct: Acct): Record<string, any> | number => !acct.host || acct.host === config.host.toLowerCase() ? { usernameLower: acct.username, @@ -78,9 +78,9 @@ router.get(webFingerPath, async ctx => { return; } - const user = await User.findOne(query); + const user = await Users.findOne(query); - if (user === null) { + if (user == null) { ctx.status = 404; return; } @@ -89,7 +89,7 @@ router.get(webFingerPath, async ctx => { const self = { rel: 'self', type: 'application/activity+json', - href: `${config.url}/users/${user._id}` + href: `${config.url}/users/${user.id}` }; const profilePage = { rel: 'http://webfinger.net/rel/profile-page', diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts index c20666ef2682c10d12a7bc27b90a4b6f1106bea2..79ca0d59f1680bb02cfbb508a6bad6344c96ef42 100644 --- a/src/services/blocking/create.ts +++ b/src/services/blocking/create.ts @@ -1,6 +1,3 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; -import FollowRequest from '../../models/follow-request'; import { publishMainStream } from '../stream'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; @@ -8,11 +5,12 @@ import renderUndo from '../../remote/activitypub/renderer/undo'; import renderBlock from '../../remote/activitypub/renderer/block'; import { deliver } from '../../queue'; import renderReject from '../../remote/activitypub/renderer/reject'; -import perUserFollowingChart from '../../services/chart/per-user-following'; -import Blocking from '../../models/blocking'; - -export default async function(blocker: IUser, blockee: IUser) { +import { User } from '../../models/entities/user'; +import { Blockings, Users, FollowRequests, Followings } from '../../models'; +import { perUserFollowingChart } from '../chart'; +import { genId } from '../../misc/gen-id'; +export default async function(blocker: User, blockee: User) { await Promise.all([ cancelRequest(blocker, blockee), cancelRequest(blockee, blocker), @@ -20,105 +18,90 @@ export default async function(blocker: IUser, blockee: IUser) { unFollow(blockee, blocker) ]); - await Blocking.insert({ + await Blockings.save({ + id: genId(), createdAt: new Date(), - blockerId: blocker._id, - blockeeId: blockee._id, + blockerId: blocker.id, + blockeeId: blockee.id, }); - if (isLocalUser(blocker) && isRemoteUser(blockee)) { + if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { const content = renderActivity(renderBlock(blocker, blockee)); deliver(blocker, content, blockee.inbox); } } -async function cancelRequest(follower: IUser, followee: IUser) { - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id +async function cancelRequest(follower: User, followee: User) { + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); if (request == null) { return; } - await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id - }); - - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - if (isLocalUser(followee)) { - packUser(followee, followee, { + if (Users.isLocalUser(followee)) { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); } - if (isLocalUser(follower)) { - packUser(followee, follower, { + if (Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } // リモートã«ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ã—ã¦ã„ãŸã‚‰UndoFollowé€ä¿¡ - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } // リモートã‹ã‚‰ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’å—ã‘ã¦ã„ãŸã‚‰Rejecté€ä¿¡ - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee)); deliver(followee, content, follower.inbox); } } -async function unFollow(follower: IUser, followee: IUser) { - const following = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id +async function unFollow(follower: User, followee: User) { + const following = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id }); if (following == null) { return; } - Following.remove({ - _id: following._id - }); + Followings.delete(following.id); //#region Decrement following count - User.update({ _id: follower._id }, { - $inc: { - followingCount: -1 - } - }); + Users.decrement({ id: follower.id }, 'followingCount', 1); //#endregion //#region Decrement followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: -1 - } - }); + Users.decrement({ id: followee.id }, 'followersCount', 1); //#endregion perUserFollowingChart.update(follower, followee, false); // Publish unfollow event - if (isLocalUser(follower)) { - packUser(followee, follower, { + if (Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } // リモートã«ãƒ•ã‚©ãƒãƒ¼ã‚’ã—ã¦ã„ãŸã‚‰UndoFollowé€ä¿¡ - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } diff --git a/src/services/blocking/delete.ts b/src/services/blocking/delete.ts index 099fa14b37a9a24c695eec402afc3619be20db1d..2c05cb7f3f8fc2430db3385aebaf976863db17bb 100644 --- a/src/services/blocking/delete.ts +++ b/src/services/blocking/delete.ts @@ -1,17 +1,17 @@ -import { isLocalUser, isRemoteUser, IUser } from '../../models/user'; -import Blocking from '../../models/blocking'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderBlock from '../../remote/activitypub/renderer/block'; import renderUndo from '../../remote/activitypub/renderer/undo'; import { deliver } from '../../queue'; import Logger from '../logger'; +import { User } from '../../models/entities/user'; +import { Blockings, Users } from '../../models'; const logger = new Logger('blocking/delete'); -export default async function(blocker: IUser, blockee: IUser) { - const blocking = await Blocking.findOne({ - blockerId: blocker._id, - blockeeId: blockee._id +export default async function(blocker: User, blockee: User) { + const blocking = await Blockings.findOne({ + blockerId: blocker.id, + blockeeId: blockee.id }); if (blocking == null) { @@ -19,12 +19,10 @@ export default async function(blocker: IUser, blockee: IUser) { return; } - Blocking.remove({ - _id: blocking._id - }); + Blockings.delete(blocking.id); // deliver if remote bloking - if (isLocalUser(blocker) && isRemoteUser(blockee)) { + if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker)); deliver(blocker, content, blockee.inbox); } diff --git a/src/services/chart/active-users.ts b/src/services/chart/active-users.ts deleted file mode 100644 index 2a4e1a97ac75335e5714ccb704fc523d1e061e5c..0000000000000000000000000000000000000000 --- a/src/services/chart/active-users.ts +++ /dev/null @@ -1,48 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import { IUser, isLocalUser } from '../../models/user'; - -/** - * アクティブユーザーã«é–¢ã™ã‚‹ãƒãƒ£ãƒ¼ãƒˆ - */ -type ActiveUsersLog = { - local: { - /** - * アクティブユーザー数 - */ - count: number; - }; - - remote: ActiveUsersLog['local']; -}; - -class ActiveUsersChart extends Chart<ActiveUsersLog> { - constructor() { - super('activeUsers'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: ActiveUsersLog): Promise<ActiveUsersLog> { - return { - local: { - count: 0 - }, - remote: { - count: 0 - } - }; - } - - @autobind - public async update(user: IUser) { - const update: Obj = { - count: 1 - }; - - await this.incIfUnique({ - [isLocalUser(user) ? 'local' : 'remote']: update - }, 'users', user._id.toHexString()); - } -} - -export default new ActiveUsersChart(); diff --git a/src/services/chart/charts/classes/active-users.ts b/src/services/chart/charts/classes/active-users.ts new file mode 100644 index 0000000000000000000000000000000000000000..5128150de6cf0b1ed20c6730200d5198e439fa06 --- /dev/null +++ b/src/services/chart/charts/classes/active-users.ts @@ -0,0 +1,35 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { name, schema } from '../schemas/active-users'; + +type ActiveUsersLog = SchemaType<typeof schema>; + +export default class ActiveUsersChart extends Chart<ActiveUsersLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: ActiveUsersLog): DeepPartial<ActiveUsersLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<ActiveUsersLog>> { + return {}; + } + + @autobind + public async update(user: User) { + const update: Obj = { + count: 1 + }; + + await this.incIfUnique({ + [Users.isLocalUser(user) ? 'local' : 'remote']: update + }, 'users', user.id); + } +} diff --git a/src/services/chart/charts/classes/drive.ts b/src/services/chart/charts/classes/drive.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae52df19ac3ceb49f1176dd6d264055cbe02b184 --- /dev/null +++ b/src/services/chart/charts/classes/drive.ts @@ -0,0 +1,69 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { DriveFiles } from '../../../../models'; +import { Not } from 'typeorm'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { name, schema } from '../schemas/drive'; + +type DriveLog = SchemaType<typeof schema>; + +export default class DriveChart extends Chart<DriveLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: DriveLog): DeepPartial<DriveLog> { + return { + local: { + totalCount: latest.local.totalCount, + totalSize: latest.local.totalSize, + }, + remote: { + totalCount: latest.remote.totalCount, + totalSize: latest.remote.totalSize, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<DriveLog>> { + const [localCount, remoteCount, localSize, remoteSize] = await Promise.all([ + DriveFiles.count({ userHost: null }), + DriveFiles.count({ userHost: Not(null) }), + DriveFiles.clacDriveUsageOfLocal(), + DriveFiles.clacDriveUsageOfRemote() + ]); + + return { + local: { + totalCount: localCount, + totalSize: localSize, + }, + remote: { + totalCount: remoteCount, + totalSize: remoteSize, + } + }; + } + + @autobind + public async update(file: DriveFile, isAdditional: boolean) { + const update: Obj = {}; + + update.totalCount = isAdditional ? 1 : -1; + update.totalSize = isAdditional ? file.size : -file.size; + if (isAdditional) { + update.incCount = 1; + update.incSize = file.size; + } else { + update.decCount = 1; + update.decSize = file.size; + } + + await this.inc({ + [file.userHost === null ? 'local' : 'remote']: update + }); + } +} diff --git a/src/services/chart/charts/classes/federation.ts b/src/services/chart/charts/classes/federation.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd2c497e7b8dfbdeaa05c4550908e536acaac255 --- /dev/null +++ b/src/services/chart/charts/classes/federation.ts @@ -0,0 +1,51 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Instances } from '../../../../models'; +import { name, schema } from '../schemas/federation'; + +type FederationLog = SchemaType<typeof schema>; + +export default class FederationChart extends Chart<FederationLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: FederationLog): DeepPartial<FederationLog> { + return { + instance: { + total: latest.instance.total, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<FederationLog>> { + const [total] = await Promise.all([ + Instances.count({}) + ]); + + return { + instance: { + total: total, + } + }; + } + + @autobind + public async update(isAdditional: boolean) { + const update: Obj = {}; + + update.total = isAdditional ? 1 : -1; + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + await this.inc({ + instance: update + }); + } +} diff --git a/src/services/chart/charts/classes/hashtag.ts b/src/services/chart/charts/classes/hashtag.ts new file mode 100644 index 0000000000000000000000000000000000000000..38c3a94f0cb21ce98301193a497389534dbbb8b8 --- /dev/null +++ b/src/services/chart/charts/classes/hashtag.ts @@ -0,0 +1,35 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { name, schema } from '../schemas/hashtag'; + +type HashtagLog = SchemaType<typeof schema>; + +export default class HashtagChart extends Chart<HashtagLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: HashtagLog): DeepPartial<HashtagLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<HashtagLog>> { + return {}; + } + + @autobind + public async update(hashtag: string, user: User) { + const update: Obj = { + count: 1 + }; + + await this.incIfUnique({ + [Users.isLocalUser(user) ? 'local' : 'remote']: update + }, 'users', user.id, hashtag); + } +} diff --git a/src/services/chart/charts/classes/instance.ts b/src/services/chart/charts/classes/instance.ts new file mode 100644 index 0000000000000000000000000000000000000000..974eac036b46eda7510bc5c273833ce47b2b8b8e --- /dev/null +++ b/src/services/chart/charts/classes/instance.ts @@ -0,0 +1,160 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { DriveFiles, Followings, Users, Notes } from '../../../../models'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { name, schema } from '../schemas/instance'; + +type InstanceLog = SchemaType<typeof schema>; + +export default class InstanceChart extends Chart<InstanceLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: InstanceLog): DeepPartial<InstanceLog> { + return { + notes: { + total: latest.notes.total, + }, + users: { + total: latest.users.total, + }, + following: { + total: latest.following.total, + }, + followers: { + total: latest.followers.total, + }, + drive: { + totalFiles: latest.drive.totalFiles, + totalUsage: latest.drive.totalUsage, + } + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<InstanceLog>> { + const [ + notesCount, + usersCount, + followingCount, + followersCount, + driveFiles, + driveUsage, + ] = await Promise.all([ + Notes.count({ userHost: group }), + Users.count({ host: group }), + Followings.count({ followerHost: group }), + Followings.count({ followeeHost: group }), + DriveFiles.count({ userHost: group }), + DriveFiles.clacDriveUsageOfHost(group), + ]); + + return { + notes: { + total: notesCount, + }, + users: { + total: usersCount, + }, + following: { + total: followingCount, + }, + followers: { + total: followersCount, + }, + drive: { + totalFiles: driveFiles, + totalUsage: driveUsage, + } + }; + } + + @autobind + public async requestReceived(host: string) { + await this.inc({ + requests: { + received: 1 + } + }, host); + } + + @autobind + public async requestSent(host: string, isSucceeded: boolean) { + const update: Obj = {}; + + if (isSucceeded) { + update.succeeded = 1; + } else { + update.failed = 1; + } + + await this.inc({ + requests: update + }, host); + } + + @autobind + public async newUser(host: string) { + await this.inc({ + users: { + total: 1, + inc: 1 + } + }, host); + } + + @autobind + public async updateNote(host: string, isAdditional: boolean) { + await this.inc({ + notes: { + total: isAdditional ? 1 : -1, + inc: isAdditional ? 1 : 0, + dec: isAdditional ? 0 : 1, + } + }, host); + } + + @autobind + public async updateFollowing(host: string, isAdditional: boolean) { + await this.inc({ + following: { + total: isAdditional ? 1 : -1, + inc: isAdditional ? 1 : 0, + dec: isAdditional ? 0 : 1, + } + }, host); + } + + @autobind + public async updateFollowers(host: string, isAdditional: boolean) { + await this.inc({ + followers: { + total: isAdditional ? 1 : -1, + inc: isAdditional ? 1 : 0, + dec: isAdditional ? 0 : 1, + } + }, host); + } + + @autobind + public async updateDrive(file: DriveFile, isAdditional: boolean) { + const update: Obj = {}; + + update.totalFiles = isAdditional ? 1 : -1; + update.totalUsage = isAdditional ? file.size : -file.size; + if (isAdditional) { + update.incFiles = 1; + update.incUsage = file.size; + } else { + update.decFiles = 1; + update.decUsage = file.size; + } + + await this.inc({ + drive: update + }, file.userHost); + } +} diff --git a/src/services/chart/charts/classes/network.ts b/src/services/chart/charts/classes/network.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b26e5c4c22f10c614c27f0e8dbe082088266322 --- /dev/null +++ b/src/services/chart/charts/classes/network.ts @@ -0,0 +1,34 @@ +import autobind from 'autobind-decorator'; +import Chart, { DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/network'; + +type NetworkLog = SchemaType<typeof schema>; + +export default class NetworkChart extends Chart<NetworkLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: NetworkLog): DeepPartial<NetworkLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<NetworkLog>> { + return {}; + } + + @autobind + public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) { + const inc: DeepPartial<NetworkLog> = { + incomingRequests: incomingRequests, + totalTime: time, + incomingBytes: incomingBytes, + outgoingBytes: outgoingBytes + }; + + await this.inc(inc); + } +} diff --git a/src/services/chart/charts/classes/notes.ts b/src/services/chart/charts/classes/notes.ts new file mode 100644 index 0000000000000000000000000000000000000000..85ccf000d8e33fb9c52fdb17a894b68eca91a640 --- /dev/null +++ b/src/services/chart/charts/classes/notes.ts @@ -0,0 +1,71 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Notes } from '../../../../models'; +import { Not } from 'typeorm'; +import { Note } from '../../../../models/entities/note'; +import { name, schema } from '../schemas/notes'; + +type NotesLog = SchemaType<typeof schema>; + +export default class NotesChart extends Chart<NotesLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: NotesLog): DeepPartial<NotesLog> { + return { + local: { + total: latest.local.total, + }, + remote: { + total: latest.remote.total, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<NotesLog>> { + const [localCount, remoteCount] = await Promise.all([ + Notes.count({ userHost: null }), + Notes.count({ userHost: Not(null) }) + ]); + + return { + local: { + total: localCount, + }, + remote: { + total: remoteCount, + } + }; + } + + @autobind + public async update(note: Note, isAdditional: boolean) { + const update: Obj = { + diffs: {} + }; + + update.total = isAdditional ? 1 : -1; + + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + if (note.replyId != null) { + update.diffs.reply = isAdditional ? 1 : -1; + } else if (note.renoteId != null) { + update.diffs.renote = isAdditional ? 1 : -1; + } else { + update.diffs.normal = isAdditional ? 1 : -1; + } + + await this.inc({ + [note.userHost === null ? 'local' : 'remote']: update + }); + } +} diff --git a/src/services/chart/charts/classes/per-user-drive.ts b/src/services/chart/charts/classes/per-user-drive.ts new file mode 100644 index 0000000000000000000000000000000000000000..822f4eda0f38fe034c097014324efa913d92d231 --- /dev/null +++ b/src/services/chart/charts/classes/per-user-drive.ts @@ -0,0 +1,52 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { DriveFiles } from '../../../../models'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { name, schema } from '../schemas/per-user-drive'; + +type PerUserDriveLog = SchemaType<typeof schema>; + +export default class PerUserDriveChart extends Chart<PerUserDriveLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserDriveLog): DeepPartial<PerUserDriveLog> { + return { + totalCount: latest.totalCount, + totalSize: latest.totalSize, + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserDriveLog>> { + const [count, size] = await Promise.all([ + DriveFiles.count({ userId: group }), + DriveFiles.clacDriveUsageOf(group) + ]); + + return { + totalCount: count, + totalSize: size, + }; + } + + @autobind + public async update(file: DriveFile, isAdditional: boolean) { + const update: Obj = {}; + + update.totalCount = isAdditional ? 1 : -1; + update.totalSize = isAdditional ? file.size : -file.size; + if (isAdditional) { + update.incCount = 1; + update.incSize = file.size; + } else { + update.decCount = 1; + update.decSize = file.size; + } + + await this.inc(update, file.userId); + } +} diff --git a/src/services/chart/charts/classes/per-user-following.ts b/src/services/chart/charts/classes/per-user-following.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3809a7c946298cc1cff0674cb388ebd3a037a64 --- /dev/null +++ b/src/services/chart/charts/classes/per-user-following.ts @@ -0,0 +1,91 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Followings, Users } from '../../../../models'; +import { Not } from 'typeorm'; +import { User } from '../../../../models/entities/user'; +import { name, schema } from '../schemas/per-user-following'; + +type PerUserFollowingLog = SchemaType<typeof schema>; + +export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserFollowingLog): DeepPartial<PerUserFollowingLog> { + return { + local: { + followings: { + total: latest.local.followings.total, + }, + followers: { + total: latest.local.followers.total, + } + }, + remote: { + followings: { + total: latest.remote.followings.total, + }, + followers: { + total: latest.remote.followers.total, + } + } + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserFollowingLog>> { + const [ + localFollowingsCount, + localFollowersCount, + remoteFollowingsCount, + remoteFollowersCount + ] = await Promise.all([ + Followings.count({ followerId: group, followeeHost: null }), + Followings.count({ followeeId: group, followerHost: null }), + Followings.count({ followerId: group, followeeHost: Not(null) }), + Followings.count({ followeeId: group, followerHost: Not(null) }) + ]); + + return { + local: { + followings: { + total: localFollowingsCount, + }, + followers: { + total: localFollowersCount, + } + }, + remote: { + followings: { + total: remoteFollowingsCount, + }, + followers: { + total: remoteFollowersCount, + } + } + }; + } + + @autobind + public async update(follower: User, followee: User, isFollow: boolean) { + const update: Obj = {}; + + update.total = isFollow ? 1 : -1; + + if (isFollow) { + update.inc = 1; + } else { + update.dec = 1; + } + + this.inc({ + [Users.isLocalUser(follower) ? 'local' : 'remote']: { followings: update } + }, follower.id); + this.inc({ + [Users.isLocalUser(followee) ? 'local' : 'remote']: { followers: update } + }, followee.id); + } +} diff --git a/src/services/chart/charts/classes/per-user-notes.ts b/src/services/chart/charts/classes/per-user-notes.ts new file mode 100644 index 0000000000000000000000000000000000000000..cccd49560453c1e3814ad1527e571296e0b86634 --- /dev/null +++ b/src/services/chart/charts/classes/per-user-notes.ts @@ -0,0 +1,58 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { SchemaType } from '../../../../misc/schema'; +import { Notes } from '../../../../models'; +import { Note } from '../../../../models/entities/note'; +import { name, schema } from '../schemas/per-user-notes'; + +type PerUserNotesLog = SchemaType<typeof schema>; + +export default class PerUserNotesChart extends Chart<PerUserNotesLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserNotesLog): DeepPartial<PerUserNotesLog> { + return { + total: latest.total, + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserNotesLog>> { + const [count] = await Promise.all([ + Notes.count({ userId: group }), + ]); + + return { + total: count, + }; + } + + @autobind + public async update(user: User, note: Note, isAdditional: boolean) { + const update: Obj = { + diffs: {} + }; + + update.total = isAdditional ? 1 : -1; + + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + if (note.replyId != null) { + update.diffs.reply = isAdditional ? 1 : -1; + } else if (note.renoteId != null) { + update.diffs.renote = isAdditional ? 1 : -1; + } else { + update.diffs.normal = isAdditional ? 1 : -1; + } + + await this.inc(update, user.id); + } +} diff --git a/src/services/chart/charts/classes/per-user-reactions.ts b/src/services/chart/charts/classes/per-user-reactions.ts new file mode 100644 index 0000000000000000000000000000000000000000..124fb4153cbd8302b8ea2c2d39c22cffa9a26772 --- /dev/null +++ b/src/services/chart/charts/classes/per-user-reactions.ts @@ -0,0 +1,32 @@ +import autobind from 'autobind-decorator'; +import Chart, { DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { Note } from '../../../../models/entities/note'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { name, schema } from '../schemas/per-user-reactions'; + +type PerUserReactionsLog = SchemaType<typeof schema>; + +export default class PerUserReactionsChart extends Chart<PerUserReactionsLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserReactionsLog): DeepPartial<PerUserReactionsLog> { + return {}; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserReactionsLog>> { + return {}; + } + + @autobind + public async update(user: User, note: Note) { + this.inc({ + [Users.isLocalUser(user) ? 'local' : 'remote']: { count: 1 } + }, note.userId); + } +} diff --git a/src/services/chart/charts/classes/test-grouped.ts b/src/services/chart/charts/classes/test-grouped.ts new file mode 100644 index 0000000000000000000000000000000000000000..e32cbcf41630e0a1b3c9d3862c39482aba67951c --- /dev/null +++ b/src/services/chart/charts/classes/test-grouped.ts @@ -0,0 +1,47 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/test-grouped'; + +type TestGroupedLog = SchemaType<typeof schema>; + +export default class TestGroupedChart extends Chart<TestGroupedLog> { + private total = {} as Record<string, number>; + + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: TestGroupedLog): DeepPartial<TestGroupedLog> { + return { + foo: { + total: latest.foo.total, + }, + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<TestGroupedLog>> { + return { + foo: { + total: this.total[group], + }, + }; + } + + @autobind + public async increment(group: string) { + if (this.total[group] == null) this.total[group] = 0; + + const update: Obj = {}; + + update.total = 1; + update.inc = 1; + this.total[group]++; + + await this.inc({ + foo: update + }, group); + } +} diff --git a/src/services/chart/charts/classes/test-unique.ts b/src/services/chart/charts/classes/test-unique.ts new file mode 100644 index 0000000000000000000000000000000000000000..1eb396c2936e22d19d3f1643418c503738a5ebe2 --- /dev/null +++ b/src/services/chart/charts/classes/test-unique.ts @@ -0,0 +1,29 @@ +import autobind from 'autobind-decorator'; +import Chart, { DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/test-unique'; + +type TestUniqueLog = SchemaType<typeof schema>; + +export default class TestUniqueChart extends Chart<TestUniqueLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: TestUniqueLog): DeepPartial<TestUniqueLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<TestUniqueLog>> { + return {}; + } + + @autobind + public async uniqueIncrement(key: string) { + await this.incIfUnique({ + foo: 1 + }, 'foos', key); + } +} diff --git a/src/services/chart/charts/classes/test.ts b/src/services/chart/charts/classes/test.ts new file mode 100644 index 0000000000000000000000000000000000000000..57c22822f2c648b742f03a162bd541df2bdac322 --- /dev/null +++ b/src/services/chart/charts/classes/test.ts @@ -0,0 +1,45 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/test'; + +type TestLog = SchemaType<typeof schema>; + +export default class TestChart extends Chart<TestLog> { + private total = 0; + + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: TestLog): DeepPartial<TestLog> { + return { + foo: { + total: latest.foo.total, + }, + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<TestLog>> { + return { + foo: { + total: this.total, + }, + }; + } + + @autobind + public async increment() { + const update: Obj = {}; + + update.total = 1; + update.inc = 1; + this.total++; + + await this.inc({ + foo: update + }); + } +} diff --git a/src/services/chart/charts/classes/users.ts b/src/services/chart/charts/classes/users.ts new file mode 100644 index 0000000000000000000000000000000000000000..eec30de8dc63731a53b6155148024b1080c03402 --- /dev/null +++ b/src/services/chart/charts/classes/users.ts @@ -0,0 +1,60 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { Not } from 'typeorm'; +import { User } from '../../../../models/entities/user'; +import { name, schema } from '../schemas/users'; + +type UsersLog = SchemaType<typeof schema>; + +export default class UsersChart extends Chart<UsersLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: UsersLog): DeepPartial<UsersLog> { + return { + local: { + total: latest.local.total, + }, + remote: { + total: latest.remote.total, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<UsersLog>> { + const [localCount, remoteCount] = await Promise.all([ + Users.count({ host: null }), + Users.count({ host: Not(null) }) + ]); + + return { + local: { + total: localCount, + }, + remote: { + total: remoteCount, + } + }; + } + + @autobind + public async update(user: User, isAdditional: boolean) { + const update: Obj = {}; + + update.total = isAdditional ? 1 : -1; + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + await this.inc({ + [Users.isLocalUser(user) ? 'local' : 'remote']: update + }); + } +} diff --git a/src/services/chart/charts/schemas/active-users.ts b/src/services/chart/charts/schemas/active-users.ts new file mode 100644 index 0000000000000000000000000000000000000000..da8c63389c80c6d3ea602197a9bfc567ee062724 --- /dev/null +++ b/src/services/chart/charts/schemas/active-users.ts @@ -0,0 +1,28 @@ +export const logSchema = { + /** + * アクティブユーザー数 + */ + count: { + type: 'number' as 'number', + description: 'アクティブユーザー数', + }, +}; + +/** + * アクティブユーザーã«é–¢ã™ã‚‹ãƒãƒ£ãƒ¼ãƒˆ + */ +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'activeUsers'; diff --git a/src/services/chart/charts/schemas/drive.ts b/src/services/chart/charts/schemas/drive.ts new file mode 100644 index 0000000000000000000000000000000000000000..47530e841761037967a62fd00c904fd8852b7215 --- /dev/null +++ b/src/services/chart/charts/schemas/drive.ts @@ -0,0 +1,65 @@ +const logSchema = { + /** + * 集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 + */ + totalCount: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' + }, + + /** + * 集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイルã®åˆè¨ˆã‚µã‚¤ã‚º + */ + totalSize: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイルã®åˆè¨ˆã‚µã‚¤ã‚º' + }, + + /** + * å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 + */ + incCount: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' + }, + + /** + * å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ + */ + incSize: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–使用é‡' + }, + + /** + * 減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 + */ + decCount: { + type: 'number' as 'number', + description: '減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' + }, + + /** + * 減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ + */ + decSize: { + type: 'number' as 'number', + description: '減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–使用é‡' + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'drive'; diff --git a/src/services/chart/charts/schemas/federation.ts b/src/services/chart/charts/schemas/federation.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1d275fc95f5f56450d847260007c6e8c6bd610b --- /dev/null +++ b/src/services/chart/charts/schemas/federation.ts @@ -0,0 +1,27 @@ +/** + * フェデレーションã«é–¢ã™ã‚‹ãƒãƒ£ãƒ¼ãƒˆ + */ +export const schema = { + type: 'object' as 'object', + properties: { + instance: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: 'インスタンス数ã®åˆè¨ˆ' + }, + inc: { + type: 'number' as 'number', + description: 'å¢—åŠ ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹æ•°' + }, + dec: { + type: 'number' as 'number', + description: '減少インスタンス数' + }, + } + } + } +}; + +export const name = 'federation'; diff --git a/src/services/chart/charts/schemas/hashtag.ts b/src/services/chart/charts/schemas/hashtag.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1904b67012367dddd73d59ebef158c268b9a21b --- /dev/null +++ b/src/services/chart/charts/schemas/hashtag.ts @@ -0,0 +1,28 @@ +export const logSchema = { + /** + * 投稿ã•ã‚ŒãŸæ•° + */ + count: { + type: 'number' as 'number', + description: '投稿ã•ã‚ŒãŸæ•°', + }, +}; + +/** + * ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã«é–¢ã™ã‚‹ãƒãƒ£ãƒ¼ãƒˆ + */ +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'hashtag'; diff --git a/src/services/chart/charts/schemas/instance.ts b/src/services/chart/charts/schemas/instance.ts new file mode 100644 index 0000000000000000000000000000000000000000..af46b33629f5f387d6c826c389918dca8c03d9ff --- /dev/null +++ b/src/services/chart/charts/schemas/instance.ts @@ -0,0 +1,124 @@ +/** + * インスタンスã”ã¨ã®ãƒãƒ£ãƒ¼ãƒˆ + */ +export const schema = { + type: 'object' as 'object', + properties: { + requests: { + type: 'object' as 'object', + properties: { + failed: { + type: 'number' as 'number', + description: '失敗ã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆæ•°' + }, + succeeded: { + type: 'number' as 'number', + description: 'æˆåŠŸã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆæ•°' + }, + received: { + type: 'number' as 'number', + description: 'å—ä¿¡ã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆæ•°' + }, + } + }, + notes: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨æŠ•ç¨¿æ•°' + }, + inc: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸæŠ•ç¨¿æ•°' + }, + dec: { + type: 'number' as 'number', + description: '減少ã—ãŸæŠ•ç¨¿æ•°' + }, + } + }, + users: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨ãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°' + }, + inc: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°' + }, + dec: { + type: 'number' as 'number', + description: '減少ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°' + }, + } + }, + following: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨ãƒ•ã‚©ãƒãƒ¼æ•°' + }, + inc: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸãƒ•ã‚©ãƒãƒ¼æ•°' + }, + dec: { + type: 'number' as 'number', + description: '減少ã—ãŸãƒ•ã‚©ãƒãƒ¼æ•°' + }, + } + }, + followers: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨ãƒ•ã‚©ãƒãƒ¯ãƒ¼æ•°' + }, + inc: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸãƒ•ã‚©ãƒãƒ¯ãƒ¼æ•°' + }, + dec: { + type: 'number' as 'number', + description: '減少ã—ãŸãƒ•ã‚©ãƒãƒ¯ãƒ¼æ•°' + }, + } + }, + drive: { + type: 'object' as 'object', + properties: { + totalFiles: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' + }, + totalUsage: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイルã®åˆè¨ˆã‚µã‚¤ã‚º' + }, + incFiles: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' + }, + incUsage: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–使用é‡' + }, + decFiles: { + type: 'number' as 'number', + description: '減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' + }, + decUsage: { + type: 'number' as 'number', + description: '減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–使用é‡' + }, + } + }, + } +}; + +export const name = 'instance'; diff --git a/src/services/chart/charts/schemas/network.ts b/src/services/chart/charts/schemas/network.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ef530c07cc0ca2f2b0ca426af59f4751afae902 --- /dev/null +++ b/src/services/chart/charts/schemas/network.ts @@ -0,0 +1,30 @@ +/** + * ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«é–¢ã™ã‚‹ãƒãƒ£ãƒ¼ãƒˆ + */ +export const schema = { + type: 'object' as 'object', + properties: { + incomingRequests: { + type: 'number' as 'number', + description: 'å—ä¿¡ã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆæ•°' + }, + outgoingRequests: { + type: 'number' as 'number', + description: 'é€ä¿¡ã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆæ•°' + }, + totalTime: { + type: 'number' as 'number', + description: 'å¿œç”時間ã®åˆè¨ˆ' // TIP: (totalTime / incomingRequests) ã§ã²ã¨ã¤ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¹³å‡ã§ã©ã‚Œãらã„ã®æ™‚é–“ãŒã‹ã‹ã£ãŸã‹çŸ¥ã‚Œã‚‹ + }, + incomingBytes: { + type: 'number' as 'number', + description: 'åˆè¨ˆå—信データé‡' + }, + outgoingBytes: { + type: 'number' as 'number', + description: 'åˆè¨ˆé€ä¿¡ãƒ‡ãƒ¼ã‚¿é‡' + }, + } +}; + +export const name = 'network'; diff --git a/src/services/chart/charts/schemas/notes.ts b/src/services/chart/charts/schemas/notes.ts new file mode 100644 index 0000000000000000000000000000000000000000..133d1e3730a22c5ab086b8a309b024a595e0b527 --- /dev/null +++ b/src/services/chart/charts/schemas/notes.ts @@ -0,0 +1,52 @@ +const logSchema = { + total: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨æŠ•ç¨¿æ•°' + }, + + inc: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸæŠ•ç¨¿æ•°' + }, + + dec: { + type: 'number' as 'number', + description: '減少ã—ãŸæŠ•ç¨¿æ•°' + }, + + diffs: { + type: 'object' as 'object', + properties: { + normal: { + type: 'number' as 'number', + description: '通常ã®æŠ•ç¨¿æ•°ã®å·®åˆ†' + }, + + reply: { + type: 'number' as 'number', + description: 'リプライã®æŠ•ç¨¿æ•°ã®å·®åˆ†' + }, + + renote: { + type: 'number' as 'number', + description: 'Renoteã®æŠ•ç¨¿æ•°ã®å·®åˆ†' + }, + } + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'notes'; diff --git a/src/services/chart/charts/schemas/per-user-drive.ts b/src/services/chart/charts/schemas/per-user-drive.ts new file mode 100644 index 0000000000000000000000000000000000000000..713bd7ed845e68ee2d5226c7193f6577d98006e4 --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-drive.ts @@ -0,0 +1,54 @@ +export const schema = { + type: 'object' as 'object', + properties: { + /** + * 集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 + */ + totalCount: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' + }, + + /** + * 集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイルã®åˆè¨ˆã‚µã‚¤ã‚º + */ + totalSize: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイルã®åˆè¨ˆã‚µã‚¤ã‚º' + }, + + /** + * å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 + */ + incCount: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' + }, + + /** + * å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ + */ + incSize: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–使用é‡' + }, + + /** + * 減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 + */ + decCount: { + type: 'number' as 'number', + description: '減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' + }, + + /** + * 減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ + */ + decSize: { + type: 'number' as 'number', + description: '減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–使用é‡' + }, + } +}; + +export const name = 'perUserDrive'; diff --git a/src/services/chart/charts/schemas/per-user-following.ts b/src/services/chart/charts/schemas/per-user-following.ts new file mode 100644 index 0000000000000000000000000000000000000000..d6ca1130e0f1f2a8d1239d995c72d06c18bfa74e --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-following.ts @@ -0,0 +1,81 @@ +export const logSchema = { + /** + * フォãƒãƒ¼ã—ã¦ã„ã‚‹ + */ + followings: { + type: 'object' as 'object', + properties: { + /** + * フォãƒãƒ¼ã—ã¦ã„ã‚‹åˆè¨ˆ + */ + total: { + type: 'number' as 'number', + description: 'フォãƒãƒ¼ã—ã¦ã„ã‚‹åˆè¨ˆ', + }, + + /** + * フォãƒãƒ¼ã—ãŸæ•° + */ + inc: { + type: 'number' as 'number', + description: 'フォãƒãƒ¼ã—ãŸæ•°', + }, + + /** + * フォãƒãƒ¼è§£é™¤ã—ãŸæ•° + */ + dec: { + type: 'number' as 'number', + description: 'フォãƒãƒ¼è§£é™¤ã—ãŸæ•°', + }, + } + }, + + /** + * フォãƒãƒ¼ã•ã‚Œã¦ã„ã‚‹ + */ + followers: { + type: 'object' as 'object', + properties: { + /** + * フォãƒãƒ¼ã•ã‚Œã¦ã„ã‚‹åˆè¨ˆ + */ + total: { + type: 'number' as 'number', + description: 'フォãƒãƒ¼ã•ã‚Œã¦ã„ã‚‹åˆè¨ˆ', + }, + + /** + * フォãƒãƒ¼ã•ã‚ŒãŸæ•° + */ + inc: { + type: 'number' as 'number', + description: 'フォãƒãƒ¼ã•ã‚ŒãŸæ•°', + }, + + /** + * フォãƒãƒ¼è§£é™¤ã•ã‚ŒãŸæ•° + */ + dec: { + type: 'number' as 'number', + description: 'フォãƒãƒ¼è§£é™¤ã•ã‚ŒãŸæ•°', + }, + } + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'perUserFollowing'; diff --git a/src/services/chart/charts/schemas/per-user-notes.ts b/src/services/chart/charts/schemas/per-user-notes.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c448c4cee2464c83706320dcdc7e533b62e8736 --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-notes.ts @@ -0,0 +1,41 @@ +export const schema = { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨æŠ•ç¨¿æ•°' + }, + + inc: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸæŠ•ç¨¿æ•°' + }, + + dec: { + type: 'number' as 'number', + description: '減少ã—ãŸæŠ•ç¨¿æ•°' + }, + + diffs: { + type: 'object' as 'object', + properties: { + normal: { + type: 'number' as 'number', + description: '通常ã®æŠ•ç¨¿æ•°ã®å·®åˆ†' + }, + + reply: { + type: 'number' as 'number', + description: 'リプライã®æŠ•ç¨¿æ•°ã®å·®åˆ†' + }, + + renote: { + type: 'number' as 'number', + description: 'Renoteã®æŠ•ç¨¿æ•°ã®å·®åˆ†' + }, + } + }, + } +}; + +export const name = 'perUserNotes'; diff --git a/src/services/chart/charts/schemas/per-user-reactions.ts b/src/services/chart/charts/schemas/per-user-reactions.ts new file mode 100644 index 0000000000000000000000000000000000000000..1278184da65837f810ceb9a0cf122690d1d5d355 --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-reactions.ts @@ -0,0 +1,28 @@ +export const logSchema = { + /** + * フォãƒãƒ¼ã—ã¦ã„ã‚‹åˆè¨ˆ + */ + count: { + type: 'number' as 'number', + description: 'リアクションã•ã‚ŒãŸæ•°', + }, +}; + +/** + * ユーザーã”ã¨ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã«é–¢ã™ã‚‹ãƒãƒ£ãƒ¼ãƒˆ + */ +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'perUserReaction'; diff --git a/src/services/chart/charts/schemas/test-grouped.ts b/src/services/chart/charts/schemas/test-grouped.ts new file mode 100644 index 0000000000000000000000000000000000000000..acf3fddb3157dbe34cece0a149bf3e92ad736e97 --- /dev/null +++ b/src/services/chart/charts/schemas/test-grouped.ts @@ -0,0 +1,26 @@ +export const schema = { + type: 'object' as 'object', + properties: { + foo: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '' + }, + + inc: { + type: 'number' as 'number', + description: '' + }, + + dec: { + type: 'number' as 'number', + description: '' + }, + } + } + } +}; + +export const name = 'testGrouped'; diff --git a/src/services/chart/charts/schemas/test-unique.ts b/src/services/chart/charts/schemas/test-unique.ts new file mode 100644 index 0000000000000000000000000000000000000000..8fcfbf3c72342013e599c06ecf33a3098c898684 --- /dev/null +++ b/src/services/chart/charts/schemas/test-unique.ts @@ -0,0 +1,11 @@ +export const schema = { + type: 'object' as 'object', + properties: { + foo: { + type: 'number' as 'number', + description: '' + }, + } +}; + +export const name = 'testUnique'; diff --git a/src/services/chart/charts/schemas/test.ts b/src/services/chart/charts/schemas/test.ts new file mode 100644 index 0000000000000000000000000000000000000000..b1344500bfb3140c6bb4362c589161ee6af3ff59 --- /dev/null +++ b/src/services/chart/charts/schemas/test.ts @@ -0,0 +1,26 @@ +export const schema = { + type: 'object' as 'object', + properties: { + foo: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '' + }, + + inc: { + type: 'number' as 'number', + description: '' + }, + + dec: { + type: 'number' as 'number', + description: '' + }, + } + } + } +}; + +export const name = 'test'; diff --git a/src/services/chart/charts/schemas/users.ts b/src/services/chart/charts/schemas/users.ts new file mode 100644 index 0000000000000000000000000000000000000000..db7e2dd057b1e02aa432f6881e2c7db83b53620d --- /dev/null +++ b/src/services/chart/charts/schemas/users.ts @@ -0,0 +1,41 @@ +const logSchema = { + /** + * 集計期間時点ã§ã®ã€å…¨ãƒ¦ãƒ¼ã‚¶ãƒ¼æ•° + */ + total: { + type: 'number' as 'number', + description: '集計期間時点ã§ã®ã€å…¨ãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°' + }, + + /** + * å¢—åŠ ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•° + */ + inc: { + type: 'number' as 'number', + description: 'å¢—åŠ ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°' + }, + + /** + * 減少ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•° + */ + dec: { + type: 'number' as 'number', + description: '減少ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°' + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'users'; diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a60b1a0a3a78d4648478c683b5753d6dc20ae83 --- /dev/null +++ b/src/services/chart/core.ts @@ -0,0 +1,460 @@ +/** + * ãƒãƒ£ãƒ¼ãƒˆã‚¨ãƒ³ã‚¸ãƒ³ + * + * Tests located in test/chart + */ + +import * as moment from 'moment'; +import * as nestedProperty from 'nested-property'; +import autobind from 'autobind-decorator'; +import Logger from '../logger'; +import { Schema } from '../../misc/schema'; +import { EntitySchema, getRepository, Repository, LessThan, MoreThanOrEqual } from 'typeorm'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; + +const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test'); + +const utc = moment.utc; + +export type Obj = { [key: string]: any }; + +export type DeepPartial<T> = { + [P in keyof T]?: DeepPartial<T[P]>; +}; + +type ArrayValue<T> = { + [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>; +}; + +type Span = 'day' | 'hour'; + +type Log = { + id: number; + + /** + * 集計ã®ã‚°ãƒ«ãƒ¼ãƒ— + */ + group: string | null; + + /** + * 集計日時ã®Unixタイムスタンプ(秒) + */ + date: number; + + /** + * 集計期間 + */ + span: Span; + + /** + * ユニークインクリメント用 + */ + unique?: Record<string, any>; +}; + +const camelToSnake = (str: string) => { + return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase()); +}; + +/** + * 様々ãªãƒãƒ£ãƒ¼ãƒˆã®ç®¡ç†ã‚’å¸ã‚‹ã‚¯ãƒ©ã‚¹ + */ +export default abstract class Chart<T extends Record<string, any>> { + private static readonly columnPrefix = '___'; + private static readonly columnDot = '_'; + + private name: string; + public schema: Schema; + protected repository: Repository<Log>; + protected abstract genNewLog(latest: T): DeepPartial<T>; + protected abstract async fetchActual(group?: string): Promise<DeepPartial<T>>; + + @autobind + private static convertSchemaToFlatColumnDefinitions(schema: Schema) { + const columns = {} as any; + const flatColumns = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}${this.columnDot}${k}` : k; + if (v.type === 'object') { + flatColumns(v.properties, p); + } else { + columns[this.columnPrefix + p] = { + type: 'integer', + }; + } + } + }; + flatColumns(schema.properties); + return columns; + } + + @autobind + private static convertFlattenColumnsToObject(x: Record<string, number>) { + const obj = {} as any; + for (const k of Object.keys(x).filter(k => k.startsWith(Chart.columnPrefix))) { + // now k is ___x_y_z + const path = k.substr(Chart.columnPrefix.length).split(Chart.columnDot).join('.'); + nestedProperty.set(obj, path, x[k]); + } + return obj; + } + + @autobind + private static convertObjectToFlattenColumns(x: Record<string, any>) { + const columns = {} as Record<string, number>; + const flatten = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}${this.columnDot}${k}` : k; + if (typeof v === 'object') { + flatten(v, p); + } else { + columns[this.columnPrefix + p] = v; + } + } + }; + flatten(x); + return columns; + } + + @autobind + private static convertQuery(x: Record<string, any>) { + const query: Record<string, Function> = {}; + + const columns = Chart.convertObjectToFlattenColumns(x); + + for (const [k, v] of Object.entries(columns)) { + if (v > 0) query[k] = () => `"${k}" + ${v}`; + if (v < 0) query[k] = () => `"${k}" - ${v}`; + } + + return query; + } + + @autobind + private static momentToTimestamp(x: moment.Moment): Log['date'] { + return x.unix(); + } + + @autobind + public static schemaToEntity(name: string, schema: Schema): EntitySchema { + return new EntitySchema({ + name: `__chart__${camelToSnake(name)}`, + columns: { + id: { + type: 'integer', + primary: true, + generated: true + }, + date: { + type: 'integer', + }, + group: { + type: 'varchar', + length: 128, + nullable: true + }, + span: { + type: 'enum', + enum: ['hour', 'day'] + }, + unique: { + type: 'jsonb', + default: {} + }, + ...Chart.convertSchemaToFlatColumnDefinitions(schema) + }, + }); + } + + constructor(name: string, schema: Schema, grouped = false) { + this.name = name; + this.schema = schema; + const entity = Chart.schemaToEntity(name, schema); + + const keys = ['span', 'date']; + if (grouped) keys.push('group'); + + entity.options.uniques = [{ + columns: keys + }]; + + this.repository = getRepository<Log>(entity); + } + + @autobind + private getNewLog(latest?: T): T { + const log = latest ? this.genNewLog(latest) : {}; + const flatColumns = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}.${k}` : k; + if (v.type === 'object') { + flatColumns(v.properties, p); + } else { + if (nestedProperty.get(log, p) == null) { + nestedProperty.set(log, p, 0); + } + } + } + }; + flatColumns(this.schema.properties); + return log as T; + } + + @autobind + private getCurrentDate(): [number, number, number, number] { + const now = moment().utc(); + + const y = now.year(); + const m = now.month(); + const d = now.date(); + const h = now.hour(); + + return [y, m, d, h]; + } + + @autobind + private getLatestLog(span: Span, group: string = null): Promise<Log> { + return this.repository.findOne({ + group: group, + span: span + }, { + order: { + date: -1 + } + }); + } + + @autobind + private async getCurrentLog(span: Span, group: string = null): Promise<Log> { + const [y, m, d, h] = this.getCurrentDate(); + + const current = + span == 'day' ? utc([y, m, d]) : + span == 'hour' ? utc([y, m, d, h]) : + null; + + // ç¾åœ¨(今日ã¾ãŸã¯ä»Šã®Hour)ã®ãƒã‚° + const currentLog = await this.repository.findOne({ + span: span, + date: Chart.momentToTimestamp(current), + ...(group ? { group: group } : {}) + }); + + // ãƒã‚°ãŒã‚ã‚Œã°ãれを返ã—ã¦çµ‚了 + if (currentLog != null) { + return currentLog; + } + + let log: Log; + let data: T; + + // 集計期間ãŒå¤‰ã‚ã£ã¦ã‹ã‚‰ã€åˆã‚ã¦ã®ãƒãƒ£ãƒ¼ãƒˆæ›´æ–°ãªã‚‰ + // 最も最近ã®ãƒã‚°ã‚’æŒã£ã¦ãã‚‹ + // * 例ãˆã°é›†è¨ˆæœŸé–“ãŒã€Œæ—¥ã€ã§ã‚ã‚‹å ´åˆã§è€ƒãˆã‚‹ã¨ã€ + // * 昨日何もãƒãƒ£ãƒ¼ãƒˆã‚’æ›´æ–°ã™ã‚‹ã‚ˆã†ãªå‡ºæ¥äº‹ãŒãªã‹ã£ãŸå ´åˆã¯ã€ + // * ãƒã‚°ãŒãã‚‚ãも作られãšãƒ‰ã‚ュメントãŒå˜åœ¨ã—ãªã„ã¨ã„ã†ã“ã¨ãŒã‚ã‚Šå¾—ã‚‹ãŸã‚〠+ // * 「昨日ã®ã€ã¨æ±ºã‚打ã¡ã›ãšã«ã€Œã‚‚ã£ã¨ã‚‚最近ã®ã€ã¨ã—ã¾ã™ + const latest = await this.getLatestLog(span, group); + + if (latest != null) { + const obj = Chart.convertFlattenColumnsToObject( + latest as Record<string, any>); + + // 空ãƒã‚°ãƒ‡ãƒ¼ã‚¿ã‚’ä½œæˆ + data = await this.getNewLog(obj); + } else { + // ãƒã‚°ãŒå˜åœ¨ã—ãªã‹ã£ãŸã‚‰ + // (Misskeyインスタンスを建ã¦ã¦åˆã‚ã¦ã®ãƒãƒ£ãƒ¼ãƒˆæ›´æ–°æ™‚) + + // åˆæœŸãƒã‚°ãƒ‡ãƒ¼ã‚¿ã‚’ä½œæˆ + data = await this.getNewLog(null); + + logger.info(`${this.name}: Initial commit created`); + } + + try { + // æ–°è¦ãƒã‚°æŒ¿å…¥ + log = await this.repository.save({ + group: group, + span: span, + date: Chart.momentToTimestamp(current), + ...Chart.convertObjectToFlattenColumns(data) + }); + } catch (e) { + // duplicate key error + // 並列動作ã—ã¦ã„ã‚‹ä»–ã®ãƒãƒ£ãƒ¼ãƒˆã‚¨ãƒ³ã‚¸ãƒ³ãƒ—ãƒã‚»ã‚¹ã¨å‡¦ç†ãŒé‡ãªã‚‹å ´åˆãŒã‚ã‚‹ + // ãã®å ´åˆã¯å†åº¦æœ€ã‚‚æ–°ã—ã„ãƒã‚°ã‚’æŒã£ã¦ãã‚‹ + if (isDuplicateKeyValueError(e)) { + log = await this.getLatestLog(span, group); + } else { + logger.error(e); + throw e; + } + } + + return log; + } + + @autobind + protected commit(query: Record<string, Function>, group: string = null, uniqueKey?: string, uniqueValue?: string): Promise<any> { + const update = async (log: Log) => { + // ユニークインクリメントã®å ´åˆã€æŒ‡å®šã®ã‚ーã«æŒ‡å®šã®å€¤ãŒæ—¢ã«å˜åœ¨ã—ã¦ã„ãŸã‚‰å¼¾ã + if ( + uniqueKey && + log.unique[uniqueKey] && + log.unique[uniqueKey].includes(uniqueValue) + ) return; + + // ユニークインクリメントã®æŒ‡å®šã®ã‚ーã«å€¤ã‚’è¿½åŠ + if (uniqueKey) { + if (log.unique[uniqueKey]) { + const sql = `jsonb_set("unique", '{${uniqueKey}}', ("unique"->>'${uniqueKey}')::jsonb || '["${uniqueValue}"]'::jsonb)`; + query['unique'] = () => sql; + } else { + const sql = `jsonb_set("unique", '{${uniqueKey}}', '["${uniqueValue}"]')`; + query['unique'] = () => sql; + } + } + + // ãƒã‚°æ›´æ–° + await this.repository.createQueryBuilder() + .update() + .set(query) + .where('id = :id', { id: log.id }) + .execute(); + }; + + return Promise.all([ + this.getCurrentLog('day', group).then(log => update(log)), + this.getCurrentLog('hour', group).then(log => update(log)), + ]); + } + + @autobind + protected async inc(inc: DeepPartial<T>, group: string = null): Promise<void> { + await this.commit(Chart.convertQuery(inc as any), group); + } + + @autobind + protected async incIfUnique(inc: DeepPartial<T>, key: string, value: string, group: string = null): Promise<void> { + await this.commit(Chart.convertQuery(inc as any), group, key, value); + } + + @autobind + public async getChart(span: Span, range: number, group: string = null): Promise<ArrayValue<T>> { + const [y, m, d, h] = this.getCurrentDate(); + + const gt = + span == 'day' ? utc([y, m, d]).subtract(range, 'days') : + span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') : + null; + + // ãƒã‚°å–å¾— + let logs = await this.repository.find({ + where: { + group: group, + span: span, + date: MoreThanOrEqual(Chart.momentToTimestamp(gt)) + }, + order: { + date: -1 + }, + }); + + // è¦æ±‚ã•ã‚ŒãŸç¯„囲ã«ãƒã‚°ãŒã²ã¨ã¤ã‚‚ãªã‹ã£ãŸã‚‰ + if (logs.length === 0) { + // ã‚‚ã£ã¨ã‚‚æ–°ã—ã„ãƒã‚°ã‚’æŒã£ã¦ãã‚‹ + // (ã™ããªãã¨ã‚‚ã²ã¨ã¤ãƒã‚°ãŒç„¡ã„ã¨éš™é–“埋ã‚ã§ããªã„ãŸã‚) + const recentLog = await this.repository.findOne({ + group: group, + span: span + }, { + order: { + date: -1 + }, + }); + + if (recentLog) { + logs = [recentLog]; + } + + // è¦æ±‚ã•ã‚ŒãŸç¯„囲ã®æœ€ã‚‚å¤ã„箇所ã«ä½ç½®ã™ã‚‹ãƒã‚°ãŒå˜åœ¨ã—ãªã‹ã£ãŸã‚‰ + } else if (!utc(logs[logs.length - 1].date * 1000).isSame(gt)) { + // è¦æ±‚ã•ã‚ŒãŸç¯„囲ã®æœ€ã‚‚å¤ã„箇所時点ã§ã®æœ€ã‚‚æ–°ã—ã„ãƒã‚°ã‚’æŒã£ã¦ãã¦æœ«å°¾ã«è¿½åŠ ã™ã‚‹ + // (隙間埋ã‚ã§ããªã„ãŸã‚) + const outdatedLog = await this.repository.findOne({ + group: group, + span: span, + date: LessThan(Chart.momentToTimestamp(gt)) + }, { + order: { + date: -1 + }, + }); + + if (outdatedLog) { + logs.push(outdatedLog); + } + } + + const chart: T[] = []; + + // æ•´å½¢ + for (let i = (range - 1); i >= 0; i--) { + const current = + span == 'day' ? utc([y, m, d]).subtract(i, 'days') : + span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') : + null; + + const log = logs.find(l => utc(l.date * 1000).isSame(current)); + + if (log) { + const data = Chart.convertFlattenColumnsToObject(log as Record<string, any>); + chart.unshift(data); + } else { + // 隙間埋゠+ const latest = logs.find(l => utc(l.date * 1000).isBefore(current)); + const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null; + chart.unshift(this.getNewLog(data)); + } + } + + const res: ArrayValue<T> = {} as any; + + /** + * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }] + * ã‚’ + * { foo: [1, 2, 3], bar: [5, 6, 7] } + * ã«ã™ã‚‹ + */ + const dive = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}.${k}` : k; + if (typeof v == 'object') { + dive(v, p); + } else { + nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p))); + } + } + }; + + dive(chart[0]); + + return res; + } +} + +export function convertLog(logSchema: Schema): Schema { + const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy + if (v.type === 'number') { + v.type = 'array'; + v.items = { + type: 'number' + }; + } else if (v.type === 'object') { + for (const k of Object.keys(v.properties)) { + v.properties[k] = convertLog(v.properties[k]); + } + } + return v; +} diff --git a/src/services/chart/drive.ts b/src/services/chart/drive.ts deleted file mode 100644 index dd23412c7dcc8c2fcb142e53aa92d427b95b61eb..0000000000000000000000000000000000000000 --- a/src/services/chart/drive.ts +++ /dev/null @@ -1,150 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; -import { isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -const logSchema = { - /** - * 集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 - */ - totalCount: { - type: 'number' as 'number', - description: '集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' - }, - - /** - * 集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイルã®åˆè¨ˆã‚µã‚¤ã‚º - */ - totalSize: { - type: 'number' as 'number', - description: '集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイルã®åˆè¨ˆã‚µã‚¤ã‚º' - }, - - /** - * å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 - */ - incCount: { - type: 'number' as 'number', - description: 'å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' - }, - - /** - * å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ - */ - incSize: { - type: 'number' as 'number', - description: 'å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–使用é‡' - }, - - /** - * 減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 - */ - decCount: { - type: 'number' as 'number', - description: '減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' - }, - - /** - * 減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ - */ - decSize: { - type: 'number' as 'number', - description: '減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–使用é‡' - }, -}; - -export const driveLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type DriveLog = SchemaType<typeof driveLogSchema>; - -class DriveChart extends Chart<DriveLog> { - constructor() { - super('drive'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: DriveLog): Promise<DriveLog> { - const calcSize = (local: boolean) => DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': local ? null : { $ne: null }, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(res => res.length > 0 ? res[0].usage : 0); - - const [localCount, remoteCount, localSize, remoteSize] = init ? await Promise.all([ - DriveFile.count({ 'metadata._user.host': null }), - DriveFile.count({ 'metadata._user.host': { $ne: null } }), - calcSize(true), - calcSize(false) - ]) : [ - latest ? latest.local.totalCount : 0, - latest ? latest.remote.totalCount : 0, - latest ? latest.local.totalSize : 0, - latest ? latest.remote.totalSize : 0 - ]; - - return { - local: { - totalCount: localCount, - totalSize: localSize, - incCount: 0, - incSize: 0, - decCount: 0, - decSize: 0 - }, - remote: { - totalCount: remoteCount, - totalSize: remoteSize, - incCount: 0, - incSize: 0, - decCount: 0, - decSize: 0 - } - }; - } - - @autobind - public async update(file: IDriveFile, isAdditional: boolean) { - const update: Obj = {}; - - update.totalCount = isAdditional ? 1 : -1; - update.totalSize = isAdditional ? file.length : -file.length; - if (isAdditional) { - update.incCount = 1; - update.incSize = file.length; - } else { - update.decCount = 1; - update.decSize = file.length; - } - - await this.inc({ - [isLocalUser(file.metadata._user) ? 'local' : 'remote']: update - }); - } -} - -export default new DriveChart(); diff --git a/src/services/chart/entities.ts b/src/services/chart/entities.ts new file mode 100644 index 0000000000000000000000000000000000000000..14fd3adba084f0a5851441fb2b18f25f25b9950e --- /dev/null +++ b/src/services/chart/entities.ts @@ -0,0 +1,8 @@ +import Chart from './core'; + +export const entities = Object.values(require('require-all')({ + dirname: __dirname + '/charts/schemas', + resolve: (x: any) => { + return Chart.schemaToEntity(x.name, x.schema); + } +})); diff --git a/src/services/chart/federation.ts b/src/services/chart/federation.ts deleted file mode 100644 index 20da7a742125b0551fdcd4fc683f4aaa6a64dbe0..0000000000000000000000000000000000000000 --- a/src/services/chart/federation.ts +++ /dev/null @@ -1,66 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import Instance from '../../models/instance'; - -/** - * フェデレーションã«é–¢ã™ã‚‹ãƒãƒ£ãƒ¼ãƒˆ - */ -type FederationLog = { - instance: { - /** - * インスタンス数ã®åˆè¨ˆ - */ - total: number; - - /** - * å¢—åŠ ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹æ•° - */ - inc: number; - - /** - * 減少インスタンス数 - */ - dec: number; - }; -}; - -class FederationChart extends Chart<FederationLog> { - constructor() { - super('federation'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: FederationLog): Promise<FederationLog> { - const [total] = init ? await Promise.all([ - Instance.count({}) - ]) : [ - latest ? latest.instance.total : 0 - ]; - - return { - instance: { - total: total, - inc: 0, - dec: 0 - } - }; - } - - @autobind - public async update(isAdditional: boolean) { - const update: Obj = {}; - - update.total = isAdditional ? 1 : -1; - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - await this.inc({ - instance: update - }); - } -} - -export default new FederationChart(); diff --git a/src/services/chart/hashtag.ts b/src/services/chart/hashtag.ts deleted file mode 100644 index 7a31e9ccedb416eeb066b5a97ce7d5a852c9386c..0000000000000000000000000000000000000000 --- a/src/services/chart/hashtag.ts +++ /dev/null @@ -1,56 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import { IUser, isLocalUser } from '../../models/user'; -import db from '../../db/mongodb'; - -/** - * ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã«é–¢ã™ã‚‹ãƒãƒ£ãƒ¼ãƒˆ - */ -type HashtagLog = { - local: { - /** - * 投稿ã•ã‚ŒãŸæ•° - */ - count: number; - }; - - remote: HashtagLog['local']; -}; - -class HashtagChart extends Chart<HashtagLog> { - constructor() { - super('hashtag', true); - - // 後方互æ›æ€§ã®ãŸã‚ - db.get('chart.hashtag').findOne().then(doc => { - if (doc != null && doc.data.local == null) { - db.get('chart.hashtag').drop(); - } - }); - } - - @autobind - protected async getTemplate(init: boolean, latest?: HashtagLog): Promise<HashtagLog> { - return { - local: { - count: 0 - }, - remote: { - count: 0 - } - }; - } - - @autobind - public async update(hashtag: string, user: IUser) { - const update: Obj = { - count: 1 - }; - - await this.incIfUnique({ - [isLocalUser(user) ? 'local' : 'remote']: update - }, 'users', user._id.toHexString(), hashtag); - } -} - -export default new HashtagChart(); diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts index 7a6470f4d87b092c443f5b9f8465eb451849d662..9626e3d6b3a308e904eac86a386b94c0ada0ba54 100644 --- a/src/services/chart/index.ts +++ b/src/services/chart/index.ts @@ -1,364 +1,25 @@ -/** - * ãƒãƒ£ãƒ¼ãƒˆã‚¨ãƒ³ã‚¸ãƒ³ - */ - -import * as moment from 'moment'; -import * as nestedProperty from 'nested-property'; -import autobind from 'autobind-decorator'; -import * as mongo from 'mongodb'; -import db from '../../db/mongodb'; -import { ICollection } from 'monk'; -import Logger from '../logger'; -import { Schema } from '../../misc/schema'; - -const logger = new Logger('chart'); - -const utc = moment.utc; - -export type Obj = { [key: string]: any }; - -export type Partial<T> = { - [P in keyof T]?: Partial<T[P]>; -}; - -type ArrayValue<T> = { - [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>; -}; - -type Span = 'day' | 'hour'; - -type Log<T extends Obj> = { - _id: mongo.ObjectID; - - /** - * 集計ã®ã‚°ãƒ«ãƒ¼ãƒ— - */ - group?: any; - - /** - * 集計日時 - */ - date: Date; - - /** - * 集計期間 - */ - span: Span; - - /** - * データ - */ - data: T; - - /** - * ユニークインクリメント用 - */ - unique?: Obj; -}; - -/** - * 様々ãªãƒãƒ£ãƒ¼ãƒˆã®ç®¡ç†ã‚’å¸ã‚‹ã‚¯ãƒ©ã‚¹ - */ -export default abstract class Chart<T extends Obj> { - protected collection: ICollection<Log<T>>; - protected abstract async getTemplate(init: boolean, latest?: T, group?: any): Promise<T>; - private name: string; - - constructor(name: string, grouped = false) { - this.name = name; - this.collection = db.get<Log<T>>(`chart.${name}`); - - const keys = { - span: -1, - date: -1 - } as { [key: string]: 1 | -1; }; - if (grouped) keys.group = -1; - - this.collection.createIndex(keys, { unique: true }); - } - - @autobind - private convertQuery(x: Obj, path: string): Obj { - const query: Obj = {}; - - const dive = (x: Obj, path: string) => { - for (const [k, v] of Object.entries(x)) { - const p = path ? `${path}.${k}` : k; - if (typeof v === 'number') { - query[p] = v; - } else { - dive(v, p); - } - } - }; - - dive(x, path); - - return query; - } - - @autobind - private getCurrentDate(): [number, number, number, number] { - const now = moment().utc(); - - const y = now.year(); - const m = now.month(); - const d = now.date(); - const h = now.hour(); - - return [y, m, d, h]; - } - - @autobind - private getLatestLog(span: Span, group?: any): Promise<Log<T>> { - return this.collection.findOne({ - group: group, - span: span - }, { - sort: { - date: -1 - } - }); - } - - @autobind - private async getCurrentLog(span: Span, group?: any): Promise<Log<T>> { - const [y, m, d, h] = this.getCurrentDate(); - - const current = - span == 'day' ? utc([y, m, d]) : - span == 'hour' ? utc([y, m, d, h]) : - null; - - // ç¾åœ¨(今日ã¾ãŸã¯ä»Šã®Hour)ã®ãƒã‚° - const currentLog = await this.collection.findOne({ - group: group, - span: span, - date: current.toDate() - }); - - // ãƒã‚°ãŒã‚ã‚Œã°ãれを返ã—ã¦çµ‚了 - if (currentLog != null) { - return currentLog; - } - - let log: Log<T>; - let data: T; - - // 集計期間ãŒå¤‰ã‚ã£ã¦ã‹ã‚‰ã€åˆã‚ã¦ã®ãƒãƒ£ãƒ¼ãƒˆæ›´æ–°ãªã‚‰ - // 最も最近ã®ãƒã‚°ã‚’æŒã£ã¦ãã‚‹ - // * 例ãˆã°é›†è¨ˆæœŸé–“ãŒã€Œæ—¥ã€ã§ã‚ã‚‹å ´åˆã§è€ƒãˆã‚‹ã¨ã€ - // * 昨日何もãƒãƒ£ãƒ¼ãƒˆã‚’æ›´æ–°ã™ã‚‹ã‚ˆã†ãªå‡ºæ¥äº‹ãŒãªã‹ã£ãŸå ´åˆã¯ã€ - // * ãƒã‚°ãŒãã‚‚ãも作られãšãƒ‰ã‚ュメントãŒå˜åœ¨ã—ãªã„ã¨ã„ã†ã“ã¨ãŒã‚ã‚Šå¾—ã‚‹ãŸã‚〠- // * 「昨日ã®ã€ã¨æ±ºã‚打ã¡ã›ãšã«ã€Œã‚‚ã£ã¨ã‚‚最近ã®ã€ã¨ã—ã¾ã™ - const latest = await this.getLatestLog(span, group); - - if (latest != null) { - // 空ãƒã‚°ãƒ‡ãƒ¼ã‚¿ã‚’ä½œæˆ - data = await this.getTemplate(false, latest.data); - } else { - // ãƒã‚°ãŒå˜åœ¨ã—ãªã‹ã£ãŸã‚‰ - // (Misskeyインスタンスを建ã¦ã¦åˆã‚ã¦ã®ãƒãƒ£ãƒ¼ãƒˆæ›´æ–°æ™‚ãªã© - // ã¾ãŸã¯ä½•ã‚‰ã‹ã®ç†ç”±ã§ãƒãƒ£ãƒ¼ãƒˆã‚³ãƒ¬ã‚¯ã‚·ãƒ§ãƒ³ã‚’抹消ã—ãŸå ´åˆ) - - // åˆæœŸãƒã‚°ãƒ‡ãƒ¼ã‚¿ã‚’ä½œæˆ - data = await this.getTemplate(true, null, group); - - logger.info(`${this.name}: Initial commit created`); - } - - try { - // æ–°è¦ãƒã‚°æŒ¿å…¥ - log = await this.collection.insert({ - group: group, - span: span, - date: current.toDate(), - data: data - }); - } catch (e) { - // 11000 is duplicate key error - // 並列動作ã—ã¦ã„ã‚‹ä»–ã®ãƒãƒ£ãƒ¼ãƒˆã‚¨ãƒ³ã‚¸ãƒ³ãƒ—ãƒã‚»ã‚¹ã¨å‡¦ç†ãŒé‡ãªã‚‹å ´åˆãŒã‚ã‚‹ - // ãã®å ´åˆã¯å†åº¦æœ€ã‚‚æ–°ã—ã„ãƒã‚°ã‚’æŒã£ã¦ãã‚‹ - if (e.code === 11000) { - log = await this.getLatestLog(span, group); - } else { - logger.error(e); - throw e; - } - } - - return log; - } - - @autobind - protected commit(query: Obj, group?: any, uniqueKey?: string, uniqueValue?: string): void { - const update = (log: Log<T>) => { - // ユニークインクリメントã®å ´åˆã€æŒ‡å®šã®ã‚ーã«æŒ‡å®šã®å€¤ãŒæ—¢ã«å˜åœ¨ã—ã¦ã„ãŸã‚‰å¼¾ã - if ( - uniqueKey && - log.unique && - log.unique[uniqueKey] && - log.unique[uniqueKey].includes(uniqueValue) - ) return; - - // ユニークインクリメントã®æŒ‡å®šã®ã‚ーã«å€¤ã‚’è¿½åŠ - if (uniqueKey) { - query['$push'] = { - [`unique.${uniqueKey}`]: uniqueValue - }; - } - - // ãƒã‚°æ›´æ–° - this.collection.update({ - _id: log._id - }, query); - }; - - this.getCurrentLog('day', group).then(log => update(log)); - this.getCurrentLog('hour', group).then(log => update(log)); - } - - @autobind - protected inc(inc: Partial<T>, group?: any): void { - this.commit({ - $inc: this.convertQuery(inc, 'data') - }, group); - } - - @autobind - protected incIfUnique(inc: Partial<T>, key: string, value: string, group?: any): void { - this.commit({ - $inc: this.convertQuery(inc, 'data') - }, group, key, value); - } - - @autobind - public async getChart(span: Span, range: number, group?: any): Promise<ArrayValue<T>> { - const promisedChart: Promise<T>[] = []; - - const [y, m, d, h] = this.getCurrentDate(); - - const gt = - span == 'day' ? utc([y, m, d]).subtract(range, 'days') : - span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') : - null; - - // ãƒã‚°å–å¾— - let logs = await this.collection.find({ - group: group, - span: span, - date: { - $gte: gt.toDate() - } - }, { - sort: { - date: -1 - }, - fields: { - _id: 0 - } - }); - - // è¦æ±‚ã•ã‚ŒãŸç¯„囲ã«ãƒã‚°ãŒã²ã¨ã¤ã‚‚ãªã‹ã£ãŸã‚‰ - if (logs.length == 0) { - // ã‚‚ã£ã¨ã‚‚æ–°ã—ã„ãƒã‚°ã‚’æŒã£ã¦ãã‚‹ - // (ã™ããªãã¨ã‚‚ã²ã¨ã¤ãƒã‚°ãŒç„¡ã„ã¨éš™é–“埋ã‚ã§ããªã„ãŸã‚) - const recentLog = await this.collection.findOne({ - group: group, - span: span - }, { - sort: { - date: -1 - }, - fields: { - _id: 0 - } - }); - - if (recentLog) { - logs = [recentLog]; - } - - // è¦æ±‚ã•ã‚ŒãŸç¯„囲ã®æœ€ã‚‚å¤ã„箇所ã«ä½ç½®ã™ã‚‹ãƒã‚°ãŒå˜åœ¨ã—ãªã‹ã£ãŸã‚‰ - } else if (!utc(logs[logs.length - 1].date).isSame(gt)) { - // è¦æ±‚ã•ã‚ŒãŸç¯„囲ã®æœ€ã‚‚å¤ã„箇所時点ã§ã®æœ€ã‚‚æ–°ã—ã„ãƒã‚°ã‚’æŒã£ã¦ãã¦æœ«å°¾ã«è¿½åŠ ã™ã‚‹ - // (隙間埋ã‚ã§ããªã„ãŸã‚) - const outdatedLog = await this.collection.findOne({ - group: group, - span: span, - date: { - $lt: gt.toDate() - } - }, { - sort: { - date: -1 - }, - fields: { - _id: 0 - } - }); - - if (outdatedLog) { - logs.push(outdatedLog); - } - } - - // æ•´å½¢ - for (let i = (range - 1); i >= 0; i--) { - const current = - span == 'day' ? utc([y, m, d]).subtract(i, 'days') : - span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') : - null; - - const log = logs.find(l => utc(l.date).isSame(current)); - - if (log) { - promisedChart.unshift(Promise.resolve(log.data)); - } else { - // 隙間埋゠- const latest = logs.find(l => utc(l.date).isBefore(current)); - promisedChart.unshift(this.getTemplate(false, latest ? latest.data : null)); - } - } - - const chart = await Promise.all(promisedChart); - - const res: ArrayValue<T> = {} as any; - - /** - * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }] - * ã‚’ - * { foo: [1, 2, 3], bar: [5, 6, 7] } - * ã«ã™ã‚‹ - */ - const dive = (x: Obj, path?: string) => { - for (const [k, v] of Object.entries(x)) { - const p = path ? `${path}.${k}` : k; - if (typeof v == 'object') { - dive(v, p); - } else { - nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p))); - } - } - }; - - dive(chart[0]); - - return res; - } -} - -export function convertLog(logSchema: Schema): Schema { - const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy - if (v.type === 'number') { - v.type = 'array'; - v.items = { - type: 'number' - }; - } else if (v.type === 'object') { - for (const k of Object.keys(v.properties)) { - v.properties[k] = convertLog(v.properties[k]); - } - } - return v; -} +import FederationChart from './charts/classes/federation'; +import NotesChart from './charts/classes/notes'; +import UsersChart from './charts/classes/users'; +import NetworkChart from './charts/classes/network'; +import ActiveUsersChart from './charts/classes/active-users'; +import InstanceChart from './charts/classes/instance'; +import PerUserNotesChart from './charts/classes/per-user-notes'; +import DriveChart from './charts/classes/drive'; +import PerUserReactionsChart from './charts/classes/per-user-reactions'; +import HashtagChart from './charts/classes/hashtag'; +import PerUserFollowingChart from './charts/classes/per-user-following'; +import PerUserDriveChart from './charts/classes/per-user-drive'; + +export const federationChart = new FederationChart(); +export const notesChart = new NotesChart(); +export const usersChart = new UsersChart(); +export const networkChart = new NetworkChart(); +export const activeUsersChart = new ActiveUsersChart(); +export const instanceChart = new InstanceChart(); +export const perUserNotesChart = new PerUserNotesChart(); +export const driveChart = new DriveChart(); +export const perUserReactionsChart = new PerUserReactionsChart(); +export const hashtagChart = new HashtagChart(); +export const perUserFollowingChart = new PerUserFollowingChart(); +export const perUserDriveChart = new PerUserDriveChart(); diff --git a/src/services/chart/instance.ts b/src/services/chart/instance.ts deleted file mode 100644 index 5af398b902b53628021d22d6d80a68276c9c1a0f..0000000000000000000000000000000000000000 --- a/src/services/chart/instance.ts +++ /dev/null @@ -1,302 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import User from '../../models/user'; -import Note from '../../models/note'; -import Following from '../../models/following'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; - -/** - * インスタンスã”ã¨ã®ãƒãƒ£ãƒ¼ãƒˆ - */ -type InstanceLog = { - requests: { - /** - * 失敗ã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆæ•° - */ - failed: number; - - /** - * æˆåŠŸã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆæ•° - */ - succeeded: number; - - /** - * å—ä¿¡ã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆæ•° - */ - received: number; - }; - - notes: { - /** - * 集計期間時点ã§ã®ã€å…¨æŠ•ç¨¿æ•° - */ - total: number; - - /** - * å¢—åŠ ã—ãŸæŠ•ç¨¿æ•° - */ - inc: number; - - /** - * 減少ã—ãŸæŠ•ç¨¿æ•° - */ - dec: number; - }; - - users: { - /** - * 集計期間時点ã§ã®ã€å…¨ãƒ¦ãƒ¼ã‚¶ãƒ¼æ•° - */ - total: number; - - /** - * å¢—åŠ ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•° - */ - inc: number; - - /** - * 減少ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•° - */ - dec: number; - }; - - following: { - /** - * 集計期間時点ã§ã®ã€å…¨ãƒ•ã‚©ãƒãƒ¼æ•° - */ - total: number; - - /** - * å¢—åŠ ã—ãŸãƒ•ã‚©ãƒãƒ¼æ•° - */ - inc: number; - - /** - * 減少ã—ãŸãƒ•ã‚©ãƒãƒ¼æ•° - */ - dec: number; - }; - - followers: { - /** - * 集計期間時点ã§ã®ã€å…¨ãƒ•ã‚©ãƒãƒ¯ãƒ¼æ•° - */ - total: number; - - /** - * å¢—åŠ ã—ãŸãƒ•ã‚©ãƒãƒ¯ãƒ¼æ•° - */ - inc: number; - - /** - * 減少ã—ãŸãƒ•ã‚©ãƒãƒ¯ãƒ¼æ•° - */ - dec: number; - }; - - drive: { - /** - * 集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 - */ - totalFiles: number; - - /** - * 集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイルã®åˆè¨ˆã‚µã‚¤ã‚º - */ - totalUsage: number; - - /** - * å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 - */ - incFiles: number; - - /** - * å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ - */ - incUsage: number; - - /** - * 減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 - */ - decFiles: number; - - /** - * 減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ - */ - decUsage: number; - }; -}; - -class InstanceChart extends Chart<InstanceLog> { - constructor() { - super('instance', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: InstanceLog, group?: any): Promise<InstanceLog> { - const calcUsage = () => DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': group, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(res => res.length > 0 ? res[0].usage : 0); - - const [ - notesCount, - usersCount, - followingCount, - followersCount, - driveFiles, - driveUsage, - ] = init ? await Promise.all([ - Note.count({ '_user.host': group }), - User.count({ host: group }), - Following.count({ '_follower.host': group }), - Following.count({ '_followee.host': group }), - DriveFile.count({ 'metadata._user.host': group }), - calcUsage(), - ]) : [ - latest ? latest.notes.total : 0, - latest ? latest.users.total : 0, - latest ? latest.following.total : 0, - latest ? latest.followers.total : 0, - latest ? latest.drive.totalFiles : 0, - latest ? latest.drive.totalUsage : 0, - ]; - - return { - requests: { - failed: 0, - succeeded: 0, - received: 0 - }, - notes: { - total: notesCount, - inc: 0, - dec: 0 - }, - users: { - total: usersCount, - inc: 0, - dec: 0 - }, - following: { - total: followingCount, - inc: 0, - dec: 0 - }, - followers: { - total: followersCount, - inc: 0, - dec: 0 - }, - drive: { - totalFiles: driveFiles, - totalUsage: driveUsage, - incFiles: 0, - incUsage: 0, - decFiles: 0, - decUsage: 0 - } - }; - } - - @autobind - public async requestReceived(host: string) { - await this.inc({ - requests: { - received: 1 - } - }, host); - } - - @autobind - public async requestSent(host: string, isSucceeded: boolean) { - const update: Obj = {}; - - if (isSucceeded) { - update.succeeded = 1; - } else { - update.failed = 1; - } - - await this.inc({ - requests: update - }, host); - } - - @autobind - public async newUser(host: string) { - await this.inc({ - users: { - total: 1, - inc: 1 - } - }, host); - } - - @autobind - public async updateNote(host: string, isAdditional: boolean) { - await this.inc({ - notes: { - total: isAdditional ? 1 : -1, - inc: isAdditional ? 1 : 0, - dec: isAdditional ? 0 : 1, - } - }, host); - } - - @autobind - public async updateFollowing(host: string, isAdditional: boolean) { - await this.inc({ - following: { - total: isAdditional ? 1 : -1, - inc: isAdditional ? 1 : 0, - dec: isAdditional ? 0 : 1, - } - }, host); - } - - @autobind - public async updateFollowers(host: string, isAdditional: boolean) { - await this.inc({ - followers: { - total: isAdditional ? 1 : -1, - inc: isAdditional ? 1 : 0, - dec: isAdditional ? 0 : 1, - } - }, host); - } - - @autobind - public async updateDrive(file: IDriveFile, isAdditional: boolean) { - const update: Obj = {}; - - update.totalFiles = isAdditional ? 1 : -1; - update.totalUsage = isAdditional ? file.length : -file.length; - if (isAdditional) { - update.incFiles = 1; - update.incUsage = file.length; - } else { - update.decFiles = 1; - update.decUsage = file.length; - } - - await this.inc({ - drive: update - }, file.metadata._user.host); - } -} - -export default new InstanceChart(); diff --git a/src/services/chart/network.ts b/src/services/chart/network.ts deleted file mode 100644 index fce47099d17d01d8268ee1aa76bba962f6ef5c3a..0000000000000000000000000000000000000000 --- a/src/services/chart/network.ts +++ /dev/null @@ -1,64 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Partial } from './'; - -/** - * ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«é–¢ã™ã‚‹ãƒãƒ£ãƒ¼ãƒˆ - */ -type NetworkLog = { - /** - * å—ä¿¡ã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆæ•° - */ - incomingRequests: number; - - /** - * é€ä¿¡ã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆæ•° - */ - outgoingRequests: number; - - /** - * å¿œç”時間ã®åˆè¨ˆ - * TIP: (totalTime / incomingRequests) ã§ã²ã¨ã¤ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¹³å‡ã§ã©ã‚Œãらã„ã®æ™‚é–“ãŒã‹ã‹ã£ãŸã‹çŸ¥ã‚Œã‚‹ - */ - totalTime: number; - - /** - * åˆè¨ˆå—ä¿¡ãƒ‡ãƒ¼ã‚¿é‡ - */ - incomingBytes: number; - - /** - * åˆè¨ˆé€ä¿¡ãƒ‡ãƒ¼ã‚¿é‡ - */ - outgoingBytes: number; -}; - -class NetworkChart extends Chart<NetworkLog> { - constructor() { - super('network'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: NetworkLog): Promise<NetworkLog> { - return { - incomingRequests: 0, - outgoingRequests: 0, - totalTime: 0, - incomingBytes: 0, - outgoingBytes: 0 - }; - } - - @autobind - public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) { - const inc: Partial<NetworkLog> = { - incomingRequests: incomingRequests, - totalTime: time, - incomingBytes: incomingBytes, - outgoingBytes: outgoingBytes - }; - - await this.inc(inc); - } -} - -export default new NetworkChart(); diff --git a/src/services/chart/notes.ts b/src/services/chart/notes.ts deleted file mode 100644 index b047ec273f01be2e5ad35d58057161deb394640d..0000000000000000000000000000000000000000 --- a/src/services/chart/notes.ts +++ /dev/null @@ -1,127 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import Note, { INote } from '../../models/note'; -import { isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -const logSchema = { - total: { - type: 'number' as 'number', - description: '集計期間時点ã§ã®ã€å…¨æŠ•ç¨¿æ•°' - }, - - inc: { - type: 'number' as 'number', - description: 'å¢—åŠ ã—ãŸæŠ•ç¨¿æ•°' - }, - - dec: { - type: 'number' as 'number', - description: '減少ã—ãŸæŠ•ç¨¿æ•°' - }, - - diffs: { - type: 'object' as 'object', - properties: { - normal: { - type: 'number' as 'number', - description: '通常ã®æŠ•ç¨¿æ•°ã®å·®åˆ†' - }, - - reply: { - type: 'number' as 'number', - description: 'リプライã®æŠ•ç¨¿æ•°ã®å·®åˆ†' - }, - - renote: { - type: 'number' as 'number', - description: 'Renoteã®æŠ•ç¨¿æ•°ã®å·®åˆ†' - }, - } - }, -}; - -export const notesLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type NotesLog = SchemaType<typeof notesLogSchema>; - -class NotesChart extends Chart<NotesLog> { - constructor() { - super('notes'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: NotesLog): Promise<NotesLog> { - const [localCount, remoteCount] = init ? await Promise.all([ - Note.count({ '_user.host': null }), - Note.count({ '_user.host': { $ne: null } }) - ]) : [ - latest ? latest.local.total : 0, - latest ? latest.remote.total : 0 - ]; - - return { - local: { - total: localCount, - inc: 0, - dec: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }, - remote: { - total: remoteCount, - inc: 0, - dec: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - } - }; - } - - @autobind - public async update(note: INote, isAdditional: boolean) { - const update: Obj = { - diffs: {} - }; - - update.total = isAdditional ? 1 : -1; - - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - if (note.replyId != null) { - update.diffs.reply = isAdditional ? 1 : -1; - } else if (note.renoteId != null) { - update.diffs.renote = isAdditional ? 1 : -1; - } else { - update.diffs.normal = isAdditional ? 1 : -1; - } - - await this.inc({ - [isLocalUser(note._user) ? 'local' : 'remote']: update - }); - } -} - -export default new NotesChart(); diff --git a/src/services/chart/per-user-drive.ts b/src/services/chart/per-user-drive.ts deleted file mode 100644 index 4f335f168898e3b7fce5cc350d30bf9b4c1cc545..0000000000000000000000000000000000000000 --- a/src/services/chart/per-user-drive.ts +++ /dev/null @@ -1,122 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; -import { SchemaType } from '../../misc/schema'; - -export const perUserDriveLogSchema = { - type: 'object' as 'object', - properties: { - /** - * 集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 - */ - totalCount: { - type: 'number' as 'number', - description: '集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' - }, - - /** - * 集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイルã®åˆè¨ˆã‚µã‚¤ã‚º - */ - totalSize: { - type: 'number' as 'number', - description: '集計期間時点ã§ã®ã€å…¨ãƒ‰ãƒ©ã‚¤ãƒ–ファイルã®åˆè¨ˆã‚µã‚¤ã‚º' - }, - - /** - * å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 - */ - incCount: { - type: 'number' as 'number', - description: 'å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' - }, - - /** - * å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ - */ - incSize: { - type: 'number' as 'number', - description: 'å¢—åŠ ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–使用é‡' - }, - - /** - * 減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数 - */ - decCount: { - type: 'number' as 'number', - description: '減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ファイル数' - }, - - /** - * 減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–ä½¿ç”¨é‡ - */ - decSize: { - type: 'number' as 'number', - description: '減少ã—ãŸãƒ‰ãƒ©ã‚¤ãƒ–使用é‡' - }, - } -}; - -type PerUserDriveLog = SchemaType<typeof perUserDriveLogSchema>; - -class PerUserDriveChart extends Chart<PerUserDriveLog> { - constructor() { - super('perUserDrive', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserDriveLog, group?: any): Promise<PerUserDriveLog> { - const calcSize = () => DriveFile - .aggregate([{ - $match: { - 'metadata.userId': group, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(res => res.length > 0 ? res[0].usage : 0); - - const [count, size] = init ? await Promise.all([ - DriveFile.count({ 'metadata.userId': group }), - calcSize() - ]) : [ - latest ? latest.totalCount : 0, - latest ? latest.totalSize : 0 - ]; - - return { - totalCount: count, - totalSize: size, - incCount: 0, - incSize: 0, - decCount: 0, - decSize: 0 - }; - } - - @autobind - public async update(file: IDriveFile, isAdditional: boolean) { - const update: Obj = {}; - - update.totalCount = isAdditional ? 1 : -1; - update.totalSize = isAdditional ? file.length : -file.length; - if (isAdditional) { - update.incCount = 1; - update.incSize = file.length; - } else { - update.decCount = 1; - update.decSize = file.length; - } - - await this.inc(update, file.metadata.userId); - } -} - -export default new PerUserDriveChart(); diff --git a/src/services/chart/per-user-following.ts b/src/services/chart/per-user-following.ts deleted file mode 100644 index 8a94a4f155cdeae0d0b0ecb480a3586e09847aa3..0000000000000000000000000000000000000000 --- a/src/services/chart/per-user-following.ts +++ /dev/null @@ -1,162 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import Following from '../../models/following'; -import { IUser, isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -export const logSchema = { - /** - * フォãƒãƒ¼ã—ã¦ã„ã‚‹ - */ - followings: { - type: 'object' as 'object', - properties: { - /** - * フォãƒãƒ¼ã—ã¦ã„ã‚‹åˆè¨ˆ - */ - total: { - type: 'number', - description: 'フォãƒãƒ¼ã—ã¦ã„ã‚‹åˆè¨ˆ', - }, - - /** - * フォãƒãƒ¼ã—ãŸæ•° - */ - inc: { - type: 'number', - description: 'フォãƒãƒ¼ã—ãŸæ•°', - }, - - /** - * フォãƒãƒ¼è§£é™¤ã—ãŸæ•° - */ - dec: { - type: 'number', - description: 'フォãƒãƒ¼è§£é™¤ã—ãŸæ•°', - }, - } - }, - - /** - * フォãƒãƒ¼ã•ã‚Œã¦ã„ã‚‹ - */ - followers: { - type: 'object' as 'object', - properties: { - /** - * フォãƒãƒ¼ã•ã‚Œã¦ã„ã‚‹åˆè¨ˆ - */ - total: { - type: 'number', - description: 'フォãƒãƒ¼ã•ã‚Œã¦ã„ã‚‹åˆè¨ˆ', - }, - - /** - * フォãƒãƒ¼ã•ã‚ŒãŸæ•° - */ - inc: { - type: 'number', - description: 'フォãƒãƒ¼ã•ã‚ŒãŸæ•°', - }, - - /** - * フォãƒãƒ¼è§£é™¤ã•ã‚ŒãŸæ•° - */ - dec: { - type: 'number', - description: 'フォãƒãƒ¼è§£é™¤ã•ã‚ŒãŸæ•°', - }, - } - }, -}; - -export const perUserFollowingLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type PerUserFollowingLog = SchemaType<typeof perUserFollowingLogSchema>; - -class PerUserFollowingChart extends Chart<PerUserFollowingLog> { - constructor() { - super('perUserFollowing', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserFollowingLog, group?: any): Promise<PerUserFollowingLog> { - const [ - localFollowingsCount, - localFollowersCount, - remoteFollowingsCount, - remoteFollowersCount - ] = init ? await Promise.all([ - Following.count({ followerId: group, '_followee.host': null }), - Following.count({ followeeId: group, '_follower.host': null }), - Following.count({ followerId: group, '_followee.host': { $ne: null } }), - Following.count({ followeeId: group, '_follower.host': { $ne: null } }) - ]) : [ - latest ? latest.local.followings.total : 0, - latest ? latest.local.followers.total : 0, - latest ? latest.remote.followings.total : 0, - latest ? latest.remote.followers.total : 0 - ]; - - return { - local: { - followings: { - total: localFollowingsCount, - inc: 0, - dec: 0 - }, - followers: { - total: localFollowersCount, - inc: 0, - dec: 0 - } - }, - remote: { - followings: { - total: remoteFollowingsCount, - inc: 0, - dec: 0 - }, - followers: { - total: remoteFollowersCount, - inc: 0, - dec: 0 - } - } - }; - } - - @autobind - public async update(follower: IUser, followee: IUser, isFollow: boolean) { - const update: Obj = {}; - - update.total = isFollow ? 1 : -1; - - if (isFollow) { - update.inc = 1; - } else { - update.dec = 1; - } - - this.inc({ - [isLocalUser(follower) ? 'local' : 'remote']: { followings: update } - }, follower._id); - this.inc({ - [isLocalUser(followee) ? 'local' : 'remote']: { followers: update } - }, followee._id); - } -} - -export default new PerUserFollowingChart(); diff --git a/src/services/chart/per-user-notes.ts b/src/services/chart/per-user-notes.ts deleted file mode 100644 index 2f4f88209104407c5712853f3f4da9298e7dafb6..0000000000000000000000000000000000000000 --- a/src/services/chart/per-user-notes.ts +++ /dev/null @@ -1,100 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import Note, { INote } from '../../models/note'; -import { IUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -export const perUserNotesLogSchema = { - type: 'object' as 'object', - properties: { - total: { - type: 'number' as 'number', - description: '集計期間時点ã§ã®ã€å…¨æŠ•ç¨¿æ•°' - }, - - inc: { - type: 'number' as 'number', - description: 'å¢—åŠ ã—ãŸæŠ•ç¨¿æ•°' - }, - - dec: { - type: 'number' as 'number', - description: '減少ã—ãŸæŠ•ç¨¿æ•°' - }, - - diffs: { - type: 'object' as 'object', - properties: { - normal: { - type: 'number' as 'number', - description: '通常ã®æŠ•ç¨¿æ•°ã®å·®åˆ†' - }, - - reply: { - type: 'number' as 'number', - description: 'リプライã®æŠ•ç¨¿æ•°ã®å·®åˆ†' - }, - - renote: { - type: 'number' as 'number', - description: 'Renoteã®æŠ•ç¨¿æ•°ã®å·®åˆ†' - }, - } - }, - } -}; - -type PerUserNotesLog = SchemaType<typeof perUserNotesLogSchema>; - -class PerUserNotesChart extends Chart<PerUserNotesLog> { - constructor() { - super('perUserNotes', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserNotesLog, group?: any): Promise<PerUserNotesLog> { - const [count] = init ? await Promise.all([ - Note.count({ userId: group, deletedAt: null }), - ]) : [ - latest ? latest.total : 0 - ]; - - return { - total: count, - inc: 0, - dec: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }; - } - - @autobind - public async update(user: IUser, note: INote, isAdditional: boolean) { - const update: Obj = { - diffs: {} - }; - - update.total = isAdditional ? 1 : -1; - - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - if (note.replyId != null) { - update.diffs.reply = isAdditional ? 1 : -1; - } else if (note.renoteId != null) { - update.diffs.renote = isAdditional ? 1 : -1; - } else { - update.diffs.normal = isAdditional ? 1 : -1; - } - - await this.inc(update, user._id); - } -} - -export default new PerUserNotesChart(); diff --git a/src/services/chart/per-user-reactions.ts b/src/services/chart/per-user-reactions.ts deleted file mode 100644 index 60495aeb02daa99f64bda710eb79d2d740522e93..0000000000000000000000000000000000000000 --- a/src/services/chart/per-user-reactions.ts +++ /dev/null @@ -1,45 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart from './'; -import { IUser, isLocalUser } from '../../models/user'; -import { INote } from '../../models/note'; - -/** - * ユーザーã”ã¨ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã«é–¢ã™ã‚‹ãƒãƒ£ãƒ¼ãƒˆ - */ -type PerUserReactionsLog = { - local: { - /** - * リアクションã•ã‚ŒãŸæ•° - */ - count: number; - }; - - remote: PerUserReactionsLog['local']; -}; - -class PerUserReactionsChart extends Chart<PerUserReactionsLog> { - constructor() { - super('perUserReaction', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserReactionsLog, group?: any): Promise<PerUserReactionsLog> { - return { - local: { - count: 0 - }, - remote: { - count: 0 - } - }; - } - - @autobind - public async update(user: IUser, note: INote) { - this.inc({ - [isLocalUser(user) ? 'local' : 'remote']: { count: 1 } - }, note.userId); - } -} - -export default new PerUserReactionsChart(); diff --git a/src/services/chart/users.ts b/src/services/chart/users.ts deleted file mode 100644 index cca9590842cc66b41a8e442f172f93e1b0779317..0000000000000000000000000000000000000000 --- a/src/services/chart/users.ts +++ /dev/null @@ -1,94 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import User, { IUser, isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -const logSchema = { - /** - * 集計期間時点ã§ã®ã€å…¨ãƒ¦ãƒ¼ã‚¶ãƒ¼æ•° - */ - total: { - type: 'number' as 'number', - description: '集計期間時点ã§ã®ã€å…¨ãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°' - }, - - /** - * å¢—åŠ ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•° - */ - inc: { - type: 'number' as 'number', - description: 'å¢—åŠ ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°' - }, - - /** - * 減少ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•° - */ - dec: { - type: 'number' as 'number', - description: '減少ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°' - }, -}; - -export const usersLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type UsersLog = SchemaType<typeof usersLogSchema>; - -class UsersChart extends Chart<UsersLog> { - constructor() { - super('users'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: UsersLog): Promise<UsersLog> { - const [localCount, remoteCount] = init ? await Promise.all([ - User.count({ host: null }), - User.count({ host: { $ne: null } }) - ]) : [ - latest ? latest.local.total : 0, - latest ? latest.remote.total : 0 - ]; - - return { - local: { - total: localCount, - inc: 0, - dec: 0 - }, - remote: { - total: remoteCount, - inc: 0, - dec: 0 - } - }; - } - - @autobind - public async update(user: IUser, isAdditional: boolean) { - const update: Obj = {}; - - update.total = isAdditional ? 1 : -1; - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - await this.inc({ - [isLocalUser(user) ? 'local' : 'remote']: update - }); - } -} - -export default new UsersChart(); diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts index 3e000ef2ed1773a30248b366bb4403f78dbfa83e..bcb8214c56829eddb748e08b6e4b6c1e5aeeaa71 100644 --- a/src/services/create-notification.ts +++ b/src/services/create-notification.ts @@ -1,62 +1,66 @@ -import * as mongo from 'mongodb'; -import Notification from '../models/notification'; -import Mute from '../models/mute'; -import { pack } from '../models/notification'; import { publishMainStream } from './stream'; -import User from '../models/user'; import pushSw from './push-notification'; +import { Notifications, Mutings } from '../models'; +import { genId } from '../misc/gen-id'; +import { User } from '../models/entities/user'; +import { Note } from '../models/entities/note'; +import { Notification } from '../models/entities/notification'; -export default ( - notifiee: mongo.ObjectID, - notifier: mongo.ObjectID, +export async function createNotification( + notifieeId: User['id'], + notifierId: User['id'], type: string, - content?: any -) => new Promise<any>(async (resolve, reject) => { - if (notifiee.equals(notifier)) { - return resolve(); + content?: { + noteId?: Note['id']; + reaction?: string; + choice?: number; + } +) { + if (notifieeId === notifierId) { + return null; } - // Create notification - const notification = await Notification.insert(Object.assign({ + const data = { + id: genId(), createdAt: new Date(), - notifieeId: notifiee, - notifierId: notifier, + notifieeId: notifieeId, + notifierId: notifierId, type: type, - isRead: false - }, content)); + isRead: false, + } as Partial<Notification>; + + if (content) { + if (content.noteId) data.noteId = content.noteId; + if (content.reaction) data.reaction = content.reaction; + if (content.choice) data.choice = content.choice; + } - resolve(notification); + // Create notification + const notification = await Notifications.save(data); - const packed = await pack(notification); + const packed = await Notifications.pack(notification); // Publish notification event - publishMainStream(notifiee, 'notification', packed); - - // Update flag - User.update({ _id: notifiee }, { - $set: { - hasUnreadNotification: true - } - }); + publishMainStream(notifieeId, 'notification', packed); // 2秒経ã£ã¦ã‚‚(今回作æˆã—ãŸ)通知ãŒæ—¢èªã«ãªã‚‰ãªã‹ã£ãŸã‚‰ã€Œæœªèªã®é€šçŸ¥ãŒã‚ã‚Šã¾ã™ã‚ˆã€ã‚¤ãƒ™ãƒ³ãƒˆã‚’発行ã™ã‚‹ setTimeout(async () => { - const fresh = await Notification.findOne({ _id: notification._id }, { isRead: true }); + const fresh = await Notifications.findOne(notification.id); if (!fresh.isRead) { //#region ãŸã ã—ミュートã—ã¦ã„るユーザーã‹ã‚‰ã®é€šçŸ¥ãªã‚‰ç„¡è¦– - const mute = await Mute.find({ - muterId: notifiee, - deletedAt: { $exists: false } + const mutings = await Mutings.find({ + muterId: notifieeId }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); - if (mutedUserIds.indexOf(notifier.toString()) != -1) { + if (mutings.map(m => m.muteeId).includes(notifierId)) { return; } //#endregion - publishMainStream(notifiee, 'unreadNotification', packed); + publishMainStream(notifieeId, 'unreadNotification', packed); - pushSw(notifiee, 'notification', packed); + pushSw(notifieeId, 'notification', packed); } }, 2000); -}); + + return notification; +} diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index cdbcb34de40a2600ea5e2495d7d4aefa20d59b56..df5eedf4c8c248b8e6d3d38a871efc25c514837d 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -1,31 +1,27 @@ import { Buffer } from 'buffer'; import * as fs from 'fs'; -import * as mongodb from 'mongodb'; import * as crypto from 'crypto'; import * as Minio from 'minio'; import * as uuid from 'uuid'; import * as sharp from 'sharp'; -import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file'; -import DriveFolder from '../../models/drive-folder'; -import { pack } from '../../models/drive-file'; import { publishMainStream, publishDriveStream } from '../stream'; -import { isLocalUser, IUser, IRemoteUser, isRemoteUser } from '../../models/user'; import delFile from './delete-file'; import config from '../../config'; -import { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic'; -import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; -import driveChart from '../../services/chart/drive'; -import perUserDriveChart from '../../services/chart/per-user-drive'; -import instanceChart from '../../services/chart/instance'; import fetchMeta from '../../misc/fetch-meta'; import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { driveLogger } from './logger'; import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor'; -import Instance from '../../models/instance'; import { contentDisposition } from '../../misc/content-disposition'; import { detectMine } from '../../misc/detect-mine'; +import { DriveFiles, DriveFolders, Users, Instances } from '../../models'; +import { InternalStorage } from './internal-storage'; +import { DriveFile } from '../../models/entities/drive-file'; +import { IRemoteUser, User } from '../../models/entities/user'; +import { driveChart, perUserDriveChart, instanceChart } from '../chart'; +import { genId } from '../../misc/gen-id'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; const logger = driveLogger.createSubLogger('register', 'yellow'); @@ -36,11 +32,10 @@ const logger = driveLogger.createSubLogger('register', 'yellow'); * @param type Content-Type for original * @param hash Hash for original * @param size Size for original - * @param metadata */ -async function save(path: string, name: string, type: string, hash: string, size: number, metadata: IMetadata): Promise<IDriveFile> { +async function save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<DriveFile> { // thunbnail, webpublic ã‚’å¿…è¦ãªã‚‰ç”Ÿæˆ - const alts = await generateAlts(path, type, !metadata.uri); + const alts = await generateAlts(path, type, !file.uri); if (config.drive && config.drive.storage == 'minio') { //#region ObjectStorage params @@ -60,10 +55,10 @@ async function save(path: string, name: string, type: string, hash: string, size const url = `${ baseUrl }/${ key }`; // for alts - let webpublicKey = null as string; - let webpublicUrl = null as string; - let thumbnailKey = null as string; - let thumbnailUrl = null as string; + let webpublicKey: string = null; + let webpublicUrl: string = null; + let thumbnailKey: string = null; + let thumbnailUrl: string = null; //#endregion //#region Uploads @@ -91,58 +86,52 @@ async function save(path: string, name: string, type: string, hash: string, size await Promise.all(uploads); //#endregion - //#region DB - Object.assign(metadata, { - withoutChunks: true, - storage: 'minio', - storageProps: { - key, - webpublicKey, - thumbnailKey, - }, - url, - webpublicUrl, - thumbnailUrl, - } as IMetadata); - - const file = await DriveFile.insert({ - length: size, - uploadDate: new Date(), - md5: hash, - filename: name, - metadata: metadata, - contentType: type - }); - //#endregion - - return file; - } else { // use MongoDB GridFS - // #region store original - const originalDst = await getDriveFileBucket(); - - // web用(Exif削除済ã¿)ãŒã‚ã‚‹å ´åˆã¯ã‚ªãƒªã‚¸ãƒŠãƒ«ã«ã‚¢ã‚¯ã‚»ã‚¹åˆ¶é™ - if (alts.webpublic) metadata.accessKey = uuid.v4(); + file.url = url; + file.thumbnailUrl = thumbnailUrl; + file.webpublicUrl = webpublicUrl; + file.accessKey = key; + file.thumbnailAccessKey = thumbnailKey; + file.webpublicAccessKey = webpublicKey; + file.name = name; + file.type = type; + file.md5 = hash; + file.size = size; + file.storedInternal = false; + + return await DriveFiles.save(file); + } else { // use internal storage + const accessKey = uuid.v4(); + const thumbnailAccessKey = uuid.v4(); + const webpublicAccessKey = uuid.v4(); + + const url = InternalStorage.saveFromPath(accessKey, path); + + let thumbnailUrl: string; + let webpublicUrl: string; - const originalFile = await storeOriginal(originalDst, name, path, type, metadata); - - logger.info(`original stored to ${originalFile._id}`); - // #endregion store original - - // #region store webpublic - if (alts.webpublic) { - const webDst = await getDriveFileWebpublicBucket(); - const webFile = await storeAlts(webDst, name, alts.webpublic.data, alts.webpublic.type, originalFile._id); - logger.info(`web stored ${webFile._id}`); + if (alts.thumbnail) { + thumbnailUrl = InternalStorage.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data); + logger.info(`thumbnail stored: ${thumbnailAccessKey}`); } - // #endregion store webpublic - if (alts.thumbnail) { - const thumDst = await getDriveFileThumbnailBucket(); - const thumFile = await storeAlts(thumDst, name, alts.thumbnail.data, alts.thumbnail.type, originalFile._id); - logger.info(`web stored ${thumFile._id}`); + if (alts.webpublic) { + webpublicUrl = InternalStorage.saveFromBuffer(webpublicAccessKey, alts.webpublic.data); + logger.info(`web stored: ${webpublicAccessKey}`); } - return originalFile; + file.storedInternal = true; + file.url = url; + file.thumbnailUrl = thumbnailUrl; + file.webpublicUrl = webpublicUrl; + file.accessKey = accessKey; + file.thumbnailAccessKey = thumbnailAccessKey; + file.webpublicAccessKey = webpublicAccessKey; + file.name = name; + file.type = type; + file.md5 = hash; + file.size = size; + + return await DriveFiles.save(file); } } @@ -211,51 +200,14 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, await minio.putObject(config.drive.bucket, key, stream, null, metadata); } -/** - * GridFSBucketã«ã‚ªãƒªã‚¸ãƒŠãƒ«ã‚’æ ¼ç´ã™ã‚‹ - */ -export async function storeOriginal(bucket: mongodb.GridFSBucket, name: string, path: string, contentType: string, metadata: any) { - return new Promise<IDriveFile>((resolve, reject) => { - const writeStream = bucket.openUploadStream(name, { - contentType, - metadata - }); - - writeStream.once('finish', resolve); - writeStream.on('error', reject); - fs.createReadStream(path).pipe(writeStream); - }); -} - -/** - * GridFSBucketã«ã‚ªãƒªã‚¸ãƒŠãƒ«ä»¥å¤–ã‚’æ ¼ç´ã™ã‚‹ - */ -export async function storeAlts(bucket: mongodb.GridFSBucket, name: string, data: Buffer, contentType: string, originalId: mongodb.ObjectID) { - return new Promise<IDriveFile>((resolve, reject) => { - const writeStream = bucket.openUploadStream(name, { - contentType, - metadata: { - originalId - } - }); - - writeStream.once('finish', resolve); - writeStream.on('error', reject); - writeStream.end(data); - }); -} - async function deleteOldFile(user: IRemoteUser) { - const oldFile = await DriveFile.findOne({ - _id: { - $nin: [user.avatarId, user.bannerId] - }, - 'metadata.userId': user._id - }, { - sort: { - _id: 1 - } - }); + const oldFile = await DriveFiles.createQueryBuilder() + .select('file') + .where('file.id != :avatarId', { avatarId: user.avatarId }) + .andWhere('file.id != :bannerId', { bannerId: user.bannerId }) + .andWhere('file.userId = :userId', { userId: user.id }) + .orderBy('file.id', 'DESC') + .getOne(); if (oldFile) { delFile(oldFile, true); @@ -278,17 +230,17 @@ async function deleteOldFile(user: IRemoteUser) { * @return Created drive file */ export default async function( - user: IUser, + user: User, path: string, name: string = null, comment: string = null, - folderId: mongodb.ObjectID = null, + folderId: any = null, force: boolean = false, isLink: boolean = false, url: string = null, uri: string = null, sensitive: boolean = null -): Promise<IDriveFile> { +): Promise<DriveFile> { // Calc md5 hash const calcHash = new Promise<string>((res, rej) => { const readable = fs.createReadStream(path); @@ -322,51 +274,29 @@ export default async function( if (!force) { // Check if there is a file with the same hash - const much = await DriveFile.findOne({ + const much = await DriveFiles.findOne({ md5: hash, - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } + userId: user.id, }); if (much) { - logger.info(`file with same hash is found: ${much._id}`); + logger.info(`file with same hash is found: ${much.id}`); return much; } } //#region Check drive usage if (!isLink) { - const usage = await DriveFile - .aggregate([{ - $match: { - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then((aggregates: any[]) => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - logger.debug(`drive usage is ${usage}`); + const usage = await DriveFiles.clacDriveUsageOf(user); const instance = await fetchMeta(); - const driveCapacity = 1024 * 1024 * (isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + + logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); // If usage limit exceeded if (usage + size > driveCapacity) { - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { throw 'no-free-space'; } else { // (ã‚¢ãƒã‚¿ãƒ¼ã¾ãŸã¯ãƒãƒŠãƒ¼ã‚’å«ã¾ãš)最もå¤ã„ファイルを削除ã™ã‚‹ @@ -381,9 +311,9 @@ export default async function( return null; } - const driveFolder = await DriveFolder.findOne({ - _id: folderId, - userId: user._id + const driveFolder = await DriveFolders.findOne({ + id: folderId, + userId: user.id }); if (driveFolder == null) throw 'folder-not-found'; @@ -437,54 +367,48 @@ export default async function( const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]); - const metadata = { - userId: user._id, - _user: { - host: user.host - }, - folderId: folder !== null ? folder._id : null, - comment: comment, - properties: properties, - withoutChunks: isLink, - isRemote: isLink, - isSensitive: isLocalUser(user) && user.settings.alwaysMarkNsfw ? true : - (sensitive !== null && sensitive !== undefined) - ? sensitive - : false - } as IMetadata; + let file = new DriveFile(); + file.id = genId(); + file.createdAt = new Date(); + file.userId = user.id; + file.userHost = user.host; + file.folderId = folder !== null ? folder.id : null; + file.comment = comment; + file.properties = properties; + file.isRemote = isLink; + file.isSensitive = Users.isLocalUser(user) && user.alwaysMarkNsfw ? true : + (sensitive !== null && sensitive !== undefined) + ? sensitive + : false; if (url !== null) { - metadata.src = url; + file.src = url; if (isLink) { - metadata.url = url; + file.url = url; } } if (uri !== null) { - metadata.uri = uri; + file.uri = uri; } - let driveFile: IDriveFile; - if (isLink) { try { - driveFile = await DriveFile.insert({ - length: 0, - uploadDate: new Date(), - md5: hash, - filename: detectedName, - metadata: metadata, - contentType: mime - }); + file.size = 0; + file.md5 = hash; + file.name = detectedName; + file.type = mime; + + file = await DriveFiles.save(file); } catch (e) { // duplicate key error (when already registered) - if (e.code === 11000) { - logger.info(`already registered ${metadata.uri}`); + if (isDuplicateKeyValueError(e)) { + logger.info(`already registered ${file.uri}`); - driveFile = await DriveFile.findOne({ - 'metadata.uri': metadata.uri, - 'metadata.userId': user._id + file = await DriveFiles.findOne({ + uri: file.uri, + userId: user.id }); } else { logger.error(e); @@ -492,29 +416,25 @@ export default async function( } } } else { - driveFile = await (save(path, detectedName, mime, hash, size, metadata)); + file = await (save(file, path, detectedName, mime, hash, size)); } - logger.succ(`drive file has been created ${driveFile._id}`); + logger.succ(`drive file has been created ${file.id}`); - pack(driveFile).then(packedFile => { + DriveFiles.pack(file).then(packedFile => { // Publish driveFileCreated event - publishMainStream(user._id, 'driveFileCreated', packedFile); - publishDriveStream(user._id, 'fileCreated', packedFile); + publishMainStream(user.id, 'driveFileCreated', packedFile); + publishDriveStream(user.id, 'fileCreated', packedFile); }); // 統計を更新 - driveChart.update(driveFile, true); - perUserDriveChart.update(driveFile, true); - if (isRemoteUser(driveFile.metadata._user)) { - instanceChart.updateDrive(driveFile, true); - Instance.update({ host: driveFile.metadata._user.host }, { - $inc: { - driveUsage: driveFile.length, - driveFiles: 1 - } - }); + driveChart.update(file, true); + perUserDriveChart.update(file, true); + if (file.userHost !== null) { + instanceChart.updateDrive(file, true); + Instances.increment({ host: file.userHost }, 'driveUsage', file.size); + Instances.increment({ host: file.userHost }, 'driveFiles', 1); } - return driveFile; + return file; } diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts index c5c15ca20bdc3865660920bea8d2f26da363cdc6..adf57416fe3dd0167beeffe5c5ff299330355d6d 100644 --- a/src/services/drive/delete-file.ts +++ b/src/services/drive/delete-file.ts @@ -1,99 +1,53 @@ import * as Minio from 'minio'; -import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file'; -import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail'; import config from '../../config'; -import driveChart from '../../services/chart/drive'; -import perUserDriveChart from '../../services/chart/per-user-drive'; -import instanceChart from '../../services/chart/instance'; -import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic'; -import Instance from '../../models/instance'; -import { isRemoteUser } from '../../models/user'; +import { DriveFile } from '../../models/entities/drive-file'; +import { InternalStorage } from './internal-storage'; +import { DriveFiles, Instances } from '../../models'; +import { driveChart, perUserDriveChart, instanceChart } from '../chart'; -export default async function(file: IDriveFile, isExpired = false) { - if (file.metadata.storage == 'minio') { - const minio = new Minio.Client(config.drive.config); - - // 後方互æ›æ€§ã®ãŸã‚ã€file.metadata.storageProps.key ãŒã‚ã‚‹ã‹ã©ã†ã‹ãƒã‚§ãƒƒã‚¯ã—ã¦ã„ã¾ã™ã€‚ - // å°†æ¥çš„ã«ã¯ const obj = file.metadata.storageProps.key; ã¨ã—ã¾ã™ã€‚ - const obj = file.metadata.storageProps.key ? file.metadata.storageProps.key : `${config.drive.prefix}/${file.metadata.storageProps.id}`; - await minio.removeObject(config.drive.bucket, obj); +export default async function(file: DriveFile, isExpired = false) { + if (file.storedInternal) { + InternalStorage.del(file.accessKey); - if (file.metadata.thumbnailUrl) { - // 後方互æ›æ€§ã®ãŸã‚ã€file.metadata.storageProps.thumbnailKey ãŒã‚ã‚‹ã‹ã©ã†ã‹ãƒã‚§ãƒƒã‚¯ã—ã¦ã„ã¾ã™ã€‚ - // å°†æ¥çš„ã«ã¯ const thumbnailObj = file.metadata.storageProps.thumbnailKey; ã¨ã—ã¾ã™ã€‚ - const thumbnailObj = file.metadata.storageProps.thumbnailKey ? file.metadata.storageProps.thumbnailKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`; - await minio.removeObject(config.drive.bucket, thumbnailObj); + if (file.thumbnailUrl) { + InternalStorage.del(file.thumbnailAccessKey); } - if (file.metadata.webpublicUrl) { - const webpublicObj = file.metadata.storageProps.webpublicKey ? file.metadata.storageProps.webpublicKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-original`; - await minio.removeObject(config.drive.bucket, webpublicObj); + if (file.webpublicUrl) { + InternalStorage.del(file.webpublicAccessKey); } - } + } else { + const minio = new Minio.Client(config.drive.config); - // ãƒãƒ£ãƒ³ã‚¯ã‚’ã™ã¹ã¦å‰Šé™¤ - await DriveFileChunk.remove({ - files_id: file._id - }); + await minio.removeObject(config.drive.bucket, file.accessKey); - const set = { - metadata: { - deletedAt: new Date(), - isExpired: isExpired + if (file.thumbnailUrl) { + await minio.removeObject(config.drive.bucket, file.thumbnailAccessKey); } - } as any; - - // リモートファイル期é™åˆ‡ã‚Œå‰Šé™¤å¾Œã¯ç›´ãƒªãƒ³ã‚¯ã«ã™ã‚‹ - if (isExpired && file.metadata && file.metadata._user && file.metadata._user.host != null) { - set.metadata.withoutChunks = true; - set.metadata.isRemote = true; - set.metadata.url = file.metadata.uri; - set.metadata.thumbnailUrl = undefined; - set.metadata.webpublicUrl = undefined; - } - - await DriveFile.update({ _id: file._id }, { - $set: set - }); - //#region サムãƒã‚¤ãƒ«ã‚‚ã‚ã‚Œã°å‰Šé™¤ - const thumbnail = await DriveFileThumbnail.findOne({ - 'metadata.originalId': file._id - }); - - if (thumbnail) { - await DriveFileThumbnailChunk.remove({ - files_id: thumbnail._id - }); - - await DriveFileThumbnail.remove({ _id: thumbnail._id }); + if (file.webpublicUrl) { + await minio.removeObject(config.drive.bucket, file.webpublicAccessKey); + } } - //#endregion - //#region Web公開用もã‚ã‚Œã°å‰Šé™¤ - const webpublic = await DriveFileWebpublic.findOne({ - 'metadata.originalId': file._id - }); - - if (webpublic) { - await DriveFileWebpublicChunk.remove({ - files_id: webpublic._id + // リモートファイル期é™åˆ‡ã‚Œå‰Šé™¤å¾Œã¯ç›´ãƒªãƒ³ã‚¯ã«ã™ã‚‹ + if (isExpired && file.userHost !== null) { + DriveFiles.update(file.id, { + isRemote: true, + url: file.uri, + thumbnailUrl: null, + webpublicUrl: null }); - - await DriveFileWebpublic.remove({ _id: webpublic._id }); + } else { + DriveFiles.delete(file.id); } - //#endregion // 統計を更新 driveChart.update(file, false); perUserDriveChart.update(file, false); - if (isRemoteUser(file.metadata._user)) { + if (file.userHost !== null) { instanceChart.updateDrive(file, false); - Instance.update({ host: file.metadata._user.host }, { - $inc: { - driveUsage: -file.length, - driveFiles: -1 - } - }); + Instances.decrement({ host: file.userHost }, 'driveUsage', file.size); + Instances.decrement({ host: file.userHost }, 'driveFiles', 1); } } diff --git a/src/services/drive/internal-storage.ts b/src/services/drive/internal-storage.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff890d7d471a28c9805dd442c7078a303c6a04dd --- /dev/null +++ b/src/services/drive/internal-storage.ts @@ -0,0 +1,27 @@ +import * as fs from 'fs'; +import * as Path from 'path'; +import config from '../../config'; + +export class InternalStorage { + private static readonly path = Path.resolve(`${__dirname}/../../../files`); + + public static read(key: string) { + return fs.createReadStream(`${InternalStorage.path}/${key}`); + } + + public static saveFromPath(key: string, srcPath: string) { + fs.mkdirSync(InternalStorage.path, { recursive: true }); + fs.copyFileSync(srcPath, `${InternalStorage.path}/${key}`); + return `${config.url}/files/${key}`; + } + + public static saveFromBuffer(key: string, data: Buffer) { + fs.mkdirSync(InternalStorage.path, { recursive: true }); + fs.writeFileSync(`${InternalStorage.path}/${key}`, data); + return `${config.url}/files/${key}`; + } + + public static del(key: string) { + fs.unlink(`${InternalStorage.path}/${key}`, () => {}); + } +} diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts index cdf6ba0cef2fdc6e33323e6f2ad91989622f9134..a7fe1fbd264acb77b3a63eb2e06b461d60e797e4 100644 --- a/src/services/drive/upload-from-url.ts +++ b/src/services/drive/upload-from-url.ts @@ -1,26 +1,26 @@ import * as URL from 'url'; - -import { IDriveFile, validateFileName } from '../../models/drive-file'; import create from './add-file'; -import { IUser } from '../../models/user'; -import * as mongodb from 'mongodb'; +import { User } from '../../models/entities/user'; import { driveLogger } from './logger'; import { createTemp } from '../../misc/create-temp'; import { downloadUrl } from '../../misc/donwload-url'; +import { DriveFolder } from '../../models/entities/drive-folder'; +import { DriveFile } from '../../models/entities/drive-file'; +import { DriveFiles } from '../../models'; const logger = driveLogger.createSubLogger('downloader'); export default async ( url: string, - user: IUser, - folderId: mongodb.ObjectID = null, + user: User, + folderId: DriveFolder['id'] = null, uri: string = null, sensitive = false, force = false, link = false -): Promise<IDriveFile> => { +): Promise<DriveFile> => { let name = URL.parse(url).pathname.split('/').pop(); - if (!validateFileName(name)) { + if (!DriveFiles.validateFileName(name)) { name = null; } @@ -30,12 +30,12 @@ export default async ( // write content at URL to temp file await downloadUrl(url, path); - let driveFile: IDriveFile; + let driveFile: DriveFile; let error; try { driveFile = await create(user, path, name, null, folderId, force, link, url, uri, sensitive); - logger.succ(`Got: ${driveFile._id}`); + logger.succ(`Got: ${driveFile.id}`); } catch (e) { error = e; logger.error(`Failed to create drive file: ${e}`, { diff --git a/src/services/following/create.ts b/src/services/following/create.ts index 1eaad750f7fcd3677e64b3a56178a1511344e698..28e4ba3c12ac8070b4837fa7e6ac0ba5250443bb 100644 --- a/src/services/following/create.ts +++ b/src/services/following/create.ts @@ -1,100 +1,70 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; -import Blocking from '../../models/blocking'; import { publishMainStream } from '../stream'; -import notify from '../../services/create-notification'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; import renderAccept from '../../remote/activitypub/renderer/accept'; import renderReject from '../../remote/activitypub/renderer/reject'; import { deliver } from '../../queue'; import createFollowRequest from './requests/create'; -import perUserFollowingChart from '../../services/chart/per-user-following'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; import Logger from '../logger'; -import FollowRequest from '../../models/follow-request'; import { IdentifiableError } from '../../misc/identifiable-error'; +import { User } from '../../models/entities/user'; +import { Followings, Users, FollowRequests, Blockings, Instances } from '../../models'; +import { instanceChart, perUserFollowingChart } from '../chart'; +import { genId } from '../../misc/gen-id'; +import { createNotification } from '../create-notification'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; const logger = new Logger('following/create'); -export async function insertFollowingDoc(followee: IUser, follower: IUser) { +export async function insertFollowingDoc(followee: User, follower: User) { + if (follower.id === followee.id) return; + let alreadyFollowed = false; - await Following.insert({ + await Followings.save({ + id: genId(), createdAt: new Date(), - followerId: follower._id, - followeeId: followee._id, + followerId: follower.id, + followeeId: followee.id, // éžæ£è¦åŒ– - _follower: { - host: follower.host, - inbox: isRemoteUser(follower) ? follower.inbox : undefined, - sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined - }, - _followee: { - host: followee.host, - inbox: isRemoteUser(followee) ? followee.inbox : undefined, - sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined - } + followerHost: follower.host, + followerInbox: Users.isRemoteUser(follower) ? follower.inbox : null, + followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : null, + followeeHost: followee.host, + followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : null, + followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : null }).catch(e => { - if (e.code === 11000 && isRemoteUser(follower) && isLocalUser(followee)) { - logger.info(`Insert duplicated ignore. ${follower._id} => ${followee._id}`); + if (isDuplicateKeyValueError(e) && Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { + logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`); alreadyFollowed = true; } else { throw e; } }); - const removed = await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - if (removed.deletedCount === 1) { - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } - }); - } - if (alreadyFollowed) return; //#region Increment counts - User.update({ _id: follower._id }, { - $inc: { - followingCount: 1 - } - }); - - User.update({ _id: followee._id }, { - $inc: { - followersCount: 1 - } - }); + Users.increment({ id: follower.id }, 'followingCount', 1); + Users.increment({ id: followee.id }, 'followersCount', 1); //#endregion //#region Update instance stats - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { registerOrFetchInstanceDoc(follower.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followingCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'followingCount', 1); instanceChart.updateFollowing(i.host, true); }); - } else if (isLocalUser(follower) && isRemoteUser(followee)) { + } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { registerOrFetchInstanceDoc(followee.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followersCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'followersCount', 1); instanceChart.updateFollowers(i.host, true); }); } @@ -103,44 +73,42 @@ export async function insertFollowingDoc(followee: IUser, follower: IUser) { perUserFollowingChart.update(follower, followee, true); // Publish follow event - if (isLocalUser(follower)) { - packUser(followee, follower, { + if (Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'follow', packed)); + }).then(packed => publishMainStream(follower.id, 'follow', packed)); } // Publish followed event - if (isLocalUser(followee)) { - packUser(follower, followee).then(packed => publishMainStream(followee._id, 'followed', packed)), + if (Users.isLocalUser(followee)) { + Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'followed', packed)), // é€šçŸ¥ã‚’ä½œæˆ - notify(followee._id, follower._id, 'follow'); + createNotification(followee.id, follower.id, 'follow'); } } -export default async function(follower: IUser, followee: IUser, requestId?: string) { +export default async function(follower: User, followee: User, requestId?: string) { // check blocking const [blocking, blocked] = await Promise.all([ - Blocking.findOne({ - blockerId: follower._id, - blockeeId: followee._id, + Blockings.findOne({ + blockerId: follower.id, + blockeeId: followee.id, }), - Blocking.findOne({ - blockerId: followee._id, - blockeeId: follower._id, + Blockings.findOne({ + blockerId: followee.id, + blockeeId: follower.id, }) ]); - if (isRemoteUser(follower) && isLocalUser(followee) && blocked) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocked) { // リモートフォãƒãƒ¼ã‚’å—ã‘ã¦ãƒ–ãƒãƒƒã‚¯ã—ã¦ã„ãŸå ´åˆã¯ã€ã‚¨ãƒ©ãƒ¼ã«ã™ã‚‹ã®ã§ã¯ãªãRejectã‚’é€ã‚Šè¿”ã—ã¦ãŠã—ã¾ã„。 const content = renderActivity(renderReject(renderFollow(follower, followee, requestId), followee)); deliver(followee , content, follower.inbox); return; - } else if (isRemoteUser(follower) && isLocalUser(followee) && blocking) { + } else if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocking) { // リモートフォãƒãƒ¼ã‚’å—ã‘ã¦ãƒ–ãƒãƒƒã‚¯ã•ã‚Œã¦ã„ã‚‹ã¯ãšã®å ´åˆã ã£ãŸã‚‰ã€ãƒ–ãƒãƒƒã‚¯è§£é™¤ã—ã¦ãŠã。 - await Blocking.remove({ - _id: blocking._id - }); + await Blockings.delete(blocking.id); } else { // ãれ以外ã¯å˜ç´”ã«ä¾‹å¤– if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); @@ -151,23 +119,23 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri // フォãƒãƒ¯ãƒ¼ãŒBotã§ã‚ã‚Šã€ãƒ•ã‚©ãƒãƒ¼å¯¾è±¡ãŒBotã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã«æ…Žé‡ã§ã‚ã‚‹ or // フォãƒãƒ¯ãƒ¼ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã§ã‚ã‚Šã€ãƒ•ã‚©ãƒãƒ¼å¯¾è±¡ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã§ã‚ã‚‹ // 上記ã®ã„ãšã‚Œã‹ã«å½“ã¦ã¯ã¾ã‚‹å ´åˆã¯ã™ãフォãƒãƒ¼ã›ãšã«ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’発行ã—ã¦ãŠã - if (followee.isLocked || (followee.carefulBot && follower.isBot) || (isLocalUser(follower) && isRemoteUser(followee))) { + if (followee.isLocked || (followee.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) { let autoAccept = false; // éµã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã‚ã£ã¦ã‚‚ã€æ—¢ã«ãƒ•ã‚©ãƒãƒ¼ã•ã‚Œã¦ã„ãŸå ´åˆã¯ã‚¹ãƒ«ãƒ¼ - const following = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id, + const following = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id, }); if (following) { autoAccept = true; } // フォãƒãƒ¼ã—ã¦ã„るユーザーã¯è‡ªå‹•æ‰¿èªã‚ªãƒ—ション - if (!autoAccept && (isLocalUser(followee) && followee.autoAcceptFollowed)) { - const followed = await Following.findOne({ - followerId: followee._id, - followeeId: follower._id + if (!autoAccept && (Users.isLocalUser(followee) && followee.autoAcceptFollowed)) { + const followed = await Followings.findOne({ + followerId: followee.id, + followeeId: follower.id }); if (followed) autoAccept = true; @@ -181,7 +149,7 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri await insertFollowingDoc(followee, follower); - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { const content = renderActivity(renderAccept(renderFollow(follower, followee, requestId), followee)); deliver(followee, content, follower.inbox); } diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts index d85c8472bb14b494a99935570897493cfd0777e5..ad09f0e6d12bc71087f328adc1da40d19400b135 100644 --- a/src/services/following/delete.ts +++ b/src/services/following/delete.ts @@ -1,22 +1,20 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; import { publishMainStream } from '../stream'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; import renderUndo from '../../remote/activitypub/renderer/undo'; import { deliver } from '../../queue'; -import perUserFollowingChart from '../../services/chart/per-user-following'; import Logger from '../logger'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; +import { User } from '../../models/entities/user'; +import { Followings, Users, Instances } from '../../models'; +import { instanceChart, perUserFollowingChart } from '../chart'; const logger = new Logger('following/delete'); -export default async function(follower: IUser, followee: IUser, silent = false) { - const following = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id +export default async function(follower: User, followee: User, silent = false) { + const following = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id }); if (following == null) { @@ -24,45 +22,25 @@ export default async function(follower: IUser, followee: IUser, silent = false) return; } - Following.remove({ - _id: following._id - }); + Followings.delete(following.id); //#region Decrement following count - User.update({ _id: follower._id }, { - $inc: { - followingCount: -1 - } - }); + Users.decrement({ id: follower.id }, 'followingCount', 1); //#endregion //#region Decrement followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: -1 - } - }); + Users.decrement({ id: followee.id }, 'followersCount', 1); //#endregion //#region Update instance stats - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { registerOrFetchInstanceDoc(follower.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followingCount: -1 - } - }); - + Instances.decrement({ id: i.id }, 'followingCount', 1); instanceChart.updateFollowing(i.host, false); }); - } else if (isLocalUser(follower) && isRemoteUser(followee)) { + } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { registerOrFetchInstanceDoc(followee.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followersCount: -1 - } - }); - + Instances.decrement({ id: i.id }, 'followersCount', 1); instanceChart.updateFollowers(i.host, false); }); } @@ -71,13 +49,13 @@ export default async function(follower: IUser, followee: IUser, silent = false) perUserFollowingChart.update(follower, followee, false); // Publish unfollow event - if (!silent && isLocalUser(follower)) { - packUser(followee, follower, { + if (!silent && Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } diff --git a/src/services/following/requests/accept-all.ts b/src/services/following/requests/accept-all.ts index cf1a9e923d1ad82071b343f6c4425971422c2432..b61c31a5139d6f0ba640a19f09e0dcab744cb9bc 100644 --- a/src/services/following/requests/accept-all.ts +++ b/src/services/following/requests/accept-all.ts @@ -1,24 +1,18 @@ -import User, { IUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import accept from './accept'; +import { User } from '../../../models/entities/user'; +import { FollowRequests, Users } from '../../../models'; /** * 指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼å®›ã¦ã®ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ã™ã¹ã¦æ‰¿èª * @param user ユーザー */ -export default async function(user: IUser) { - const requests = await FollowRequest.find({ - followeeId: user._id +export default async function(user: User) { + const requests = await FollowRequests.find({ + followeeId: user.id }); for (const request of requests) { - const follower = await User.findOne({ _id: request.followerId }); + const follower = await Users.findOne(request.followerId); accept(user, follower); } - - User.update({ _id: user._id }, { - $set: { - pendingReceivedFollowRequestsCount: 0 - } - }); } diff --git a/src/services/following/requests/accept.ts b/src/services/following/requests/accept.ts index 284c6d5e195cca1a24dac01dcd5aca028e0776e3..0be8e24e1a222385a1b373c61606e1842d3bdab8 100644 --- a/src/services/following/requests/accept.ts +++ b/src/services/following/requests/accept.ts @@ -1,26 +1,26 @@ -import { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderAccept from '../../../remote/activitypub/renderer/accept'; import { deliver } from '../../../queue'; import { publishMainStream } from '../../stream'; import { insertFollowingDoc } from '../create'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { FollowRequests, Users } from '../../../models'; -export default async function(followee: IUser, follower: IUser) { - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id +export default async function(followee: User, follower: User) { + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); await insertFollowingDoc(followee, follower); - if (isRemoteUser(follower) && request) { + if (Users.isRemoteUser(follower) && request) { const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId), followee as ILocalUser)); deliver(followee as ILocalUser, content, follower.inbox); } - packUser(followee, followee, { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); } diff --git a/src/services/following/requests/cancel.ts b/src/services/following/requests/cancel.ts index af4cca85fe1871b43b8deb72784d81f6e295b4b2..98fec5d331da2ac551c2a92e79bc1288c4d85164 100644 --- a/src/services/following/requests/cancel.ts +++ b/src/services/following/requests/cancel.ts @@ -1,39 +1,33 @@ -import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderUndo from '../../../remote/activitypub/renderer/undo'; import { deliver } from '../../../queue'; import { publishMainStream } from '../../stream'; import { IdentifiableError } from '../../../misc/identifiable-error'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { Users, FollowRequests } from '../../../models'; -export default async function(followee: IUser, follower: IUser) { - if (isRemoteUser(followee)) { +export default async function(followee: User, follower: User) { + if (Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower as ILocalUser, content, followee.inbox); } - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); if (request == null) { throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); } - await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } - }); - - packUser(followee, followee, { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); } diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts index 10c534f5298c0e0b7a95a3849fe22b96989cee93..32e79d136d72819d9f9e7bdd3ed40a314d28908d 100644 --- a/src/services/following/requests/create.ts +++ b/src/services/following/requests/create.ts @@ -1,66 +1,59 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../../models/user'; import { publishMainStream } from '../../stream'; -import notify from '../../../services/create-notification'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import { deliver } from '../../../queue'; -import FollowRequest from '../../../models/follow-request'; -import Blocking from '../../../models/blocking'; +import { User } from '../../../models/entities/user'; +import { Blockings, FollowRequests, Users } from '../../../models'; +import { genId } from '../../../misc/gen-id'; +import { createNotification } from '../../create-notification'; + +export default async function(follower: User, followee: User, requestId?: string) { + if (follower.id === followee.id) return; -export default async function(follower: IUser, followee: IUser, requestId?: string) { // check blocking const [blocking, blocked] = await Promise.all([ - Blocking.findOne({ - blockerId: follower._id, - blockeeId: followee._id, + Blockings.findOne({ + blockerId: follower.id, + blockeeId: followee.id, }), - Blocking.findOne({ - blockerId: followee._id, - blockeeId: follower._id, + Blockings.findOne({ + blockerId: followee.id, + blockeeId: follower.id, }) ]); if (blocking != null) throw new Error('blocking'); if (blocked != null) throw new Error('blocked'); - await FollowRequest.insert({ + await FollowRequests.save({ + id: genId(), createdAt: new Date(), - followerId: follower._id, - followeeId: followee._id, + followerId: follower.id, + followeeId: followee.id, requestId, // éžæ£è¦åŒ– - _follower: { - host: follower.host, - inbox: isRemoteUser(follower) ? follower.inbox : undefined, - sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined - }, - _followee: { - host: followee.host, - inbox: isRemoteUser(followee) ? followee.inbox : undefined, - sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined - } - }); - - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: 1 - } + followerHost: follower.host, + followerInbox: Users.isRemoteUser(follower) ? follower.inbox : undefined, + followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : undefined, + followeeHost: followee.host, + followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : undefined, + followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : undefined }); // Publish receiveRequest event - if (isLocalUser(followee)) { - packUser(follower, followee).then(packed => publishMainStream(followee._id, 'receiveFollowRequest', packed)); + if (Users.isLocalUser(followee)) { + Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'receiveFollowRequest', packed)); - packUser(followee, followee, { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); // é€šçŸ¥ã‚’ä½œæˆ - notify(followee._id, follower._id, 'receiveFollowRequest'); + createNotification(followee.id, follower.id, 'receiveFollowRequest'); } - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderFollow(follower, followee)); deliver(follower, content, followee.inbox); } diff --git a/src/services/following/requests/reject.ts b/src/services/following/requests/reject.ts index cb924df81123ac333349cae55a423e81afa3050a..c590edcfd8677a55e549eece5bac385425e7f5d7 100644 --- a/src/services/following/requests/reject.ts +++ b/src/services/following/requests/reject.ts @@ -1,34 +1,28 @@ -import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderReject from '../../../remote/activitypub/renderer/reject'; import { deliver } from '../../../queue'; import { publishMainStream } from '../../stream'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { Users, FollowRequests } from '../../../models'; -export default async function(followee: IUser, follower: IUser) { - if (isRemoteUser(follower)) { - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id +export default async function(followee: User, follower: User) { + if (Users.isRemoteUser(follower)) { + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee as ILocalUser)); deliver(followee as ILocalUser, content, follower.inbox); } - await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } - }); - - packUser(followee, follower, { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts index 4d0ae3c1491beb1a7c7f35a8b9000020f0077650..4e43421bdc9bbfbf930924a4e3ab8fa56af8a4cd 100644 --- a/src/services/i/pin.ts +++ b/src/services/i/pin.ts @@ -1,59 +1,51 @@ import config from '../../config'; -import * as mongo from 'mongodb'; -import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user'; -import Note, { packMany } from '../../models/note'; -import Following from '../../models/following'; import renderAdd from '../../remote/activitypub/renderer/add'; import renderRemove from '../../remote/activitypub/renderer/remove'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; import { IdentifiableError } from '../../misc/identifiable-error'; +import { User, ILocalUser } from '../../models/entities/user'; +import { Note } from '../../models/entities/note'; +import { Notes, UserNotePinings, Users, Followings } from '../../models'; +import { UserNotePining } from '../../models/entities/user-note-pinings'; +import { genId } from '../../misc/gen-id'; /** * 指定ã—ãŸæŠ•ç¨¿ã‚’ピン留ã‚ã—ã¾ã™ * @param user * @param noteId */ -export async function addPinned(user: IUser, noteId: mongo.ObjectID) { +export async function addPinned(user: User, noteId: Note['id']) { // Fetch pinee - const note = await Note.findOne({ - _id: noteId, - userId: user._id + const note = await Notes.findOne({ + id: noteId, + userId: user.id }); - if (note === null) { + if (note == null) { throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.'); } - let pinnedNoteIds = user.pinnedNoteIds || []; + const pinings = await UserNotePinings.find({ userId: user.id }); - //#region ç¾åœ¨ãƒ”ン留ã‚投稿ã—ã¦ã„る投稿ãŒå®Ÿéš›ã«ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã«å˜åœ¨ã—ã¦ã„ã‚‹ã®ã‹ãƒã‚§ãƒƒã‚¯ - // データベースã®æ¬ æãªã©ã§å˜åœ¨ã—ã¦ã„ãªã„(ã¾ãŸã¯ç ´æã—ã¦ã„ã‚‹)å ´åˆãŒã‚ã‚‹ã®ã§ã€‚ - // å˜åœ¨ã—ã¦ã„ãªã‹ã£ãŸã‚‰ãƒ”ン留ã‚投稿ã‹ã‚‰å¤–ã™ - const pinnedNotes = await packMany(pinnedNoteIds, null, { detail: true }); - - pinnedNoteIds = pinnedNoteIds.filter(id => pinnedNotes.some(n => n.id.toString() === id.toHexString())); - //#endregion - - if (pinnedNoteIds.length >= 5) { + if (pinings.length >= 5) { throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.'); } - if (pinnedNoteIds.some(id => id.equals(note._id))) { + if (pinings.some(pining => pining.noteId === note.id)) { throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.'); } - pinnedNoteIds.unshift(note._id); - - await User.update(user._id, { - $set: { - pinnedNoteIds: pinnedNoteIds - } - }); + await UserNotePinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + noteId: note.id + } as UserNotePining); // Deliver to remote followers - if (isLocalUser(user)) { - deliverPinnedChange(user._id, note._id, true); + if (Users.isLocalUser(user)) { + deliverPinnedChange(user.id, note.id, true); } } @@ -62,43 +54,40 @@ export async function addPinned(user: IUser, noteId: mongo.ObjectID) { * @param user * @param noteId */ -export async function removePinned(user: IUser, noteId: mongo.ObjectID) { +export async function removePinned(user: User, noteId: Note['id']) { // Fetch unpinee - const note = await Note.findOne({ - _id: noteId, - userId: user._id + const note = await Notes.findOne({ + id: noteId, + userId: user.id }); - if (note === null) { + if (note == null) { throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.'); } - const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id)); - - await User.update(user._id, { - $set: { - pinnedNoteIds: pinnedNoteIds - } + UserNotePinings.delete({ + userId: user.id, + noteId: note.id }); // Deliver to remote followers - if (isLocalUser(user)) { - deliverPinnedChange(user._id, noteId, false); + if (Users.isLocalUser(user)) { + deliverPinnedChange(user.id, noteId, false); } } -export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) { - const user = await User.findOne({ - _id: userId +export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) { + const user = await Users.findOne({ + id: userId }); - if (!isLocalUser(user)) return; + if (!Users.isLocalUser(user)) return; const queue = await CreateRemoteInboxes(user); if (queue.length < 1) return; - const target = `${config.url}/users/${user._id}/collections/featured`; + const target = `${config.url}/users/${user.id}/collections/featured`; const item = `${config.url}/notes/${noteId}`; const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item)); @@ -112,16 +101,20 @@ export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo. * @param user ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ */ async function CreateRemoteInboxes(user: ILocalUser): Promise<string[]> { - const followers = await Following.find({ - followeeId: user._id + const followers = await Followings.find({ + followeeId: user.id }); const queue: string[] = []; for (const following of followers) { - const follower = following._follower; + const follower = { + host: following.followerHost, + inbox: following.followerInbox, + sharedInbox: following.followerSharedInbox, + }; - if (isRemoteUser(follower)) { + if (follower.host !== null) { const inbox = follower.sharedInbox || follower.inbox; if (!queue.includes(inbox)) queue.push(inbox); } diff --git a/src/services/i/update.ts b/src/services/i/update.ts index 887cecb04c00c15f3ec86ae103fb0c7ed6d9610e..7dba472e78326ecc2b9d24b3b20c57b855be2c0f 100644 --- a/src/services/i/update.ts +++ b/src/services/i/update.ts @@ -1,29 +1,26 @@ -import * as mongo from 'mongodb'; -import User, { isLocalUser, isRemoteUser } from '../../models/user'; -import Following from '../../models/following'; -import renderPerson from '../../remote/activitypub/renderer/person'; import renderUpdate from '../../remote/activitypub/renderer/update'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; +import { Followings, Users } from '../../models'; +import { User } from '../../models/entities/user'; +import { renderPerson } from '../../remote/activitypub/renderer/person'; -export async function publishToFollowers(userId: mongo.ObjectID) { - const user = await User.findOne({ - _id: userId +export async function publishToFollowers(userId: User['id']) { + const user = await Users.findOne({ + id: userId }); - const followers = await Following.find({ - followeeId: user._id + const followers = await Followings.find({ + followeeId: user.id }); const queue: string[] = []; // フォãƒãƒ¯ãƒ¼ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰Updateã‚’é…ä¿¡ - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { for (const following of followers) { - const follower = following._follower; - - if (isRemoteUser(follower)) { - const inbox = follower.sharedInbox || follower.inbox; + if (following.followerHost !== null) { + const inbox = following.followerSharedInbox || following.followerInbox; if (!queue.includes(inbox)) queue.push(inbox); } } diff --git a/src/services/logger.ts b/src/services/logger.ts index aa93954bc11e0081804cb1db3d1d71fb3386210c..e6a54e626df0d8140e8a86fc1427a3ddaeeaa494 100644 --- a/src/services/logger.ts +++ b/src/services/logger.ts @@ -3,7 +3,9 @@ import * as os from 'os'; import chalk from 'chalk'; import * as dateformat from 'dateformat'; import { program } from '../argv'; -import Log from '../models/log'; +import { getRepository } from 'typeorm'; +import { Log } from '../models/entities/log'; +import { genId } from '../misc/gen-id'; type Domain = { name: string; @@ -33,7 +35,6 @@ export default class Logger { private log(level: Level, message: string, data: Record<string, any>, important = false, subDomains: Domain[] = [], store = true): void { if (program.quiet) return; - if (process.env.NODE_ENV === 'test') return; if (!this.store) store = false; if (this.parentLogger) { @@ -65,15 +66,17 @@ export default class Logger { console.log(important ? chalk.bold(log) : log); if (store) { - Log.insert({ + const Logs = getRepository(Log); + Logs.insert({ + id: genId(), createdAt: new Date(), machine: os.hostname(), - worker: worker, + worker: worker.toString(), domain: [this.domain].concat(subDomains).map(d => d.name), level: level, message: message, data: data, - }); + } as Log); } } diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 85201086d40d02a9bad11569c8a8ece73030b9f7..9ac9223d3cce2aab6fec63ed34364daceddcf7f8 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -1,61 +1,54 @@ import es from '../../db/elasticsearch'; -import Note, { pack, INote, IChoice } from '../../models/note'; -import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user'; -import { publishMainStream, publishHomeTimelineStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../stream'; -import Following from '../../models/following'; +import { publishMainStream, publishNotesStream } from '../stream'; import { deliver } from '../../queue'; import renderNote from '../../remote/activitypub/renderer/note'; import renderCreate from '../../remote/activitypub/renderer/create'; import renderAnnounce from '../../remote/activitypub/renderer/announce'; import { renderActivity } from '../../remote/activitypub/renderer'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; -import notify from '../../services/create-notification'; -import NoteWatching from '../../models/note-watching'; import watch from './watch'; -import Mute from '../../models/mute'; import { parse } from '../../mfm/parse'; -import { IApp } from '../../models/app'; -import UserList from '../../models/user-list'; import resolveUser from '../../remote/resolve-user'; -import Meta from '../../models/meta'; import config from '../../config'; import { updateHashtag } from '../update-hashtag'; -import isQuote from '../../misc/is-quote'; -import notesChart from '../../services/chart/notes'; -import perUserNotesChart from '../../services/chart/per-user-notes'; -import activeUsersChart from '../../services/chart/active-users'; -import instanceChart from '../../services/chart/instance'; -import * as deepcopy from 'deepcopy'; - import { erase, concat } from '../../prelude/array'; import insertNoteUnread from './unread'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; import extractMentions from '../../misc/extract-mentions'; import extractEmojis from '../../misc/extract-emojis'; import extractHashtags from '../../misc/extract-hashtags'; +import { Note } from '../../models/entities/note'; +import { Mutings, Users, NoteWatchings, Followings, Notes, Instances, Polls } from '../../models'; +import { DriveFile } from '../../models/entities/drive-file'; +import { App } from '../../models/entities/app'; +import { Not } from 'typeorm'; +import { User, ILocalUser, IRemoteUser } from '../../models/entities/user'; +import { genId } from '../../misc/gen-id'; +import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '../chart'; +import { Poll, IPoll } from '../../models/entities/poll'; +import { createNotification } from '../create-notification'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; class NotificationManager { - private notifier: IUser; - private note: INote; + private notifier: User; + private note: Note; private queue: { - target: ILocalUser['_id']; + target: ILocalUser['id']; reason: NotificationType; }[]; - constructor(notifier: IUser, note: INote) { + constructor(notifier: User, note: Note) { this.notifier = notifier; this.note = note; this.queue = []; } - public push(notifiee: ILocalUser['_id'], reason: NotificationType) { + public push(notifiee: ILocalUser['id'], reason: NotificationType) { // 自分自身ã¸ã¯é€šçŸ¥ã—ãªã„ - if (this.notifier._id.equals(notifiee)) return; + if (this.notifier.id === notifiee) return; - const exist = this.queue.find(x => x.target.equals(notifiee)); + const exist = this.queue.find(x => x.target === notifiee); if (exist) { // 「メンションã•ã‚Œã¦ã„ã‚‹ã‹ã¤è¿”ä¿¡ã•ã‚Œã¦ã„ã‚‹ã€å ´åˆã¯ã€ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã¨ã—ã¦ã®é€šçŸ¥ã§ã¯ãªã返信ã¨ã—ã¦ã®é€šçŸ¥ã«ã™ã‚‹ @@ -73,16 +66,16 @@ class NotificationManager { public async deliver() { for (const x of this.queue) { // ãƒŸãƒ¥ãƒ¼ãƒˆæƒ…å ±ã‚’å–å¾— - const mentioneeMutes = await Mute.find({ + const mentioneeMutes = await Mutings.find({ muterId: x.target }); - const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId); // 通知ã•ã‚Œã‚‹å´ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒé€šçŸ¥ã™ã‚‹å´ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ミュートã—ã¦ã„ãªã„é™ã‚Šã¯é€šçŸ¥ã™ã‚‹ - if (!mentioneesMutedUserIds.includes(this.notifier._id.toString())) { - notify(x.target, this.notifier._id, x.reason, { - noteId: this.note._id + if (!mentioneesMutedUserIds.includes(this.notifier.id)) { + createNotification(x.target, this.notifier.id, x.reason, { + noteId: this.note.id }); } } @@ -93,25 +86,25 @@ type Option = { createdAt?: Date; name?: string; text?: string; - reply?: INote; - renote?: INote; - files?: IDriveFile[]; + reply?: Note; + renote?: Note; + files?: DriveFile[]; geo?: any; - poll?: any; + poll?: IPoll; viaMobile?: boolean; localOnly?: boolean; cw?: string; visibility?: string; - visibleUsers?: IUser[]; - apMentions?: IUser[]; + visibleUsers?: User[]; + apMentions?: User[]; apHashtags?: string[]; apEmojis?: string[]; questionUri?: string; uri?: string; - app?: IApp; + app?: App; }; -export default async (user: IUser, data: Option, silent = false) => new Promise<INote>(async (res, rej) => { +export default async (user: User, data: Option, silent = false) => new Promise<Note>(async (res, rej) => { const isFirstNote = user.notesCount === 0; if (data.createdAt == null) data.createdAt = new Date(); @@ -128,16 +121,6 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< data.visibleUsers = erase(null, data.visibleUsers); } - // リプライ対象ãŒå‰Šé™¤ã•ã‚ŒãŸæŠ•ç¨¿ã ã£ãŸã‚‰reject - if (data.reply && data.reply.deletedAt != null) { - return rej('Reply target has been deleted'); - } - - // Renote対象ãŒå‰Šé™¤ã•ã‚ŒãŸæŠ•ç¨¿ã ã£ãŸã‚‰reject - if (data.renote && data.renote.deletedAt != null) { - return rej('Renote target has been deleted'); - } - // Renote対象ãŒã€Œãƒ›ãƒ¼ãƒ ã¾ãŸã¯å…¨ä½“ã€ä»¥å¤–ã®å…¬é–‹ç¯„囲ãªã‚‰reject if (data.renote && data.renote.visibility != 'public' && data.renote.visibility != 'home') { return rej('Renote target is not public or home'); @@ -176,7 +159,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const tokens = data.text ? parse(data.text) : []; const cwTokens = data.cw ? parse(data.cw) : []; const choiceTokens = data.poll && data.poll.choices - ? concat((data.poll.choices as IChoice[]).map(choice => parse(choice.text))) + ? concat(data.poll.choices.map(choice => parse(choice))) : []; const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); @@ -188,24 +171,21 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens); } - // MongoDBã®ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹å¯¾è±¡ã¯128æ–‡å—以上ã«ã§ããªã„ tags = tags.filter(tag => tag.length <= 100); - if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) { - mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); + if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply.userId)) { + mentionedUsers.push(await Users.findOne(data.reply.userId)); } if (data.visibility == 'specified') { for (const u of data.visibleUsers) { - if (!mentionedUsers.some(x => x._id.equals(u._id))) { + if (!mentionedUsers.some(x => x.id === u.id)) { mentionedUsers.push(u); } } - for (const u of mentionedUsers) { - if (!data.visibleUsers.some(x => x._id.equals(u._id))) { - data.visibleUsers.push(u); - } + if (data.reply && !data.visibleUsers.some(x => x.id === data.reply.userId)) { + data.visibleUsers.push(await Users.findOne(data.reply.userId)); } } @@ -221,17 +201,12 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< notesChart.update(note, true); perUserNotesChart.update(user, note, true); // ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒãƒ£ãƒ¼ãƒˆã¯ã‚¿ã‚¤ãƒ ラインå–得時ã«æ›´æ–°ã—ã¦ã„ã‚‹ã®ã§ãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã®å ´åˆã ã‘ã§ã‚ˆã„ - if (isRemoteUser(user)) activeUsersChart.update(user); + if (Users.isRemoteUser(user)) activeUsersChart.update(user); // Register host - if (isRemoteUser(user)) { + if (Users.isRemoteUser(user)) { registerOrFetchInstanceDoc(user.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - notesCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'notesCount', 1); instanceChart.updateNote(i.host, true); }); } @@ -239,20 +214,6 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< // ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°æ›´æ–° for (const tag of tags) updateHashtag(user, tag); - // ファイルãŒæ·»ä»˜ã•ã‚Œã¦ã„ãŸå ´åˆãƒ‰ãƒ©ã‚¤ãƒ–ã®ãƒ•ã‚¡ã‚¤ãƒ«ã®ã€Œã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ä¸€è¦§ã€ãƒ—ãƒãƒ‘ティã«ã“ã®æŠ•ç¨¿ã‚’è¿½åŠ - if (data.files) { - for (const file of data.files) { - DriveFile.update({ _id: file._id }, { - $push: { - 'metadata.attachedNoteIds': note._id - } - }); - } - } - - // Increment notes count - incNotesCount(user); - // Increment notes count (user) incNotesCountOfUser(user); @@ -275,20 +236,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< incRenoteCount(data.renote); } - if (isQuote(note)) { - saveQuote(data.renote, note); - } - // Pack the note - const noteObj = await pack(note); + const noteObj = await Notes.pack(note); if (isFirstNote) { noteObj.isFirstNote = true; } - if (tags.length > 0) { - publishHashtagStream(noteObj); - } + publishNotesStream(noteObj); const nm = new NotificationManager(user, note); const nmRelatedPromises = []; @@ -297,7 +252,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const noteActivity = await renderNoteOrRenoteActivity(data, note); - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity); } @@ -307,12 +262,12 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm)); // ã“ã®æŠ•ç¨¿ã‚’Watchã™ã‚‹ - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, data.reply); + if (Users.isLocalUser(user) && user.autoWatch !== false) { + watch(user.id, data.reply); } // 通知 - if (isLocalUser(data.reply._user)) { + if (data.reply.userHost === null) { nm.push(data.reply.userId, 'reply'); publishMainStream(data.reply.userId, 'reply', noteObj); } @@ -323,7 +278,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const type = data.text ? 'quote' : 'renote'; // Notify - if (isLocalUser(data.renote._user)) { + if (data.renote.userHost === null) { nm.push(data.renote.userId, type); } @@ -331,18 +286,18 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type)); // ã“ã®æŠ•ç¨¿ã‚’Watchã™ã‚‹ - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, data.renote); + if (Users.isLocalUser(user) && user.autoWatch !== false) { + watch(user.id, data.renote); } // Publish event - if (!user._id.equals(data.renote.userId) && isLocalUser(data.renote._user)) { + if ((user.id !== data.renote.userId) && data.renote.userHost === null) { publishMainStream(data.renote.userId, 'renote', noteObj); } } if (!silent) { - publish(user, note, noteObj, data.reply, data.renote, data.visibleUsers, noteActivity); + publish(user, note, data.reply, data.renote, noteActivity); } Promise.all(nmRelatedPromises).then(() => { @@ -353,245 +308,166 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< index(note); }); -async function renderNoteOrRenoteActivity(data: Option, note: INote) { +async function renderNoteOrRenoteActivity(data: Option, note: Note) { if (data.localOnly) return null; const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0) - ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note) + ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note) : renderCreate(await renderNote(note, false), note); return renderActivity(content); } -function incRenoteCount(renote: INote) { - Note.update({ _id: renote._id }, { - $inc: { - renoteCount: 1, - score: 1 - } - }); +function incRenoteCount(renote: Note) { + Notes.increment({ id: renote.id }, 'renoteCount', 1); + Notes.increment({ id: renote.id }, 'score', 1); } -async function publish(user: IUser, note: INote, noteObj: any, reply: INote, renote: INote, visibleUsers: IUser[], noteActivity: any) { - if (isLocalUser(user)) { +async function publish(user: User, note: Note, reply: Note, renote: Note, noteActivity: any) { + if (Users.isLocalUser(user)) { // 投稿ãŒãƒªãƒ—ライã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤ãƒªãƒ—ライ先ã®æŠ•ç¨¿ã®æŠ•ç¨¿è€…ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰é…é€ - if (reply && isRemoteUser(reply._user)) { - deliver(user, noteActivity, reply._user.inbox); + if (reply && reply.userHost !== null) { + deliver(user, noteActivity, reply.userInbox); } // 投稿ãŒRenoteã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤Renoteå…ƒã®æŠ•ç¨¿ã®æŠ•ç¨¿è€…ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰é…é€ - if (renote && isRemoteUser(renote._user)) { - deliver(user, noteActivity, renote._user.inbox); - } - - if (['followers', 'specified'].includes(note.visibility)) { - const detailPackedNote = await pack(note, user, { - detail: true - }); - // Publish event to myself's stream - publishHomeTimelineStream(note.userId, detailPackedNote); - publishHybridTimelineStream(note.userId, detailPackedNote); - - if (note.visibility == 'specified') { - for (const u of visibleUsers) { - if (!u._id.equals(user._id)) { - publishHomeTimelineStream(u._id, detailPackedNote); - publishHybridTimelineStream(u._id, detailPackedNote); - } - } - } - } else { - // Publish event to myself's stream - publishHomeTimelineStream(note.userId, noteObj); - - // Publish note to local and hybrid timeline stream - if (note.visibility != 'home') { - publishLocalTimelineStream(noteObj); - } - - if (note.visibility == 'public') { - publishHybridTimelineStream(null, noteObj); - } else { - // Publish event to myself's stream - publishHybridTimelineStream(note.userId, noteObj); - } + if (renote && renote.userHost !== null) { + deliver(user, noteActivity, renote.userInbox); } } - // Publish note to global timeline stream - if (note.visibility == 'public' && note.replyId == null) { - publishGlobalTimelineStream(noteObj); - } - if (['public', 'home', 'followers'].includes(note.visibility)) { // フォãƒãƒ¯ãƒ¼ã«é…ä¿¡ - publishToFollowers(note, user, noteActivity); + publishToFollowers(note, user, noteActivity, reply); } - - // リストã«é…ä¿¡ - publishToUserLists(note, noteObj); } -async function insertNote(user: IUser, data: Option, tags: string[], emojis: string[], mentionedUsers: IUser[]) { - const insert: any = { +async function insertNote(user: User, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) { + const insert: Partial<Note> = { + id: genId(data.createdAt), createdAt: data.createdAt, - fileIds: data.files ? data.files.map(file => file._id) : [], - replyId: data.reply ? data.reply._id : null, - renoteId: data.renote ? data.renote._id : null, + fileIds: data.files ? data.files.map(file => file.id) : [], + replyId: data.reply ? data.reply.id : null, + renoteId: data.renote ? data.renote.id : null, name: data.name, text: data.text, - poll: data.poll, + hasPoll: data.poll != null, cw: data.cw == null ? null : data.cw, - tags, - tagsLower: tags.map(tag => tag.toLowerCase()), + tags: tags.map(tag => tag.toLowerCase()), emojis, - userId: user._id, + userId: user.id, viaMobile: data.viaMobile, localOnly: data.localOnly, geo: data.geo || null, - appId: data.app ? data.app._id : null, - visibility: data.visibility, + appId: data.app ? data.app.id : null, + visibility: data.visibility as any, visibleUserIds: data.visibility == 'specified' ? data.visibleUsers - ? data.visibleUsers.map(u => u._id) + ? data.visibleUsers.map(u => u.id) : [] : [], + attachedFileTypes: data.files ? data.files.map(file => file.type) : [], + // 以下éžæ£è¦åŒ–データ - _reply: data.reply ? { - userId: data.reply.userId, - user: { - host: data.reply._user.host - } - } : null, - _renote: data.renote ? { - userId: data.renote.userId, - user: { - host: data.renote._user.host - } - } : null, - _user: { - host: user.host, - inbox: isRemoteUser(user) ? user.inbox : undefined - }, - _files: data.files ? data.files : [] + replyUserId: data.reply ? data.reply.userId : null, + replyUserHost: data.reply ? data.reply.userHost : null, + renoteUserId: data.renote ? data.renote.userId : null, + renoteUserHost: data.renote ? data.renote.userHost : null, + userHost: user.host, + userInbox: user.inbox, }; if (data.uri != null) insert.uri = data.uri; // Append mentions data if (mentionedUsers.length > 0) { - insert.mentions = mentionedUsers.map(u => u._id); - insert.mentionedRemoteUsers = mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({ + insert.mentions = mentionedUsers.map(u => u.id); + insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => ({ uri: (u as IRemoteUser).uri, username: u.username, host: u.host - })); + }))); } // æŠ•ç¨¿ã‚’ä½œæˆ try { - return await Note.insert(insert); + const note = await Notes.save(insert); + + if (note.hasPoll) { + await Polls.save({ + id: genId(), + noteId: note.id, + choices: data.poll.choices, + expiresAt: data.poll.expiresAt, + multiple: data.poll.multiple, + votes: new Array(data.poll.choices.length).fill(0), + noteVisibility: note.visibility, + userId: user.id, + userHost: user.host + } as Poll); + } + + return note; } catch (e) { // duplicate key error - if (e.code === 11000) { + if (isDuplicateKeyValueError(e)) { return null; } + console.error(e); + throw 'something happened'; } } -function index(note: INote) { +function index(note: Note) { if (note.text == null || config.elasticsearch == null) return; es.index({ index: 'misskey', type: 'note', - id: note._id.toString(), + id: note.id.toString(), body: { text: note.text } }); } -async function notifyToWatchersOfRenotee(renote: INote, user: IUser, nm: NotificationManager, type: NotificationType) { - const watchers = await NoteWatching.find({ - noteId: renote._id, - userId: { $ne: user._id } - }, { - fields: { - userId: true - } - }); +async function notifyToWatchersOfRenotee(renote: Note, user: User, nm: NotificationManager, type: NotificationType) { + const watchers = await NoteWatchings.find({ + noteId: renote.id, + userId: Not(user.id) + }); for (const watcher of watchers) { nm.push(watcher.userId, type); } } -async function notifyToWatchersOfReplyee(reply: INote, user: IUser, nm: NotificationManager) { - const watchers = await NoteWatching.find({ - noteId: reply._id, - userId: { $ne: user._id } - }, { - fields: { - userId: true - } - }); +async function notifyToWatchersOfReplyee(reply: Note, user: User, nm: NotificationManager) { + const watchers = await NoteWatchings.find({ + noteId: reply.id, + userId: Not(user.id) + }); for (const watcher of watchers) { nm.push(watcher.userId, 'reply'); } } -async function publishToUserLists(note: INote, noteObj: any) { - const lists = await UserList.find({ - userIds: note.userId - }); - - for (const list of lists) { - if (note.visibility == 'specified') { - if (note.visibleUserIds.some(id => id.equals(list.userId))) { - publishUserListStream(list._id, 'note', noteObj); - } - } else { - publishUserListStream(list._id, 'note', noteObj); - } - } -} - -async function publishToFollowers(note: INote, user: IUser, noteActivity: any) { - const detailPackedNote = await pack(note, null, { - detail: true, - skipHide: true - }); - - const followers = await Following.find({ - followeeId: note.userId, - followerId: { $ne: note.userId } // ãƒã‚°ã§ãƒ•ã‚©ãƒãƒ¯ãƒ¼ã«è‡ªåˆ†ãŒã„ã‚‹ã“ã¨ãŒã‚ã‚‹ãŸã‚ +async function publishToFollowers(note: Note, user: User, noteActivity: any, reply: Note) { + const followers = await Followings.find({ + followeeId: note.userId }); const queue: string[] = []; for (const following of followers) { - const follower = following._follower; - - if (isLocalUser(follower)) { - // ã“ã®æŠ•ç¨¿ãŒè¿”ä¿¡ãªã‚‰ã‚¹ã‚ップ - if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) - continue; - - // Publish event to followers stream - publishHomeTimelineStream(following.followerId, detailPackedNote); - - if (isRemoteUser(user) || note.visibility != 'public') { - publishHybridTimelineStream(following.followerId, detailPackedNote); - } - } else { + if (following.followerHost !== null) { // フォãƒãƒ¯ãƒ¼ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰æŠ•ç¨¿ã‚’é…ä¿¡ - if (isLocalUser(user)) { - const inbox = follower.sharedInbox || follower.inbox; + if (Users.isLocalUser(user)) { + const inbox = following.followerSharedInbox || following.followerInbox; if (!queue.includes(inbox)) queue.push(inbox); } } @@ -600,104 +476,52 @@ async function publishToFollowers(note: INote, user: IUser, noteActivity: any) { for (const inbox of queue) { deliver(user as any, noteActivity, inbox); } - - // 後方互æ›è£½ã®ãŸã‚ã€Questionã¯æ™‚é–“å·®ã§Noteã§ã‚‚é€ã‚‹ - // Questionã«å¯¾å¿œã—ã¦ãªã„インスタンスã¯ã€2ã¤ã‚ã®Noteã ã‘を採用ã™ã‚‹ - // Questionã«å¯¾å¿œã—ã¦ã„るインスタンスã¯ã€åŒIDã§æŽ¡ç•ªã•ã‚Œã¦ã„ã‚‹2ã¤ã‚ã®Noteを無視ã™ã‚‹ - setTimeout(() => { - if (noteActivity.object.type === 'Question') { - const asNote = deepcopy(noteActivity); - - asNote.object.type = 'Note'; - asNote.object.content = asNote.object._misskey_fallback_content; - - for (const inbox of queue) { - deliver(user as any, asNote, inbox); - } - } - }, 10 * 1000); } -function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocalUser, noteActivity: any) { - for (const u of mentionedUsers.filter(u => isRemoteUser(u))) { +function deliverNoteToMentionedRemoteUsers(mentionedUsers: User[], user: ILocalUser, noteActivity: any) { + for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) { deliver(user, noteActivity, (u as IRemoteUser).inbox); } } -async function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) { - for (const u of mentionedUsers.filter(u => isLocalUser(u))) { - const detailPackedNote = await pack(note, u, { +async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager) { + for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) { + const detailPackedNote = await Notes.pack(note, u, { detail: true }); - publishMainStream(u._id, 'mention', detailPackedNote); + publishMainStream(u.id, 'mention', detailPackedNote); // Create notification - nm.push(u._id, 'mention'); + nm.push(u.id, 'mention'); } } -function saveQuote(renote: INote, note: INote) { - Note.update({ _id: renote._id }, { - $push: { - _quoteIds: note._id - } - }); -} - -function saveReply(reply: INote, note: INote) { - Note.update({ _id: reply._id }, { - $inc: { - repliesCount: 1 - } - }); +function saveReply(reply: Note, note: Note) { + Notes.increment({ id: reply.id }, 'repliesCount', 1); } -function incNotesCountOfUser(user: IUser) { - User.update({ _id: user._id }, { - $set: { - updatedAt: new Date() - }, - $inc: { - notesCount: 1 - } +function incNotesCountOfUser(user: User) { + Users.increment({ id: user.id }, 'notesCount', 1); + Users.update({ id: user.id }, { + updatedAt: new Date() }); } -function incNotesCount(user: IUser) { - if (isLocalUser(user)) { - Meta.update({}, { - $inc: { - 'stats.notesCount': 1, - 'stats.originalNotesCount': 1 - } - }, { upsert: true }); - } else { - Meta.update({}, { - $inc: { - 'stats.notesCount': 1 - } - }, { upsert: true }); - } -} - -async function extractMentionedUsers(user: IUser, tokens: ReturnType<typeof parse>): Promise<IUser[]> { +async function extractMentionedUsers(user: User, tokens: ReturnType<typeof parse>): Promise<User[]> { if (tokens == null) return []; const mentions = extractMentions(tokens); - let mentionedUsers = - erase(null, await Promise.all(mentions.map(async m => { - try { - return await resolveUser(m.username, m.host ? m.host : user.host); - } catch (e) { - return null; - } - }))); + let mentionedUsers = await Promise.all(mentions.map(m => + resolveUser(m.username, m.host || user.host).catch(() => null) + )); + + mentionedUsers = mentionedUsers.filter(x => x != null); // Drop duplicate users mentionedUsers = mentionedUsers.filter((u, i, self) => - i === self.findIndex(u2 => u._id.equals(u2._id)) + i === self.findIndex(u2 => u.id === u2.id) ); return mentionedUsers; diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts index d71c97b2cab4dddf5a8660bc94f5fbfca5996499..7f04d12cd579d02c8f2c9ea336e6af3379b1b618 100644 --- a/src/services/note/delete.ts +++ b/src/services/note/delete.ts @@ -1,99 +1,50 @@ -import Note, { INote } from '../../models/note'; -import { IUser, isLocalUser, isRemoteUser } from '../../models/user'; import { publishNoteStream } from '../stream'; import renderDelete from '../../remote/activitypub/renderer/delete'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; -import Following from '../../models/following'; import renderTombstone from '../../remote/activitypub/renderer/tombstone'; -import notesChart from '../../services/chart/notes'; -import perUserNotesChart from '../../services/chart/per-user-notes'; import config from '../../config'; -import NoteUnread from '../../models/note-unread'; -import read from './read'; -import DriveFile from '../../models/drive-file'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; -import Favorite from '../../models/favorite'; +import { User } from '../../models/entities/user'; +import { Note } from '../../models/entities/note'; +import { Notes, Users, Followings, Instances } from '../../models'; +import { Not } from 'typeorm'; +import { notesChart, perUserNotesChart, instanceChart } from '../chart'; /** * 投稿を削除ã—ã¾ã™ã€‚ * @param user 投稿者 * @param note 投稿 */ -export default async function(user: IUser, note: INote, quiet = false) { +export default async function(user: User, note: Note, quiet = false) { const deletedAt = new Date(); - await Note.update({ - _id: note._id, - userId: user._id - }, { - $set: { - deletedAt: deletedAt, - text: null, - tags: [], - fileIds: [], - renoteId: null, - poll: null, - geo: null, - cw: null - } + await Notes.delete({ + id: note.id, + userId: user.id }); if (note.renoteId) { - Note.update({ _id: note.renoteId }, { - $inc: { - renoteCount: -1, - score: -1 - }, - $pull: { - _quoteIds: note._id - } - }); - } - - // ã“ã®æŠ•ç¨¿ãŒé–¢ã‚る未èªé€šçŸ¥ã‚’削除 - NoteUnread.find({ - noteId: note._id - }).then(unreads => { - for (const unread of unreads) { - read(unread.userId, unread.noteId); - } - }); - - // ã“ã®æŠ•ç¨¿ã‚’ãŠæ°—ã«å…¥ã‚Šã‹ã‚‰å‰Šé™¤ - Favorite.remove({ - noteId: note._id - }); - - // ファイルãŒæ·»ä»˜ã•ã‚Œã¦ã„ãŸå ´åˆãƒ‰ãƒ©ã‚¤ãƒ–ã®ãƒ•ã‚¡ã‚¤ãƒ«ã®ã€Œã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ä¸€è¦§ã€ãƒ—ãƒãƒ‘ティã‹ã‚‰ã“ã®æŠ•ç¨¿ã‚’削除 - if (note.fileIds) { - for (const fileId of note.fileIds) { - DriveFile.update({ _id: fileId }, { - $pull: { - 'metadata.attachedNoteIds': note._id - } - }); - } + Notes.decrement({ id: note.renoteId }, 'renoteCount', 1); + Notes.decrement({ id: note.renoteId }, 'score', 1); } if (!quiet) { - publishNoteStream(note._id, 'deleted', { + publishNoteStream(note.id, 'deleted', { deletedAt: deletedAt }); //#region ãƒãƒ¼ã‚«ãƒ«ã®æŠ•ç¨¿ãªã‚‰å‰Šé™¤ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティをé…é€ - if (isLocalUser(user)) { - const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user)); + if (Users.isLocalUser(user)) { + const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user)); - const followings = await Following.find({ - followeeId: user._id, - '_follower.host': { $ne: null } + const followings = await Followings.find({ + followeeId: user.id, + followerHost: Not(null) }); for (const following of followings) { - deliver(user, content, following._follower.inbox); + deliver(user, content, following.followerInbox); } } //#endregion @@ -102,14 +53,9 @@ export default async function(user: IUser, note: INote, quiet = false) { notesChart.update(note, false); perUserNotesChart.update(user, note, false); - if (isRemoteUser(user)) { + if (Users.isRemoteUser(user)) { registerOrFetchInstanceDoc(user.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - notesCount: -1 - } - }); - + Instances.decrement({ id: i.id }, 'notesCount', 1); instanceChart.updateNote(i.host, false); }); } diff --git a/src/services/note/polls/update.ts b/src/services/note/polls/update.ts index d4e183889d9b748f2ae9329a0f11f3fd4b61301e..ff8e8d59efd4f893604c05a66460e14bbde1f1ec 100644 --- a/src/services/note/polls/update.ts +++ b/src/services/note/polls/update.ts @@ -1,51 +1,48 @@ -import * as mongo from 'mongodb'; -import Note, { INote } from '../../../models/note'; import { updateQuestion } from '../../../remote/activitypub/models/question'; import ms = require('ms'); import Logger from '../../logger'; -import User, { isLocalUser, isRemoteUser } from '../../../models/user'; -import Following from '../../../models/following'; import renderUpdate from '../../../remote/activitypub/renderer/update'; import { renderActivity } from '../../../remote/activitypub/renderer'; import { deliver } from '../../../queue'; import renderNote from '../../../remote/activitypub/renderer/note'; +import { Users, Notes, Followings } from '../../../models'; +import { Note } from '../../../models/entities/note'; const logger = new Logger('pollsUpdate'); -export async function triggerUpdate(note: INote) { +export async function triggerUpdate(note: Note) { if (!note.updatedAt || Date.now() - new Date(note.updatedAt).getTime() > ms('1min')) { - logger.info(`Updating ${note._id}`); + logger.info(`Updating ${note.id}`); try { const updated = await updateQuestion(note.uri); - logger.info(`Updated ${note._id} ${updated ? 'changed' : 'nochange'}`); + logger.info(`Updated ${note.id} ${updated ? 'changed' : 'nochange'}`); } catch (e) { logger.error(e); } } } -export async function deliverQuestionUpdate(noteId: mongo.ObjectID) { - const note = await Note.findOne({ - _id: noteId, - }); +export async function deliverQuestionUpdate(noteId: Note['id']) { + const note = await Notes.findOne(noteId); - const user = await User.findOne({ - _id: note.userId - }); + const user = await Users.findOne(note.userId); - const followers = await Following.find({ - followeeId: user._id + const followers = await Followings.find({ + followeeId: user.id }); const queue: string[] = []; // フォãƒãƒ¯ãƒ¼ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰Updateã‚’é…ä¿¡ - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { for (const following of followers) { - const follower = following._follower; + const follower = { + inbox: following.followerInbox, + sharedInbox: following.followerSharedInbox + }; - if (isRemoteUser(follower)) { + if (following.followerHost !== null) { const inbox = follower.sharedInbox || follower.inbox; if (!queue.includes(inbox)) queue.push(inbox); } diff --git a/src/services/note/polls/vote.ts b/src/services/note/polls/vote.ts index a23cdc1cb47bfc295331e4a9f8aa8b88ef12808c..15f1ddffbca87ab3ac0116fc918eced556e54bee 100644 --- a/src/services/note/polls/vote.ts +++ b/src/services/note/polls/vote.ts @@ -1,79 +1,74 @@ -import Vote from '../../../models/poll-vote'; -import Note, { INote } from '../../../models/note'; -import Watching from '../../../models/note-watching'; import watch from '../../../services/note/watch'; import { publishNoteStream } from '../../stream'; -import notify from '../../../services/create-notification'; -import { isLocalUser, IUser } from '../../../models/user'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { PollVotes, Users, NoteWatchings, Polls } from '../../../models'; +import { Not } from 'typeorm'; +import { genId } from '../../../misc/gen-id'; +import { createNotification } from '../../create-notification'; -export default (user: IUser, note: INote, choice: number) => new Promise(async (res, rej) => { - if (!note.poll.choices.some(x => x.id == choice)) return rej('invalid choice param'); +export default (user: User, note: Note, choice: number) => new Promise(async (res, rej) => { + const poll = await Polls.findOne({ noteId: note.id }); + + // Check whether is valid choice + if (poll.choices[choice] == null) return rej('invalid choice param'); // if already voted - const exist = await Vote.find({ - noteId: note._id, - userId: user._id + const exist = await PollVotes.find({ + noteId: note.id, + userId: user.id }); - if (note.poll.multiple) { - if (exist.some(x => x.choice === choice)) + if (poll.multiple) { + if (exist.some(x => x.choice === choice)) { return rej('already voted'); - } else if (exist.length) { + } + } else if (exist.length !== 0) { return rej('already voted'); } // Create vote - await Vote.insert({ + await PollVotes.save({ + id: genId(), createdAt: new Date(), - noteId: note._id, - userId: user._id, + noteId: note.id, + userId: user.id, choice: choice }); res(); - const inc: any = {}; - inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == choice)}.votes`] = 1; - // Increment votes count - await Note.update({ _id: note._id }, { - $inc: inc - }); + const index = choice + 1; // In SQL, array index is 1 based + await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE id = '${poll.id}'`); - publishNoteStream(note._id, 'pollVoted', { + publishNoteStream(note.id, 'pollVoted', { choice: choice, - userId: user._id.toHexString() + userId: user.id }); // Notify - notify(note.userId, user._id, 'poll_vote', { - noteId: note._id, + createNotification(note.userId, user.id, 'pollVote', { + noteId: note.id, choice: choice }); // Fetch watchers - Watching - .find({ - noteId: note._id, - userId: { $ne: user._id }, - // 削除ã•ã‚ŒãŸãƒ‰ã‚ュメントã¯é™¤ã - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - for (const watcher of watchers) { - notify(watcher.userId, user._id, 'poll_vote', { - noteId: note._id, - choice: choice - }); - } - }); + NoteWatchings.find({ + noteId: note.id, + userId: Not(user.id), + }) + .then(watchers => { + for (const watcher of watchers) { + createNotification(watcher.userId, user.id, 'pollVote', { + noteId: note.id, + choice: choice + }); + } + }); // ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒæŠ•ç¥¨ã—ãŸå ´åˆã“ã®æŠ•ç¨¿ã‚’Watchã™ã‚‹ - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, note); + if (Users.isLocalUser(user) && user.autoWatch) { + watch(user.id, note); } }); diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 4fdaf92ac6fe74bb1b809e16a04ff7cbf6430daa..437b213ded31acacdee08ca50343c9a1ace6a9cc 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -1,21 +1,24 @@ -import { IUser, isLocalUser, isRemoteUser } from '../../../models/user'; -import Note, { INote } from '../../../models/note'; -import NoteReaction from '../../../models/note-reaction'; import { publishNoteStream } from '../../stream'; -import notify from '../../create-notification'; -import NoteWatching from '../../../models/note-watching'; import watch from '../watch'; import renderLike from '../../../remote/activitypub/renderer/like'; import { deliver } from '../../../queue'; import { renderActivity } from '../../../remote/activitypub/renderer'; -import perUserReactionsChart from '../../../services/chart/per-user-reactions'; import { IdentifiableError } from '../../../misc/identifiable-error'; import { toDbReaction } from '../../../misc/reaction-lib'; import fetchMeta from '../../../misc/fetch-meta'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { NoteReactions, Users, NoteWatchings, Notes } from '../../../models'; +import { Not } from 'typeorm'; +import { perUserReactionsChart } from '../../chart'; +import { genId } from '../../../misc/gen-id'; +import { NoteReaction } from '../../../models/entities/note-reaction'; +import { createNotification } from '../../create-notification'; +import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; -export default async (user: IUser, note: INote, reaction: string) => { +export default async (user: User, note: Note, reaction: string) => { // Myself - if (note.userId.equals(user._id)) { + if (note.userId === user.id) { throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note'); } @@ -23,14 +26,15 @@ export default async (user: IUser, note: INote, reaction: string) => { reaction = await toDbReaction(reaction, meta.enableEmojiReaction); // Create reaction - await NoteReaction.insert({ + await NoteReactions.save({ + id: genId(), createdAt: new Date(), - noteId: note._id, - userId: user._id, + noteId: note.id, + userId: user.id, reaction - }).catch(e => { + } as NoteReaction).catch(e => { // duplicate key error - if (e.code === 11000) { + if (isDuplicateKeyValueError(e)) { throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298', 'already reacted'); } @@ -38,59 +42,53 @@ export default async (user: IUser, note: INote, reaction: string) => { }); // Increment reactions count - await Note.update({ _id: note._id }, { - $inc: { - [`reactionCounts.${reaction}`]: 1, - score: 1 - } - }); + const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; + await Notes.createQueryBuilder().update() + .set({ + reactions: () => sql, + }) + .where('id = :id', { id: note.id }) + .execute(); + // v11 inc score perUserReactionsChart.update(user, note); - publishNoteStream(note._id, 'reacted', { + publishNoteStream(note.id, 'reacted', { reaction: reaction, - userId: user._id + userId: user.id }); // リアクションã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰é€šçŸ¥ã‚’ä½œæˆ - if (isLocalUser(note._user)) { - notify(note.userId, user._id, 'reaction', { - noteId: note._id, + if (note.userHost === null) { + createNotification(note.userId, user.id, 'reaction', { + noteId: note.id, reaction: reaction }); } // Fetch watchers - NoteWatching - .find({ - noteId: note._id, - userId: { $ne: user._id } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - for (const watcher of watchers) { - notify(watcher.userId, user._id, 'reaction', { - noteId: note._id, - reaction: reaction - }); - } - }); + NoteWatchings.find({ + noteId: note.id, + userId: Not(user.id) + }).then(watchers => { + for (const watcher of watchers) { + createNotification(watcher.userId, user.id, 'reaction', { + noteId: note.id, + reaction: reaction + }); + } + }); // ユーザーãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤è‡ªå‹•ã‚¦ã‚©ãƒƒãƒè¨å®šãŒã‚ªãƒ³ãªã‚‰ã°ã“ã®æŠ•ç¨¿ã‚’Watchã™ã‚‹ - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, note); + if (Users.isLocalUser(user) && user.autoWatch !== false) { + watch(user.id, note); } //#region é…ä¿¡ // リアクターãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³å¯¾è±¡ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ãªã‚‰é…é€ - if (isLocalUser(user) && isRemoteUser(note._user)) { + if (Users.isLocalUser(user) && note.userHost !== null) { const content = renderActivity(renderLike(user, note, reaction)); - deliver(user, content, note._user.inbox); + deliver(user, content, note.userInbox); } //#endregion - - return; }; diff --git a/src/services/note/reaction/delete.ts b/src/services/note/reaction/delete.ts index 695534db612794b0c057e5afc27b70e6a4543ad2..ce180aaecae409b8f40fe9ca3945d25022bae1ef 100644 --- a/src/services/note/reaction/delete.ts +++ b/src/services/note/reaction/delete.ts @@ -1,50 +1,47 @@ -import { IUser, isLocalUser, isRemoteUser } from '../../../models/user'; -import Note, { INote } from '../../../models/note'; -import NoteReaction from '../../../models/note-reaction'; import { publishNoteStream } from '../../stream'; import renderLike from '../../../remote/activitypub/renderer/like'; import renderUndo from '../../../remote/activitypub/renderer/undo'; import { renderActivity } from '../../../remote/activitypub/renderer'; import { deliver } from '../../../queue'; import { IdentifiableError } from '../../../misc/identifiable-error'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { NoteReactions, Users, Notes } from '../../../models'; -export default async (user: IUser, note: INote) => { +export default async (user: User, note: Note) => { // if already unreacted - const exist = await NoteReaction.findOne({ - noteId: note._id, - userId: user._id, - deletedAt: { $exists: false } + const exist = await NoteReactions.findOne({ + noteId: note.id, + userId: user.id, }); - if (exist === null) { + if (exist == null) { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } // Delete reaction - await NoteReaction.remove({ - _id: exist._id - }); - - const dec: any = {}; - dec[`reactionCounts.${exist.reaction}`] = -1; + await NoteReactions.delete(exist.id); // Decrement reactions count - Note.update({ _id: note._id }, { - $inc: dec - }); - - publishNoteStream(note._id, 'unreacted', { + const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; + await Notes.createQueryBuilder().update() + .set({ + reactions: () => sql, + }) + .where('id = :id', { id: note.id }) + .execute(); + // v11 dec score + + publishNoteStream(note.id, 'unreacted', { reaction: exist.reaction, - userId: user._id + userId: user.id }); //#region é…ä¿¡ // リアクターãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³å¯¾è±¡ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ãªã‚‰é…é€ - if (isLocalUser(user) && isRemoteUser(note._user)) { + if (Users.isLocalUser(user) && (note.userHost !== null)) { const content = renderActivity(renderUndo(renderLike(user, note, exist.reaction), user)); - deliver(user, content, note._user.inbox); + deliver(user, content, note.userInbox); } //#endregion - - return; }; diff --git a/src/services/note/read.ts b/src/services/note/read.ts index 8b52445cf0b352d85375729692ce0c36fdece881..44d75bd850f8ef12299bd16f9feb43f66d865a67 100644 --- a/src/services/note/read.ts +++ b/src/services/note/read.ts @@ -1,59 +1,35 @@ -import * as mongo from 'mongodb'; -import isObjectId from '../../misc/is-objectid'; import { publishMainStream } from '../stream'; -import User from '../../models/user'; -import NoteUnread from '../../models/note-unread'; +import { Note } from '../../models/entities/note'; +import { User } from '../../models/entities/user'; +import { NoteUnreads } from '../../models'; /** * Mark a note as read */ export default ( - user: string | mongo.ObjectID, - note: string | mongo.ObjectID + userId: User['id'], + noteId: Note['id'] ) => new Promise<any>(async (resolve, reject) => { - - const userId: mongo.ObjectID = isObjectId(user) - ? user as mongo.ObjectID - : new mongo.ObjectID(user); - - const noteId: mongo.ObjectID = isObjectId(note) - ? note as mongo.ObjectID - : new mongo.ObjectID(note); - // Remove document - const res = await NoteUnread.remove({ + const res = await NoteUnreads.delete({ userId: userId, noteId: noteId }); - if (res.deletedCount == 0) { + // v11 TODO: https://github.com/typeorm/typeorm/issues/2415 + if (res.affected == 0) { return; } - const count1 = await NoteUnread - .count({ - userId: userId, - isSpecified: false - }, { - limit: 1 - }); - - const count2 = await NoteUnread - .count({ - userId: userId, - isSpecified: true - }, { - limit: 1 - }); + const count1 = await NoteUnreads.count({ + userId: userId, + isSpecified: false + }); - if (count1 == 0 || count2 == 0) { - User.update({ _id: userId }, { - $set: { - hasUnreadMentions: count1 != 0 || count2 != 0, - hasUnreadSpecifiedNotes: count2 != 0 - } - }); - } + const count2 = await NoteUnreads.count({ + userId: userId, + isSpecified: true + }); if (count1 == 0) { // å…¨ã¦æ—¢èªã«ãªã£ãŸã‚¤ãƒ™ãƒ³ãƒˆã‚’発行 diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts index e70c63c76531e7e09d5a775c8afb25f81c067cda..203cff8d39045297c2e1246a38b257c1c2cc51c9 100644 --- a/src/services/note/unread.ts +++ b/src/services/note/unread.ts @@ -1,47 +1,34 @@ -import NoteUnread from '../../models/note-unread'; -import User, { IUser } from '../../models/user'; -import { INote } from '../../models/note'; -import Mute from '../../models/mute'; +import { Note } from '../../models/entities/note'; import { publishMainStream } from '../stream'; +import { User } from '../../models/entities/user'; +import { Mutings, NoteUnreads } from '../../models'; +import { genId } from '../../misc/gen-id'; -export default async function(user: IUser, note: INote, isSpecified = false) { +export default async function(user: User, note: Note, isSpecified = false) { //#region ミュートã—ã¦ã„ã‚‹ãªã‚‰ç„¡è¦– - const mute = await Mute.find({ - muterId: user._id + const mute = await Mutings.find({ + muterId: user.id }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); - if (mutedUserIds.includes(note.userId.toString())) return; + if (mute.map(m => m.muteeId).includes(note.userId)) return; //#endregion - const unread = await NoteUnread.insert({ - noteId: note._id, - userId: user._id, + const unread = await NoteUnreads.save({ + id: genId(), + noteId: note.id, + userId: user.id, isSpecified, - _note: { - userId: note.userId - } + noteUserId: note.userId }); // 2秒経ã£ã¦ã‚‚æ—¢èªã«ãªã‚‰ãªã‹ã£ãŸã‚‰ã€Œæœªèªã®æŠ•ç¨¿ãŒã‚ã‚Šã¾ã™ã‚ˆã€ã‚¤ãƒ™ãƒ³ãƒˆã‚’発行ã™ã‚‹ setTimeout(async () => { - const exist = await NoteUnread.findOne({ _id: unread._id }); + const exist = await NoteUnreads.findOne(unread.id); if (exist == null) return; - User.update({ - _id: user._id - }, { - $set: isSpecified ? { - hasUnreadSpecifiedNotes: true, - hasUnreadMentions: true - } : { - hasUnreadMentions: true - } - }); - - publishMainStream(user._id, 'unreadMention', note._id); + publishMainStream(user.id, 'unreadMention', note.id); if (isSpecified) { - publishMainStream(user._id, 'unreadSpecifiedNote', note._id); + publishMainStream(user.id, 'unreadSpecifiedNote', note.id); } }, 2000); } diff --git a/src/services/note/unwatch.ts b/src/services/note/unwatch.ts index ef5783231b05ee26d1410cfbb8d1a6a8f6549cd9..047ac343be367636c1766466eabab7d25cba8438 100644 --- a/src/services/note/unwatch.ts +++ b/src/services/note/unwatch.ts @@ -1,9 +1,10 @@ -import * as mongodb from 'mongodb'; -import Watching from '../../models/note-watching'; +import { User } from '../../models/entities/user'; +import { NoteWatchings } from '../../models'; +import { Note } from '../../models/entities/note'; -export default async (me: mongodb.ObjectID, note: object) => { - await Watching.remove({ - noteId: (note as any)._id, +export default async (me: User['id'], note: Note) => { + await NoteWatchings.delete({ + noteId: note.id, userId: me }); }; diff --git a/src/services/note/watch.ts b/src/services/note/watch.ts index aad53610d83fc03979cd0b1d52c9fbb103d684ce..d3c955369646a4d0ead7049987536761676545a2 100644 --- a/src/services/note/watch.ts +++ b/src/services/note/watch.ts @@ -1,25 +1,20 @@ -import * as mongodb from 'mongodb'; -import Watching from '../../models/note-watching'; +import { User } from '../../models/entities/user'; +import { Note } from '../../models/entities/note'; +import { NoteWatchings } from '../../models'; +import { genId } from '../../misc/gen-id'; +import { NoteWatching } from '../../models/entities/note-watching'; -export default async (me: mongodb.ObjectID, note: object) => { +export default async (me: User['id'], note: Note) => { // 自分ã®æŠ•ç¨¿ã¯watchã§ããªã„ - if (me.equals((note as any).userId)) { + if (me === note.userId) { return; } - // if watching now - const exist = await Watching.findOne({ - noteId: (note as any)._id, - userId: me - }); - - if (exist !== null) { - return; - } - - await Watching.insert({ + await NoteWatchings.save({ + id: genId(), createdAt: new Date(), - noteId: (note as any)._id, - userId: me - }); + noteId: note.id, + userId: me, + noteUserId: note.userId + } as NoteWatching); }; diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts index ceb762b2fac1da6c8063e91fce7c590dd982b4c6..defd4d6e2de42f827cecb6d360b15bc3067e3896 100644 --- a/src/services/push-notification.ts +++ b/src/services/push-notification.ts @@ -1,11 +1,10 @@ import * as push from 'web-push'; -import * as mongo from 'mongodb'; -import Subscription from '../models/sw-subscription'; import config from '../config'; +import { SwSubscriptions } from '../models'; +import { Meta } from '../models/entities/meta'; import fetchMeta from '../misc/fetch-meta'; -import { IMeta } from '../models/meta'; -let meta: IMeta = null; +let meta: Meta = null; setInterval(() => { fetchMeta().then(m => { @@ -20,15 +19,11 @@ setInterval(() => { }); }, 3000); -export default async function(userId: mongo.ObjectID | string, type: string, body?: any) { +export default async function(userId: string, type: string, body?: any) { if (!meta.enableServiceWorker) return; - if (typeof userId === 'string') { - userId = new mongo.ObjectID(userId); - } - // Fetch - const subscriptions = await Subscription.find({ + const subscriptions = await SwSubscriptions.find({ userId: userId }); @@ -49,7 +44,7 @@ export default async function(userId: mongo.ObjectID | string, type: string, bod //swLogger.info(err.body); if (err.statusCode == 410) { - Subscription.remove({ + SwSubscriptions.delete({ userId: userId, endpoint: subscription.endpoint, auth: subscription.auth, diff --git a/src/services/register-or-fetch-instance-doc.ts b/src/services/register-or-fetch-instance-doc.ts index d418cd12cecfdc6767115f255411d6dff42de8aa..c96c8a1e325b126c14246f0f924f72a199900062 100644 --- a/src/services/register-or-fetch-instance-doc.ts +++ b/src/services/register-or-fetch-instance-doc.ts @@ -1,15 +1,19 @@ -import Instance, { IInstance } from '../models/instance'; -import federationChart from '../services/chart/federation'; +import { Instance } from '../models/entities/instance'; +import { Instances } from '../models'; +import { federationChart } from './chart'; +import { genId } from '../misc/gen-id'; -export async function registerOrFetchInstanceDoc(host: string): Promise<IInstance> { +export async function registerOrFetchInstanceDoc(host: string): Promise<Instance> { if (host == null) return null; - const index = await Instance.findOne({ host }); + const index = await Instances.findOne({ host }); if (index == null) { - const i = await Instance.insert({ + const i = await Instances.save({ + id: genId(), host, caughtAt: new Date(), + lastCommunicatedAt: new Date(), system: null // TODO }); diff --git a/src/services/stream.ts b/src/services/stream.ts index 813c9eb7c08935a91e0a52ddc86dcc811eb2e441..c1d14b27794c1d78358f635e1317236bb4c20871 100644 --- a/src/services/stream.ts +++ b/src/services/stream.ts @@ -1,8 +1,9 @@ -import * as mongo from 'mongodb'; import redis from '../db/redis'; import Xev from 'xev'; - -type ID = string | mongo.ObjectID; +import { User } from '../models/entities/user'; +import { Note } from '../models/entities/note'; +import { UserList } from '../models/entities/user-list'; +import { ReversiGame } from '../models/entities/games/reversi/game'; class Publisher { private ev: Xev; @@ -29,66 +30,50 @@ class Publisher { } } - public publishMainStream = (userId: ID, type: string, value?: any): void => { + public publishMainStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishDriveStream = (userId: ID, type: string, value?: any): void => { + public publishDriveStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishNoteStream = (noteId: ID, type: string, value: any): void => { + public publishNoteStream = (noteId: Note['id'], type: string, value: any): void => { this.publish(`noteStream:${noteId}`, type, { id: noteId, body: value }); } - public publishUserListStream = (listId: ID, type: string, value?: any): void => { + public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => { this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); } - public publishMessagingStream = (userId: ID, otherpartyId: ID, type: string, value?: any): void => { + public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => { this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); } - public publishMessagingIndexStream = (userId: ID, type: string, value?: any): void => { + public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishReversiStream = (userId: ID, type: string, value?: any): void => { + public publishReversiStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishReversiGameStream = (gameId: ID, type: string, value?: any): void => { + public publishReversiGameStream = (gameId: ReversiGame['id'], type: string, value?: any): void => { this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value); } - public publishHomeTimelineStream = (userId: ID, note: any): void => { - this.publish(`homeTimeline:${userId}`, null, note); - } - - public publishLocalTimelineStream = async (note: any): Promise<void> => { - this.publish('localTimeline', null, note); - } - - public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => { - this.publish(userId ? `hybridTimeline:${userId}` : 'hybridTimeline', null, note); - } - - public publishGlobalTimelineStream = (note: any): void => { - this.publish('globalTimeline', null, note); - } - - public publishHashtagStream = (note: any): void => { - this.publish('hashtag', null, note); + public publishNotesStream = (note: any): void => { + this.publish('notesStream', null, note); } public publishApLogStream = (log: any): void => { this.publish('apLog', null, log); } - public publishAdminStream = (userId: ID, type: string, value?: any): void => { + public publishAdminStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } } @@ -100,15 +85,11 @@ export default publisher; export const publishMainStream = publisher.publishMainStream; export const publishDriveStream = publisher.publishDriveStream; export const publishNoteStream = publisher.publishNoteStream; +export const publishNotesStream = publisher.publishNotesStream; export const publishUserListStream = publisher.publishUserListStream; export const publishMessagingStream = publisher.publishMessagingStream; export const publishMessagingIndexStream = publisher.publishMessagingIndexStream; export const publishReversiStream = publisher.publishReversiStream; export const publishReversiGameStream = publisher.publishReversiGameStream; -export const publishHomeTimelineStream = publisher.publishHomeTimelineStream; -export const publishLocalTimelineStream = publisher.publishLocalTimelineStream; -export const publishHybridTimelineStream = publisher.publishHybridTimelineStream; -export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream; -export const publishHashtagStream = publisher.publishHashtagStream; export const publishApLogStream = publisher.publishApLogStream; export const publishAdminStream = publisher.publishAdminStream; diff --git a/src/services/update-hashtag.ts b/src/services/update-hashtag.ts index 23c39312c051c7d9e30d69e5f4126a726e511a42..6f6d5c4691ee824298e825f025807c304be7e393 100644 --- a/src/services/update-hashtag.ts +++ b/src/services/update-hashtag.ts @@ -1,103 +1,104 @@ -import { IUser, isLocalUser, isRemoteUser } from '../models/user'; -import Hashtag from '../models/hashtag'; -import hashtagChart from './chart/hashtag'; +import { User } from '../models/entities/user'; +import { Hashtags, Users } from '../models'; +import { hashtagChart } from './chart'; +import { genId } from '../misc/gen-id'; +import { Hashtag } from '../models/entities/hashtag'; -export async function updateHashtag(user: IUser, tag: string, isUserAttached = false, inc = true) { +export async function updateHashtag(user: User, tag: string, isUserAttached = false, inc = true) { tag = tag.toLowerCase(); - const index = await Hashtag.findOne({ tag }); + const index = await Hashtags.findOne({ name: tag }); if (index == null && !inc) return; if (index != null) { - const $push = {} as any; - const $pull = {} as any; - const $inc = {} as any; + const q = Hashtags.createQueryBuilder('tag').update() + .where('tag.name = :name', { name: tag }); + + const set = {} as any; if (isUserAttached) { if (inc) { // 自分ãŒåˆã‚ã¦ã“ã®ã‚¿ã‚°ã‚’使ã£ãŸãªã‚‰ - if (!index.attachedUserIds.some(id => id.equals(user._id))) { - $push.attachedUserIds = user._id; - $inc.attachedUsersCount = 1; + if (!index.attachedUserIds.some(id => id === user.id)) { + set.attachedUserIds = () => `array_append(tag.attachedUserIds, '${user.id}')`; + set.attachedUsersCount = () => `tag.attachedUsersCount + 1`; } // 自分ãŒ(ãƒãƒ¼ã‚«ãƒ«å†…ã§)åˆã‚ã¦ã“ã®ã‚¿ã‚°ã‚’使ã£ãŸãªã‚‰ - if (isLocalUser(user) && !index.attachedLocalUserIds.some(id => id.equals(user._id))) { - $push.attachedLocalUserIds = user._id; - $inc.attachedLocalUsersCount = 1; + if (Users.isLocalUser(user) && !index.attachedLocalUserIds.some(id => id === user.id)) { + set.attachedLocalUserIds = () => `array_append(tag.attachedLocalUserIds, '${user.id}')`; + set.attachedLocalUsersCount = () => `tag.attachedLocalUsersCount + 1`; } // 自分ãŒ(リモートã§)åˆã‚ã¦ã“ã®ã‚¿ã‚°ã‚’使ã£ãŸãªã‚‰ - if (isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id.equals(user._id))) { - $push.attachedRemoteUserIds = user._id; - $inc.attachedRemoteUsersCount = 1; + if (Users.isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id === user.id)) { + set.attachedRemoteUserIds = () => `array_append(tag.attachedRemoteUserIds, '${user.id}')`; + set.attachedRemoteUsersCount = () => `tag.attachedRemoteUsersCount + 1`; } } else { - $pull.attachedUserIds = user._id; - $inc.attachedUsersCount = -1; - if (isLocalUser(user)) { - $pull.attachedLocalUserIds = user._id; - $inc.attachedLocalUsersCount = -1; + set.attachedUserIds = () => `array_remove(tag.attachedUserIds, '${user.id}')`; + set.attachedUsersCount = () => `tag.attachedUsersCount - 1`; + if (Users.isLocalUser(user)) { + set.attachedLocalUserIds = () => `array_remove(tag.attachedLocalUserIds, '${user.id}')`; + set.attachedLocalUsersCount = () => `tag.attachedLocalUsersCount - 1`; } else { - $pull.attachedRemoteUserIds = user._id; - $inc.attachedRemoteUsersCount = -1; + set.attachedRemoteUserIds = () => `array_remove(tag.attachedRemoteUserIds, '${user.id}')`; + set.attachedRemoteUsersCount = () => `tag.attachedRemoteUsersCount - 1`; } } } else { // 自分ãŒåˆã‚ã¦ã“ã®ã‚¿ã‚°ã‚’使ã£ãŸãªã‚‰ - if (!index.mentionedUserIds.some(id => id.equals(user._id))) { - $push.mentionedUserIds = user._id; - $inc.mentionedUsersCount = 1; + if (!index.mentionedUserIds.some(id => id === user.id)) { + set.mentionedUserIds = () => `array_append(tag.mentionedUserIds, '${user.id}')`; + set.mentionedUsersCount = () => `tag.mentionedUsersCount + 1`; } // 自分ãŒ(ãƒãƒ¼ã‚«ãƒ«å†…ã§)åˆã‚ã¦ã“ã®ã‚¿ã‚°ã‚’使ã£ãŸãªã‚‰ - if (isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id.equals(user._id))) { - $push.mentionedLocalUserIds = user._id; - $inc.mentionedLocalUsersCount = 1; + if (Users.isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id === user.id)) { + set.mentionedLocalUserIds = () => `array_append(tag.mentionedLocalUserIds, '${user.id}')`; + set.mentionedLocalUsersCount = () => `tag.mentionedLocalUsersCount + 1`; } // 自分ãŒ(リモートã§)åˆã‚ã¦ã“ã®ã‚¿ã‚°ã‚’使ã£ãŸãªã‚‰ - if (isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id.equals(user._id))) { - $push.mentionedRemoteUserIds = user._id; - $inc.mentionedRemoteUsersCount = 1; + if (Users.isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id === user.id)) { + set.mentionedRemoteUserIds = () => `array_append(tag.mentionedRemoteUserIds, '${user.id}')`; + set.mentionedRemoteUsersCount = () => `tag.mentionedRemoteUsersCount + 1`; } } - const q = {} as any; - if (Object.keys($push).length > 0) q.$push = $push; - if (Object.keys($pull).length > 0) q.$pull = $pull; - if (Object.keys($inc).length > 0) q.$inc = $inc; - if (Object.keys(q).length > 0) Hashtag.update({ tag }, q); + q.execute(); } else { if (isUserAttached) { - Hashtag.insert({ - tag, + Hashtags.save({ + id: genId(), + name: tag, mentionedUserIds: [], mentionedUsersCount: 0, mentionedLocalUserIds: [], mentionedLocalUsersCount: 0, mentionedRemoteUserIds: [], mentionedRemoteUsersCount: 0, - attachedUserIds: [user._id], + attachedUserIds: [user.id], attachedUsersCount: 1, - attachedLocalUserIds: isLocalUser(user) ? [user._id] : [], - attachedLocalUsersCount: isLocalUser(user) ? 1 : 0, - attachedRemoteUserIds: isRemoteUser(user) ? [user._id] : [], - attachedRemoteUsersCount: isRemoteUser(user) ? 1 : 0, - }); + attachedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [], + attachedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0, + attachedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [], + attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, + } as Hashtag); } else { - Hashtag.insert({ - tag, - mentionedUserIds: [user._id], + Hashtags.save({ + id: genId(), + name: tag, + mentionedUserIds: [user.id], mentionedUsersCount: 1, - mentionedLocalUserIds: isLocalUser(user) ? [user._id] : [], - mentionedLocalUsersCount: isLocalUser(user) ? 1 : 0, - mentionedRemoteUserIds: isRemoteUser(user) ? [user._id] : [], - mentionedRemoteUsersCount: isRemoteUser(user) ? 1 : 0, + mentionedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [], + mentionedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0, + mentionedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [], + mentionedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, attachedUserIds: [], attachedUsersCount: 0, attachedLocalUserIds: [], attachedLocalUsersCount: 0, attachedRemoteUserIds: [], attachedRemoteUsersCount: 0, - }); + } as Hashtag); } } diff --git a/src/services/user-list/push.ts b/src/services/user-list/push.ts index 5ad4a148275928b32a27ac764c74dda95d2a4c05..958d54b0907331560b8c320f5a5fce768c9d55b1 100644 --- a/src/services/user-list/push.ts +++ b/src/services/user-list/push.ts @@ -1,21 +1,26 @@ -import { pack as packUser, IUser, isRemoteUser, fetchProxyAccount } from '../../models/user'; -import UserList, { IUserList } from '../../models/user-list'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; import renderFollow from '../../remote/activitypub/renderer/follow'; import { publishUserListStream } from '../stream'; +import { User } from '../../models/entities/user'; +import { UserList } from '../../models/entities/user-list'; +import { UserListJoinings, Users } from '../../models'; +import { UserListJoining } from '../../models/entities/user-list-joining'; +import { genId } from '../../misc/gen-id'; +import { fetchProxyAccount } from '../../misc/fetch-proxy-account'; -export async function pushUserToUserList(target: IUser, list: IUserList) { - await UserList.update({ _id: list._id }, { - $push: { - userIds: target._id - } - }); +export async function pushUserToUserList(target: User, list: UserList) { + await UserListJoinings.save({ + id: genId(), + createdAt: new Date(), + userId: target.id, + userListId: list.id + } as UserListJoining); - publishUserListStream(list._id, 'userAdded', await packUser(target)); + publishUserListStream(list.id, 'userAdded', await Users.pack(target)); // ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹å†…ã«ã“ã®ãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’フォãƒãƒ¼ã—ã¦ã„るユーザーãŒã„ãªãã¦ã‚‚投稿をå—ã‘å–ã‚‹ãŸã‚ã«ãƒ€ãƒŸãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒ•ã‚©ãƒãƒ¼ã—ãŸã¨ã„ã†ã“ã¨ã«ã™ã‚‹ - if (isRemoteUser(target)) { + if (Users.isRemoteUser(target)) { const proxy = await fetchProxyAccount(); const content = renderActivity(renderFollow(proxy, target)); deliver(proxy, content, target.inbox); diff --git a/src/tools/add-emoji.ts b/src/tools/add-emoji.ts index 2aa99e37aec1fd7ff04f0bf9e6edefcbaa14e83f..a75798bdadbc03f2683fcbe37abc010e81ef40c3 100644 --- a/src/tools/add-emoji.ts +++ b/src/tools/add-emoji.ts @@ -1,9 +1,11 @@ -import Emoji from '../models/emoji'; +import { Emojis } from '../models'; +import { genId } from '../misc/gen-id'; async function main(name: string, url: string, alias?: string): Promise<any> { const aliases = alias != null ? [ alias ] : []; - await Emoji.insert({ + await Emojis.save({ + id: genId(), host: null, name, url, diff --git a/src/tools/clean-remote-files.ts b/src/tools/clean-remote-files.ts index 28c76345c766c26186065dec0a76194654e9c7e1..f64affea978b3d57e89d3cf6c0e47a8e946f9e8b 100644 --- a/src/tools/clean-remote-files.ts +++ b/src/tools/clean-remote-files.ts @@ -1,18 +1,13 @@ import * as promiseLimit from 'promise-limit'; -import DriveFile, { IDriveFile } from '../models/drive-file'; import del from '../services/drive/delete-file'; +import { DriveFiles } from '../models'; +import { Not } from 'typeorm'; +import { DriveFile } from '../models/entities/drive-file'; const limit = promiseLimit(16); -DriveFile.find({ - 'metadata._user.host': { - $ne: null - }, - 'metadata.deletedAt': { $exists: false } -}, { - fields: { - _id: true - } +DriveFiles.find({ + userHost: Not(null) }).then(async files => { console.log(`there is ${files.length} files`); @@ -21,10 +16,10 @@ DriveFile.find({ console.log('ALL DONE'); }); -async function job(file: IDriveFile): Promise<any> { - file = await DriveFile.findOne({ _id: file._id }); +async function job(file: DriveFile): Promise<any> { + file = await DriveFiles.findOne(file.id); await del(file, true); - console.log('done', file._id); + console.log('done', file.id); } diff --git a/src/tools/move-drive-files.ts b/src/tools/move-drive-files.ts deleted file mode 100644 index 8a1e944503e6ec8bb852524ea3c4ab7e62a4acf1..0000000000000000000000000000000000000000 --- a/src/tools/move-drive-files.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as Minio from 'minio'; -import * as uuid from 'uuid'; -import * as promiseLimit from 'promise-limit'; -import DriveFile, { DriveFileChunk, getDriveFileBucket, IDriveFile } from '../models/drive-file'; -import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../models/drive-file-thumbnail'; -import config from '../config'; - -const limit = promiseLimit(16); - -DriveFile.find({ - $or: [{ - 'metadata.withoutChunks': { $exists: false } - }, { - 'metadata.withoutChunks': false - }], - 'metadata.deletedAt': { $exists: false } -}, { - fields: { - _id: true - } -}).then(async files => { - console.log(`there is ${files.length} files`); - - await Promise.all(files.map(file => limit(() => job(file)))); - - console.log('ALL DONE'); -}); - -async function job(file: IDriveFile): Promise<any> { - file = await DriveFile.findOne({ _id: file._id }); - - const minio = new Minio.Client(config.drive.config); - - const name = file.filename.substr(0, 50); - const keyDir = `${config.drive.prefix}/${uuid.v4()}`; - const key = `${keyDir}/${name}`; - const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`; - const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`; - - const baseUrl = config.drive.baseUrl - || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; - - const bucket = await getDriveFileBucket(); - const readable = bucket.openDownloadStream(file._id); - - await minio.putObject(config.drive.bucket, key, readable, file.length, { - 'Content-Type': file.contentType, - 'Cache-Control': 'max-age=31536000, immutable' - }); - - await DriveFile.findOneAndUpdate({ _id: file._id }, { - $set: { - 'metadata.withoutChunks': true, - 'metadata.storage': 'minio', - 'metadata.storageProps': { - key: key, - thumbnailKey: thumbnailKey - }, - 'metadata.url': `${ baseUrl }/${ keyDir }/${ encodeURIComponent(name) }`, - } - }); - - // ãƒãƒ£ãƒ³ã‚¯ã‚’ã™ã¹ã¦å‰Šé™¤ - await DriveFileChunk.remove({ - files_id: file._id - }); - - //#region サムãƒã‚¤ãƒ«ã‚‚ã‚ã‚Œã°å‰Šé™¤ - const thumbnail = await DriveFileThumbnail.findOne({ - 'metadata.originalId': file._id - }); - - if (thumbnail) { - await DriveFileThumbnailChunk.remove({ - files_id: thumbnail._id - }); - - await DriveFileThumbnail.remove({ _id: thumbnail._id }); - } - //#endregion - - console.log('done', file._id); -} diff --git a/src/tools/show-signin-history.ts b/src/tools/show-signin-history.ts index e770710322edb79cffeb8b8552d57e0fed63039c..584bece6bba87f3d57e0de11c29aafcbe4f4388e 100644 --- a/src/tools/show-signin-history.ts +++ b/src/tools/show-signin-history.ts @@ -1,3 +1,5 @@ +import { Users, Signins } from '../models'; + // node built/tools/show-signin-history username // => {Success} {Date} {IPAddrsss} @@ -7,19 +9,16 @@ // node built/tools/show-signin-history username all // with full request headers -import User from '../models/user'; -import Signin from '../models/signin'; - async function main(username: string, headers: string[]) { - const user = await User.findOne({ + const user = await Users.findOne({ host: null, usernameLower: username.toLowerCase(), }); - if (user === null) throw 'User not found'; + if (user == null) throw 'User not found'; - const history = await Signin.find({ - userId: user._id + const history = await Signins.find({ + userId: user.id }); for (const signin of history) { diff --git a/test/api-visibility.ts b/test/api-visibility.ts index 8380d54f1d8a7477430dc93e1159fba75cacfad5..894d0d07530f3ae9956dd8a44844832b3ba43f54 100644 --- a/test/api-visibility.ts +++ b/test/api-visibility.ts @@ -6,40 +6,33 @@ * * To specify test: * > mocha test/api-visibility.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 */ -import * as http from 'http'; -import * as assert from 'chai'; -import { async, _signup, _request, _uploadFile, _post, _react, resetDb } from './utils'; - -const expect = assert.expect; - -//#region process -Error.stackTraceLimit = Infinity; -// During the test the env variable is set to test process.env.NODE_ENV = 'test'; -// Display detail of unhandled promise rejection -process.on('unhandledRejection', console.dir); -//#endregion - -const app = require('../built/server/api').default; -const db = require('../built/db/mongodb').default; - -const server = http.createServer(app.callback()); - -//#region Utilities -const request = _request(server); -const signup = _signup(request); -const post = _post(request); -//#endregion +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post } from './utils'; describe('API visibility', () => { - // Reset database each test - before(resetDb(db)); + let p: childProcess.ChildProcess; + + before(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', message => { + if (message === 'ok') done(); + }); + }); after(() => { - server.close(); + p.kill(); }); describe('Note visibility', async () => { @@ -61,8 +54,6 @@ describe('API visibility', () => { let fol: any; /** specified-post */ let spe: any; - /** private-post */ - let pri: any; /** public-reply to target's post */ let pubR: any; @@ -72,8 +63,6 @@ describe('API visibility', () => { let folR: any; /** specified-reply to target's post */ let speR: any; - /** private-reply to target's post */ - let priR: any; /** public-mention to target */ let pubM: any; @@ -83,8 +72,6 @@ describe('API visibility', () => { let folM: any; /** specified-mention to target */ let speM: any; - /** private-mention to target */ - let priM: any; /** reply target post */ let tgt: any; @@ -112,7 +99,6 @@ describe('API visibility', () => { home = await post(alice, { text: 'x', visibility: 'home' }); fol = await post(alice, { text: 'x', visibility: 'followers' }); spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); - pri = await post(alice, { text: 'x', visibility: 'private' }); // replies tgt = await post(target, { text: 'y', visibility: 'public' }); @@ -120,14 +106,12 @@ describe('API visibility', () => { homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' }); folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); - priR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'private' }); // mentions pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' }); folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); speM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'specified' }); - priM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'private' }); //#endregion }); @@ -135,111 +119,90 @@ describe('API visibility', () => { // public it('[show] public-postを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pub.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-postをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pub.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-postã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pub.id, other); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-postを未èªè¨¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pub.id, null); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); // home it('[show] home-postを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(home.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-postをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(home.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-postã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(home.id, other); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-postを未èªè¨¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(home.id, null); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); // followers it('[show] followers-postを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(fol.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] followers-postをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(fol.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] followers-postã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(fol.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] followers-postを未èªè¨¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(fol.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); // specified it('[show] specified-postを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(spe.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] specified-postを指定ユーザーãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(spe.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] specified-postをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(spe.id, follower); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-postã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(spe.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-postを未èªè¨¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(spe.id, null); - expect(res.body).have.property('isHidden').eql(true); - })); - - // private - it('[show] private-postを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { - const res = await show(pri.id, alice); - expect(res.body).have.property('text').eql('x'); - })); - - it('[show] private-postをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { - const res = await show(pri.id, follower); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-postã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { - const res = await show(pri.id, other); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-postを未èªè¨¼ãŒè¦‹ã‚Œãªã„', async(async () => { - const res = await show(pri.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); //#endregion @@ -247,131 +210,110 @@ describe('API visibility', () => { // public it('[show] public-replyを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pubR.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-replyã‚’ã•ã‚ŒãŸäººãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pubR.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-replyをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pubR.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-replyã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pubR.id, other); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-replyを未èªè¨¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pubR.id, null); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); // home it('[show] home-replyを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(homeR.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-replyã‚’ã•ã‚ŒãŸäººãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(homeR.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-replyをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(homeR.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-replyã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(homeR.id, other); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-replyを未èªè¨¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(homeR.id, null); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); // followers it('[show] followers-replyを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(folR.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] followers-replyã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ã§ã‚‚リプライã•ã‚Œã¦ã„ã‚Œã°è¦‹ã‚Œã‚‹', async(async () => { const res = await show(folR.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] followers-replyをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(folR.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] followers-replyã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(folR.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] followers-replyを未èªè¨¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(folR.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); // specified it('[show] specified-replyを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(speR.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] specified-replyを指定ユーザーãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(speR.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] specified-replyã‚’ã•ã‚ŒãŸäººãŒæŒ‡å®šã•ã‚Œã¦ãªãã¦ã‚‚見れる', async(async () => { const res = await show(speR.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] specified-replyをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(speR.id, follower); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-replyã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(speR.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-replyを未èªè¨¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(speR.id, null); - expect(res.body).have.property('isHidden').eql(true); - })); - - // private - it('[show] private-replyを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { - const res = await show(priR.id, alice); - expect(res.body).have.property('text').eql('x'); - })); - - it('[show] private-replyをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { - const res = await show(priR.id, follower); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-replyã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { - const res = await show(priR.id, other); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-replyを未èªè¨¼ãŒè¦‹ã‚Œãªã„', async(async () => { - const res = await show(priR.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); //#endregion @@ -379,193 +321,172 @@ describe('API visibility', () => { // public it('[show] public-mentionを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pubM.id, alice); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] public-mentionã‚’ã•ã‚ŒãŸäººãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pubM.id, target); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] public-mentionをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pubM.id, follower); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] public-mentionã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pubM.id, other); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] public-mentionを未èªè¨¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(pubM.id, null); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); // home it('[show] home-mentionを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(homeM.id, alice); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] home-mentionã‚’ã•ã‚ŒãŸäººãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(homeM.id, target); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] home-mentionをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(homeM.id, follower); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] home-mentionã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(homeM.id, other); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] home-mentionを未èªè¨¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(homeM.id, null); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); // followers it('[show] followers-mentionを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(folM.id, alice); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); - it('[show] followers-mentionã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ã§ã‚‚メンションã•ã‚Œã¦ã„ã‚Œã°è¦‹ã‚Œã‚‹', async(async () => { + it('[show] followers-mentionã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã•ã‚Œã¦ã„ã¦ã‚‚見れãªã„', async(async () => { const res = await show(folM.id, target); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.isHidden, true); })); it('[show] followers-mentionをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(folM.id, follower); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] followers-mentionã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(folM.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] followers-mentionを未èªè¨¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(folM.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); // specified it('[show] specified-mentionを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(speM.id, alice); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] specified-mentionを指定ユーザーãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await show(speM.id, target); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); - it('[show] specified-mentionã‚’ã•ã‚ŒãŸäººãŒæŒ‡å®šã•ã‚Œã¦ãªãã¦ã‚‚見れる', async(async () => { + it('[show] specified-mentionã‚’ã•ã‚ŒãŸäººãŒæŒ‡å®šã•ã‚Œã¦ãªã‹ã£ãŸã‚‰è¦‹ã‚Œãªã„', async(async () => { const res = await show(speM.id, target); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-mentionをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(speM.id, follower); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-mentionã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(speM.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-mentionを未èªè¨¼ãŒè¦‹ã‚Œãªã„', async(async () => { const res = await show(speM.id, null); - expect(res.body).have.property('isHidden').eql(true); - })); - - // private - it('[show] private-mentionを自分ãŒè¦‹ã‚Œã‚‹', async(async () => { - const res = await show(priM.id, alice); - expect(res.body).have.property('text').eql('@target x'); - })); - - it('[show] private-mentionをフォãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { - const res = await show(priM.id, follower); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-mentionã‚’éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒè¦‹ã‚Œãªã„', async(async () => { - const res = await show(priM.id, other); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-mentionを未èªè¨¼ãŒè¦‹ã‚Œãªã„', async(async () => { - const res = await show(priM.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); //#endregion //#region HTL it('[HTL] public-post ㌠自分ãŒè¦‹ã‚Œã‚‹', async(async () => { const res = await request('/notes/timeline', { limit: 100 }, alice); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == pub.id); - expect(notes[0]).have.property('text').eql('x'); + assert.strictEqual(notes[0].text, 'x'); })); it('[HTL] public-post ㌠éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ã‹ã‚‰è¦‹ã‚Œãªã„', async(async () => { const res = await request('/notes/timeline', { limit: 100 }, other); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == pub.id); - expect(notes).length(0); + assert.strictEqual(notes.length, 0); })); it('[HTL] followers-post ㌠フォãƒãƒ¯ãƒ¼ã‹ã‚‰è¦‹ã‚Œã‚‹', async(async () => { const res = await request('/notes/timeline', { limit: 100 }, follower); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == fol.id); - expect(notes[0]).have.property('text').eql('x'); + assert.strictEqual(notes[0].text, 'x'); })); //#endregion //#region RTL it('[replies] followers-reply ㌠フォãƒãƒ¯ãƒ¼ã‹ã‚‰è¦‹ã‚Œã‚‹', async(async () => { const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == folR.id); - expect(notes[0]).have.property('text').eql('x'); + assert.strictEqual(notes[0].text, 'x'); })); it('[replies] followers-reply ㌠éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ (リプライ先ã§ã¯ãªã„) ã‹ã‚‰è¦‹ã‚Œãªã„', async(async () => { const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == folR.id); - expect(notes).length(0); + assert.strictEqual(notes.length, 0); })); it('[replies] followers-reply ㌠éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ (リプライ先ã§ã‚ã‚‹) ã‹ã‚‰è¦‹ã‚Œã‚‹', async(async () => { const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == folR.id); - expect(notes[0]).have.property('text').eql('x'); + assert.strictEqual(notes[0].text, 'x'); })); //#endregion //#region MTL it('[mentions] followers-reply ㌠éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ (リプライ先ã§ã‚ã‚‹) ã‹ã‚‰è¦‹ã‚Œã‚‹', async(async () => { const res = await request('/notes/mentions', { limit: 100 }, target); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == folR.id); - expect(notes[0]).have.property('text').eql('x'); + assert.strictEqual(notes[0].text, 'x'); })); it('[mentions] followers-mention ㌠éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ (メンション先ã§ã‚ã‚‹) ã‹ã‚‰è¦‹ã‚Œã‚‹', async(async () => { const res = await request('/notes/mentions', { limit: 100 }, target); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == folM.id); - expect(notes[0]).have.property('text').eql('@target x'); + assert.strictEqual(notes[0].text, '@target x'); })); //#endregion }); diff --git a/test/api.ts b/test/api.ts index cc4521d3dc858e1c05d04873b3c1f5cf05ecc635..71443c5730fe4d4647a3e80b1d37bebe1294ce6a 100644 --- a/test/api.ts +++ b/test/api.ts @@ -6,44 +6,35 @@ * * To specify test: * > mocha test/api.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 */ -import * as http from 'http'; -import * as fs from 'fs'; -import * as assert from 'chai'; -import { async, _signup, _request, _uploadFile, _post, _react, resetDb } from './utils'; - -const expect = assert.expect; - -//#region process -Error.stackTraceLimit = Infinity; - -// During the test the env variable is set to test process.env.NODE_ENV = 'test'; -// Display detail of unhandled promise rejection -process.on('unhandledRejection', console.dir); -//#endregion - -const app = require('../built/server/api').default; -const db = require('../built/db/mongodb').default; - -const server = http.createServer(app.callback()); - -//#region Utilities -const request = _request(server); -const signup = _signup(request); -const post = _post(request); -const react = _react(request); -const uploadFile = _uploadFile(server); -//#endregion +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, react, uploadFile } from './utils'; describe('API', () => { - // Reset database each test - beforeEach(resetDb(db)); + let p: childProcess.ChildProcess; + + beforeEach(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', message => { + if (message === 'ok') { + done(); + } + }); + }); - after(() => { - server.close(); + afterEach(() => { + p.kill(); }); describe('signup', () => { @@ -52,7 +43,7 @@ describe('API', () => { username: 'test.', password: 'test' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('空ã®ãƒ‘スワードã§ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒä½œæˆã§ããªã„', async(async () => { @@ -60,7 +51,7 @@ describe('API', () => { username: 'test', password: '' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('æ£ã—ãアカウントãŒä½œæˆã§ãã‚‹', async(async () => { @@ -71,9 +62,9 @@ describe('API', () => { const res = await request('/signup', me); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('username').eql(me.username); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.username, me.username); })); it('åŒã˜ãƒ¦ãƒ¼ã‚¶ãƒ¼åã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ä½œæˆã§ããªã„', async(async () => { @@ -86,7 +77,7 @@ describe('API', () => { password: 'test' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -102,7 +93,7 @@ describe('API', () => { password: 'bar' }); - expect(res).have.status(403); + assert.strictEqual(res.status, 403); })); it('クエリをインジェクションã§ããªã„', async(async () => { @@ -117,7 +108,7 @@ describe('API', () => { } }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('æ£ã—ã„æƒ…å ±ã§ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã§ãã‚‹', async(async () => { @@ -131,7 +122,7 @@ describe('API', () => { password: 'foo' }); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); })); }); @@ -149,12 +140,11 @@ describe('API', () => { birthday: myBirthday }, me); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql(myName); - expect(res.body).have.nested.property('profile').a('object'); - expect(res.body).have.nested.property('profile.location').eql(myLocation); - expect(res.body).have.nested.property('profile.birthday').eql(myBirthday); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, myName); + assert.strictEqual(res.body.location, myLocation); + assert.strictEqual(res.body.birthday, myBirthday); })); it('åå‰ã‚’空白ã«ã§ããªã„', async(async () => { @@ -162,7 +152,7 @@ describe('API', () => { const res = await request('/i/update', { name: ' ' }, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('誕生日ã®è¨å®šã‚’削除ã§ãã‚‹', async(async () => { @@ -175,10 +165,9 @@ describe('API', () => { birthday: null }, me); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.nested.property('profile').a('object'); - expect(res.body).have.nested.property('profile.birthday').eql(null); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.birthday, null); })); it('ä¸æ£ãªèª•ç”Ÿæ—¥ã®å½¢å¼ã§æ€’られる', async(async () => { @@ -186,7 +175,7 @@ describe('API', () => { const res = await request('/i/update', { birthday: '2000/09/07' }, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -198,365 +187,23 @@ describe('API', () => { userId: me.id }, me); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('id').eql(me.id); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.id, me.id); })); it('ユーザーãŒå˜åœ¨ã—ãªã‹ã£ãŸã‚‰æ€’ã‚‹', async(async () => { const res = await request('/users/show', { userId: '000000000000000000000000' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('é–“é•ã£ãŸIDã§æ€’られる', async(async () => { const res = await request('/users/show', { userId: 'kyoppie' }); - expect(res).have.status(400); - })); - }); - - describe('notes/create', () => { - it('投稿ã§ãã‚‹', async(async () => { - const me = await signup(); - const post = { - text: 'test' - }; - - const res = await request('/notes/create', post, me); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('text').eql(post.text); - })); - - it('ファイルを添付ã§ãã‚‹', async(async () => { - const me = await signup(); - const file = await uploadFile(me); - - const res = await request('/notes/create', { - fileIds: [file.id] - }, me); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('fileIds').eql([file.id]); - })); - - it('他人ã®ãƒ•ã‚¡ã‚¤ãƒ«ã¯ç„¡è¦–', async(async () => { - const me = await signup({ username: 'alice' }); - const bob = await signup({ username: 'bob' }); - const file = await uploadFile(bob); - - const res = await request('/notes/create', { - text: 'test', - fileIds: [file.id] - }, me); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('fileIds').eql([]); - })); - - it('å˜åœ¨ã—ãªã„ファイルã¯ç„¡è¦–', async(async () => { - const me = await signup(); - - const res = await request('/notes/create', { - text: 'test', - fileIds: ['000000000000000000000000'] - }, me); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('fileIds').eql([]); - })); - - it('ä¸æ£ãªãƒ•ã‚¡ã‚¤ãƒ«IDã§æ€’られる', async(async () => { - const me = await signup(); - const res = await request('/notes/create', { - fileIds: ['kyoppie'] - }, me); - expect(res).have.status(400); - })); - - it('返信ã§ãã‚‹', async(async () => { - const bob = await signup({ username: 'bob' }); - const bobPost = await post(bob); - - const alice = await signup({ username: 'alice' }); - const alicePost = { - text: 'test', - replyId: bobPost.id - }; - - const res = await request('/notes/create', alicePost, alice); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('text').eql(alicePost.text); - expect(res.body.createdNote).have.property('replyId').eql(alicePost.replyId); - expect(res.body.createdNote).have.property('reply'); - expect(res.body.createdNote.reply).have.property('text').eql(alicePost.text); - })); - - it('renoteã§ãã‚‹', async(async () => { - const bob = await signup({ username: 'bob' }); - const bobPost = await post(bob, { - text: 'test' - }); - - const alice = await signup({ username: 'alice' }); - const alicePost = { - renoteId: bobPost.id - }; - - const res = await request('/notes/create', alicePost, alice); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('renoteId').eql(alicePost.renoteId); - expect(res.body.createdNote).have.property('renote'); - expect(res.body.createdNote.renote).have.property('text').eql(bobPost.text); - })); - - it('引用renoteã§ãã‚‹', async(async () => { - const bob = await signup({ username: 'bob' }); - const bobPost = await post(bob, { - text: 'test' - }); - - const alice = await signup({ username: 'alice' }); - const alicePost = { - text: 'test', - renoteId: bobPost.id - }; - - const res = await request('/notes/create', alicePost, alice); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('text').eql(alicePost.text); - expect(res.body.createdNote).have.property('renoteId').eql(alicePost.renoteId); - expect(res.body.createdNote).have.property('renote'); - expect(res.body.createdNote.renote).have.property('text').eql(bobPost.text); - })); - - it('æ–‡å—æ•°ãŽã‚ŠãŽã‚Šã§æ€’られãªã„', async(async () => { - const me = await signup(); - const post = { - text: '!'.repeat(1000) - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(200); - })); - - it('æ–‡å—数オーãƒãƒ¼ã§æ€’られる', async(async () => { - const me = await signup(); - const post = { - text: '!'.repeat(1001) - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(400); - })); - - it('å˜åœ¨ã—ãªã„リプライ先ã§æ€’られる', async(async () => { - const me = await signup(); - const post = { - text: 'test', - replyId: '000000000000000000000000' - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(400); - })); - - it('å˜åœ¨ã—ãªã„renote対象ã§æ€’られる', async(async () => { - const me = await signup(); - const post = { - renoteId: '000000000000000000000000' - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(400); - })); - - it('ä¸æ£ãªãƒªãƒ—ライ先IDã§æ€’られる', async(async () => { - const me = await signup(); - const post = { - text: 'test', - replyId: 'foo' - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(400); - })); - - it('ä¸æ£ãªrenote対象IDã§æ€’られる', async(async () => { - const me = await signup(); - const post = { - renoteId: 'foo' - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(400); - })); - - it('投票を添付ã§ãã‚‹', async(async () => { - const me = await signup(); - - const res = await request('/notes/create', { - text: 'test', - poll: { - choices: ['foo', 'bar'] - } - }, me); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('poll'); - })); - - it('投票ã®é¸æŠžè‚¢ãŒç„¡ãã¦æ€’られる', async(async () => { - const me = await signup(); - const res = await request('/notes/create', { - poll: {} - }, me); - expect(res).have.status(400); - })); - - it('投票ã®é¸æŠžè‚¢ãŒç„¡ãã¦æ€’られる (空ã®é…列)', async(async () => { - const me = await signup(); - const res = await request('/notes/create', { - poll: { - choices: [] - } - }, me); - expect(res).have.status(400); - })); - - it('投票ã®é¸æŠžè‚¢ãŒ1ã¤ã§æ€’られる', async(async () => { - const me = await signup(); - const res = await request('/notes/create', { - poll: { - choices: ['Strawberry Pasta'] - } - }, me); - expect(res).have.status(400); - })); - - it('投票ã§ãã‚‹', async(async () => { - const me = await signup(); - - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'] - } - }, me); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1 - }, me); - - expect(res).have.status(204); - })); - - it('複数投票ã§ããªã„', async(async () => { - const me = await signup(); - - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'] - } - }, me); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 0 - }, me); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 2 - }, me); - - expect(res).have.status(400); - })); - - it('許å¯ã•ã‚Œã¦ã„ã‚‹å ´åˆã¯è¤‡æ•°æŠ•ç¥¨ã§ãã‚‹', async(async () => { - const me = await signup(); - - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - multiple: true - } - }, me); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 0 - }, me); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1 - }, me); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 2 - }, me); - - expect(res).have.status(204); - })); - - it('ç· ã‚切られã¦ã„ã‚‹å ´åˆã¯æŠ•ç¥¨ã§ããªã„', async(async () => { - const me = await signup(); - - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - expiredAfter: 1 - } - }, me); - - await new Promise(x => setTimeout(x, 2)); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1 - }, me); - - expect(res).have.status(400); - })); - - it('åŒã˜ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«è¤‡æ•°ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã—ã¦ã‚‚内部的ã«ã¾ã¨ã‚られる', async(async () => { - const alice = await signup({ username: 'alice' }); - const bob = await signup({ username: 'bob' }); - const post = { - text: '@bob @bob @bob yo' - }; - - const res = await request('/notes/create', post, alice); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('text').eql(post.text); - - const noteDoc = await db.get('notes').findOne({ _id: res.body.createdNote.id }); - expect(noteDoc.mentions.map((id: any) => id.toString())).eql([bob.id.toString()]); + assert.strictEqual(res.status, 400); })); }); @@ -571,24 +218,24 @@ describe('API', () => { noteId: myPost.id }, me); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('id').eql(myPost.id); - expect(res.body).have.property('text').eql(myPost.text); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.id, myPost.id); + assert.strictEqual(res.body.text, myPost.text); })); it('投稿ãŒå˜åœ¨ã—ãªã‹ã£ãŸã‚‰æ€’ã‚‹', async(async () => { const res = await request('/notes/show', { noteId: '000000000000000000000000' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('é–“é•ã£ãŸIDã§æ€’られる', async(async () => { const res = await request('/notes/show', { noteId: 'kyoppie' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -603,7 +250,7 @@ describe('API', () => { reaction: 'like' }, alice); - expect(res).have.status(204); + assert.strictEqual(res.status, 204); })); it('自分ã®æŠ•ç¨¿ã«ã¯ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ããªã„', async(async () => { @@ -615,7 +262,7 @@ describe('API', () => { reaction: 'like' }, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('二é‡ã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ããªã„', async(async () => { @@ -630,7 +277,7 @@ describe('API', () => { reaction: 'like' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('å˜åœ¨ã—ãªã„投稿ã«ã¯ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ããªã„', async(async () => { @@ -641,7 +288,7 @@ describe('API', () => { reaction: 'like' }, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('空ã®ãƒ‘ラメータã§æ€’られる', async(async () => { @@ -649,7 +296,7 @@ describe('API', () => { const res = await request('/notes/reactions/create', {}, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('é–“é•ã£ãŸIDã§æ€’られる', async(async () => { @@ -660,7 +307,7 @@ describe('API', () => { reaction: 'like' }, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -673,7 +320,7 @@ describe('API', () => { userId: alice.id }, bob); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); })); it('æ—¢ã«ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ã‚‹å ´åˆã¯æ€’ã‚‹', async(async () => { @@ -687,7 +334,7 @@ describe('API', () => { userId: alice.id }, bob); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('å˜åœ¨ã—ãªã„ユーザーã¯ãƒ•ã‚©ãƒãƒ¼ã§ããªã„', async(async () => { @@ -697,7 +344,7 @@ describe('API', () => { userId: '000000000000000000000000' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('自分自身ã¯ãƒ•ã‚©ãƒãƒ¼ã§ããªã„', async(async () => { @@ -707,7 +354,7 @@ describe('API', () => { userId: alice.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('空ã®ãƒ‘ラメータã§æ€’られる', async(async () => { @@ -715,7 +362,7 @@ describe('API', () => { const res = await request('/following/create', {}, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('é–“é•ã£ãŸIDã§æ€’られる', async(async () => { @@ -725,7 +372,7 @@ describe('API', () => { userId: 'foo' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -741,7 +388,7 @@ describe('API', () => { userId: alice.id }, bob); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); })); it('フォãƒãƒ¼ã—ã¦ã„ãªã„å ´åˆã¯æ€’ã‚‹', async(async () => { @@ -752,7 +399,7 @@ describe('API', () => { userId: alice.id }, bob); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('å˜åœ¨ã—ãªã„ユーザーã¯ãƒ•ã‚©ãƒãƒ¼è§£é™¤ã§ããªã„', async(async () => { @@ -762,7 +409,7 @@ describe('API', () => { userId: '000000000000000000000000' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('自分自身ã¯ãƒ•ã‚©ãƒãƒ¼è§£é™¤ã§ããªã„', async(async () => { @@ -772,7 +419,7 @@ describe('API', () => { userId: alice.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('空ã®ãƒ‘ラメータã§æ€’られる', async(async () => { @@ -780,7 +427,7 @@ describe('API', () => { const res = await request('/following/delete', {}, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('é–“é•ã£ãŸIDã§æ€’られる', async(async () => { @@ -790,7 +437,7 @@ describe('API', () => { userId: 'kyoppie' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -799,20 +446,20 @@ describe('API', () => { it('ãƒ‰ãƒ©ã‚¤ãƒ–æƒ…å ±ã‚’å–å¾—ã§ãã‚‹', async(async () => { const bob = await signup({ username: 'bob' }); await uploadFile({ - userId: me._id, - datasize: 256 + userId: me.id, + size: 256 }); await uploadFile({ - userId: me._id, - datasize: 512 + userId: me.id, + size: 512 }); await uploadFile({ - userId: me._id, - datasize: 1024 + userId: me.id, + size: 1024 }); const res = await request('/drive', {}, me); - expect(res).have.status(200); - expect(res.body).be.a('object'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); expect(res.body).have.property('usage').eql(1792); }));*/ }); @@ -821,14 +468,11 @@ describe('API', () => { it('ファイルを作æˆã§ãã‚‹', async(async () => { const alice = await signup({ username: 'alice' }); - const res = await assert.request(server) - .post('/drive/files/create') - .field('i', alice.token) - .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png'); + const res = await uploadFile(alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('Lenna.png'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'Lenna.png'); })); it('ファイル無ã—ã§æ€’られる', async(async () => { @@ -836,21 +480,18 @@ describe('API', () => { const res = await request('/drive/files/create', {}, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('SVGファイルを作æˆã§ãã‚‹', async(async () => { const izumi = await signup({ username: 'izumi' }); - const res = await assert.request(server) - .post('/drive/files/create') - .field('i', izumi.token) - .attach('file', fs.readFileSync(__dirname + '/resources/image.svg'), 'image.svg'); + const res = await uploadFile(izumi, __dirname + '/resources/image.svg'); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('image.svg'); - expect(res.body).have.property('type').eql('image/svg+xml'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'image.svg'); + assert.strictEqual(res.body.type, 'image/svg+xml'); })); }); @@ -865,9 +506,9 @@ describe('API', () => { name: newName }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql(newName); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, newName); })); it('他人ã®ãƒ•ã‚¡ã‚¤ãƒ«ã¯æ›´æ–°ã§ããªã„', async(async () => { @@ -880,7 +521,7 @@ describe('API', () => { name: 'ã„ã¡ã”パスタ.png' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('親フォルダを更新ã§ãã‚‹', async(async () => { @@ -895,9 +536,9 @@ describe('API', () => { folderId: folder.id }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('folderId').eql(folder.id); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.folderId, folder.id); })); it('親フォルダを無ã—ã«ã§ãã‚‹', async(async () => { @@ -918,9 +559,9 @@ describe('API', () => { folderId: null }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('folderId').eql(null); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.folderId, null); })); it('他人ã®ãƒ•ã‚©ãƒ«ãƒ€ã«ã¯å…¥ã‚Œã‚‰ã‚Œãªã„', async(async () => { @@ -936,7 +577,7 @@ describe('API', () => { folderId: folder.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('å˜åœ¨ã—ãªã„フォルダã§æ€’られる', async(async () => { @@ -948,7 +589,7 @@ describe('API', () => { folderId: '000000000000000000000000' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('ä¸æ£ãªãƒ•ã‚©ãƒ«ãƒ€IDã§æ€’られる', async(async () => { @@ -960,7 +601,7 @@ describe('API', () => { folderId: 'foo' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('ファイルãŒå˜åœ¨ã—ãªã‹ã£ãŸã‚‰æ€’ã‚‹', async(async () => { @@ -971,7 +612,7 @@ describe('API', () => { name: 'ã„ã¡ã”パスタ.png' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('é–“é•ã£ãŸIDã§æ€’られる', async(async () => { @@ -982,7 +623,7 @@ describe('API', () => { name: 'ã„ã¡ã”パスタ.png' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -994,9 +635,9 @@ describe('API', () => { name: 'test' }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('test'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'test'); })); }); @@ -1012,9 +653,9 @@ describe('API', () => { name: 'new name' }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('new name'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'new name'); })); it('他人ã®ãƒ•ã‚©ãƒ«ãƒ€ã‚’æ›´æ–°ã§ããªã„', async(async () => { @@ -1029,7 +670,7 @@ describe('API', () => { name: 'new name' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('親フォルダを更新ã§ãã‚‹', async(async () => { @@ -1046,9 +687,9 @@ describe('API', () => { parentId: parentFolder.id }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('parentId').eql(parentFolder.id); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.parentId, parentFolder.id); })); it('親フォルダを無ã—ã«æ›´æ–°ã§ãã‚‹', async(async () => { @@ -1069,9 +710,9 @@ describe('API', () => { parentId: null }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('parentId').eql(null); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.parentId, null); })); it('他人ã®ãƒ•ã‚©ãƒ«ãƒ€ã‚’親フォルダã«è¨å®šã§ããªã„', async(async () => { @@ -1089,7 +730,7 @@ describe('API', () => { parentId: parentFolder.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('フォルダãŒå¾ªç’°ã™ã‚‹ã‚ˆã†ãªæ§‹é€ ã«ã§ããªã„', async(async () => { @@ -1110,7 +751,7 @@ describe('API', () => { parentId: parentFolder.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('フォルダãŒå¾ªç’°ã™ã‚‹ã‚ˆã†ãªæ§‹é€ ã«ã§ããªã„(å†å¸°çš„)', async(async () => { @@ -1138,7 +779,7 @@ describe('API', () => { parentId: folderC.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('フォルダãŒå¾ªç’°ã™ã‚‹ã‚ˆã†ãªæ§‹é€ ã«ã§ããªã„(自身)', async(async () => { @@ -1166,7 +807,7 @@ describe('API', () => { parentId: '000000000000000000000000' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('ä¸æ£ãªè¦ªãƒ•ã‚©ãƒ«ãƒ€IDã§æ€’られる', async(async () => { @@ -1180,7 +821,7 @@ describe('API', () => { parentId: 'foo' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('å˜åœ¨ã—ãªã„フォルダを更新ã§ããªã„', async(async () => { @@ -1190,7 +831,7 @@ describe('API', () => { folderId: '000000000000000000000000' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('ä¸æ£ãªãƒ•ã‚©ãƒ«ãƒ€IDã§æ€’られる', async(async () => { @@ -1200,7 +841,7 @@ describe('API', () => { folderId: 'foo' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -1214,9 +855,9 @@ describe('API', () => { text: 'test' }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('text').eql('test'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.text, 'test'); })); it('自分自身ã«ã¯ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã§ããªã„', async(async () => { @@ -1227,7 +868,7 @@ describe('API', () => { text: 'Yo' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('å˜åœ¨ã—ãªã„ユーザーã«ã¯ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã§ããªã„', async(async () => { @@ -1238,7 +879,7 @@ describe('API', () => { text: 'test' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('ä¸æ£ãªãƒ¦ãƒ¼ã‚¶ãƒ¼IDã§æ€’られる', async(async () => { @@ -1249,7 +890,7 @@ describe('API', () => { text: 'test' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('テã‚ストãŒç„¡ãã¦æ€’られる', async(async () => { @@ -1260,7 +901,7 @@ describe('API', () => { userId: bob.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('æ–‡å—数オーãƒãƒ¼ã§æ€’られる', async(async () => { @@ -1272,7 +913,7 @@ describe('API', () => { text: '!'.repeat(1001) }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -1297,9 +938,9 @@ describe('API', () => { noteId: alicePost.id }, carol); - expect(res).have.status(200); - expect(res.body).be.a('array'); - expect(res.body).length(0); + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 0); })); }); @@ -1319,10 +960,10 @@ describe('API', () => { const res = await request('/notes/timeline', {}, bob); - expect(res).have.status(200); - expect(res.body).be.a('array'); - expect(res.body).length(1); - expect(res.body[0].id).equals(alicePost.id); + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 1); + assert.strictEqual(res.body[0].id, alicePost.id); })); }); }); diff --git a/test/chart.ts b/test/chart.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3976b03bae98559c0898d6caff0f136ff12a17b --- /dev/null +++ b/test/chart.ts @@ -0,0 +1,323 @@ +/* + * Tests of chart engine + * + * How to run the tests: + * > mocha test/chart.ts --require ts-node/register + * + * To specify test: + * > mocha test/chart.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as lolex from 'lolex'; +import { async } from './utils'; +import { getConnection, createConnection } from 'typeorm'; +const config = require('../built/config').default; +const Chart = require('../built/services/chart/core').default; +const _TestChart = require('../built/services/chart/charts/schemas/test'); +const _TestGroupedChart = require('../built/services/chart/charts/schemas/test-grouped'); +const _TestUniqueChart = require('../built/services/chart/charts/schemas/test-unique'); + +function initDb() { + try { + const conn = getConnection(); + return Promise.resolve(conn); + } catch (e) {} + + return createConnection({ + type: 'postgres', + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, + synchronize: true, + dropSchema: true, + entities: [ + Chart.schemaToEntity(_TestChart.name, _TestChart.schema), + Chart.schemaToEntity(_TestGroupedChart.name, _TestGroupedChart.schema), + Chart.schemaToEntity(_TestUniqueChart.name, _TestUniqueChart.schema) + ] + }); +} + +describe('Chart', () => { + let testChart: any; + let testGroupedChart: any; + let testUniqueChart: any; + let connection: any; + let clock: lolex.InstalledClock<lolex.Clock>; + + before(done => { + initDb().then(c => { + connection = c; + done(); + }); + }); + + beforeEach(done => { + const TestChart = require('../built/services/chart/charts/classes/test').default; + testChart = new TestChart(); + + const TestGroupedChart = require('../built/services/chart/charts/classes/test-grouped').default; + testGroupedChart = new TestGroupedChart(); + + const TestUniqueChart = require('../built/services/chart/charts/classes/test-unique').default; + testUniqueChart = new TestUniqueChart(); + + clock = lolex.install({ + now: new Date('2000-01-01 00:00:00') + }); + + connection.synchronize().then(done); + }); + + afterEach(done => { + clock.uninstall(); + connection.dropDatabase().then(done); + }); + + it('Can updates', async(async () => { + await testChart.increment(); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0] + }, + }); + })); + + it('Empty chart', async(async () => { + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0] + }, + }); + })); + + it('Can updates at multiple times at same time', async(async () => { + await testChart.increment(); + await testChart.increment(); + await testChart.increment(); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [3, 0, 0], + total: [3, 0, 0] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [3, 0, 0], + total: [3, 0, 0] + }, + }); + })); + + it('Can updates at different times', async(async () => { + await testChart.increment(); + + clock.tick('01:00:00'); + + await testChart.increment(); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 1, 0], + total: [2, 1, 0] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0] + }, + }); + })); + + it('Can padding', async(async () => { + await testChart.increment(); + + clock.tick('02:00:00'); + + await testChart.increment(); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 1], + total: [2, 1, 1] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0] + }, + }); + })); + + // è¦æ±‚ã•ã‚ŒãŸç¯„囲ã«ãƒã‚°ãŒã²ã¨ã¤ã‚‚ãªã„å ´åˆã§ã‚‚パディングã§ãã‚‹ + it('Can padding from past range', async(async () => { + await testChart.increment(); + + clock.tick('05:00:00'); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [1, 1, 1] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0] + }, + }); + })); + + // è¦æ±‚ã•ã‚ŒãŸç¯„囲ã®æœ€ã‚‚å¤ã„箇所ã«ä½ç½®ã™ã‚‹ãƒã‚°ãŒå˜åœ¨ã—ãªã„å ´åˆã§ã‚‚パディングã§ãã‚‹ + // Issue #3190 + it('Can padding from past range 2', async(async () => { + await testChart.increment(); + clock.tick('05:00:00'); + await testChart.increment(); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [2, 1, 1] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0] + }, + }); + })); + + describe('Grouped', () => { + it('Can updates', async(async () => { + await testGroupedChart.increment('alice'); + + const aliceChartHours = await testGroupedChart.getChart('hour', 3, 'alice'); + const aliceChartDays = await testGroupedChart.getChart('day', 3, 'alice'); + const bobChartHours = await testGroupedChart.getChart('hour', 3, 'bob'); + const bobChartDays = await testGroupedChart.getChart('day', 3, 'bob'); + + assert.deepStrictEqual(aliceChartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0] + }, + }); + + assert.deepStrictEqual(aliceChartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0] + }, + }); + + assert.deepStrictEqual(bobChartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0] + }, + }); + + assert.deepStrictEqual(bobChartDays, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0] + }, + }); + })); + }); + + describe('Unique increment', () => { + it('Can updates', async(async () => { + await testUniqueChart.uniqueIncrement('alice'); + await testUniqueChart.uniqueIncrement('alice'); + await testUniqueChart.uniqueIncrement('bob'); + + const chartHours = await testUniqueChart.getChart('hour', 3); + const chartDays = await testUniqueChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: [2, 0, 0], + }); + + assert.deepStrictEqual(chartDays, { + foo: [2, 0, 0], + }); + })); + }); +}); diff --git a/test/mfm.ts b/test/mfm.ts index 191ee5e0ed0d4641b61a28b2d798ad21096e04b9..69260a54155d1004f421bc22191ee60a2500e8c4 100644 --- a/test/mfm.ts +++ b/test/mfm.ts @@ -6,6 +6,10 @@ * * To specify test: * > mocha test/mfm.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 */ import * as assert from 'assert'; diff --git a/test/mocha.opts b/test/mocha.opts index 907011807d680edb272de32bab832a628e2d5064..e114c53bd8adf9df94559c0a223a4c4ed5031caa 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1 +1,2 @@ ---timeout 10000 +--timeout 30000 +--slow 1000 diff --git a/test/mute.ts b/test/mute.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf24b55ee514aa4a8d41299dbe552a67341e9191 --- /dev/null +++ b/test/mute.ts @@ -0,0 +1,170 @@ +/* + * Tests of mute + * + * How to run the tests: + * > mocha test/mute.ts --require ts-node/register + * + * To specify test: + * > mocha test/mute.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, react, connectStream } from './utils'; + +describe('Mute', () => { + let p: childProcess.ChildProcess; + + // alice mutes carol + let alice: any; + let bob: any; + let carol: any; + + before(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', async message => { + if (message === 'ok') { + (p.channel as any).onread = () => {}; + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + done(); + } + }); + }); + + after(() => { + p.kill(); + }); + + it('ミュート作æˆ', async(async () => { + const res = await request('/mute/create', { + userId: carol.id + }, alice); + + assert.strictEqual(res.status, 204); + })); + + it('「自分宛ã¦ã®æŠ•ç¨¿ã€ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async(async () => { + const bobNote = await post(bob, { text: '@alice hi' }); + const carolNote = await post(carol, { text: '@alice hi' }); + + const res = await request('/notes/mentions', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + })); + + it('ミュートã—ã¦ã„るユーザーã‹ã‚‰ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã•ã‚Œã¦ã‚‚ã€hasUnreadMentions ㌠true ã«ãªã‚‰ãªã„', async(async () => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + + await post(carol, { text: '@alice hi' }); + + const res = await request('/i', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.hasUnreadMentions, false); + })); + + it('ミュートã—ã¦ã„るユーザーã‹ã‚‰ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã•ã‚Œã¦ã‚‚ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ ã« unreadMention イベントãŒæµã‚Œã¦ã“ãªã„', () => new Promise(async done => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + + let fired = false; + + const ws = await connectStream(alice, 'main', ({ type }) => { + if (type == 'unreadMention') { + fired = true; + } + }); + + post(carol, { text: '@alice hi' }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 5000); + })); + + it('ミュートã—ã¦ã„るユーザーã‹ã‚‰ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã•ã‚Œã¦ã‚‚ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ ã« unreadNotification イベントãŒæµã‚Œã¦ã“ãªã„', () => new Promise(async done => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + await request('/notifications/mark-all-as-read', {}, alice); + + let fired = false; + + const ws = await connectStream(alice, 'main', ({ type }) => { + if (type == 'unreadNotification') { + fired = true; + } + }); + + post(carol, { text: '@alice hi' }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 5000); + })); + + describe('Timeline', () => { + it('タイムラインã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async(async () => { + const aliceNote = await post(alice); + const bobNote = await post(bob); + const carolNote = await post(carol); + + const res = await request('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + })); + + it('タイムラインã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®æŠ•ç¨¿ã®RenoteãŒå«ã¾ã‚Œãªã„', async(async () => { + const aliceNote = await post(alice); + const carolNote = await post(carol); + const bobNote = await post(bob, { + renoteId: carolNote.id + }); + + const res = await request('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + })); + }); + + describe('Notification', () => { + it('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®é€šçŸ¥ãŒå«ã¾ã‚Œãªã„(リアクション)', async(async () => { + const aliceNote = await post(alice); + await react(bob, aliceNote, 'like'); + await react(carol, aliceNote, 'like'); + + const res = await request('/i/notifications', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some(notification => notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => notification.userId === carol.id), false); + })); + }); +}); diff --git a/test/note.ts b/test/note.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a05930eaeb35f87f1bf6d294d5ff832e7bd9c27 --- /dev/null +++ b/test/note.ts @@ -0,0 +1,361 @@ +/* + * Tests of Note + * + * How to run the tests: + * > mocha test/note.ts --require ts-node/register + * + * To specify test: + * > mocha test/note.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, uploadFile } from './utils'; +import { Note } from '../built/models/entities/note'; +const initDb = require('../built/db/postgre.js').initDb; + +describe('Note', () => { + let p: childProcess.ChildProcess; + let Notes: any; + + let alice: any; + let bob: any; + + before(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', message => { + if (message === 'ok') { + (p.channel as any).onread = () => {}; + initDb(true).then(async connection => { + Notes = connection.getRepository(Note); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + done(); + }); + } + }); + }); + + after(() => { + p.kill(); + }); + + it('投稿ã§ãã‚‹', async(async () => { + const post = { + text: 'test' + }; + + const res = await request('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + })); + + it('ファイルを添付ã§ãã‚‹', async(async () => { + const file = await uploadFile(alice); + + const res = await request('/notes/create', { + fileIds: [file.id] + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]); + })); + + it('他人ã®ãƒ•ã‚¡ã‚¤ãƒ«ã¯ç„¡è¦–', async(async () => { + const file = await uploadFile(bob); + + const res = await request('/notes/create', { + text: 'test', + fileIds: [file.id] + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, []); + })); + + it('å˜åœ¨ã—ãªã„ファイルã¯ç„¡è¦–', async(async () => { + const res = await request('/notes/create', { + text: 'test', + fileIds: ['000000000000000000000000'] + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, []); + })); + + it('ä¸æ£ãªãƒ•ã‚¡ã‚¤ãƒ«IDã§æ€’られる', async(async () => { + const res = await request('/notes/create', { + fileIds: ['kyoppie'] + }, alice); + assert.strictEqual(res.status, 400); + })); + + it('返信ã§ãã‚‹', async(async () => { + const bobPost = await post(bob, { + text: 'foo' + }); + + const alicePost = { + text: 'bar', + replyId: bobPost.id + }; + + const res = await request('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, alicePost.text); + assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); + assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); + })); + + it('renoteã§ãã‚‹', async(async () => { + const bobPost = await post(bob, { + text: 'test' + }); + + const alicePost = { + renoteId: bobPost.id + }; + + const res = await request('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); + assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); + })); + + it('引用renoteã§ãã‚‹', async(async () => { + const bobPost = await post(bob, { + text: 'test' + }); + + const alicePost = { + text: 'test', + renoteId: bobPost.id + }; + + const res = await request('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, alicePost.text); + assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); + assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); + })); + + it('æ–‡å—æ•°ãŽã‚ŠãŽã‚Šã§æ€’られãªã„', async(async () => { + const post = { + text: '!'.repeat(1000) + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 200); + })); + + it('æ–‡å—数オーãƒãƒ¼ã§æ€’られる', async(async () => { + const post = { + text: '!'.repeat(1001) + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + })); + + it('å˜åœ¨ã—ãªã„リプライ先ã§æ€’られる', async(async () => { + const post = { + text: 'test', + replyId: '000000000000000000000000' + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + })); + + it('å˜åœ¨ã—ãªã„renote対象ã§æ€’られる', async(async () => { + const post = { + renoteId: '000000000000000000000000' + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + })); + + it('ä¸æ£ãªãƒªãƒ—ライ先IDã§æ€’られる', async(async () => { + const post = { + text: 'test', + replyId: 'foo' + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + })); + + it('ä¸æ£ãªrenote対象IDã§æ€’られる', async(async () => { + const post = { + renoteId: 'foo' + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + })); + + it('å˜åœ¨ã—ãªã„ユーザーã«ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã§ãã‚‹', async(async () => { + const post = { + text: '@ghost yo' + }; + + const res = await request('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + })); + + it('åŒã˜ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«è¤‡æ•°ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã—ã¦ã‚‚内部的ã«ã¾ã¨ã‚られる', async(async () => { + const post = { + text: '@bob @bob @bob yo' + }; + + const res = await request('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + + const noteDoc = await Notes.findOne(res.body.createdNote.id); + assert.deepStrictEqual(noteDoc.mentions, [bob.id]); + })); + + describe('notes/create', () => { + it('投票を添付ã§ãã‚‹', async(async () => { + const res = await request('/notes/create', { + text: 'test', + poll: { + choices: ['foo', 'bar'] + } + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.poll != null, true); + })); + + it('投票ã®é¸æŠžè‚¢ãŒç„¡ãã¦æ€’られる', async(async () => { + const res = await request('/notes/create', { + poll: {} + }, alice); + assert.strictEqual(res.status, 400); + })); + + it('投票ã®é¸æŠžè‚¢ãŒç„¡ãã¦æ€’られる (空ã®é…列)', async(async () => { + const res = await request('/notes/create', { + poll: { + choices: [] + } + }, alice); + assert.strictEqual(res.status, 400); + })); + + it('投票ã®é¸æŠžè‚¢ãŒ1ã¤ã§æ€’られる', async(async () => { + const res = await request('/notes/create', { + poll: { + choices: ['Strawberry Pasta'] + } + }, alice); + assert.strictEqual(res.status, 400); + })); + + it('投票ã§ãã‚‹', async(async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'] + } + }, alice); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1 + }, alice); + + assert.strictEqual(res.status, 204); + })); + + it('複数投票ã§ããªã„', async(async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'] + } + }, alice); + + await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 0 + }, alice); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 2 + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('許å¯ã•ã‚Œã¦ã„ã‚‹å ´åˆã¯è¤‡æ•°æŠ•ç¥¨ã§ãã‚‹', async(async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + multiple: true + } + }, alice); + + await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 0 + }, alice); + + await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1 + }, alice); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 2 + }, alice); + + assert.strictEqual(res.status, 204); + })); + + it('ç· ã‚切られã¦ã„ã‚‹å ´åˆã¯æŠ•ç¥¨ã§ããªã„', async(async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + expiredAfter: 1 + } + }, alice); + + await new Promise(x => setTimeout(x, 2)); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1 + }, alice); + + assert.strictEqual(res.status, 400); + })); + }); +}); diff --git a/test/reaction-lib.ts b/test/reaction-lib.ts index 2f6c8ea81b278992ac08059f45e2035326bc45d0..3a7ff1ab33fcc88d2fb28fb6ecac93cce28868c6 100644 --- a/test/reaction-lib.ts +++ b/test/reaction-lib.ts @@ -6,6 +6,10 @@ * * To specify test: * > mocha test/reaction-lib.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 */ /* diff --git a/test/streaming.ts b/test/streaming.ts index 500324d520d4e5603b3d308210296821e19e2589..74a5aaa0b4bd9168cc6c45e43f1b4006a977a148 100644 --- a/test/streaming.ts +++ b/test/streaming.ts @@ -6,143 +6,844 @@ * * To specify test: * > mocha test/streaming.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 */ -import * as http from 'http'; -import * as WebSocket from 'ws'; +process.env.NODE_ENV = 'test'; + import * as assert from 'assert'; -import { _signup, _request, _uploadFile, _post, _react, resetDb } from './utils'; +import * as childProcess from 'child_process'; +import { connectStream, signup, request, post } from './utils'; +import { Following } from '../built/models/entities/following'; +const initDb = require('../built/db/postgre.js').initDb; -//#region process -Error.stackTraceLimit = Infinity; +describe('Streaming', () => { + let p: childProcess.ChildProcess; + let Followings: any; -// During the test the env variable is set to test -process.env.NODE_ENV = 'test'; + beforeEach(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', message => { + if (message === 'ok') { + (p.channel as any).onread = () => {}; + initDb(true).then(async connection => { + Followings = connection.getRepository(Following); + done(); + }); + } + }); + }); -// Display detail of unhandled promise rejection -process.on('unhandledRejection', console.dir); -//#endregion + afterEach(() => { + p.kill(); + }); -const app = require('../built/server/api').default; -const server = require('../built/server').startServer(); -const db = require('../built/db/mongodb').default; + const follow = async (follower, followee) => { + await Followings.save({ + id: 'a', + createdAt: new Date(), + followerId: follower.id, + followeeId: followee.id, + followerHost: follower.host, + followerInbox: null, + followerSharedInbox: null, + followeeHost: followee.host, + followeeInbox: null, + followeeSharedInbox: null + }); + }; -const apiServer = http.createServer(app.callback()); + it('mention event', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); -//#region Utilities -const request = _request(apiServer); -const signup = _signup(request); -const post = _post(request); -//#endregion + const ws = await connectStream(bob, 'main', ({ type, body }) => { + if (type == 'mention') { + assert.deepStrictEqual(body.userId, alice.id); + ws.close(); + done(); + } + }); -describe('Streaming', () => { - // Reset database each test - beforeEach(resetDb(db)); + post(alice, { + text: 'foo @bob bar' + }); + })); + + it('renote event', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + const bobNote = await post(bob, { + text: 'foo' + }); + + const ws = await connectStream(bob, 'main', ({ type, body }) => { + if (type == 'renote') { + assert.deepStrictEqual(body.renoteId, bobNote.id); + ws.close(); + done(); + } + }); + + post(alice, { + renoteId: bobNote.id + }); + })); + + describe('Home Timeline', () => { + it('自分ã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const post = { + text: 'foo' + }; + + const me = await signup(); + + const ws = await connectStream(me, 'homeTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.text, post.text); + ws.close(); + done(); + } + }); + + request('/notes/create', post, me); + })); + + it('フォãƒãƒ¼ã—ã¦ã„るユーザーã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // Alice ㌠Bob をフォãƒãƒ¼ + await request('/following/create', { + userId: bob.id + }, alice); + + const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }); + + post(bob, { + text: 'foo' + }); + })); + + it('フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®æŠ•ç¨¿ã¯æµã‚Œãªã„', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + let fired = false; + + const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }); + + post(bob, { + text: 'foo' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + + it('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // Alice ㌠Bob をフォãƒãƒ¼ + await request('/following/create', { + userId: bob.id + }, alice); + + const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + assert.deepStrictEqual(body.text, 'foo'); + ws.close(); + done(); + } + }); + + // Bob ㌠Alice å®›ã¦ã®ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ + post(bob, { + text: 'foo', + visibility: 'specified', + visibleUserIds: [alice.id] + }); + })); + + it('フォãƒãƒ¼ã—ã¦ã„るユーザーã§ã‚‚自分ãŒæŒ‡å®šã•ã‚Œã¦ã„ãªã„ダイレクト投稿ã¯æµã‚Œãªã„', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + const carol = await signup({ username: 'carol' }); + + // Alice ㌠Bob をフォãƒãƒ¼ + await request('/following/create', { + userId: bob.id + }, alice); + + let fired = false; + + const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }); - after(() => { - server.close(); + // Bob ㌠Carol å®›ã¦ã®ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ + post(bob, { + text: 'foo', + visibility: 'specified', + visibleUserIds: [carol.id] + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); }); - it('投稿ãŒã‚¿ã‚¤ãƒ ラインã«æµã‚Œã‚‹', () => new Promise(async done => { - const post = { - text: 'foo' - }; + describe('Local Timeline', () => { + it('自分ã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const me = await signup(); + + const ws = await connectStream(me, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, me.id); + ws.close(); + done(); + } + }); + + post(me, { + text: 'foo' + }); + })); - const me = await signup(); - const ws = new WebSocket(`ws://localhost/streaming?i=${me.token}`); + it('フォãƒãƒ¼ã—ã¦ã„ãªã„ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); - ws.on('open', () => { - ws.on('message', data => { - const msg = JSON.parse(data.toString()); - if (msg.type == 'channel' && msg.body.id == 'a') { - if (msg.body.type == 'note') { - assert.deepStrictEqual(msg.body.body.text, post.text); - ws.close(); - done(); - } - } else if (msg.type == 'connected' && msg.body.id == 'a') { - request('/notes/create', post, me); + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); } }); - ws.send(JSON.stringify({ - type: 'connect', - body: { - channel: 'homeTimeline', - id: 'a', - pong: true + post(bob, { + text: 'foo' + }); + })); + + it('リモートユーザーã®æŠ•ç¨¿ã¯æµã‚Œãªã„', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob', host: 'example.com' }); + + let fired = false; + + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; } - })); - }); - })); + }); - it('mention event', () => new Promise(async done => { - const alice = await signup({ username: 'alice' }); - const bob = await signup({ username: 'bob' }); - const aliceNote = { - text: 'foo @bob bar' - }; + post(bob, { + text: 'foo' + }); - const ws = new WebSocket(`ws://localhost/streaming?i=${bob.token}`); + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); - ws.on('open', () => { - ws.on('message', data => { - const msg = JSON.parse(data.toString()); - if (msg.type == 'channel' && msg.body.id == 'a') { - if (msg.body.type == 'mention') { - assert.deepStrictEqual(msg.body.body.text, aliceNote.text); - ws.close(); - done(); - } - } else if (msg.type == 'connected' && msg.body.id == 'a') { - request('/notes/create', aliceNote, alice); + it('フォãƒãƒ¼ã—ã¦ãŸã¨ã—ã¦ã‚‚リモートユーザーã®æŠ•ç¨¿ã¯æµã‚Œãªã„', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob', host: 'example.com' }); + + // Alice ㌠Bob をフォãƒãƒ¼ + await request('/following/create', { + userId: bob.id + }, alice); + + let fired = false; + + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; } }); - ws.send(JSON.stringify({ - type: 'connect', - body: { - channel: 'main', - id: 'a', - pong: true + post(bob, { + text: 'foo' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + + it('ホーム指定ã®æŠ•ç¨¿ã¯æµã‚Œãªã„', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + let fired = false; + + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; } - })); - }); - })); + }); - it('renote event', () => new Promise(async done => { - const alice = await signup({ username: 'alice' }); - const bob = await signup({ username: 'bob' }); - const bobNote = await post(bob, { - text: 'foo' - }); + // ホーム指定 + post(bob, { + text: 'foo', + visibility: 'home' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + + it('フォãƒãƒ¼ã—ã¦ã„ã‚‹ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); - const ws = new WebSocket(`ws://localhost/streaming?i=${bob.token}`); + // Alice ㌠Bob をフォãƒãƒ¼ + await request('/following/create', { + userId: bob.id + }, alice); - ws.on('open', () => { - ws.on('message', data => { - const msg = JSON.parse(data.toString()); - if (msg.type == 'channel' && msg.body.id == 'a') { - if (msg.body.type == 'renote') { - assert.deepStrictEqual(msg.body.body.renoteId, bobNote.id); - ws.close(); - done(); - } - } else if (msg.type == 'connected' && msg.body.id == 'a') { - request('/notes/create', { - renoteId: bobNote.id - }, alice); + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + assert.deepStrictEqual(body.text, 'foo'); + ws.close(); + done(); } }); - ws.send(JSON.stringify({ - type: 'connect', - body: { - channel: 'main', - id: 'a', - pong: true + // Bob ㌠Alice å®›ã¦ã®ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ + post(bob, { + text: 'foo', + visibility: 'specified', + visibleUserIds: [alice.id] + }); + })); + + it('フォãƒãƒ¼ã—ã¦ã„ãªã„ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼å®›ã¦æŠ•ç¨¿ã¯æµã‚Œãªã„', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + let fired = false; + + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; } - })); - }); - })); + }); + + // フォãƒãƒ¯ãƒ¼å®›ã¦æŠ•ç¨¿ + post(bob, { + text: 'foo', + visibility: 'followers' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + }); + + describe('Social Timeline', () => { + it('自分ã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const me = await signup(); + + const ws = await connectStream(me, 'socialTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, me.id); + ws.close(); + done(); + } + }); + + post(me, { + text: 'foo' + }); + })); + + it('フォãƒãƒ¼ã—ã¦ã„ãªã„ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }); + + post(bob, { + text: 'foo' + }); + })); + + it('フォãƒãƒ¼ã—ã¦ã„るリモートユーザーã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob', host: 'example.com' }); + + // Alice ㌠Bob をフォãƒãƒ¼ + await follow(alice, bob); + + const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }); + + post(bob, { + text: 'foo' + }); + })); + + it('フォãƒãƒ¼ã—ã¦ã„ãªã„リモートユーザーã®æŠ•ç¨¿ã¯æµã‚Œãªã„', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob', host: 'example.com' }); + + let fired = false; + + const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }); + + post(bob, { + text: 'foo' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + + it('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // Alice ㌠Bob をフォãƒãƒ¼ + await request('/following/create', { + userId: bob.id + }, alice); + + const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + assert.deepStrictEqual(body.text, 'foo'); + ws.close(); + done(); + } + }); + + // Bob ㌠Alice å®›ã¦ã®ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ + post(bob, { + text: 'foo', + visibility: 'specified', + visibleUserIds: [alice.id] + }); + })); + + it('フォãƒãƒ¼ã—ã¦ã„ãªã„ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼å®›ã¦æŠ•ç¨¿ã¯æµã‚Œãªã„', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + let fired = false; + + const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }); + + // フォãƒãƒ¯ãƒ¼å®›ã¦æŠ•ç¨¿ + post(bob, { + text: 'foo', + visibility: 'followers' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + }); + + describe('Global Timeline', () => { + it('フォãƒãƒ¼ã—ã¦ã„ãªã„ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }); + + post(bob, { + text: 'foo' + }); + })); + + it('フォãƒãƒ¼ã—ã¦ã„ãªã„リモートユーザーã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob', host: 'example.com' }); + + const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }); + + post(bob, { + text: 'foo' + }); + })); + }); + + describe('UserList Timeline', () => { + it('リストã«å…¥ã‚Œã¦ã„るユーザーã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // ãƒªã‚¹ãƒˆä½œæˆ + const list = await request('/users/lists/create', { + title: 'my list' + }, alice).then(x => x.body); + + // Alice ㌠Bob をリスイン + await request('/users/lists/push', { + listId: list.id, + userId: bob.id + }, alice); + + const ws = await connectStream(alice, 'userList', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }, { + listId: list.id + }); + + post(bob, { + text: 'foo' + }); + })); + + it('リストã«å…¥ã‚Œã¦ã„ãªã„ユーザーã®æŠ•ç¨¿ã¯æµã‚Œãªã„', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // ãƒªã‚¹ãƒˆä½œæˆ + const list = await request('/users/lists/create', { + title: 'my list' + }, alice).then(x => x.body); + + let fired = false; + + const ws = await connectStream(alice, 'userList', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }, { + listId: list.id + }); + + post(bob, { + text: 'foo' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + + // #4471 + it('リストã«å…¥ã‚Œã¦ã„るユーザーã®ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // ãƒªã‚¹ãƒˆä½œæˆ + const list = await request('/users/lists/create', { + title: 'my list' + }, alice).then(x => x.body); + + // Alice ㌠Bob をリスイン + await request('/users/lists/push', { + listId: list.id, + userId: bob.id + }, alice); + + const ws = await connectStream(alice, 'userList', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + assert.deepStrictEqual(body.text, 'foo'); + ws.close(); + done(); + } + }, { + listId: list.id + }); + + // Bob ㌠Alice å®›ã¦ã®ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ + post(bob, { + text: 'foo', + visibility: 'specified', + visibleUserIds: [alice.id] + }); + })); + + // #4335 + it('リストã«å…¥ã‚Œã¦ã„ã‚‹ãŒãƒ•ã‚©ãƒãƒ¼ã¯ã—ã¦ãªã„ユーザーã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼å®›ã¦æŠ•ç¨¿ã¯æµã‚Œãªã„', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // ãƒªã‚¹ãƒˆä½œæˆ + const list = await request('/users/lists/create', { + title: 'my list' + }, alice).then(x => x.body); + + // Alice ㌠Bob をリスイン + await request('/users/lists/push', { + listId: list.id, + userId: bob.id + }, alice); + + let fired = false; + + const ws = await connectStream(alice, 'userList', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }, { + listId: list.id + }); + + // フォãƒãƒ¯ãƒ¼å®›ã¦æŠ•ç¨¿ + post(bob, { + text: 'foo', + visibility: 'followers' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + }); + + describe('Hashtag Timeline', () => { + it('指定ã—ãŸãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®æŠ•ç¨¿ãŒæµã‚Œã‚‹', () => new Promise(async done => { + const me = await signup(); + + const ws = await connectStream(me, 'hashtag', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.text, '#foo'); + ws.close(); + done(); + } + }, { + q: [ + ['foo'] + ] + }); + + post(me, { + text: '#foo' + }); + })); + + it('指定ã—ãŸãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®æŠ•ç¨¿ãŒæµã‚Œã‚‹ (AND)', () => new Promise(async done => { + const me = await signup(); + + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + + const ws = await connectStream(me, 'hashtag', ({ type, body }) => { + if (type == 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + } + }, { + q: [ + ['foo', 'bar'] + ] + }); + + post(me, { + text: '#foo' + }); + + post(me, { + text: '#bar' + }); + + post(me, { + text: '#foo #bar' + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 0); + assert.strictEqual(barCount, 0); + assert.strictEqual(fooBarCount, 1); + ws.close(); + done(); + }, 3000); + })); + + it('指定ã—ãŸãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®æŠ•ç¨¿ãŒæµã‚Œã‚‹ (OR)', () => new Promise(async done => { + const me = await signup(); + + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + let piyoCount = 0; + + const ws = await connectStream(me, 'hashtag', ({ type, body }) => { + if (type == 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + if (body.text === '#piyo') piyoCount++; + } + }, { + q: [ + ['foo'], + ['bar'] + ] + }); + + post(me, { + text: '#foo' + }); + + post(me, { + text: '#bar' + }); + + post(me, { + text: '#foo #bar' + }); + + post(me, { + text: '#piyo' + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 1); + assert.strictEqual(barCount, 1); + assert.strictEqual(fooBarCount, 1); + assert.strictEqual(piyoCount, 0); + ws.close(); + done(); + }, 3000); + })); + + it('指定ã—ãŸãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®æŠ•ç¨¿ãŒæµã‚Œã‚‹ (AND + OR)', () => new Promise(async done => { + const me = await signup(); + + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + let piyoCount = 0; + let waaaCount = 0; + + const ws = await connectStream(me, 'hashtag', ({ type, body }) => { + if (type == 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + if (body.text === '#piyo') piyoCount++; + if (body.text === '#waaa') waaaCount++; + } + }, { + q: [ + ['foo', 'bar'], + ['piyo'] + ] + }); + + post(me, { + text: '#foo' + }); + + post(me, { + text: '#bar' + }); + + post(me, { + text: '#foo #bar' + }); + + post(me, { + text: '#piyo' + }); + + post(me, { + text: '#waaa' + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 0); + assert.strictEqual(barCount, 0); + assert.strictEqual(fooBarCount, 1); + assert.strictEqual(piyoCount, 1); + assert.strictEqual(waaaCount, 0); + ws.close(); + done(); + }, 3000); + })); + }); }); diff --git a/test/user-notes.ts b/test/user-notes.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e457d66921f8a894047a73e874411230343d33c --- /dev/null +++ b/test/user-notes.ts @@ -0,0 +1,86 @@ +/* + * Tests of Note + * + * How to run the tests: + * > mocha test/user-notes.ts --require ts-node/register + * + * To specify test: + * > mocha test/user-notes.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, uploadFile } from './utils'; + +describe('users/notes', () => { + let p: childProcess.ChildProcess; + + let alice: any; + let jpgNote: any; + let pngNote: any; + let jpgPngNote: any; + + before(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', async message => { + if (message === 'ok') { + (p.channel as any).onread = () => {}; + + alice = await signup({ username: 'alice' }); + const jpg = await uploadFile(alice, __dirname + '/resources/Lenna.jpg'); + const png = await uploadFile(alice, __dirname + '/resources/Lenna.png'); + jpgNote = await post(alice, { + fileIds: [jpg.id] + }); + pngNote = await post(alice, { + fileIds: [png.id] + }); + jpgPngNote = await post(alice, { + fileIds: [jpg.id, png.id] + }); + + done(); + } + }); + }); + + after(() => { + p.kill(); + }); + + it('ファイルタイプ指定 (jpg)', async(async () => { + const res = await request('/users/notes', { + userId: alice.id, + fileType: ['image/jpeg'] + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 2); + assert.strictEqual(res.body.some(note => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some(note => note.id === jpgPngNote.id), true); + })); + + it('ファイルタイプ指定 (jpg or png)', async(async () => { + const res = await request('/users/notes', { + userId: alice.id, + fileType: ['image/jpeg', 'image/png'] + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 3); + assert.strictEqual(res.body.some(note => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some(note => note.id === pngNote.id), true); + assert.strictEqual(res.body.some(note => note.id === jpgPngNote.id), true); + })); +}); diff --git a/test/utils.ts b/test/utils.ts index 1377122478d604a0f2f24f856ae8e603f57682a2..fbba9a68c932846212a8542b54b9addfba5e25a8 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; -import * as http from 'http'; -import * as assert from 'chai'; -assert.use(require('chai-http')); +import * as WebSocket from 'ws'; +const fetch = require('node-fetch'); +import * as req from 'request'; export const async = (fn: Function) => (done: Function) => { fn().then(() => { @@ -11,19 +11,31 @@ export const async = (fn: Function) => (done: Function) => { }); }; -export const _request = (server: http.Server) => async (endpoint: string, params: any, me?: any): Promise<ChaiHttp.Response> => { +export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => { const auth = me ? { i: me.token } : {}; - const res = await assert.request(server) - .post(endpoint) - .send(Object.assign(auth, params)); + try { + const res = await fetch('http://localhost:80/api' + endpoint, { + method: 'POST', + body: JSON.stringify(Object.assign(auth, params)) + }); - return res; + const status = res.status; + const body = res.status !== 204 ? await res.json().catch() : null; + + return { + body, status + }; + } catch (e) { + return { + body: null, status: 500 + }; + } }; -export const _signup = (request: ReturnType<typeof _request>) => async (params?: any): Promise<any> => { +export const signup = async (params?: any): Promise<any> => { const q = Object.assign({ username: 'test', password: 'test' @@ -34,50 +46,59 @@ export const _signup = (request: ReturnType<typeof _request>) => async (params?: return res.body; }; -export const _post = (request: ReturnType<typeof _request>) => async (user: any, params?: any): Promise<any> => { +export const post = async (user: any, params?: any): Promise<any> => { const q = Object.assign({ text: 'test' }, params); const res = await request('/notes/create', q, user); - return res.body.createdNote; + return res.body ? res.body.createdNote : null; }; -export const _react = (request: ReturnType<typeof _request>) => async (user: any, note: any, reaction: string): Promise<any> => { +export const react = async (user: any, note: any, reaction: string): Promise<any> => { await request('/notes/reactions/create', { noteId: note.id, reaction: reaction }, user); }; -export const _uploadFile = (server: http.Server) => async (user: any): Promise<any> => { - const res = await assert.request(server) - .post('/drive/files/create') - .field('i', user.token) - .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png'); +export const uploadFile = (user: any, path?: string): Promise<any> => new Promise((ok, rej) => { + req.post({ + url: 'http://localhost:80/api/drive/files/create', + formData: { + i: user.token, + file: fs.createReadStream(path || __dirname + '/resources/Lenna.png') + }, + json: true + }, (err, httpResponse, body) => { + ok(body); + }); +}); - return res.body; -}; +export function connectStream(user: any, channel: string, listener: any, params?: any): Promise<WebSocket> { + return new Promise((res, rej) => { + const ws = new WebSocket(`ws://localhost/streaming?i=${user.token}`); -export const resetDb = (db: any) => () => new Promise(res => { - // APIãŒãªã«ã‹ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã‚’è¿”ã—ãŸå¾Œã«ã€å¾Œå‡¦ç†ã‚’è¡Œã†å ´åˆãŒã‚り〠- // レスãƒãƒ³ã‚¹ã‚’å—ã‘å–ã£ã¦ã™ãデータベースをリセットã™ã‚‹ã¨ - // ãã®å¾Œå‡¦ç†ã¨ç«¶åˆã—(テスト自体ã¯åˆæ ¼ã™ã‚‹ã‚‚ã®ã®)エラーãŒã‚³ãƒ³ã‚½ãƒ¼ãƒ«ã«å‡ºåŠ›ã•ã‚Œ - // 見ãŸç›®çš„ã«æ°—æŒã¡æ‚ªããªã‚‹ã®ã§ã€å¾Œå‡¦ç†ãŒçµ‚ã‚‹ã®ã‚’å¾…ã¤ãŸã‚ã«500msãらã„å¾…ã£ã¦ã‹ã‚‰ - // データベースをリセットã™ã‚‹ã‚ˆã†ã«ã™ã‚‹ - setTimeout(async () => { - await Promise.all([ - db.get('users').drop(), - db.get('notes').drop(), - db.get('driveFiles.files').drop(), - db.get('driveFiles.chunks').drop(), - db.get('driveFolders').drop(), - db.get('apps').drop(), - db.get('accessTokens').drop(), - db.get('authSessions').drop() - ]); - - res(); - }, 500); -}); + ws.on('open', () => { + ws.on('message', data => { + const msg = JSON.parse(data.toString()); + if (msg.type == 'channel' && msg.body.id == 'a') { + listener(msg.body); + } else if (msg.type == 'connected' && msg.body.id == 'a') { + res(ws); + } + }); + + ws.send(JSON.stringify({ + type: 'connect', + body: { + channel: channel, + id: 'a', + pong: true, + params: params + } + })); + }); + }); +} diff --git a/tsconfig.json b/tsconfig.json index 09da750c35ec933ad650d390090369c2ae1bab7c..6bd857120707ece10ef0d65e17238c8fb040e54f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "strict": true, "strictNullChecks": false, "experimentalDecorators": true, + "emitDecoratorMetadata": true, "resolveJsonModule": true, "typeRoots": [ "node_modules/@types",