Merge branch 'develop' into feat-12997

This commit is contained in:
かっこかり 2024-03-05 10:54:34 +09:00 committed by GitHub
commit e8f158fa3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1826 additions and 1491 deletions

View file

@ -0,0 +1,40 @@
name: "Release Manager: sync changelog with PR"
on:
push:
branches:
- release/**
paths:
- 'CHANGELOG.md'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
issues: write
pull-requests: write
jobs:
edit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得
- name: Get PR
run: |
echo "pr_number=$(gh pr list --limit 1 --head "${{ github.ref_name }}" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
- name: Get target version
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1
id: v
# CHANGELOG.mdの内容を取得
- name: Get changelog
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1
with:
version: ${{ steps.v.outputs.target_version }}
id: changelog
# PRのnotesを更新
- name: Update PR
run: |
gh pr edit ${{ steps.get_pr.outputs.pr_number }} --body "${{ steps.changelog.outputs.changelog }}"

View file

@ -0,0 +1,122 @@
name: "Release Manager [Dispatch]"
on:
workflow_dispatch:
inputs:
## Specify the type of the next release.
#version_increment_type:
# type: choice
# description: 'VERSION INCREMENT TYPE'
# default: 'patch'
# required: false
# options:
# - 'major'
# - 'minor'
# - 'patch'
merge:
type: boolean
description: 'MERGE RELEASE BRANCH TO MAIN'
default: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
issues: write
pull-requests: write
jobs:
get-pr:
runs-on: ubuntu-latest
outputs:
pr_number: ${{ steps.get_pr.outputs.pr_number }}
steps:
- uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得
- name: Get PRs
run: |
echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
merge:
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
# Text to prepend to the changelog
# The first line must be `## Unreleased`
changes_template: |
## Unreleased
### General
-
### Client
-
### Server
-
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
create-prerelease:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
create-target:
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number == '' }}
with:
# The script for version increment.
# process.env.CURRENT_VERSION: The current version.
#
# Misskey calender versioning (yyyy.MM.patch) example
version_increment_script: |
const now = new Date();
const year = now.toLocaleDateString('en-US', { year: 'numeric', timeZone: 'Asia/Tokyo' });
const month = now.toLocaleDateString('en-US', { month: 'numeric', timeZone: 'Asia/Tokyo' });
const [major, minor, _patch] = process.env.CURRENT_VERSION.split('.');
const patch = Number(_patch.split('-')[0]);
if (Number.isNaN(patch)) {
console.error('Invalid patch version', year, month, process.env.CURRENT_VERSION, major, minor, _patch);
throw new Error('Invalid patch version');
}
if (year !== major || month !== minor) {
return `${year}.${month}.0`;
} else {
return `${major}.${minor}.${patch + 1}`;
}
##Semver example
#version_increment_script: |
# const [major, minor, patch] = process.env.CURRENT_VERSION.split('.');
# if ("${{ inputs.version_increment_type }}" === "major") {
# return `${Number(major) + 1}.0.0`;
# } else if ("${{ inputs.version_increment_type }}" === "minor") {
# return `${major}.${Number(minor) + 1}.0`;
# } else {
# return `${major}.${minor}.${Number(patch) + 1}`;
# }
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}

View file

@ -0,0 +1,38 @@
name: "Release Manager: release RC when ready for review"
on:
pull_request:
types: [ready_for_review]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
issues: write
pull-requests: write
jobs:
check:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.get_pr.outputs.ref }}
steps:
- uses: actions/checkout@v4
# PR情報を取得
- name: Get PR
run: |
pr_json=$(gh pr view ${{ github.event.pull_request.number }} --json isDraft,headRefName)
echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
id: get_pr
release:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
needs: check
if: startsWith(needs.check.outputs.ref, 'release/')
with:
pr_number: ${{ github.event.pull_request.number }}
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

View file

@ -1,5 +1,4 @@
<!--
## 202x.x.x (unreleased)
## Unreleased
### General
-
@ -10,12 +9,25 @@
### Server
-
-->
## 202x.x.x (unreleased)
## 2024.3.1
### General
-
### Client
- Fix: 絵文字関係の不具合を修正 (#13485)
- 履歴に残っている or ピン留めされた絵文字がコントロールパネルより削除されていた際にリアクションデッキが表示できなくなる
- Unicode絵文字が履歴に残っている or ピン留めされているとリアクションデッキが表示できなくなる
- Fix: カスタム絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 #13487
### Server
-
## 2024.3.0
### General
- Enhance: 投稿者のロールに応じて、一つのノートに含むことのできるメンションとダイレクト投稿の宛先の人数に上限を設定できるように
* デフォルトのメンション上限は20アカウントに設定されます。管理者はベースロールの設定で変更可能です。)
* デフォルトのメンション上限は20アカウントに設定されます。管理者はベースロールの設定で変更可能です。
* 連合の問い合わせに応答しないサーバーのリモートユーザーへのメンションは、上限の人数に含めない実装になっています。
- Enhance: 通知がミュート、凍結を考慮するようになりました
- Enhance: サーバーごとにモデレーションノートを残せるように
@ -33,6 +45,7 @@
- Fix: 設定のバックアップ作成時に名前を入力しなかった場合、ローカライゼーションがおかしくなる問題を修正
- Fix: ページ`/admin/emojis`の絵文字編集ダイアログで「リアクションとして使えるロール」を追加する際に何も選択せずOKを押下すると画面が固まる問題を修正
- Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正
- Fix: ユーザの情報のポップアップが消えなくなることがある問題を修正
### Server
- Enhance: エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました
@ -119,7 +132,6 @@
- Fix: エラー画像URLを設定した後解除するとデフォルトの画像が表示されない問題の修正
- Fix: MkCodeEditorで行がずれていってしまう問題の修正
- Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196
- Fix: ユーザの情報のポップアップが消えなくなることがある問題を修正
### Server
- Enhance: 連合先のレートリミットを超過した際にリトライするようになりました

View file

@ -1,9 +1,11 @@
<div align="center">
<a href="https://misskey-hub.net">
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="300"/>
</a>
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
**🌎 **Misskey** is an open source, federated social media platform that's free forever! 🚀**
[Learn more](https://misskey-hub.net/)
---
@ -22,41 +24,6 @@
<a href="https://www.patreon.com/syuilo">
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
---
[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)
</div>
<div>
<a href="https://xn--931a.moe/"><img src="https://github.com/misskey-dev/misskey/blob/develop/assets/ai.png?raw=true" align="right" height="320px"/></a>
## ✨ Features
- **ActivityPub support**\
Not on Misskey? No problem! Not only can Misskey instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed!
- **Reactions**\
You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button.
- **Drive**\
With Misskey's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made!
- **Rich Web UI**\
Misskey has a rich and easy to use Web UI!
It is highly customizable, from changing the layout and adding widgets to making custom themes.
Furthermore, plugins can be created using AiScript, an original programming language.
- And much more...
</div>
<div style="clear: both;"></div>
## Documentation
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/docs/), some of the links and graphics above also lead to specific portions of it.
## Sponsors
<div align="center">
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
</div>
## Thanks

View file

@ -380,8 +380,11 @@ hcaptcha: "hCaptcha"
enableHcaptcha: "Activer hCaptcha"
hcaptchaSiteKey: "Clé du site"
hcaptchaSecretKey: "Clé secrète"
mcaptcha: "mCaptcha"
enableMcaptcha: "Activer mCaptcha"
mcaptchaSiteKey: "Clé du site"
mcaptchaSecretKey: "Clé secrète"
mcaptchaInstanceUrl: "URL de l'instance de mCaptcha"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Activer reCAPTCHA"
recaptchaSiteKey: "Clé du site"
@ -523,7 +526,7 @@ hideThisNote: "Masquer cette note"
showFeaturedNotesInTimeline: "Afficher les notes des Tendances dans le fil d'actualité"
objectStorage: "Stockage d'objets"
useObjectStorage: "Utiliser le stockage d'objets"
objectStorageBaseUrl: "Base URL"
objectStorageBaseUrl: "URL de base"
objectStorageBaseUrlDesc: "Préfixe dURL utilisé pour construire lURL vers le référencement dobjet (média). Spécifiez son URL si vous utilisez un CDN ou un proxy, sinon spécifiez ladresse accessible au public selon le guide de service que vous allez utiliser. P.ex. 'https://<bucket>.s3.amazonaws.com' pour AWS S3 et 'https://storage.googleapis.com/<bucket>' pour GCS."
objectStorageBucket: "Bucket"
objectStorageBucketDesc: "Veuillez spécifier le nom du compartiment utilisé sur le service configuré."
@ -628,6 +631,7 @@ medium: "Moyen"
small: "Petit"
generateAccessToken: "Générer un jeton d'accès"
permission: "Autorisations "
adminPermission: "Droits de l'administrateur"
enableAll: "Tout activer"
disableAll: "Tout désactiver"
tokenRequested: "Autoriser l'accès au compte"
@ -1031,12 +1035,18 @@ nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non sensibles seulement (mentions j'
rolesAssignedToMe: "Rôles attribués à moi"
resetPasswordConfirm: "Souhaitez-vous réinitialiser votre mot de passe ?"
sensitiveWords: "Mots sensibles"
sensitiveWordsDescription2: "Séparer par une espace pour créer une expression AND ; entourer de barres obliques pour créer une expression régulière."
prohibitedWords: "Mots interdits"
prohibitedWordsDescription2: "Séparer par une espace pour créer une expression AND ; entourer de barres obliques pour créer une expression régulière."
hiddenTags: "Hashtags cachés"
hiddenTagsDescription: "Les hashtags définis ne s'afficheront pas dans les tendances. Vous pouvez définir plusieurs hashtags en faisant un saut de ligne."
notesSearchNotAvailable: "La recherche de notes n'est pas disponible."
license: "Licence"
unfavoriteConfirm: "Vraiment supprimer des favoris ?"
myClips: "Mes clips"
drivecleaner: "Nettoyeur du Disque"
retryAllQueuesNow: "Réessayer tous les fils d'attente immédiatement"
retryAllQueuesConfirmTitle: "Vraiment réessayer ?"
retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur."
enableChartsForRemoteUser: "Générer les graphiques pour les utilisateurs distants"
enableChartsForFederatedInstances: "Générer les graphiques pour les instances distantes"
@ -1046,6 +1056,8 @@ limitWidthOfReaction: "Limiter la largeur maximale des réactions et les affiche
noteIdOrUrl: "Identifiant de la note ou URL"
video: "Vidéo"
videos: "Vidéos"
audio: "Audio"
audioFiles: "Fichiers audio"
dataSaver: "Économiseur de données"
accountMigration: "Migration de compte"
accountMoved: "Cet·te utilisateur·rice a migré son compte vers :"
@ -1084,7 +1096,10 @@ specifyUser: "Spécifier l'utilisateur·rice"
failedToPreviewUrl: "Aperçu d'URL échoué"
update: "Mettre à jour"
rolesThatCanBeUsedThisEmojiAsReaction: "Rôles qui peuvent utiliser cet émoji comme réaction"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Si aucun rôle n'est spécifié, tout le monde peut utiliser cet émoji comme réaction."
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Il faut un rôle public."
cancelReactionConfirm: "Supprimez la réaction ?"
changeReactionConfirm: "Changer la réaction ?"
later: "Plus tard"
goToMisskey: "Retour vers Misskey"
additionalEmojiDictionary: "Dictionnaires d'émojis additionnels"
@ -1110,11 +1125,13 @@ used: "Utilisé"
expired: "Expiré"
doYouAgree: "Êtes-vous daccord ?"
beSureToReadThisAsItIsImportant: "Assurez-vous de le lire; c'est important."
iHaveReadXCarefullyAndAgree: "J'ai lu le contenu de « {x} » et donne mon accord."
dialog: "Dialogue"
icon: "Avatar"
forYou: "Pour vous"
currentAnnouncements: "Annonces actuelles"
pastAnnouncements: "Annonces passées"
youHaveUnreadAnnouncements: "Il y a des annonces non lues."
replies: "Réponses"
renotes: "Renotes"
loadReplies: "Inclure les réponses"
@ -1129,6 +1146,7 @@ showRenotes: "Afficher les renotes"
edited: "Modifié"
notificationRecieveConfig: "Paramètres des notifications"
mutualFollow: "Abonnement mutuel"
fileAttachedOnly: "Avec fichiers joints seulement"
showRepliesToOthersInTimeline: "Afficher les réponses aux autres dans le fil"
hideRepliesToOthersInTimeline: "Masquer les réponses aux autres dans le fil"
showRepliesToOthersInTimelineAll: "Afficher les réponses de toutes les personnes que vous suivez dans le fil"
@ -1137,6 +1155,11 @@ confirmShowRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment
confirmHideRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment masquer les réponses de toutes les personnes que vous suivez dans le fil ?"
externalServices: "Services externes"
sourceCode: "Code source"
sourceCodeIsNotYetProvided: "Le code source n'est pas encore disponible. Veuillez signaler ce problème aux administrateurs."
repositoryUrl: "URL du dépôt"
repositoryUrlDescription: "Entrez l'URL du dépôt où se trouve le code source ici. Si vous utilisez Misskey tel quel (sans changer le code source), entrez https://github.com/misskey-dev/misskey"
feedback: "Commentaires"
feedbackUrl: "URL pour les commentaires"
impressum: "Impressum"
impressumUrl: "URL de l'impressum"
impressumDescription: "Dans certains pays comme l'Allemagne, il est obligatoire d'afficher les informations sur l'opérateur d'un site (un impressum)."
@ -1164,11 +1187,32 @@ remainingN: "Restants : {n}"
overwriteContentConfirm: "Voulez-vous remplacer le contenu actuel ?"
seasonalScreenEffect: "Effet d'écran saisonnier"
decorate: "Décorer"
addMfmFunction: "Insérer MFM"
enableQuickAddMfmFunction: "Afficher le sélecteur de MFM avancé"
bubbleGame: "Jeu de bulles"
sfx: "Effets sonores"
soundWillBePlayed: "Le son sera joué"
showReplay: "Voir le replay"
replay: "Rediffusion"
replaying: "En cours de rediffusion"
endReplay: "Arrêter la rediffusion"
copyReplayData: "Copier les données de la rediffusion"
ranking: "Classement"
lastNDays: "Derniers {n} jours"
backToTitle: "Retourner au titre"
hemisphere: "Votre région"
enableHorizontalSwipe: "Glisser pour changer d'onglet"
loading: "Chargement en cours"
surrender: "Annuler"
gameRetry: "Réessayer"
_bubbleGame:
howToPlay: "Comment jouer"
hold: "Réserver"
_score:
score: "Score"
scoreYen: "Montant gagné"
highScore: "Meilleur score"
yen: "{yen} yens"
_announcement:
forExistingUsers: "Pour les utilisateurs existants seulement"
readConfirmTitle: "Marquer comme lu ?"

View file

@ -1655,6 +1655,7 @@ _role:
gtlAvailable: "瀏覽全域時間軸"
ltlAvailable: "瀏覽本地時間軸"
canPublicNote: "允許公開貼文"
mentionMax: "貼文內的最大提及數"
canInvite: "發行伺服器邀請碼"
inviteLimit: "可建立邀請碼的數量"
inviteLimitCycle: "邀請碼的發放間隔"
@ -2299,6 +2300,7 @@ _notification:
reactedBySomeUsers: "{n}人做出了反應"
renotedBySomeUsers: "{n}人做了轉發"
followedBySomeUsers: "被{n}人追隨了"
flushNotification: "重置通知歷史紀錄"
_types:
all: "全部 "
note: "使用者的最新貼文"

View file

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.2.0",
"version": "2024.3.1",
"codename": "nasubi",
"repository": {
"type": "git",

View file

@ -81,9 +81,9 @@
"@fastify/view": "8.2.0",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.0.3",
"@nestjs/common": "10.2.10",
"@nestjs/core": "10.2.10",
"@nestjs/testing": "10.2.10",
"@nestjs/common": "10.3.3",
"@nestjs/core": "10.3.3",
"@nestjs/testing": "10.3.3",
"@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "9.0.3",
"@sinonjs/fake-timers": "11.2.2",
@ -159,7 +159,7 @@
"ratelimiter": "3.4.1",
"re2": "1.20.9",
"redis-lock": "0.1.4",
"reflect-metadata": "0.1.14",
"reflect-metadata": "0.2.1",
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.1",

View file

@ -51,21 +51,35 @@ export class FetchInstanceMetadataService {
}
@bindThis
public async tryLock(host: string): Promise<boolean> {
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
return mutex !== '1';
// public for test
public async tryLock(host: string): Promise<string | null> {
// TODO: マイグレーションなのであとで消す (2024.3.1)
this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`);
return await this.redisClient.set(
`fetchInstanceMetadata:mutex:v2:${host}`, '1',
'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
'GET' // 古い値を返すなかったらnull
);
}
@bindThis
public unlock(host: string): Promise<'OK'> {
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
// public for test
public unlock(host: string): Promise<number> {
return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`);
}
@bindThis
public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
const host = instance.host;
// Acquire mutex to ensure no parallel runs
if (!await this.tryLock(host)) return;
// finallyでunlockされてしまうのでtry内でロックチェックをしない
// returnであってもfinallyは実行される
if (!force && await this.tryLock(host) === '1') {
// 1が返ってきていたらロックされているという意味なので、何もしない
return;
}
try {
if (!force) {
const _instance = await this.federatedInstanceService.fetch(host);

View file

@ -263,7 +263,13 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) {
const hasProhibitedWords = await this.checkProhibitedWordsContain({
cw: data.cw,
text: data.text,
pollChoices: data.poll?.choices,
}, meta.prohibitedWords);
if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
}
@ -995,6 +1001,23 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
if (prohibitedWords == null) {
prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
}
if (
this.utilityService.isKeyWordIncluded(
this.utilityService.concatNoteContentsForKeyWordCheck(content),
prohibitedWords,
)
) {
return true;
}
return false;
}
@bindThis
public dispose(): void {
this.#shutdownController.abort();

View file

@ -42,6 +42,20 @@ export class UtilityService {
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis
public concatNoteContentsForKeyWordCheck(content: {
cw?: string | null;
text?: string | null;
pollChoices?: string[] | null;
others?: string[] | null;
}): string {
/**
*
* cwとtextは内容が繋がっているかもしれないので間に何も入れずにチェックする
*/
return `${content.cw ?? ''}${content.text ?? ''}\n${(content.pollChoices ?? []).join('\n')}\n${(content.others ?? []).join('\n')}`;
}
@bindThis
public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
if (keyWords.length === 0) return false;

View file

@ -36,7 +36,6 @@ import { ApResolverService } from './ApResolverService.js';
import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js';
import { CacheService } from '@/core/CacheService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Resolver } from './ApResolverService.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';

View file

@ -24,6 +24,8 @@ import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
@ -37,7 +39,6 @@ import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js';
import { isNotNull } from '@/misc/is-not-null.js';
@Injectable()
export class ApNoteService {
@ -152,11 +153,47 @@ export class ApNoteService {
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
}
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser;
const uri = getOneApId(note.attributedTo);
// 投稿者が凍結されていたらスキップ
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
if (cachedActor && cachedActor.isSuspended) {
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
}
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
const apHashtags = extractApHashtags(note.tag);
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
text = note.source.content;
} else if (typeof note._misskey_content !== 'undefined') {
text = note._misskey_content;
} else if (typeof note.content === 'string') {
text = this.apMfmService.htmlToMfm(note.content, note.tag);
}
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
//#region Contents Check
// 添付ファイルとユーザーをこのサーバーで登録する前に内容をチェックする
/**
*
*/
const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
}
//#endregion
const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
// 解決した投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new Error('actor has been suspended');
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
}
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
@ -171,9 +208,6 @@ export class ApNoteService {
}
}
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
const apHashtags = extractApHashtags(note.tag);
// 添付ファイル
// TODO: attachmentは必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない
@ -233,18 +267,6 @@ export class ApNoteService {
}
}
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
text = note.source.content;
} else if (typeof note._misskey_content !== 'undefined') {
text = note._misskey_content;
} else if (typeof note.content === 'string') {
text = this.apMfmService.htmlToMfm(note.content, note.tag);
}
// vote
if (reply && reply.hasPoll) {
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
@ -274,8 +296,6 @@ export class ApNoteService {
const apEmojis = emojis.map(emoji => emoji.name);
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
try {
return await this.noteCreateService.create(actor, {
createdAt: note.published ? new Date(note.published) : null,

View file

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs/promises';
import type { PathLike } from 'node:fs';

View file

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { TransformStream } from 'node:stream/web';
/**

View file

@ -185,7 +185,10 @@ export class InboxProcessorService {
await this.apInboxService.performActivity(authUser.user, activity);
} catch (e) {
if (e instanceof IdentifiableError) {
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') return 'blocked notes with prohibited words';
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
return 'blocked notes with prohibited words';
}
if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') return 'actor has been suspended';
}
throw e;
}

View file

@ -18,7 +18,6 @@
* achievementEarned -
* app -
* test -
*
*/
export const notificationTypes = [
'note',

View file

@ -187,7 +187,7 @@ describe('2要素認証', () => {
}, 1000 * 60 * 2);
test('が設定でき、OTPでログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
@ -197,18 +197,18 @@ describe('2要素認証', () => {
assert.strictEqual(registerResponse.body.label, username);
assert.strictEqual(registerResponse.body.issuer, config.host);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('/users/show', {
const usersShowResponse = await api('users/show', {
username,
}, alice);
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
const signinResponse = await api('/signin', {
const signinResponse = await api('signin', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
@ -216,24 +216,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
const registerKeyResponse = await api('i/2fa/register-key', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
@ -243,23 +243,23 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
}) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName);
const usersShowResponse = await api('/users/show', {
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, true);
const signinResponse = await api('/signin', {
const signinResponse = await api('signin', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
@ -268,7 +268,7 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.allowCredentials, undefined);
assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
keyName,
credentialId,
requestOptions: signinResponse.body,
@ -277,24 +277,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
@ -302,33 +302,33 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
}) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
const passwordLessResponse = await api('/i/2fa/password-less', {
const passwordLessResponse = await api('i/2fa/password-less', {
value: true,
}, alice);
assert.strictEqual(passwordLessResponse.status, 204);
const usersShowResponse = await api('/users/show', {
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true);
const signinResponse = await api('/signin', {
const signinResponse = await api('signin', {
...signinParam(),
password: '',
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined);
const signinResponse2 = await api('/signin', {
const signinResponse2 = await api('signin', {
...signinWithSecurityKeyParam({
keyName,
credentialId,
@ -340,24 +340,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
@ -365,22 +365,22 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
}) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
const renamedKey = 'other-key';
const updateKeyResponse = await api('/i/2fa/update-key', {
const updateKeyResponse = await api('i/2fa/update-key', {
name: renamedKey,
credentialId: credentialId.toString('base64url'),
}, alice);
assert.strictEqual(updateKeyResponse.status, 200);
const iResponse = await api('/i', {
const iResponse = await api('i', {
}, alice);
assert.strictEqual(iResponse.status, 200);
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
@ -389,24 +389,24 @@ describe('2要素認証', () => {
assert.notEqual(securityKeys[0].lastUsed, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーを削除できる。', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
@ -414,20 +414,20 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
}) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
// テストの実行順によっては複数残ってるので全部消す
const iResponse = await api('/i', {
const iResponse = await api('i', {
}, alice);
assert.strictEqual(iResponse.status, 200);
for (const key of iResponse.body.securityKeysList) {
const removeKeyResponse = await api('/i/2fa/remove-key', {
const removeKeyResponse = await api('i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret),
password,
credentialId: key.id,
@ -435,13 +435,13 @@ describe('2要素認証', () => {
assert.strictEqual(removeKeyResponse.status, 200);
}
const usersShowResponse = await api('/users/show', {
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, false);
const signinResponse = await api('/signin', {
const signinResponse = await api('signin', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
@ -449,43 +449,43 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('/users/show', {
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
const unregisterResponse = await api('/i/2fa/unregister', {
const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(unregisterResponse.status, 204);
const signinResponse = await api('/signin', {
const signinResponse = await api('signin', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);

View file

@ -7,7 +7,6 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import type { Packed } from '@/misc/json-schema.js';
import {
api,
failedApiCall,
@ -29,10 +28,7 @@ describe('アンテナ', () => {
// エンティティとしてのアンテナを主眼においたテストを記述する
// (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからートを取得するエンドポイントをテストする)
// BUG misskey-jsとjson-schemaが一致していない。
// - srcのenumにgroupが残っている
// - userGroupIdが残っている, isActiveがない
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
type Antenna = misskey.entities.Antenna;
type User = misskey.entities.SignupResponse;
type Note = misskey.entities.Note;
@ -80,7 +76,7 @@ describe('アンテナ', () => {
aliceList = await userList(alice, {});
bob = await signup({ username: 'bob' });
aliceList = await userList(alice, {});
bobFile = (await uploadFile(bob)).body;
bobFile = (await uploadFile(bob)).body!;
bobList = await userList(bob);
carol = await signup({ username: 'carol' });
await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice);
@ -129,9 +125,9 @@ describe('アンテナ', () => {
beforeEach(async () => {
// テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) {
const list = await api('/antennas/list', {}, user);
const list = await api('antennas/list', {}, user);
for (const antenna of list.body) {
await api('/antennas/delete', { antennaId: antenna.id }, user);
await api('antennas/delete', { antennaId: antenna.id }, user);
}
}
});
@ -141,11 +137,11 @@ describe('アンテナ', () => {
test('が作成できること、キーが過不足なく入っていること。', async () => {
const response = await successfulApiCall({
endpoint: 'antennas/create',
parameters: { ...defaultParam },
parameters: defaultParam,
user: alice,
});
assert.match(response.id, /[0-9a-z]{10}/);
const expected = {
const expected: Antenna = {
id: response.id,
caseSensitive: false,
createdAt: new Date(response.createdAt).toISOString(),
@ -161,7 +157,7 @@ describe('アンテナ', () => {
withFile: false,
withReplies: false,
localOnly: false,
} as Antenna;
};
assert.deepStrictEqual(response, expected);
});
@ -202,27 +198,27 @@ describe('アンテナ', () => {
});
const antennaParamPattern = [
{ parameters: (): object => ({ name: 'x'.repeat(100) }) },
{ parameters: (): object => ({ name: 'x' }) },
{ parameters: (): object => ({ src: 'home' }) },
{ parameters: (): object => ({ src: 'all' }) },
{ parameters: (): object => ({ src: 'users' }) },
{ parameters: (): object => ({ src: 'list' }) },
{ parameters: (): object => ({ userListId: null }) },
{ parameters: (): object => ({ src: 'list', userListId: aliceList.id }) },
{ parameters: (): object => ({ keywords: [['x']] }) },
{ parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: (): object => ({ users: [alice.username] }) },
{ parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) },
{ parameters: (): object => ({ caseSensitive: false }) },
{ parameters: (): object => ({ caseSensitive: true }) },
{ parameters: (): object => ({ withReplies: false }) },
{ parameters: (): object => ({ withReplies: true }) },
{ parameters: (): object => ({ withFile: false }) },
{ parameters: (): object => ({ withFile: true }) },
{ parameters: (): object => ({ notify: false }) },
{ parameters: (): object => ({ notify: true }) },
{ parameters: () => ({ name: 'x'.repeat(100) }) },
{ parameters: () => ({ name: 'x' }) },
{ parameters: () => ({ src: 'home' as const }) },
{ parameters: () => ({ src: 'all' as const }) },
{ parameters: () => ({ src: 'users' as const }) },
{ parameters: () => ({ src: 'list' as const }) },
{ parameters: () => ({ userListId: null }) },
{ parameters: () => ({ src: 'list' as const, userListId: aliceList.id }) },
{ parameters: () => ({ keywords: [['x']] }) },
{ parameters: () => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: () => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: () => ({ users: [alice.username] }) },
{ parameters: () => ({ users: [alice.username, bob.username, carol.username] }) },
{ parameters: () => ({ caseSensitive: false }) },
{ parameters: () => ({ caseSensitive: true }) },
{ parameters: () => ({ withReplies: false }) },
{ parameters: () => ({ withReplies: true }) },
{ parameters: () => ({ withFile: false }) },
{ parameters: () => ({ withFile: true }) },
{ parameters: () => ({ notify: false }) },
{ parameters: () => ({ notify: true }) },
];
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
const response = await successfulApiCall({
@ -335,7 +331,7 @@ describe('アンテナ', () => {
test.each([
{
label: '全体から',
parameters: (): object => ({ src: 'all' }),
parameters: () => ({ src: 'all' }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
@ -346,7 +342,7 @@ describe('アンテナ', () => {
{
// BUG e4144a1 以降home指定は壊れている(allと同じ)
label: 'ホーム指定はallと同じ',
parameters: (): object => ({ src: 'home' }),
parameters: () => ({ src: 'home' }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
@ -357,7 +353,7 @@ describe('アンテナ', () => {
{
// https://github.com/misskey-dev/misskey/issues/9025
label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true },
@ -367,56 +363,56 @@ describe('アンテナ', () => {
},
{
label: 'ブロックしているユーザーのノートは含む',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true },
],
},
{
label: 'ブロックされているユーザーのノートは含まない',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) },
],
},
{
label: 'ミュートしているユーザーのノートは含まない',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) },
],
},
{
label: 'ミュートされているユーザーのノートは含む',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true },
],
},
{
label: '「見つけやすくする」がOFFのユーザーのートも含まれる',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true },
],
},
{
label: '鍵付きユーザーのノートも含まれる',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true },
],
},
{
label: 'サイレンスのノートも含まれる',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true },
],
},
{
label: '削除ユーザーのノートも含まれる',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true },
@ -424,7 +420,7 @@ describe('アンテナ', () => {
},
{
label: 'ユーザー指定で',
parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
parameters: () => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -433,7 +429,7 @@ describe('アンテナ', () => {
},
{
label: 'リスト指定で',
parameters: (): object => ({ src: 'list', userListId: aliceList.id }),
parameters: () => ({ src: 'list', userListId: aliceList.id }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -442,14 +438,14 @@ describe('アンテナ', () => {
},
{
label: 'CWにもマッチする',
parameters: (): object => ({ keywords: [[keyword]] }),
parameters: () => ({ keywords: [[keyword]] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true },
],
},
{
label: 'キーワード1つ',
parameters: (): object => ({ keywords: [[keyword]] }),
parameters: () => ({ keywords: [[keyword]] }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: 'test' }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -458,7 +454,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード3つ(AND)',
parameters: (): object => ({ keywords: [['A', 'B', 'C']] }),
parameters: () => ({ keywords: [['A', 'B', 'C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test A' }) },
{ note: (): Promise<Note> => post(bob, { text: 'test A B' }) },
@ -469,7 +465,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード3つ(OR)',
parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }),
parameters: () => ({ keywords: [['A'], ['B'], ['C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test' }) },
{ note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true },
@ -482,7 +478,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード3つ(AND)',
parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }),
parameters: () => ({ excludeKeywords: [['A', 'B', 'C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true },
@ -495,7 +491,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード3つ(OR)',
parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
parameters: () => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) },
@ -508,7 +504,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード1つ(大文字小文字区別する)',
parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }),
parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }) },
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) },
@ -517,7 +513,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード1つ(大文字小文字区別しない)',
parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }),
parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true },
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true },
@ -526,7 +522,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード1つ(大文字小文字区別する)',
parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true },
@ -536,7 +532,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード1つ(大文字小文字区別しない)',
parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) },
@ -546,7 +542,7 @@ describe('アンテナ', () => {
},
{
label: '添付ファイルを問わない',
parameters: (): object => ({ withFile: false }),
parameters: () => ({ withFile: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -554,7 +550,7 @@ describe('アンテナ', () => {
},
{
label: '添付ファイル付きのみ',
parameters: (): object => ({ withFile: true }),
parameters: () => ({ withFile: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }) },
@ -562,7 +558,7 @@ describe('アンテナ', () => {
},
{
label: 'リプライ以外',
parameters: (): object => ({ withReplies: false }),
parameters: () => ({ withReplies: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -570,7 +566,7 @@ describe('アンテナ', () => {
},
{
label: 'リプライも含む',
parameters: (): object => ({ withReplies: true }),
parameters: () => ({ withReplies: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -633,7 +629,7 @@ describe('アンテナ', () => {
endpoint: 'antennas/notes',
parameters: { antennaId: antenna.id, ...paginationParam },
user: alice,
}) as any as Note[];
});
}, offsetBy, 'desc');
});

View file

@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { api, post, signup } from '../utils.js';
import { UserToken, api, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('API visibility', () => {
@ -24,38 +24,38 @@ describe('API visibility', () => {
let target2: misskey.entities.SignupResponse;
/** public-post */
let pub: any;
let pub: misskey.entities.Note;
/** home-post */
let home: any;
let home: misskey.entities.Note;
/** followers-post */
let fol: any;
let fol: misskey.entities.Note;
/** specified-post */
let spe: any;
let spe: misskey.entities.Note;
/** public-reply to target's post */
let pubR: any;
let pubR: misskey.entities.Note;
/** home-reply to target's post */
let homeR: any;
let homeR: misskey.entities.Note;
/** followers-reply to target's post */
let folR: any;
let folR: misskey.entities.Note;
/** specified-reply to target's post */
let speR: any;
let speR: misskey.entities.Note;
/** public-mention to target */
let pubM: any;
let pubM: misskey.entities.Note;
/** home-mention to target */
let homeM: any;
let homeM: misskey.entities.Note;
/** followers-mention to target */
let folM: any;
let folM: misskey.entities.Note;
/** specified-mention to target */
let speM: any;
let speM: misskey.entities.Note;
/** reply target post */
let tgt: any;
let tgt: misskey.entities.Note;
//#endregion
const show = async (noteId: any, by: any) => {
return await api('/notes/show', {
const show = async (noteId: misskey.entities.Note['id'], by?: UserToken) => {
return await api('notes/show', {
noteId,
}, by);
};
@ -70,7 +70,7 @@ describe('API visibility', () => {
target2 = await signup({ username: 'target2' });
// follow alice <= follower
await api('/following/create', { userId: alice.id }, follower);
await api('following/create', { userId: alice.id }, follower);
// normal posts
pub = await post(alice, { text: 'x', visibility: 'public' });
@ -111,7 +111,7 @@ describe('API visibility', () => {
});
test('[show] public-postを未認証が見れる', async () => {
const res = await show(pub.id, null);
const res = await show(pub.id);
assert.strictEqual(res.body.text, 'x');
});
@ -132,7 +132,7 @@ describe('API visibility', () => {
});
test('[show] home-postを未認証が見れる', async () => {
const res = await show(home.id, null);
const res = await show(home.id);
assert.strictEqual(res.body.text, 'x');
});
@ -153,7 +153,7 @@ describe('API visibility', () => {
});
test('[show] followers-postを未認証が見れない', async () => {
const res = await show(fol.id, null);
const res = await show(fol.id);
assert.strictEqual(res.body.isHidden, true);
});
@ -179,7 +179,7 @@ describe('API visibility', () => {
});
test('[show] specified-postを未認証が見れない', async () => {
const res = await show(spe.id, null);
const res = await show(spe.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
@ -207,7 +207,7 @@ describe('API visibility', () => {
});
test('[show] public-replyを未認証が見れる', async () => {
const res = await show(pubR.id, null);
const res = await show(pubR.id);
assert.strictEqual(res.body.text, 'x');
});
@ -233,7 +233,7 @@ describe('API visibility', () => {
});
test('[show] home-replyを未認証が見れる', async () => {
const res = await show(homeR.id, null);
const res = await show(homeR.id);
assert.strictEqual(res.body.text, 'x');
});
@ -259,7 +259,7 @@ describe('API visibility', () => {
});
test('[show] followers-replyを未認証が見れない', async () => {
const res = await show(folR.id, null);
const res = await show(folR.id);
assert.strictEqual(res.body.isHidden, true);
});
@ -290,7 +290,7 @@ describe('API visibility', () => {
});
test('[show] specified-replyを未認証が見れない', async () => {
const res = await show(speR.id, null);
const res = await show(speR.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
@ -318,7 +318,7 @@ describe('API visibility', () => {
});
test('[show] public-mentionを未認証が見れる', async () => {
const res = await show(pubM.id, null);
const res = await show(pubM.id);
assert.strictEqual(res.body.text, '@target x');
});
@ -344,7 +344,7 @@ describe('API visibility', () => {
});
test('[show] home-mentionを未認証が見れる', async () => {
const res = await show(homeM.id, null);
const res = await show(homeM.id);
assert.strictEqual(res.body.text, '@target x');
});
@ -370,7 +370,7 @@ describe('API visibility', () => {
});
test('[show] followers-mentionを未認証が見れない', async () => {
const res = await show(folM.id, null);
const res = await show(folM.id);
assert.strictEqual(res.body.isHidden, true);
});
@ -401,28 +401,28 @@ describe('API visibility', () => {
});
test('[show] specified-mentionを未認証が見れない', async () => {
const res = await show(speM.id, null);
const res = await show(speM.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
//#region HTL
test('[HTL] public-post が 自分が見れる', async () => {
const res = await api('/notes/timeline', { limit: 100 }, alice);
const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes[0].text, 'x');
});
test('[HTL] public-post が 非フォロワーから見れない', async () => {
const res = await api('/notes/timeline', { limit: 100 }, other);
const res = await api('notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes.length, 0);
});
test('[HTL] followers-post が フォロワーから見れる', async () => {
const res = await api('/notes/timeline', { limit: 100 }, follower);
const res = await api('notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === fol.id);
assert.strictEqual(notes[0].text, 'x');
@ -431,21 +431,21 @@ describe('API visibility', () => {
//#region RTL
test('[replies] followers-reply が フォロワーから見れる', async () => {
const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
});
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes.length, 0);
});
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
@ -454,14 +454,14 @@ describe('API visibility', () => {
//#region MTL
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await api('/notes/mentions', { limit: 100 }, target);
const res = await api('notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
});
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
const res = await api('/notes/mentions', { limit: 100 }, target);
const res = await api('notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folM.id);
assert.strictEqual(notes[0].text, '@target x');

View file

@ -23,32 +23,32 @@ import type * as misskey from 'misskey-js';
describe('API', () => {
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2);
describe('General validation', () => {
test('wrong type', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
// @ts-expect-error string must be string
string: 42,
});
assert.strictEqual(res.status, 400);
});
test('missing require param', async () => {
const res = await api('/test', {
// @ts-expect-error required is required
const res = await api('test', {
string: 'a',
});
assert.strictEqual(res.status, 400);
});
test('invalid misskey:id (empty string)', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
id: '',
});
@ -56,7 +56,7 @@ describe('API', () => {
});
test('valid misskey:id', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
id: '8wvhjghbxu',
});
@ -64,7 +64,7 @@ describe('API', () => {
});
test('default value', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
string: 'a',
});
@ -73,7 +73,7 @@ describe('API', () => {
});
test('can set null even if it has default value', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
nullableDefault: null,
});
@ -82,7 +82,7 @@ describe('API', () => {
});
test('cannot set undefined if it has default value', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
nullableDefault: undefined,
});
@ -99,14 +99,14 @@ describe('API', () => {
// aliceは管理者、APIを使える
await successfulApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: alice,
});
// bobは一般ユーザーだからダメ
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: bob,
}, {
@ -117,7 +117,7 @@ describe('API', () => {
// publicアクセスももちろんダメ
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: undefined,
}, {
@ -128,7 +128,7 @@ describe('API', () => {
// ごまがしもダメ
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: 'tsukawasete' },
}, {
@ -138,13 +138,13 @@ describe('API', () => {
});
await successfulApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application2 },
});
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application },
}, {
@ -154,7 +154,7 @@ describe('API', () => {
});
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application3 },
}, {
@ -164,7 +164,7 @@ describe('API', () => {
});
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application4 },
}, {
@ -177,7 +177,7 @@ describe('API', () => {
describe('Authentication header', () => {
test('一般リクエスト', async () => {
await successfulApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: {
token: alice.token,
@ -211,7 +211,7 @@ describe('API', () => {
describe('tokenエラー応答でWWW-Authenticate headerを送る', () => {
describe('invalid_token', () => {
test('一般リクエスト', async () => {
const result = await api('/admin/get-index-stats', {}, {
const result = await api('admin/get-index-stats', {}, {
token: 'syuilo',
bearer: true,
});
@ -246,7 +246,7 @@ describe('API', () => {
describe('tokenがないとrealmだけおくる', () => {
test('一般リクエスト', async () => {
const result = await api('/admin/get-index-stats', {});
const result = await api('admin/get-index-stats', {});
assert.strictEqual(result.status, 401);
assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"');
});
@ -259,7 +259,8 @@ describe('API', () => {
});
test('invalid_request', async () => {
const result = await api('/notes/create', { text: true }, {
// @ts-expect-error text must be string
const result = await api('notes/create', { text: true }, {
token: alice.token,
bearer: true,
});

View file

@ -22,7 +22,7 @@ describe('Block', () => {
}, 1000 * 60 * 2);
test('Block作成', async () => {
const res = await api('/blocking/create', {
const res = await api('blocking/create', {
userId: bob.id,
}, alice);
@ -30,7 +30,7 @@ describe('Block', () => {
});
test('ブロックされているユーザーをフォローできない', async () => {
const res = await api('/following/create', { userId: alice.id }, bob);
const res = await api('following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
@ -39,7 +39,7 @@ describe('Block', () => {
test('ブロックされているユーザーにリアクションできない', async () => {
const note = await post(alice, { text: 'hello' });
const res = await api('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
@ -48,7 +48,7 @@ describe('Block', () => {
test('ブロックされているユーザーに返信できない', async () => {
const note = await post(alice, { text: 'hello' });
const res = await api('/notes/create', { replyId: note.id, text: 'yo' }, bob);
const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -57,7 +57,7 @@ describe('Block', () => {
test('ブロックされているユーザーのートをRenoteできない', async () => {
const note = await post(alice, { text: 'hello' });
const res = await api('/notes/create', { renoteId: note.id, text: 'yo' }, bob);
const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -72,12 +72,13 @@ describe('Block', () => {
const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' });
const res = await api('/notes/local-timeline', {}, bob);
const res = await api('notes/local-timeline', {}, bob);
const body = res.body as misskey.entities.Note[];
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
assert.strictEqual(body.some(note => note.id === aliceNote.id), false);
assert.strictEqual(body.some(note => note.id === bobNote.id), true);
assert.strictEqual(body.some(note => note.id === carolNote.id), true);
});
});

View file

@ -6,47 +6,34 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { JTDDataType } from 'ajv/dist/jtd';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import type { Packed } from '@/misc/json-schema.js';
import { paramDef as CreateParamDef } from '@/server/api/endpoints/clips/create.js';
import { paramDef as UpdateParamDef } from '@/server/api/endpoints/clips/update.js';
import { paramDef as DeleteParamDef } from '@/server/api/endpoints/clips/delete.js';
import { paramDef as ShowParamDef } from '@/server/api/endpoints/clips/show.js';
import { paramDef as FavoriteParamDef } from '@/server/api/endpoints/clips/favorite.js';
import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unfavorite.js';
import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js';
import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js';
import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js';
import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js';
import type * as Misskey from 'misskey-js';
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
describe('クリップ', () => {
type User = Packed<'User'>;
type Note = Packed<'Note'>;
type Clip = Packed<'Clip'>;
let alice: User;
let bob: User;
let aliceNote: Note;
let aliceHomeNote: Note;
let aliceFollowersNote: Note;
let aliceSpecifiedNote: Note;
let bobNote: Note;
let bobHomeNote: Note;
let bobFollowersNote: Note;
let bobSpecifiedNote: Note;
let alice: Misskey.entities.SignupResponse;
let bob: Misskey.entities.SignupResponse;
let aliceNote: Misskey.entities.Note;
let aliceHomeNote: Misskey.entities.Note;
let aliceFollowersNote: Misskey.entities.Note;
let aliceSpecifiedNote: Misskey.entities.Note;
let bobNote: Misskey.entities.Note;
let bobHomeNote: Misskey.entities.Note;
let bobFollowersNote: Misskey.entities.Note;
let bobSpecifiedNote: Misskey.entities.Note;
const compareBy = <T extends { id: string }, >(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
return selector(a).localeCompare(selector(b));
};
type CreateParam = JTDDataType<typeof CreateParamDef>;
const defaultCreate = (): Partial<CreateParam> => ({
const defaultCreate = (): Pick<Misskey.entities.ClipsCreateRequest, 'name'> => ({
name: 'test',
});
const create = async (parameters: Partial<CreateParam> = {}, request: Partial<ApiRequest> = {}): Promise<Clip> => {
const clip = await successfulApiCall<Clip>({
endpoint: '/clips/create',
const create = async (parameters: Partial<Misskey.entities.ClipsCreateRequest> = {}, request: Partial<ApiRequest<'clips/create'>> = {}): Promise<Misskey.entities.Clip> => {
const clip = await successfulApiCall({
endpoint: 'clips/create',
parameters: {
...defaultCreate(),
...parameters,
@ -64,17 +51,16 @@ describe('クリップ', () => {
return clip;
};
const createMany = async (parameters: Partial<CreateParam>, count = 10, user = alice): Promise<Clip[]> => {
const createMany = async (parameters: Partial<Misskey.entities.ClipsCreateRequest>, count = 10, user = alice): Promise<Misskey.entities.Clip[]> => {
return await Promise.all([...Array(count)].map((_, i) => create({
name: `test${i}`,
...parameters,
}, { user })));
};
type UpdateParam = JTDDataType<typeof UpdateParamDef>;
const update = async (parameters: Partial<UpdateParam>, request: Partial<ApiRequest> = {}): Promise<Clip> => {
const clip = await successfulApiCall<Clip>({
endpoint: '/clips/update',
const update = async (parameters: Optional<Misskey.entities.ClipsUpdateRequest, 'name'>, request: Partial<ApiRequest<'clips/update'>> = {}): Promise<Misskey.entities.Clip> => {
const clip = await successfulApiCall({
endpoint: 'clips/update',
parameters: {
name: 'updated',
...parameters,
@ -92,41 +78,39 @@ describe('クリップ', () => {
return clip;
};
type DeleteParam = JTDDataType<typeof DeleteParamDef>;
const deleteClip = async (parameters: DeleteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
return await successfulApiCall<void>({
endpoint: '/clips/delete',
const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
return await successfulApiCall({
endpoint: 'clips/delete',
parameters,
user: alice,
...request,
}, {
status: 204,
});
}) as any as void;
};
type ShowParam = JTDDataType<typeof ShowParamDef>;
const show = async (parameters: ShowParam, request: Partial<ApiRequest> = {}): Promise<Clip> => {
return await successfulApiCall<Clip>({
endpoint: '/clips/show',
const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
return await successfulApiCall({
endpoint: 'clips/show',
parameters,
user: alice,
...request,
});
};
const list = async (request: Partial<ApiRequest>): Promise<Clip[]> => {
return successfulApiCall<Clip[]>({
endpoint: '/clips/list',
const list = async (request: Partial<ApiRequest<'clips/list'>>): Promise<Misskey.entities.Clip[]> => {
return successfulApiCall({
endpoint: 'clips/list',
parameters: {},
user: alice,
...request,
});
};
const usersClips = async (request: Partial<ApiRequest>): Promise<Clip[]> => {
return await successfulApiCall<Clip[]>({
endpoint: '/users/clips',
parameters: {},
const usersClips = async (parameters: Misskey.entities.UsersClipsRequest, request: Partial<ApiRequest<'users/clips'>> = {}): Promise<Misskey.entities.Clip[]> => {
return await successfulApiCall({
endpoint: 'users/clips',
parameters,
user: alice,
...request,
});
@ -136,23 +120,22 @@ describe('クリップ', () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
// FIXME: misskey-jsのNoteはoutdatedなので直接変換できない
aliceNote = await post(alice, { text: 'test' }) as any;
aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any;
aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any;
aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any;
bobNote = await post(bob, { text: 'test' }) as any;
bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any;
bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any;
bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any;
aliceNote = await post(alice, { text: 'test' });
aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' });
aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' });
aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' });
bobNote = await post(bob, { text: 'test' });
bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' });
bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' });
bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' });
}, 1000 * 60 * 2);
afterEach(async () => {
// テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) {
const list = await api('/clips/list', { limit: 11 }, user);
const list = await api('clips/list', { limit: 11 }, user);
for (const clip of list.body) {
await api('/clips/delete', { clipId: clip.id }, user);
await api('clips/delete', { clipId: clip.id }, user);
}
}
});
@ -177,7 +160,7 @@ describe('クリップ', () => {
}
await failedApiCall({
endpoint: '/clips/create',
endpoint: 'clips/create',
parameters: defaultCreate(),
user: alice,
}, {
@ -204,7 +187,8 @@ describe('クリップ', () => {
{ label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } },
];
test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({
endpoint: '/clips/create',
endpoint: 'clips/create',
// @ts-expect-error invalid params
parameters: {
...defaultCreate(),
...parameters,
@ -246,15 +230,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
} },
{ label: '他人のクリップ', user: (): User => bob, assertion: {
{ label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
} },
...createClipDenyPattern as any,
])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/update',
endpoint: 'clips/update',
parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
name: 'updated',
...parameters,
},
@ -279,14 +263,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
} },
{ label: '他人のクリップ', user: (): User => bob, assertion: {
{ label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
} },
])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/delete',
endpoint: 'clips/delete',
parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
// @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters,
},
user: alice,
@ -306,7 +291,7 @@ describe('クリップ', () => {
test('のID指定取得は他人のPrivateなクリップは取得できない', async () => {
const clip = await create({ isPublic: false }, { user: bob } );
failedApiCall({
endpoint: '/clips/show',
endpoint: 'clips/show',
parameters: { clipId: clip.id },
user: alice,
}, {
@ -323,7 +308,8 @@ describe('クリップ', () => {
id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20',
} },
])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({
endpoint: '/clips/show',
endpoint: 'clips/show',
// @ts-expect-error clipId must not be undefined
parameters: {
...parameters,
},
@ -356,27 +342,23 @@ describe('クリップ', () => {
test('の一覧が取得できる(空)', async () => {
const res = await usersClips({
parameters: {
userId: alice.id,
},
userId: alice.id,
});
assert.deepStrictEqual(res, []);
});
test.each([
{ label: '' },
{ label: '他人アカウントから', user: (): User => bob },
{ label: '他人アカウントから', user: () => bob },
])('の一覧が$label取得できる', async () => {
const clips = await createMany({ isPublic: true });
const res = await usersClips({
parameters: {
userId: alice.id,
},
userId: alice.id,
});
// 返ってくる配列には順序保障がないのでidでソートして厳密比較
assert.deepStrictEqual(
res.sort(compareBy<Clip>(s => s.id)),
res.sort(compareBy<Misskey.entities.Clip>(s => s.id)),
clips.sort(compareBy(s => s.id)));
// 認証状態で見たときだけisFavoritedが入っている
@ -386,17 +368,16 @@ describe('クリップ', () => {
});
test.each([
{ label: '未認証', user: (): undefined => undefined },
{ label: '未認証', user: () => undefined },
{ label: '存在しないユーザーのもの', parameters: { userId: 'xxxxxxx' } },
])('の一覧は$labelでも取得できる', async ({ parameters, user }) => {
const clips = await createMany({ isPublic: true });
const res = await usersClips({
parameters: {
userId: alice.id,
limit: clips.length,
...parameters,
},
user: (user ?? ((): User => alice))(),
userId: alice.id,
limit: clips.length,
...parameters,
}, {
user: (user ?? (() => alice))(),
});
// 未認証で見たときはisFavoritedは入らない
@ -409,10 +390,8 @@ describe('クリップ', () => {
await create({ isPublic: false });
const aliceClip = await create({ isPublic: true });
const res = await usersClips({
parameters: {
userId: alice.id,
limit: 2,
},
userId: alice.id,
limit: 2,
});
assert.deepStrictEqual(res, [aliceClip]);
});
@ -421,17 +400,15 @@ describe('クリップ', () => {
const clips = await createMany({ isPublic: true }, 7);
clips.sort(compareBy(s => s.id));
const res = await usersClips({
parameters: {
userId: alice.id,
sinceId: clips[1].id,
untilId: clips[5].id,
limit: 4,
},
userId: alice.id,
sinceId: clips[1].id,
untilId: clips[5].id,
limit: 4,
});
// Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較
assert.deepStrictEqual(
res.sort(compareBy<Clip>(s => s.id)),
res.sort(compareBy<Misskey.entities.Clip>(s => s.id)),
[clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない
clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id));
});
@ -441,8 +418,9 @@ describe('クリップ', () => {
{ label: 'limitゼロ', parameters: { limit: 0 } },
{ label: 'limit最大+1', parameters: { limit: 101 } },
])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({
endpoint: '/users/clips',
endpoint: 'users/clips',
parameters: {
// @ts-expect-error userId must not be undefined
userId: alice.id,
...parameters,
},
@ -454,15 +432,15 @@ describe('クリップ', () => {
}));
test.each([
{ label: '作成', endpoint: '/clips/create' },
{ label: '更新', endpoint: '/clips/update' },
{ label: '削除', endpoint: '/clips/delete' },
{ label: '取得', endpoint: '/clips/list' },
{ label: 'お気に入り設定', endpoint: '/clips/favorite' },
{ label: 'お気に入り解除', endpoint: '/clips/unfavorite' },
{ label: 'お気に入り取得', endpoint: '/clips/my-favorites' },
{ label: 'ノート追加', endpoint: '/clips/add-note' },
{ label: 'ノート削除', endpoint: '/clips/remove-note' },
{ label: '作成', endpoint: 'clips/create' as const },
{ label: '更新', endpoint: 'clips/update' as const },
{ label: '削除', endpoint: 'clips/delete' as const },
{ label: '取得', endpoint: 'clips/list' as const },
{ label: 'お気に入り設定', endpoint: 'clips/favorite' as const },
{ label: 'お気に入り解除', endpoint: 'clips/unfavorite' as const },
{ label: 'お気に入り取得', endpoint: 'clips/my-favorites' as const },
{ label: 'ノート追加', endpoint: 'clips/add-note' as const },
{ label: 'ノート削除', endpoint: 'clips/remove-note' as const },
])('の$labelは未認証ではできない', async ({ endpoint }) => await failedApiCall({
endpoint: endpoint,
parameters: {},
@ -474,35 +452,33 @@ describe('クリップ', () => {
}));
describe('のお気に入り', () => {
let aliceClip: Clip;
let aliceClip: Misskey.entities.Clip;
type FavoriteParam = JTDDataType<typeof FavoriteParamDef>;
const favorite = async (parameters: FavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
return successfulApiCall<void>({
endpoint: '/clips/favorite',
const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
return successfulApiCall({
endpoint: 'clips/favorite',
parameters,
user: alice,
...request,
}, {
status: 204,
});
}) as any as void;
};
type UnfavoriteParam = JTDDataType<typeof UnfavoriteParamDef>;
const unfavorite = async (parameters: UnfavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
return successfulApiCall<void>({
endpoint: '/clips/unfavorite',
const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
return successfulApiCall({
endpoint: 'clips/unfavorite',
parameters,
user: alice,
...request,
}, {
status: 204,
});
}) as any as void;
};
const myFavorites = async (request: Partial<ApiRequest> = {}): Promise<Clip[]> => {
return successfulApiCall<Clip[]>({
endpoint: '/clips/my-favorites',
const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
return successfulApiCall({
endpoint: 'clips/my-favorites',
parameters: {},
user: alice,
...request,
@ -568,7 +544,7 @@ describe('クリップ', () => {
test('は同じクリップに対して二回設定できない。', async () => {
await favorite({ clipId: aliceClip.id });
await failedApiCall({
endpoint: '/clips/favorite',
endpoint: 'clips/favorite',
parameters: {
clipId: aliceClip.id,
},
@ -586,14 +562,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
} },
{ label: '他人のクリップ', user: (): User => bob, assertion: {
{ label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
} },
])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/favorite',
endpoint: 'clips/favorite',
parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
// @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters,
},
user: alice,
@ -619,7 +596,7 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '2603966e-b865-426c-94a7-af4a01241dc1',
} },
{ label: '他人のクリップ', user: (): User => bob, assertion: {
{ label: '他人のクリップ', user: () => bob, assertion: {
code: 'NOT_FAVORITED',
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
} },
@ -628,9 +605,10 @@ describe('クリップ', () => {
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
} },
])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/unfavorite',
endpoint: 'clips/unfavorite',
parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
// @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters,
},
user: alice,
@ -655,41 +633,38 @@ describe('クリップ', () => {
});
describe('に紐づくノート', () => {
let aliceClip: Clip;
let aliceClip: Misskey.entities.Clip;
const sampleNotes = (): Note[] => [
const sampleNotes = (): Misskey.entities.Note[] => [
aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote,
bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote,
];
type AddNoteParam = JTDDataType<typeof AddNoteParamDef>;
const addNote = async (parameters: AddNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
return successfulApiCall<void>({
endpoint: '/clips/add-note',
const addNote = async (parameters: Misskey.entities.ClipsAddNoteRequest, request: Partial<ApiRequest<'clips/add-note'>> = {}): Promise<void> => {
return successfulApiCall({
endpoint: 'clips/add-note',
parameters,
user: alice,
...request,
}, {
status: 204,
});
}) as any as void;
};
type RemoveNoteParam = JTDDataType<typeof RemoveNoteParamDef>;
const removeNote = async (parameters: RemoveNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
return successfulApiCall<void>({
endpoint: '/clips/remove-note',
const removeNote = async (parameters: Misskey.entities.ClipsRemoveNoteRequest, request: Partial<ApiRequest<'clips/remove-note'>> = {}): Promise<void> => {
return successfulApiCall({
endpoint: 'clips/remove-note',
parameters,
user: alice,
...request,
}, {
status: 204,
});
}) as any as void;
};
type NotesParam = JTDDataType<typeof NotesParamDef>;
const notes = async (parameters: Partial<NotesParam>, request: Partial<ApiRequest> = {}): Promise<Note[]> => {
return successfulApiCall<Note[]>({
endpoint: '/clips/notes',
const notes = async (parameters: Misskey.entities.ClipsNotesRequest, request: Partial<ApiRequest<'clips/notes'>> = {}): Promise<Misskey.entities.Note[]> => {
return successfulApiCall({
endpoint: 'clips/notes',
parameters,
user: alice,
...request,
@ -715,7 +690,7 @@ describe('クリップ', () => {
test('として同じノートを二回紐づけることはできない', async () => {
await addNote({ clipId: aliceClip.id, noteId: aliceNote.id });
await failedApiCall({
endpoint: '/clips/add-note',
endpoint: 'clips/add-note',
parameters: {
clipId: aliceClip.id,
noteId: aliceNote.id,
@ -733,11 +708,11 @@ describe('クリップ', () => {
const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1;
const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, {
text: `test ${i}`,
}) as unknown)) as Note[];
}) as unknown)) as Misskey.entities.Note[];
await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id })));
await failedApiCall({
endpoint: '/clips/add-note',
endpoint: 'clips/add-note',
parameters: {
clipId: aliceClip.id,
noteId: aliceNote.id,
@ -751,7 +726,7 @@ describe('クリップ', () => {
});
test('は他人のクリップへ追加できない。', async () => await failedApiCall({
endpoint: '/clips/add-note',
endpoint: 'clips/add-note',
parameters: {
clipId: aliceClip.id,
noteId: aliceNote.id,
@ -774,18 +749,20 @@ describe('クリップ', () => {
code: 'NO_SUCH_NOTE',
id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b',
} },
{ label: '他人のクリップ', user: (): object => bob, assetion: {
{ label: '他人のクリップ', user: () => bob, assetion: {
code: 'NO_SUCH_CLIP',
id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf',
} },
])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({
endpoint: '/clips/add-note',
endpoint: 'clips/add-note',
parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id,
// @ts-expect-error noteId must not be undefined
noteId: aliceNote.id,
...parameters,
},
user: (user ?? ((): User => alice))(),
user: (user ?? (() => alice))(),
}, {
status: 400,
code: 'INVALID_PARAM',
@ -810,18 +787,20 @@ describe('クリップ', () => {
code: 'NO_SUCH_NOTE',
id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteと異なる
} },
{ label: '他人のクリップ', user: (): object => bob, assetion: {
{ label: '他人のクリップ', user: () => bob, assetion: {
code: 'NO_SUCH_CLIP',
id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる
} },
])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({
endpoint: '/clips/remove-note',
endpoint: 'clips/remove-note',
parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id,
// @ts-expect-error noteId must not be undefined
noteId: aliceNote.id,
...parameters,
},
user: (user ?? ((): User => alice))(),
user: (user ?? (() => alice))(),
}, {
status: 400,
code: 'INVALID_PARAM',
@ -925,21 +904,22 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} },
{ label: '他人のPrivateなクリップから', user: (): object => bob, assertion: {
{ label: '他人のPrivateなクリップから', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} },
{ label: '未認証でPrivateなクリップから', user: (): undefined => undefined, assertion: {
{ label: '未認証でPrivateなクリップから', user: () => undefined, assertion: {
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} },
])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/notes',
endpoint: 'clips/notes',
parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id,
...parameters,
},
user: (user ?? ((): User => alice))(),
user: (user ?? (() => alice))(),
}, {
status: 400,
code: 'INVALID_PARAM',

View file

@ -6,22 +6,14 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { MiNote } from '@/models/Note.js';
import { api, initTestDb, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import { api, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
import type{ Repository } from 'typeorm'
import type { Packed } from '@/misc/json-schema.js';
describe('Drive', () => {
let Notes: Repository<MiNote>;
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
beforeAll(async () => {
const connection = await initTestDb(true);
Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2);
@ -31,13 +23,13 @@ describe('Drive', () => {
const marker = Math.random().toString();
const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'
const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg';
const catcher = makeStreamCatcher(
alice,
'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>,
(msg) => msg.body.file,
10 * 1000);
const res = await api('drive/files/upload-from-url', {
@ -51,7 +43,7 @@ describe('Drive', () => {
assert.strictEqual(res.status, 204);
assert.strictEqual(file.name, 'Lenna.jpg');
assert.strictEqual(file.type, 'image/jpeg');
})
});
test('ローカルからアップロードできる', async () => {
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
@ -59,27 +51,27 @@ describe('Drive', () => {
const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' });
assert.strictEqual(res.body?.name, 'テスト画像.jpg');
assert.strictEqual(res.body?.type, 'image/jpeg');
})
assert.strictEqual(res.body.type, 'image/jpeg');
});
test('添付ノート一覧を取得できる', async () => {
const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id)
const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id);
const note0 = await post(alice, { fileIds: [ids[0]] });
const note1 = await post(alice, { fileIds: [ids[0], ids[1]] });
const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice);
assert.strictEqual(attached0.body.length, 2);
assert.strictEqual(attached0.body[0].id, note1.id)
assert.strictEqual(attached0.body[1].id, note0.id)
assert.strictEqual(attached0.body[0].id, note1.id);
assert.strictEqual(attached0.body[1].id, note0.id);
const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice);
assert.strictEqual(attached1.body.length, 1);
assert.strictEqual(attached1.body[0].id, note1.id)
assert.strictEqual(attached1.body[0].id, note1.id);
const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice);
assert.strictEqual(attached2.body.length, 0)
})
assert.strictEqual(attached2.body.length, 0);
});
test('添付ノート一覧は他の人から見えない', async () => {
const file = await uploadFile(alice);
@ -89,7 +81,5 @@ describe('Drive', () => {
const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual('error' in res.body, true);
})
});
});

View file

@ -79,6 +79,7 @@ describe('Endpoints', () => {
test('クエリをインジェクションできない', async () => {
const res = await api('signin', {
username: 'test1',
// @ts-expect-error password must be string
password: {
$gt: '',
},
@ -103,7 +104,7 @@ describe('Endpoints', () => {
const myLocation = '七森中';
const myBirthday = '2000-09-07';
const res = await api('/i/update', {
const res = await api('i/update', {
name: myName,
location: myLocation,
birthday: myBirthday,
@ -117,7 +118,7 @@ describe('Endpoints', () => {
});
test('名前を空白にできる', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
name: ' ',
}, alice);
assert.strictEqual(res.status, 200);
@ -125,11 +126,11 @@ describe('Endpoints', () => {
});
test('誕生日の設定を削除できる', async () => {
await api('/i/update', {
await api('i/update', {
birthday: '2000-09-07',
}, alice);
const res = await api('/i/update', {
const res = await api('i/update', {
birthday: null,
}, alice);
@ -139,7 +140,7 @@ describe('Endpoints', () => {
});
test('不正な誕生日の形式で怒られる', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
birthday: '2000/09/07',
}, alice);
assert.strictEqual(res.status, 400);
@ -148,7 +149,7 @@ describe('Endpoints', () => {
describe('users/show', () => {
test('ユーザーが取得できる', async () => {
const res = await api('/users/show', {
const res = await api('users/show', {
userId: alice.id,
}, alice);
@ -158,14 +159,14 @@ describe('Endpoints', () => {
});
test('ユーザーが存在しなかったら怒る', async () => {
const res = await api('/users/show', {
const res = await api('users/show', {
userId: '000000000000000000000000',
});
assert.strictEqual(res.status, 404);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/users/show', {
const res = await api('users/show', {
userId: 'kyoppie',
});
assert.strictEqual(res.status, 404);
@ -178,7 +179,7 @@ describe('Endpoints', () => {
text: 'test',
});
const res = await api('/notes/show', {
const res = await api('notes/show', {
noteId: myPost.id,
}, alice);
@ -189,14 +190,14 @@ describe('Endpoints', () => {
});
test('投稿が存在しなかったら怒る', async () => {
const res = await api('/notes/show', {
const res = await api('notes/show', {
noteId: '000000000000000000000000',
});
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/notes/show', {
const res = await api('notes/show', {
noteId: 'kyoppie',
});
assert.strictEqual(res.status, 400);
@ -207,14 +208,14 @@ describe('Endpoints', () => {
test('リアクションできる', async () => {
const bobPost = await post(bob, { text: 'hi' });
const res = await api('/notes/reactions/create', {
const res = await api('notes/reactions/create', {
noteId: bobPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
const resNote = await api('/notes/show', {
const resNote = await api('notes/show', {
noteId: bobPost.id,
}, alice);
@ -225,7 +226,7 @@ describe('Endpoints', () => {
test('自分の投稿にもリアクションできる', async () => {
const myPost = await post(alice, { text: 'hi' });
const res = await api('/notes/reactions/create', {
const res = await api('notes/reactions/create', {
noteId: myPost.id,
reaction: '🚀',
}, alice);
@ -236,19 +237,19 @@ describe('Endpoints', () => {
test('二重にリアクションすると上書きされる', async () => {
const bobPost = await post(bob, { text: 'hi' });
await api('/notes/reactions/create', {
await api('notes/reactions/create', {
noteId: bobPost.id,
reaction: '🥰',
}, alice);
const res = await api('/notes/reactions/create', {
const res = await api('notes/reactions/create', {
noteId: bobPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
const resNote = await api('/notes/show', {
const resNote = await api('notes/show', {
noteId: bobPost.id,
}, alice);
@ -257,7 +258,7 @@ describe('Endpoints', () => {
});
test('存在しない投稿にはリアクションできない', async () => {
const res = await api('/notes/reactions/create', {
const res = await api('notes/reactions/create', {
noteId: '000000000000000000000000',
reaction: '🚀',
}, alice);
@ -266,13 +267,14 @@ describe('Endpoints', () => {
});
test('空のパラメータで怒られる', async () => {
const res = await api('/notes/reactions/create', {}, alice);
// @ts-expect-error param must not be empty
const res = await api('notes/reactions/create', {}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/notes/reactions/create', {
const res = await api('notes/reactions/create', {
noteId: 'kyoppie',
reaction: '🚀',
}, alice);
@ -283,7 +285,7 @@ describe('Endpoints', () => {
describe('following/create', () => {
test('フォローできる', async () => {
const res = await api('/following/create', {
const res = await api('following/create', {
userId: alice.id,
}, bob);
@ -301,7 +303,7 @@ describe('Endpoints', () => {
});
test('既にフォローしている場合は怒る', async () => {
const res = await api('/following/create', {
const res = await api('following/create', {
userId: alice.id,
}, bob);
@ -309,7 +311,7 @@ describe('Endpoints', () => {
});
test('存在しないユーザーはフォローできない', async () => {
const res = await api('/following/create', {
const res = await api('following/create', {
userId: '000000000000000000000000',
}, alice);
@ -317,7 +319,7 @@ describe('Endpoints', () => {
});
test('自分自身はフォローできない', async () => {
const res = await api('/following/create', {
const res = await api('following/create', {
userId: alice.id,
}, alice);
@ -325,13 +327,14 @@ describe('Endpoints', () => {
});
test('空のパラメータで怒られる', async () => {
const res = await api('/following/create', {}, alice);
// @ts-expect-error params must not be empty
const res = await api('following/create', {}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/following/create', {
const res = await api('following/create', {
userId: 'foo',
}, alice);
@ -341,11 +344,11 @@ describe('Endpoints', () => {
describe('following/delete', () => {
test('フォロー解除できる', async () => {
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const res = await api('/following/delete', {
const res = await api('following/delete', {
userId: alice.id,
}, bob);
@ -363,7 +366,7 @@ describe('Endpoints', () => {
});
test('フォローしていない場合は怒る', async () => {
const res = await api('/following/delete', {
const res = await api('following/delete', {
userId: alice.id,
}, bob);
@ -371,7 +374,7 @@ describe('Endpoints', () => {
});
test('存在しないユーザーはフォロー解除できない', async () => {
const res = await api('/following/delete', {
const res = await api('following/delete', {
userId: '000000000000000000000000',
}, alice);
@ -379,7 +382,7 @@ describe('Endpoints', () => {
});
test('自分自身はフォロー解除できない', async () => {
const res = await api('/following/delete', {
const res = await api('following/delete', {
userId: alice.id,
}, alice);
@ -387,13 +390,14 @@ describe('Endpoints', () => {
});
test('空のパラメータで怒られる', async () => {
const res = await api('/following/delete', {}, alice);
// @ts-expect-error params must not be empty
const res = await api('following/delete', {}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/following/delete', {
const res = await api('following/delete', {
userId: 'kyoppie',
}, alice);
@ -403,20 +407,20 @@ describe('Endpoints', () => {
describe('channels/search', () => {
test('空白検索で一覧を取得できる', async () => {
await api('/channels/create', {
await api('channels/create', {
name: 'aaa',
description: 'bbb',
}, bob);
await api('/channels/create', {
await api('channels/create', {
name: 'ccc1',
description: 'ddd1',
}, bob);
await api('/channels/create', {
await api('channels/create', {
name: 'ccc2',
description: 'ddd2',
}, bob);
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: '',
}, bob);
@ -425,7 +429,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 3);
});
test('名前のみの検索で名前を検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'aaa',
type: 'nameOnly',
}, bob);
@ -436,7 +440,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'aaa');
});
test('名前のみの検索で名前を複数検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'ccc',
type: 'nameOnly',
}, bob);
@ -446,7 +450,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 2);
});
test('名前のみの検索で説明は検索できない', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'bbb',
type: 'nameOnly',
}, bob);
@ -456,7 +460,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 0);
});
test('名前と説明の検索で名前を検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'ccc1',
}, bob);
@ -466,7 +470,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'ccc1');
});
test('名前と説明での検索で説明を検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'ddd1',
}, bob);
@ -476,7 +480,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'ccc1');
});
test('名前と説明の検索で名前を複数検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'ccc',
}, bob);
@ -485,7 +489,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 2);
});
test('名前と説明での検索で説明を複数検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'ddd',
}, bob);
@ -506,7 +510,7 @@ describe('Endpoints', () => {
await uploadFile(alice, {
blob: new Blob([new Uint8Array(1024)]),
});
const res = await api('/drive', {}, alice);
const res = await api('drive', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).toHaveProperty('usage', 1792);
@ -519,7 +523,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Lenna.jpg');
assert.strictEqual(res.body!.name, 'Lenna.jpg');
});
test('ファイルに名前を付けられる', async () => {
@ -527,7 +531,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Belmond.jpg');
assert.strictEqual(res.body!.name, 'Belmond.jpg');
});
test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => {
@ -535,11 +539,12 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Belmond.png.jpg');
assert.strictEqual(res.body!.name, 'Belmond.png.jpg');
});
test('ファイル無しで怒られる', async () => {
const res = await api('/drive/files/create', {}, alice);
// @ts-expect-error params must not be empty
const res = await api('drive/files/create', {}, alice);
assert.strictEqual(res.status, 400);
});
@ -549,14 +554,14 @@ describe('Endpoints', () => {
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');
assert.strictEqual(res.body!.name, 'image.svg');
assert.strictEqual(res.body!.type, 'image/svg+xml');
});
for (const type of ['webp', 'avif']) {
const mediaType = `image/${type}`;
const getWebpublicType = async (user: any, fileId: string): Promise<string> => {
const getWebpublicType = async (user: misskey.entities.SignupResponse, fileId: string): Promise<string> => {
// drive/files/create does not expose webpublicType directly, so get it by posting it
const res = await post(user, {
text: mediaType,
@ -573,10 +578,10 @@ describe('Endpoints', () => {
const res = await uploadFile(alice, { path });
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, path);
assert.strictEqual(res.body.type, mediaType);
assert.strictEqual(res.body!.name, path);
assert.strictEqual(res.body!.type, mediaType);
const webpublicType = await getWebpublicType(alice, res.body.id);
const webpublicType = await getWebpublicType(alice, res.body!.id);
assert.strictEqual(webpublicType, 'image/webp');
});
@ -584,10 +589,10 @@ describe('Endpoints', () => {
const path = `without-alpha.${type}`;
const res = await uploadFile(alice, { path });
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, path);
assert.strictEqual(res.body.type, mediaType);
assert.strictEqual(res.body!.name, path);
assert.strictEqual(res.body!.type, mediaType);
const webpublicType = await getWebpublicType(alice, res.body.id);
const webpublicType = await getWebpublicType(alice, res.body!.id);
assert.strictEqual(webpublicType, 'image/webp');
});
}
@ -598,8 +603,8 @@ describe('Endpoints', () => {
const file = (await uploadFile(alice)).body;
const newName = 'いちごパスタ.png';
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
name: newName,
}, alice);
@ -611,8 +616,8 @@ describe('Endpoints', () => {
test('他人のファイルは更新できない', async () => {
const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
name: 'いちごパスタ.png',
}, bob);
@ -621,12 +626,12 @@ describe('Endpoints', () => {
test('親フォルダを更新できる', async () => {
const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
folderId: folder.id,
}, alice);
@ -638,17 +643,17 @@ describe('Endpoints', () => {
test('親フォルダを無しにできる', async () => {
const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
await api('/drive/files/update', {
fileId: file.id,
await api('drive/files/update', {
fileId: file!.id,
folderId: folder.id,
}, alice);
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
folderId: null,
}, alice);
@ -659,12 +664,12 @@ describe('Endpoints', () => {
test('他人のフォルダには入れられない', async () => {
const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, bob)).body;
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
folderId: folder.id,
}, alice);
@ -674,8 +679,8 @@ describe('Endpoints', () => {
test('存在しないフォルダで怒られる', async () => {
const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
folderId: '000000000000000000000000',
}, alice);
@ -685,8 +690,8 @@ describe('Endpoints', () => {
test('不正なフォルダIDで怒られる', async () => {
const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
folderId: 'foo',
}, alice);
@ -694,7 +699,7 @@ describe('Endpoints', () => {
});
test('ファイルが存在しなかったら怒る', async () => {
const res = await api('/drive/files/update', {
const res = await api('drive/files/update', {
fileId: '000000000000000000000000',
name: 'いちごパスタ.png',
}, alice);
@ -706,8 +711,8 @@ describe('Endpoints', () => {
const file = (await uploadFile(alice)).body;
const newName = '';
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
name: newName,
}, alice);
@ -715,7 +720,7 @@ describe('Endpoints', () => {
});
test('間違ったIDで怒られる', async () => {
const res = await api('/drive/files/update', {
const res = await api('drive/files/update', {
fileId: 'kyoppie',
name: 'いちごパスタ.png',
}, alice);
@ -726,7 +731,7 @@ describe('Endpoints', () => {
describe('drive/folders/create', () => {
test('フォルダを作成できる', async () => {
const res = await api('/drive/folders/create', {
const res = await api('drive/folders/create', {
name: 'test',
}, alice);
@ -738,11 +743,11 @@ describe('Endpoints', () => {
describe('drive/folders/update', () => {
test('名前を更新できる', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
name: 'new name',
}, alice);
@ -753,11 +758,11 @@ describe('Endpoints', () => {
});
test('他人のフォルダを更新できない', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, bob)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
name: 'new name',
}, alice);
@ -766,14 +771,14 @@ describe('Endpoints', () => {
});
test('親フォルダを更新できる', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const parentFolder = (await api('/drive/folders/create', {
const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, alice)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
@ -784,18 +789,18 @@ describe('Endpoints', () => {
});
test('親フォルダを無しに更新できる', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const parentFolder = (await api('/drive/folders/create', {
const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, alice)).body;
await api('/drive/folders/update', {
await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: null,
}, alice);
@ -806,14 +811,14 @@ describe('Endpoints', () => {
});
test('他人のフォルダを親フォルダに設定できない', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const parentFolder = (await api('/drive/folders/create', {
const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, bob)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
@ -822,18 +827,18 @@ describe('Endpoints', () => {
});
test('フォルダが循環するような構造にできない', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const parentFolder = (await api('/drive/folders/create', {
const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, alice)).body;
await api('/drive/folders/update', {
await api('drive/folders/update', {
folderId: parentFolder.id,
parentId: folder.id,
}, alice);
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
@ -842,25 +847,25 @@ describe('Endpoints', () => {
});
test('フォルダが循環するような構造にできない(再帰的)', async () => {
const folderA = (await api('/drive/folders/create', {
const folderA = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const folderB = (await api('/drive/folders/create', {
const folderB = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const folderC = (await api('/drive/folders/create', {
const folderC = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
await api('/drive/folders/update', {
await api('drive/folders/update', {
folderId: folderB.id,
parentId: folderA.id,
}, alice);
await api('/drive/folders/update', {
await api('drive/folders/update', {
folderId: folderC.id,
parentId: folderB.id,
}, alice);
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folderA.id,
parentId: folderC.id,
}, alice);
@ -869,11 +874,11 @@ describe('Endpoints', () => {
});
test('フォルダが循環するような構造にできない(自身)', async () => {
const folderA = (await api('/drive/folders/create', {
const folderA = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folderA.id,
parentId: folderA.id,
}, alice);
@ -882,11 +887,11 @@ describe('Endpoints', () => {
});
test('存在しない親フォルダを設定できない', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: '000000000000000000000000',
}, alice);
@ -895,11 +900,11 @@ describe('Endpoints', () => {
});
test('不正な親フォルダIDで怒られる', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: 'foo',
}, alice);
@ -908,7 +913,7 @@ describe('Endpoints', () => {
});
test('存在しないフォルダを更新できない', async () => {
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: '000000000000000000000000',
}, alice);
@ -916,7 +921,7 @@ describe('Endpoints', () => {
});
test('不正なフォルダIDで怒られる', async () => {
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: 'foo',
}, alice);
@ -937,7 +942,7 @@ describe('Endpoints', () => {
visibleUserIds: [alice.id],
});
const res = await api('/notes/replies', {
const res = await api('notes/replies', {
noteId: alicePost.id,
}, carol);
@ -949,7 +954,7 @@ describe('Endpoints', () => {
describe('notes/timeline', () => {
test('フォロワー限定投稿が含まれる', async () => {
await api('/following/create', {
await api('following/create', {
userId: carol.id,
}, dave);
@ -958,7 +963,7 @@ describe('Endpoints', () => {
visibility: 'followers',
});
const res = await api('/notes/timeline', {}, dave);
const res = await api('notes/timeline', {}, dave);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -979,12 +984,12 @@ describe('Endpoints', () => {
test('他者に関するメモを更新できる', async () => {
const memo = '10月まで低浮上とのこと。';
const res1 = await api('/users/update-memo', {
const res1 = await api('users/update-memo', {
memo,
userId: bob.id,
}, alice);
const res2 = await api('/users/show', {
const res2 = await api('users/show', {
userId: bob.id,
}, alice);
assert.strictEqual(res1.status, 204);
@ -994,12 +999,12 @@ describe('Endpoints', () => {
test('自分に関するメモを更新できる', async () => {
const memo = 'チケットを月末までに買う。';
const res1 = await api('/users/update-memo', {
const res1 = await api('users/update-memo', {
memo,
userId: alice.id,
}, alice);
const res2 = await api('/users/show', {
const res2 = await api('users/show', {
userId: alice.id,
}, alice);
assert.strictEqual(res1.status, 204);
@ -1009,17 +1014,17 @@ describe('Endpoints', () => {
test('メモを削除できる', async () => {
const memo = '10月まで低浮上とのこと。';
await api('/users/update-memo', {
await api('users/update-memo', {
memo,
userId: bob.id,
}, alice);
await api('/users/update-memo', {
await api('users/update-memo', {
memo: '',
userId: bob.id,
}, alice);
const res = await api('/users/show', {
const res = await api('users/show', {
userId: bob.id,
}, alice);
@ -1032,21 +1037,21 @@ describe('Endpoints', () => {
const memoCarolToBob = '例の件について今度問いただす。';
await Promise.all([
api('/users/update-memo', {
api('users/update-memo', {
memo: memoAliceToBob,
userId: bob.id,
}, alice),
api('/users/update-memo', {
api('users/update-memo', {
memo: memoCarolToBob,
userId: bob.id,
}, carol),
]);
const [resAlice, resCarol] = await Promise.all([
api('/users/show', {
api('users/show', {
userId: bob.id,
}, alice),
api('/users/show', {
api('users/show', {
userId: bob.id,
}, carol),
]);

View file

@ -18,7 +18,7 @@ describe('export-clips', () => {
// XXX: Any better way to get the result?
async function pollFirstDriveFile() {
while (true) {
const files = (await api('/drive/files', {}, alice)).body;
const files = (await api('drive/files', {}, alice)).body;
if (!files.length) {
await new Promise(r => setTimeout(r, 100));
continue;
@ -26,7 +26,7 @@ describe('export-clips', () => {
if (files.length > 1) {
throw new Error('Too many files?');
}
const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body;
const file = (await api('drive/files/show', { fileId: files[0].id }, alice)).body;
const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`));
return await res.json();
}
@ -44,16 +44,16 @@ describe('export-clips', () => {
beforeEach(async () => {
// Clean all clips and files of alice
const clips = (await api('/clips/list', {}, alice)).body;
const clips = (await api('clips/list', {}, alice)).body;
for (const clip of clips) {
const res = await api('/clips/delete', { clipId: clip.id }, alice);
const res = await api('clips/delete', { clipId: clip.id }, alice);
if (res.status !== 204) {
throw new Error('Failed to delete clip');
}
}
const files = (await api('/drive/files', {}, alice)).body;
const files = (await api('drive/files', {}, alice)).body;
for (const file of files) {
const res = await api('/drive/files/delete', { fileId: file.id }, alice);
const res = await api('drive/files/delete', { fileId: file.id }, alice);
if (res.status !== 204) {
throw new Error('Failed to delete file');
}
@ -61,13 +61,13 @@ describe('export-clips', () => {
});
test('basic export', async () => {
let res = await api('/clips/create', {
let res = await api('clips/create', {
name: 'foo',
description: 'bar',
}, alice);
assert.strictEqual(res.status, 200);
res = await api('/i/export-clips', {}, alice);
res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
@ -77,7 +77,7 @@ describe('export-clips', () => {
});
test('export with notes', async () => {
let res = await api('/clips/create', {
let res = await api('clips/create', {
name: 'foo',
description: 'bar',
}, alice);
@ -96,14 +96,14 @@ describe('export-clips', () => {
});
for (const note of [note1, note2]) {
res = await api('/clips/add-note', {
res = await api('clips/add-note', {
clipId: clip.id,
noteId: note.id,
}, alice);
assert.strictEqual(res.status, 204);
}
res = await api('/i/export-clips', {}, alice);
res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
@ -116,14 +116,14 @@ describe('export-clips', () => {
});
test('multiple clips', async () => {
let res = await api('/clips/create', {
let res = await api('clips/create', {
name: 'kawaii',
description: 'kawaii',
}, alice);
assert.strictEqual(res.status, 200);
const clip1 = res.body;
res = await api('/clips/create', {
res = await api('clips/create', {
name: 'yuri',
description: 'yuri',
}, alice);
@ -138,19 +138,19 @@ describe('export-clips', () => {
text: 'baz2',
});
res = await api('/clips/add-note', {
res = await api('clips/add-note', {
clipId: clip1.id,
noteId: note1.id,
}, alice);
assert.strictEqual(res.status, 204);
res = await api('/clips/add-note', {
res = await api('clips/add-note', {
clipId: clip2.id,
noteId: note2.id,
}, alice);
assert.strictEqual(res.status, 204);
res = await api('/i/export-clips', {}, alice);
res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
@ -163,7 +163,7 @@ describe('export-clips', () => {
});
test('Clipping other user\'s note', async () => {
let res = await api('/clips/create', {
let res = await api('clips/create', {
name: 'kawaii',
description: 'kawaii',
}, alice);
@ -175,13 +175,13 @@ describe('export-clips', () => {
visibility: 'followers',
});
res = await api('/clips/add-note', {
res = await api('clips/add-note', {
clipId: clip.id,
noteId: note.id,
}, alice);
assert.strictEqual(res.status, 204);
res = await api('/i/export-clips', {}, alice);
res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();

View file

@ -23,13 +23,13 @@ const JSON_UTF8 = 'application/json; charset=utf-8';
describe('Webリソース', () => {
let alice: misskey.entities.SignupResponse;
let aliceUploadedFile: any;
let alicesPost: any;
let alicePage: any;
let alicePlay: any;
let aliceClip: any;
let aliceGalleryPost: any;
let aliceChannel: any;
let aliceUploadedFile: misskey.entities.DriveFile | null;
let alicesPost: misskey.entities.Note;
let alicePage: misskey.entities.Page;
let alicePlay: misskey.entities.Flash;
let aliceClip: misskey.entities.Clip;
let aliceGalleryPost: misskey.entities.GalleryPost;
let aliceChannel: misskey.entities.Channel;
let bob: misskey.entities.SignupResponse;
@ -77,7 +77,7 @@ describe('Webリソース', () => {
beforeAll(async () => {
alice = await signup({ username: 'alice' });
aliceUploadedFile = await uploadFile(alice);
aliceUploadedFile = (await uploadFile(alice)).body;
alicesPost = await post(alice, {
text: 'test',
});
@ -85,7 +85,7 @@ describe('Webリソース', () => {
alicePlay = await play(alice, {});
aliceClip = await clip(alice, {});
aliceGalleryPost = await galleryPost(alice, {
fileIds: [aliceUploadedFile.body.id],
fileIds: [aliceUploadedFile!.id],
});
aliceChannel = await channel(alice, {});

View file

@ -19,15 +19,15 @@ describe('FF visibility', () => {
}, 1000 * 60 * 2);
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@ -39,36 +39,36 @@ describe('FF visibility', () => {
test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
@ -78,36 +78,36 @@ describe('FF visibility', () => {
test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
@ -116,15 +116,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
@ -136,36 +136,36 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
@ -175,36 +175,36 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
@ -213,15 +213,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@ -231,34 +231,34 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
@ -267,34 +267,34 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
@ -302,19 +302,19 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@ -326,45 +326,45 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
@ -374,45 +374,45 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
@ -421,15 +421,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
@ -441,36 +441,36 @@ describe('FF visibility', () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
@ -480,36 +480,36 @@ describe('FF visibility', () => {
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
@ -518,15 +518,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@ -536,34 +536,34 @@ describe('FF visibility', () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
@ -572,34 +572,34 @@ describe('FF visibility', () => {
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
@ -609,7 +609,7 @@ describe('FF visibility', () => {
describe('AP', () => {
test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
}, alice);
@ -617,7 +617,7 @@ describe('FF visibility', () => {
assert.strictEqual(followingRes.status, 200);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
}, alice);
@ -625,7 +625,7 @@ describe('FF visibility', () => {
assert.strictEqual(followingRes.status, 403);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
}, alice);
@ -636,7 +636,7 @@ describe('FF visibility', () => {
test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => {
{
await api('/i/update', {
await api('i/update', {
followersVisibility: 'public',
}, alice);
@ -644,7 +644,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 200);
}
{
await api('/i/update', {
await api('i/update', {
followersVisibility: 'followers',
}, alice);
@ -652,7 +652,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 403);
}
{
await api('/i/update', {
await api('i/update', {
followersVisibility: 'private',
}, alice);

View file

@ -55,7 +55,7 @@ describe('Account Move', () => {
}, 1000 * 10);
test('Able to create an alias', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
@ -67,7 +67,7 @@ describe('Account Move', () => {
});
test('Able to create a local alias without hostname', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: ['@alice'],
}, bob);
@ -77,7 +77,7 @@ describe('Account Move', () => {
});
test('Able to create a local alias without @', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: ['alice'],
}, bob);
@ -87,7 +87,7 @@ describe('Account Move', () => {
});
test('Able to set remote user (but may fail)', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: ['@syuilo@example.com'],
}, bob);
@ -97,7 +97,7 @@ describe('Account Move', () => {
});
test('Unable to add duplicated aliases to alsoKnownAs', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`],
}, bob);
@ -107,7 +107,7 @@ describe('Account Move', () => {
});
test('Unable to add itself', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@bob@${url.hostname}`],
}, bob);
@ -117,7 +117,7 @@ describe('Account Move', () => {
});
test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
const res1 = await api('/i/update', {
const res1 = await api('i/update', {
alsoKnownAs: [`@nonexist@${url.hostname}`],
}, bob);
@ -125,7 +125,7 @@ describe('Account Move', () => {
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
const res2 = await api('/i/update', {
const res2 = await api('i/update', {
alsoKnownAs: ['@alice', 'nonexist'],
}, bob);
@ -135,7 +135,7 @@ describe('Account Move', () => {
});
test('Able to add two existing local account to alsoKnownAs', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`],
}, bob);
@ -146,10 +146,10 @@ describe('Account Move', () => {
});
test('Able to properly overwrite alsoKnownAs', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`],
}, bob);
@ -164,27 +164,27 @@ describe('Account Move', () => {
let antennaId = '';
beforeAll(async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, root);
const listRoot = await api('/users/lists/create', {
const listRoot = await api('users/lists/create', {
name: secureRndstr(8),
}, root);
await api('/users/lists/push', {
await api('users/lists/push', {
listId: listRoot.body.id,
userId: alice.id,
}, root);
await api('/following/create', {
await api('following/create', {
userId: root.id,
}, alice);
await api('/following/create', {
await api('following/create', {
userId: eve.id,
}, alice);
const antenna = await api('/antennas/create', {
const antenna = await api('antennas/create', {
name: secureRndstr(8),
src: 'home',
keywords: [secureRndstr(8)],
keywords: [[secureRndstr(8)]],
excludeKeywords: [],
users: [],
caseSensitive: false,
@ -195,48 +195,48 @@ describe('Account Move', () => {
}, alice);
antennaId = antenna.body.id;
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, carol);
await api('/mute/create', {
await api('mute/create', {
userId: alice.id,
}, dave);
await api('/blocking/create', {
await api('blocking/create', {
userId: alice.id,
}, dave);
await api('/following/create', {
await api('following/create', {
userId: eve.id,
}, dave);
await api('/following/create', {
await api('following/create', {
userId: dave.id,
}, eve);
const listEve = await api('/users/lists/create', {
const listEve = await api('users/lists/create', {
name: secureRndstr(8),
}, eve);
await api('/users/lists/push', {
await api('users/lists/push', {
listId: listEve.body.id,
userId: bob.id,
}, eve);
await api('/i/update', {
await api('i/update', {
isLocked: true,
}, frank);
await api('/following/create', {
await api('following/create', {
userId: frank.id,
}, alice);
await api('/following/requests/accept', {
await api('following/requests/accept', {
userId: alice.id,
}, frank);
}, 1000 * 10);
test('Prohibit the root account from moving', async () => {
const res = await api('/i/move', {
const res = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`,
}, root);
@ -246,7 +246,7 @@ describe('Account Move', () => {
});
test('Unable to move to a nonexisting local account', async () => {
const res = await api('/i/move', {
const res = await api('i/move', {
moveToAccount: `@nonexist@${url.hostname}`,
}, alice);
@ -256,7 +256,7 @@ describe('Account Move', () => {
});
test('Unable to move if alsoKnownAs is invalid', async () => {
const res = await api('/i/move', {
const res = await api('i/move', {
moveToAccount: `@carol@${url.hostname}`,
}, alice);
@ -266,7 +266,7 @@ describe('Account Move', () => {
});
test('Relationships have been properly migrated', async () => {
const move = await api('/i/move', {
const move = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`,
}, alice);
@ -275,13 +275,13 @@ describe('Account Move', () => {
await sleep(1000 * 3); // wait for jobs to finish
// Unfollow delayed?
const aliceFollowings = await api('/users/following', {
const aliceFollowings = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(aliceFollowings.status, 200);
assert.strictEqual(aliceFollowings.body.length, 3);
const carolFollowings = await api('/users/following', {
const carolFollowings = await api('users/following', {
userId: carol.id,
}, carol);
assert.strictEqual(carolFollowings.status, 200);
@ -289,25 +289,25 @@ describe('Account Move', () => {
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
const blockings = await api('/blocking/list', {}, dave);
const blockings = await api('blocking/list', {}, dave);
assert.strictEqual(blockings.status, 200);
assert.strictEqual(blockings.body.length, 2);
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
const mutings = await api('/mute/list', {}, dave);
const mutings = await api('mute/list', {}, dave);
assert.strictEqual(mutings.status, 200);
assert.strictEqual(mutings.body.length, 2);
assert.strictEqual(mutings.body[0].muteeId, bob.id);
assert.strictEqual(mutings.body[1].muteeId, alice.id);
const rootLists = await api('/users/lists/list', {}, root);
const rootLists = await api('users/lists/list', {}, root);
assert.strictEqual(rootLists.status, 200);
assert.strictEqual(rootLists.body[0].userIds.length, 2);
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
const eveLists = await api('/users/lists/list', {}, eve);
const eveLists = await api('users/lists/list', {}, eve);
assert.strictEqual(eveLists.status, 200);
assert.strictEqual(eveLists.body[0].userIds.length, 1);
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
@ -315,13 +315,13 @@ describe('Account Move', () => {
test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => {
await successfulApiCall({
endpoint: '/following/create',
endpoint: 'following/create',
parameters: {
userId: frank.id,
},
user: bob,
});
const followers = await api('/users/followers', {
const followers = await api('users/followers', {
userId: frank.id,
}, frank);
@ -333,7 +333,7 @@ describe('Account Move', () => {
test('Unfollowed after 10 sec (24 hours in production).', async () => {
await sleep(1000 * 8);
const following = await api('/users/following', {
const following = await api('users/following', {
userId: alice.id,
}, alice);
@ -342,7 +342,7 @@ describe('Account Move', () => {
});
test('Unable to move if the destination account has already moved.', async () => {
const res = await api('/i/move', {
const res = await api('i/move', {
moveToAccount: `@alice@${url.hostname}`,
}, bob);
@ -352,7 +352,7 @@ describe('Account Move', () => {
});
test('Follow and follower counts are properly adjusted', async () => {
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, eve);
const newAlice = await Users.findOneByOrFail({ id: alice.id });
@ -365,7 +365,7 @@ describe('Account Move', () => {
assert.strictEqual(newEve.followingCount, 1);
assert.strictEqual(newEve.followersCount, 1);
await api('/following/delete', {
await api('following/delete', {
userId: alice.id,
}, eve);
newEve = await Users.findOneByOrFail({ id: eve.id });
@ -374,49 +374,49 @@ describe('Account Move', () => {
});
test.each([
'/antennas/create',
'/channels/create',
'/channels/favorite',
'/channels/follow',
'/channels/unfavorite',
'/channels/unfollow',
'/clips/add-note',
'/clips/create',
'/clips/favorite',
'/clips/remove-note',
'/clips/unfavorite',
'/clips/update',
'/drive/files/upload-from-url',
'/flash/create',
'/flash/like',
'/flash/unlike',
'/flash/update',
'/following/create',
'/gallery/posts/create',
'/gallery/posts/like',
'/gallery/posts/unlike',
'/gallery/posts/update',
'/i/claim-achievement',
'/i/move',
'/i/import-blocking',
'/i/import-following',
'/i/import-muting',
'/i/import-user-lists',
'/i/pin',
'/mute/create',
'/notes/create',
'/notes/favorites/create',
'/notes/polls/vote',
'/notes/reactions/create',
'/pages/create',
'/pages/like',
'/pages/unlike',
'/pages/update',
'/renote-mute/create',
'/users/lists/create',
'/users/lists/pull',
'/users/lists/push',
])('Prohibit access after moving: %s', async (endpoint) => {
'antennas/create',
'channels/create',
'channels/favorite',
'channels/follow',
'channels/unfavorite',
'channels/unfollow',
'clips/add-note',
'clips/create',
'clips/favorite',
'clips/remove-note',
'clips/unfavorite',
'clips/update',
'drive/files/upload-from-url',
'flash/create',
'flash/like',
'flash/unlike',
'flash/update',
'following/create',
'gallery/posts/create',
'gallery/posts/like',
'gallery/posts/unlike',
'gallery/posts/update',
'i/claim-achievement',
'i/move',
'i/import-blocking',
'i/import-following',
'i/import-muting',
'i/import-user-lists',
'i/pin',
'mute/create',
'notes/create',
'notes/favorites/create',
'notes/polls/vote',
'notes/reactions/create',
'pages/create',
'pages/like',
'pages/unlike',
'pages/update',
'renote-mute/create',
'users/lists/create',
'users/lists/pull',
'users/lists/push',
] as const)('Prohibit access after moving: %s', async (endpoint) => {
const res = await api(endpoint, {}, alice);
assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
@ -424,11 +424,11 @@ describe('Account Move', () => {
});
test('Prohibit access after moving: /antennas/update', async () => {
const res = await api('/antennas/update', {
const res = await api('antennas/update', {
antennaId,
name: secureRndstr(8),
src: 'users',
keywords: [secureRndstr(8)],
keywords: [[secureRndstr(8)]],
excludeKeywords: [],
users: [eve.id],
caseSensitive: false,
@ -447,12 +447,12 @@ describe('Account Move', () => {
const res = await uploadFile(alice);
assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED');
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
});
test('Prohibit updating alsoKnownAs after moving', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@eve@${url.hostname}`],
}, alice);

View file

@ -19,21 +19,31 @@ describe('Mute', () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
// Mute: alice ==> carol
await api('mute/create', {
userId: carol.id,
}, alice);
}, 1000 * 60 * 2);
test('ミュート作成', async () => {
const res = await api('/mute/create', {
userId: carol.id,
const res = await api('mute/create', {
userId: bob.id,
}, alice);
assert.strictEqual(res.status, 204);
// 単体でも走らせられるように副作用消す
await api('mute/delete', {
userId: bob.id,
}, alice);
});
test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice hi' });
const carolNote = await post(carol, { text: '@alice hi' });
const res = await api('/notes/mentions', {}, alice);
const res = await api('notes/mentions', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -43,11 +53,11 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット
await api('/i/read-all-unread-notes', {}, alice);
await api('i/read-all-unread-notes', {}, alice);
await post(carol, { text: '@alice hi' });
const res = await api('/i', {}, alice);
const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
@ -55,7 +65,7 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
// 状態リセット
await api('/i/read-all-unread-notes', {}, alice);
await api('i/read-all-unread-notes', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention');
@ -64,8 +74,8 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット
await api('/i/read-all-unread-notes', {}, alice);
await api('/notifications/mark-all-as-read', {}, alice);
await api('i/read-all-unread-notes', {}, alice);
await api('notifications/mark-all-as-read', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
@ -78,7 +88,7 @@ describe('Mute', () => {
const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' });
const res = await api('/notes/local-timeline', {}, alice);
const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -94,7 +104,7 @@ describe('Mute', () => {
renoteId: carolNote.id,
});
const res = await api('/notes/local-timeline', {}, alice);
const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -110,19 +120,20 @@ describe('Mute', () => {
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
});
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
const aliceNote = await post(alice, { text: 'hi' });
await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id });
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -136,7 +147,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' });
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -150,7 +161,7 @@ describe('Mute', () => {
await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id });
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -164,7 +175,7 @@ describe('Mute', () => {
await post(bob, { renoteId: aliceNote.id });
await post(carol, { renoteId: aliceNote.id });
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -174,30 +185,36 @@ describe('Mute', () => {
});
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
await api('/i/follow', { userId: alice.id }, bob);
await api('/i/follow', { userId: alice.id }, carol);
await api('following/create', { userId: alice.id }, bob);
await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
});
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
await api('/i/update/', { isLocked: true }, alice);
await api('/following/create', { userId: alice.id }, bob);
await api('/following/create', { userId: alice.id }, carol);
await api('i/update', { isLocked: true }, alice);
await api('following/create', { userId: alice.id }, bob);
await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
});
});
@ -207,7 +224,7 @@ describe('Mute', () => {
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -219,7 +236,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -233,7 +250,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' });
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -247,7 +264,7 @@ describe('Mute', () => {
await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -261,7 +278,7 @@ describe('Mute', () => {
await post(bob, { renoteId: aliceNote.id });
await post(carol, { renoteId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -271,24 +288,27 @@ describe('Mute', () => {
});
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
await api('/i/follow', { userId: alice.id }, bob);
await api('/i/follow', { userId: alice.id }, carol);
await api('following/create', { userId: alice.id }, bob);
await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
});
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
await api('/i/update/', { isLocked: true }, alice);
await api('/following/create', { userId: alice.id }, bob);
await api('/following/create', { userId: alice.id }, carol);
await api('i/update', { isLocked: true }, alice);
await api('following/create', { userId: alice.id }, bob);
await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);

View file

@ -31,7 +31,7 @@ describe('Note', () => {
text: 'test',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -41,7 +41,7 @@ describe('Note', () => {
test('ファイルを添付できる', async () => {
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await api('/notes/create', {
const res = await api('notes/create', {
fileIds: [file.id],
}, alice);
@ -53,7 +53,7 @@ describe('Note', () => {
test('他人のファイルで怒られる', async () => {
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await api('/notes/create', {
const res = await api('notes/create', {
text: 'test',
fileIds: [file.id],
}, alice);
@ -64,7 +64,7 @@ describe('Note', () => {
}, 1000 * 10);
test('存在しないファイルで怒られる', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
text: 'test',
fileIds: ['000000000000000000000000'],
}, alice);
@ -75,7 +75,7 @@ describe('Note', () => {
});
test('不正なファイルIDで怒られる', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
fileIds: ['kyoppie'],
}, alice);
assert.strictEqual(res.status, 400);
@ -93,7 +93,7 @@ describe('Note', () => {
replyId: bobPost.id,
};
const res = await api('/notes/create', alicePost, alice);
const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -111,7 +111,7 @@ describe('Note', () => {
renoteId: bobPost.id,
};
const res = await api('/notes/create', alicePost, alice);
const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -129,7 +129,7 @@ describe('Note', () => {
renoteId: bobPost.id,
};
const res = await api('/notes/create', alicePost, alice);
const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -142,7 +142,7 @@ describe('Note', () => {
const bobPost = await post(bob, {
text: 'test',
});
const res = await api('/notes/create', {
const res = await api('notes/create', {
text: ' ',
renoteId: bobPost.id,
}, alice);
@ -152,7 +152,7 @@ describe('Note', () => {
});
test('visibility: followersでrenoteできる', async () => {
const createRes = await api('/notes/create', {
const createRes = await api('notes/create', {
text: 'test',
visibility: 'followers',
}, alice);
@ -160,7 +160,7 @@ describe('Note', () => {
assert.strictEqual(createRes.status, 200);
const renoteId = createRes.body.createdNote.id;
const renoteRes = await api('/notes/create', {
const renoteRes = await api('notes/create', {
visibility: 'followers',
renoteId,
}, alice);
@ -169,7 +169,7 @@ describe('Note', () => {
assert.strictEqual(renoteRes.body.createdNote.renoteId, renoteId);
assert.strictEqual(renoteRes.body.createdNote.visibility, 'followers');
const deleteRes = await api('/notes/delete', {
const deleteRes = await api('notes/delete', {
noteId: renoteRes.body.createdNote.id,
}, alice);
@ -177,11 +177,11 @@ describe('Note', () => {
});
test('visibility: followersなートに対してフォロワーはリプライできる', async () => {
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const aliceNote = await api('/notes/create', {
const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'followers',
}, alice);
@ -189,7 +189,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200);
const replyId = aliceNote.body.createdNote.id;
const bobReply = await api('/notes/create', {
const bobReply = await api('notes/create', {
text: 'reply to alice note',
replyId,
}, bob);
@ -197,20 +197,20 @@ describe('Note', () => {
assert.strictEqual(bobReply.status, 200);
assert.strictEqual(bobReply.body.createdNote.replyId, replyId);
await api('/following/delete', {
await api('following/delete', {
userId: alice.id,
}, bob);
});
test('visibility: followersなートに対してフォロワーでないユーザーがリプライしようとすると怒られる', async () => {
const aliceNote = await api('/notes/create', {
const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'followers',
}, alice);
assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', {
const bobReply = await api('notes/create', {
text: 'reply to alice note',
replyId: aliceNote.body.createdNote.id,
}, bob);
@ -220,7 +220,7 @@ describe('Note', () => {
});
test('visibility: specifiedなートに対してvisibility: specifiedで返信できる', async () => {
const aliceNote = await api('/notes/create', {
const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'specified',
visibleUserIds: [bob.id],
@ -228,7 +228,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', {
const bobReply = await api('notes/create', {
text: 'reply to alice note',
replyId: aliceNote.body.createdNote.id,
visibility: 'specified',
@ -239,7 +239,7 @@ describe('Note', () => {
});
test('visibility: specifiedなートに対してvisibility: follwersで返信しようとすると怒られる', async () => {
const aliceNote = await api('/notes/create', {
const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'specified',
visibleUserIds: [bob.id],
@ -247,7 +247,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', {
const bobReply = await api('notes/create', {
text: 'reply to alice note with visibility: followers',
replyId: aliceNote.body.createdNote.id,
visibility: 'followers',
@ -261,7 +261,7 @@ describe('Note', () => {
const post = {
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
});
@ -269,7 +269,7 @@ describe('Note', () => {
const post = {
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@ -278,7 +278,7 @@ describe('Note', () => {
text: 'test',
replyId: '000000000000000000000000',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@ -286,7 +286,7 @@ describe('Note', () => {
const post = {
renoteId: '000000000000000000000000',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@ -295,7 +295,7 @@ describe('Note', () => {
text: 'test',
replyId: 'foo',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@ -303,7 +303,7 @@ describe('Note', () => {
const post = {
renoteId: 'foo',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@ -312,7 +312,7 @@ describe('Note', () => {
text: '@ghost yo',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -324,7 +324,7 @@ describe('Note', () => {
text: '@bob @bob @bob yo',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -337,25 +337,25 @@ describe('Note', () => {
describe('添付ファイル情報', () => {
test('ファイルを添付した場合、投稿成功時にファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
const res = await api('/notes/create', {
fileIds: [file.body.id],
const res = await api('notes/create', {
fileIds: [file.body!.id],
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.files.length, 1);
assert.strictEqual(res.body.createdNote.files[0].id, file.body.id);
assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
});
test('ファイルを添付した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
const createdNote = await api('/notes/create', {
fileIds: [file.body.id],
const createdNote = await api('notes/create', {
fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
const res = await api('/notes', {
const res = await api('notes', {
withFiles: true,
}, alice);
@ -364,23 +364,23 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id);
assert.notEqual(myNote, null);
assert.strictEqual(myNote.files.length, 1);
assert.strictEqual(myNote.files[0].id, file.body.id);
assert.strictEqual(myNote.files[0].id, file.body!.id);
});
test('ファイルが添付されたノートをリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
const createdNote = await api('/notes/create', {
fileIds: [file.body.id],
const createdNote = await api('notes/create', {
fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
const renoted = await api('/notes/create', {
const renoted = await api('notes/create', {
renoteId: createdNote.body.createdNote.id,
}, alice);
assert.strictEqual(renoted.status, 200);
const res = await api('/notes', {
const res = await api('notes', {
renote: true,
}, alice);
@ -389,24 +389,24 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null);
assert.strictEqual(myNote.renote.files.length, 1);
assert.strictEqual(myNote.renote.files[0].id, file.body.id);
assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
});
test('ファイルが添付されたノートに返信した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
const createdNote = await api('/notes/create', {
fileIds: [file.body.id],
const createdNote = await api('notes/create', {
fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
const reply = await api('/notes/create', {
const reply = await api('notes/create', {
replyId: createdNote.body.createdNote.id,
text: 'this is reply',
}, alice);
assert.strictEqual(reply.status, 200);
const res = await api('/notes', {
const res = await api('notes', {
reply: true,
}, alice);
@ -415,29 +415,29 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
assert.notEqual(myNote, null);
assert.strictEqual(myNote.reply.files.length, 1);
assert.strictEqual(myNote.reply.files[0].id, file.body.id);
assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
});
test('ファイルが添付されたノートへの返信をリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
const createdNote = await api('/notes/create', {
fileIds: [file.body.id],
const createdNote = await api('notes/create', {
fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
const reply = await api('/notes/create', {
const reply = await api('notes/create', {
replyId: createdNote.body.createdNote.id,
text: 'this is reply',
}, alice);
assert.strictEqual(reply.status, 200);
const renoted = await api('/notes/create', {
const renoted = await api('notes/create', {
renoteId: reply.body.createdNote.id,
}, alice);
assert.strictEqual(renoted.status, 200);
const res = await api('/notes', {
const res = await api('notes', {
renote: true,
}, alice);
@ -446,7 +446,7 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null);
assert.strictEqual(myNote.renote.reply.files.length, 1);
assert.strictEqual(myNote.renote.reply.files[0].id, file.body.id);
assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
});
test('NSFWが強制されている場合変更できない', async () => {
@ -483,15 +483,15 @@ describe('Note', () => {
}, alice);
assert.strictEqual(assign.status, 204);
assert.strictEqual(file.body.isSensitive, false);
assert.strictEqual(file.body!.isSensitive, false);
const nsfwfile = await uploadFile(alice);
assert.strictEqual(nsfwfile.status, 200);
assert.strictEqual(nsfwfile.body.isSensitive, true);
assert.strictEqual(nsfwfile.body!.isSensitive, true);
const liftnsfw = await api('drive/files/update', {
fileId: nsfwfile.body.id,
fileId: nsfwfile.body!.id,
isSensitive: false,
}, alice);
@ -499,7 +499,7 @@ describe('Note', () => {
assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE');
const oldaddnsfw = await api('drive/files/update', {
fileId: file.body.id,
fileId: file.body!.id,
isSensitive: true,
}, alice);
@ -518,7 +518,7 @@ describe('Note', () => {
describe('notes/create', () => {
test('投票を添付できる', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
text: 'test',
poll: {
choices: ['foo', 'bar'],
@ -531,14 +531,15 @@ describe('Note', () => {
});
test('投票の選択肢が無くて怒られる', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
// @ts-expect-error poll must not be empty
poll: {},
}, alice);
assert.strictEqual(res.status, 400);
});
test('投票の選択肢が無くて怒られる (空の配列)', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
poll: {
choices: [],
},
@ -547,7 +548,7 @@ describe('Note', () => {
});
test('投票の選択肢が1つで怒られる', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
poll: {
choices: ['Strawberry Pasta'],
},
@ -556,14 +557,14 @@ describe('Note', () => {
});
test('投票できる', async () => {
const { body } = await api('/notes/create', {
const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
},
}, alice);
const res = await api('/notes/polls/vote', {
const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
@ -572,19 +573,19 @@ describe('Note', () => {
});
test('複数投票できない', async () => {
const { body } = await api('/notes/create', {
const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
},
}, alice);
await api('/notes/polls/vote', {
await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 0,
}, alice);
const res = await api('/notes/polls/vote', {
const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 2,
}, alice);
@ -593,7 +594,7 @@ describe('Note', () => {
});
test('許可されている場合は複数投票できる', async () => {
const { body } = await api('/notes/create', {
const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
@ -601,17 +602,17 @@ describe('Note', () => {
},
}, alice);
await api('/notes/polls/vote', {
await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 0,
}, alice);
await api('/notes/polls/vote', {
await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
const res = await api('/notes/polls/vote', {
const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 2,
}, alice);
@ -620,7 +621,7 @@ describe('Note', () => {
});
test('締め切られている場合は投票できない', async () => {
const { body } = await api('/notes/create', {
const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
@ -630,7 +631,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const res = await api('/notes/polls/vote', {
const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
@ -649,7 +650,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
const note1 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@ -666,7 +667,7 @@ describe('Note', () => {
assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', {
const note2 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@ -683,7 +684,7 @@ describe('Note', () => {
assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', {
const note2 = await api('notes/create', {
text: 'hogeTesthuge',
}, alice);
@ -702,7 +703,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
const note1 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@ -719,7 +720,7 @@ describe('Note', () => {
assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', {
const note2 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@ -736,7 +737,7 @@ describe('Note', () => {
assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', {
const note2 = await api('notes/create', {
text: 'hogeTesthuge',
}, alice);
@ -755,7 +756,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
const note1 = await api('notes/create', {
text: 'hogetesthuge',
}, tom);
@ -799,7 +800,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', {
const note = await api('notes/create', {
text: '@bob potentially annoying text',
}, alice);
@ -853,10 +854,10 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', {
const note = await api('notes/create', {
text: 'potentially annoying text',
visibility: 'specified',
visibleUserIds: [ bob.id ],
visibleUserIds: [bob.id],
}, alice);
assert.strictEqual(note.status, 400);
@ -909,10 +910,10 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', {
const note = await api('notes/create', {
text: '@bob potentially annoying text',
visibility: 'specified',
visibleUserIds: [ bob.id ],
visibleUserIds: [bob.id],
}, alice);
assert.strictEqual(note.status, 200);

View file

@ -22,7 +22,7 @@ describe('Renote Mute', () => {
}, 1000 * 60 * 2);
test('ミュート作成', async () => {
const res = await api('/renote-mute/create', {
const res = await api('renote-mute/create', {
userId: carol.id,
}, alice);
@ -37,7 +37,7 @@ describe('Renote Mute', () => {
// redisに追加されるのを待つ
await sleep(100);
const res = await api('/notes/local-timeline', {}, alice);
const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -54,7 +54,7 @@ describe('Renote Mute', () => {
// redisに追加されるのを待つ
await sleep(100);
const res = await api('/notes/local-timeline', {}, alice);
const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);

View file

@ -601,7 +601,7 @@ describe('Streaming', () => {
// #10443
test('ミュートしているサーバのートがリストTLに流れない', async () => {
await api('/i/update', {
await api('i/update', {
mutedInstances: ['example.com'],
}, chitose);
@ -618,7 +618,7 @@ describe('Streaming', () => {
// #10443
test('ミュートしているサーバのートに対するリプライがリストTLに流れない', async () => {
await api('/i/update', {
await api('i/update', {
mutedInstances: ['example.com'],
}, chitose);
@ -635,7 +635,7 @@ describe('Streaming', () => {
// #10443
test('ミュートしているサーバのートに対するリートがリストTLに流れない', async () => {
await api('/i/update', {
await api('i/update', {
mutedInstances: ['example.com'],
}, chitose);

View file

@ -24,12 +24,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await api('/notes/mentions', {}, alice);
const res = await api('notes/mentions', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -40,15 +40,15 @@ describe('Note thread mute', () => {
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット
await api('/i/read-all-unread-notes', {}, alice);
await api('i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const res = await api('/i', {}, alice);
const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
@ -56,11 +56,11 @@ describe('Note thread mute', () => {
test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise<void>(async done => {
// 状態リセット
await api('/i/read-all-unread-notes', {}, alice);
await api('i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
let fired = false;
@ -84,12 +84,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);

File diff suppressed because it is too large Load diff

View file

@ -11,9 +11,9 @@ import type * as misskey from 'misskey-js';
describe('users/notes', () => {
let alice: misskey.entities.SignupResponse;
let jpgNote: any;
let pngNote: any;
let jpgPngNote: any;
let jpgNote: misskey.entities.Note;
let pngNote: misskey.entities.Note;
let jpgPngNote: misskey.entities.Note;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
@ -31,7 +31,7 @@ describe('users/notes', () => {
}, 1000 * 60 * 2);
test('withFiles', async () => {
const res = await api('/users/notes', {
const res = await api('users/notes', {
userId: alice.id,
withFiles: true,
}, alice);

View file

@ -8,7 +8,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { inspect } from 'node:util';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { api, page, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('ユーザー', () => {
@ -24,31 +24,12 @@ describe('ユーザー', () => {
}, {});
};
// BUG misskey-jsとjson-schemaと実際に返ってくるデータが全部違う
type UserLite = misskey.entities.UserLite & {
badgeRoles: any[],
};
type UserDetailedNotMe = UserLite &
misskey.entities.UserDetailed & {
roles: any[],
};
type MeDetailed = UserDetailedNotMe &
misskey.entities.MeDetailed & {
achievements: object[],
loggedInDays: number,
policies: object,
};
type User = MeDetailed & { token: string };
const show = async (id: string, me = root): Promise<MeDetailed | UserDetailedNotMe> => {
return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any;
const show = async (id: string, me = root): Promise<misskey.entities.UserDetailed> => {
return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me });
};
// UserLiteのキーが過不足なく入っている
const userLite = (user: User): Partial<UserLite> => {
const userLite = (user: misskey.entities.UserLite): Partial<misskey.entities.UserLite> => {
return stripUndefined({
id: user.id,
name: user.name,
@ -71,7 +52,7 @@ describe('ユーザー', () => {
};
// UserDetailedNotMeのキーが過不足なく入っている
const userDetailedNotMe = (user: User): Partial<UserDetailedNotMe> => {
const userDetailedNotMe = (user: misskey.entities.SignupResponse): Partial<misskey.entities.UserDetailedNotMe> => {
return stripUndefined({
...userLite(user),
url: user.url,
@ -111,7 +92,7 @@ describe('ユーザー', () => {
};
// Relations関連のキーが過不足なく入っている
const userDetailedNotMeWithRelations = (user: User): Partial<UserDetailedNotMe> => {
const userDetailedNotMeWithRelations = (user: misskey.entities.SignupResponse): Partial<misskey.entities.UserDetailedNotMe> => {
return stripUndefined({
...userDetailedNotMe(user),
isFollowing: user.isFollowing ?? false,
@ -128,7 +109,7 @@ describe('ユーザー', () => {
};
// MeDetailedのキーが過不足なく入っている
const meDetailed = (user: User, security = false): Partial<MeDetailed> => {
const meDetailed = (user: misskey.entities.SignupResponse, security = false): Partial<misskey.entities.MeDetailed> => {
return stripUndefined({
...userDetailedNotMe(user),
avatarId: user.avatarId,
@ -159,6 +140,7 @@ describe('ユーザー', () => {
mutedWords: user.mutedWords,
hardMutedWords: user.hardMutedWords,
mutedInstances: user.mutedInstances,
// @ts-expect-error 後方互換性
mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig,
emailNotificationTypes: user.emailNotificationTypes,
@ -173,61 +155,53 @@ describe('ユーザー', () => {
});
};
let root: User;
let alice: User;
let root: misskey.entities.SignupResponse;
let alice: misskey.entities.SignupResponse;
let aliceNote: misskey.entities.Note;
let alicePage: misskey.entities.Page;
let aliceList: misskey.entities.UserList;
let bob: User;
let bobNote: misskey.entities.Note;
let bob: misskey.entities.SignupResponse;
let carol: User;
let dave: User;
let ellen: User;
let frank: User;
// NOTE: これがないと落ちるbob の updatedAt が null になってしまうため?)
let bobNote: misskey.entities.Note; // eslint-disable-line @typescript-eslint/no-unused-vars
let usersReplying: User[];
let carol: misskey.entities.SignupResponse;
let userNoNote: User;
let userNotExplorable: User;
let userLocking: User;
let userAdmin: User;
let roleAdmin: any;
let userModerator: User;
let roleModerator: any;
let userRolePublic: User;
let rolePublic: any;
let userRoleBadge: User;
let roleBadge: any;
let userSilenced: User;
let roleSilenced: any;
let userSuspended: User;
let userDeletedBySelf: User;
let userDeletedByAdmin: User;
let userFollowingAlice: User;
let userFollowedByAlice: User;
let userBlockingAlice: User;
let userBlockedByAlice: User;
let userMutingAlice: User;
let userMutedByAlice: User;
let userRnMutingAlice: User;
let userRnMutedByAlice: User;
let userFollowRequesting: User;
let userFollowRequested: User;
let usersReplying: misskey.entities.SignupResponse[];
let userNoNote: misskey.entities.SignupResponse;
let userNotExplorable: misskey.entities.SignupResponse;
let userLocking: misskey.entities.SignupResponse;
let userAdmin: misskey.entities.SignupResponse;
let roleAdmin: misskey.entities.Role;
let userModerator: misskey.entities.SignupResponse;
let roleModerator: misskey.entities.Role;
let userRolePublic: misskey.entities.SignupResponse;
let rolePublic: misskey.entities.Role;
let userRoleBadge: misskey.entities.SignupResponse;
let roleBadge: misskey.entities.Role;
let userSilenced: misskey.entities.SignupResponse;
let roleSilenced: misskey.entities.Role;
let userSuspended: misskey.entities.SignupResponse;
let userDeletedBySelf: misskey.entities.SignupResponse;
let userDeletedByAdmin: misskey.entities.SignupResponse;
let userFollowingAlice: misskey.entities.SignupResponse;
let userFollowedByAlice: misskey.entities.SignupResponse;
let userBlockingAlice: misskey.entities.SignupResponse;
let userBlockedByAlice: misskey.entities.SignupResponse;
let userMutingAlice: misskey.entities.SignupResponse;
let userMutedByAlice: misskey.entities.SignupResponse;
let userRnMutingAlice: misskey.entities.SignupResponse;
let userRnMutedByAlice: misskey.entities.SignupResponse;
let userFollowRequesting: misskey.entities.SignupResponse;
let userFollowRequested: misskey.entities.SignupResponse;
beforeAll(async () => {
root = await signup({ username: 'root' });
alice = await signup({ username: 'alice' });
aliceNote = await post(alice, { text: 'test' }) as any;
alicePage = await page(alice);
aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body;
aliceNote = await post(alice, { text: 'test' });
bob = await signup({ username: 'bob' });
bobNote = await post(bob, { text: 'test' }) as any;
bobNote = await post(bob, { text: 'test' });
carol = await signup({ username: 'carol' });
dave = await signup({ username: 'dave' });
ellen = await signup({ username: 'ellen' });
frank = await signup({ username: 'frank' });
// @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする
usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => {
@ -238,7 +212,7 @@ describe('ユーザー', () => {
}
return (await acc).concat(u);
}, Promise.resolve([] as User[]));
}, Promise.resolve([] as misskey.entities.SignupResponse[]));
userNoNote = await signup({ username: 'userNoNote' });
userNotExplorable = await signup({ username: 'userNotExplorable' });
@ -306,7 +280,7 @@ describe('ユーザー', () => {
beforeEach(async () => {
alice = {
...alice,
...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }) as any,
...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }),
};
aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice });
});
@ -319,7 +293,7 @@ describe('ユーザー', () => {
endpoint: 'signup',
parameters: { username: 'zoe', password: 'password' },
user: undefined,
}) as unknown as User; // BUG MeDetailedに足りないキーがある
}) as unknown as misskey.entities.SignupResponse; // BUG MeDetailedに足りないキーがある
// signupの時はtokenが含まれる特別なMeDetailedが返ってくる
assert.match(response.token, /[a-zA-Z0-9]{16}/);
@ -329,7 +303,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.name, null);
assert.strictEqual(response.username, 'zoe');
assert.strictEqual(response.host, null);
assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
response.avatarUrl && assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.strictEqual(response.avatarBlurhash, null);
assert.deepStrictEqual(response.avatarDecorations, []);
assert.strictEqual(response.isBot, false);
@ -401,6 +375,7 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.unreadAnnouncements, []);
assert.deepStrictEqual(response.mutedWords, []);
assert.deepStrictEqual(response.mutedInstances, []);
// @ts-expect-error 後方互換のため
assert.deepStrictEqual(response.mutingNotificationTypes, []);
assert.deepStrictEqual(response.notificationRecieveConfig, {});
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
@ -430,66 +405,66 @@ describe('ユーザー', () => {
//#region 自分の情報の更新(i/update)
test.each([
{ parameters: (): object => ({ name: null }) },
{ parameters: (): object => ({ name: 'x'.repeat(50) }) },
{ parameters: (): object => ({ name: 'x' }) },
{ parameters: (): object => ({ name: 'My name' }) },
{ parameters: (): object => ({ description: null }) },
{ parameters: (): object => ({ description: 'x'.repeat(1500) }) },
{ parameters: (): object => ({ description: 'x' }) },
{ parameters: (): object => ({ description: 'My description' }) },
{ parameters: (): object => ({ location: null }) },
{ parameters: (): object => ({ location: 'x'.repeat(50) }) },
{ parameters: (): object => ({ location: 'x' }) },
{ parameters: (): object => ({ location: 'My location' }) },
{ parameters: (): object => ({ birthday: '0000-00-00' }) },
{ parameters: (): object => ({ birthday: '9999-99-99' }) },
{ parameters: (): object => ({ lang: 'en-US' }) },
{ parameters: (): object => ({ fields: [] }) },
{ parameters: (): object => ({ fields: [{ name: 'x', value: 'x' }] }) },
{ parameters: (): object => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない
{ parameters: (): object => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) },
{ parameters: (): object => ({ isLocked: true }) },
{ parameters: (): object => ({ isLocked: false }) },
{ parameters: (): object => ({ isExplorable: false }) },
{ parameters: (): object => ({ isExplorable: true }) },
{ parameters: (): object => ({ hideOnlineStatus: true }) },
{ parameters: (): object => ({ hideOnlineStatus: false }) },
{ parameters: (): object => ({ publicReactions: false }) },
{ parameters: (): object => ({ publicReactions: true }) },
{ parameters: (): object => ({ autoAcceptFollowed: true }) },
{ parameters: (): object => ({ autoAcceptFollowed: false }) },
{ parameters: (): object => ({ noCrawle: true }) },
{ parameters: (): object => ({ noCrawle: false }) },
{ parameters: (): object => ({ preventAiLearning: false }) },
{ parameters: (): object => ({ preventAiLearning: true }) },
{ parameters: (): object => ({ isBot: true }) },
{ parameters: (): object => ({ isBot: false }) },
{ parameters: (): object => ({ isCat: true }) },
{ parameters: (): object => ({ isCat: false }) },
{ parameters: (): object => ({ injectFeaturedNote: true }) },
{ parameters: (): object => ({ injectFeaturedNote: false }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: true }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: false }) },
{ parameters: (): object => ({ alwaysMarkNsfw: true }) },
{ parameters: (): object => ({ alwaysMarkNsfw: false }) },
{ parameters: (): object => ({ autoSensitive: true }) },
{ parameters: (): object => ({ autoSensitive: false }) },
{ parameters: (): object => ({ followingVisibility: 'private' }) },
{ parameters: (): object => ({ followingVisibility: 'followers' }) },
{ parameters: (): object => ({ followingVisibility: 'public' }) },
{ parameters: (): object => ({ followersVisibility: 'private' }) },
{ parameters: (): object => ({ followersVisibility: 'followers' }) },
{ parameters: (): object => ({ followersVisibility: 'public' }) },
{ parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
{ parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) },
{ parameters: (): object => ({ mutedWords: [] }) },
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: (): object => ({ mutedInstances: [] }) },
{ parameters: (): object => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
{ parameters: (): object => ({ notificationRecieveConfig: {} }) },
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
{ parameters: () => ({ name: null }) },
{ parameters: () => ({ name: 'x'.repeat(50) }) },
{ parameters: () => ({ name: 'x' }) },
{ parameters: () => ({ name: 'My name' }) },
{ parameters: () => ({ description: null }) },
{ parameters: () => ({ description: 'x'.repeat(1500) }) },
{ parameters: () => ({ description: 'x' }) },
{ parameters: () => ({ description: 'My description' }) },
{ parameters: () => ({ location: null }) },
{ parameters: () => ({ location: 'x'.repeat(50) }) },
{ parameters: () => ({ location: 'x' }) },
{ parameters: () => ({ location: 'My location' }) },
{ parameters: () => ({ birthday: '0000-00-00' }) },
{ parameters: () => ({ birthday: '9999-99-99' }) },
{ parameters: () => ({ lang: 'en-US' as const }) },
{ parameters: () => ({ fields: [] }) },
{ parameters: () => ({ fields: [{ name: 'x', value: 'x' }] }) },
{ parameters: () => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない
{ parameters: () => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) },
{ parameters: () => ({ isLocked: true }) },
{ parameters: () => ({ isLocked: false }) },
{ parameters: () => ({ isExplorable: false }) },
{ parameters: () => ({ isExplorable: true }) },
{ parameters: () => ({ hideOnlineStatus: true }) },
{ parameters: () => ({ hideOnlineStatus: false }) },
{ parameters: () => ({ publicReactions: false }) },
{ parameters: () => ({ publicReactions: true }) },
{ parameters: () => ({ autoAcceptFollowed: true }) },
{ parameters: () => ({ autoAcceptFollowed: false }) },
{ parameters: () => ({ noCrawle: true }) },
{ parameters: () => ({ noCrawle: false }) },
{ parameters: () => ({ preventAiLearning: false }) },
{ parameters: () => ({ preventAiLearning: true }) },
{ parameters: () => ({ isBot: true }) },
{ parameters: () => ({ isBot: false }) },
{ parameters: () => ({ isCat: true }) },
{ parameters: () => ({ isCat: false }) },
{ parameters: () => ({ injectFeaturedNote: true }) },
{ parameters: () => ({ injectFeaturedNote: false }) },
{ parameters: () => ({ receiveAnnouncementEmail: true }) },
{ parameters: () => ({ receiveAnnouncementEmail: false }) },
{ parameters: () => ({ alwaysMarkNsfw: true }) },
{ parameters: () => ({ alwaysMarkNsfw: false }) },
{ parameters: () => ({ autoSensitive: true }) },
{ parameters: () => ({ autoSensitive: false }) },
{ parameters: () => ({ followingVisibility: 'private' as const }) },
{ parameters: () => ({ followingVisibility: 'followers' as const }) },
{ parameters: () => ({ followingVisibility: 'public' as const }) },
{ parameters: () => ({ followersVisibility: 'private' as const }) },
{ parameters: () => ({ followersVisibility: 'followers' as const }) },
{ parameters: () => ({ followersVisibility: 'public' as const }) },
{ parameters: () => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
{ parameters: () => ({ mutedWords: [['x'.repeat(194)]] }) },
{ parameters: () => ({ mutedWords: [] }) },
{ parameters: () => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: () => ({ mutedInstances: [] }) },
{ parameters: () => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
{ parameters: () => ({ notificationRecieveConfig: {} }) },
{ parameters: () => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: () => ({ emailNotificationTypes: [] }) },
] as const)('を書き換えることができる($#)', async ({ parameters }) => {
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice });
const expected = { ...meDetailed(alice, true), ...parameters() };
@ -498,13 +473,13 @@ describe('ユーザー', () => {
test('を書き換えることができる(Avatar)', async () => {
const aliceFile = (await uploadFile(alice)).body;
const parameters = { avatarId: aliceFile.id };
const parameters = { avatarId: aliceFile!.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/);
const expected = {
...meDetailed(alice, true),
avatarId: aliceFile.id,
avatarId: aliceFile!.id,
avatarBlurhash: response.avatarBlurhash,
avatarUrl: response.avatarUrl,
};
@ -523,13 +498,13 @@ describe('ユーザー', () => {
test('を書き換えることができる(Banner)', async () => {
const aliceFile = (await uploadFile(alice)).body;
const parameters = { bannerId: aliceFile.id };
const parameters = { bannerId: aliceFile!.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/);
const expected = {
...meDetailed(alice, true),
bannerId: aliceFile.id,
bannerId: aliceFile!.id,
bannerBlurhash: response.bannerBlurhash,
bannerUrl: response.bannerUrl,
};
@ -579,13 +554,13 @@ describe('ユーザー', () => {
//#region ユーザー(users)
test.each([
{ label: 'ID昇順', parameters: { limit: 5 }, selector: (u: UserLite): string => u.id },
{ label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
{ label: 'ID昇順', parameters: { limit: 5 }, selector: (u: misskey.entities.UserLite): string => u.id },
{ label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
] as const)('をリスト形式で取得することができる($label', async ({ parameters, selector }) => {
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
@ -598,15 +573,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれない', user: (): User => userNotExplorable, excluded: true },
{ label: 'ミュートユーザーが含まれない', user: (): User => userMutedByAlice, excluded: true },
{ label: 'ブロックされているユーザーが含まれない', user: (): User => userBlockedByAlice, excluded: true },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
{ label: '「見つけやすくする」がOFFのユーザーが含まれない', user: () => userNotExplorable, excluded: true },
{ label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true },
{ label: 'ブロックされているユーザーが含まれない', user: () => userBlockedByAlice, excluded: true },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => {
const parameters = { limit: 100 };
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
@ -620,39 +595,44 @@ describe('ユーザー', () => {
//#region ユーザー情報(users/show)
test.each([
{ label: 'ID指定で自分自身を', parameters: (): object => ({ userId: alice.id }), user: (): User => alice, type: meDetailed },
{ label: 'ID指定で他人を', parameters: (): object => ({ userId: alice.id }), user: (): User => bob, type: userDetailedNotMeWithRelations },
{ label: 'ID指定かつ未認証', parameters: (): object => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe },
{ label: '@指定で自分自身を', parameters: (): object => ({ username: alice.username }), user: (): User => alice, type: meDetailed },
{ label: '@指定で他人を', parameters: (): object => ({ username: alice.username }), user: (): User => bob, type: userDetailedNotMeWithRelations },
{ label: '@指定かつ未認証', parameters: (): object => ({ username: alice.username }), user: undefined, type: userDetailedNotMe },
{ label: 'ID指定で自分自身を', parameters: () => ({ userId: alice.id }), user: () => alice, type: meDetailed },
{ label: 'ID指定で他人を', parameters: () => ({ userId: alice.id }), user: () => bob, type: userDetailedNotMeWithRelations },
{ label: 'ID指定かつ未認証', parameters: () => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe },
{ label: '@指定で自分自身を', parameters: () => ({ username: alice.username }), user: () => alice, type: meDetailed },
{ label: '@指定で他人を', parameters: () => ({ username: alice.username }), user: () => bob, type: userDetailedNotMeWithRelations },
{ label: '@指定かつ未認証', parameters: () => ({ username: alice.username }), user: undefined, type: userDetailedNotMe },
] as const)('を取得することができる($label', async ({ parameters, user, type }) => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() });
const expected = type(alice);
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: 'Administratorになっている', user: (): User => userAdmin, me: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin },
{ label: '自分以外から見たときはAdministratorか判定できない', user: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin, expected: (): undefined => undefined },
{ label: 'Moderatorになっている', user: (): User => userModerator, me: (): User => userModerator, selector: (user: User): unknown => user.isModerator },
{ label: '自分以外から見たときはModeratorか判定できない', user: (): User => userModerator, selector: (user: User): unknown => user.isModerator, expected: (): undefined => undefined },
{ label: 'サイレンスになっている', user: (): User => userSilenced, selector: (user: User): unknown => user.isSilenced },
//{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended },
{ label: '削除済みになっている', user: (): User => userDeletedBySelf, me: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted },
{ label: '自分以外から見たときは削除済みか判定できない', user: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined },
{ label: '削除済み(byAdmin)になっている', user: (): User => userDeletedByAdmin, me: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted },
{ label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined },
{ label: 'フォロー中になっている', user: (): User => userFollowedByAlice, selector: (user: User): unknown => user.isFollowing },
{ label: 'フォローされている', user: (): User => userFollowingAlice, selector: (user: User): unknown => user.isFollowed },
{ label: 'ブロック中になっている', user: (): User => userBlockedByAlice, selector: (user: User): unknown => user.isBlocking },
{ label: 'ブロックされている', user: (): User => userBlockingAlice, selector: (user: User): unknown => user.isBlocked },
{ label: 'ミュート中になっている', user: (): User => userMutedByAlice, selector: (user: User): unknown => user.isMuted },
{ label: 'リノートミュート中になっている', user: (): User => userRnMutedByAlice, selector: (user: User): unknown => user.isRenoteMuted },
{ label: 'フォローリクエスト中になっている', user: (): User => userFollowRequested, me: (): User => userFollowRequesting, selector: (user: User): unknown => user.hasPendingFollowRequestFromYou },
{ label: 'フォローリクエストされている', user: (): User => userFollowRequesting, me: (): User => userFollowRequested, selector: (user: User): unknown => user.hasPendingFollowRequestToYou },
{ label: 'Administratorになっている', user: () => userAdmin, me: () => userAdmin, selector: (user: misskey.entities.MeDetailed) => user.isAdmin },
// @ts-expect-error UserDetailedNotMe doesn't include isAdmin
{ label: '自分以外から見たときはAdministratorか判定できない', user: () => userAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isAdmin, expected: () => undefined },
{ label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
// @ts-expect-error UserDetailedNotMe doesn't include isModerator
{ label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined },
{ label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
// FIXME: 落ちる
//{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },
{ label: '削除済みになっている', user: () => userDeletedBySelf, me: () => userDeletedBySelf, selector: (user: misskey.entities.MeDetailed) => user.isDeleted },
// @ts-expect-error UserDetailedNotMe doesn't include isDeleted
{ label: '自分以外から見たときは削除済みか判定できない', user: () => userDeletedBySelf, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined },
{ label: '削除済み(byAdmin)になっている', user: () => userDeletedByAdmin, me: () => userDeletedByAdmin, selector: (user: misskey.entities.MeDetailed) => user.isDeleted },
// @ts-expect-error UserDetailedNotMe doesn't include isDeleted
{ label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: () => userDeletedByAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined },
{ label: 'フォロー中になっている', user: () => userFollowedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowing },
{ label: 'フォローされている', user: () => userFollowingAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowed },
{ label: 'ブロック中になっている', user: () => userBlockedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocking },
{ label: 'ブロックされている', user: () => userBlockingAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocked },
{ label: 'ミュート中になっている', user: () => userMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isMuted },
{ label: 'リノートミュート中になっている', user: () => userRnMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isRenoteMuted },
{ label: 'フォローリクエスト中になっている', user: () => userFollowRequested, me: () => userFollowRequesting, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestFromYou },
{ label: 'フォローリクエストされている', user: () => userFollowRequesting, me: () => userFollowRequested, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestToYou },
] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice });
assert.strictEqual(selector(response), (expected ?? ((): true => true))());
assert.strictEqual(selector(response as any), (expected ?? ((): true => true))());
});
test('を取得することができ、Publicなロールがセットされていること', async () => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice });
@ -694,17 +674,18 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: (): User => userSuspended, me: (): User => root },
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: () => userSuspended, me: () => root },
// BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる
//{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
//{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: () => userSuspended, me: () => bob, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
// @ts-expect-error excluded は上でコメントアウトされているので
] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => {
const parameters = { userIds: [user().id] };
const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice });
@ -729,15 +710,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => {
const parameters = { query: user().username, limit: 1 };
const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
@ -751,30 +732,30 @@ describe('ユーザー', () => {
//#region ID指定検索(users/search-by-username-and-host)
test.each([
{ label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] },
{ label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] },
{ label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] },
{ label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: (): User[] => [] },
{ label: 'ローカルの他人1', parameters: { username: 'bob' }, user: (): User[] => [bob] },
{ label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: (): User[] => [bob] },
{ label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: (): User[] => [bob] },
{ label: 'ローカル', parameters: { host: null, limit: 1 }, user: (): User[] => [userFollowedByAlice] },
{ label: 'ローカル', parameters: { host: '.', limit: 1 }, user: (): User[] => [userFollowedByAlice] },
{ label: '自分', parameters: { username: 'alice' }, user: () => [alice] },
{ label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: () => [alice] },
{ label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: () => [userFollowedByAlice] },
{ label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: () => [] },
{ label: 'ローカルの他人1', parameters: { username: 'bob' }, user: () => [bob] },
{ label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: () => [bob] },
{ label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: () => [bob] },
{ label: 'ローカル', parameters: { host: null, limit: 1 }, user: () => [userFollowedByAlice] },
{ label: 'ローカル', parameters: { host: '.', limit: 1 }, user: () => [userFollowedByAlice] },
])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => {
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
const expected = await Promise.all(user().map(u => show(u.id, alice)));
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => {
const parameters = { username: user().username };
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
@ -796,15 +777,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれない', user: (): User => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
//{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれない', user: () => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
//{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => {
const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0];
await post(alice, { text: `@${user().username} test`, replyId: replyTo.id });
@ -818,12 +799,12 @@ describe('ユーザー', () => {
//#region ハッシュタグ(hashtags/users)
test.each([
{ label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
{ label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => {
const hashtag = 'test_hashtag';
await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice });
@ -837,15 +818,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => {
const hashtag = `user_test${user().username}`;
if (user() !== userSuspended) {

View file

@ -51,7 +51,7 @@ describe('AnnouncementService', () => {
function createAnnouncement(data: Partial<MiAnnouncement & { createdAt: Date }> = {}) {
return announcementsRepository.insert({
id: genAidx(data.createdAt ?? new Date()),
id: genAidx(data.createdAt?.getTime() ?? Date.now()),
updatedAt: null,
title: 'Title',
text: 'Text',

View file

@ -19,8 +19,8 @@ import { DI } from '@/di-symbols.js';
import type { TestingModule } from '@nestjs/testing';
function mockRedis() {
const hash = {};
const set = jest.fn((key, value) => {
const hash = {} as any;
const set = jest.fn((key: string, value) => {
const ret = hash[key];
hash[key] = value;
return ret;
@ -56,12 +56,13 @@ describe('FetchInstanceMetadataService', () => {
} else if (token === DI.redis) {
return mockRedis;
}
return null;
})
.compile();
app.enableShutdownHooks();
fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService);
fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService) as jest.Mocked<FetchInstanceMetadataService>;
federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService) as jest.Mocked<FederatedInstanceService>;
redisClient = app.get<Redis>(DI.redis) as jest.Mocked<Redis>;
httpRequestService = app.get<HttpRequestService>(HttpRequestService) as jest.Mocked<HttpRequestService>;
@ -74,11 +75,12 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and update', async () => {
redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } });
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
@ -88,11 +90,12 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and don\'t update', async () => {
redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now } });
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
@ -101,15 +104,33 @@ describe('FetchInstanceMetadataService', () => {
test('Do nothing when lock not acquired', async () => {
redisClient.set = mockRedis();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } });
const now = Date.now();
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.tryLock('example.com');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
expect(tryLockSpy).toHaveBeenCalledTimes(2);
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(0);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
});
test('Do when lock not acquired but forced', async () => {
redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
expect(tryLockSpy).toHaveBeenCalledTimes(0);
expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalled();
});
});

View file

@ -228,11 +228,14 @@ describe('RoleService', () => {
},
target: 'conditional',
condFormula: {
id: '232a4221-9816-49a6-a967-ae0fac52ec5e',
type: 'and',
values: [{
id: '2a37ef43-2d93-4c4d-87f6-f2fdb7d9b530',
type: 'followersMoreThanOrEq',
value: 10,
}, {
id: '1bd67839-b126-4f92-bad0-4e285dab453b',
type: 'createdMoreThan',
sec: 60 * 60 * 24 * 7,
}],

View file

@ -9,11 +9,10 @@ import { basename, isAbsolute } from 'node:path';
import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit } from 'node-fetch';
import fetch, { File, RequestInit, type Headers } from 'node-fetch';
import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { Packed } from '@/misc/json-schema.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js';
@ -21,7 +20,7 @@ import type * as misskey from 'misskey-js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
interface UserToken {
export interface UserToken {
token: string;
bearer?: boolean;
}
@ -35,20 +34,15 @@ export const cookie = (me: UserToken): string => {
return `token=${me.token};`;
};
export const api = async (endpoint: string, params: any, me?: UserToken) => {
const normalized = endpoint.replace(/^\//, '');
return await request(`api/${normalized}`, params, me);
};
export type ApiRequest = {
endpoint: string,
parameters: object,
export type ApiRequest<E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req'] = misskey.Endpoints[E]['req']> = {
endpoint: E,
parameters: P,
user: UserToken | undefined,
};
export const successfulApiCall = async <T, >(request: ApiRequest, assertion: {
export const successfulApiCall = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status?: number,
} = {}): Promise<T> => {
} = {}): Promise<misskey.api.SwitchCaseResponseType<E, P>> => {
const { endpoint, parameters, user } = request;
const res = await api(endpoint, parameters, user);
const status = assertion.status ?? (res.body == null ? 204 : 200);
@ -56,7 +50,7 @@ export const successfulApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body;
};
export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
export const failedApiCall = async <T, E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status: number,
code: string,
id: string
@ -70,7 +64,7 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body;
};
const request = async (path: string, params: any, me?: UserToken): Promise<{
export const api = async <E extends keyof misskey.Endpoints>(path: E, params: misskey.Endpoints[E]['req'], me?: UserToken): Promise<{
status: number,
headers: Headers,
body: any
@ -86,7 +80,7 @@ const request = async (path: string, params: any, me?: UserToken): Promise<{
bodyAuth.i = me.token;
}
const res = await relativeFetch(path, {
const res = await relativeFetch(`api/${path}`, {
method: 'POST',
headers,
body: JSON.stringify(Object.assign(bodyAuth, params)),
@ -141,7 +135,7 @@ export const signup = async (params?: Partial<misskey.Endpoints['signup']['req']
return res.body;
};
export const post = async (user: UserToken, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
export const post = async (user: UserToken, params: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
const q = params;
const res = await api('notes/create', q, user);
@ -159,8 +153,8 @@ export const createAppToken = async (user: UserToken, permissions: (typeof missk
};
// 非公開ートをAPI越しに見たときのート NoteEntityService.ts
export const hiddenNote = (note: any): any => {
const temp = {
export const hiddenNote = (note: misskey.entities.Note): misskey.entities.Note => {
const temp: misskey.entities.Note = {
...note,
fileIds: [],
files: [],
@ -173,21 +167,22 @@ export const hiddenNote = (note: any): any => {
return temp;
};
export const react = async (user: UserToken, note: any, reaction: string): Promise<any> => {
export const react = async (user: UserToken, note: misskey.entities.Note, reaction: string): Promise<void> => {
await api('notes/reactions/create', {
noteId: note.id,
reaction: reaction,
}, user);
};
export const userList = async (user: UserToken, userList: any = {}): Promise<any> => {
export const userList = async (user: UserToken, userList: Partial<misskey.entities.UserList> = {}): Promise<misskey.entities.UserList> => {
const res = await api('users/lists/create', {
name: 'test',
...userList,
}, user);
return res.body;
};
export const page = async (user: UserToken, page: any = {}): Promise<any> => {
export const page = async (user: UserToken, page: Partial<misskey.entities.Page> = {}): Promise<misskey.entities.Page> => {
const res = await api('pages/create', {
alignCenter: false,
content: [
@ -198,7 +193,7 @@ export const page = async (user: UserToken, page: any = {}): Promise<any> => {
},
],
eyeCatchingImageId: null,
font: 'sans-serif',
font: 'sans-serif' as any,
hideTitleWhenPinned: false,
name: '1678594845072',
script: '',
@ -210,7 +205,7 @@ export const page = async (user: UserToken, page: any = {}): Promise<any> => {
return res.body;
};
export const play = async (user: UserToken, play: any = {}): Promise<any> => {
export const play = async (user: UserToken, play: Partial<misskey.entities.Flash> = {}): Promise<misskey.entities.Flash> => {
const res = await api('flash/create', {
permissions: [],
script: 'test',
@ -221,7 +216,7 @@ export const play = async (user: UserToken, play: any = {}): Promise<any> => {
return res.body;
};
export const clip = async (user: UserToken, clip: any = {}): Promise<any> => {
export const clip = async (user: UserToken, clip: Partial<misskey.entities.Clip> = {}): Promise<misskey.entities.Clip> => {
const res = await api('clips/create', {
description: null,
isPublic: true,
@ -231,18 +226,18 @@ export const clip = async (user: UserToken, clip: any = {}): Promise<any> => {
return res.body;
};
export const galleryPost = async (user: UserToken, channel: any = {}): Promise<any> => {
export const galleryPost = async (user: UserToken, galleryPost: Partial<misskey.entities.GalleryPost> = {}): Promise<misskey.entities.GalleryPost> => {
const res = await api('gallery/posts/create', {
description: null,
fileIds: [],
isSensitive: false,
title: 'test',
...channel,
...galleryPost,
}, user);
return res.body;
};
export const channel = async (user: UserToken, channel: any = {}): Promise<any> => {
export const channel = async (user: UserToken, channel: Partial<misskey.entities.Channel> = {}): Promise<misskey.entities.Channel> => {
const res = await api('channels/create', {
bannerId: null,
description: null,
@ -252,7 +247,7 @@ export const channel = async (user: UserToken, channel: any = {}): Promise<any>
return res.body;
};
export const role = async (user: UserToken, role: any = {}, policies: any = {}): Promise<any> => {
export const role = async (user: UserToken, role: Partial<misskey.entities.Role> = {}, policies: any = {}): Promise<misskey.entities.Role> => {
const res = await api('admin/roles/create', {
asBadge: false,
canEditMembersByModerator: false,
@ -260,7 +255,7 @@ export const role = async (user: UserToken, role: any = {}, policies: any = {}):
condFormula: {
id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85',
type: 'isRemote',
},
} as any,
description: '',
displayOrder: 0,
iconUrl: null,
@ -298,7 +293,7 @@ interface UploadOptions {
export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{
status: number,
headers: Headers,
body: misskey.Endpoints['drive/files/create']['res'] | null
body: misskey.entities.DriveFile | null
}> => {
const absPath = path == null
? new URL('resources/Lenna.jpg', import.meta.url)
@ -335,14 +330,14 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO
};
};
export const uploadUrl = async (user: UserToken, url: string): Promise<Packed<'DriveFile'>> => {
export const uploadUrl = async (user: UserToken, url: string): Promise<misskey.entities.DriveFile> => {
const marker = Math.random().toString();
const catcher = makeStreamCatcher(
user,
'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>,
(msg) => msg.body.file,
60 * 1000,
);

View file

@ -33,7 +33,7 @@
"astring": "1.8.6",
"broadcast-channel": "7.0.0",
"buraha": "0.0.1",
"canvas-confetti": "1.6.1",
"canvas-confetti": "1.9.2",
"chart.js": "4.4.2",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "2.0.1",
@ -60,17 +60,17 @@
"rollup": "4.12.0",
"sanitize-html": "2.12.1",
"sass": "1.71.1",
"shiki": "1.0.0-beta.3",
"shiki": "1.1.7",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.161.0",
"three": "0.162.0",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0",
"typescript": "5.3.3",
"uuid": "9.0.1",
"v-code-diff": "1.7.2",
"v-code-diff": "1.9.0",
"vite": "5.1.4",
"vue": "3.4.21",
"vuedraggable": "next"

View file

@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</ol>
<ol v-else-if="emojis.length > 0" ref="suggests" :class="$style.list">
<li v-for="emoji in emojis" :key="emoji.emoji" :class="$style.item" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
<MkCustomEmoji v-if="'isCustomEmoji' in emoji && emoji.isCustomEmoji" :name="emoji.emoji" :class="$style.emoji"/>
<MkCustomEmoji v-if="'isCustomEmoji' in emoji && emoji.isCustomEmoji" :name="emoji.emoji" :class="$style.emoji" :fallbackToImage="true"/>
<MkEmoji v-else :emoji="emoji.emoji" :class="$style.emoji"/>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="q" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
@ -77,7 +77,7 @@ const emojiDb = computed(() => {
unicodeEmojiDB.push({
emoji: emoji,
name: k,
aliasOf: getEmojiName(emoji)!,
aliasOf: getEmojiName(emoji),
url: char2path(emoji),
});
}

View file

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@pointerenter="computeButtonTitle"
@click="emit('chosen', emoji, $event)"
>
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true" :fallbackToImage="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
</button>
</div>
@ -87,7 +87,7 @@ const shown = ref(!!props.initialShown);
function computeButtonTitle(ev: MouseEvent): void {
const elm = ev.target as HTMLElement;
const emoji = elm.dataset.emoji as string;
elm.title = getEmojiName(emoji) ?? emoji;
elm.title = getEmojiName(emoji);
}
function nestedChosen(emoji: any, ev: MouseEvent) {

View file

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkCustomEmoji class="emoji" :name="emoji.name"/>
<MkCustomEmoji class="emoji" :name="emoji.name" :fallbackToImage="true"/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0" class="body">
@ -353,7 +353,7 @@ watch(q, () => {
searchResultUnicode.value = Array.from(searchUnicode());
});
function canReact(emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef): boolean {
function canReact(emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean {
return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
}
@ -378,11 +378,14 @@ function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef):
return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`;
}
function getDef(emoji: string) {
function getDef(emoji: string): string | Misskey.entities.EmojiSimple | UnicodeEmojiDef {
if (emoji.includes(':')) {
return customEmojisMap.get(emoji.replace(/:/g, ''))!;
//
// undefined
const name = emoji.replaceAll(':', '');
return customEmojisMap.get(name) ?? emoji;
} else {
return getUnicodeEmoji(emoji)!;
return getUnicodeEmoji(emoji);
}
}
@ -390,7 +393,7 @@ function getDef(emoji: string) {
function computeButtonTitle(ev: MouseEvent): void {
const elm = ev.target as HTMLElement;
const emoji = elm.dataset.emoji as string;
elm.title = getEmojiName(emoji) ?? emoji;
elm.title = getEmojiName(emoji);
}
function chosen(emoji: any, ev?: MouseEvent) {

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/>
<MkCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl" :fallbackToImage="true"/>
<MkEmoji v-else ref="elRef" :emoji="reaction" :normal="true" :noStyle="noStyle"/>
</template>

View file

@ -44,7 +44,7 @@ function getReactionName(reaction: string): string {
if (trimLocal.startsWith(':')) {
return trimLocal;
}
return getEmojiName(reaction) ?? reaction;
return getEmojiName(reaction);
}
</script>

View file

@ -48,3 +48,18 @@ export const Missing = {
name: Default.args.name,
},
} satisfies StoryObj<typeof MkCustomEmoji>;
export const ErrorToText = {
...Default,
args: {
url: 'https://example.com/404',
name: Default.args.name,
},
} satisfies StoryObj<typeof MkCustomEmoji>;
export const ErrorToImage = {
...Default,
args: {
url: 'https://example.com/404',
name: Default.args.name,
fallbackToImage: true,
},
} satisfies StoryObj<typeof MkCustomEmoji>;

View file

@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<span v-if="errored">:{{ customEmojiName }}:</span>
<img
v-if="errored && fallbackToImage"
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
src="/client-assets/dummy.png"
:title="alt"
/>
<span v-else-if="errored">:{{ customEmojiName }}:</span>
<img
v-else
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
@ -39,6 +45,7 @@ const props = defineProps<{
useOriginalSize?: boolean;
menu?: boolean;
menuReaction?: boolean;
fallbackToImage?: boolean;
}>();
const react = inject<((name: string) => void) | null>('react', null);

View file

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, inject } from 'vue';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@/scripts/emoji-base.js';
import { defaultStore } from '@/store.js';
import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
import * as os from '@/os.js';
@ -34,8 +34,7 @@ const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
function computeTitle(event: PointerEvent): void {
const title = getEmojiName(props.emoji as string) ?? props.emoji as string;
(event.target as HTMLElement).title = title;
(event.target as HTMLElement).title = getEmojiName(props.emoji);
}
function onClick(ev: MouseEvent) {

View file

@ -407,6 +407,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
useOriginalSize: scale >= 2.5,
menu: props.enableEmojiMenu,
menuReaction: props.enableEmojiMenuReaction,
fallbackToImage: false,
})];
} else {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition

View file

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="misskey">Misskey</div>
<div class="version">v{{ version }}</div>
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }">
<MkCustomEmoji v-if="emoji.emoji[0] === ':'" class="emoji" :name="emoji.emoji" :normal="true" :noStyle="true"/>
<MkCustomEmoji v-if="emoji.emoji[0] === ':'" class="emoji" :name="emoji.emoji" :normal="true" :noStyle="true" :fallbackToImage="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji.emoji" :normal="true" :noStyle="true"/>
</span>
</div>

View file

@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<template #item="{element}">
<button class="_button" :class="$style.emojisItem" @click="removeReaction(element, $event)">
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
<MkEmoji v-else :emoji="element" :normal="true"/>
</button>
</template>
@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<template #item="{element}">
<button class="_button" :class="$style.emojisItem" @click="removeEmoji(element, $event)">
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
<MkEmoji v-else :emoji="element" :normal="true"/>
</button>
</template>

View file

@ -1,12 +1,12 @@
import * as Misskey from 'misskey-js';
import { UnicodeEmojiDef } from './emojilist.js';
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef): boolean {
if ('char' in emoji) return true; // UnicodeEmojiDefなら常にリアクション可能
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean {
if (typeof emoji === 'string') return true; // UnicodeEmojiDefにも無い絵文字であれば文字列で来る。Unicode絵文字であることには変わりないので常にリアクション可能とする;
if ('char' in emoji) return true; // UnicodeEmojiDefなら常にリアクション可能
emoji = emoji as Misskey.entities.EmojiSimple;
const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
return !(emoji.localOnly && note.user.host !== me.host)
const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
return !(emoji.localOnly && note.user.host !== me.host)
&& !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
&& (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
}

View file

@ -39,21 +39,29 @@ for (let i = 0; i < emojilist.length; i++) {
export const emojiCharByCategory = _charGroupByCategory;
export function getUnicodeEmoji(char: string): UnicodeEmojiDef | null {
export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string {
// Colorize it because emojilist.json assumes that
return unicodeEmojisMap.get(colorizeEmoji(char)) ?? null;
return unicodeEmojisMap.get(colorizeEmoji(char))
// カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする
?? unicodeEmojisMap.get(char)
// それでも見つからない場合はそのまま返す絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する
?? char;
}
export function getEmojiName(char: string): string | null {
export function getEmojiName(char: string): string {
// Colorize it because emojilist.json assumes that
const idx = _indexByChar.get(colorizeEmoji(char));
if (idx == null) {
return null;
const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char);
if (idx === undefined) {
// 絵文字情報がjsonに無い場合は名前の取得が出来ないのでそのまま返すしか無い
return char;
} else {
return emojilist[idx].name;
}
}
/**
* U+260Eなどの1文字で表現される絵文字VS16:U+FE0Fを付与
*/
export function colorizeEmoji(char: string) {
return char.length === 1 ? `${char}\uFE0F` : char;
}

View file

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type EmojiDef = {
emoji: string;
name: string;

View file

@ -7,28 +7,28 @@ import { assert, describe, test } from 'vitest';
import { searchEmoji } from '@/scripts/search-emoji.js';
describe('emoji autocomplete', () => {
test('名前の完全一致は名前の前方一致より優先される', async () => {
const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
test('名前の完全一致は名前の前方一致より優先される', async () => {
const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
test('名前の前方一致は名前の部分一致より優先される', async () => {
const result = searchEmoji('baaa', [{ emoji: ':baaar:', name: 'baaar' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
assert.equal(result[0].emoji, ':baaar:');
});
test('名前の前方一致は名前の部分一致より優先される', async () => {
const result = searchEmoji('baaa', [{ emoji: ':baaar:', name: 'baaar' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
assert.equal(result[0].emoji, ':baaar:');
});
test('名前の完全一致はタグの完全一致より優先される', async () => {
const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
test('名前の完全一致はタグの完全一致より優先される', async () => {
const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
test('名前の前方一致はタグの前方一致より優先される', async () => {
const result = searchEmoji('foo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
test('名前の前方一致はタグの前方一致より優先される', async () => {
const result = searchEmoji('foo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
test('名前の部分一致はタグの部分一致より優先される', async () => {
const result = searchEmoji('oooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
test('名前の部分一致はタグの部分一致より優先される', async () => {
const result = searchEmoji('oooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
});

View file

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.2.0",
"version": "2024.3.1",
"description": "Misskey SDK for JavaScript",
"types": "./built/dts/index.d.ts",
"exports": {

View file

@ -117,14 +117,14 @@ importers:
specifier: 5.0.3
version: 5.0.3
'@nestjs/common':
specifier: 10.2.10
version: 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1)
specifier: 10.3.3
version: 10.3.3(reflect-metadata@0.2.1)(rxjs@7.8.1)
'@nestjs/core':
specifier: 10.2.10
version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1)
specifier: 10.3.3
version: 10.3.3(@nestjs/common@10.3.3)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1)
'@nestjs/testing':
specifier: 10.2.10
version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.3)
specifier: 10.3.3
version: 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(@nestjs/platform-express@10.3.3)
'@peertube/http-signature':
specifier: 1.7.0
version: 1.7.0
@ -351,8 +351,8 @@ importers:
specifier: 0.1.4
version: 0.1.4
reflect-metadata:
specifier: 0.1.14
version: 0.1.14
specifier: 0.2.1
version: 0.2.1
rename:
specifier: 1.0.4
version: 1.0.4
@ -513,7 +513,7 @@ importers:
version: 1.0.0(@typescript-eslint/eslint-plugin@7.1.0)(@typescript-eslint/parser@7.1.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
'@nestjs/platform-express':
specifier: 10.3.3
version: 10.3.3(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)
version: 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)
'@simplewebauthn/types':
specifier: 9.0.1
version: 9.0.1
@ -725,8 +725,8 @@ importers:
specifier: 0.0.1
version: 0.0.1
canvas-confetti:
specifier: 1.6.1
version: 1.6.1
specifier: 1.9.2
version: 1.9.2
chart.js:
specifier: 4.4.2
version: 4.4.2
@ -806,8 +806,8 @@ importers:
specifier: 1.71.1
version: 1.71.1
shiki:
specifier: 1.0.0-beta.3
version: 1.0.0-beta.3
specifier: 1.1.7
version: 1.1.7
strict-event-emitter-types:
specifier: 2.0.0
version: 2.0.0
@ -815,8 +815,8 @@ importers:
specifier: 3.1.0
version: 3.1.0
three:
specifier: 0.161.0
version: 0.161.0
specifier: 0.162.0
version: 0.162.0
throttle-debounce:
specifier: 5.0.0
version: 5.0.0
@ -836,8 +836,8 @@ importers:
specifier: 9.0.1
version: 9.0.1
v-code-diff:
specifier: 1.7.2
version: 1.7.2(vue@3.4.21)
specifier: 1.9.0
version: 1.9.0(vue@3.4.21)
vite:
specifier: 5.1.4
version: 5.1.4(@types/node@20.11.22)(sass@1.71.1)(terser@5.28.1)
@ -4860,12 +4860,12 @@ packages:
tar-fs: 2.1.1
dev: true
/@nestjs/common@10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1):
resolution: {integrity: sha512-fwAk931rjW8CNH2Mgwawq/7HWHH1dxkOLdcgs7U52ddLk8CtHXjejm1cbNahewlSbNhvlOl7y1STLHutE6sUqw==}
/@nestjs/common@10.3.3(reflect-metadata@0.2.1)(rxjs@7.8.1):
resolution: {integrity: sha512-LAkTe8/CF0uNWM0ecuDwUNTHCi1lVSITmmR4FQ6Ftz1E7ujQCnJ5pMRzd8JRN14vdBkxZZ8VbVF0BDUKoKNxMQ==}
peerDependencies:
class-transformer: '*'
class-validator: '*'
reflect-metadata: ^0.1.12
reflect-metadata: ^0.1.12 || ^0.2.0
rxjs: ^7.1.0
peerDependenciesMeta:
class-transformer:
@ -4874,20 +4874,20 @@ packages:
optional: true
dependencies:
iterare: 1.2.1
reflect-metadata: 0.1.14
reflect-metadata: 0.2.1
rxjs: 7.8.1
tslib: 2.6.2
uid: 2.0.2
/@nestjs/core@10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1):
resolution: {integrity: sha512-+ckOI6BPi2ZMHikT9MCG4ctHDc4OnjhoIytrn7f2AYMMXI4bnutJhqyQKc30VDka5x3Wq6QAD57pgSP7y+JjJg==}
/@nestjs/core@10.3.3(@nestjs/common@10.3.3)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1):
resolution: {integrity: sha512-kxJWggQAPX3RuZx9JVec69eSLaYLNIox2emkZJpfBJ5Qq7cAq7edQIt1r4LGjTKq6kFubNTPsqhWf5y7yFRBPw==}
requiresBuild: true
peerDependencies:
'@nestjs/common': ^10.0.0
'@nestjs/microservices': ^10.0.0
'@nestjs/platform-express': ^10.0.0
'@nestjs/websockets': ^10.0.0
reflect-metadata: ^0.1.12
reflect-metadata: ^0.1.12 || ^0.2.0
rxjs: ^7.1.0
peerDependenciesMeta:
'@nestjs/microservices':
@ -4897,27 +4897,27 @@ packages:
'@nestjs/websockets':
optional: true
dependencies:
'@nestjs/common': 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1)
'@nestjs/platform-express': 10.3.3(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)
'@nestjs/common': 10.3.3(reflect-metadata@0.2.1)(rxjs@7.8.1)
'@nestjs/platform-express': 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)
'@nuxtjs/opencollective': 0.3.2
fast-safe-stringify: 2.1.1
iterare: 1.2.1
path-to-regexp: 3.2.0
reflect-metadata: 0.1.14
reflect-metadata: 0.2.1
rxjs: 7.8.1
tslib: 2.6.2
uid: 2.0.2
transitivePeerDependencies:
- encoding
/@nestjs/platform-express@10.3.3(@nestjs/common@10.2.10)(@nestjs/core@10.2.10):
/@nestjs/platform-express@10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3):
resolution: {integrity: sha512-GGKSEU48Os7nYFIsUM0nutuFUGn5AbeP8gzFBiBCAtiuJWrXZXpZ58pMBYxAbMf7IrcOZFInHEukjHGAQU0OZw==}
peerDependencies:
'@nestjs/common': ^10.0.0
'@nestjs/core': ^10.0.0
dependencies:
'@nestjs/common': 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1)
'@nestjs/core': 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1)
'@nestjs/common': 10.3.3(reflect-metadata@0.2.1)(rxjs@7.8.1)
'@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1)
body-parser: 1.20.2
cors: 2.8.5
express: 4.18.2
@ -4926,8 +4926,8 @@ packages:
transitivePeerDependencies:
- supports-color
/@nestjs/testing@10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.3):
resolution: {integrity: sha512-IVLUnPz/+fkBtPATYfqTIP+phN9yjkXejmj+JyhmcfPJZpxBmD1i9VSMqa4u54l37j0xkGPscQ0IXpbhqMYUKw==}
/@nestjs/testing@10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(@nestjs/platform-express@10.3.3):
resolution: {integrity: sha512-kX20GfjAImL5grd/i69uD/x7sc00BaqGcP2dRG3ilqshQUuy5DOmspLCr3a2C8xmVU7kzK4spT0oTxhe6WcCAA==}
peerDependencies:
'@nestjs/common': ^10.0.0
'@nestjs/core': ^10.0.0
@ -4939,9 +4939,9 @@ packages:
'@nestjs/platform-express':
optional: true
dependencies:
'@nestjs/common': 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1)
'@nestjs/core': 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.1.14)(rxjs@7.8.1)
'@nestjs/platform-express': 10.3.3(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)
'@nestjs/common': 10.3.3(reflect-metadata@0.2.1)(rxjs@7.8.1)
'@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/platform-express@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1)
'@nestjs/platform-express': 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)
tslib: 2.6.2
dev: false
@ -5327,8 +5327,8 @@ packages:
string-argv: 0.3.1
dev: true
/@shikijs/core@1.0.0-beta.3:
resolution: {integrity: sha512-SCwPom2Wn8XxNlEeqdzycU93SKgzYeVsedjqDsgZaz4XiiPpZUzlHt2NAEQTwTnPcHNZapZ6vbkwJ8P11ggL3Q==}
/@shikijs/core@1.1.7:
resolution: {integrity: sha512-gTYLUIuD1UbZp/11qozD3fWpUTuMqPSf3svDMMrL0UmlGU7D9dPw/V1FonwAorCUJBltaaESxq90jrSjQyGixg==}
dev: false
/@sideway/address@4.1.4:
@ -9473,8 +9473,8 @@ packages:
resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==}
dev: false
/canvas-confetti@1.6.1:
resolution: {integrity: sha512-CgGR5DL9+dkne4AwcpvWQc0LIQq43yDIxlwdZcyrq3yklricNfuPHoOSoM6Ya7yCQ+sXmZ2iNV2feiKjVG8C1g==}
/canvas-confetti@1.9.2:
resolution: {integrity: sha512-6Xi7aHHzKwxZsem4mCKoqP6YwUG3HamaHHAlz1hTNQPCqXhARFpSXnkC9TWlahHY5CG6hSL5XexNjxK8irVErg==}
dev: false
/caseless@0.12.0:
@ -12500,8 +12500,8 @@ packages:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
dev: false
/highlight.js@11.8.0:
resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==}
/highlight.js@11.9.0:
resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==}
engines: {node: '>=12.0.0'}
dev: false
@ -17110,12 +17110,8 @@ packages:
redis-errors: 1.2.0
dev: false
/reflect-metadata@0.1.14:
resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==}
/reflect-metadata@0.2.1:
resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==}
dev: false
/regenerate-unicode-properties@10.1.0:
resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
@ -17669,10 +17665,10 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
/shiki@1.0.0-beta.3:
resolution: {integrity: sha512-z7cHTNSSvwGx2DfeLwjSNLo+HcVxifgNIzLm6Ye52eXcIwNHXT0wHbhy7FDOKSKveuEHBwt9opfj3Hoc8LE1Yg==}
/shiki@1.1.7:
resolution: {integrity: sha512-9kUTMjZtcPH3i7vHunA6EraTPpPOITYTdA5uMrvsJRexktqP0s7P3s9HVK80b4pP42FRVe03D7fT3NmJv2yYhw==}
dependencies:
'@shikijs/core': 1.0.0-beta.3
'@shikijs/core': 1.1.7
dev: false
/side-channel@1.0.4:
@ -18507,8 +18503,8 @@ packages:
real-require: 0.2.0
dev: false
/three@0.161.0:
resolution: {integrity: sha512-LC28VFtjbOyEu5b93K0bNRLw1rQlMJ85lilKsYj6dgTu+7i17W+JCCEbvrpmNHF1F3NAUqDSWq50UD7w9H2xQw==}
/three@0.162.0:
resolution: {integrity: sha512-xfCYj4RnlozReCmUd+XQzj6/5OjDNHBy5nT6rVwrOKGENAvpXe2z1jL+DZYaMu4/9pNsjH/4Os/VvS9IrH7IOQ==}
dev: false
/throttle-debounce@5.0.0:
@ -19187,8 +19183,8 @@ packages:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
/v-code-diff@1.7.2(vue@3.4.21):
resolution: {integrity: sha512-y+q8ZHf8GfphYLhcZbjAKcId/h6vZujS71Ryq5u+dI6Jg4ZLTdLrBNVSzYpHywHSSFFfBMdilm6XvVryEaH4+A==}
/v-code-diff@1.9.0(vue@3.4.21):
resolution: {integrity: sha512-alg6krCxFvwTob/rJq+3LzjdIbLb/ni8tS8YmBbI0wckOkbJuN1cShFJ6XEkm82tMgpv5NYEeWLEWhggeV7BDg==}
requiresBuild: true
peerDependencies:
'@vue/composition-api': ^1.4.9
@ -19199,9 +19195,9 @@ packages:
dependencies:
diff: 5.1.0
diff-match-patch: 1.0.5
highlight.js: 11.8.0
highlight.js: 11.9.0
vue: 3.4.21(typescript@5.3.3)
vue-demi: 0.13.11(vue@3.4.21)
vue-demi: 0.14.7(vue@3.4.21)
dev: false
/v8-to-istanbul@9.2.0:
@ -19456,8 +19452,8 @@ packages:
resolution: {integrity: sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==}
dev: true
/vue-demi@0.13.11(vue@3.4.21):
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
/vue-demi@0.14.7(vue@3.4.21):
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true