diff --git a/.config/example.yml b/.config/example.yml index 96e0d092354c..7612c063e62e 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -129,23 +129,6 @@ redis: # index: '' # scope: local -# ┌────────────────────────────────────┐ -#───┘ File storage (Drive) configuration └────────────────────── - -#s3: -# baseUrl: s3.example.com -# bucket: example-bucket -# prefix: example-prefix -# endpoint: s3.example.com -# region: us-east-1 -# useSSL: true -# accessKey: example-access-key -# secretKey: example-secret-key -# options: -# setPublicRead: true -# forcePathStyle: false -# useProxy: false - # ┌───────────────┐ #───┘ ID generation └─────────────────────────────────────────── diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml deleted file mode 100644 index 859f20b916e0..000000000000 --- a/.github/workflows/dockle.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Dockle - -on: - push: - branches: - - beta - - io - - host - pull_request: - -jobs: - dockle: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Build an image from Dockerfile - uses: docker/build-push-action@v5 - with: - context: . - push: false - provenance: false - cache-from: type=registry,ref=ghcr.io/misskeyio/misskey:io-buildcache - tags: | - misskey:scan - - name: Run dockle - uses: goodwithtech/dockle-action@main - with: - image: 'misskey:scan' - format: 'list' - exit-code: '1' - exit-level: 'warn' - ignore: 'CIS-DI-0005,CIS-DI-0010' diff --git a/.github/workflows/reviewer_lottery.yml b/.github/workflows/reviewer_lottery.yml new file mode 100644 index 000000000000..9a13d3d253d1 --- /dev/null +++ b/.github/workflows/reviewer_lottery.yml @@ -0,0 +1,13 @@ +name: "Reviewer lottery" +on: + pull_request_target: + types: [] #[opened, ready_for_review, reopened] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: uesteibar/reviewer-lottery@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml new file mode 100644 index 000000000000..bb93be023a6a --- /dev/null +++ b/.github/workflows/storybook.yml @@ -0,0 +1,112 @@ +name: Storybook + +on: + push: + branches: [] + #- master + #- develop + pull_request_target: [] + +jobs: + build: + runs-on: ubuntu-latest + + env: + NODE_OPTIONS: "--max_old_space_size=7168" + + steps: + - uses: actions/checkout@v3.3.0 + if: github.event_name != 'pull_request_target' + with: + fetch-depth: 0 + submodules: true + - uses: actions/checkout@v3.3.0 + if: github.event_name == 'pull_request_target' + with: + fetch-depth: 0 + submodules: true + ref: "refs/pull/${{ github.event.number }}/merge" + - name: Checkout actual HEAD + if: github.event_name == 'pull_request_target' + id: rev + run: | + echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT + git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3) + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - name: Use Node.js 20.x + uses: actions/setup-node@v3.6.0 + with: + node-version-file: '.node-version' + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + - name: Build misskey-js + run: pnpm --filter misskey-js build + - name: Build storybook + run: pnpm --filter frontend build-storybook + - name: Publish to Chromatic + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/master' + run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static + env: + CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + - name: Publish to Chromatic + if: github.event_name != 'pull_request_target' && github.ref != 'refs/heads/master' + id: chromatic_push + run: | + DIFF="${{ github.event.before }} HEAD" + if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then + DIFF="HEAD" + fi + CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))" + if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + fi + if pnpm --filter frontend chromatic -d storybook-static $(echo "$CHROMATIC_PARAMETER"); then + echo "success=true" >> $GITHUB_OUTPUT + else + echo "success=false" >> $GITHUB_OUTPUT + fi + env: + CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + - name: Publish to Chromatic + if: github.event_name == 'pull_request_target' + id: chromatic_pull_request + run: | + DIFF="${{ steps.rev.outputs.base }} HEAD" + if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then + DIFF="HEAD" + fi + CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))" + if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + fi + BRANCH="${{ github.event.pull_request.head.user.login }}:${{ github.event.pull_request.head.ref }}" + if [ "$BRANCH" = "misskey-dev:${{ github.event.pull_request.head.ref }}" ]; then + BRANCH="${{ github.event.pull_request.head.ref }}" + fi + pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER") + env: + CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + - name: Notify that Chromatic detects changes + uses: actions/github-script@v6.4.0 + if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.repos.createCommitComment({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.sha, + body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).' + }) + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: storybook + path: packages/frontend/storybook-static diff --git a/Dockerfile b/Dockerfile index 744502ad1e70..cbaf745c3bfe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=20 +ARG NODE_VERSION=22 # build assets & compile TypeScript diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index dc17cf7c1889..57b4c3f93599 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -65,21 +65,6 @@ type Source = { index: string; scope?: 'local' | 'global' | string[]; }; - s3?: { - baseUrl: string; - bucket: string; - prefix: string; - endpoint: string; - region?: string; - useSSL: boolean; - accessKey: string; - secretKey: string; - options?: { - setPublicRead?: boolean; - forcePathStyle?: boolean; - useProxy?: boolean; - } - }; skebStatus?: { method: string; diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 2985d9c7a8ea..b15fbeaab83a 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -147,7 +147,9 @@ export class DriveService { // thunbnail, webpublic を必要なら生成 const alts = await this.generateAlts(path, type, !file.uri); - if (this.config.s3) { + const meta = await this.metaService.fetch(); + + if (meta.useObjectStorage) { //#region ObjectStorage params let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']); @@ -166,11 +168,11 @@ export class DriveService { ext = ''; } - const baseUrl = this.config.s3.baseUrl - ?? `${ this.config.s3.useSSL ? 'https' : 'http' }://${ this.config.s3.endpoint }/${ this.config.s3.bucket }`; + const baseUrl = meta.objectStorageBaseUrl + ?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; // for original - const key = `${this.config.s3.prefix}/${randomUUID()}${ext}`; + const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`; const url = `${ baseUrl }/${ key }`; // for alts @@ -187,7 +189,7 @@ export class DriveService { ]; if (alts.webpublic) { - webpublicKey = `${this.config.s3.prefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; + webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`; this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); @@ -195,7 +197,7 @@ export class DriveService { } if (alts.thumbnail) { - thumbnailKey = `${this.config.s3.prefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; + thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); @@ -372,8 +374,10 @@ export class DriveService { if (type === 'image/apng') type = 'image/png'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; + const meta = await this.metaService.fetch(); + const params = { - Bucket: this.config.s3?.bucket, + Bucket: meta.objectStorageBucket, Key: key, Body: stream, ContentType: type, @@ -386,9 +390,9 @@ export class DriveService { // 許可されているファイル形式でしか拡張子をつけない ext ? correctFilename(filename, ext) : filename, ); - if (this.config.s3?.options?.setPublicRead) params.ACL = 'public-read'; + if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; - await this.s3Service.upload(params) + await this.s3Service.upload(meta, params) .then( result => { if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput @@ -805,13 +809,14 @@ export class DriveService { @bindThis public async deleteObjectStorageFile(key: string) { + const meta = await this.metaService.fetch(); try { const param = { - Bucket: this.config.s3?.bucket, + Bucket: meta.objectStorageBucket, Key: key, } as DeleteObjectCommandInput; - await this.s3Service.delete(param); + await this.s3Service.delete(meta, param); } catch (err: any) { if (err.name === 'NoSuchKey') { this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error); diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index 025f01e10d9c..bf55deaf713b 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -13,6 +13,7 @@ import { NodeHttpHandler, NodeHttpHandlerOptions } from '@smithy/node-http-handl import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; +import type { Meta } from '@/models/entities/Meta.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3'; @@ -27,33 +28,35 @@ export class S3Service { } @bindThis - public getS3Client(): S3Client { - const u = `${this.config.s3?.useSSL ? 'https' : 'http'}://${this.config.s3?.endpoint ?? 'example.net'}`; // dummy url to select http(s) agent + public getS3Client(meta: Meta): S3Client { + const u = meta.objectStorageEndpoint + ? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}` + : `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent - const agent = this.httpRequestService.getAgentByUrl(new URL(u), !this.config.s3?.options?.useProxy); + const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy); const handlerOption: NodeHttpHandlerOptions = {}; - if (this.config.s3?.useSSL) { + if (meta.objectStorageUseSSL) { handlerOption.httpsAgent = agent as https.Agent; } else { handlerOption.httpAgent = agent as http.Agent; } return new S3Client({ - endpoint: this.config.s3?.endpoint ? u : undefined, - credentials: (this.config.s3 && (this.config.s3.accessKey !== null && this.config.s3.secretKey !== null)) ? { - accessKeyId: this.config.s3.accessKey, - secretAccessKey: this.config.s3.secretKey, + endpoint: meta.objectStorageEndpoint ? u : undefined, + credentials: (meta.objectStorageAccessKey !== null && meta.objectStorageSecretKey !== null) ? { + accessKeyId: meta.objectStorageAccessKey, + secretAccessKey: meta.objectStorageSecretKey, } : undefined, - region: this.config.s3?.region ?? 'us-east-1', - tls: this.config.s3?.useSSL, - forcePathStyle: this.config.s3?.options?.forcePathStyle ?? false, // AWS with endPoint omitted + region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, // 空文字列もundefinedにするため ?? は使わない + tls: meta.objectStorageUseSSL, + forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted requestHandler: new NodeHttpHandler(handlerOption), }); } @bindThis - public async upload(input: PutObjectCommandInput) { - const client = this.getS3Client(); + public async upload(meta: Meta, input: PutObjectCommandInput) { + const client = this.getS3Client(meta); return new Upload({ client, params: input, @@ -64,8 +67,8 @@ export class S3Service { } @bindThis - public delete(input: DeleteObjectCommandInput) { - const client = this.getS3Client(); + public delete(meta: Meta, input: DeleteObjectCommandInput) { + const client = this.getS3Client(meta); return client.send(new DeleteObjectCommand(input)); } } diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index d6d3a73af516..369196727014 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -8,6 +8,7 @@ import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository } from '@/models/_.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; +import { MetaService } from '@/core/MetaService.js'; import { truncate } from '@/misc/truncate.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { DriveService } from '@/core/DriveService.js'; @@ -26,6 +27,7 @@ export class ApImageService { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + private metaService: MetaService, private apResolverService: ApResolverService, private driveService: DriveService, private apLoggerService: ApLoggerService, @@ -61,12 +63,19 @@ export class ApImageService { this.logger.info(`Creating the Image: ${image.url}`); + const instance = await this.metaService.fetch(); + + // Cache if remote file cache is on AND either + // 1. remote sensitive file is also on + // 2. or the image is not sensitive + const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive); + const file = await this.driveService.uploadFromUrl({ url: image.url, user: actor, uri: image.url, sensitive: image.sensitive, - isLink: true, + isLink: !shouldBeCached, comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH), }); if (!file.isLink || file.url === image.url) return file; diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 63c29834e4ea..41dc10edd6a0 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -157,6 +157,16 @@ export class MiMeta { }) public infoImageUrl: string | null; + @Column('boolean', { + default: true, + }) + public cacheRemoteFiles: boolean; + + @Column('boolean', { + default: true, + }) + public cacheRemoteSensitiveFiles: boolean; + @Column({ ...id(), nullable: true, @@ -383,6 +393,78 @@ export class MiMeta { }) public defaultDarkTheme: string | null; + @Column('boolean', { + default: false, + }) + public useObjectStorage: boolean; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public objectStorageBucket: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public objectStoragePrefix: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public objectStorageBaseUrl: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public objectStorageEndpoint: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public objectStorageRegion: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public objectStorageAccessKey: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public objectStorageSecretKey: string | null; + + @Column('integer', { + nullable: true, + }) + public objectStoragePort: number | null; + + @Column('boolean', { + default: true, + }) + public objectStorageUseSSL: boolean; + + @Column('boolean', { + default: true, + }) + public objectStorageUseProxy: boolean; + + @Column('boolean', { + default: false, + }) + public objectStorageSetPublicRead: boolean; + + @Column('boolean', { + default: true, + }) + public objectStorageS3ForcePathStyle: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index f9ee6a016f92..5b2c96ad1618 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -21,6 +21,14 @@ export const meta = { type: 'object', optional: false, nullable: false, properties: { + cacheRemoteFiles: { + type: 'boolean', + optional: false, nullable: false, + }, + cacheRemoteSensitiveFiles: { + type: 'boolean', + optional: false, nullable: false, + }, emailRequiredForSignup: { type: 'boolean', optional: false, nullable: false, @@ -245,6 +253,54 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + useObjectStorage: { + type: 'boolean', + optional: true, nullable: false, + }, + objectStorageBaseUrl: { + type: 'string', + optional: true, nullable: true, + }, + objectStorageBucket: { + type: 'string', + optional: true, nullable: true, + }, + objectStoragePrefix: { + type: 'string', + optional: true, nullable: true, + }, + objectStorageEndpoint: { + type: 'string', + optional: true, nullable: true, + }, + objectStorageRegion: { + type: 'string', + optional: true, nullable: true, + }, + objectStoragePort: { + type: 'number', + optional: true, nullable: true, + }, + objectStorageAccessKey: { + type: 'string', + optional: true, nullable: true, + }, + objectStorageSecretKey: { + type: 'string', + optional: true, nullable: true, + }, + objectStorageUseSSL: { + type: 'boolean', + optional: true, nullable: false, + }, + objectStorageUseProxy: { + type: 'boolean', + optional: true, nullable: false, + }, + objectStorageSetPublicRead: { + type: 'boolean', + optional: true, nullable: false, + }, enableIpLogging: { type: 'boolean', optional: false, nullable: false, @@ -514,6 +570,8 @@ export default class extends Endpoint { // eslint- enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, translatorAvailable: instance.deeplAuthKey != null, + cacheRemoteFiles: instance.cacheRemoteFiles, + cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, pinnedUsers: instance.pinnedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, @@ -538,6 +596,19 @@ export default class extends Endpoint { // eslint- smtpUser: instance.smtpUser, smtpPass: instance.smtpPass, swPrivateKey: instance.swPrivateKey, + useObjectStorage: instance.useObjectStorage, + objectStorageBaseUrl: instance.objectStorageBaseUrl, + objectStorageBucket: instance.objectStorageBucket, + objectStoragePrefix: instance.objectStoragePrefix, + objectStorageEndpoint: instance.objectStorageEndpoint, + objectStorageRegion: instance.objectStorageRegion, + objectStoragePort: instance.objectStoragePort, + objectStorageAccessKey: instance.objectStorageAccessKey, + objectStorageSecretKey: instance.objectStorageSecretKey, + objectStorageUseSSL: instance.objectStorageUseSSL, + objectStorageUseProxy: instance.objectStorageUseProxy, + objectStorageSetPublicRead: instance.objectStorageSetPublicRead, + objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, deeplAuthKey: instance.deeplAuthKey, deeplIsPro: instance.deeplIsPro, enableIpLogging: instance.enableIpLogging, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 6c393194efc0..9948e06f89cb 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -62,6 +62,8 @@ export const paramDef = { description: { type: 'string', nullable: true }, defaultLightTheme: { type: 'string', nullable: true }, defaultDarkTheme: { type: 'string', nullable: true }, + cacheRemoteFiles: { type: 'boolean' }, + cacheRemoteSensitiveFiles: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' }, enableHcaptcha: { type: 'boolean' }, hcaptchaSiteKey: { type: 'string', nullable: true }, @@ -105,6 +107,19 @@ export const paramDef = { feedbackUrl: { type: 'string', nullable: true }, impressumUrl: { type: 'string', nullable: true }, privacyPolicyUrl: { type: 'string', nullable: true }, + useObjectStorage: { type: 'boolean' }, + objectStorageBaseUrl: { type: 'string', nullable: true }, + objectStorageBucket: { type: 'string', nullable: true }, + objectStoragePrefix: { type: 'string', nullable: true }, + objectStorageEndpoint: { type: 'string', nullable: true }, + objectStorageRegion: { type: 'string', nullable: true }, + objectStoragePort: { type: 'integer', nullable: true }, + objectStorageAccessKey: { type: 'string', nullable: true }, + objectStorageSecretKey: { type: 'string', nullable: true }, + objectStorageUseSSL: { type: 'boolean' }, + objectStorageUseProxy: { type: 'boolean' }, + objectStorageSetPublicRead: { type: 'boolean' }, + objectStorageS3ForcePathStyle: { type: 'boolean' }, enableIpLogging: { type: 'boolean' }, enableActiveEmailValidation: { type: 'boolean' }, enableVerifymailApi: { type: 'boolean' }, @@ -295,6 +310,14 @@ export default class extends Endpoint { // eslint- set.defaultDarkTheme = ps.defaultDarkTheme; } + if (ps.cacheRemoteFiles !== undefined) { + set.cacheRemoteFiles = ps.cacheRemoteFiles; + } + + if (ps.cacheRemoteSensitiveFiles !== undefined) { + set.cacheRemoteSensitiveFiles = ps.cacheRemoteSensitiveFiles; + } + if (ps.emailRequiredForSignup !== undefined) { set.emailRequiredForSignup = ps.emailRequiredForSignup; } @@ -443,6 +466,58 @@ export default class extends Endpoint { // eslint- set.privacyPolicyUrl = ps.privacyPolicyUrl; } + if (ps.useObjectStorage !== undefined) { + set.useObjectStorage = ps.useObjectStorage; + } + + if (ps.objectStorageBaseUrl !== undefined) { + set.objectStorageBaseUrl = ps.objectStorageBaseUrl; + } + + if (ps.objectStorageBucket !== undefined) { + set.objectStorageBucket = ps.objectStorageBucket; + } + + if (ps.objectStoragePrefix !== undefined) { + set.objectStoragePrefix = ps.objectStoragePrefix; + } + + if (ps.objectStorageEndpoint !== undefined) { + set.objectStorageEndpoint = ps.objectStorageEndpoint; + } + + if (ps.objectStorageRegion !== undefined) { + set.objectStorageRegion = ps.objectStorageRegion; + } + + if (ps.objectStoragePort !== undefined) { + set.objectStoragePort = ps.objectStoragePort; + } + + if (ps.objectStorageAccessKey !== undefined) { + set.objectStorageAccessKey = ps.objectStorageAccessKey; + } + + if (ps.objectStorageSecretKey !== undefined) { + set.objectStorageSecretKey = ps.objectStorageSecretKey; + } + + if (ps.objectStorageUseSSL !== undefined) { + set.objectStorageUseSSL = ps.objectStorageUseSSL; + } + + if (ps.objectStorageUseProxy !== undefined) { + set.objectStorageUseProxy = ps.objectStorageUseProxy; + } + + if (ps.objectStorageSetPublicRead !== undefined) { + set.objectStorageSetPublicRead = ps.objectStorageSetPublicRead; + } + + if (ps.objectStorageS3ForcePathStyle !== undefined) { + set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle; + } + if (ps.deeplAuthKey !== undefined) { if (ps.deeplAuthKey === '') { set.deeplAuthKey = null; diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 5460635e1d40..ca9f58198b23 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -35,7 +35,98 @@ export default class extends Endpoint { // eslint- private metaEntityService: MetaEntityService, ) { super(meta, paramDef, async (ps, me) => { - return ps.detail ? await this.metaEntityService.packDetailed() : await this.metaEntityService.pack(); + const instance = await this.metaService.fetch(true); + + const ads = await this.adsRepository.createQueryBuilder('ads') + .where('ads.expiresAt > :now', { now: new Date() }) + .andWhere('ads.startsAt <= :now', { now: new Date() }) + .andWhere(new Brackets(qb => { + // 曜日のビットフラグを確認する + qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() }) + .orWhere('ads.dayOfWeek = 0'); + })) + .getMany(); + + const response: any = { + maintainerName: instance.maintainerName, + maintainerEmail: instance.maintainerEmail, + + version: this.config.version, + + name: instance.name, + uri: this.config.url, + description: instance.description, + langs: instance.langs, + tosUrl: instance.termsOfServiceUrl, + repositoryUrl: instance.repositoryUrl, + feedbackUrl: instance.feedbackUrl, + disableRegistration: instance.disableRegistration, + emailRequiredForSignup: instance.emailRequiredForSignup, + enableHcaptcha: instance.enableHcaptcha, + hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableRecaptcha: instance.enableRecaptcha, + recaptchaSiteKey: instance.recaptchaSiteKey, + enableTurnstile: instance.enableTurnstile, + turnstileSiteKey: instance.turnstileSiteKey, + swPublickey: instance.swPublicKey, + themeColor: instance.themeColor, + mascotImageUrl: instance.mascotImageUrl, + bannerUrl: instance.bannerUrl, + infoImageUrl: instance.infoImageUrl, + serverErrorImageUrl: instance.serverErrorImageUrl, + notFoundImageUrl: instance.notFoundImageUrl, + iconUrl: instance.iconUrl, + backgroundImageUrl: instance.backgroundImageUrl, + logoImageUrl: instance.logoImageUrl, + maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, + // クライアントの手間を減らすためあらかじめJSONに変換しておく + defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null, + defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null, + ads: ads.map(ad => ({ + id: ad.id, + url: ad.url, + place: ad.place, + ratio: ad.ratio, + imageUrl: ad.imageUrl, + dayOfWeek: ad.dayOfWeek, + })), + enableEmail: instance.enableEmail, + enableServiceWorker: instance.enableServiceWorker, + + translatorAvailable: instance.deeplAuthKey != null, + + serverRules: instance.serverRules, + + policies: { ...DEFAULT_POLICIES, ...instance.policies }, + + mediaProxy: this.config.mediaProxy, + + ...(ps.detail ? { + cacheRemoteFiles: instance.cacheRemoteFiles, + cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, + requireSetup: (await this.usersRepository.countBy({ + host: IsNull(), + })) === 0, + } : {}), + }; + + if (ps.detail) { + const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId).catch(() => null) : null; + + response.proxyAccountName = proxyAccount ? proxyAccount.username : null; + response.features = { + registration: !instance.disableRegistration, + emailRequiredForSignup: instance.emailRequiredForSignup, + hcaptcha: instance.enableHcaptcha, + recaptcha: instance.enableRecaptcha, + turnstile: instance.enableTurnstile, + objectStorage: instance.useObjectStorage, + serviceWorker: instance.enableServiceWorker, + miauth: true, + }; + } + + return response; }); } } diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts index 7adb9b9254a4..1879fe631d5c 100644 --- a/packages/backend/test/unit/S3Service.ts +++ b/packages/backend/test/unit/S3Service.ts @@ -17,6 +17,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { S3Service } from '@/core/S3Service.js'; +import { MiMeta } from '@/models/Meta.js'; import type { TestingModule } from '@nestjs/testing'; describe('S3Service', () => { @@ -45,7 +46,7 @@ describe('S3Service', () => { test('upload a file', async () => { s3Mock.on(PutObjectCommand).resolves({}); - await s3Service.upload({ + await s3Service.upload({ objectStorageRegion: 'us-east-1' } as MiMeta, { Bucket: 'fake', Key: 'fake', Body: 'x', @@ -57,7 +58,7 @@ describe('S3Service', () => { s3Mock.on(UploadPartCommand).resolves({ ETag: '1' }); s3Mock.on(CompleteMultipartUploadCommand).resolves({ Bucket: 'fake', Key: 'fake' }); - await s3Service.upload({ + await s3Service.upload({} as MiMeta, { Bucket: 'fake', Key: 'fake', Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeにしている 8 * 1024 * 1024 を越えるサイズ @@ -67,7 +68,7 @@ describe('S3Service', () => { test('upload a file error', async () => { s3Mock.on(PutObjectCommand).rejects({ name: 'Fake Error' }); - await expect(s3Service.upload({ + await expect(s3Service.upload({ objectStorageRegion: 'us-east-1' } as MiMeta, { Bucket: 'fake', Key: 'fake', Body: 'x', @@ -77,7 +78,7 @@ describe('S3Service', () => { test('upload a large file error', async () => { s3Mock.on(UploadPartCommand).rejects(); - await expect(s3Service.upload({ + await expect(s3Service.upload({} as MiMeta, { Bucket: 'fake', Key: 'fake', Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeにしている 8 * 1024 * 1024 を越えるサイズ diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 208466fb9317..3241bcabe963 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -99,6 +99,8 @@ describe('ActivityPub', () => { perUserHomeTimelineCacheMax: 100, perLocalUserUserTimelineCacheMax: 100, perRemoteUserUserTimelineCacheMax: 100, + cacheRemoteFiles: true, + cacheRemoteSensitiveFiles: true, blockedHosts: [] as string[], sensitiveWords: [] as string[], prohibitedWords: [] as string[], @@ -297,7 +299,36 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), imageObject, ); - assert.ok(driveFile && driveFile.isLink); + assert.ok(!driveFile.isLink); + + const sensitiveImageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/bar.png', + name: '', + sensitive: true, + }; + const sensitiveDriveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + sensitiveImageObject, + ); + assert.ok(!sensitiveDriveFile.isLink); + }); + + test('cacheRemoteFiles=false disables caching', async () => { + meta = { ...metaInitial, cacheRemoteFiles: false }; + + const imageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/foo.png', + name: '', + }; + const driveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + imageObject, + ); + assert.ok(driveFile.isLink); const sensitiveImageObject: IApDocument = { type: 'Document', @@ -362,5 +393,113 @@ describe('ActivityPub', () => { // undefined: 'test test baz', }); }); + + test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => { + meta = { ...metaInitial, cacheRemoteSensitiveFiles: false }; + + const imageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/foo.png', + name: '', + }; + const driveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + imageObject, + ); + assert.ok(!driveFile.isLink); + + const sensitiveImageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/bar.png', + name: '', + sensitive: true, + }; + const sensitiveDriveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + sensitiveImageObject, + ); + assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink); + }); + + test('Link is not an attachment files', async () => { + const linkObject: IObject = { + type: 'Link', + href: 'https://example.com/', + }; + const driveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + linkObject, + ); + assert.strictEqual(driveFile, null); + }); + }); + + describe('JSON-LD', () => { + test('Compaction', async () => { + const jsonLd = jsonLdService.use(); + + const object = { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + _misskey_quote: 'https://misskey-hub.net/ns#_misskey_quote', + unknown: 'https://example.org/ns#unknown', + undefined: null, + }, + ], + id: 'https://example.com/notes/42', + type: 'Note', + attributedTo: 'https://example.com/users/1', + to: ['https://www.w3.org/ns/activitystreams#Public'], + content: 'test test foo', + _misskey_quote: 'https://example.com/notes/1', + unknown: 'test test bar', + undefined: 'test test baz', + }; + const compacted = await jsonLd.compact(object); + + assert.deepStrictEqual(compacted, { + '@context': CONTEXTS, + id: 'https://example.com/notes/42', + type: 'Note', + attributedTo: 'https://example.com/users/1', + to: 'as:Public', + content: 'test test foo', + _misskey_quote: 'https://example.com/notes/1', + 'https://example.org/ns#unknown': 'test test bar', + // undefined: 'test test baz', + }); + }); + + test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => { + meta = { ...metaInitial, cacheRemoteSensitiveFiles: false }; + + const imageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/foo.png', + name: '', + }; + const driveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + imageObject, + ); + assert.ok(!driveFile.isLink); + + const sensitiveImageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/bar.png', + name: '', + sensitive: true, + }; + const sensitiveDriveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + sensitiveImageObject, + ); + assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink); + }); }); }); diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts index 89e99077d869..dae3e9031155 100644 --- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts @@ -382,7 +382,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void { if (childNode.name !== ident) return; this.replace({ type: 'Identifier', - name: node.declarations[0].id.name, + name: (node.declarations[0].id as estree.Identifier).name, }); }, }); diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts index 7c6e537fbc17..35a639b7ef95 100644 --- a/packages/frontend/src/_dev_boot_.ts +++ b/packages/frontend/src/_dev_boot_.ts @@ -40,7 +40,7 @@ async function main() { // TODO:今のままだと言語ファイル変更後はpnpm devをリスタートする必要があるので、chokidarを使ったり等で対応できるようにする const locale = _LANGS_FULL_.find(it => it[0] === lang); localStorage.setItem('lang', lang); - localStorage.setItem('locale', JSON.stringify(locale[1])); + localStorage.setItem('locale', JSON.stringify(locale![1])); localStorage.setItem('localeVersion', _VERSION_); //#endregion @@ -48,7 +48,7 @@ async function main() { const theme = localStorage.getItem('theme'); if (theme) { for (const [k, v] of Object.entries(JSON.parse(theme))) { - document.documentElement.style.setProperty(`--${k}`, v.toString()); + document.documentElement.style.setProperty(`--${k}`, v?.toString() ?? null); // HTMLの theme-color 適用 if (k === 'htmlThemeColor') { diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 06c3b609b181..9e5bcbcb508b 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -22,7 +22,7 @@ import * as Misskey from 'misskey-js'; import MkNotes from '@/components/MkNotes.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import { useStream } from '@/stream.js'; -import * as sound from '@/scripts/sound.js'; +import * as MkSound from '@/scripts/sound.js'; import { $i } from '@/account.js'; import { instance } from '@/instance.js'; import { defaultStore } from '@/store.js'; @@ -82,7 +82,7 @@ function prepend(note) { emit('note'); if (props.sound) { - sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note'); + MkSound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note'); } } @@ -97,38 +97,38 @@ function connectChannel() { if (props.antenna == null) return; connection = stream.useChannel('antenna', { antennaId: props.antenna, - }); + }) as unknown as Misskey.ChannelConnection; } else if (props.src === 'home') { connection = stream.useChannel('homeTimeline', { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, - }); - connection2 = stream.useChannel('main'); + }) as unknown as Misskey.ChannelConnection; + connection2 = stream.useChannel('main') as unknown as Misskey.ChannelConnection; } else if (props.src === 'local') { connection = stream.useChannel('localTimeline', { withRenotes: props.withRenotes, withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, - }); + }) as unknown as Misskey.ChannelConnection; } else if (props.src === 'media') { connection = stream.useChannel('hybridTimeline', { withRenotes: props.withRenotes, withReplies: props.withReplies, withFiles: true, - }); + }) as unknown as Misskey.ChannelConnection; } else if (props.src === 'social') { connection = stream.useChannel('hybridTimeline', { withRenotes: props.withRenotes, withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, - }); + }) as unknown as Misskey.ChannelConnection; } else if (props.src === 'global') { connection = stream.useChannel('globalTimeline', { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, - }); + }) as unknown as Misskey.ChannelConnection; } else if (props.src === 'mentions') { - connection = stream.useChannel('main'); + connection = stream.useChannel('main') as unknown as Misskey.ChannelConnection; connection.on('mention', prepend); } else if (props.src === 'directs') { const onNote = note => { @@ -136,7 +136,7 @@ function connectChannel() { prepend(note); } }; - connection = stream.useChannel('main'); + connection = stream.useChannel('main') as unknown as Misskey.ChannelConnection; connection.on('mention', onNote); } else if (props.src === 'list') { if (props.list == null) return; @@ -144,17 +144,17 @@ function connectChannel() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list, - }); + }) as unknown as Misskey.ChannelConnection; } else if (props.src === 'channel') { if (props.channel == null) return; connection = stream.useChannel('channel', { channelId: props.channel, - }); + }) as unknown as Misskey.ChannelConnection; } else if (props.src === 'role') { if (props.role == null) return; connection = stream.useChannel('roleTimeline', { roleId: props.role, - }); + }) as unknown as Misskey.ChannelConnection; } if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend); } diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 2bcb0a9b9d44..3aa750241fb5 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }} + {{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name as string }) }} @@ -357,6 +357,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null); const containerEl = shallowRef(); function iconLoaded() { + if (!containerEl.value) return; const emojis = defaultStore.state.reactions; const containerWidth = containerEl.value.offsetWidth; for (let i = 0; i < 32; i++) { @@ -375,6 +376,7 @@ function iconLoaded() { function gravity() { if (!easterEggReady) return; + if (!containerEl.value) return; easterEggReady = false; easterEggEngine.value = physics(containerEl.value); } diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index 24e96b4f4e5e..9a1047683238 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index d8947c106c64..e239718d23f2 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -114,11 +114,11 @@ SPDX-License-Identifier: AGPL-3.0-only - + - + diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue index 77ab473ea256..556d38b8d99c 100644 --- a/packages/frontend/src/pages/achievements.vue +++ b/packages/frontend/src/pages/achievements.vue @@ -7,13 +7,14 @@ SPDX-License-Identifier: AGPL-3.0-only - + + + diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 4ef1883096b0..cba53d5dc0ef 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -50,6 +50,24 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
+ + + + + + +
+
+ @@ -198,6 +216,7 @@ import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; + const name = ref(null); const shortName = ref(null); const description = ref(null); @@ -205,6 +224,8 @@ const maintainerName = ref(null); const maintainerEmail = ref(null); const impressumUrl = ref(null); const pinnedUsers = ref(''); +const cacheRemoteFiles = ref(false); +const cacheRemoteSensitiveFiles = ref(false); const featuredGameChannels = ref(''); const enableServiceWorker = ref(false); const swPublicKey = ref(null); @@ -232,6 +253,8 @@ async function init(): Promise { maintainerEmail.value = meta.maintainerEmail; impressumUrl.value = meta.impressumUrl; pinnedUsers.value = meta.pinnedUsers.join('\n'); + cacheRemoteFiles.value = meta.cacheRemoteFiles; + cacheRemoteSensitiveFiles.value = meta.cacheRemoteSensitiveFiles; featuredGameChannels.value = meta.featuredGameChannels.join('\n'); enableServiceWorker.value = meta.enableServiceWorker; swPublicKey.value = meta.swPublickey; @@ -260,6 +283,8 @@ async function save() { maintainerEmail: maintainerEmail.value, impressumUrl: impressumUrl.value, pinnedUsers: pinnedUsers.value.split('\n'), + cacheRemoteFiles: cacheRemoteFiles.value, + cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value, featuredGameChannels: featuredGameChannels.value.split('\n'), enableServiceWorker: enableServiceWorker.value, swPublicKey: swPublicKey.value, diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue index b31807f9f5de..cdd3a34388b9 100644 --- a/packages/frontend/src/pages/ads.vue +++ b/packages/frontend/src/pages/ads.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue index 45c6b4cf4f43..c8e33add5059 100644 --- a/packages/frontend/src/pages/announcement.vue +++ b/packages/frontend/src/pages/announcement.vue @@ -74,21 +74,21 @@ function fetch() { }); } -async function read(announcement): Promise { - if (announcement.needConfirmationToRead) { +async function read(_announcement: Misskey.entities.Announcement): Promise { + if (_announcement.needConfirmationToRead) { const confirm = await os.confirm({ type: 'question', title: i18n.ts._announcement.readConfirmTitle, - text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }), + text: i18n.tsx._announcement.readConfirmText({ title: _announcement.title }), }); if (confirm.canceled) return; } - announcement.isRead = true; - await misskeyApi('i/read-announcement', { announcementId: announcement.id }); + _announcement.isRead = true; + await misskeyApi('i/read-announcement', { announcementId: _announcement.id }); if ($i) { updateAccount({ - unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== announcement.id), + unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== _announcement.id), }); } } diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 273250d1d000..f7fc6cb40631 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -53,16 +53,16 @@ function queueUpdated(q) { } function top() { - scroll(rootEl.value, { top: 0 }); + scroll(rootEl.value as HTMLElement, { top: 0 }); } async function timetravel() { const { canceled, result: date } = await os.inputDate({ - title: i18n.ts.date, + title: i18n.ts.date as string, }); if (canceled) return; - tlEl.value.timetravel(date); + tlEl.value!.timetravel(date); } function settings() { @@ -70,7 +70,7 @@ function settings() { } function focus() { - tlEl.value.focus(); + tlEl.value!.focus(); } watch(() => props.antennaId, async () => { diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue index 30f12a8fb39b..30309283d001 100644 --- a/packages/frontend/src/pages/api-console.vue +++ b/packages/frontend/src/pages/api-console.vue @@ -70,7 +70,7 @@ function send() { function onEndpointChange() { misskeyApi('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => { const endpointBody = {}; - for (const p of resp.params) { + for (const p of resp!.params) { endpointBody[p.name] = p.type === 'String' ? '' : p.type === 'Number' ? 0 : diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index d8f8d0b42858..70348cd1b4b9 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -80,7 +80,7 @@ onMounted(async () => { }); // 既に連携していた場合 - if (session.value.app.isAuthorized) { + if (session.value?.app.isAuthorized) { await misskeyApi('auth/accept', { token: session.value.token, }); diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index ad9ec3c4eece..905d09f420f5 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -50,7 +50,7 @@ const avatarDecorations = ref { + os.apiWithDialog('channels/create', params as any).then(created => { router.push(`/channels/${created.id}`); }); } @@ -174,14 +176,14 @@ function save() { async function archive() { const { canceled } = await os.confirm({ type: 'warning', - title: i18n.tsx.channelArchiveConfirmTitle({ name: name.value }), + title: i18n.tsx.channelArchiveConfirmTitle({ name: name.value as string }), text: i18n.ts.channelArchiveConfirmDescription, }); if (canceled) return; misskeyApi('channels/update', { - channelId: props.channelId, + channelId: props.channelId as string, isArchived: true, }).then(() => { os.success(); diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 611ae6feca0f..3476c22e0117 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -121,12 +121,12 @@ watch(() => props.channelId, async () => { channel.value = await misskeyApi('channels/show', { channelId: props.channelId, }); - favorited.value = channel.value.isFavorited ?? false; - if (favorited.value || channel.value.isFollowing) { + favorited.value = channel.value?.isFavorited ?? false; + if (favorited.value || channel.value?.isFollowing) { tab.value = 'timeline'; } - if ((favorited.value || channel.value.isFollowing) && channel.value.lastNotedAt) { + if ((favorited.value || channel.value?.isFollowing) && channel.value?.lastNotedAt) { const lastReadedAt: number = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.value.id}`) ?? 0; const lastNotedAt = Date.parse(channel.value.lastNotedAt); diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index a6e9acb15b25..6a63939da477 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index fd64a55c651e..e64d1f5bfc8b 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -65,7 +65,7 @@ watch(() => props.clipId, async () => { clip.value = await misskeyApi('clips/show', { clipId: props.clipId, }); - favorited.value = clip.value.isFavorited; + favorited.value = clip.value?.isFavorited ?? false; }, { immediate: true, }); @@ -97,11 +97,11 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ icon: 'ti ti-pencil', text: i18n.ts.edit, handler: async (): Promise => { - const { canceled, result } = await os.form(clip.value.name, { + const { canceled, result } = await os.form(clip.value!.name, { name: { type: 'string', label: i18n.ts.name, - default: clip.value.name, + default: clip.value?.name as string, }, description: { type: 'string', @@ -109,18 +109,18 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ multiline: true, treatAsMfm: true, label: i18n.ts.description, - default: clip.value.description, + default: clip.value?.description as string, }, isPublic: { type: 'boolean', label: i18n.ts.public, - default: clip.value.isPublic, + default: clip.value?.isPublic ?? false, }, }); if (canceled) return; os.apiWithDialog('clips/update', { - clipId: clip.value.id, + clipId: clip.value?.id as string, ...result, }); @@ -130,7 +130,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ icon: 'ti ti-link', text: i18n.ts.copyUrl, handler: async (): Promise => { - copyToClipboard(`${url}/clips/${clip.value.id}`); + copyToClipboard(`${url}/clips/${clip.value?.id}`); os.success(); }, }] : []), ...(clip.value.isPublic && isSupportShare() ? [{ @@ -138,9 +138,9 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ text: i18n.ts.share, handler: async (): Promise => { navigator.share({ - title: clip.value.name, - text: clip.value.description, - url: `${url}/clips/${clip.value.id}`, + title: clip.value?.name as string, + text: clip.value?.description ?? undefined, + url: `${url}/clips/${clip.value?.id}`, }); }, }] : []), { @@ -150,12 +150,12 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ handler: async (): Promise => { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }), + text: i18n.tsx.deleteAreYouSure({ x: clip.value?.name as string }), }); if (canceled) return; await os.apiWithDialog('clips/delete', { - clipId: clip.value.id, + clipId: clip.value?.id as string, }); clipsCache.delete(); diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 6d86a60d9e78..61136fca3cbc 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only