diff --git a/locales/en-US.yml b/locales/en-US.yml index 6f67ab3df..ec2b0f9c0 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -977,6 +977,7 @@ failedToUpload: "Upload failed" cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially inappropriate." cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity." cannotUploadBecauseExceedsFileSizeLimit: "This file cannot be uploaded as it exceeds the file size limit." +cannotUploadBecauseTimeout: "The file could not be uploaded due to a connection timeout." beta: "Beta" enableAutoSensitive: "Automatic marking as sensitive" enableAutoSensitiveDescription: "Allows automatic detection and marking of sensitive media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide." @@ -1270,6 +1271,7 @@ youAreHidingSensitiveInformation: "This content was hidden by 'Private Mode'." temporarilySeeThis: "Nevermind, just show me this" sensitiveDoubleClickRequired: "Require double-click to open sensitive media" mutualLink: "Mutual Link" +saveThisFile: "Save this file to Drive" _bubbleGame: howToPlay: "How to play" hold: "Hold" diff --git a/locales/index.d.ts b/locales/index.d.ts index d93bc7f3c..bc4a7215b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -3932,6 +3932,10 @@ export interface Locale extends ILocale { * ファイルサイズの制限を超えているためアップロードできません。 */ "cannotUploadBecauseExceedsFileSizeLimit": string; + /** + * 接続がタイムアウトしたため、ファイルをアップロードできませんでした。 + */ + "cannotUploadBecauseTimeout": string; /** * ベータ */ @@ -5171,6 +5175,10 @@ export interface Locale extends ILocale { * モデレーター権限を利用するには、まず二要素認証を有効にする必要があります。 */ "youNeedToEnableTwoFactor": string; + /** + * このファイルをドライブに保存する + */ + "saveThisFile": string; "_bubbleGame": { /** * 遊び方 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c774bf8f0..3cefb4971 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -979,6 +979,7 @@ failedToUpload: "アップロード失敗" cannotUploadBecauseInappropriate: "不適切な内容を含む可能性があると判定されたためアップロードできません。" cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。" +cannotUploadBecauseTimeout: "接続がタイムアウトしたため、ファイルをアップロードできませんでした。" beta: "ベータ" enableAutoSensitive: "自動センシティブ判定" enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。" @@ -1288,6 +1289,7 @@ removeAllFollowings: "相互フォロー解除" areYouSureToRemoveAllFollowings: "本当に{host}とのすべてのフォロー関係を削除しますか? 実行後は元に戻せません。 相手インスタンスが閉鎖されたと判断した場合のみ実行してください。" mutualLink: "相互リンク" youNeedToEnableTwoFactor: "モデレーター権限を利用するには、まず二要素認証を有効にする必要があります。" +saveThisFile: "このファイルをドライブに保存する" _bubbleGame: howToPlay: "遊び方" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 166b2adf6..df554a810 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -976,6 +976,7 @@ failedToUpload: "업로드 실패" cannotUploadBecauseInappropriate: "이 파일은 부적절한 내용을 포함한다고 판단되어 업로드할 수 없습니다." cannotUploadBecauseNoFreeSpace: "드라이브 용량이 부족하여 업로드할 수 없습니다." cannotUploadBecauseExceedsFileSizeLimit: "파일 크기가 너무 크기 때문에 업로드할 수 없습니다." +cannotUploadBecauseTimeout: "접속 시간이 초과되어 파일을 업로드할 수 없습니다." beta: "베타" enableAutoSensitive: "자동 NSFW 탐지" enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 서버 정책에 따라 자동으로 설정될 수 있습니다." @@ -1273,6 +1274,7 @@ removeAllFollowings: "모든 팔로우 관계를 제거하기" areYouSureToRemoveAllFollowings: "정말로 {host}와의 모든 팔로우 관계를 제거하시겠습니까? 실행한 후에는 되돌릴 수 없습니다. 상대 인스턴스가 폐쇄됐다고 판단되는 경우에만 실행하세요." mutualLink: "서로링크" youNeedToEnableTwoFactor: "관리 권한을 이용하려면 먼저 2단계 인증을 활성화해야 합니다." +saveThisFile: "이 파일을 드라이브에 저장" _bubbleGame: howToPlay: "설명" hold: "홀드" diff --git a/package.json b/package.json index 5aebd6f41..f78bf892d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "https://github.com/SWREI/oscar_surf.git" }, - "packageManager": "pnpm@9.1.2", + "packageManager": "pnpm@9.7.1", "workspaces": [ "packages/frontend", "packages/backend", diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts index b69ca8362..df3de63e8 100644 --- a/packages/backend/src/core/ClipService.ts +++ b/packages/backend/src/core/ClipService.ts @@ -134,6 +134,10 @@ export class ClipService { throw new ClipService.NoSuchClipError(); } + if (await this.clipNotesRepository.existsBy({ clipId, noteId })) { + throw new ClipService.AlreadyAddedError(); + } + const policies = await this.roleService.getUserPolicies(me.id); const currentClipCount = await this.clipsRepository.countBy({ @@ -143,6 +147,13 @@ export class ClipService { throw new ClipService.ClipLimitExceededError(); } + const currentNoteCount = await this.clipNotesRepository.countBy({ + clipId: clip.id, + }); + if (currentNoteCount >= policies.noteEachClipsLimit) { + throw new ClipService.TooManyClipNotesError(); + } + const currentNoteCounts = await this.clipNotesRepository .createQueryBuilder('cn') .select('COUNT(*)') @@ -154,13 +165,6 @@ export class ClipService { throw new ClipService.ClipNotesLimitExceededError(); } - const currentNoteCount = await this.clipNotesRepository.countBy({ - clipId: clip.id, - }); - if (currentNoteCount >= policies.noteEachClipsLimit) { - throw new ClipService.TooManyClipNotesError(); - } - try { await this.clipNotesRepository.insert({ id: this.idService.gen(), diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index f9cf38684..677db309d 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -9,7 +9,16 @@ import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import type { MiUser } from '@/models/User.js'; -import type { FollowingsRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { + AntennasRepository, + ClipNotesRepository, + ClipsRepository, + FollowingsRepository, + FollowRequestsRepository, + UserListMembershipsRepository, + UserListsRepository, + WebhooksRepository, +} from '@/models/_.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -30,6 +39,24 @@ export class UserSuspendService { @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, + private queueService: QueueService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, @@ -45,10 +72,41 @@ export class UserSuspendService { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); - await Promise.all([ + const promises: Promise[] = []; + + let cursor = ''; + while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition, no-constant-condition + const clipNotes = await this.clipNotesRepository.createQueryBuilder('c') + .select('c.id') + .innerJoin('c.note', 'n') + .where('n.userId = :userId', { userId: user.id }) + .andWhere('c.id > :cursor', { cursor }) + .orderBy('c.id', 'ASC') + .limit(500) + .getRawMany<{ id: string }>(); + + if (clipNotes.length === 0) break; + + cursor = clipNotes.at(-1)?.id ?? ''; + + promises.push(this.clipNotesRepository.createQueryBuilder() + .delete() + .where('id IN (:...ids)', { ids: clipNotes.map((clipNote) => clipNote.id) }) + .execute()); + } + + await Promise.allSettled([ this.followRequestsRepository.delete({ followeeId: user.id }), this.followRequestsRepository.delete({ followerId: user.id }), - ]).catch(() => null); + + this.antennasRepository.delete({ userId: user.id }), + this.webhooksRepository.delete({ userId: user.id }), + this.userListsRepository.delete({ userId: user.id }), + this.clipsRepository.delete({ userId: user.id }), + + ...promises, + this.userListMembershipsRepository.delete({ userId: user.id }), + ]); if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにDelete配信 diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-link.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-link.ts index faae10b01..6efc6fda5 100644 --- a/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-link.ts +++ b/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-link.ts @@ -19,8 +19,9 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id' }, + itemId: { type: 'string', format: 'misskey:id' }, }, - required: ['userId'], + required: ['userId', 'itemId'], } as const; @Injectable() @@ -42,7 +43,10 @@ export default class extends Endpoint { // eslint- } await this.userProfilesRepository.update(user.id, { - mutualLinkSections: [], + mutualLinkSections: userProfile.mutualLinkSections.map(section => ({ + ...section, + mutualLinks: section.mutualLinks.filter(item => item.id !== ps.itemId), + })), }); this.moderationLogService.log(me, 'unsetUserMutualLink', { diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 5a18f0d65..e64af9f2c 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -43,7 +43,7 @@ "chartjs-plugin-zoom": "2.0.1", "chromatic": "11.7.0", "compare-versions": "6.1.1", - "cropperjs": "2.0.0-beta.4", + "cropperjs": "2.0.0-rc.0", "date-fns": "3.6.0", "escape-regexp": "0.0.1", "estree-walker": "3.0.3", diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index f87628610..9db710a14 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -62,6 +62,7 @@ import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import { $i, iAmModerator } from '@/account.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const props = withDefaults(defineProps<{ image: Misskey.entities.DriveFile; @@ -92,6 +93,17 @@ function showMenu(ev: MouseEvent) { action: () => { hide.value = true; }, + }, { + text: i18n.ts.saveThisFile, + icon: 'ti ti-cloud-upload', + action: () => { + os.selectDriveFolder(false).then(async folder => { + misskeyApi('drive/files/upload-from-url', { + url: props.image.url, + folderId: folder[0]?.id, + }); + }); + }, }]; if ($i?.id === props.image.userId || iAmModerator) { diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 26b441757..32ff46888 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -66,7 +66,19 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.resetPassword }} {{ i18n.ts.unsetUserAvatar }} {{ i18n.ts.unsetUserBanner }} - {{ i18n.ts.unsetUserMutualLink }} + + + + +
+
+

{{ mutualLink.url }}

+ +

{{ mutualLink.description }}

+ {{ i18n.ts.unsetUserMutualLink }} +
+
+
@@ -365,15 +377,16 @@ async function unsetUserBanner() { }).then(refreshUser); } -async function unsetUserMutualLink() { +async function unsetUserMutualLink(mutualLinkid: string) { const confirm = await os.confirm({ type: 'warning', text: i18n.ts.unsetUserMutualLinkConfirm, }); if (confirm.canceled) return; - await os.apiWithDialog('admin/unset-user-mutual-banner', { + await os.apiWithDialog('admin/unset-user-mutual-link', { userId: user.value.id, + itemId: mutualLinkid, }).then(refreshUser); } @@ -705,4 +718,16 @@ definePageMetadata(() => ({ border-radius: 6px; cursor: pointer; } + +.mutualLinkImg { + max-width: 200px; + max-height: 40px; +} +.fields { + padding: 24px; + border-bottom: solid 0.5px var(--divider); + &:last-child { + border-bottom: none; + } +} diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index b2b91bbb7..dfeba525a 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -112,7 +112,9 @@ SPDX-License-Identifier: AGPL-3.0-only