Compare commits
29 Commits
2023.9.0-b
...
fetch-outb
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a926e84b14 | ||
![]() |
814e28459e | ||
![]() |
d2831c612f | ||
![]() |
df636659e2 | ||
![]() |
888cd2eb9f | ||
![]() |
ff4b3d2d9e | ||
![]() |
60fd848182 | ||
![]() |
76def0032e | ||
![]() |
da0804eb17 | ||
![]() |
6b26ce3768 | ||
![]() |
5a0d7d41e6 | ||
![]() |
08e2b6ee32 | ||
![]() |
ca0c673b44 | ||
![]() |
70bb9a4d1f | ||
![]() |
b93046c071 | ||
![]() |
f34f0dfcb6 | ||
![]() |
26040c2bb0 | ||
![]() |
bdbad4605b | ||
![]() |
ec62fe02b1 | ||
![]() |
a74af07992 | ||
![]() |
aa78c29e8c | ||
![]() |
45d0b46e7a | ||
![]() |
6087d02047 | ||
![]() |
7bf318ae98 | ||
![]() |
71d74676f0 | ||
![]() |
5077df2973 | ||
![]() |
a1388a8444 | ||
![]() |
630e97bd06 | ||
![]() |
2e1de4fca9 |
@@ -8,7 +8,7 @@
|
|||||||
-
|
-
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
- 最初照会したユーザーの最新ノートを受け取るように
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
- 二要素認証のバックアップコードが生成されるようになりました ref. https://github.com/MisskeyIO/misskey/pull/121
|
- 二要素認証のバックアップコードが生成されるようになりました ref. https://github.com/MisskeyIO/misskey/pull/121
|
||||||
- 二要素認証でパスキーをサポートするようになりました
|
- 二要素認証でパスキーをサポートするようになりました
|
||||||
- 通知をテストできるようになりました
|
- 通知をテストできるようになりました
|
||||||
|
- PWAのアイコンが設定できるようになりました
|
||||||
|
- manifest.jsonをオーバーライド可能に
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- プロフィールにその人が作ったPlayの一覧出せるように
|
- プロフィールにその人が作ったPlayの一覧出せるように
|
||||||
|
@@ -187,6 +187,10 @@ id: "aidx"
|
|||||||
# Sign to ActivityPub GET request (default: true)
|
# Sign to ActivityPub GET request (default: true)
|
||||||
signToActivityPubGet: true
|
signToActivityPubGet: true
|
||||||
|
|
||||||
|
# Limit of notes to fetch from outbox with remote user first fetched (default: 5)
|
||||||
|
# https://github.com/misskey-dev/misskey/pull/11130
|
||||||
|
outboxNotesFetchLimit: 5
|
||||||
|
|
||||||
#allowedPrivateNetworks: [
|
#allowedPrivateNetworks: [
|
||||||
# '127.0.0.1/32'
|
# '127.0.0.1/32'
|
||||||
#]
|
#]
|
||||||
|
9
locales/index.d.ts
vendored
9
locales/index.d.ts
vendored
@@ -359,7 +359,6 @@ export interface Locale {
|
|||||||
"driveCapacityPerLocalAccount": string;
|
"driveCapacityPerLocalAccount": string;
|
||||||
"driveCapacityPerRemoteAccount": string;
|
"driveCapacityPerRemoteAccount": string;
|
||||||
"inMb": string;
|
"inMb": string;
|
||||||
"iconUrl": string;
|
|
||||||
"bannerUrl": string;
|
"bannerUrl": string;
|
||||||
"backgroundImageUrl": string;
|
"backgroundImageUrl": string;
|
||||||
"basicInfo": string;
|
"basicInfo": string;
|
||||||
@@ -1140,6 +1139,14 @@ export interface Locale {
|
|||||||
"_serverRules": {
|
"_serverRules": {
|
||||||
"description": string;
|
"description": string;
|
||||||
};
|
};
|
||||||
|
"_serverSettings": {
|
||||||
|
"iconUrl": string;
|
||||||
|
"appIconDescription": string;
|
||||||
|
"appIconUsageExample": string;
|
||||||
|
"appIconStyleRecommendation": string;
|
||||||
|
"appIconResolutionMustBe": string;
|
||||||
|
"manifestJsonOverride": string;
|
||||||
|
};
|
||||||
"_accountMigration": {
|
"_accountMigration": {
|
||||||
"moveFrom": string;
|
"moveFrom": string;
|
||||||
"moveFromSub": string;
|
"moveFromSub": string;
|
||||||
|
@@ -356,7 +356,6 @@ invite: "招待"
|
|||||||
driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量"
|
driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量"
|
||||||
driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量"
|
driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量"
|
||||||
inMb: "メガバイト単位"
|
inMb: "メガバイト単位"
|
||||||
iconUrl: "アイコン画像のURL (faviconなど)"
|
|
||||||
bannerUrl: "バナー画像のURL"
|
bannerUrl: "バナー画像のURL"
|
||||||
backgroundImageUrl: "背景画像のURL"
|
backgroundImageUrl: "背景画像のURL"
|
||||||
basicInfo: "基本情報"
|
basicInfo: "基本情報"
|
||||||
@@ -1138,6 +1137,14 @@ _initialAccountSetting:
|
|||||||
_serverRules:
|
_serverRules:
|
||||||
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
||||||
|
|
||||||
|
_serverSettings:
|
||||||
|
iconUrl: "アイコン画像のURL"
|
||||||
|
appIconDescription: "{host}がアプリとして表示される際のアイコンを指定します。"
|
||||||
|
appIconUsageExample: "例: PWAや、スマートフォンのホーム画面にブックマークとして追加された時など"
|
||||||
|
appIconStyleRecommendation: "画像は透過部分が無く、塗りつぶされた余白がある背景を持つことが推奨されます。"
|
||||||
|
appIconResolutionMustBe: "解像度は必ず{resolution}である必要があります。"
|
||||||
|
manifestJsonOverride: "manifest.jsonのオーバーライド"
|
||||||
|
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "別のアカウントからこのアカウントに移行"
|
moveFrom: "別のアカウントからこのアカウントに移行"
|
||||||
moveFromSub: "別のアカウントへエイリアスを作成"
|
moveFromSub: "別のアカウントへエイリアスを作成"
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
export class ServerIconsAndManifest1694850832075 {
|
||||||
|
name = 'ServerIconsAndManifest1694850832075'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "app192IconUrl" character varying(1024)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "app512IconUrl" character varying(1024)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "manifestJsonOverride" character varying(8192) NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "manifestJsonOverride"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "app512IconUrl"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "app192IconUrl"`);
|
||||||
|
}
|
||||||
|
}
|
@@ -85,6 +85,7 @@ type Source = {
|
|||||||
videoThumbnailGenerator?: string;
|
videoThumbnailGenerator?: string;
|
||||||
|
|
||||||
signToActivityPubGet?: boolean;
|
signToActivityPubGet?: boolean;
|
||||||
|
outboxNotesFetchLimit?: number;
|
||||||
|
|
||||||
perChannelMaxNoteCacheCount?: number;
|
perChannelMaxNoteCacheCount?: number;
|
||||||
perUserNotificationsMaxCount?: number;
|
perUserNotificationsMaxCount?: number;
|
||||||
|
@@ -27,7 +27,7 @@ import { QueueService } from '@/core/QueueService.js';
|
|||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { MiRemoteUser } from '@/models/entities/User.js';
|
import type { MiRemoteUser } from '@/models/entities/User.js';
|
||||||
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isOrderedCollectionPage, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||||
import { ApNoteService } from './models/ApNoteService.js';
|
import { ApNoteService } from './models/ApNoteService.js';
|
||||||
import { ApLoggerService } from './ApLoggerService.js';
|
import { ApLoggerService } from './ApLoggerService.js';
|
||||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
@@ -87,11 +87,19 @@ export class ApInboxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
|
public async performActivity(actor: MiRemoteUser, activity: IObject, {
|
||||||
if (isCollectionOrOrderedCollection(activity)) {
|
limit = Infinity,
|
||||||
|
allow = null as (string[] | null) } = {},
|
||||||
|
): Promise<void> {
|
||||||
|
if (isCollectionOrOrderedCollection(activity) || isOrderedCollectionPage(activity)) {
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = this.apResolverService.createResolver();
|
||||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems).slice(0, limit)) {
|
||||||
const act = await resolver.resolve(item);
|
const act = await resolver.resolve(item);
|
||||||
|
const type = getApType(act);
|
||||||
|
if (allow && !allow.includes(type)) {
|
||||||
|
this.logger.info(`skipping activity type: ${type}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await this.performOneActivity(actor, act);
|
await this.performOneActivity(actor, act);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -367,7 +375,7 @@ export class ApInboxService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isPost(object)) {
|
if (isPost(object)) {
|
||||||
this.createNote(resolver, actor, object, false, activity);
|
await this.createNote(resolver, actor, object, false, activity);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
||||||
}
|
}
|
||||||
|
@@ -15,11 +15,11 @@ import { UtilityService } from '@/core/UtilityService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { isCollectionOrOrderedCollection } from './type.js';
|
import { isCollectionOrOrderedCollection, isOrderedCollectionPage } from './type.js';
|
||||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
import { ApRendererService } from './ApRendererService.js';
|
import { ApRendererService } from './ApRendererService.js';
|
||||||
import { ApRequestService } from './ApRequestService.js';
|
import { ApRequestService } from './ApRequestService.js';
|
||||||
import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
import type { IObject, ICollection, IOrderedCollection, IOrderedCollectionPage } from './type.js';
|
||||||
|
|
||||||
export class Resolver {
|
export class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
@@ -64,6 +64,18 @@ export class Resolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async resolveOrderedCollectionPage(value: string | IObject): Promise<IOrderedCollectionPage> {
|
||||||
|
const collection = typeof value === 'string'
|
||||||
|
? await this.resolve(value)
|
||||||
|
: value;
|
||||||
|
|
||||||
|
if (isOrderedCollectionPage(collection)) {
|
||||||
|
return collection;
|
||||||
|
} else {
|
||||||
|
throw new Error(`unrecognized collection type: ${collection.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolve(value: string | IObject): Promise<IObject> {
|
public async resolve(value: string | IObject): Promise<IObject> {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
|
@@ -38,7 +38,8 @@ import { MetaService } from '@/core/MetaService.js';
|
|||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isOrderedCollection, isOrderedCollectionPage, isPropertyValue } from '../type.js';
|
||||||
|
import { ApInboxService } from '../ApInboxService.js';
|
||||||
import { extractApHashtags } from './tag.js';
|
import { extractApHashtags } from './tag.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { ApNoteService } from './ApNoteService.js';
|
import type { ApNoteService } from './ApNoteService.js';
|
||||||
@@ -68,6 +69,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
private apResolverService: ApResolverService;
|
private apResolverService: ApResolverService;
|
||||||
private apNoteService: ApNoteService;
|
private apNoteService: ApNoteService;
|
||||||
private apImageService: ApImageService;
|
private apImageService: ApImageService;
|
||||||
|
private apInboxService: ApInboxService;
|
||||||
private apMfmService: ApMfmService;
|
private apMfmService: ApMfmService;
|
||||||
private mfmService: MfmService;
|
private mfmService: MfmService;
|
||||||
private hashtagService: HashtagService;
|
private hashtagService: HashtagService;
|
||||||
@@ -116,6 +118,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
this.apResolverService = this.moduleRef.get('ApResolverService');
|
this.apResolverService = this.moduleRef.get('ApResolverService');
|
||||||
this.apNoteService = this.moduleRef.get('ApNoteService');
|
this.apNoteService = this.moduleRef.get('ApNoteService');
|
||||||
this.apImageService = this.moduleRef.get('ApImageService');
|
this.apImageService = this.moduleRef.get('ApImageService');
|
||||||
|
this.apInboxService = this.moduleRef.get('ApInboxService');
|
||||||
this.apMfmService = this.moduleRef.get('ApMfmService');
|
this.apMfmService = this.moduleRef.get('ApMfmService');
|
||||||
this.mfmService = this.moduleRef.get('MfmService');
|
this.mfmService = this.moduleRef.get('MfmService');
|
||||||
this.hashtagService = this.moduleRef.get('HashtagService');
|
this.hashtagService = this.moduleRef.get('HashtagService');
|
||||||
@@ -384,7 +387,10 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err));
|
await Promise.allSettled([
|
||||||
|
this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err)),
|
||||||
|
this.updateOutboxFirstPage(user, person.outbox, resolver).catch(err => this.logger.error(err)),
|
||||||
|
]);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -589,7 +595,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
||||||
const items = await Promise.all(toArray(unresolvedItems).map(x => _resolver.resolve(x)));
|
const items = await Promise.all(toArray(unresolvedItems).map(x => _resolver.resolve(x)));
|
||||||
|
|
||||||
// Resolve and regist Notes
|
// Resolve and register Notes
|
||||||
const limit = promiseLimit<MiNote | null>(2);
|
const limit = promiseLimit<MiNote | null>(2);
|
||||||
const featuredNotes = await Promise.all(items
|
const featuredNotes = await Promise.all(items
|
||||||
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
||||||
@@ -616,6 +622,35 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve outbox from an actor object.
|
||||||
|
*
|
||||||
|
* This only retrieves the first page for now.
|
||||||
|
*/
|
||||||
|
public async updateOutboxFirstPage(user: RemoteUser, outbox: IActor['outbox'], resolver: Resolver): Promise<void> {
|
||||||
|
if (!this.config.outboxNotesFetchLimit) return;
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/activitypub/#actor-objects
|
||||||
|
// Outbox is a required property for all actors
|
||||||
|
if (!outbox) {
|
||||||
|
throw new Error('No outbox property');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Fetching the outbox for ${user.uri}: ${outbox}`);
|
||||||
|
|
||||||
|
const collection = await resolver.resolveCollection(outbox);
|
||||||
|
if (!isOrderedCollection(collection)) {
|
||||||
|
throw new Error('Outbox must be an ordered collection');
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstPage = collection.first ?
|
||||||
|
await resolver.resolveOrderedCollectionPage(collection.first) :
|
||||||
|
collection;
|
||||||
|
|
||||||
|
// Perform activity but only the first outboxNotesFetchLimit ones with `type: Create`
|
||||||
|
await this.apInboxService.performActivity(user, firstPage, { limit: this.config.outboxNotesFetchLimit, allow: ['Create'] });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* リモート由来のアカウント移行処理を行います
|
* リモート由来のアカウント移行処理を行います
|
||||||
* @param src 移行元アカウント(リモートかつupdatePerson後である必要がある、というかこれ自体がupdatePersonで呼ばれる前提)
|
* @param src 移行元アカウント(リモートかつupdatePerson後である必要がある、というかこれ自体がupdatePersonで呼ばれる前提)
|
||||||
|
@@ -92,16 +92,37 @@ export interface IActivity extends IObject {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collection
|
||||||
export interface ICollection extends IObject {
|
export interface ICollection extends IObject {
|
||||||
type: 'Collection';
|
type: 'Collection';
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
|
current?: ICollectionPage | string;
|
||||||
|
first?: ICollectionPage | string;
|
||||||
|
last?: ICollectionPage | string;
|
||||||
items: ApObject;
|
items: ApObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IOrderedCollection extends IObject {
|
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection
|
||||||
|
export interface IOrderedCollection extends Omit<ICollection, 'type' | 'items'> {
|
||||||
type: 'OrderedCollection';
|
type: 'OrderedCollection';
|
||||||
totalItems: number;
|
|
||||||
orderedItems: ApObject;
|
// orderedItems is not defined well
|
||||||
|
// https://github.com/w3c/activitystreams/issues/494
|
||||||
|
orderedItems?: ApObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage
|
||||||
|
export interface ICollectionPage extends Omit<ICollection, 'type'> {
|
||||||
|
type: 'CollectionPage';
|
||||||
|
partOf?: ICollection | string;
|
||||||
|
next?: ICollectionPage | string;
|
||||||
|
prev?: ICollectionPage | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollectionpage
|
||||||
|
export interface IOrderedCollectionPage extends Omit<IOrderedCollection, 'type'>, Omit<ICollectionPage, 'type' | 'items'> {
|
||||||
|
type: 'OrderedCollectionPage';
|
||||||
|
startIndex?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
|
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
|
||||||
@@ -188,6 +209,9 @@ export const isCollection = (object: IObject): object is ICollection =>
|
|||||||
export const isOrderedCollection = (object: IObject): object is IOrderedCollection =>
|
export const isOrderedCollection = (object: IObject): object is IOrderedCollection =>
|
||||||
getApType(object) === 'OrderedCollection';
|
getApType(object) === 'OrderedCollection';
|
||||||
|
|
||||||
|
export const isOrderedCollectionPage = (object: IObject): object is IOrderedCollectionPage =>
|
||||||
|
getApType(object) === 'OrderedCollectionPage';
|
||||||
|
|
||||||
export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
|
export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
|
||||||
isCollection(object) || isOrderedCollection(object);
|
isCollection(object) || isOrderedCollection(object);
|
||||||
|
|
||||||
|
@@ -107,6 +107,18 @@ export class MiMeta {
|
|||||||
})
|
})
|
||||||
public iconUrl: string | null;
|
public iconUrl: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public app192IconUrl: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public app512IconUrl: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 1024,
|
length: 1024,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
@@ -444,6 +456,12 @@ export class MiMeta {
|
|||||||
})
|
})
|
||||||
public serverRules: string[];
|
public serverRules: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 8192,
|
||||||
|
default: '{}',
|
||||||
|
})
|
||||||
|
public manifestJsonOverride: string;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
|
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
|
||||||
})
|
})
|
||||||
|
@@ -85,6 +85,14 @@ export const meta = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
app192IconUrl: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
app512IconUrl: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
enableEmail: {
|
enableEmail: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
@@ -278,6 +286,10 @@ export const meta = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
manifestJsonOverride: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
policies: {
|
policies: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
@@ -331,6 +343,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
notFoundImageUrl: instance.notFoundImageUrl,
|
notFoundImageUrl: instance.notFoundImageUrl,
|
||||||
infoImageUrl: instance.infoImageUrl,
|
infoImageUrl: instance.infoImageUrl,
|
||||||
iconUrl: instance.iconUrl,
|
iconUrl: instance.iconUrl,
|
||||||
|
app192IconUrl: instance.app192IconUrl,
|
||||||
|
app512IconUrl: instance.app512IconUrl,
|
||||||
backgroundImageUrl: instance.backgroundImageUrl,
|
backgroundImageUrl: instance.backgroundImageUrl,
|
||||||
logoImageUrl: instance.logoImageUrl,
|
logoImageUrl: instance.logoImageUrl,
|
||||||
defaultLightTheme: instance.defaultLightTheme,
|
defaultLightTheme: instance.defaultLightTheme,
|
||||||
@@ -383,6 +397,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
enableServerMachineStats: instance.enableServerMachineStats,
|
enableServerMachineStats: instance.enableServerMachineStats,
|
||||||
enableIdenticonGeneration: instance.enableIdenticonGeneration,
|
enableIdenticonGeneration: instance.enableIdenticonGeneration,
|
||||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||||
|
manifestJsonOverride: instance.manifestJsonOverride,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -39,6 +39,8 @@ export const paramDef = {
|
|||||||
infoImageUrl: { type: 'string', nullable: true },
|
infoImageUrl: { type: 'string', nullable: true },
|
||||||
notFoundImageUrl: { type: 'string', nullable: true },
|
notFoundImageUrl: { type: 'string', nullable: true },
|
||||||
iconUrl: { type: 'string', nullable: true },
|
iconUrl: { type: 'string', nullable: true },
|
||||||
|
app192IconUrl: { type: 'string', nullable: true },
|
||||||
|
app512IconUrl: { type: 'string', nullable: true },
|
||||||
backgroundImageUrl: { type: 'string', nullable: true },
|
backgroundImageUrl: { type: 'string', nullable: true },
|
||||||
logoImageUrl: { type: 'string', nullable: true },
|
logoImageUrl: { type: 'string', nullable: true },
|
||||||
name: { type: 'string', nullable: true },
|
name: { type: 'string', nullable: true },
|
||||||
@@ -104,6 +106,7 @@ export const paramDef = {
|
|||||||
enableIdenticonGeneration: { type: 'boolean' },
|
enableIdenticonGeneration: { type: 'boolean' },
|
||||||
serverRules: { type: 'array', items: { type: 'string' } },
|
serverRules: { type: 'array', items: { type: 'string' } },
|
||||||
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
||||||
|
manifestJsonOverride: { type: 'string' },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -153,6 +156,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
set.iconUrl = ps.iconUrl;
|
set.iconUrl = ps.iconUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.app192IconUrl !== undefined) {
|
||||||
|
set.app192IconUrl = ps.app192IconUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.app512IconUrl !== undefined) {
|
||||||
|
set.app512IconUrl = ps.app512IconUrl;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.serverErrorImageUrl !== undefined) {
|
if (ps.serverErrorImageUrl !== undefined) {
|
||||||
set.serverErrorImageUrl = ps.serverErrorImageUrl;
|
set.serverErrorImageUrl = ps.serverErrorImageUrl;
|
||||||
}
|
}
|
||||||
@@ -421,6 +432,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
set.preservedUsernames = ps.preservedUsernames;
|
set.preservedUsernames = ps.preservedUsernames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.manifestJsonOverride !== undefined) {
|
||||||
|
set.manifestJsonOverride = ps.manifestJsonOverride;
|
||||||
|
}
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
this.moderationLogService.insertModerationLog(me, 'updateMeta');
|
this.moderationLogService.insertModerationLog(me, 'updateMeta');
|
||||||
});
|
});
|
||||||
|
@@ -51,45 +51,6 @@ const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
|
|||||||
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
|
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
|
||||||
const viteOut = `${_dirname}/../../../../../built/_vite_/`;
|
const viteOut = `${_dirname}/../../../../../built/_vite_/`;
|
||||||
|
|
||||||
const manifest = {
|
|
||||||
'short_name': 'Misskey',
|
|
||||||
'name': 'Misskey',
|
|
||||||
'start_url': '/',
|
|
||||||
'display': 'standalone',
|
|
||||||
'background_color': '#313a42',
|
|
||||||
'theme_color': '#86b300',
|
|
||||||
'icons': [
|
|
||||||
{
|
|
||||||
'src': '/static-assets/icons/192.png',
|
|
||||||
'sizes': '192x192',
|
|
||||||
'type': 'image/png',
|
|
||||||
'purpose': 'maskable',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'src': '/static-assets/icons/512.png',
|
|
||||||
'sizes': '512x512',
|
|
||||||
'type': 'image/png',
|
|
||||||
'purpose': 'maskable',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'src': '/static-assets/splash.png',
|
|
||||||
'sizes': '300x300',
|
|
||||||
'type': 'image/png',
|
|
||||||
'purpose': 'any',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'share_target': {
|
|
||||||
'action': '/share/',
|
|
||||||
'method': 'GET',
|
|
||||||
'enctype': 'application/x-www-form-urlencoded',
|
|
||||||
'params': {
|
|
||||||
'title': 'title',
|
|
||||||
'text': 'text',
|
|
||||||
'url': 'url',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ClientServerService {
|
export class ClientServerService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@@ -148,16 +109,60 @@ export class ClientServerService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async manifestHandler(reply: FastifyReply) {
|
private async manifestHandler(reply: FastifyReply) {
|
||||||
const res = deepClone(manifest);
|
|
||||||
|
|
||||||
const instance = await this.metaService.fetch(true);
|
const instance = await this.metaService.fetch(true);
|
||||||
|
|
||||||
res.short_name = instance.name ?? 'Misskey';
|
let manifest = {
|
||||||
res.name = instance.name ?? 'Misskey';
|
// 空文字列の場合右辺を使いたいため
|
||||||
if (instance.themeColor) res.theme_color = instance.themeColor;
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
'short_name': instance.name || 'Misskey',
|
||||||
|
// 空文字列の場合右辺を使いたいため
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
'name': instance.name || 'Misskey',
|
||||||
|
'start_url': '/',
|
||||||
|
'display': 'standalone',
|
||||||
|
'background_color': '#313a42',
|
||||||
|
// 空文字列の場合右辺を使いたいため
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
'theme_color': instance.themeColor || '#86b300',
|
||||||
|
'icons': [{
|
||||||
|
// 空文字列の場合右辺を使いたいため
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
'src': instance.app192IconUrl || '/static-assets/icons/192.png',
|
||||||
|
'sizes': '192x192',
|
||||||
|
'type': 'image/png',
|
||||||
|
'purpose': 'maskable',
|
||||||
|
}, {
|
||||||
|
// 空文字列の場合右辺を使いたいため
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
'src': instance.app512IconUrl || '/static-assets/icons/512.png',
|
||||||
|
'sizes': '512x512',
|
||||||
|
'type': 'image/png',
|
||||||
|
'purpose': 'maskable',
|
||||||
|
}, {
|
||||||
|
'src': '/static-assets/splash.png',
|
||||||
|
'sizes': '300x300',
|
||||||
|
'type': 'image/png',
|
||||||
|
'purpose': 'any',
|
||||||
|
}],
|
||||||
|
'share_target': {
|
||||||
|
'action': '/share/',
|
||||||
|
'method': 'GET',
|
||||||
|
'enctype': 'application/x-www-form-urlencoded',
|
||||||
|
'params': {
|
||||||
|
'title': 'title',
|
||||||
|
'text': 'text',
|
||||||
|
'url': 'url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
...manifest,
|
||||||
|
...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride),
|
||||||
|
};
|
||||||
|
|
||||||
reply.header('Cache-Control', 'max-age=300');
|
reply.header('Cache-Control', 'max-age=300');
|
||||||
return (res);
|
return (manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -165,6 +170,7 @@ export class ClientServerService {
|
|||||||
return {
|
return {
|
||||||
instanceName: meta.name ?? 'Misskey',
|
instanceName: meta.name ?? 'Misskey',
|
||||||
icon: meta.iconUrl,
|
icon: meta.iconUrl,
|
||||||
|
appleTouchIcon: meta.app512IconUrl,
|
||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
|
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
|
||||||
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
|
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
|
||||||
|
@@ -28,7 +28,7 @@ html
|
|||||||
meta(property='og:site_name' content= instanceName || 'Misskey')
|
meta(property='og:site_name' content= instanceName || 'Misskey')
|
||||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||||
link(rel='icon' href= icon || '/favicon.ico')
|
link(rel='icon' href= icon || '/favicon.ico')
|
||||||
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
|
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
||||||
link(rel='manifest' href='/manifest.json')
|
link(rel='manifest' href='/manifest.json')
|
||||||
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
|
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
|
||||||
link(rel='prefetch' href=serverErrorImageUrl)
|
link(rel='prefetch' href=serverErrorImageUrl)
|
||||||
|
@@ -68,7 +68,7 @@ export class MockResolver extends Resolver {
|
|||||||
const r = this.#responseMap.get(value);
|
const r = this.#responseMap.get(value);
|
||||||
|
|
||||||
if (!r) {
|
if (!r) {
|
||||||
throw new Error('Not registed for mock');
|
throw new Error('Not registered for mock');
|
||||||
}
|
}
|
||||||
|
|
||||||
const object = JSON.parse(r.content);
|
const object = JSON.parse(r.content);
|
||||||
|
@@ -17,7 +17,7 @@ import { GlobalModule } from '@/GlobalModule.js';
|
|||||||
import { CoreModule } from '@/core/CoreModule.js';
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import type { IActor, IApDocument, ICollection, IPost } from '@/core/activitypub/type.js';
|
import type { IActivity, IApDocument, IActor, ICollection, IObject, IOrderedCollection, IOrderedCollectionPage, IPost } from '@/core/activitypub/type.js';
|
||||||
import { MiMeta, MiNote } from '@/models/_.js';
|
import { MiMeta, MiNote } from '@/models/_.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
@@ -29,6 +29,16 @@ const host = 'https://host1.test';
|
|||||||
|
|
||||||
type NonTransientIActor = IActor & { id: string };
|
type NonTransientIActor = IActor & { id: string };
|
||||||
type NonTransientIPost = IPost & { id: string };
|
type NonTransientIPost = IPost & { id: string };
|
||||||
|
type NonTransientICollection = ICollection & { id: string };
|
||||||
|
type NonTransientIOrderedCollection = IOrderedCollection & { id: string };
|
||||||
|
type NonTransientIOrderedCollectionPage = IOrderedCollectionPage & { id: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use when the order of the array is not definitive
|
||||||
|
*/
|
||||||
|
function deepSortedEqual<T extends unknown[]>(array1: unknown[], array2: T): asserts array1 is T {
|
||||||
|
return assert.deepStrictEqual(array1.sort(), array2.sort());
|
||||||
|
}
|
||||||
|
|
||||||
function createRandomActor({ actorHost = host } = {}): NonTransientIActor {
|
function createRandomActor({ actorHost = host } = {}): NonTransientIActor {
|
||||||
const preferredUsername = secureRndstr(8);
|
const preferredUsername = secureRndstr(8);
|
||||||
@@ -60,7 +70,7 @@ function createRandomNotes(actor: NonTransientIActor, length: number): NonTransi
|
|||||||
return new Array(length).fill(null).map(() => createRandomNote(actor));
|
return new Array(length).fill(null).map(() => createRandomNote(actor));
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRandomFeaturedCollection(actor: NonTransientIActor, length: number): ICollection {
|
function createRandomFeaturedCollection(actor: NonTransientIActor, length: number): NonTransientICollection {
|
||||||
const items = createRandomNotes(actor, length);
|
const items = createRandomNotes(actor, length);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -72,6 +82,53 @@ function createRandomFeaturedCollection(actor: NonTransientIActor, length: numbe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRandomActivities(actor: NonTransientIActor, type: string, length: number): IActivity[] {
|
||||||
|
return new Array(length).fill(null).map((): IActivity => {
|
||||||
|
const note = createRandomNote(actor);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
id: `${note.id}/activity`,
|
||||||
|
actor,
|
||||||
|
object: note,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRandomNonPagedOutbox(actor: NonTransientIActor, length: number): NonTransientIOrderedCollection {
|
||||||
|
const orderedItems = createRandomActivities(actor, 'Create', length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'OrderedCollection',
|
||||||
|
id: actor.outbox as string,
|
||||||
|
totalItems: orderedItems.length,
|
||||||
|
orderedItems,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRandomOutboxPage(actor: NonTransientIActor, id: string, length: number): NonTransientIOrderedCollectionPage {
|
||||||
|
const orderedItems = createRandomActivities(actor, 'Create', length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'OrderedCollectionPage',
|
||||||
|
id,
|
||||||
|
totalItems: orderedItems.length,
|
||||||
|
orderedItems,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRandomPagedOutbox(actor: NonTransientIActor): NonTransientIOrderedCollection {
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'OrderedCollection',
|
||||||
|
id: actor.outbox as string,
|
||||||
|
totalItems: 10,
|
||||||
|
first: `${actor.outbox}?first`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function createRandomRemoteUser(
|
async function createRandomRemoteUser(
|
||||||
resolver: MockResolver,
|
resolver: MockResolver,
|
||||||
personService: ApPersonService,
|
personService: ApPersonService,
|
||||||
@@ -196,7 +253,7 @@ describe('ActivityPub', () => {
|
|||||||
|
|
||||||
describe('Renderer', () => {
|
describe('Renderer', () => {
|
||||||
test('Render an announce with visibility: followers', () => {
|
test('Render an announce with visibility: followers', () => {
|
||||||
rendererService.renderAnnounce(null, {
|
rendererService.renderAnnounce('hoge', {
|
||||||
createdAt: new Date(0),
|
createdAt: new Date(0),
|
||||||
visibility: 'followers',
|
visibility: 'followers',
|
||||||
} as MiNote);
|
} as MiNote);
|
||||||
@@ -216,7 +273,7 @@ describe('ActivityPub', () => {
|
|||||||
await personService.createPerson(actor.id, resolver);
|
await personService.createPerson(actor.id, resolver);
|
||||||
|
|
||||||
// All notes in `featured` are same-origin, no need to fetch notes again
|
// All notes in `featured` are same-origin, no need to fetch notes again
|
||||||
assert.deepStrictEqual(resolver.remoteGetTrials(), [actor.id, actor.featured]);
|
deepSortedEqual(resolver.remoteGetTrials(), [actor.id, actor.featured, actor.outbox]);
|
||||||
|
|
||||||
// Created notes without resolving anything
|
// Created notes without resolving anything
|
||||||
for (const item of featured.items as IPost[]) {
|
for (const item of featured.items as IPost[]) {
|
||||||
@@ -247,9 +304,9 @@ describe('ActivityPub', () => {
|
|||||||
await personService.createPerson(actor1.id, resolver);
|
await personService.createPerson(actor1.id, resolver);
|
||||||
|
|
||||||
// actor2Note is from a different server and needs to be fetched again
|
// actor2Note is from a different server and needs to be fetched again
|
||||||
assert.deepStrictEqual(
|
deepSortedEqual(
|
||||||
resolver.remoteGetTrials(),
|
resolver.remoteGetTrials(),
|
||||||
[actor1.id, actor1.featured, actor2Note.id, actor2.id],
|
[actor1.id, actor1.featured, actor1.outbox, actor2Note.id, actor2.id, actor2.outbox],
|
||||||
);
|
);
|
||||||
|
|
||||||
const note = await noteService.fetchNote(actor2Note.id);
|
const note = await noteService.fetchNote(actor2Note.id);
|
||||||
@@ -276,6 +333,95 @@ describe('ActivityPub', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Outbox', () => {
|
||||||
|
test('Fetch non-paged outbox from IActor', async () => {
|
||||||
|
const actor = createRandomActor();
|
||||||
|
const outbox = createRandomNonPagedOutbox(actor, 10);
|
||||||
|
|
||||||
|
resolver.register(actor.id, actor);
|
||||||
|
resolver.register(actor.outbox as string, outbox);
|
||||||
|
|
||||||
|
await personService.createPerson(actor.id, resolver);
|
||||||
|
|
||||||
|
deepSortedEqual(
|
||||||
|
resolver.remoteGetTrials(),
|
||||||
|
[actor.id, actor.outbox],
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const item of outbox.orderedItems as IActivity[]) {
|
||||||
|
const note = await noteService.fetchNote(item.object);
|
||||||
|
assert.ok(note);
|
||||||
|
assert.strictEqual(note.text, 'test test foo');
|
||||||
|
assert.strictEqual(note.uri, (item.object as IObject).id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fetch paged outbox from IActor', async () => {
|
||||||
|
const actor = createRandomActor();
|
||||||
|
const outbox = createRandomPagedOutbox(actor);
|
||||||
|
const page = createRandomOutboxPage(actor, outbox.id, 10);
|
||||||
|
|
||||||
|
resolver.register(actor.id, actor);
|
||||||
|
resolver.register(actor.outbox as string, outbox);
|
||||||
|
resolver.register(outbox.first as string, page);
|
||||||
|
|
||||||
|
await personService.createPerson(actor.id, resolver);
|
||||||
|
|
||||||
|
deepSortedEqual(
|
||||||
|
resolver.remoteGetTrials(),
|
||||||
|
[actor.id, actor.outbox, outbox.first],
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const item of page.orderedItems as IActivity[]) {
|
||||||
|
const note = await noteService.fetchNote(item.object);
|
||||||
|
assert.ok(note);
|
||||||
|
assert.strictEqual(note.text, 'test test foo');
|
||||||
|
assert.strictEqual(note.uri, (item.object as IObject).id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fetch only the first 20 items', async () => {
|
||||||
|
const actor = createRandomActor();
|
||||||
|
const outbox = createRandomNonPagedOutbox(actor, 200);
|
||||||
|
|
||||||
|
resolver.register(actor.id, actor);
|
||||||
|
resolver.register(actor.outbox as string, outbox);
|
||||||
|
|
||||||
|
await personService.createPerson(actor.id, resolver);
|
||||||
|
|
||||||
|
const items = outbox.orderedItems as IActivity[];
|
||||||
|
|
||||||
|
deepSortedEqual(
|
||||||
|
resolver.remoteGetTrials(),
|
||||||
|
[actor.id, actor.outbox],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(await noteService.fetchNote(items[19].object));
|
||||||
|
assert.ok(!await noteService.fetchNote(items[20].object));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Perform only Create activities', async () => {
|
||||||
|
const actor = createRandomActor();
|
||||||
|
const outbox = createRandomNonPagedOutbox(actor, 0);
|
||||||
|
outbox.orderedItems = createRandomActivities(actor, 'Announce', 10);
|
||||||
|
|
||||||
|
resolver.register(actor.id, actor);
|
||||||
|
resolver.register(actor.outbox as string, outbox);
|
||||||
|
|
||||||
|
await personService.createPerson(actor.id, resolver);
|
||||||
|
|
||||||
|
deepSortedEqual(
|
||||||
|
resolver.remoteGetTrials(),
|
||||||
|
[actor.id, actor.outbox],
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const item of outbox.orderedItems as IActivity[]) {
|
||||||
|
const note = await noteService.fetchNote(item.object);
|
||||||
|
assert.ok(!note);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Images', () => {
|
describe('Images', () => {
|
||||||
test('Create images', async () => {
|
test('Create images', async () => {
|
||||||
const imageObject: IApDocument = {
|
const imageObject: IApDocument = {
|
||||||
|
@@ -12,7 +12,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkInput v-model="iconUrl">
|
<MkInput v-model="iconUrl">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.iconUrl }}</template>
|
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
|
||||||
|
</MkInput>
|
||||||
|
|
||||||
|
<MkInput v-model="app192IconUrl">
|
||||||
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
|
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
|
||||||
|
<template #caption>
|
||||||
|
<div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div>
|
||||||
|
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
|
||||||
|
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
|
||||||
|
<div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '192x192px' }) }}</strong></div>
|
||||||
|
</template>
|
||||||
|
</MkInput>
|
||||||
|
|
||||||
|
<MkInput v-model="app512IconUrl">
|
||||||
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
|
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
|
||||||
|
<template #caption>
|
||||||
|
<div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div>
|
||||||
|
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
|
||||||
|
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
|
||||||
|
<div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '512x512px' }) }}</strong></div>
|
||||||
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="bannerUrl">
|
<MkInput v-model="bannerUrl">
|
||||||
@@ -53,6 +75,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
|
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
|
||||||
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
|
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
|
||||||
|
<MkTextarea v-model="manifestJsonOverride">
|
||||||
|
<template #label>{{ i18n.ts._serverSettings.manifestJsonOverride }}</template>
|
||||||
|
</MkTextarea>
|
||||||
</div>
|
</div>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
@@ -69,6 +95,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
|
import JSON5 from 'json5';
|
||||||
import XHeader from './_header_.vue';
|
import XHeader from './_header_.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
@@ -77,13 +104,16 @@ import FormSection from '@/components/form/section.vue';
|
|||||||
import FormSplit from '@/components/form/split.vue';
|
import FormSplit from '@/components/form/split.vue';
|
||||||
import FormSuspense from '@/components/form/suspense.vue';
|
import FormSuspense from '@/components/form/suspense.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { fetchInstance } from '@/instance';
|
import { instance, fetchInstance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkColorInput from '@/components/MkColorInput.vue';
|
import MkColorInput from '@/components/MkColorInput.vue';
|
||||||
|
import { host } from '@/config';
|
||||||
|
|
||||||
let iconUrl: string | null = $ref(null);
|
let iconUrl: string | null = $ref(null);
|
||||||
|
let app192IconUrl: string | null = $ref(null);
|
||||||
|
let app512IconUrl: string | null = $ref(null);
|
||||||
let bannerUrl: string | null = $ref(null);
|
let bannerUrl: string | null = $ref(null);
|
||||||
let backgroundImageUrl: string | null = $ref(null);
|
let backgroundImageUrl: string | null = $ref(null);
|
||||||
let themeColor: any = $ref(null);
|
let themeColor: any = $ref(null);
|
||||||
@@ -92,10 +122,13 @@ let defaultDarkTheme: any = $ref(null);
|
|||||||
let serverErrorImageUrl: string | null = $ref(null);
|
let serverErrorImageUrl: string | null = $ref(null);
|
||||||
let infoImageUrl: string | null = $ref(null);
|
let infoImageUrl: string | null = $ref(null);
|
||||||
let notFoundImageUrl: string | null = $ref(null);
|
let notFoundImageUrl: string | null = $ref(null);
|
||||||
|
let manifestJsonOverride: string = $ref('{}');
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await os.api('admin/meta');
|
const meta = await os.api('admin/meta');
|
||||||
iconUrl = meta.iconUrl;
|
iconUrl = meta.iconUrl;
|
||||||
|
app192IconUrl = meta.app192IconUrl;
|
||||||
|
app512IconUrl = meta.app512IconUrl;
|
||||||
bannerUrl = meta.bannerUrl;
|
bannerUrl = meta.bannerUrl;
|
||||||
backgroundImageUrl = meta.backgroundImageUrl;
|
backgroundImageUrl = meta.backgroundImageUrl;
|
||||||
themeColor = meta.themeColor;
|
themeColor = meta.themeColor;
|
||||||
@@ -104,11 +137,14 @@ async function init() {
|
|||||||
serverErrorImageUrl = meta.serverErrorImageUrl;
|
serverErrorImageUrl = meta.serverErrorImageUrl;
|
||||||
infoImageUrl = meta.infoImageUrl;
|
infoImageUrl = meta.infoImageUrl;
|
||||||
notFoundImageUrl = meta.notFoundImageUrl;
|
notFoundImageUrl = meta.notFoundImageUrl;
|
||||||
|
manifestJsonOverride = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
iconUrl,
|
iconUrl,
|
||||||
|
app192IconUrl,
|
||||||
|
app512IconUrl,
|
||||||
bannerUrl,
|
bannerUrl,
|
||||||
backgroundImageUrl,
|
backgroundImageUrl,
|
||||||
themeColor: themeColor === '' ? null : themeColor,
|
themeColor: themeColor === '' ? null : themeColor,
|
||||||
@@ -117,6 +153,7 @@ function save() {
|
|||||||
infoImageUrl,
|
infoImageUrl,
|
||||||
notFoundImageUrl,
|
notFoundImageUrl,
|
||||||
serverErrorImageUrl,
|
serverErrorImageUrl,
|
||||||
|
manifestJsonOverride: manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride)),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
});
|
});
|
||||||
|
@@ -112,8 +112,8 @@ export function getConfig(): UserConfig {
|
|||||||
|
|
||||||
build: {
|
build: {
|
||||||
target: [
|
target: [
|
||||||
'chrome108',
|
'chrome116',
|
||||||
'firefox109',
|
'firefox116',
|
||||||
'safari16',
|
'safari16',
|
||||||
],
|
],
|
||||||
manifest: 'manifest.json',
|
manifest: 'manifest.json',
|
||||||
|
@@ -353,6 +353,9 @@ export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;
|
|||||||
export type AdminInstanceMetadata = DetailedInstanceMetadata & {
|
export type AdminInstanceMetadata = DetailedInstanceMetadata & {
|
||||||
// TODO: There are more fields.
|
// TODO: There are more fields.
|
||||||
blockedHosts: string[];
|
blockedHosts: string[];
|
||||||
|
app192IconUrl: string | null;
|
||||||
|
app512IconUrl: string | null;
|
||||||
|
manifestJsonOverride: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServerInfo = {
|
export type ServerInfo = {
|
||||||
|
Reference in New Issue
Block a user