0
0

Compare commits

...

5 Commits

18 changed files with 595 additions and 154 deletions

View File

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RemoteObjectStorage1729518620697 {
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN useRemoteObjectStorage BOOLEAN DEFAULT false;`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStorageBucket VARCHAR(1024) NOT NULL;`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStoragePrefix VARCHAR(1024) NOT NULL;`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStorageBaseUrl VARCHAR(1024);`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStorageEndpoint VARCHAR(1024);`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStorageRegion VARCHAR(1024);`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStorageAccessKey VARCHAR(1024);`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStorageSecretKey VARCHAR(1024);`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStoragePort INTEGER;`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStorageUseSSL BOOLEAN DEFAULT true;`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStorageUseProxy BOOLEAN DEFAULT true;`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStorageSetPublicRead BOOLEAN DEFAULT false;`);
await queryRunner.query(`ALTER TABLE "meta" ADD COLUMN remoteObjectStorageS3ForcePathStyle BOOLEAN DEFAULT true;`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStorageS3ForcePathStyle;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStorageSetPublicRead;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStorageUseProxy;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStorageUseSSL;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStoragePort;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStorageSecretKey;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStorageAccessKey;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStorageRegion;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStorageEndpoint;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStorageBaseUrl;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStoragePrefix;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN remoteObjectStorageBucket;`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN useRemoteObjectStorage;`);
}
}

View File

@ -146,7 +146,7 @@ export class DriveService {
* @param size Size for original * @param size Size for original
*/ */
@bindThis @bindThis
private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<MiDriveFile> { private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number, isRemote = false): Promise<MiDriveFile> {
// thunbnail, webpublic を必要なら生成 // thunbnail, webpublic を必要なら生成
const alts = await this.generateAlts(path, type, !file.uri); const alts = await this.generateAlts(path, type, !file.uri);
@ -169,11 +169,37 @@ export class DriveService {
ext = ''; ext = '';
} }
const baseUrl = this.meta.objectStorageBaseUrl const useRemoteObjectStorage = isRemote && this.meta.useRemoteObjectStorage;
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
const objectStorageBaseUrl = useRemoteObjectStorage
? this.meta.remoteObjectStorageBaseUrl
: this.meta.objectStorageBaseUrl;
const objectStorageUseSSL = useRemoteObjectStorage
? this.meta.remoteObjectStorageUseSSL
: this.meta.objectStorageUseSSL;
const objectStorageEndpoint = useRemoteObjectStorage
? this.meta.remoteObjectStorageEndpoint
: this.meta.objectStorageEndpoint;
const objectStoragePort = useRemoteObjectStorage
? this.meta.remoteObjectStoragePort
: this.meta.objectStoragePort;
const objectStorageBucket = useRemoteObjectStorage
? this.meta.remoteObjectStorageBucket
: this.meta.objectStorageBucket;
const objectStoragePrefix = useRemoteObjectStorage
? this.meta.remoteObjectStoragePrefix
: this.meta.objectStoragePrefix;
const baseUrl = objectStorageBaseUrl
?? `${objectStorageUseSSL ? 'https' : 'http'}://${objectStorageEndpoint}${objectStoragePort ? `:${objectStoragePort}` : ''}/${objectStorageBucket}`;
// for original // for original
const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`; const key = `${objectStoragePrefix}/${randomUUID()}${ext}`;
const url = `${ baseUrl }/${ key }`; const url = `${ baseUrl }/${ key }`;
// for alts // for alts
@ -186,7 +212,7 @@ export class DriveService {
//#region Uploads //#region Uploads
this.registerLogger.info(`uploading original: ${key}`); this.registerLogger.info(`uploading original: ${key}`);
const uploads = [ const uploads = [
this.upload(key, fs.createReadStream(path), type, null, name), this.upload(key, fs.createReadStream(path), type, isRemote, null, name),
]; ];
if (alts.webpublic) { if (alts.webpublic) {
@ -194,7 +220,7 @@ export class DriveService {
webpublicUrl = `${ baseUrl }/${ webpublicKey }`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, alts.webpublic.ext, name)); uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, isRemote, alts.webpublic.ext, name));
} }
if (alts.thumbnail) { if (alts.thumbnail) {
@ -202,7 +228,7 @@ export class DriveService {
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext, `${name}.thumbnail`)); uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, isRemote, alts.thumbnail.ext, `${name}.thumbnail`));
} }
await Promise.all(uploads); await Promise.all(uploads);
@ -371,12 +397,22 @@ export class DriveService {
* Upload to ObjectStorage * Upload to ObjectStorage
*/ */
@bindThis @bindThis
private async upload(key: string, stream: fs.ReadStream | Buffer, type: string, ext?: string | null, filename?: string) { private async upload(key: string, stream: fs.ReadStream | Buffer, type: string, isRemote = false, ext?: string | null, filename?: string) {
if (type === 'image/apng') type = 'image/png'; if (type === 'image/apng') type = 'image/png';
if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream';
const useRemoteObjectStorage = isRemote && this.meta.useRemoteObjectStorage;
const objectStorageBucket = useRemoteObjectStorage
? this.meta.remoteObjectStorageBucket
: this.meta.objectStorageBucket;
const objectStorageSetPublicRead = useRemoteObjectStorage
? this.meta.remoteObjectStorageSetPublicRead
: this.meta.objectStorageSetPublicRead;
const params = { const params = {
Bucket: this.meta.objectStorageBucket, Bucket: objectStorageBucket,
Key: key, Key: key,
Body: stream, Body: stream,
ContentType: type, ContentType: type,
@ -389,9 +425,9 @@ export class DriveService {
// 許可されているファイル形式でしか拡張子をつけない // 許可されているファイル形式でしか拡張子をつけない
ext ? correctFilename(filename, ext) : filename, ext ? correctFilename(filename, ext) : filename,
); );
if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read'; if (objectStorageSetPublicRead) params.ACL = 'public-read';
await this.s3Service.upload(this.meta, params) await this.s3Service.upload(this.meta, params, isRemote)
.then( .then(
result => { result => {
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
@ -630,7 +666,8 @@ export class DriveService {
} }
} }
} else { } else {
file = await (this.save(file, path, detectedName, info.type.mime, info.md5, info.size)); const isRemote = user ? this.userEntityService.isRemoteUser(user) : false;
file = await (this.save(file, path, detectedName, info.type.mime, info.md5, info.size, isRemote));
} }
this.registerLogger.succ(`drive file has been created ${file.id}`); this.registerLogger.succ(`drive file has been created ${file.id}`);
@ -740,7 +777,7 @@ export class DriveService {
} }
@bindThis @bindThis
public async deleteFileSync(file: MiDriveFile, isExpired = false, deleter?: MiUser) { public async deleteFileSync(file: MiDriveFile, isExpired = false, isRemote = false, deleter?: MiUser) {
if (file.storedInternal) { if (file.storedInternal) {
this.internalStorageService.del(file.accessKey!); this.internalStorageService.del(file.accessKey!);
@ -754,14 +791,14 @@ export class DriveService {
} else if (!file.isLink) { } else if (!file.isLink) {
const promises = []; const promises = [];
promises.push(this.deleteObjectStorageFile(file.accessKey!)); promises.push(this.deleteObjectStorageFile(file.accessKey!, isRemote));
if (file.thumbnailUrl) { if (file.thumbnailUrl) {
promises.push(this.deleteObjectStorageFile(file.thumbnailAccessKey!)); promises.push(this.deleteObjectStorageFile(file.thumbnailAccessKey!, isRemote));
} }
if (file.webpublicUrl) { if (file.webpublicUrl) {
promises.push(this.deleteObjectStorageFile(file.webpublicAccessKey!)); promises.push(this.deleteObjectStorageFile(file.webpublicAccessKey!, isRemote));
} }
await Promise.all(promises); await Promise.all(promises);
@ -815,14 +852,19 @@ export class DriveService {
} }
@bindThis @bindThis
public async deleteObjectStorageFile(key: string) { public async deleteObjectStorageFile(key: string, isRemote = false) {
const useRemoteObjectStorage = isRemote && this.meta.useRemoteObjectStorage;
const objectStorageBucket = useRemoteObjectStorage
? this.meta.remoteObjectStorageBucket
: this.meta.objectStorageBucket;
try { try {
const param = { const param = {
Bucket: this.meta.objectStorageBucket, Bucket: objectStorageBucket,
Key: key, Key: key,
} as DeleteObjectCommandInput; } as DeleteObjectCommandInput;
await this.s3Service.delete(this.meta, param); await this.s3Service.delete(this.meta, param, isRemote);
} catch (err: any) { } catch (err: any) {
if (err.name === 'NoSuchKey') { if (err.name === 'NoSuchKey') {
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error); this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);

View File

@ -19,39 +19,64 @@ import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/c
export class S3Service { export class S3Service {
constructor( constructor(
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
) { ) {}
}
@bindThis @bindThis
public getS3Client(meta: MiMeta): S3Client { public getS3Client(meta: MiMeta, isRemote = false): S3Client {
const u = meta.objectStorageEndpoint const useRemoteObjectStorage = isRemote && meta.useRemoteObjectStorage;
? `${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), !meta.objectStorageUseProxy); const objectStorageEndpoint = useRemoteObjectStorage
? meta.remoteObjectStorageEndpoint
: meta.objectStorageEndpoint;
const objectStorageUseSSL = useRemoteObjectStorage
? meta.remoteObjectStorageUseSSL
: meta.objectStorageUseSSL;
const objectStorageAccessKey = useRemoteObjectStorage
? meta.remoteObjectStorageAccessKey
: meta.objectStorageAccessKey;
const objectStorageSecretKey = useRemoteObjectStorage
? meta.remoteObjectStorageSecretKey
: meta.objectStorageSecretKey;
const objectStorageRegion = useRemoteObjectStorage
? meta.remoteObjectStorageRegion
: meta.objectStorageRegion;
const objectStorageS3ForcePathStyle = useRemoteObjectStorage
? meta.remoteObjectStorageS3ForcePathStyle
: meta.objectStorageS3ForcePathStyle;
const u = objectStorageEndpoint
? `${objectStorageUseSSL ? 'https' : 'http'}://${objectStorageEndpoint}`
: `${objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent
const agent = this.httpRequestService.getAgentByUrl(new URL(u), !objectStorageUseSSL);
const handlerOption: NodeHttpHandlerOptions = {}; const handlerOption: NodeHttpHandlerOptions = {};
if (meta.objectStorageUseSSL) { if (objectStorageUseSSL) {
handlerOption.httpsAgent = agent as https.Agent; handlerOption.httpsAgent = agent as https.Agent;
} else { } else {
handlerOption.httpAgent = agent as http.Agent; handlerOption.httpAgent = agent as http.Agent;
} }
return new S3Client({ return new S3Client({
endpoint: meta.objectStorageEndpoint ? u : undefined, endpoint: objectStorageEndpoint ? u : undefined,
credentials: (meta.objectStorageAccessKey !== null && meta.objectStorageSecretKey !== null) ? { credentials: (objectStorageAccessKey !== null && objectStorageSecretKey !== null) ? {
accessKeyId: meta.objectStorageAccessKey, accessKeyId: objectStorageAccessKey,
secretAccessKey: meta.objectStorageSecretKey, secretAccessKey: objectStorageSecretKey,
} : undefined, } : undefined,
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, // 空文字列もundefinedにするため ?? は使わない region: objectStorageRegion || undefined, // 空文字列もundefinedにするため ?? は使わない
tls: meta.objectStorageUseSSL, tls: objectStorageUseSSL,
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted forcePathStyle: objectStorageEndpoint ? objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
requestHandler: new NodeHttpHandler(handlerOption), requestHandler: new NodeHttpHandler(handlerOption),
}); });
} }
@bindThis @bindThis
public async upload(meta: MiMeta, input: PutObjectCommandInput) { public async upload(meta: MiMeta, input: PutObjectCommandInput, isRemote = false) {
const client = this.getS3Client(meta); const client = this.getS3Client(meta, isRemote);
return new Upload({ return new Upload({
client, client,
params: input, params: input,
@ -62,8 +87,8 @@ export class S3Service {
} }
@bindThis @bindThis
public delete(meta: MiMeta, input: DeleteObjectCommandInput) { public delete(meta: MiMeta, input: DeleteObjectCommandInput, isRemote = false) {
const client = this.getS3Client(meta); const client = this.getS3Client(meta, isRemote);
return client.send(new DeleteObjectCommand(input)); return client.send(new DeleteObjectCommand(input));
} }
} }

View File

@ -481,6 +481,78 @@ export class MiMeta {
}) })
public objectStorageS3ForcePathStyle: boolean; public objectStorageS3ForcePathStyle: boolean;
@Column('boolean', {
default: false,
})
public useRemoteObjectStorage: boolean;
@Column('varchar', {
length: 1024,
nullable: true,
})
public remoteObjectStorageBucket: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public remoteObjectStoragePrefix: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public remoteObjectStorageBaseUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public remoteObjectStorageEndpoint: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public remoteObjectStorageRegion: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public remoteObjectStorageAccessKey: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public remoteObjectStorageSecretKey: string | null;
@Column('integer', {
nullable: true,
})
public remoteObjectStoragePort: number | null;
@Column('boolean', {
default: true,
})
public remoteObjectStorageUseSSL: boolean;
@Column('boolean', {
default: true,
})
public remoteObjectStorageUseProxy: boolean;
@Column('boolean', {
default: false,
})
public remoteObjectStorageSetPublicRead: boolean;
@Column('boolean', {
default: true,
})
public remoteObjectStorageS3ForcePathStyle: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View File

@ -54,7 +54,7 @@ export class CleanRemoteFilesProcessorService {
cursor = files.at(-1)?.id ?? null; cursor = files.at(-1)?.id ?? null;
await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true))); await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true, true)));
deletedCount += 8; deletedCount += 8;

View File

@ -17,6 +17,7 @@ import { SearchService } from '@/core/SearchService.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import type { DbUserDeleteJobData } from '../types.js'; import type { DbUserDeleteJobData } from '../types.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@Injectable() @Injectable()
export class DeleteAccountProcessorService { export class DeleteAccountProcessorService {
@ -35,6 +36,7 @@ export class DeleteAccountProcessorService {
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
private userEntityService: UserEntityService,
private driveService: DriveService, private driveService: DriveService,
private emailService: EmailService, private emailService: EmailService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
@ -48,6 +50,7 @@ export class DeleteAccountProcessorService {
this.logger.info(`Deleting account of ${job.data.user.id} ...`); this.logger.info(`Deleting account of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
const isRemote = user ? this.userEntityService.isRemoteUser(user) : false;
if (user == null) { if (user == null) {
return; return;
} }
@ -105,7 +108,7 @@ export class DeleteAccountProcessorService {
cursor = files.at(-1)?.id ?? null; cursor = files.at(-1)?.id ?? null;
for (const file of files) { for (const file of files) {
await this.driveService.deleteFileSync(file); await this.driveService.deleteFileSync(file, undefined, isRemote);
} }
} }

View File

@ -13,6 +13,7 @@ import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js'; import type { DbJobDataWithUser } from '../types.js';
import { UserEntityService } from "@/core/entities/UserEntityService.js";
@Injectable() @Injectable()
export class DeleteDriveFilesProcessorService { export class DeleteDriveFilesProcessorService {
@ -25,6 +26,7 @@ export class DeleteDriveFilesProcessorService {
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
private userEntityService: UserEntityService,
private driveService: DriveService, private driveService: DriveService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
) { ) {
@ -36,6 +38,7 @@ export class DeleteDriveFilesProcessorService {
this.logger.info(`Deleting drive files of ${job.data.user.id} ...`); this.logger.info(`Deleting drive files of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
const isRemote = user ? this.userEntityService.isRemoteUser(user) : false;
if (user == null) { if (user == null) {
return; return;
} }
@ -63,7 +66,7 @@ export class DeleteDriveFilesProcessorService {
cursor = files.at(-1)?.id ?? null; cursor = files.at(-1)?.id ?? null;
for (const file of files) { for (const file of files) {
await this.driveService.deleteFileSync(file); await this.driveService.deleteFileSync(file, undefined, isRemote);
deletedCount++; deletedCount++;
} }

View File

@ -3,19 +3,26 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Injectable } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import type { ObjectStorageFileJobData } from '../types.js'; import type { ObjectStorageFileJobData } from '../types.js';
import type { DriveFilesRepository } from "@/models/_.js";
import { UserEntityService } from "@/core/entities/UserEntityService.js";
import { DI } from '@/di-symbols.js';
@Injectable() @Injectable()
export class DeleteFileProcessorService { export class DeleteFileProcessorService {
private logger: Logger; private logger: Logger;
constructor( constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private userEntityService: UserEntityService,
private driveService: DriveService, private driveService: DriveService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
) { ) {
@ -26,7 +33,14 @@ export class DeleteFileProcessorService {
public async process(job: Bull.Job<ObjectStorageFileJobData>): Promise<string> { public async process(job: Bull.Job<ObjectStorageFileJobData>): Promise<string> {
const key: string = job.data.key; const key: string = job.data.key;
await this.driveService.deleteObjectStorageFile(key); const file = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.accessKey = :accessKey', { accessKey: key })
.orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
.getOne();
const isRemote = file?.user ? this.userEntityService.isRemoteUser(file.user) : false;
await this.driveService.deleteObjectStorageFile(key, isRemote);
return 'Success'; return 'Success';
} }

View File

@ -312,6 +312,66 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
useRemoteObjectStorage: {
type: 'boolean',
optional: false,
nullable: false,
},
remoteObjectStorageBaseUrl: {
type: 'string',
optional: false,
nullable: true,
},
remoteObjectStorageBucket: {
type: 'string',
optional: false,
nullable: true,
},
remoteObjectStoragePrefix: {
type: 'string',
optional: false,
nullable: true,
},
remoteObjectStorageEndpoint: {
type: 'string',
optional: false,
nullable: true,
},
remoteObjectStorageRegion: {
type: 'string',
optional: false,
nullable: true,
},
remoteObjectStoragePort: {
type: 'number',
optional: false,
nullable: true,
},
remoteObjectStorageAccessKey: {
type: 'string',
optional: false,
nullable: true,
},
remoteObjectStorageSecretKey: {
type: 'string',
optional: false,
nullable: true,
},
remoteObjectStorageUseSSL: {
type: 'boolean',
optional: false,
nullable: false,
},
remoteObjectStorageUseProxy: {
type: 'boolean',
optional: false,
nullable: false,
},
remoteObjectStorageSetPublicRead: {
type: 'boolean',
optional: false,
nullable: false,
},
enableIpLogging: { enableIpLogging: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -452,6 +512,10 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
remoteObjectStorageS3ForcePathStyle: {
type: 'boolean',
optional: false, nullable: false,
},
privacyPolicyUrl: { privacyPolicyUrl: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
@ -628,6 +692,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
objectStorageUseProxy: instance.objectStorageUseProxy, objectStorageUseProxy: instance.objectStorageUseProxy,
objectStorageSetPublicRead: instance.objectStorageSetPublicRead, objectStorageSetPublicRead: instance.objectStorageSetPublicRead,
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
useRemoteObjectStorage: instance.useRemoteObjectStorage,
remoteObjectStorageBaseUrl: instance.remoteObjectStorageBaseUrl,
remoteObjectStorageBucket: instance.remoteObjectStorageBucket,
remoteObjectStoragePrefix: instance.remoteObjectStoragePrefix,
remoteObjectStorageEndpoint: instance.remoteObjectStorageEndpoint,
remoteObjectStorageRegion: instance.remoteObjectStorageRegion,
remoteObjectStoragePort: instance.remoteObjectStoragePort,
remoteObjectStorageAccessKey: instance.remoteObjectStorageAccessKey,
remoteObjectStorageSecretKey: instance.remoteObjectStorageSecretKey,
remoteObjectStorageUseSSL: instance.remoteObjectStorageUseSSL,
remoteObjectStorageUseProxy: instance.remoteObjectStorageUseProxy,
remoteObjectStorageSetPublicRead: instance.remoteObjectStorageSetPublicRead,
remoteObjectStorageS3ForcePathStyle: instance.remoteObjectStorageS3ForcePathStyle,
deeplAuthKey: instance.deeplAuthKey, deeplAuthKey: instance.deeplAuthKey,
deeplIsPro: instance.deeplIsPro, deeplIsPro: instance.deeplIsPro,
enableIpLogging: instance.enableIpLogging, enableIpLogging: instance.enableIpLogging,

View File

@ -127,6 +127,19 @@ export const paramDef = {
objectStorageUseProxy: { type: 'boolean' }, objectStorageUseProxy: { type: 'boolean' },
objectStorageSetPublicRead: { type: 'boolean' }, objectStorageSetPublicRead: { type: 'boolean' },
objectStorageS3ForcePathStyle: { type: 'boolean' }, objectStorageS3ForcePathStyle: { type: 'boolean' },
useRemoteObjectStorage: { type: 'boolean' },
remoteObjectStorageBaseUrl: { type: 'string', nullable: true },
remoteObjectStorageBucket: { type: 'string', nullable: true },
remoteObjectStoragePrefix: { type: 'string', nullable: true },
remoteObjectStorageEndpoint: { type: 'string', nullable: true },
remoteObjectStorageRegion: { type: 'string', nullable: true },
remoteObjectStoragePort: { type: 'integer', nullable: true },
remoteObjectStorageAccessKey: { type: 'string', nullable: true },
remoteObjectStorageSecretKey: { type: 'string', nullable: true },
remoteObjectStorageUseSSL: { type: 'boolean' },
remoteObjectStorageUseProxy: { type: 'boolean' },
remoteObjectStorageSetPublicRead: { type: 'boolean' },
remoteObjectStorageS3ForcePathStyle: { type: 'boolean' },
enableIpLogging: { type: 'boolean' }, enableIpLogging: { type: 'boolean' },
enableActiveEmailValidation: { type: 'boolean' }, enableActiveEmailValidation: { type: 'boolean' },
enableVerifymailApi: { type: 'boolean' }, enableVerifymailApi: { type: 'boolean' },
@ -519,6 +532,58 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle; set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle;
} }
if (ps.useRemoteObjectStorage !== undefined) {
set.useRemoteObjectStorage = ps.useRemoteObjectStorage;
}
if (ps.remoteObjectStorageBaseUrl !== undefined) {
set.remoteObjectStorageBaseUrl = ps.remoteObjectStorageBaseUrl;
}
if (ps.remoteObjectStorageBucket !== undefined) {
set.remoteObjectStorageBucket = ps.remoteObjectStorageBucket;
}
if (ps.remoteObjectStoragePrefix !== undefined) {
set.remoteObjectStoragePrefix = ps.remoteObjectStoragePrefix;
}
if (ps.remoteObjectStorageEndpoint !== undefined) {
set.remoteObjectStorageEndpoint = ps.remoteObjectStorageEndpoint;
}
if (ps.remoteObjectStorageRegion !== undefined) {
set.remoteObjectStorageRegion = ps.remoteObjectStorageRegion;
}
if (ps.remoteObjectStoragePort !== undefined) {
set.remoteObjectStoragePort = ps.remoteObjectStoragePort;
}
if (ps.remoteObjectStorageAccessKey !== undefined) {
set.remoteObjectStorageAccessKey = ps.remoteObjectStorageAccessKey;
}
if (ps.remoteObjectStorageSecretKey !== undefined) {
set.remoteObjectStorageSecretKey = ps.remoteObjectStorageSecretKey;
}
if (ps.remoteObjectStorageUseSSL !== undefined) {
set.remoteObjectStorageUseSSL = ps.remoteObjectStorageUseSSL;
}
if (ps.remoteObjectStorageUseProxy !== undefined) {
set.remoteObjectStorageUseProxy = ps.remoteObjectStorageUseProxy;
}
if (ps.remoteObjectStorageSetPublicRead !== undefined) {
set.remoteObjectStorageSetPublicRead = ps.remoteObjectStorageSetPublicRead;
}
if (ps.remoteObjectStorageS3ForcePathStyle !== undefined) {
set.remoteObjectStorageS3ForcePathStyle = ps.remoteObjectStorageS3ForcePathStyle;
}
if (ps.deeplAuthKey !== undefined) { if (ps.deeplAuthKey !== undefined) {
if (ps.deeplAuthKey === '') { if (ps.deeplAuthKey === '') {
set.deeplAuthKey = null; set.deeplAuthKey = null;

View File

@ -14,7 +14,10 @@ import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = { export const meta = {
tags: ['federation'], tags: ['federation'],
requireCredential: false, requireCredential: true,
requireModerator: true,
secure: true,
kind: 'read:admin:federation',
allowGet: true, allowGet: true,
cacheSec: 3600, cacheSec: 3600,

View File

@ -24,7 +24,7 @@ export const meta = {
limit: { limit: {
duration: ms('1hour'), duration: ms('1hour'),
max: 10, max: 300,
minInterval: ms('1sec'), minInterval: ms('1sec'),
}, },

View File

@ -116,6 +116,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="appearNote.createdAt" mode="detail" colored/> <MkTime :time="appearNote.createdAt" mode="detail" colored/>
</MkA> </MkA>
</div> </div>
<div v-if="appearNote.updatedAt" style="margin-top: 0; opacity: 0.7; font-size: 0.7em;">
<MkA :to="notePage(appearNote)">
{{ i18n.ts.updatedAt }}: <MkTime :time="appearNote.updatedAt" mode="detail"/>
</MkA>
</div>
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
<button class="_button" :class="$style.noteFooterButton" @click="reply()"> <button class="_button" :class="$style.noteFooterButton" @click="reply()">
<i class="ti ti-arrow-back-up"></i> <i class="ti ti-arrow-back-up"></i>

View File

@ -13,9 +13,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20"> <MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
<XEmojis/> <XEmojis/>
</MkSpacer> </MkSpacer>
<MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20">
<XFederation/>
</MkSpacer>
<MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20"> <MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20">
<MkInstanceStats/> <MkInstanceStats/>
</MkSpacer> </MkSpacer>
@ -32,7 +29,6 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue')); const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue')); const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue'));
const XFederation = defineAsyncComponent(() => import('@/pages/about.federation.vue'));
const MkInstanceStats = defineAsyncComponent(() => import('@/components/MkInstanceStats.vue')); const MkInstanceStats = defineAsyncComponent(() => import('@/components/MkInstanceStats.vue'));
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -58,10 +54,6 @@ const headerTabs = computed(() => [{
key: 'emojis', key: 'emojis',
title: i18n.ts.customEmojis, title: i18n.ts.customEmojis,
icon: 'ti ti-icons', icon: 'ti ti-icons',
}, {
key: 'federation',
title: i18n.ts.federation,
icon: 'ti ti-whirl',
}, { }, {
key: 'charts', key: 'charts',
title: i18n.ts.charts, title: i18n.ts.charts,

View File

@ -4,11 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><XHeader :tabs="headerTabs"/></template> <template #header><XHeader :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_gaps_m"> <div class="_gaps_m">
<MkFolder :defaultOpen="false">
<template #icon><i class="ti ti-cloud"></i></template>
<template #label>{{ i18n.ts.objectStorage }}</template>
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch> <MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
<template v-if="useObjectStorage"> <template v-if="useObjectStorage">
@ -69,6 +73,73 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.s3ForcePathStyleDesc }}</template> <template #caption>{{ i18n.ts.s3ForcePathStyleDesc }}</template>
</MkSwitch> </MkSwitch>
</template> </template>
</MkFolder>
<MkFolder :defaultOpen="false">
<template #icon><i class="ti ti-cloud"></i></template>
<template #label>{{ i18n.ts.objectStorage }} ({{ i18n.ts.remote }})</template>
<MkSwitch v-model="useRemoteObjectStorage">{{ i18n.ts.useObjectStorage }} ({{ i18n.ts.remote }})</MkSwitch>
<template v-if="useRemoteObjectStorage">
<MkInput v-model="remoteObjectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
</MkInput>
<MkInput v-model="remoteObjectStorageBucket">
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
</MkInput>
<MkInput v-model="remoteObjectStoragePrefix">
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
</MkInput>
<MkInput v-model="remoteObjectStorageEndpoint" :placeholder="'example.com'">
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #prefix>https://</template>
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</MkInput>
<MkInput v-model="remoteObjectStorageRegion">
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</MkInput>
<FormSplit :minWidth="280">
<MkInput v-model="remoteObjectStorageAccessKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Access key</template>
</MkInput>
<MkInput v-model="remoteObjectStorageSecretKey" type="password">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Secret key</template>
</MkInput>
</FormSplit>
<MkSwitch v-model="remoteObjectStorageUseSSL">
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
</MkSwitch>
<MkSwitch v-model="remoteObjectStorageUseProxy">
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
</MkSwitch>
<MkSwitch v-model="remoteObjectStorageSetPublicRead">
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
</MkSwitch>
<MkSwitch v-model="remoteObjectStorageS3ForcePathStyle">
<template #label>s3ForcePathStyle</template>
<template #caption>{{ i18n.ts.s3ForcePathStyleDesc }}</template>
</MkSwitch>
</template>
</MkFolder>
</div> </div>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer>
@ -79,7 +150,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSpacer> </MkSpacer>
</div> </div>
</template> </template>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -95,6 +166,7 @@ import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkFolder from "@/components/MkFolder.vue";
const useObjectStorage = ref<boolean>(false); const useObjectStorage = ref<boolean>(false);
const objectStorageBaseUrl = ref<string | null>(null); const objectStorageBaseUrl = ref<string | null>(null);
@ -109,6 +181,19 @@ const objectStorageUseSSL = ref<boolean>(false);
const objectStorageUseProxy = ref<boolean>(false); const objectStorageUseProxy = ref<boolean>(false);
const objectStorageSetPublicRead = ref<boolean>(false); const objectStorageSetPublicRead = ref<boolean>(false);
const objectStorageS3ForcePathStyle = ref<boolean>(true); const objectStorageS3ForcePathStyle = ref<boolean>(true);
const useRemoteObjectStorage = ref<boolean>(false);
const remoteObjectStorageBaseUrl = ref<string | null>(null);
const remoteObjectStorageBucket = ref<string | null>(null);
const remoteObjectStoragePrefix = ref<string | null>(null);
const remoteObjectStorageEndpoint = ref<string | null>(null);
const remoteObjectStorageRegion = ref<string | null>(null);
const remoteObjectStoragePort = ref<number | null>(null);
const remoteObjectStorageAccessKey = ref<string | null>(null);
const remoteObjectStorageSecretKey = ref<string | null>(null);
const remoteObjectStorageUseSSL = ref<boolean>(false);
const remoteObjectStorageUseProxy = ref<boolean>(false);
const remoteObjectStorageSetPublicRead = ref<boolean>(false);
const remoteObjectStorageS3ForcePathStyle = ref<boolean>(true);
async function init() { async function init() {
const meta = await misskeyApi('admin/meta'); const meta = await misskeyApi('admin/meta');
@ -125,6 +210,19 @@ async function init() {
objectStorageUseProxy.value = meta.objectStorageUseProxy; objectStorageUseProxy.value = meta.objectStorageUseProxy;
objectStorageSetPublicRead.value = meta.objectStorageSetPublicRead; objectStorageSetPublicRead.value = meta.objectStorageSetPublicRead;
objectStorageS3ForcePathStyle.value = meta.objectStorageS3ForcePathStyle; objectStorageS3ForcePathStyle.value = meta.objectStorageS3ForcePathStyle;
useRemoteObjectStorage.value = meta.useRemoteObjectStorage;
remoteObjectStorageBaseUrl.value = meta.remoteObjectStorageBaseUrl;
remoteObjectStorageBucket.value = meta.remoteObjectStorageBucket;
remoteObjectStoragePrefix.value = meta.remoteObjectStoragePrefix;
remoteObjectStorageEndpoint.value = meta.remoteObjectStorageEndpoint;
remoteObjectStorageRegion.value = meta.remoteObjectStorageRegion;
remoteObjectStoragePort.value = meta.remoteObjectStoragePort;
remoteObjectStorageAccessKey.value = meta.remoteObjectStorageAccessKey;
remoteObjectStorageSecretKey.value = meta.remoteObjectStorageSecretKey;
remoteObjectStorageUseSSL.value = meta.remoteObjectStorageUseSSL;
remoteObjectStorageUseProxy.value = meta.remoteObjectStorageUseProxy;
remoteObjectStorageSetPublicRead.value = meta.remoteObjectStorageSetPublicRead;
remoteObjectStorageS3ForcePathStyle.value = meta.remoteObjectStorageS3ForcePathStyle;
} }
function save() { function save() {
@ -142,6 +240,19 @@ function save() {
objectStorageUseProxy: objectStorageUseProxy.value, objectStorageUseProxy: objectStorageUseProxy.value,
objectStorageSetPublicRead: objectStorageSetPublicRead.value, objectStorageSetPublicRead: objectStorageSetPublicRead.value,
objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value, objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value,
useRemoteObjectStorage: useRemoteObjectStorage.value,
remoteObjectStorageBaseUrl: remoteObjectStorageBaseUrl.value,
remoteObjectStorageBucket: remoteObjectStorageBucket.value,
remoteObjectStoragePrefix: remoteObjectStoragePrefix.value,
remoteObjectStorageEndpoint: remoteObjectStorageEndpoint.value,
remoteObjectStorageRegion: remoteObjectStorageRegion.value,
remoteObjectStoragePort: remoteObjectStoragePort.value,
remoteObjectStorageAccessKey: remoteObjectStorageAccessKey.value,
remoteObjectStorageSecretKey: remoteObjectStorageSecretKey.value,
remoteObjectStorageUseSSL: remoteObjectStorageUseSSL.value,
remoteObjectStorageUseProxy: remoteObjectStorageUseProxy.value,
remoteObjectStorageSetPublicRead: remoteObjectStorageSetPublicRead.value,
remoteObjectStorageS3ForcePathStyle: remoteObjectStorageS3ForcePathStyle.value,
}).then(() => { }).then(() => {
fetchInstance(true); fetchInstance(true);
}); });

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="shape2"></div> <div class="shape2"></div>
<div class="logo-wrapper"> <div class="logo-wrapper">
<div class="powered-by">Powered by</div> <div class="powered-by">Powered by</div>
<img :src="misskeysvg" class="misskey"/> <img :src="misskeysvg" class="misskey" alt="Misskey Logo"/>
</div> </div>
<div class="emojis"> <div class="emojis">
<MkEmoji :normal="true" :noStyle="true" emoji="🍮"/> <MkEmoji :normal="true" :noStyle="true" emoji="🍮"/>
@ -23,45 +23,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="contents"> <div class="contents">
<MkVisitorDashboard/> <MkVisitorDashboard/>
</div> </div>
<div v-if="instances && instances.length > 0" class="federation">
<MarqueeText :duration="40">
<MkA v-for="instance in instances" :key="instance.id" :class="$style.federationInstance" :to="`/instance-info/${instance.host}`" behavior="window">
<!--<MkInstanceCardMini :instance="instance"/>-->
<img v-if="instance.iconUrl" class="icon" :src="getInstanceIcon(instance)" alt=""/>
<span class="name _monospace">{{ instance.host }}</span>
</MkA>
</MarqueeText>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import XTimeline from './welcome.timeline.vue'; import XTimeline from './welcome.timeline.vue';
import MarqueeText from '@/components/MkMarquee.vue';
import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue'; import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue';
import misskeysvg from '/client-assets/misskey.svg'; import misskeysvg from '/client-assets/misskey.svg';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
import { instance as meta } from '@/instance.js'; import { instance as meta } from '@/instance.js';
const instances = ref<Misskey.entities.FederationInstance[]>();
function getInstanceIcon(instance: Misskey.entities.FederationInstance): string {
if (!instance.iconUrl) {
return '';
}
return getProxiedImageUrl(instance.iconUrl, 'preview');
}
misskeyApiGet('federation/instances', {
sort: '+pubSub',
limit: 20,
}).then(_instances => {
instances.value = _instances;
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -4,10 +4,10 @@
*/ */
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import { host } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { host } from '@@/js/config.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
@ -56,11 +56,6 @@ export function openInstanceMenu(ev: MouseEvent) {
text: i18n.ts.customEmojis, text: i18n.ts.customEmojis,
icon: 'ti ti-icons', icon: 'ti ti-icons',
to: '/about#emojis', to: '/about#emojis',
}, {
type: 'link',
text: i18n.ts.federation,
icon: 'ti ti-whirl',
to: '/about#federation',
}, { }, {
type: 'link', type: 'link',
text: i18n.ts.charts, text: i18n.ts.charts,

View File

@ -5169,6 +5169,18 @@ export type operations = {
objectStorageUseSSL: boolean; objectStorageUseSSL: boolean;
objectStorageUseProxy: boolean; objectStorageUseProxy: boolean;
objectStorageSetPublicRead: boolean; objectStorageSetPublicRead: boolean;
useRemoteObjectStorage: boolean;
remoteObjectStorageBaseUrl: string | null;
remoteObjectStorageBucket: string | null;
remoteObjectStoragePrefix: string | null;
remoteObjectStorageEndpoint: string | null;
remoteObjectStorageRegion: string | null;
remoteObjectStoragePort: number | null;
remoteObjectStorageAccessKey: string | null;
remoteObjectStorageSecretKey: string | null;
remoteObjectStorageUseSSL: boolean;
remoteObjectStorageUseProxy: boolean;
remoteObjectStorageSetPublicRead: boolean;
enableIpLogging: boolean; enableIpLogging: boolean;
enableActiveEmailValidation: boolean; enableActiveEmailValidation: boolean;
enableVerifymailApi: boolean; enableVerifymailApi: boolean;
@ -5204,6 +5216,7 @@ export type operations = {
name: string | null; name: string | null;
shortName: string | null; shortName: string | null;
objectStorageS3ForcePathStyle: boolean; objectStorageS3ForcePathStyle: boolean;
remoteObjectStorageS3ForcePathStyle: boolean;
privacyPolicyUrl: string | null; privacyPolicyUrl: string | null;
inquiryUrl: string | null; inquiryUrl: string | null;
repositoryUrl: string | null; repositoryUrl: string | null;
@ -9552,6 +9565,19 @@ export type operations = {
objectStorageUseProxy?: boolean; objectStorageUseProxy?: boolean;
objectStorageSetPublicRead?: boolean; objectStorageSetPublicRead?: boolean;
objectStorageS3ForcePathStyle?: boolean; objectStorageS3ForcePathStyle?: boolean;
useRemoteObjectStorage?: boolean;
remoteObjectStorageBaseUrl?: string | null;
remoteObjectStorageBucket?: string | null;
remoteObjectStoragePrefix?: string | null;
remoteObjectStorageEndpoint?: string | null;
remoteObjectStorageRegion?: string | null;
remoteObjectStoragePort?: number | null;
remoteObjectStorageAccessKey?: string | null;
remoteObjectStorageSecretKey?: string | null;
remoteObjectStorageUseSSL?: boolean;
remoteObjectStorageUseProxy?: boolean;
remoteObjectStorageSetPublicRead?: boolean;
remoteObjectStorageS3ForcePathStyle?: boolean;
enableIpLogging?: boolean; enableIpLogging?: boolean;
enableActiveEmailValidation?: boolean; enableActiveEmailValidation?: boolean;
enableVerifymailApi?: boolean; enableVerifymailApi?: boolean;