refactor(backend): core/activitypub
(#11247)
* eslint: `explicit-function-return-type` * eslint: `no-unnecessary-condition` * eslint: `eslint-disable-next-line` * eslint: `no-unused-vars` * eslint: `comma-dangle` * eslint: `import/order` * cleanup: unnecessary non-null assertion * cleanup: `IActivity`に`actor`は常に存在するようなので * cleanup: unnecessary `as` * cleanup: unnecessary `Promise.resolve` * cleanup * refactor: `String.prototype.match()`である必要がない部分をよりシンプルな書き方に変更 * refactor: よりよい型定義 * refactor: よりよい型定義 - `LdSignature`の`normalize`メソッドでの使われ方から、 - `data`引数の型定義を`any`から`JsonLdDocument`へ修正 - `getLoader`メソッドの返り値の型定義の一部を`any`から`RemoteDocument`へ修正 - `contextUrl`が不正な値(`null`)となっていたことが判明したため`undefined`へ修正 - `document`の型と合わせるために`CONTEXTS`の型定義の一部を`unknown`から`JsonLd`へ修正 - とりあえず`satisfies`を使用 - `document`の型と合わせるために`fetchDocument`メソッドの返り値の型定義の一部を`unknown`から`JsonLd`へ修正 - どうしようもなく`as`を使用 * refactor: 型ガードを使うことでnon-null assertionをやめた * refactor: non-null assertionをやめた `.filter()`で行っている型ガードなどの文脈から、より適しているだろうと思われる書き方に変更した。 * refactor: 型ガードを使うことで`as`をやめた * refactor: `as`をやめた * refactor: よりよい型定義 - `id`は`null`とのunionになっていたが、`null`を渡している場面はなかった - またおそらくこのメソッドは`IOrderedCollection`を返すため、そちらに合わせて`null`とのunionをやめた - `IOrderedCollection`とはまだ型に相違がある - `totalItems`をコメントや使われ方を元に`number`へ推論 * refactor: `for-of` -> `Array.prototype.map` * refactor: `delete`演算子を使わない形に
This commit is contained in:
parent
b6a432fd7b
commit
e35a370af0
13 changed files with 163 additions and 159 deletions
|
@ -16,6 +16,8 @@ type AudienceInfo = {
|
||||||
visibleUsers: User[],
|
visibleUsers: User[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GroupedAudience = Record<'public' | 'followers' | 'other', string[]>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApAudienceService {
|
export class ApAudienceService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -67,11 +69,11 @@ export class ApAudienceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private groupingAudience(ids: string[], actor: RemoteUser) {
|
private groupingAudience(ids: string[], actor: RemoteUser): GroupedAudience {
|
||||||
const groups = {
|
const groups: GroupedAudience = {
|
||||||
public: [] as string[],
|
public: [],
|
||||||
followers: [] as string[],
|
followers: [],
|
||||||
other: [] as string[],
|
other: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
|
@ -90,7 +92,7 @@ export class ApAudienceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isPublic(id: string) {
|
private isPublic(id: string): boolean {
|
||||||
return [
|
return [
|
||||||
'https://www.w3.org/ns/activitystreams#Public',
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
'as#Public',
|
'as#Public',
|
||||||
|
@ -99,9 +101,7 @@ export class ApAudienceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isFollowers(id: string, actor: RemoteUser) {
|
private isFollowers(id: string, actor: RemoteUser): boolean {
|
||||||
return (
|
return id === (actor.followersUri ?? `${actor.uri}/followers`);
|
||||||
id === (actor.followersUri ?? `${actor.uri}/followers`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,13 +99,15 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
if (parsed.local) {
|
if (parsed.local) {
|
||||||
if (parsed.type !== 'users') return null;
|
if (parsed.type !== 'users') return null;
|
||||||
|
|
||||||
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
|
return await this.cacheService.userByIdCache.fetchMaybe(
|
||||||
id: parsed.id,
|
parsed.id,
|
||||||
}).then(x => x ?? undefined)) as LocalUser | undefined ?? null;
|
() => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined),
|
||||||
|
) as LocalUser | undefined ?? null;
|
||||||
} else {
|
} else {
|
||||||
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
|
return await this.cacheService.uriPersonCache.fetch(
|
||||||
uri: parsed.uri,
|
parsed.uri,
|
||||||
})) as RemoteUser | null;
|
() => this.usersRepository.findOneBy({ uri: parsed.uri }),
|
||||||
|
) as RemoteUser | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,9 +147,11 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
} | null> {
|
} | null> {
|
||||||
const user = await this.apPersonService.resolvePerson(uri) as RemoteUser;
|
const user = await this.apPersonService.resolvePerson(uri) as RemoteUser;
|
||||||
|
|
||||||
if (user == null) return null;
|
const key = await this.publicKeyByUserIdCache.fetch(
|
||||||
|
user.id,
|
||||||
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null);
|
() => this.userPublickeysRepository.findOneBy({ userId: user.id }),
|
||||||
|
v => v != null,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
|
|
@ -52,7 +52,7 @@ export class ApDeliverManagerService {
|
||||||
* @param activity Activity
|
* @param activity Activity
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) {
|
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity): Promise<void> {
|
||||||
const manager = new DeliverManager(
|
const manager = new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
|
@ -71,7 +71,7 @@ export class ApDeliverManagerService {
|
||||||
* @param to Target user
|
* @param to Target user
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) {
|
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser): Promise<void> {
|
||||||
const manager = new DeliverManager(
|
const manager = new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
|
@ -84,7 +84,7 @@ export class ApDeliverManagerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) {
|
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null): DeliverManager {
|
||||||
return new DeliverManager(
|
return new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
|
@ -118,6 +118,7 @@ class DeliverManager {
|
||||||
activity: IActivity | null,
|
activity: IActivity | null,
|
||||||
) {
|
) {
|
||||||
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (actor.host != null) throw new Error('actor.host must be null');
|
if (actor.host != null) throw new Error('actor.host must be null');
|
||||||
|
|
||||||
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
||||||
|
@ -131,10 +132,10 @@ class DeliverManager {
|
||||||
* Add recipe for followers deliver
|
* Add recipe for followers deliver
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public addFollowersRecipe() {
|
public addFollowersRecipe(): void {
|
||||||
const deliver = {
|
const deliver: IFollowersRecipe = {
|
||||||
type: 'Followers',
|
type: 'Followers',
|
||||||
} as IFollowersRecipe;
|
};
|
||||||
|
|
||||||
this.addRecipe(deliver);
|
this.addRecipe(deliver);
|
||||||
}
|
}
|
||||||
|
@ -144,11 +145,11 @@ class DeliverManager {
|
||||||
* @param to To
|
* @param to To
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public addDirectRecipe(to: RemoteUser) {
|
public addDirectRecipe(to: RemoteUser): void {
|
||||||
const recipe = {
|
const recipe: IDirectRecipe = {
|
||||||
type: 'Direct',
|
type: 'Direct',
|
||||||
to,
|
to,
|
||||||
} as IDirectRecipe;
|
};
|
||||||
|
|
||||||
this.addRecipe(recipe);
|
this.addRecipe(recipe);
|
||||||
}
|
}
|
||||||
|
@ -158,7 +159,7 @@ class DeliverManager {
|
||||||
* @param recipe Recipe
|
* @param recipe Recipe
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public addRecipe(recipe: IRecipe) {
|
public addRecipe(recipe: IRecipe): void {
|
||||||
this.recipes.push(recipe);
|
this.recipes.push(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,17 +167,13 @@ class DeliverManager {
|
||||||
* Execute delivers
|
* Execute delivers
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async execute() {
|
public async execute(): Promise<void> {
|
||||||
// The value flags whether it is shared or not.
|
// The value flags whether it is shared or not.
|
||||||
// key: inbox URL, value: whether it is sharedInbox
|
// key: inbox URL, value: whether it is sharedInbox
|
||||||
const inboxes = new Map<string, boolean>();
|
const inboxes = new Map<string, boolean>();
|
||||||
|
|
||||||
/*
|
// build inbox list
|
||||||
build inbox list
|
// Process follower recipes first to avoid duplication when processing direct recipes later.
|
||||||
|
|
||||||
Process follower recipes first to avoid duplication when processing
|
|
||||||
direct recipes later.
|
|
||||||
*/
|
|
||||||
if (this.recipes.some(r => isFollowers(r))) {
|
if (this.recipes.some(r => isFollowers(r))) {
|
||||||
// followers deliver
|
// followers deliver
|
||||||
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
||||||
|
@ -190,26 +187,24 @@ class DeliverManager {
|
||||||
followerSharedInbox: true,
|
followerSharedInbox: true,
|
||||||
followerInbox: true,
|
followerInbox: true,
|
||||||
},
|
},
|
||||||
}) as {
|
});
|
||||||
followerSharedInbox: string | null;
|
|
||||||
followerInbox: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
for (const following of followers) {
|
for (const following of followers) {
|
||||||
const inbox = following.followerSharedInbox ?? following.followerInbox;
|
const inbox = following.followerSharedInbox ?? following.followerInbox;
|
||||||
|
if (inbox === null) throw new Error('inbox is null');
|
||||||
inboxes.set(inbox, following.followerSharedInbox != null);
|
inboxes.set(inbox, following.followerSharedInbox != null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recipes.filter((recipe): recipe is IDirectRecipe =>
|
for (const recipe of this.recipes.filter(isDirect)) {
|
||||||
// followers recipes have already been processed
|
|
||||||
isDirect(recipe)
|
|
||||||
// check that shared inbox has not been added yet
|
// check that shared inbox has not been added yet
|
||||||
&& !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox))
|
if (recipe.to.sharedInbox !== null && inboxes.has(recipe.to.sharedInbox)) continue;
|
||||||
|
|
||||||
// check that they actually have an inbox
|
// check that they actually have an inbox
|
||||||
&& recipe.to.inbox != null,
|
if (recipe.to.inbox === null) continue;
|
||||||
)
|
|
||||||
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
inboxes.set(recipe.to.inbox, false);
|
||||||
|
}
|
||||||
|
|
||||||
// deliver
|
// deliver
|
||||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||||
|
|
|
@ -21,10 +21,10 @@ import { CacheService } from '@/core/CacheService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { RemoteUser } from '@/models/entities/User.js';
|
import type { RemoteUser } from '@/models/entities/User.js';
|
||||||
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, 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, 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';
|
||||||
|
@ -86,7 +86,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async performActivity(actor: RemoteUser, activity: IObject) {
|
public async performActivity(actor: RemoteUser, activity: IObject): Promise<void> {
|
||||||
if (isCollectionOrOrderedCollection(activity)) {
|
if (isCollectionOrOrderedCollection(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)) {
|
||||||
|
@ -107,7 +107,7 @@ export class ApInboxService {
|
||||||
if (actor.uri) {
|
if (actor.uri) {
|
||||||
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
this.apPersonService.updatePerson(actor.uri!);
|
this.apPersonService.updatePerson(actor.uri);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,7 +229,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async add(actor: RemoteUser, activity: IAdd): Promise<void> {
|
private async add(actor: RemoteUser, activity: IAdd): Promise<void> {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,7 +273,7 @@ export class ApInboxService {
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 既に同じURIを持つものが登録されていないかチェック
|
// 既に同じURIを持つものが登録されていないかチェック
|
||||||
const exist = await this.apNoteService.fetchNote(uri);
|
const exist = await this.apNoteService.fetchNote(uri);
|
||||||
if (exist) {
|
if (exist) {
|
||||||
return;
|
return;
|
||||||
|
@ -292,7 +292,7 @@ export class ApInboxService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode ?? err}`);
|
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
@ -409,7 +409,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async delete(actor: RemoteUser, activity: IDelete): Promise<string> {
|
private async delete(actor: RemoteUser, activity: IDelete): Promise<string> {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,7 +420,7 @@ export class ApInboxService {
|
||||||
// typeが不明だけど、どうせ消えてるのでremote resolveしない
|
// typeが不明だけど、どうせ消えてるのでremote resolveしない
|
||||||
formerType = undefined;
|
formerType = undefined;
|
||||||
} else {
|
} else {
|
||||||
const object = activity.object as IObject;
|
const object = activity.object;
|
||||||
if (isTombstone(object)) {
|
if (isTombstone(object)) {
|
||||||
formerType = toSingle(object.formerType);
|
formerType = toSingle(object.formerType);
|
||||||
} else {
|
} else {
|
||||||
|
@ -503,7 +503,10 @@ export class ApInboxService {
|
||||||
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
|
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
|
||||||
const uris = getApIds(activity.object);
|
const uris = getApIds(activity.object);
|
||||||
|
|
||||||
const userIds = uris.filter(uri => uri.startsWith(this.config.url + '/users/')).map(uri => uri.split('/').pop()!);
|
const userIds = uris
|
||||||
|
.filter(uri => uri.startsWith(this.config.url + '/users/'))
|
||||||
|
.map(uri => uri.split('/').at(-1))
|
||||||
|
.filter((userId): userId is string => userId !== undefined);
|
||||||
const users = await this.usersRepository.findBy({
|
const users = await this.usersRepository.findBy({
|
||||||
id: In(userIds),
|
id: In(userIds),
|
||||||
});
|
});
|
||||||
|
@ -566,7 +569,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async remove(actor: RemoteUser, activity: IRemove): Promise<void> {
|
private async remove(actor: RemoteUser, activity: IRemove): Promise<void> {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -586,7 +589,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async undo(actor: RemoteUser, activity: IUndo): Promise<string> {
|
private async undo(actor: RemoteUser, activity: IUndo): Promise<string> {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -719,7 +722,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async update(actor: RemoteUser, activity: IUpdate): Promise<string> {
|
private async update(actor: RemoteUser, activity: IUpdate): Promise<string> {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
return 'skip: invalid actor';
|
return 'skip: invalid actor';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -733,7 +736,7 @@ export class ApInboxService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isActor(object)) {
|
if (isActor(object)) {
|
||||||
await this.apPersonService.updatePerson(actor.uri!, resolver, object);
|
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
||||||
return 'ok: Person updated';
|
return 'ok: Person updated';
|
||||||
} else if (getApType(object) === 'Question') {
|
} else if (getApType(object) === 'Question') {
|
||||||
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { MfmService } from '@/core/MfmService.js';
|
import { MfmService } from '@/core/MfmService.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
import { extractApHashtagObjects } from './models/tag.js';
|
import { extractApHashtagObjects } from './models/tag.js';
|
||||||
import type { IObject } from './type.js';
|
import type { IObject } from './type.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApMfmService {
|
export class ApMfmService {
|
||||||
|
@ -19,14 +19,13 @@ export class ApMfmService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public htmlToMfm(html: string, tag?: IObject | IObject[]) {
|
public htmlToMfm(html: string, tag?: IObject | IObject[]): string {
|
||||||
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
|
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name);
|
||||||
|
|
||||||
return this.mfmService.fromHtml(html, hashtagNames);
|
return this.mfmService.fromHtml(html, hashtagNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getNoteHtml(note: Note) {
|
public getNoteHtml(note: Note): string | null {
|
||||||
if (!note.text) return '';
|
if (!note.text) return '';
|
||||||
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createPublicKey } from 'node:crypto';
|
import { createPublicKey } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In, IsNull } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -26,7 +26,6 @@ import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
import { LdSignatureService } from './LdSignatureService.js';
|
import { LdSignatureService } from './LdSignatureService.js';
|
||||||
import { ApMfmService } from './ApMfmService.js';
|
import { ApMfmService } from './ApMfmService.js';
|
||||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||||
import type { IIdentifier } from './models/identifier.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApRendererService {
|
export class ApRendererService {
|
||||||
|
@ -63,7 +62,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept {
|
public renderAccept(object: string | IObject, user: { id: User['id']; host: null }): IAccept {
|
||||||
return {
|
return {
|
||||||
type: 'Accept',
|
type: 'Accept',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -72,7 +71,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAdd(user: LocalUser, target: any, object: any): IAdd {
|
public renderAdd(user: LocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
|
||||||
return {
|
return {
|
||||||
type: 'Add',
|
type: 'Add',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -82,7 +81,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAnnounce(object: any, note: Note): IAnnounce {
|
public renderAnnounce(object: string | IObject, note: Note): IAnnounce {
|
||||||
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
||||||
|
|
||||||
let to: string[] = [];
|
let to: string[] = [];
|
||||||
|
@ -133,13 +132,13 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderCreate(object: IObject, note: Note): ICreate {
|
public renderCreate(object: IObject, note: Note): ICreate {
|
||||||
const activity = {
|
const activity: ICreate = {
|
||||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
object,
|
object,
|
||||||
} as ICreate;
|
};
|
||||||
|
|
||||||
if (object.to) activity.to = object.to;
|
if (object.to) activity.to = object.to;
|
||||||
if (object.cc) activity.cc = object.cc;
|
if (object.cc) activity.cc = object.cc;
|
||||||
|
@ -209,7 +208,7 @@ export class ApRendererService {
|
||||||
* @param id Follower|Followee ID
|
* @param id Follower|Followee ID
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderFollowUser(id: User['id']) {
|
public async renderFollowUser(id: User['id']): Promise<string> {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
|
const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
|
||||||
return this.userEntityService.getUserUri(user);
|
return this.userEntityService.getUserUri(user);
|
||||||
}
|
}
|
||||||
|
@ -223,8 +222,8 @@ export class ApRendererService {
|
||||||
return {
|
return {
|
||||||
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: this.userEntityService.getUserUri(follower)!,
|
actor: this.userEntityService.getUserUri(follower),
|
||||||
object: this.userEntityService.getUserUri(followee)!,
|
object: this.userEntityService.getUserUri(followee),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,14 +263,14 @@ export class ApRendererService {
|
||||||
public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> {
|
public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> {
|
||||||
const reaction = noteReaction.reaction;
|
const reaction = noteReaction.reaction;
|
||||||
|
|
||||||
const object = {
|
const object: ILike = {
|
||||||
type: 'Like',
|
type: 'Like',
|
||||||
id: `${this.config.url}/likes/${noteReaction.id}`,
|
id: `${this.config.url}/likes/${noteReaction.id}`,
|
||||||
actor: `${this.config.url}/users/${noteReaction.userId}`,
|
actor: `${this.config.url}/users/${noteReaction.userId}`,
|
||||||
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
||||||
content: reaction,
|
content: reaction,
|
||||||
_misskey_reaction: reaction,
|
_misskey_reaction: reaction,
|
||||||
} as ILike;
|
};
|
||||||
|
|
||||||
if (reaction.startsWith(':')) {
|
if (reaction.startsWith(':')) {
|
||||||
const name = reaction.replaceAll(':', '');
|
const name = reaction.replaceAll(':', '');
|
||||||
|
@ -287,7 +286,7 @@ export class ApRendererService {
|
||||||
public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
|
public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
|
||||||
return {
|
return {
|
||||||
type: 'Mention',
|
type: 'Mention',
|
||||||
href: this.userEntityService.getUserUri(mention)!,
|
href: this.userEntityService.getUserUri(mention),
|
||||||
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
|
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -297,8 +296,8 @@ export class ApRendererService {
|
||||||
src: PartialLocalUser | PartialRemoteUser,
|
src: PartialLocalUser | PartialRemoteUser,
|
||||||
dst: PartialLocalUser | PartialRemoteUser,
|
dst: PartialLocalUser | PartialRemoteUser,
|
||||||
): IMove {
|
): IMove {
|
||||||
const actor = this.userEntityService.getUserUri(src)!;
|
const actor = this.userEntityService.getUserUri(src);
|
||||||
const target = this.userEntityService.getUserUri(dst)!;
|
const target = this.userEntityService.getUserUri(dst);
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
||||||
actor,
|
actor,
|
||||||
|
@ -310,10 +309,10 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderNote(note: Note, dive = true): Promise<IPost> {
|
public async renderNote(note: Note, dive = true): Promise<IPost> {
|
||||||
const getPromisedFiles = async (ids: string[]) => {
|
const getPromisedFiles = async (ids: string[]): Promise<DriveFile[]> => {
|
||||||
if (!ids || ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||||
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
|
return ids.map(id => items.find(item => item.id === id)).filter((item): item is DriveFile => item != null);
|
||||||
};
|
};
|
||||||
|
|
||||||
let inReplyTo;
|
let inReplyTo;
|
||||||
|
@ -375,7 +374,7 @@ export class ApRendererService {
|
||||||
id: In(note.mentions),
|
id: In(note.mentions),
|
||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
|
const hashtagTags = note.tags.map(tag => this.renderHashtag(tag));
|
||||||
const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
|
const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
|
||||||
|
|
||||||
const files = await getPromisedFiles(note.fileIds);
|
const files = await getPromisedFiles(note.fileIds);
|
||||||
|
@ -451,37 +450,26 @@ export class ApRendererService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderPerson(user: LocalUser) {
|
public async renderPerson(user: LocalUser) {
|
||||||
const id = this.userEntityService.genLocalUserUri(user.id);
|
const id = this.userEntityService.genLocalUserUri(user.id);
|
||||||
const isSystem = !!user.username.match(/\./);
|
const isSystem = user.username.includes('.');
|
||||||
|
|
||||||
const [avatar, banner, profile] = await Promise.all([
|
const [avatar, banner, profile] = await Promise.all([
|
||||||
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined),
|
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
|
||||||
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined),
|
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
|
||||||
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
|
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const attachment: {
|
const attachment = profile.fields.map(field => ({
|
||||||
type: 'PropertyValue',
|
type: 'PropertyValue',
|
||||||
name: string,
|
name: field.name,
|
||||||
value: string,
|
value: /^https?:/.test(field.value)
|
||||||
identifier?: IIdentifier,
|
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
|
||||||
}[] = [];
|
: field.value,
|
||||||
|
}));
|
||||||
if (profile.fields) {
|
|
||||||
for (const field of profile.fields) {
|
|
||||||
attachment.push({
|
|
||||||
type: 'PropertyValue',
|
|
||||||
name: field.name,
|
|
||||||
value: (field.value != null && field.value.match(/^https?:/))
|
|
||||||
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
|
|
||||||
: field.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = await this.getEmojis(user.emojis);
|
const emojis = await this.getEmojis(user.emojis);
|
||||||
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||||
|
|
||||||
const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag));
|
const hashtagTags = user.tags.map(tag => this.renderHashtag(tag));
|
||||||
|
|
||||||
const tag = [
|
const tag = [
|
||||||
...apemojis,
|
...apemojis,
|
||||||
|
@ -490,7 +478,7 @@ export class ApRendererService {
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const person = {
|
const person: any = {
|
||||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||||
id,
|
id,
|
||||||
inbox: `${id}/inbox`,
|
inbox: `${id}/inbox`,
|
||||||
|
@ -508,11 +496,11 @@ export class ApRendererService {
|
||||||
image: banner ? this.renderImage(banner) : null,
|
image: banner ? this.renderImage(banner) : null,
|
||||||
tag,
|
tag,
|
||||||
manuallyApprovesFollowers: user.isLocked,
|
manuallyApprovesFollowers: user.isLocked,
|
||||||
discoverable: !!user.isExplorable,
|
discoverable: user.isExplorable,
|
||||||
publicKey: this.renderKey(user, keypair, '#main-key'),
|
publicKey: this.renderKey(user, keypair, '#main-key'),
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
attachment: attachment.length ? attachment : undefined,
|
attachment: attachment.length ? attachment : undefined,
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
if (user.movedToUri) {
|
if (user.movedToUri) {
|
||||||
person.movedTo = user.movedToUri;
|
person.movedTo = user.movedToUri;
|
||||||
|
@ -552,7 +540,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderReject(object: any, user: { id: User['id'] }): IReject {
|
public renderReject(object: string | IObject, user: { id: User['id'] }): IReject {
|
||||||
return {
|
return {
|
||||||
type: 'Reject',
|
type: 'Reject',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -561,7 +549,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove {
|
public renderRemove(user: { id: User['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
|
||||||
return {
|
return {
|
||||||
type: 'Remove',
|
type: 'Remove',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -579,8 +567,8 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderUndo(object: any, user: { id: User['id'] }): IUndo {
|
public renderUndo(object: string | IObject, user: { id: User['id'] }): IUndo {
|
||||||
const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
|
const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'Undo',
|
type: 'Undo',
|
||||||
|
@ -592,7 +580,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate {
|
public renderUpdate(object: string | IObject, user: { id: User['id'] }): IUpdate {
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -658,7 +646,7 @@ export class ApRendererService {
|
||||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}, x as T & { id: string; });
|
}, x as T & { id: string });
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -683,13 +671,13 @@ export class ApRendererService {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
|
public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
|
||||||
const page = {
|
const page: any = {
|
||||||
id,
|
id,
|
||||||
partOf,
|
partOf,
|
||||||
type: 'OrderedCollectionPage',
|
type: 'OrderedCollectionPage',
|
||||||
totalItems,
|
totalItems,
|
||||||
orderedItems,
|
orderedItems,
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
if (prev) page.prev = prev;
|
if (prev) page.prev = prev;
|
||||||
if (next) page.next = next;
|
if (next) page.next = next;
|
||||||
|
@ -706,7 +694,7 @@ export class ApRendererService {
|
||||||
* @param orderedItems attached objects (optional)
|
* @param orderedItems attached objects (optional)
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) {
|
public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) {
|
||||||
const page: any = {
|
const page: any = {
|
||||||
id,
|
id,
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
|
@ -722,7 +710,7 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
||||||
if (names == null || names.length === 0) return [];
|
if (names.length === 0) return [];
|
||||||
|
|
||||||
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
||||||
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
||||||
|
|
|
@ -140,7 +140,7 @@ export class ApRequestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
|
public async signedPost(user: { id: User['id'] }, url: string, object: unknown): Promise<void> {
|
||||||
const body = JSON.stringify(object);
|
const body = JSON.stringify(object);
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
@ -169,7 +169,7 @@ export class ApRequestService {
|
||||||
* @param url URL to fetch
|
* @param url URL to fetch
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedGet(url: string, user: { id: User['id'] }) {
|
public async signedGet(url: string, user: { id: User['id'] }): Promise<unknown> {
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const req = ApRequestCreator.createSignedGet({
|
const req = ApRequestCreator.createSignedGet({
|
||||||
|
|
|
@ -61,10 +61,6 @@ export class Resolver {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolve(value: string | IObject): Promise<IObject> {
|
public async resolve(value: string | IObject): Promise<IObject> {
|
||||||
if (value == null) {
|
|
||||||
throw new Error('resolvee is null (or undefined)');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
@ -104,11 +100,11 @@ export class Resolver {
|
||||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
? await this.apRequestService.signedGet(value, this.user) as IObject
|
||||||
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
|
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
|
||||||
|
|
||||||
if (object == null || (
|
if (
|
||||||
Array.isArray(object['@context']) ?
|
Array.isArray(object['@context']) ?
|
||||||
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
|
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
|
||||||
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
||||||
)) {
|
) {
|
||||||
throw new Error('invalid response');
|
throw new Error('invalid response');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CONTEXTS } from './misc/contexts.js';
|
import { CONTEXTS } from './misc/contexts.js';
|
||||||
|
import type { JsonLdDocument } from 'jsonld';
|
||||||
|
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
|
||||||
|
|
||||||
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
|
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
|
||||||
|
|
||||||
|
@ -18,22 +20,21 @@ class LdSignature {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
|
public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
|
||||||
const options = {
|
const options: {
|
||||||
type: 'RsaSignature2017',
|
|
||||||
creator,
|
|
||||||
domain,
|
|
||||||
nonce: crypto.randomBytes(16).toString('hex'),
|
|
||||||
created: (created ?? new Date()).toISOString(),
|
|
||||||
} as {
|
|
||||||
type: string;
|
type: string;
|
||||||
creator: string;
|
creator: string;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
nonce: string;
|
nonce: string;
|
||||||
created: string;
|
created: string;
|
||||||
|
} = {
|
||||||
|
type: 'RsaSignature2017',
|
||||||
|
creator,
|
||||||
|
nonce: crypto.randomBytes(16).toString('hex'),
|
||||||
|
created: (created ?? new Date()).toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!domain) {
|
if (domain) {
|
||||||
delete options.domain;
|
options.domain = domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toBeSigned = await this.createVerifyData(data, options);
|
const toBeSigned = await this.createVerifyData(data, options);
|
||||||
|
@ -62,7 +63,7 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createVerifyData(data: any, options: any) {
|
public async createVerifyData(data: any, options: any): Promise<string> {
|
||||||
const transformedOptions = {
|
const transformedOptions = {
|
||||||
...options,
|
...options,
|
||||||
'@context': 'https://w3id.org/identity/v1',
|
'@context': 'https://w3id.org/identity/v1',
|
||||||
|
@ -82,7 +83,7 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async normalize(data: any) {
|
public async normalize(data: JsonLdDocument): Promise<string> {
|
||||||
const customLoader = this.getLoader();
|
const customLoader = this.getLoader();
|
||||||
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
|
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
|
||||||
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
|
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
|
||||||
|
@ -93,14 +94,14 @@ class LdSignature {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private getLoader() {
|
private getLoader() {
|
||||||
return async (url: string): Promise<any> => {
|
return async (url: string): Promise<RemoteDocument> => {
|
||||||
if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`);
|
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
|
||||||
|
|
||||||
if (this.preLoad) {
|
if (this.preLoad) {
|
||||||
if (url in CONTEXTS) {
|
if (url in CONTEXTS) {
|
||||||
if (this.debug) console.debug(`HIT: ${url}`);
|
if (this.debug) console.debug(`HIT: ${url}`);
|
||||||
return {
|
return {
|
||||||
contextUrl: null,
|
contextUrl: undefined,
|
||||||
document: CONTEXTS[url],
|
document: CONTEXTS[url],
|
||||||
documentUrl: url,
|
documentUrl: url,
|
||||||
};
|
};
|
||||||
|
@ -110,7 +111,7 @@ class LdSignature {
|
||||||
if (this.debug) console.debug(`MISS: ${url}`);
|
if (this.debug) console.debug(`MISS: ${url}`);
|
||||||
const document = await this.fetchDocument(url);
|
const document = await this.fetchDocument(url);
|
||||||
return {
|
return {
|
||||||
contextUrl: null,
|
contextUrl: undefined,
|
||||||
document: document,
|
document: document,
|
||||||
documentUrl: url,
|
documentUrl: url,
|
||||||
};
|
};
|
||||||
|
@ -118,13 +119,17 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchDocument(url: string) {
|
private async fetchDocument(url: string): Promise<JsonLd> {
|
||||||
const json = await this.httpRequestService.send(url, {
|
const json = await this.httpRequestService.send(
|
||||||
headers: {
|
url,
|
||||||
Accept: 'application/ld+json, application/json',
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/ld+json, application/json',
|
||||||
|
},
|
||||||
|
timeout: this.loderTimeout,
|
||||||
},
|
},
|
||||||
timeout: this.loderTimeout,
|
{ throwErrorWhenResponseNotOk: false },
|
||||||
}, { throwErrorWhenResponseNotOk: false }).then(res => {
|
).then(res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`${res.status} ${res.statusText}`);
|
throw new Error(`${res.status} ${res.statusText}`);
|
||||||
} else {
|
} else {
|
||||||
|
@ -132,7 +137,7 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return json;
|
return json as JsonLd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { JsonLd } from 'jsonld/jsonld-spec.js';
|
||||||
|
|
||||||
/* eslint:disable:quotemark indent */
|
/* eslint:disable:quotemark indent */
|
||||||
const id_v1 = {
|
const id_v1 = {
|
||||||
'@context': {
|
'@context': {
|
||||||
|
@ -86,7 +88,7 @@ const id_v1 = {
|
||||||
'accessControl': { '@id': 'perm:accessControl', '@type': '@id' },
|
'accessControl': { '@id': 'perm:accessControl', '@type': '@id' },
|
||||||
'writePermission': { '@id': 'perm:writePermission', '@type': '@id' },
|
'writePermission': { '@id': 'perm:writePermission', '@type': '@id' },
|
||||||
},
|
},
|
||||||
};
|
} satisfies JsonLd;
|
||||||
|
|
||||||
const security_v1 = {
|
const security_v1 = {
|
||||||
'@context': {
|
'@context': {
|
||||||
|
@ -137,7 +139,7 @@ const security_v1 = {
|
||||||
'signatureAlgorithm': 'sec:signingAlgorithm',
|
'signatureAlgorithm': 'sec:signingAlgorithm',
|
||||||
'signatureValue': 'sec:signatureValue',
|
'signatureValue': 'sec:signatureValue',
|
||||||
},
|
},
|
||||||
};
|
} satisfies JsonLd;
|
||||||
|
|
||||||
const activitystreams = {
|
const activitystreams = {
|
||||||
'@context': {
|
'@context': {
|
||||||
|
@ -517,9 +519,9 @@ const activitystreams = {
|
||||||
'@type': '@id',
|
'@type': '@id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} satisfies JsonLd;
|
||||||
|
|
||||||
export const CONTEXTS: Record<string, unknown> = {
|
export const CONTEXTS: Record<string, JsonLd> = {
|
||||||
'https://w3id.org/identity/v1': id_v1,
|
'https://w3id.org/identity/v1': id_v1,
|
||||||
'https://w3id.org/security/v1': security_v1,
|
'https://w3id.org/security/v1': security_v1,
|
||||||
'https://www.w3.org/ns/activitystreams': activitystreams,
|
'https://www.w3.org/ns/activitystreams': activitystreams,
|
||||||
|
|
|
@ -200,7 +200,7 @@ export class ApNoteService {
|
||||||
| { status: 'ok'; res: Note }
|
| { status: 'ok'; res: Note }
|
||||||
| { status: 'permerror' | 'temperror' }
|
| { status: 'permerror' | 'temperror' }
|
||||||
> => {
|
> => {
|
||||||
if (!uri.match(/^https?:/)) return { status: 'permerror' };
|
if (!/^https?:/.test(uri)) return { status: 'permerror' };
|
||||||
try {
|
try {
|
||||||
const res = await this.resolveNote(uri);
|
const res = await this.resolveNote(uri);
|
||||||
if (res == null) return { status: 'permerror' };
|
if (res == null) return { status: 'permerror' };
|
||||||
|
|
|
@ -194,7 +194,6 @@ export interface IApPropertyValue extends IObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
||||||
object &&
|
|
||||||
getApType(object) === 'PropertyValue' &&
|
getApType(object) === 'PropertyValue' &&
|
||||||
typeof object.name === 'string' &&
|
typeof object.name === 'string' &&
|
||||||
'value' in object &&
|
'value' in object &&
|
||||||
|
|
|
@ -189,7 +189,11 @@ export class ActivityPubServerService {
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
} else {
|
} else {
|
||||||
// index page
|
// index page
|
||||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
|
const rendered = this.apRendererService.renderOrderedCollection(
|
||||||
|
partOf,
|
||||||
|
user.followersCount,
|
||||||
|
`${partOf}?page=true`,
|
||||||
|
);
|
||||||
reply.header('Cache-Control', 'public, max-age=180');
|
reply.header('Cache-Control', 'public, max-age=180');
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
|
@ -277,7 +281,11 @@ export class ActivityPubServerService {
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
} else {
|
} else {
|
||||||
// index page
|
// index page
|
||||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
|
const rendered = this.apRendererService.renderOrderedCollection(
|
||||||
|
partOf,
|
||||||
|
user.followingCount,
|
||||||
|
`${partOf}?page=true`,
|
||||||
|
);
|
||||||
reply.header('Cache-Control', 'public, max-age=180');
|
reply.header('Cache-Control', 'public, max-age=180');
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
|
@ -310,7 +318,10 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const rendered = this.apRendererService.renderOrderedCollection(
|
const rendered = this.apRendererService.renderOrderedCollection(
|
||||||
`${this.config.url}/users/${userId}/collections/featured`,
|
`${this.config.url}/users/${userId}/collections/featured`,
|
||||||
renderedNotes.length, undefined, undefined, renderedNotes,
|
renderedNotes.length,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
renderedNotes,
|
||||||
);
|
);
|
||||||
|
|
||||||
reply.header('Cache-Control', 'public, max-age=180');
|
reply.header('Cache-Control', 'public, max-age=180');
|
||||||
|
@ -395,7 +406,9 @@ export class ActivityPubServerService {
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
} else {
|
} else {
|
||||||
// index page
|
// index page
|
||||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount,
|
const rendered = this.apRendererService.renderOrderedCollection(
|
||||||
|
partOf,
|
||||||
|
user.notesCount,
|
||||||
`${partOf}?page=true`,
|
`${partOf}?page=true`,
|
||||||
`${partOf}?page=true&since_id=000000000000000000000000`,
|
`${partOf}?page=true&since_id=000000000000000000000000`,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue