Merge pull request 'Branch sync' (#2) from Sharkey/Sharkey:develop into trackeroptdev

Reviewed-on: https://git.joinsharkey.org/vavency/Sharkey/pulls/2
This commit is contained in:
vavency 2024-01-14 17:33:47 +01:00
commit a41a647646
475 changed files with 8921 additions and 2797 deletions

View file

@ -2,3 +2,4 @@
POSTGRES_PASSWORD=example-misskey-pass
POSTGRES_USER=example-misskey-user
POSTGRES_DB=misskey
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"

View file

@ -167,6 +167,9 @@ id: 'aidx'
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1)
maxNoteLength: 3000
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
@ -197,6 +200,8 @@ proxyRemoteFiles: true
# Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true
# check that inbound ActivityPub GET requests are signed ("authorized fetch")
checkActivityPubGetSignature: false
# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".

View file

@ -179,6 +179,9 @@ id: 'aidx'
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1)
maxNoteLength: 3000
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128

View file

@ -41,7 +41,6 @@ jobs:
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=stable
- name: Log in to GHCR
uses: https://github.com/docker/login-action@v3

View file

@ -8,6 +8,12 @@ on:
paths:
- packages/**
pull_request:
paths:
- packages/backend/**
- packages/frontend/**
- packages/sw/**
- packages/misskey-js/**
- packages/shared/.eslintrc.js
jobs:
pnpm_install:

View file

@ -19,4 +19,4 @@ body:
attributes:
label: Do you want to implement this feature yourself?
options:
- label: Yes, I will implement this by myself and send a pull request
- label: Yes, I will implement this by myself and send a pull request

1
.gitignore vendored
View file

@ -41,6 +41,7 @@ docker-compose.yml
# misskey
/build
built
built-test
/data
/.cache-loader
/db

3
.gitmodules vendored
View file

@ -4,3 +4,6 @@
[submodule "fluent-emojis"]
path = fluent-emojis
url = https://github.com/misskey-dev/emojis.git
[submodule "tossface-emojis"]
path = tossface-emojis
url = https://git.joinsharkey.org/Sharkey/tossface-emojis.git

View file

@ -1,16 +1,27 @@
<!--
## 2023.x.x (unreleased)
## 202x.x.x (Unreleased)
### General
-
- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
### Client
-
- Feat: 新しいゲームを追加
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように
- Enhance: AiScriptを0.17.0に更新 [CHANGELOG](https://github.com/aiscript-dev/aiscript/blob/bb89d132b633a622d3cb0eff0d0cc7e476c0cfdd/CHANGELOG.md)
- 配列の範囲外・非整数のインデックスへの代入が完全禁止になるので注意
- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように
- Fix: ネイティブモードの絵文字がモノクロにならないように
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
- Fix: v2023.12.1で追加された`$[clickable ...]`および`onClickEv`が正しく機能していないのを修正
### Server
-
-->
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
- Enhance: クリップをエクスポートできるように
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
## 2023.12.2

View file

@ -1,5 +1,5 @@
Unless otherwise stated this repository is
Copyright © 2014-2023 syuilo and contributers
Copyright © 2014-2024 syuilo and contributors
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.

View file

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4
ARG NODE_VERSION=21.4.0-alpine3.18
ARG NODE_VERSION=20.10.0-alpine3.18
FROM node:${NODE_VERSION} as build
@ -44,6 +44,7 @@ COPY --from=build /sharkey/packages/megalodon/node_modules ./packages/megalodon/
COPY --from=build /sharkey/packages/misskey-js/built ./packages/misskey-js/built
COPY --from=build /sharkey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
COPY --from=build /sharkey/fluent-emojis ./fluent-emojis
COPY --from=build /sharkey/tossface-emojis/dist ./tossface-emojis/dist
COPY --from=build /sharkey/sharkey-assets ./packages/frontend/assets
COPY package.json ./package.json

13
IMPORTANT_NOTES.md Normal file
View file

@ -0,0 +1,13 @@
# Basic Precautions
When using a service with Sharkey, there are several important points to keep in mind.
1. Because it is decentralized, there is no guarantee that data you upload will be deleted from all other servers even if you delete it once. (However, this applies to the internet in general.)
2. Even for posts made in private, there is no guarantee that the recipient's server will treat them as private in the same way. Please exercise caution when posting personal or confidential information. (Again, this applies to the internet in general.)
3. Account deletion can be a resource-intensive process and may take a long time. In cases with a lot of uploaded data, it may even be impossible to delete an account.
4. Please disable ad blockers. Some servers may rely on advertising revenue to cover operating costs. Additionally, ad blockers can mistakenly block content and features unrelated to ads, potentially causing issues with the client's functionality and preventing normal use of Sharkey. Therefore, we recommend turning off ad blockers and similar features when using Sharkey.
Please understand these points and enjoy using the service.

View file

@ -6,6 +6,7 @@ Also, the later tasks are more indefinite and are subject to change as developme
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
- ~~Make the number of type errors zero (backend)~~ → Done ✔️
- Make the number of type errors zero (frontend)
- Improve CI
- ~~Fix tests~~ → Done ✔️
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986

View file

@ -2,13 +2,13 @@ version: "3"
services:
web:
# replace image below with git.joinsharkey.org/sharkey/sharkey:stable on next release
# image: ghcr.io/transfem-org/sharkey:stable
# image: git.joinsharkey.org/sharkey/sharkey:latest
build: .
restart: always
links:
- db
- redis
# - mcaptcha
# - meilisearch
depends_on:
db:
@ -49,12 +49,43 @@ services:
interval: 5s
retries: 20
# mcaptcha:
# restart: always
# image: mcaptcha/mcaptcha:latest
# networks:
# internal_network:
# external_network:
# aliases:
# - localhost
# ports:
# - 7493:7493
# env_file:
# - .config/docker.env
# environment:
# PORT: 7493
# MCAPTCHA_redis_URL: "redis://mcaptcha_redis/"
# depends_on:
# db:
# condition: service_healthy
# mcaptcha_redis:
# condition: service_healthy
#
# mcaptcha_redis:
# image: mcaptcha/cache:latest
# networks:
# - internal_network
# healthcheck:
# test: "redis-cli ping"
# interval: 5s
# retries: 20
# meilisearch:
# restart: always
# image: getmeili/meilisearch:v1.3.4
# environment:
# - MEILI_NO_ANALYTICS=true
# - MEILI_ENV=production
# - MEILI_MASTER_KEY=ChangeThis
# networks:
# - shonk
# volumes:

View file

@ -973,6 +973,7 @@ neverShow: "Nicht wieder anzeigen"
remindMeLater: "Vielleicht später"
didYouLikeMisskey: "Gefällt dir Sharkey?"
pleaseDonate: "Sharkey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!"
pleaseDonateInstance: "Du kannst {host} auch direkt unterstützen, indem du an deine Instanz Administration spendest."
roles: "Rollen"
role: "Rolle"
noRole: "Rolle nicht gefunden"
@ -1150,6 +1151,8 @@ impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung,
privacyPolicy: "Datenschutzerklärung"
privacyPolicyUrl: "Datenschutzerklärungs-URL"
tosAndPrivacyPolicy: "Nutzungsbedingungen und Datenschutzerklärung"
donation: "Spenden"
donationUrl: "Spenden-URL"
avatarDecorations: "Profilbilddekoration"
attach: "Anbringen"
detach: "Entfernen"

View file

@ -126,7 +126,11 @@ add: "Add"
reaction: "Reactions"
reactions: "Reactions"
emojiPicker: "Emoji picker"
pinnedEmojisForReactionSettingDescription: "Set the emojis which should be pinned and displayed immediately when reacting."
pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker"
emojiPickerDisplay: "Emoji picker display"
overwriteFromPinnedEmojisForReaction: "Override from reaction settings"
overwriteFromPinnedEmojis: "Override from general settings"
reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add."
rememberNoteVisibility: "Remember note visibility settings"
attachCancel: "Remove attachment"
@ -955,6 +959,10 @@ numberOfPageCache: "Number of cached pages"
numberOfPageCacheDescription: "Increasing this number will improve convenience for but cause more load as more memory usage on the user's device."
numberOfReplies: "Number of replies in a thread"
numberOfRepliesDescription: "Increasing this number will display more replies. Setting this too high can cause replies to be cramped and unreadable."
boostSettings: "Boost Settings"
showVisibilitySelectorOnBoost: "Show Visibility Selector"
showVisibilitySelectorOnBoostDescription: "Shows the visiblity selector if enabled when clicking boost, if disabled it will use the default visiblity defined below and the selector will not show up."
visibilityOnBoost: "Default boost visibility"
logoutConfirm: "Really log out?"
lastActiveDate: "Last used at"
statusbar: "Status bar"
@ -1006,6 +1014,7 @@ neverShow: "Don't show again"
remindMeLater: "Maybe later"
didYouLikeMisskey: "Have you taken a liking to Sharkey?"
pleaseDonate: "{host} uses the free software, Sharkey. We would highly appreciate your donations so development of Sharkey can continue!"
pleaseDonateInstance: "You can also support {host} directly by donating to your instance administration."
roles: "Roles"
role: "Role"
noRole: "Role not found"
@ -1192,9 +1201,12 @@ impressumDescription: "In some countries, like germany, the inclusion of operato
privacyPolicy: "Privacy Policy"
privacyPolicyUrl: "Privacy Policy URL"
tosAndPrivacyPolicy: "Terms of Service and Privacy Policy"
donation: "Donate"
donationUrl: "Donation URL"
avatarDecorations: "Avatar decorations"
attach: "Attach"
detach: "Remove"
detachAll: "Remove all"
angle: "Angle"
flip: "Flip"
showAvatarDecorations: "Show avatar decorations"
@ -1208,7 +1220,12 @@ cwNotationRequired: "If \"Hide content\" is enabled, a description must be provi
doReaction: "Add reaction"
code: "Code"
reloadRequiredToApplySettings: "Reloading is required to apply the settings."
remainingN: "Remaining: {n}"
overwriteContentConfirm: "Are you sure you want to overwrite the current content?"
seasonalScreenEffect: "Seasonal screen effects"
decorate: "Decorate"
addMfmFunction: "Add MFM"
enableQuickAddMfmFunction: "Show advanced MFM picker"
_announcement:
forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@ -1642,6 +1659,7 @@ _role:
canHideAds: "Can hide ads"
canSearchNotes: "Usage of note search"
canUseTranslator: "Translator usage"
avatarDecorationLimit: "Maximum number of avatar decorations that can be applied"
_condition:
isLocal: "Local user"
isRemote: "Remote user"
@ -1670,6 +1688,7 @@ _emailUnavailable:
disposable: "Disposable email addresses may not be used"
mx: "This email server is invalid"
smtp: "This email server is not responding"
banned: "This email address is banned"
_ffVisibility:
public: "Public"
followers: "Visible to followers only"
@ -1964,6 +1983,55 @@ _permissions:
"write:flash": "Edit Plays"
"read:flash-likes": "View list of liked Plays"
"write:flash-likes": "Edit list of liked Plays"
"read:admin:abuse-user-reports": "View user reports"
"write:admin:delete-account": "Delete account"
"write:admin:delete-all-files-of-a-user": "Delete all files of a user"
"read:admin:index-stats": "View information about database indexes"
"read:admin:table-stats": "View information about database tables"
"read:admin:user-ips": "View user IP address"
"read:admin:meta": "View instance metadata"
"write:admin:reset-password": "Reset user passwords"
"write:admin:resolve-abuse-user-report": "Resolve user reports"
"write:admin:send-email": "Send Email"
"read:admin:server-info": "View server info"
"read:admin:show-moderation-log": "View moderation log"
"read:admin:show-user": "View user information"
"read:admin:show-users": "View users"
"write:admin:suspend-user": "Suspend user"
"write:admin:unset-user-avatar": "Remove avatar from user"
"write:admin:unset-user-banner": "Remove banner from user"
"write:admin:unsuspend-user": "Unsuspend user"
"write:admin:meta": "Edit instance metadata"
"write:admin:user-note": "Edit user note"
"write:admin:roles": "Edit roles"
"read:admin:roles": "View roles"
"write:admin:relays": "Edit relays"
"read:admin:relays": "View relays"
"write:admin:invite-codes": "Edit invite codes"
"read:admin:invite-codes": "View invite codes"
"write:admin:announcements": "Edit announcements"
"read:admin:announcements": "View announcements"
"write:admin:avatar-decorations": "Edit avatar decorations"
"read:admin:avatar-decorations": "View avatar decorations"
"write:admin:federation": "Edit remote instance information"
"write:admin:account": "Edit users"
"read:admin:account": "View information about user"
"write:admin:emoji": "Edit emojis"
"read:admin:emoji": "View emojis"
"write:admin:queue": "Edit queue"
"read:admin:queue": "View queue"
"write:admin:promo": "Edit promo"
"write:admin:drive": "Edit user drive"
"read:admin:drive": "View user drive"
"read:admin:stream": "Using the Websocket API for Admin"
"write:admin:ad": "Edit ads"
"read:admin:ad": "View ads"
"write:invite-codes": "Create Invitation Code"
"read:invite-codes": "View Invitation Code"
"write:clip-favorite": "Edit clips and likes"
"read:clip-favorite": "View clips and likes"
"read:federation": "View information about remote instance"
"write:report-abuse": "Report abuse"
_auth:
shareAccessTitle: "Granting application permissions"
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
@ -2082,8 +2150,13 @@ _profile:
metadataContent: "Content"
changeAvatar: "Change avatar"
changeBanner: "Change banner"
updateBanner: "Update banner"
removeBanner: "Remove banner"
changeBackground: "Change background"
updateBackground: "Update background"
removeBackground: "Remove background"
verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field."
avatarDecorationMax: "You can add up to {max} decorations."
_exportOrImport:
allNotes: "All notes"
favoritedNotes: "Favorite notes"
@ -2464,11 +2537,13 @@ _dataRequest:
_dataSaver:
_media:
title: "Loading Media"
description: "Prevents images/videos from being loaded automatically. Hidden images/videos will be loaded when tapped."
_avatar:
title: "Avatar image"
description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic."
_urlPreview:
title: "URL preview thumbnails"
description: "URL preview thumbnail images will no longer be loaded."
_code:
title: "Code highlighting"
description: "If code highlighting notations are used in MFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data."

43
locales/index.d.ts vendored
View file

@ -393,6 +393,11 @@ export interface Locale {
"enableHcaptcha": string;
"hcaptchaSiteKey": string;
"hcaptchaSecretKey": string;
"mcaptcha": string;
"enableMcaptcha": string;
"mcaptchaSiteKey": string;
"mcaptchaSecretKey": string;
"mcaptchaInstanceUrl": string;
"recaptcha": string;
"enableRecaptcha": string;
"recaptchaSiteKey": string;
@ -643,6 +648,7 @@ export interface Locale {
"small": string;
"generateAccessToken": string;
"permission": string;
"adminPermission": string;
"enableAll": string;
"disableAll": string;
"tokenRequested": string;
@ -686,6 +692,7 @@ export interface Locale {
"other": string;
"regenerateLoginToken": string;
"regenerateLoginTokenDescription": string;
"theKeywordWhenSearchingForCustomEmoji": string;
"setMultipleBySeparatingWithSpace": string;
"fileIdOrUrl": string;
"behavior": string;
@ -962,6 +969,10 @@ export interface Locale {
"numberOfPageCacheDescription": string;
"numberOfReplies": string;
"numberOfRepliesDescription": string;
"boostSettings": string;
"showVisibilitySelectorOnBoost": string;
"showVisibilitySelectorOnBoostDescription": string;
"visibilityOnBoost": string;
"logoutConfirm": string;
"lastActiveDate": string;
"statusbar": string;
@ -1013,6 +1024,7 @@ export interface Locale {
"remindMeLater": string;
"didYouLikeMisskey": string;
"pleaseDonate": string;
"pleaseDonateInstance": string;
"roles": string;
"role": string;
"noRole": string;
@ -1199,6 +1211,8 @@ export interface Locale {
"privacyPolicy": string;
"privacyPolicyUrl": string;
"tosAndPrivacyPolicy": string;
"donation": string;
"donationUrl": string;
"avatarDecorations": string;
"attach": string;
"detach": string;
@ -1222,6 +1236,21 @@ export interface Locale {
"decorate": string;
"addMfmFunction": string;
"enableQuickAddMfmFunction": string;
"bubbleGame": string;
"sfx": string;
"soundWillBePlayed": string;
"showReplay": string;
"replay": string;
"replaying": string;
"ranking": string;
"_bubbleGame": {
"howToPlay": string;
"_howToPlay": {
"section1": string;
"section2": string;
"section3": string;
};
};
"_announcement": {
"forExistingUsers": string;
"forExistingUsersDescription": string;
@ -1686,6 +1715,15 @@ export interface Locale {
"title": string;
"description": string;
};
"_bubbleGameExplodingHead": {
"title": string;
"description": string;
};
"_bubbleGameDoubleExplodingHead": {
"title": string;
"description": string;
"flavor": string;
};
};
};
"_role": {
@ -2287,13 +2325,18 @@ export interface Locale {
"metadataContent": string;
"changeAvatar": string;
"changeBanner": string;
"updateBanner": string;
"removeBanner": string;
"changeBackground": string;
"updateBackground": string;
"removeBackground": string;
"verifiedLinkDescription": string;
"avatarDecorationMax": string;
};
"_exportOrImport": {
"allNotes": string;
"favoritedNotes": string;
"clips": string;
"followingList": string;
"muteList": string;
"blockingList": string;

View file

@ -15,7 +15,7 @@ gotIt: "わかった"
cancel: "キャンセル"
noThankYou: "やめておく"
enterUsername: "ユーザー名を入力"
renotedBy: "{user}がリノート"
renotedBy: "{user}がブースト"
noNotes: "ノートはありません"
noNotifications: "通知はありません"
instance: "サーバー"
@ -46,16 +46,16 @@ pin: "ピン留め"
unpin: "ピン留め解除"
copyContent: "内容をコピー"
copyLink: "リンクをコピー"
copyLinkRenote: "リノートのリンクをコピー"
copyLinkRenote: "ブーストのリンクをコピー"
delete: "削除"
deleteAndEdit: "削除して編集"
deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、リノート、返信も全て削除されます。"
deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、ブースト、返信も全て削除されます。"
addToList: "リストに追加"
addToAntenna: "アンテナに追加"
sendMessage: "メッセージを送信"
copyRSS: "RSSをコピー"
copyUsername: "ユーザー名をコピー"
openRemoteProfile: "リモートプロファイルを開く"
openRemoteProfile: "リモートプロフィールを開く"
copyUserId: "ユーザーIDをコピー"
copyNoteId: "ートIDをコピー"
copyFileId: "ファイルIDをコピー"
@ -107,15 +107,15 @@ followRequests: "フォロー申請"
unfollow: "フォロー解除"
followRequestPending: "フォロー許可待ち"
enterEmoji: "絵文字を入力"
renote: "リノート"
unrenote: "リノート解除"
renoted: "ブースト。"
renote: "ブースト"
unrenote: "ブースト解除"
renoted: "ブーストしました。"
quoted: "引用。"
rmboost: "アンブースト。"
cantRenote: "この投稿はリノートできません。"
cantReRenote: "リノートをリノートすることはできません。"
rmboost: "ブースト解除しました。"
cantRenote: "この投稿はブーストできません。"
cantReRenote: "ブーストをブーストすることはできません。"
quote: "引用"
inChannelRenote: "チャンネル内リノート"
inChannelRenote: "チャンネル内ブースト"
inChannelQuote: "チャンネル内引用"
pinnedNote: "ピン留めされたノート"
pinned: "ピン留め"
@ -139,8 +139,8 @@ unmarkAsSensitive: "センシティブを解除する"
enterFileName: "ファイル名を入力"
mute: "ミュート"
unmute: "ミュート解除"
renoteMute: "リノートをミュート"
renoteUnmute: "リノートのミュートを解除"
renoteMute: "ブーストをミュート"
renoteUnmute: "ブーストのミュートを解除"
block: "ブロック"
unblock: "ブロック解除"
markAsNSFW: "ユーザーのすべてのメディアをNSFWとしてマークする"
@ -209,8 +209,8 @@ charts: "チャート"
perHour: "1時間ごと"
perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送を停止"
blockThisInstance: "このサーバーをブロック"
silenceThisInstance: "サーバーをサイレンス"
blockThisInstance: "このインスタンスをブロック"
silenceThisInstance: "インスタンスをサイレンス"
operations: "操作"
software: "ソフトウェア"
version: "バージョン"
@ -231,7 +231,7 @@ clearCachedFilesConfirm: "キャッシュされたリモートファイルをす
blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。"
silencedInstances: "サイレンスしたサーバー"
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。"
silencedInstancesDescription: "サイレンスしたいインスタンスのホストを改行で区切って設定します。サイレンスされたインスタンスに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。"
muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
@ -390,6 +390,11 @@ hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptchaを有効にする"
hcaptchaSiteKey: "サイトキー"
hcaptchaSecretKey: "シークレットキー"
mcaptcha: "mCaptcha"
enableMcaptcha: "mCaptchaを有効にする"
mcaptchaSiteKey: "サイトキー"
mcaptchaSecretKey: "シークレットキー"
mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL"
recaptcha: "reCAPTCHA"
enableRecaptcha: "reCAPTCHAを有効にする"
recaptchaSiteKey: "サイトキー"
@ -640,6 +645,7 @@ medium: "中"
small: "小"
generateAccessToken: "アクセストークンの発行"
permission: "権限"
adminPermission: "管理者権限"
enableAll: "全て有効にする"
disableAll: "全て無効にする"
tokenRequested: "アカウントへのアクセス許可"
@ -683,13 +689,14 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
other: "その他"
regenerateLoginToken: "ログイントークンを再生成"
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。"
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
fileIdOrUrl: "ファイルIDまたはURL"
behavior: "動作"
sample: "サンプル"
abuseReports: "通報"
reportAbuse: "通報"
reportAbuseRenote: "リノートを通報"
reportAbuseRenote: "ブーストを通報"
reportAbuseOf: "{name}を通報する"
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のートがある場合はそのURLも記入してください。"
abuseReported: "内容が送信されました。ご報告ありがとうございました。"
@ -723,9 +730,9 @@ manageAccessTokens: "アクセストークンの管理"
accountInfo: "アカウント情報"
notesCount: "ノートの数"
repliesCount: "返信した数"
renotesCount: "リノートした数"
renotesCount: "ブーストした数"
repliedCount: "返信された数"
renotedCount: "リノートされた数"
renotedCount: "ブーストされた数"
followingCount: "フォロー数"
followersCount: "フォロワー数"
sentReactionsCount: "リアクションした数"
@ -959,6 +966,10 @@ numberOfPageCache: "ページキャッシュ数"
numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
numberOfReplies: "スレッド内の返信数"
numberOfRepliesDescription: "この数値を大きくすると、より多くの返信が表示されます。この値を大きくしすぎると、返信が窮屈になり、読めなくなることがあります。"
boostSettings: "ブースト設定"
showVisibilitySelectorOnBoost: "可視性セレクタを表示"
showVisibilitySelectorOnBoostDescription: "無効の場合、以下で定義されるデフォルトの可視性が使用され、セレクタは表示されません。"
visibilityOnBoost: "デフォルトのブースト可視性の設定"
logoutConfirm: "ログアウトしますか?"
lastActiveDate: "最終利用日時"
statusbar: "ステータスバー"
@ -1010,6 +1021,7 @@ neverShow: "今後表示しない"
remindMeLater: "また後で"
didYouLikeMisskey: "Sharkeyを気に入っていただけましたか"
pleaseDonate: "Sharkeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
pleaseDonateInstance: "インスタンス管理者への寄付によって{host}を直接サポートすることもできます。"
roles: "ロール"
role: "ロール"
noRole: "ロールはありません"
@ -1036,7 +1048,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑になる可能性があります
thisPostMayBeAnnoyingHome: "ホームに投稿"
thisPostMayBeAnnoyingCancel: "やめる"
thisPostMayBeAnnoyingIgnore: "このまま投稿"
collapseRenotes: "見たことのあるリノートを省略して表示"
collapseRenotes: "見たことのあるブーストを省略して表示"
collapseFiles: "ファイルを折りたたむ"
autoloadConversation: "返信に会話を読み込む"
internalServerError: "サーバー内部エラー"
@ -1090,7 +1102,7 @@ forceShowAds: "常に広告を表示する"
addMemo: "メモを追加"
editMemo: "メモを編集"
reactionsList: "リアクション一覧"
renotesList: "リノート一覧"
renotesList: "ブースト一覧"
notificationDisplay: "通知の表示"
leftTop: "左上"
rightTop: "右上"
@ -1132,9 +1144,9 @@ installed: "インストール済み"
branding: "ブランディング"
enableServerMachineStats: "サーバーのマシン情報を公開する"
enableAchievements: "実績を有効にする"
turnOffAchievements: "これをオフにすると、達成システムは無効になります。"
enableBotTrending: "ハッシュタグにボットを追加する"
turnOffBotTrending: "これをオフにするとボットがハッシュタグを入力しなくなります。"
turnOffAchievements: "オフにすると実績システムは無効になります。"
enableBotTrending: "botのハッシュタグ追加を許可する"
turnOffBotTrending: "オフにするとボットがハッシュタグを入力しなくなります。"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
createInviteCode: "招待コードを作成"
@ -1165,7 +1177,7 @@ pastAnnouncements: "過去のお知らせ"
youHaveUnreadAnnouncements: "未読のお知らせがあります。"
useSecurityKey: "ブラウザまたはデバイスの指示に従って、セキュリティキーまたはパスキーを使用してください。"
replies: "返信"
renotes: "リノート"
renotes: "ブースト"
loadReplies: "返信を見る"
loadConversation: "会話を見る"
pinnedList: "ピン留めされたリスト"
@ -1178,7 +1190,7 @@ unnotifyNotes: "投稿の通知を解除"
authentication: "認証"
authenticationRequiredToContinue: "続けるには認証を行ってください"
dateAndTime: "日時"
showRenotes: "リノートを表示"
showRenotes: "ブーストを表示"
edited: "編集済み"
notificationRecieveConfig: "通知の受信設定"
mutualFollow: "相互フォロー"
@ -1196,6 +1208,8 @@ impressumDescription: "ドイツなどの一部の国と地域では表示が義
privacyPolicy: "プライバシーポリシー"
privacyPolicyUrl: "プライバシーポリシーURL"
tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
donation: "寄付する"
donationUrl: "寄付URL"
avatarDecorations: "アイコンデコレーション"
attach: "付ける"
detach: "外す"
@ -1219,6 +1233,20 @@ seasonalScreenEffect: "季節に応じた画面の演出"
decorate: "デコる"
addMfmFunction: "装飾を追加"
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
bubbleGame: "バブルゲーム"
sfx: "効果音"
soundWillBePlayed: "サウンドが再生されます"
showReplay: "リプレイを見る"
replay: "リプレイ"
replaying: "リプレイ中"
ranking: "ランキング"
_bubbleGame:
howToPlay: "遊び方"
_howToPlay:
section1: "位置を調整してハコにモノを落とします。"
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!"
_announcement:
forExistingUsers: "既存ユーザーのみ"
@ -1288,8 +1316,8 @@ _initialTutorial:
_visibility:
description: "ノートを表示できる相手を制限できます。"
public: "すべてのユーザーに公開。"
home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。"
followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。"
home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・ブーストから、他のユーザーも見ることができます。"
followers: "フォロワーにのみ公開。本人以外がブーストすることはできず、またフォロワー以外は閲覧できません。"
direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。"
doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。"
doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。"
@ -1597,6 +1625,13 @@ _achievements:
_tutorialCompleted:
title: "Sharkey初心者講座 修了証"
description: "チュートリアルを完了した"
_bubbleGameExplodingHead:
title: "🤯"
description: "バブルゲームで最も大きいモノを出した"
_bubbleGameDoubleExplodingHead:
title: "ダブル🤯"
description: "バブルゲームで最も大きいモを2つ同時に出した"
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
_role:
new: "ロールの作成"
@ -1636,7 +1671,7 @@ _role:
high: "高"
_options:
gtlAvailable: "グローバルタイムラインの閲覧"
btlAvailable: "バブルのタイムラインを見ることができる"
btlAvailable: "バブルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canImportNotes: "ノートのインポートが可能"
@ -1774,7 +1809,7 @@ _registry:
createKey: "キーを作成"
_aboutMisskey:
about: "Sharkeyは、2014年からsyuiloによって開発されているMisskeyをベースにしたオープンソースのソフトウェアです。"
about: "Sharkeyは、Misskeyをベースにしたオープンソースのソフトウェアです。"
contributors: "主なコントリビューター"
allContributors: "全てのコントリビューター"
source: "ソースコード"
@ -1812,7 +1847,7 @@ _channel:
notesCount: "{n}投稿があります"
nameAndDescription: "名前と説明"
nameOnly: "名前のみ"
allowRenoteToExternal: "チャンネル外へのリノートと引用リノートを許可する"
allowRenoteToExternal: "チャンネル外へのブーストと引用ブーストを許可する"
_menuDisplay:
sideFull: "横"
@ -1826,7 +1861,7 @@ _wordMute:
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
_instanceMute:
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。"
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとブーストをミュートします。"
instanceMuteDescription2: "改行で区切って設定します"
title: "設定したサーバーのノートを隠します。"
heading: "ミュートするサーバー"
@ -1880,7 +1915,7 @@ _theme:
hashtag: "ハッシュタグ"
mention: "メンション"
mentionMe: "あなた宛てメンション"
renote: "Renote"
renote: "Boost"
modalBg: "モーダルの背景"
divider: "分割線"
scrollbarHandle: "スクロールバーの取っ手"
@ -2190,13 +2225,18 @@ _profile:
metadataContent: "内容"
changeAvatar: "アイコン画像を変更"
changeBanner: "バナー画像を変更"
updateBanner: "更新バナー"
removeBanner: "バナーを削除"
changeBackground: "背景を変更する"
updateBackground: "背景を更新する"
removeBackground: "背景を削除する"
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
_exportOrImport:
allNotes: "全てのノート"
favoritedNotes: "お気に入りにしたノート"
clips: "クリップ"
followingList: "フォロー"
muteList: "ミュート"
blockingList: "ブロック"
@ -2316,7 +2356,7 @@ _notification:
youGotMention: "{name}からのメンション"
youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしました"
youRenoted: "{name}がBoostしました"
youWereFollowed: "フォローされました"
youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました"
@ -2331,7 +2371,7 @@ _notification:
sendTestNotification: "テスト通知を送信する"
notificationWillBeDisplayedLikeThis: "通知はこのように表示されます"
reactedBySomeUsers: "{n}人がリアクションしました"
renotedBySomeUsers: "{n}人がリノートしました"
renotedBySomeUsers: "{n}人がブーストしました"
followedBySomeUsers: "{n}人にフォローされました"
_types:
@ -2340,7 +2380,7 @@ _notification:
follow: "フォロー"
mention: "メンション"
reply: "リプライ"
renote: "Renote"
renote: "Boost"
quote: "引用"
reaction: "リアクション"
pollEnded: "アンケートが終了"
@ -2353,7 +2393,7 @@ _notification:
_actions:
followBack: "フォローバック"
reply: "返信"
renote: "Renote"
renote: "Boost"
_deck:
alwaysShowMainColumn: "常にメインカラムを表示"
@ -2411,7 +2451,7 @@ _webhookSettings:
followed: "フォローされたとき"
note: "ノートを投稿したとき"
reply: "返信されたとき"
renote: "Renoteされたとき"
renote: "Boostされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"
@ -2508,15 +2548,15 @@ _animatedMFM:
play: "MFMアニメーションを再生"
stop: "MFMアニメーション停止"
_alert:
text: "アニメーションMFMには、点滅するライトや高速で動くテキスト/絵文字を含めることができる。"
confirm: "アニメイト"
text: "MFMアニメーションには、点滅するライトや高速で動くテキスト/絵文字を含まれる場合があります。"
confirm: "再生する"
_dataRequest:
title: "リクエストデータ"
warn: "データのリクエストは3日ごとにしかできない。"
text: "データのダウンロードが完了すると、このアカウントに登録されているEメールアドレスにEメールが送信されます。"
title: "データリクエスト"
warn: "データリクエストは3日ごとに可能です。"
text: "データの保存が完了すると、このアカウントに登録されているEメールアドレスにメールが送信されます。"
button: "リクエスト"
_dataSaver:
_media:
title: "メディアの読み込み"

View file

@ -15,7 +15,7 @@ gotIt: "ほい"
cancel: "やめとく"
noThankYou: "やめとく"
enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がリノートしたで"
renotedBy: "{user}がブーストしたで"
noNotes: "ノートはあらへん"
noNotifications: "通知はあらへん"
instance: "サーバー"
@ -45,10 +45,10 @@ pin: "ピン留めしとく"
unpin: "やっぱピン留めせん"
copyContent: "内容をコピー"
copyLink: "リンクをコピー"
copyLinkRenote: "リノートのリンクをコピーするで?"
copyLinkRenote: "ブーストのリンクをコピーするで?"
delete: "ほかす"
deleteAndEdit: "ほかして直す"
deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、リノート、返信も全部消えるんやけどそれでもええん?"
deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、ブースト、返信も全部消えるんやけどそれでもええん?"
addToList: "リストに入れたる"
addToAntenna: "アンテナに入れる"
sendMessage: "メッセージを送る"
@ -105,13 +105,13 @@ followRequests: "フォロー申請"
unfollow: "フォローやめる"
followRequestPending: "フォロー許してくれるん待っとる"
enterEmoji: "絵文字を入れてや"
renote: "リノート"
unrenote: "リノートやめる"
renoted: "リノートしたで。"
cantRenote: "この投稿はリノートできへんっぽい。"
cantReRenote: "リノート自体はリノートできへんで。"
renote: "ブースト"
unrenote: "ブーストやめる"
renoted: "ブーストしたで。"
cantRenote: "この投稿はブーストできへんっぽい。"
cantReRenote: "ブースト自体はブーストできへんで。"
quote: "引用"
inChannelRenote: "チャンネルの中でリノート"
inChannelRenote: "チャンネルの中でブースト"
inChannelQuote: "チャンネル内引用"
pinnedNote: "ピン留めされとるノート"
pinned: "ピン留めしとく"
@ -135,8 +135,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ"
enterFileName: "ファイル名を入れてや"
mute: "ミュート"
unmute: "ミュートやめたる"
renoteMute: "リノートは見いひん"
renoteUnmute: "リノートもやっぱ見るわ"
renoteMute: "ブーストは見いひん"
renoteUnmute: "ブーストもやっぱ見るわ"
block: "ブロック"
unblock: "ブロックやめたる"
suspend: "凍結"
@ -677,7 +677,7 @@ behavior: "動作"
sample: "サンプル"
abuseReports: "通報"
reportAbuse: "通報"
reportAbuseRenote: "リノート苦情だすで?"
reportAbuseRenote: "ブースト苦情だすで?"
reportAbuseOf: "{name}を通報する"
fillAbuseReportDescription: "細かい通報理由を書いてなー。対象ートがある時はそのURLも書いといてなー。"
abuseReported: "無事内容が送信されたみたいやで。おおきに〜。"
@ -711,9 +711,9 @@ manageAccessTokens: "アクセストークンの管理"
accountInfo: "アカウント情報"
notesCount: "ノートの数やで"
repliesCount: "返信した数やで"
renotesCount: "リノートした数やで"
renotesCount: "ブーストした数やで"
repliedCount: "返信された数やで"
renotedCount: "リノートされた数やで"
renotedCount: "ブーストされた数やで"
followingCount: "フォロー数やで"
followersCount: "フォロワー数やで"
sentReactionsCount: "ツッコんだ数"
@ -1009,7 +1009,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。"
thisPostMayBeAnnoyingHome: "ホームに投稿"
thisPostMayBeAnnoyingCancel: "やめとく"
thisPostMayBeAnnoyingIgnore: "このまま投稿"
collapseRenotes: "見たことあるリノートは飛ばして表示するで"
collapseRenotes: "見たことあるブーストは飛ばして表示するで"
internalServerError: "サーバー内部エラー"
internalServerErrorDescription: "サーバーでなんか変なこと起こっとるわ。"
copyErrorInfo: "エラー情報をコピるで"
@ -1060,7 +1060,7 @@ forceShowAds: "いっつも広告を映す"
addMemo: "メモを足す"
editMemo: "メモをいらう"
reactionsList: "ツッコミ一覧"
renotesList: "リノート一覧"
renotesList: "ブースト一覧"
notificationDisplay: "通知見せる"
leftTop: "左上"
rightTop: "右上"
@ -1131,7 +1131,7 @@ pastAnnouncements: "過去のお知らせやで"
youHaveUnreadAnnouncements: "あんたまだこのお知らせ読んどらんやろ。"
useSecurityKey: "ブラウザまたはデバイスの言う通りに、セキュリティキーまたはパスキーを使ってや。"
replies: "返事"
renotes: "リノート"
renotes: "ブースト"
loadReplies: "返信を見るで"
loadConversation: "会話を見るで"
pinnedList: "ピン留めしはったリスト"
@ -1142,7 +1142,7 @@ unnotifyNotes: "投稿の通知やめる"
authentication: "認証"
authenticationRequiredToContinue: "続けるんなら認証してや。"
dateAndTime: "日時"
showRenotes: "リノート出す"
showRenotes: "ブースト出す"
edited: "いじったやつ"
notificationRecieveConfig: "通知もらうかの設定"
mutualFollow: "お互いフォローしてんで"
@ -1247,8 +1247,8 @@ _initialTutorial:
_visibility:
description: "ノートを見れる相手を制限できるわ。"
public: "みんなに見せるで。"
home: "ホームタイムラインにだけ見せるで。フォロワーとか、プロフィールを見に来た人、リノートからも見れるから、実質は全員見れるけどな。あんまし広がりにくいってことや。"
followers: "フォロワーにだけ見せるで。自分以外はリノートできへんし、フォロワー以外は絶対に見れへん。"
home: "ホームタイムラインにだけ見せるで。フォロワーとか、プロフィールを見に来た人、ブーストからも見れるから、実質は全員見れるけどな。あんまし広がりにくいってことや。"
followers: "フォロワーにだけ見せるで。自分以外はブーストできへんし、フォロワー以外は絶対に見れへん。"
direct: "指定した人にだけ公開されて、ついでに通知も送るで。ダイレクトメールの代わりとして使ってな。"
doNotSendConfidencialOnDirect1: "機密情報を送るときは十分注意せえよ。"
doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容が見れるから、信用できへんサーバーのひとにダイレクト投稿するときには、めっちゃ用心しとくんやで。"
@ -1709,7 +1709,7 @@ _registry:
domain: "ドメイン"
createKey: "キーを作る"
_aboutMisskey:
about: "Sharkeyは、syuiloが2014年からずっと作ってはる、Misskeyをベースにしたオープンソースなソフトウェアや。"
about: "Sharkeyは、Misskeyをベースにしたオープンソースなソフトウェアや。"
contributors: "主な貢献者"
allContributors: "全ての貢献者"
source: "ソースコード"
@ -1742,7 +1742,7 @@ _channel:
notesCount: "{n}こ投稿があるで"
nameAndDescription: "名前と説明"
nameOnly: "名前だけ"
allowRenoteToExternal: "チャンネルの外にリノートできるようにする"
allowRenoteToExternal: "チャンネルの外にブーストできるようにする"
_menuDisplay:
sideFull: "横"
sideIcon: "横(アイコン)"
@ -2164,7 +2164,7 @@ _notification:
youGotMention: "{name}からのメンション"
youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用"
youRenoted: "{name}がリノートしたみたいやで"
youRenoted: "{name}がブーストしたみたいやで"
youWereFollowed: "フォローされたで"
youReceivedFollowRequest: "フォロー許可してほしいみたいやな"
yourFollowRequestAccepted: "フォローさせてもろたで"
@ -2178,7 +2178,7 @@ _notification:
sendTestNotification: "テスト通知を送信するで"
notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで"
reactedBySomeUsers: "{n}人がツッコんだで"
renotedBySomeUsers: "{n}人がリノートしたで"
renotedBySomeUsers: "{n}人がブーストしたで"
followedBySomeUsers: "{n}人にフォローされたで"
_types:
all: "すべて"
@ -2249,7 +2249,7 @@ _webhookSettings:
followed: "フォローもらったとき~!"
note: "ノートを投稿したとき~!"
reply: "返信があるとき~!"
renote: "リノートされるとき~!"
renote: "ブーストされるとき~!"
reaction: "ツッコまれたとき~!"
mention: "メンションがあるとき~!"
_moderationLogTypes:

View file

@ -1,6 +1,6 @@
{
"name": "sharkey",
"version": "2023.12.0.beta3",
"version": "2024.1.0.beta2",
"codename": "shonk",
"repository": {
"type": "git",

View file

@ -160,7 +160,6 @@ module.exports = {
testMatch: [
"<rootDir>/test/unit/**/*.ts",
"<rootDir>/src/**/*.test.ts",
"<rootDir>/test/e2e/**/*.ts",
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped

View file

@ -0,0 +1,15 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/en/configuration.html
*/
const base = require('./jest.config.cjs')
module.exports = {
...base,
globalSetup: "<rootDir>/built-test/entry.js",
setupFilesAfterEnv: ["<rootDir>/test/jest.setup.ts"],
testMatch: [
"<rootDir>/test/e2e/**/*.ts",
],
};

View file

@ -0,0 +1,14 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/en/configuration.html
*/
const base = require('./jest.config.cjs')
module.exports = {
...base,
testMatch: [
"<rootDir>/test/unit/**/*.ts",
"<rootDir>/src/**/*.test.ts",
],
};

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SupportTrueMailApi1703658526000 {
name = 'SupportTrueMailApi1703658526000'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "truemailInstance" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "truemailAuthKey" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableTruemailApi" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTruemailApi"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailInstance"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailAuthKey"`);
}
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SupportMcaptcha1704373210054 {
name = 'SupportMcaptcha1704373210054'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableMcaptcha" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSitekey" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSecretKey" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaInstanceUrl" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaInstanceUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSecretKey"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSitekey"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableMcaptcha"`);
}
}

View file

@ -0,0 +1,10 @@
export class AddDonationUrl1704744370000 {
name = 'AddDonationUrl1704744370000'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "donationUrl" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "donationUrl"`);
}
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class BubbleGameRecord1704959805077 {
name = 'BubbleGameRecord1704959805077'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "bubble_game_record" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "seededAt" TIMESTAMP WITH TIME ZONE NOT NULL, "seed" character varying(1024) NOT NULL, "gameVersion" integer NOT NULL, "gameMode" character varying(128) NOT NULL, "score" integer NOT NULL, "logs" jsonb NOT NULL DEFAULT '[]', "isVerified" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_a75395fe404b392e2893b50d7ea" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_75276757070d21fdfaf4c05290" ON "bubble_game_record" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_4ae7053179014915d1432d3f40" ON "bubble_game_record" ("seededAt") `);
await queryRunner.query(`CREATE INDEX "IDX_26d4ee490b5a487142d35466ee" ON "bubble_game_record" ("score") `);
await queryRunner.query(`ALTER TABLE "bubble_game_record" ADD CONSTRAINT "FK_75276757070d21fdfaf4c052909" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "bubble_game_record" DROP CONSTRAINT "FK_75276757070d21fdfaf4c052909"`);
await queryRunner.query(`DROP INDEX "public"."IDX_26d4ee490b5a487142d35466ee"`);
await queryRunner.query(`DROP INDEX "public"."IDX_4ae7053179014915d1432d3f40"`);
await queryRunner.query(`DROP INDEX "public"."IDX_75276757070d21fdfaf4c05290"`);
await queryRunner.query(`DROP TABLE "bubble_game_record"`);
}
}

View file

@ -13,6 +13,7 @@
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
"check:connect": "node ./check_connect.js",
"build": "swc src -d built -D",
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc",
"watch:swc": "swc src -d built -D -w",
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
"watch": "node watch.mjs",
@ -21,11 +22,15 @@
"typecheck": "pnpm --filter megalodon build && tsc --noEmit",
"eslint": "eslint --quiet \"src/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint",
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit",
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
"test": "pnpm jest",
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
"test-and-coverage": "pnpm jest-and-coverage",
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
"generate-api-json": "node ./generate_api_json.js"
},
"optionalDependencies": {
@ -72,11 +77,13 @@
"@fastify/multipart": "8.0.0",
"@fastify/static": "6.12.0",
"@fastify/view": "8.2.0",
"@misskey-dev/sharp-read-bmp": "^1.1.1",
"@misskey-dev/summaly": "^5.0.3",
"@nestjs/common": "10.2.10",
"@nestjs/core": "10.2.10",
"@nestjs/testing": "10.2.10",
"@peertube/http-signature": "1.7.0",
"@sharkey/sfm-js": "0.24.3",
"@sharkey/sfm-js": "0.24.4",
"@simplewebauthn/server": "8.3.5",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.1.10",
@ -158,11 +165,9 @@
"sanitize-html": "2.11.0",
"secure-json-parse": "2.7.0",
"sharp": "0.32.6",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.21.20",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
@ -179,6 +184,8 @@
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "^1.0.0",
"@nestjs/platform-express": "^10.3.0",
"@simplewebauthn/typescript-types": "8.3.4",
"@swc/jest": "0.2.29",
"@types/accepts": "1.3.7",
@ -228,9 +235,11 @@
"eslint": "8.56.0",
"eslint-plugin-import": "2.29.1",
"execa": "8.0.1",
"fkill": "^9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"nodemon": "3.0.2",
"pid-port": "^1.0.0",
"simple-oauth2": "5.0.0"
}
}

View file

@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setTimeout } from 'node:timers/promises';
import { Global, Inject, Module } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DataSource } from 'typeorm';
@ -12,6 +11,7 @@ import { DI } from './di-symbols.js';
import { Config, loadConfig } from './config.js';
import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js';
import { allSettled } from './misc/promise-tracker.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
const $config: Provider = {
@ -33,7 +33,7 @@ const $meilisearch: Provider = {
useFactory: (config: Config) => {
if (config.meilisearch) {
return new MeiliSearch({
host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`,
apiKey: config.meilisearch.apiKey,
});
} else {
@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown {
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
) {}
) { }
public async dispose(): Promise<void> {
if (process.env.NODE_ENV === 'test') {
// XXX:
// Shutting down the existing connections causes errors on Jest as
// Misskey has asynchronous postgres/redis connections that are not
// awaited.
// Let's wait for some random time for them to finish.
await setTimeout(5000);
}
// Wait for all potential DB queries
await allSettled();
// And then disconnect from DB
await Promise.all([
this.db.destroy(),
this.redisClient.disconnect(),

View file

@ -7,8 +7,8 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
import type { RedisOptions } from 'ioredis';
import { globSync } from 'glob';
import type { RedisOptions } from 'ioredis';
type RedisOptionsSource = Partial<RedisOptions> & {
host: string;
@ -65,6 +65,7 @@ type Source = {
allowedPrivateNetworks?: string[];
maxFileSize?: number;
maxNoteLength?: number;
clusterLimit?: number;
@ -133,6 +134,7 @@ export type Config = {
proxyBypassHosts: string[] | undefined;
allowedPrivateNetworks: string[] | undefined;
maxFileSize: number | undefined;
maxNoteLength: number;
clusterLimit: number | undefined;
id: string;
outgoingAddress: string | undefined;
@ -199,7 +201,7 @@ export function loadConfig(): Config {
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
const config = globSync(path)
const config = globSync(path).sort()
.map(path => fs.readFileSync(path, 'utf-8'))
.map(contents => yaml.load(contents) as Source)
.reduce(
@ -249,6 +251,7 @@ export function loadConfig(): Config {
proxyBypassHosts: config.proxyBypassHosts,
allowedPrivateNetworks: config.allowedPrivateNetworks,
maxFileSize: config.maxFileSize,
maxNoteLength: config.maxNoteLength ?? 3000,
clusterLimit: config.clusterLimit,
outgoingAddress: config.outgoingAddress,
outgoingAddressFamily: config.outgoingAddressFamily,

View file

@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [
'brainDiver',
'smashTestNotificationButton',
'tutorialCompleted',
'bubbleGameExplodingHead',
'bubbleGameDoubleExplodingHead',
] as const;
@Injectable()

View file

@ -73,6 +73,37 @@ export class CaptchaService {
}
}
// https://codeberg.org/Gusted/mCaptcha/src/branch/main/mcaptcha.go
@bindThis
public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> {
if (response == null) {
throw new Error('mcaptcha-failed: no response provided');
}
const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost);
const result = await this.httpRequestService.send(endpointUrl.toString(), {
method: 'POST',
body: JSON.stringify({
key: siteKey,
secret: secret,
token: response,
}),
headers: {
'Content-Type': 'application/json',
},
});
if (result.status !== 200) {
throw new Error('mcaptcha-failed: mcaptcha didn\'t return 200 OK');
}
const resp = (await result.json()) as { valid: boolean };
if (!resp.valid) {
throw new Error('mcaptcha-request-failed');
}
}
@bindThis
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) {

View file

@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common';
import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp';
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import { IsNull } from 'typeorm';
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
import { DI } from '@/di-symbols.js';
@ -634,7 +634,7 @@ export class DriveService {
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
if (values.name && !this.driveFileEntityService.validateFileName(file.name)) {
if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) {
throw new DriveService.InvalidFileNameError();
}

View file

@ -156,7 +156,7 @@ export class EmailService {
@bindThis
public async validateEmailForAccount(emailAddress: string): Promise<{
available: boolean;
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned';
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
}> {
const meta = await this.metaService.fetch();
@ -173,6 +173,8 @@ export class EmailService {
if (meta.enableActiveEmailValidation) {
if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
} else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) {
validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey);
} else {
validated = await validateEmail({
email: emailAddress,
@ -201,6 +203,8 @@ export class EmailService {
validated.reason === 'disposable' ? 'disposable' :
validated.reason === 'mx' ? 'mx' :
validated.reason === 'smtp' ? 'smtp' :
validated.reason === 'network' ? 'network' :
validated.reason === 'blacklist' ? 'blacklist' :
null,
};
}
@ -265,4 +269,67 @@ export class EmailService {
reason: null,
};
}
private async trueMail<T>(truemailInstance: string, emailAddress: string, truemailAuthKey: string): Promise<{
valid: boolean;
reason: 'used' | 'format' | 'blacklist' | 'mx' | 'smtp' | 'network' | T | null;
}> {
const endpoint = truemailInstance + '?email=' + emailAddress;
try {
const res = await this.httpRequestService.send(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: truemailAuthKey
},
});
const json = (await res.json()) as {
email: string;
success: boolean;
errors?: {
list_match?: string;
regex?: string;
mx?: string;
smtp?: string;
} | null;
};
if (json.email === undefined || (json.email !== undefined && json.errors?.regex)) {
return {
valid: false,
reason: 'format',
};
}
if (json.errors?.smtp) {
return {
valid: false,
reason: 'smtp',
};
}
if (json.errors?.mx) {
return {
valid: false,
reason: 'mx',
};
}
if (!json.success) {
return {
valid: false,
reason: json.errors?.list_match as T || 'blacklist',
};
}
return {
valid: true,
reason: null,
};
} catch (error) {
return {
valid: false,
reason: 'network',
};
}
}
}

View file

@ -58,6 +58,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -862,7 +863,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.relayService.deliverToRelays(user, noteActivity);
}
dm.execute();
trackPromise(dm.execute());
})();
}
//#endregion

View file

@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { trackPromise } from '@/misc/promise-tracker.js';
@Injectable()
export class NoteReadService implements OnApplicationShutdown {
@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown {
// TODO: ↓まとめてクエリしたい
this.noteUnreadsRepository.countBy({
trackPromise(this.noteUnreadsRepository.countBy({
userId: userId,
isMentioned: true,
}).then(mentionsCount => {
@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
}
});
}));
this.noteUnreadsRepository.countBy({
trackPromise(this.noteUnreadsRepository.countBy({
userId: userId,
isSpecified: true,
}).then(specifiedCount => {
@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
}
});
}));
}
}

View file

@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import { UserListService } from '@/core/UserListService.js';
import type { FilterUnionByProperty } from '@/types.js';
import { trackPromise } from '@/misc/promise-tracker.js';
@Injectable()
export class NotificationService implements OnApplicationShutdown {
@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown {
}
@bindThis
public async createNotification<T extends MiNotification['type']>(
public createNotification<T extends MiNotification['type']>(
notifieeId: MiUser['id'],
type: T,
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
notifierId?: MiUser['id'] | null,
) {
trackPromise(
this.#createNotificationInternal(notifieeId, type, data, notifierId),
);
}
async #createNotificationInternal<T extends MiNotification['type']>(
notifieeId: MiUser['id'],
type: T,
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,

View file

@ -3,12 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setTimeout } from 'node:timers/promises';
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
import { allSettled } from '@/misc/promise-tracker.js';
import type { Provider } from '@nestjs/common';
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
@ -106,14 +106,9 @@ export class QueueModule implements OnApplicationShutdown {
) {}
public async dispose(): Promise<void> {
if (process.env.NODE_ENV === 'test') {
// XXX:
// Shutting down the existing connections causes errors on Jest as
// Misskey has asynchronous postgres/redis connections that are not
// awaited.
// Let's wait for some random time for them to finish.
await setTimeout(5000);
}
// Wait for all potential queue jobs
await allSettled();
// And then close all queues
await Promise.all([
this.systemQueue.close(),
this.endedPollNotificationQueue.close(),

View file

@ -17,6 +17,7 @@ import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '.
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
import { MiNote } from '@/models/Note.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
@Injectable()
export class QueueService {
@ -75,11 +76,15 @@ export class QueueService {
if (content == null) return null;
if (to == null) return null;
const contentBody = JSON.stringify(content);
const digest = ApRequestCreator.createDigest(contentBody);
const data: DeliverJobData = {
user: {
id: user.id,
},
content,
content: contentBody,
digest,
to,
isSharedInbox,
};
@ -104,6 +109,8 @@ export class QueueService {
@bindThis
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
if (content == null) return null;
const contentBody = JSON.stringify(content);
const digest = ApRequestCreator.createDigest(contentBody);
const opts = {
attempts: this.config.deliverJobMaxAttempts ?? 12,
@ -118,7 +125,8 @@ export class QueueService {
name: d[0],
data: {
user,
content,
content: contentBody,
digest,
to: d[0],
isSharedInbox: d[1],
} as DeliverJobData,
@ -185,6 +193,16 @@ export class QueueService {
});
}
@bindThis
public createExportClipsJob(user: ThinUser) {
return this.dbQueue.add('exportClips', {
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
});
}
@bindThis
public createExportFavoritesJob(user: ThinUser) {
return this.dbQueue.add('exportFavorites', {

View file

@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
const FALLBACK = '❤';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
@ -280,7 +281,7 @@ export class ReactionService {
}
}
dm.execute();
trackPromise(dm.execute());
}
//#endregion
}
@ -328,7 +329,7 @@ export class ReactionService {
dm.addDirectRecipe(reactee as MiRemoteUser);
}
dm.addFollowersRecipe();
dm.execute();
trackPromise(dm.execute());
}
//#endregion
}

View file

@ -144,7 +144,7 @@ class DeliverManager {
}
// deliver
this.queueService.deliverMany(this.actor, this.activity, inboxes);
await this.queueService.deliverMany(this.actor, this.activity, inboxes);
}
}

View file

@ -97,6 +97,8 @@ export class ApInboxService {
} catch (err) {
if (err instanceof Error || typeof err === 'string') {
this.logger.error(err);
} else {
throw err;
}
}
}
@ -256,7 +258,7 @@ export class ApInboxService {
const targetUri = getApId(activity.object);
this.announceNote(actor, activity, targetUri);
await this.announceNote(actor, activity, targetUri);
}
@bindThis
@ -288,7 +290,7 @@ export class ApInboxService {
} catch (err) {
// 対象が4xxならスキップ
if (err instanceof StatusError) {
if (err.isClientError) {
if (!err.isRetryable) {
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
return;
}
@ -373,7 +375,7 @@ export class ApInboxService {
});
if (isPost(object)) {
this.createNote(resolver, actor, object, false, activity);
await this.createNote(resolver, actor, object, false, activity);
} else {
this.logger.warn(`Unknown type: ${getApType(object)}`);
}
@ -404,7 +406,7 @@ export class ApInboxService {
await this.apNoteService.createNote(note, resolver, silent);
return 'ok';
} catch (err) {
if (err instanceof StatusError && err.isClientError) {
if (err instanceof StatusError && !err.isRetryable) {
return `skip ${err.statusCode}`;
} else {
throw err;

View file

@ -34,9 +34,9 @@ type PrivateKey = {
};
export class ApRequestCreator {
static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }): Signed {
static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed {
const u = new URL(args.url);
const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
const digestHeader = args.digest ?? this.createDigest(args.body);
const request: Request = {
url: u.href,
@ -59,6 +59,10 @@ export class ApRequestCreator {
};
}
static createDigest(body: string) {
return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
}
static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
const u = new URL(args.url);
@ -145,8 +149,8 @@ export class ApRequestService {
}
@bindThis
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown): Promise<void> {
const body = JSON.stringify(object);
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> {
const body = typeof object === 'string' ? object : JSON.stringify(object);
const keypair = await this.userKeypairService.getUserKeypair(user.id);
@ -157,6 +161,7 @@ export class ApRequestService {
},
url,
body,
digest,
additionalHeaders: {
},
});

View file

@ -221,7 +221,7 @@ export class ApNoteService {
return { status: 'ok', res };
} catch (e) {
return {
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
};
}
};

View file

@ -369,6 +369,7 @@ export class NoteEntityService implements OnModuleInit {
color: channel.color,
isSensitive: channel.isSensitive,
allowRenoteToExternal: channel.allowRenoteToExternal,
userId: channel.userId,
} : undefined,
mentions: note.mentions && note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri ?? undefined,

View file

@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown {
const log = [] as any[];
ev.on('requestServerStatsLog', x => {
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50));
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length));
});
const tick = async () => {

View file

@ -79,5 +79,6 @@ export const DI = {
flashLikesRepository: Symbol('flashLikesRepository'),
userMemosRepository: Symbol('userMemosRepository'),
noteEditRepository: Symbol('noteEditRepository'),
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
//#endregion
};

View file

@ -71,8 +71,11 @@ export default class Logger {
let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`;
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
console.log(important ? chalk.bold(log) : log);
if (level === 'error' && data) console.log(data);
const args: unknown[] = [important ? chalk.bold(log) : log];
if (data != null) {
args.push(data);
}
console.log(...args);
}
@bindThis

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set();
/**
* This tracks promises that other modules decided not to wait for,
* and makes sure they are all settled before fully closing down the server.
*/
export function trackPromise(promise: Promise<unknown>) {
if (process.env.NODE_ENV !== 'test') {
return;
}
const ref = new WeakRef(promise);
promiseRefs.add(ref);
promise.finally(() => promiseRefs.delete(ref));
}
export async function allSettled(): Promise<void> {
await Promise.allSettled([...promiseRefs].map(r => r.deref()));
}

View file

@ -7,6 +7,7 @@ export class StatusError extends Error {
public statusCode: number;
public statusMessage?: string;
public isClientError: boolean;
public isRetryable: boolean;
constructor(message: string, statusCode: number, statusMessage?: string) {
super(message);
@ -14,5 +15,6 @@ export class StatusError extends Error {
this.statusCode = statusCode;
this.statusMessage = statusMessage;
this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
this.isRetryable = !this.isClientError || this.statusCode === 429;
}
}

View file

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@Entity('bubble_game_record')
export class MiBubbleGameRecord {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Index()
@Column('timestamp with time zone')
public seededAt: Date;
@Column('varchar', {
length: 1024,
})
public seed: string;
@Column('integer')
public gameVersion: number;
@Column('varchar', {
length: 128,
})
public gameMode: string;
@Index()
@Column('integer')
public score: number;
@Column('jsonb', {
default: [],
})
public logs: any[];
@Column('boolean', {
default: false,
})
public isVerified: boolean;
}

View file

@ -196,6 +196,29 @@ export class MiMeta {
})
public hcaptchaSecretKey: string | null;
@Column('boolean', {
default: false,
})
public enableMcaptcha: boolean;
@Column('varchar', {
length: 1024,
nullable: true,
})
public mcaptchaSitekey: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public mcaptchaSecretKey: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public mcaptchaInstanceUrl: string | null;
@Column('boolean', {
default: false,
})
@ -362,6 +385,12 @@ export class MiMeta {
})
public privacyPolicyUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public donationUrl: string | null;
@Column('varchar', {
length: 8192,
nullable: true,
@ -467,6 +496,23 @@ export class MiMeta {
})
public verifymailAuthKey: string | null;
@Column('boolean', {
default: false,
})
public enableTruemailApi: boolean;
@Column('varchar', {
length: 1024,
nullable: true,
})
public truemailInstance: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public truemailAuthKey: string | null;
@Column('boolean', {
default: true,
})

View file

@ -5,7 +5,7 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit } from './_.js';
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit, MiBubbleGameRecord } from './_.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@ -405,6 +405,12 @@ const $noteEditRepository: Provider = {
inject: [DI.db],
};
const $bubbleGameRecordsRepository: Provider = {
provide: DI.bubbleGameRecordsRepository,
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord),
inject: [DI.db],
};
@Module({
imports: [
],
@ -475,6 +481,7 @@ const $noteEditRepository: Provider = {
$flashLikesRepository,
$userMemosRepository,
$noteEditRepository,
$bubbleGameRecordsRepository,
],
exports: [
$usersRepository,
@ -543,6 +550,7 @@ const $noteEditRepository: Provider = {
$flashLikesRepository,
$userMemosRepository,
$noteEditRepository,
$bubbleGameRecordsRepository,
],
})
export class RepositoryModule {}

View file

@ -69,6 +69,7 @@ import { MiFlash } from '@/models/Flash.js';
import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { NoteEdit } from '@/models/NoteEdit.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import type { Repository } from 'typeorm';
export {
@ -138,6 +139,7 @@ export {
MiFlashLike,
MiUserMemo,
NoteEdit,
MiBubbleGameRecord,
};
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>;
@ -206,3 +208,4 @@ export type FlashsRepository = Repository<MiFlash>;
export type FlashLikesRepository = Repository<MiFlashLike>;
export type UserMemoRepository = Repository<MiUserMemo>;
export type NoteEditRepository = Repository<NoteEdit>;
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>;

View file

@ -148,6 +148,10 @@ export const packedNoteSchema = {
type: 'boolean',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: true,
},
},
},
localOnly: {

View file

@ -77,6 +77,7 @@ import { MiFlash } from '@/models/Flash.js';
import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserMemo } from '@/models/UserMemo.js';
import { NoteEdit } from '@/models/NoteEdit.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js';
@ -192,6 +193,7 @@ export const entities = [
MiFlashLike,
MiUserMemo,
NoteEdit,
MiBubbleGameRecord,
...charts,
];

View file

@ -25,6 +25,7 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
@ -56,6 +57,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
ExportAccountDataProcessorService,
ExportCustomEmojisProcessorService,
ExportNotesProcessorService,
ExportClipsProcessorService,
ExportFavoritesProcessorService,
ExportFollowingProcessorService,
ExportMutingProcessorService,

View file

@ -17,6 +17,7 @@ import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesP
import { ExportAccountDataProcessorService } from './processors/ExportAccountDataProcessorService.js';
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
@ -94,6 +95,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private exportAccountDataProcessorService: ExportAccountDataProcessorService,
private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
private exportNotesProcessorService: ExportNotesProcessorService,
private exportClipsProcessorService: ExportClipsProcessorService,
private exportFavoritesProcessorService: ExportFavoritesProcessorService,
private exportFollowingProcessorService: ExportFollowingProcessorService,
private exportMutingProcessorService: ExportMutingProcessorService,
@ -169,6 +171,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'exportAccountData': return this.exportAccountDataProcessorService.process(job);
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
case 'exportNotes': return this.exportNotesProcessorService.process(job);
case 'exportClips': return this.exportClipsProcessorService.process(job);
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
case 'exportMuting': return this.exportMutingProcessorService.process(job);

View file

@ -72,7 +72,7 @@ export class DeliverProcessorService {
}
try {
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content);
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
// Update stats
this.federatedInstanceService.fetch(host).then(i => {
@ -111,7 +111,7 @@ export class DeliverProcessorService {
if (res instanceof StatusError) {
// 4xx
if (res.isClientError) {
if (!res.isRetryable) {
// 相手が閉鎖していることを明示しているため、配送停止する
if (job.data.isSharedInbox && res.statusCode === 410) {
this.federatedInstanceService.fetch(host).then(i => {

View file

@ -0,0 +1,206 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs';
import { Writable } from 'node:stream';
import { Inject, Injectable, StreamableFile } from '@nestjs/common';
import { MoreThan } from 'typeorm';
import { format as dateFormat } from 'date-fns';
import { DI } from '@/di-symbols.js';
import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import type { MiPoll } from '@/models/Poll.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@Injectable()
export class ExportClipsProcessorService {
private logger: Logger;
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository,
@Inject(DI.clipNotesRepository)
private clipNotesRepository: ClipNotesRepository,
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
private idService: IdService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
}
@bindThis
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
this.logger.info(`Exporting clips of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
return;
}
// Create temp file
const [path, cleanup] = await createTemp();
this.logger.info(`Temp file is ${path}`);
try {
const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' }));
const writer = stream.getWriter();
writer.closed.catch(this.logger.error);
await writer.write('[');
await this.processClips(writer, user, job);
await writer.write(']');
await writer.close();
this.logger.succ(`Exported to: ${path}`);
const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`);
} finally {
cleanup();
}
}
async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job<DbJobDataWithUser>) {
let exportedClipsCount = 0;
let cursor: MiClip['id'] | null = null;
while (true) {
const clips = await this.clipsRepository.find({
where: {
userId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
});
if (clips.length === 0) {
job.updateProgress(100);
break;
}
cursor = clips.at(-1)?.id ?? null;
for (const clip of clips) {
// Stringify but remove the last `]}`
const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2);
const isFirst = exportedClipsCount === 0;
await writer.write(isFirst ? content : ',\n' + content);
await this.processClipNotes(writer, clip.id);
await writer.write(']}');
exportedClipsCount++;
}
const total = await this.clipsRepository.countBy({
userId: user.id,
});
job.updateProgress(exportedClipsCount / total);
}
}
async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> {
let exportedClipNotesCount = 0;
let cursor: MiClipNote['id'] | null = null;
while (true) {
const clipNotes = await this.clipNotesRepository.find({
where: {
clipId,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
relations: ['note', 'note.user'],
}) as (MiClipNote & { note: MiNote & { user: MiUser } })[];
if (clipNotes.length === 0) {
break;
}
cursor = clipNotes.at(-1)?.id ?? null;
for (const clipNote of clipNotes) {
let poll: MiPoll | undefined;
if (clipNote.note.hasPoll) {
poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
}
const content = JSON.stringify(this.serializeClipNote(clipNote, poll));
const isFirst = exportedClipNotesCount === 0;
await writer.write(isFirst ? content : ',\n' + content);
exportedClipNotesCount++;
}
}
}
private serializeClip(clip: MiClip): Record<string, unknown> {
return {
id: clip.id,
name: clip.name,
description: clip.description,
lastClippedAt: clip.lastClippedAt?.toISOString(),
clipNotes: [],
};
}
private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record<string, unknown> {
return {
id: clip.id,
createdAt: this.idService.parse(clip.id).date.toISOString(),
note: {
id: clip.note.id,
text: clip.note.text,
createdAt: this.idService.parse(clip.note.id).date.toISOString(),
fileIds: clip.note.fileIds,
replyId: clip.note.replyId,
renoteId: clip.note.renoteId,
poll: poll,
cw: clip.note.cw,
visibility: clip.note.visibility,
visibleUserIds: clip.note.visibleUserIds,
localOnly: clip.note.localOnly,
reactionAcceptance: clip.note.reactionAcceptance,
uri: clip.note.uri,
url: clip.note.url,
user: {
id: clip.note.user.id,
name: clip.note.user.name,
username: clip.note.user.username,
host: clip.note.user.host,
uri: clip.note.user.uri,
},
},
};
}
}

View file

@ -1,5 +1,6 @@
import * as fs from 'node:fs';
import * as vm from 'node:vm';
import * as crypto from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import { ZipReader } from 'slacc';
import { DI } from '@/di-symbols.js';
@ -72,7 +73,6 @@ export class ImportNotesProcessorService {
}
}
// Function was taken from Firefish and modified for our needs
@bindThis
private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise<any[]> {
type NotesMap = {
@ -378,7 +378,11 @@ export class ImportNotesProcessorService {
return;
}
if (toot.directMessage) return;
const followers = toot.to.some((str: string) => str.includes('/followers'));
if (toot.directMessage || !toot.to.includes('https://www.w3.org/ns/activitystreams#Public') && !followers) return;
const visibility = followers ? toot.cc.includes('https://www.w3.org/ns/activitystreams#Public') ? 'home' : 'followers' : 'public';
const date = new Date(toot.object.published);
let text = undefined;
@ -417,7 +421,7 @@ export class ImportNotesProcessorService {
}
}
const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply });
const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, visibility: visibility, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply });
if (toot.childNotes) this.queueService.createImportMastoToDbJob(user, toot.childNotes, createdNote.id);
}
@ -469,7 +473,9 @@ export class ImportNotesProcessorService {
for await (const file of post.object.attachment) {
const slashdex = file.url.lastIndexOf('/');
const name = file.url.substring(slashdex + 1);
const filename = file.url.substring(slashdex + 1);
const hash = crypto.createHash('md5').update(file.url).digest('base64url');
const name = `${hash}-${filename}`;
const [filePath, cleanup] = await createTemp();
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: pleroFolder?.id });
@ -484,6 +490,7 @@ export class ImportNotesProcessorService {
user: user,
path: filePath,
name: name,
comment: file.name,
folderId: pleroFolder?.id,
});
files.push(driveFile);

View file

@ -85,7 +85,7 @@ export class InboxProcessorService {
} catch (err) {
// 対象が4xxならスキップ
if (err instanceof StatusError) {
if (err.isClientError) {
if (!err.isRetryable) {
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
}
throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);

View file

@ -71,7 +71,7 @@ export class WebhookDeliverProcessorService {
if (res instanceof StatusError) {
// 4xx
if (res.isClientError) {
if (!res.isRetryable) {
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
}

View file

@ -15,7 +15,9 @@ export type DeliverJobData = {
/** Actor */
user: ThinUser;
/** Activity */
content: unknown;
content: string;
/** Digest header */
digest: string;
/** inbox URL to deliver */
to: string;
/** whether it is sharedInbox */

View file

@ -129,6 +129,13 @@ export class ActivityPubServerService {
this is also inspired by FireFish's `checkFetch`
*/
/* tell any caching proxy that they should not cache these
responses: we wouldn't want the proxy to return a 403 to
someone presenting a valid signature, or return a cached
response body to someone we've blocked!
*/
reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
/* we always allow requests about our instance actor, because when
a remote instance needs to check our signature on a request we
sent, it will need to fetch information about the user that
@ -155,23 +162,25 @@ export class ActivityPubServerService {
return true;
}
const keyId = new URL(signature.keyId);
const keyHost = this.utilityService.toPuny(keyId.hostname);
const logPrefix = `${request.id} ${request.url} (by ${request.headers['user-agent']}) apparently from ${keyHost}:`;
if (signature.params.headers.indexOf('host') === -1
|| request.headers.host !== this.config.host) {
// no destination host, or not us: refuse
this.authlogger.warn(`${request.id} ${request.url} no destination host, or not us: refuse`);
this.authlogger.warn(`${logPrefix} no destination host, or not us: refuse`);
reply.code(401);
return true;
}
const keyId = new URL(signature.keyId);
const keyHost = this.utilityService.toPuny(keyId.hostname);
const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, keyHost)) {
/* blocked instance: refuse (we don't care if the signature is
good, if they even pretend to be from a blocked instance,
they're out) */
this.authlogger.warn(`${request.id} ${request.url} instance ${keyHost} is blocked: refuse`);
this.authlogger.warn(`${logPrefix} instance is blocked: refuse`);
reply.code(401);
return true;
}
@ -186,13 +195,13 @@ export class ActivityPubServerService {
/* keyId is often in the shape `${user.uri}#${keyname}`, try
fetching information about the remote user */
const candidate = formatURL(keyId, { fragment: false });
this.authlogger.info(`${request.id} ${request.url} we don't know the user for keyId ${keyId}, trying to fetch via ${candidate}`);
this.authlogger.info(`${logPrefix} we don't know the user for keyId ${keyId}, trying to fetch via ${candidate}`);
authUser = await this.apDbResolverService.getAuthUserFromApId(candidate);
}
if (authUser?.key == null) {
// we can't figure out who the signer is, or we can't get their key: refuse
this.authlogger.warn(`${request.id} ${request.url} we can't figure out who the signer is, or we can't get their key: refuse`);
this.authlogger.warn(`${logPrefix} we can't figure out who the signer is, or we can't get their key: refuse`);
reply.code(401);
return true;
}
@ -200,20 +209,20 @@ export class ActivityPubServerService {
let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
if (!httpSignatureValidated) {
this.authlogger.info(`${request.id} ${request.url} failed to validate signature, re-fetching the key for ${authUser.user.uri}`);
this.authlogger.info(`${logPrefix} failed to validate signature, re-fetching the key for ${authUser.user.uri}`);
// maybe they changed their key? refetch it
authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user);
if (authUser.key != null) {
httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
} else {
this.authlogger.warn(`${request.id} ${request.url} failed to re-fetch key for ${authUser.user}`);
this.authlogger.warn(`${logPrefix} failed to re-fetch key for ${authUser.user}`);
}
}
if (!httpSignatureValidated) {
// bad signature: refuse
this.authlogger.info(`${request.id} ${request.url} failed to validate signature: refuse`);
this.authlogger.info(`${logPrefix} failed to validate signature: refuse`);
reply.code(401);
return true;
}
@ -322,11 +331,11 @@ export class ActivityPubServerService {
if (profile.followersVisibility === 'private') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
return;
} else if (profile.followersVisibility === 'followers') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
return;
}
//#endregion
@ -378,7 +387,7 @@ export class ActivityPubServerService {
user.followersCount,
`${partOf}?page=true`,
);
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
}
@ -416,11 +425,11 @@ export class ActivityPubServerService {
if (profile.followingVisibility === 'private') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
return;
} else if (profile.followingVisibility === 'followers') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
return;
}
//#endregion
@ -472,7 +481,7 @@ export class ActivityPubServerService {
user.followingCount,
`${partOf}?page=true`,
);
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
}
@ -513,7 +522,7 @@ export class ActivityPubServerService {
renderedNotes,
);
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
}
@ -604,7 +613,7 @@ export class ActivityPubServerService {
`${partOf}?page=true`,
`${partOf}?page=true&since_id=000000000000000000000000`,
);
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
}
@ -617,7 +626,7 @@ export class ActivityPubServerService {
return;
}
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
}
@ -707,7 +716,7 @@ export class ActivityPubServerService {
return;
}
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return this.apRendererService.addContext(await this.apRendererService.renderNote(note, false));
});
@ -730,7 +739,7 @@ export class ActivityPubServerService {
return;
}
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.packActivity(note)));
});
@ -775,7 +784,7 @@ export class ActivityPubServerService {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
if (this.userEntityService.isLocalUser(user)) {
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair)));
} else {
@ -825,7 +834,7 @@ export class ActivityPubServerService {
return;
}
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji)));
});
@ -848,7 +857,7 @@ export class ActivityPubServerService {
return;
}
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, note)));
});
@ -876,7 +885,7 @@ export class ActivityPubServerService {
return;
}
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
});
@ -913,7 +922,7 @@ export class ActivityPubServerService {
return;
}
reply.header('Cache-Control', 'public, max-age=180');
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
});

View file

@ -9,7 +9,7 @@ import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
import rename from 'rename';
import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp';
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import type { Config } from '@/config.js';
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';

View file

@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { MemorySingleCache } from '@/misc/cache.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
@ -109,6 +108,7 @@ export class NodeinfoServerService {
tosUrl: meta.termsOfServiceUrl,
privacyPolicyUrl: meta.privacyPolicyUrl,
impressumUrl: meta.impressumUrl,
donationUrl: meta.donationUrl,
repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl,
disableRegistration: meta.disableRegistration,
@ -118,7 +118,7 @@ export class NodeinfoServerService {
emailRequiredForSignup: meta.emailRequiredForSignup,
enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
maxNoteTextLength: this.config.maxNoteLength,
enableEmail: meta.enableEmail,
enableServiceWorker: meta.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null,

View file

@ -214,6 +214,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
@ -376,6 +377,8 @@ import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
import * as ep___retention from './endpoints/retention.js';
import * as ep___sponsors from './endpoints/sponsors.js';
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
import { GetterService } from './GetterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
import type { Provider } from '@nestjs/common';
@ -588,6 +591,7 @@ const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass:
const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default };
const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default };
const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default };
const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default };
const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default };
const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default };
const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default };
@ -750,6 +754,8 @@ const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.d
const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default };
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.default };
const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default };
const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default };
@Module({
imports: [
@ -966,6 +972,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$i_exportFollowing,
$i_exportMute,
$i_exportNotes,
$i_exportClips,
$i_exportFavorites,
$i_exportUserLists,
$i_exportAntennas,
@ -1128,6 +1135,8 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$fetchExternalResources,
$retention,
$sponsors,
$bubbleGame_register,
$bubbleGame_ranking,
],
exports: [
$admin_meta,
@ -1338,6 +1347,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$i_exportFollowing,
$i_exportMute,
$i_exportNotes,
$i_exportClips,
$i_exportFavorites,
$i_exportUserLists,
$i_exportAntennas,
@ -1497,6 +1507,8 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$fetchExternalResources,
$retention,
$sponsors,
$bubbleGame_register,
$bubbleGame_ranking,
],
})
export class EndpointsModule {}

View file

@ -70,6 +70,7 @@ export class SignupApiService {
'hcaptcha-response'?: string;
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'm-captcha-response'?: string;
}
}>,
reply: FastifyReply,
@ -87,6 +88,12 @@ export class SignupApiService {
});
}
if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) {
await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);

View file

@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Schema } from '@/misc/json-schema.js';
import { permissions } from 'misskey-js';
import type { Schema } from '@/misc/json-schema.js';
import { RolePolicies } from '@/core/RoleService.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
@ -215,6 +215,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
@ -377,6 +378,8 @@ import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
import * as ep___retention from './endpoints/retention.js';
import * as ep___sponsors from './endpoints/sponsors.js';
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
const eps = [
['admin/meta', ep___admin_meta],
@ -587,6 +590,7 @@ const eps = [
['i/export-following', ep___i_exportFollowing],
['i/export-mute', ep___i_exportMute],
['i/export-notes', ep___i_exportNotes],
['i/export-clips', ep___i_exportClips],
['i/export-favorites', ep___i_exportFavorites],
['i/export-user-lists', ep___i_exportUserLists],
['i/export-antennas', ep___i_exportAntennas],
@ -749,6 +753,8 @@ const eps = [
['fetch-external-resources', ep___fetchExternalResources],
['retention', ep___retention],
['sponsors', ep___sponsors],
['bubble-game/register', ep___bubbleGame_register],
['bubble-game/ranking', ep___bubbleGame_ranking],
];
interface IEndpointMetaBase {

View file

@ -98,11 +98,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.query) {
q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' })
.orderBy('length(emoji.name)', 'ASC');
}
const emojis = await q
.orderBy('emoji.id', 'DESC')
.addOrderBy('emoji.id', 'DESC')
.limit(ps.limit)
.getMany();

View file

@ -91,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
//const emojis = await q.limit(ps.limit).getMany();
emojis = await q.getMany();
emojis = await q.orderBy('length(emoji.name)', 'ASC').getMany();
const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g);
if (queryarry) {

View file

@ -45,6 +45,18 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
enableMcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
mcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
mcaptchaInstanceUrl: {
type: 'string',
optional: false, nullable: true,
},
enableRecaptcha: {
type: 'boolean',
optional: false, nullable: false,
@ -174,6 +186,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
mcaptchaSecretKey: {
type: 'string',
optional: false, nullable: true,
},
recaptchaSecretKey: {
type: 'string',
optional: false, nullable: true,
@ -299,6 +315,18 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
enableTruemailApi: {
type: 'boolean',
optional: false, nullable: false,
},
truemailInstance: {
type: 'string',
optional: false, nullable: true,
},
truemailAuthKey: {
type: 'string',
optional: false, nullable: true,
},
enableChartsForRemoteUser: {
type: 'boolean',
optional: false, nullable: false,
@ -387,6 +415,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
donationUrl: {
type: 'string',
optional: false, nullable: true,
},
maintainerEmail: {
type: 'string',
optional: false, nullable: true,
@ -470,12 +502,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
donationUrl: instance.donationUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
approvalRequiredForSignup: instance.approvalRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha,
mcaptchaSiteKey: instance.mcaptchaSitekey,
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
@ -508,6 +544,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
preservedUsernames: instance.preservedUsernames,
bubbleInstances: instance.bubbleInstances,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
mcaptchaSecretKey: instance.mcaptchaSecretKey,
recaptchaSecretKey: instance.recaptchaSecretKey,
turnstileSecretKey: instance.turnstileSecretKey,
sensitiveMediaDetection: instance.sensitiveMediaDetection,
@ -543,6 +580,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableActiveEmailValidation: instance.enableActiveEmailValidation,
enableVerifymailApi: instance.enableVerifymailApi,
verifymailAuthKey: instance.verifymailAuthKey,
enableTruemailApi: instance.enableTruemailApi,
truemailInstance: instance.truemailInstance,
truemailAuthKey: instance.truemailAuthKey,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,

View file

@ -65,6 +65,10 @@ export const paramDef = {
enableHcaptcha: { type: 'boolean' },
hcaptchaSiteKey: { type: 'string', nullable: true },
hcaptchaSecretKey: { type: 'string', nullable: true },
enableMcaptcha: { type: 'boolean' },
mcaptchaSiteKey: { type: 'string', nullable: true },
mcaptchaInstanceUrl: { type: 'string', nullable: true },
mcaptchaSecretKey: { type: 'string', nullable: true },
enableRecaptcha: { type: 'boolean' },
recaptchaSiteKey: { type: 'string', nullable: true },
recaptchaSecretKey: { type: 'string', nullable: true },
@ -101,6 +105,7 @@ export const paramDef = {
repositoryUrl: { type: 'string' },
feedbackUrl: { type: 'string' },
impressumUrl: { type: 'string', nullable: true },
donationUrl: { type: 'string', nullable: true },
privacyPolicyUrl: { type: 'string', nullable: true },
useObjectStorage: { type: 'boolean' },
objectStorageBaseUrl: { type: 'string', nullable: true },
@ -119,6 +124,9 @@ export const paramDef = {
enableActiveEmailValidation: { type: 'boolean' },
enableVerifymailApi: { type: 'boolean' },
verifymailAuthKey: { type: 'string', nullable: true },
enableTruemailApi: { type: 'boolean' },
truemailInstance: { type: 'string', nullable: true },
truemailAuthKey: { type: 'string', nullable: true },
enableChartsForRemoteUser: { type: 'boolean' },
enableChartsForFederatedInstances: { type: 'boolean' },
enableServerMachineStats: { type: 'boolean' },
@ -279,6 +287,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.hcaptchaSecretKey = ps.hcaptchaSecretKey;
}
if (ps.enableMcaptcha !== undefined) {
set.enableMcaptcha = ps.enableMcaptcha;
}
if (ps.mcaptchaSiteKey !== undefined) {
set.mcaptchaSitekey = ps.mcaptchaSiteKey;
}
if (ps.mcaptchaInstanceUrl !== undefined) {
set.mcaptchaInstanceUrl = ps.mcaptchaInstanceUrl;
}
if (ps.mcaptchaSecretKey !== undefined) {
set.mcaptchaSecretKey = ps.mcaptchaSecretKey;
}
if (ps.enableRecaptcha !== undefined) {
set.enableRecaptcha = ps.enableRecaptcha;
}
@ -383,6 +407,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.impressumUrl = ps.impressumUrl;
}
if (ps.donationUrl !== undefined) {
set.donationUrl = ps.donationUrl;
}
if (ps.privacyPolicyUrl !== undefined) {
set.privacyPolicyUrl = ps.privacyPolicyUrl;
}
@ -471,6 +499,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
if (ps.enableTruemailApi !== undefined) {
set.enableTruemailApi = ps.enableTruemailApi;
}
if (ps.truemailInstance !== undefined) {
if (ps.truemailInstance === '') {
set.truemailInstance = null;
} else {
set.truemailInstance = ps.truemailInstance;
}
}
if (ps.truemailAuthKey !== undefined) {
if (ps.truemailAuthKey === '') {
set.truemailAuthKey = null;
} else {
set.truemailAuthKey = ps.truemailAuthKey;
}
}
if (ps.enableChartsForRemoteUser !== undefined) {
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
}

View file

@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { IdService } from '@/core/IdService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
antenna.isActive = true;
antenna.lastUsedAt = new Date();
this.antennasRepository.update(antenna.id, antenna);
trackPromise(this.antennasRepository.update(antenna.id, antenna));
if (needPublishEvent) {
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);

View file

@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { MoreThan } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { BubbleGameRecordsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
export const meta = {
tags: [],
allowGet: true,
cacheSec: 60,
errors: {
},
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: { type: 'string', format: 'misskey:id' },
score: { type: 'integer' },
user: { ref: 'UserLite' },
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
gameMode: { type: 'string' },
},
required: ['gameMode'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.bubbleGameRecordsRepository)
private bubbleGameRecordsRepository: BubbleGameRecordsRepository,
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps) => {
const records = await this.bubbleGameRecordsRepository.find({
where: {
gameMode: ps.gameMode,
seededAt: MoreThan(new Date(Date.now() - 1000 * 60 * 60 * 24 * 7)),
},
order: {
score: 'DESC',
},
take: 10,
relations: ['user'],
});
const users = await this.userEntityService.packMany(records.map(r => r.user!), null, { detail: false });
return records.map(r => ({
id: r.id,
score: r.score,
user: users.find(u => u.id === r.user!.id),
}));
});
}
}

View file

@ -0,0 +1,86 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js';
import type { BubbleGameRecordsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: [],
requireCredential: true,
kind: 'write:account',
limit: {
duration: ms('1hour'),
max: 120,
minInterval: ms('30sec'),
},
errors: {
invalidSeed: {
message: 'Provided seed is invalid.',
code: 'INVALID_SEED',
id: 'eb627bc7-574b-4a52-a860-3c3eae772b88',
},
},
res: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
score: { type: 'integer', minimum: 0 },
seed: { type: 'string', minLength: 1, maxLength: 1024 },
logs: { type: 'array' },
gameMode: { type: 'string' },
gameVersion: { type: 'integer' },
},
required: ['score', 'seed', 'logs', 'gameMode', 'gameVersion'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.bubbleGameRecordsRepository)
private bubbleGameRecordsRepository: BubbleGameRecordsRepository,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const seedDate = new Date(parseInt(ps.seed, 10));
const now = new Date();
// シードが未来なのは通常のプレイではありえないので弾く
if (seedDate.getTime() > now.getTime()) {
throw new ApiError(meta.errors.invalidSeed);
}
// シードが古すぎる(1時間以上前)のも弾く
if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60) {
throw new ApiError(meta.errors.invalidSeed);
}
await this.bubbleGameRecordsRepository.insert({
id: this.idService.gen(now.getTime()),
seed: ps.seed,
seededAt: seedDate,
userId: me.id,
score: ps.score,
logs: ps.logs,
gameMode: ps.gameMode,
gameVersion: ps.gameVersion,
isVerified: false,
});
});
}
}

View file

@ -41,6 +41,7 @@ export const paramDef = {
subscribing: { type: 'boolean', nullable: true },
publishing: { type: 'boolean', nullable: true },
nsfw: { type: 'boolean', nullable: true },
bubble: { type: 'boolean', nullable: true },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
offset: { type: 'integer', default: 0 },
sort: {
@ -148,6 +149,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
if (typeof ps.bubble === 'boolean') {
const meta = await this.metaService.fetch(true);
if (ps.bubble) {
if (meta.bubbleInstances.length === 0) {
return [];
}
query.andWhere('instance.host IN (:...bubble)', {
bubble: meta.bubbleInstances,
});
} else if (meta.bubbleInstances.length > 0) {
query.andWhere('instance.host NOT IN (:...bubble)', {
bubble: meta.bubbleInstances,
});
}
}
if (typeof ps.federating === 'boolean') {
if (ps.federating) {
query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))');

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: ms('1day'),
max: 1,
},
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
this.queueService.createExportClipsJob(me);
});
}
}

View file

@ -7,7 +7,6 @@ import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import JSON5 from 'json5';
import type { AdsRepository, UsersRepository } from '@/models/_.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { MetaService } from '@/core/MetaService.js';
@ -116,6 +115,18 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
enableMcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
mcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
mcaptchaInstanceUrl: {
type: 'string',
optional: false, nullable: true,
},
enableRecaptcha: {
type: 'boolean',
optional: false, nullable: false,
@ -280,6 +291,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
donationUrl: {
type: 'string',
optional: false, nullable: true,
},
logoImageUrl: {
type: 'string',
optional: false, nullable: true,
@ -354,12 +369,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
donationUrl: instance.donationUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
approvalRequiredForSignup: instance.approvalRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha,
mcaptchaSiteKey: instance.mcaptchaSitekey,
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
enableRecaptcha: instance.enableRecaptcha,
enableAchievements: instance.enableAchievements,
recaptchaSiteKey: instance.recaptchaSiteKey,
@ -375,7 +394,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
maxNoteTextLength: this.config.maxNoteLength,
// クライアントの手間を減らすためあらかじめJSONに変換しておく
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,

View file

@ -11,7 +11,7 @@ import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesR
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
import type { MiChannel } from '@/models/Channel.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import type { Config } from '@/config.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
@ -82,6 +82,12 @@ export const meta = {
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
},
maxLength: {
message: 'You tried posting a note which is too long.',
code: 'MAX_LENGTH',
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16',
},
cannotCreateAlreadyExpiredPoll: {
message: 'Poll is already expired.',
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
@ -136,7 +142,6 @@ export const paramDef = {
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: true,
},
fileIds: {
@ -162,7 +167,7 @@ export const paramDef = {
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
items: { type: 'string', minLength: 1, maxLength: 150 },
},
multiple: { type: 'boolean' },
expiresAt: { type: 'integer', nullable: true },
@ -184,6 +189,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -203,6 +211,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteCreateService: NoteCreateService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.text && (ps.text.length > this.config.maxNoteLength)) {
throw new ApiError(meta.errors.maxLength);
}
let visibleUsers: MiUser[] = [];
if (ps.visibleUserIds) {
visibleUsers = await this.usersRepository.findBy({

View file

@ -6,7 +6,7 @@ import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesR
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
import type { MiChannel } from '@/models/Channel.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import type { Config } from '@/config.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteEditService } from '@/core/NoteEditService.js';
@ -135,6 +135,12 @@ export const meta = {
code: 'CANNOT_QUOTE_THE_CURRENT_NOTE',
id: '33510210-8452-094c-6227-4a6c05d99f02',
},
maxLength: {
message: 'You tried posting a note which is too long.',
code: 'MAX_LENGTH',
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16',
},
},
} as const;
@ -163,7 +169,6 @@ export const paramDef = {
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: true,
},
fileIds: {
@ -205,7 +210,6 @@ export const paramDef = {
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: false,
},
},
@ -236,6 +240,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -255,6 +262,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEditService: NoteEditService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.text && (ps.text.length > this.config.maxNoteLength)) {
throw new ApiError(meta.errors.maxLength);
}
let visibleUsers: MiUser[] = [];
if (ps.visibleUserIds) {
visibleUsers = await this.usersRepository.findBy({

View file

@ -19,8 +19,8 @@ export const meta = {
limit: {
duration: ms('1hour'),
max: 60,
minInterval: ms('3sec'),
max: 80,
minInterval: ms('1sec'),
},
errors: {

View file

@ -3,10 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import type { NotesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -40,14 +42,27 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private noteEntityService: NoteEntityService,
private getterService: GetterService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNote(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const query = await this.notesRepository.createQueryBuilder('note')
.where('note.id = :noteId', { noteId: ps.noteId });
this.queryService.generateVisibilityQuery(query, me);
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
}
const note = await query.getOne();
if (note === null) {
throw new ApiError(meta.errors.noSuchNote);
}
return await this.noteEntityService.pack(note, me, {
detail: true,

View file

@ -1,5 +1,5 @@
import { Entity } from 'megalodon';
import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import type { Config } from '@/config.js';
import type { MiMeta } from '@/models/Meta.js';
@ -35,7 +35,7 @@ export async function getInstance(
max_featured_tags: 20,
},
statuses: {
max_characters: MAX_NOTE_TEXT_LENGTH,
max_characters: config.maxNoteLength,
max_media_attachments: 16,
characters_reserved_per_url: response.uri.length,
},

View file

@ -21,6 +21,7 @@ class UserListChannel extends Channel {
private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
private listUsersClock: NodeJS.Timeout;
private withFiles: boolean;
private withRenotes: boolean;
constructor(
private userListsRepository: UserListsRepository,
@ -39,6 +40,7 @@ class UserListChannel extends Channel {
public async init(params: any) {
this.listId = params.listId as string;
this.withFiles = params.withFiles ?? false;
this.withRenotes = params.withRenotes ?? true;
// Check existence and owner
const listExist = await this.userListsRepository.exist({
@ -104,6 +106,8 @@ class UserListChannel extends Channel {
}
}
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View file

@ -131,7 +131,7 @@ export class OAuth2ProviderService {
fastify.register(multer.contentParser);
fastify.get('/oauth/authorize', async (request, reply) => {
fastify.get('/authorize', async (request, reply) => {
const query: any = request.query;
let param = "mastodon=true";
if (query.state) param += `&state=${query.state}`;
@ -142,7 +142,7 @@ export class OAuth2ProviderService {
);
});
fastify.get('/oauth/authorize/', async (request, reply) => {
fastify.get('/authorize/', async (request, reply) => {
const query: any = request.query;
let param = "mastodon=true";
if (query.state) param += `&state=${query.state}`;
@ -153,7 +153,7 @@ export class OAuth2ProviderService {
);
});
fastify.post('/oauth/token', { preHandler: upload.none() }, async (request, reply) => {
fastify.post('/token', { preHandler: upload.none() }, async (request, reply) => {
const body: any = request.body || request.query;
if (body.grant_type === "client_credentials") {
const ret = {

View file

@ -327,6 +327,21 @@ export class ClientServerService {
});
});
fastify.get<{ Params: { path: string } }>('/tossface/:path(.*)', async (request, reply) => {
const path = request.params.path;
if (!path.match(/^[0-9a-f-]+\.svg$/)) {
reply.code(404);
return;
}
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
return await reply.sendFile(path, `${_dirname}/../../../../../tossface-emojis/dist`, {
maxAge: ms('30 days'),
});
});
fastify.get<{ Params: { path: string } }>('/twemoji-badge/:path(.*)', async (request, reply) => {
const path = request.params.path;

View file

@ -4,7 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { summaly } from 'summaly';
import { summaly } from '@misskey-dev/summaly';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';

View file

@ -0,0 +1,32 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../../shared/.eslintrc.js',
],
rules: {
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'pathGroups': [
{
'pattern': '@/**',
'group': 'external',
'position': 'after'
}
],
}],
'no-restricted-globals': [
'error',
{
'name': '__dirname',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
},
{
'name': '__filename',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
}
]
},
};

View file

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript",
"dynamicImport": true,
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"experimental": {
"keepImportAssertions": true
},
"baseUrl": "../built",
"paths": {
"@/*": ["*"]
},
"target": "es2022"
},
"minify": false
}

View file

@ -0,0 +1,80 @@
import { portToPid } from 'pid-port';
import fkill from 'fkill';
import Fastify from 'fastify';
import { NestFactory } from '@nestjs/core';
import { MainModule } from '@/MainModule.js';
import { ServerService } from '@/server/ServerService.js';
import { loadConfig } from '@/config.js';
import { NestLogger } from '@/NestLogger.js';
const config = loadConfig();
const originEnv = JSON.stringify(process.env);
process.env.NODE_ENV = 'test';
/**
*
*/
async function launch() {
await killTestServer();
console.log('starting application...');
const app = await NestFactory.createApplicationContext(MainModule, {
logger: new NestLogger(),
});
const serverService = app.get(ServerService);
await serverService.launch();
await startControllerEndpoints();
// ジョブキューは必要な時にテストコード側で起動する
// ジョブキューが動くとテスト結果の確認に支障が出ることがあるので意図的に動かさないでいる
console.log('application initialized.');
}
/**
* killする
*/
async function killTestServer() {
//
try {
const pid = await portToPid(config.port);
if (pid) {
await fkill(pid, { force: true });
}
} catch {
// NOP;
}
}
/**
*
* @param port
*/
async function startControllerEndpoints(port = config.port + 1000) {
const fastify = Fastify();
fastify.post<{ Body: { key?: string, value?: string } }>('/env', async (req, res) => {
console.log(req.body);
const key = req.body['key'];
if (!key) {
res.code(400).send({ success: false });
return;
}
process.env[key] = req.body['value'];
res.code(200).send({ success: true });
});
fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => {
process.env = JSON.parse(originEnv);
res.code(200).send({ success: true });
});
await fastify.listen({ port: port, host: 'localhost' });
}
export default launch;

View file

@ -0,0 +1,52 @@
{
"compilerOptions": {
"allowJs": true,
"noEmitOnError": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedParameters": false,
"noUnusedLocals": false,
"noFallthroughCasesInSwitch": true,
"declaration": false,
"sourceMap": true,
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"allowSyntheticDefaultImports": true,
"removeComments": false,
"noLib": false,
"strict": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"isolatedModules": true,
"rootDir": "../src",
"baseUrl": "./",
"paths": {
"@/*": ["../src/*"]
},
"outDir": "../built-test",
"types": [
"node"
],
"typeRoots": [
"../src/@types",
"../node_modules/@types",
"../node_modules"
],
"lib": [
"esnext"
]
},
"compileOnSave": false,
"include": [
"./**/*.ts",
"../src/**/*.ts"
],
"exclude": [
"../src/**/*.test.ts"
]
}

View file

@ -10,7 +10,7 @@ import * as crypto from 'node:crypto';
import cbor from 'cbor';
import * as OTPAuth from 'otpauth';
import { loadConfig } from '@/config.js';
import { api, signup, startServer } from '../utils.js';
import { api, signup } from '../utils.js';
import type {
AuthenticationResponseJSON,
AuthenticatorAssertionResponseJSON,
@ -19,12 +19,10 @@ import type {
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from '@simplewebauthn/typescript-types';
import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js';
describe('2要素認証', () => {
let app: INestApplicationContext;
let alice: misskey.entities.MeSignup;
let alice: misskey.entities.SignupResponse;
const config = loadConfig();
const password = 'test';
@ -185,14 +183,9 @@ describe('2要素認証', () => {
};
beforeAll(async () => {
app = await startServer();
alice = await signup({ username, password });
}, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
test('が設定でき、OTPでログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', {
password,

View file

@ -6,24 +6,20 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { inspect } from 'node:util';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import type { Packed } from '@/misc/json-schema.js';
import {
signup,
post,
userList,
page,
role,
startServer,
api,
successfulApiCall,
failedApiCall,
uploadFile,
post,
role,
signup,
successfulApiCall,
testPaginationConsistency,
uploadFile,
userList,
} from '../utils.js';
import type * as misskey from 'misskey-js';
import type { INestApplicationContext } from '@nestjs/common';
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));
@ -37,7 +33,7 @@ describe('アンテナ', () => {
// - srcのenumにgroupが残っている
// - userGroupIdが残っている, isActiveがない
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
type User = misskey.entities.MeSignup;
type User = misskey.entities.SignupResponse;
type Note = misskey.entities.Note;
// アンテナを作成できる最小のパラメタ
@ -54,8 +50,6 @@ describe('アンテナ', () => {
withReplies: false,
};
let app: INestApplicationContext;
let root: User;
let alice: User;
let bob: User;
@ -79,10 +73,6 @@ describe('アンテナ', () => {
let userMutingAlice: User;
let userMutedByAlice: User;
beforeAll(async () => {
app = await startServer();
}, 1000 * 60 * 2);
beforeAll(async () => {
root = await signup({ username: 'root' });
alice = await signup({ username: 'alice' });
@ -136,10 +126,6 @@ describe('アンテナ', () => {
await api('mute/create', { userId: userMutedByAlice.id }, alice);
}, 1000 * 60 * 10);
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
// テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) {

View file

@ -6,33 +6,22 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { signup, api, post, startServer } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import { api, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('API visibility', () => {
let app: INestApplicationContext;
beforeAll(async () => {
app = await startServer();
}, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
describe('Note visibility', () => {
//#region vars
/** ヒロイン */
let alice: misskey.entities.MeSignup;
let alice: misskey.entities.SignupResponse;
/** フォロワー */
let follower: misskey.entities.MeSignup;
let follower: misskey.entities.SignupResponse;
/** 非フォロワー */
let other: misskey.entities.MeSignup;
let other: misskey.entities.SignupResponse;
/** 非フォロワーでもリプライやメンションをされた人 */
let target: misskey.entities.MeSignup;
let target: misskey.entities.SignupResponse;
/** specified mentionでmentionを飛ばされる人 */
let target2: misskey.entities.MeSignup;
let target2: misskey.entities.SignupResponse;
/** public-post */
let pub: any;

View file

@ -7,27 +7,30 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { IncomingMessage } from 'http';
import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch, createAppToken } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import {
api,
connectStream,
createAppToken,
failedApiCall,
relativeFetch,
signup,
successfulApiCall,
uploadFile,
waitFire,
} from '../utils.js';
import type * as misskey from 'misskey-js';
describe('API', () => {
let app: INestApplicationContext;
let alice: misskey.entities.MeSignup;
let bob: misskey.entities.MeSignup;
let carol: misskey.entities.MeSignup;
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse;
beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
describe('General validation', () => {
test('wrong type', async () => {
const res = await api('/test', {

View file

@ -6,29 +6,21 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { signup, api, post, startServer } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import { api, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('Block', () => {
let app: INestApplicationContext;
// alice blocks bob
let alice: misskey.entities.MeSignup;
let bob: misskey.entities.MeSignup;
let carol: misskey.entities.MeSignup;
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse;
beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
test('Block作成', async () => {
const res = await api('/blocking/create', {
userId: bob.id,

View file

@ -18,25 +18,13 @@ import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unf
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 {
signup,
post,
startServer,
api,
successfulApiCall,
failedApiCall,
ApiRequest,
hiddenNote,
} from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js';
describe('クリップ', () => {
type User = Packed<'User'>;
type Note = Packed<'Note'>;
type Clip = Packed<'Clip'>;
let app: INestApplicationContext;
let alice: User;
let bob: User;
let aliceNote: Note;
@ -145,7 +133,6 @@ describe('クリップ', () => {
};
beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
@ -160,10 +147,6 @@ describe('クリップ', () => {
bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any;
}, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
afterEach(async () => {
// テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) {

View file

@ -10,30 +10,22 @@ import * as assert from 'assert';
// https://github.com/node-fetch/node-fetch/pull/1664
import { Blob } from 'node-fetch';
import { MiUser } from '@/models/_.js';
import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('Endpoints', () => {
let app: INestApplicationContext;
let alice: misskey.entities.MeSignup;
let bob: misskey.entities.MeSignup;
let carol: misskey.entities.MeSignup;
let dave: misskey.entities.MeSignup;
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse;
let dave: misskey.entities.SignupResponse;
beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
dave = await signup({ username: 'dave' });
}, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
describe('signup', () => {
test('不正なユーザー名でアカウントが作成できない', async () => {
const res = await api('signup', {
@ -710,6 +702,18 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
test('不正なファイル名で怒られる', async () => {
const file = (await uploadFile(alice)).body;
const newName = '';
const res = await api('/drive/files/update', {
fileId: file.id,
name: newName,
}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/drive/files/update', {
fileId: 'kyoppie',

Some files were not shown because too many files have changed in this diff Show more