Merge branch 'develop' into pag-back
This commit is contained in:
		| @@ -27,6 +27,7 @@ | |||||||
| - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように | - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように | ||||||
| - 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように | - 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように | ||||||
| - プロフィールURLをコピーできるボタンを追加 #11190 | - プロフィールURLをコピーできるボタンを追加 #11190 | ||||||
|  | - `CURRENT_URL`で現在表示中のURLを取得できるように(AiScript) | ||||||
| - ユーザーのContextMenuに「アンテナに追加」ボタンを追加 | - ユーザーのContextMenuに「アンテナに追加」ボタンを追加 | ||||||
| - フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように | - フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように | ||||||
| - 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように | - 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ function greet() { | |||||||
| 		console.log(themeColor(' |     |_|___ ___| |_ ___ _ _ ')); | 		console.log(themeColor(' |     |_|___ ___| |_ ___ _ _ ')); | ||||||
| 		console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |')); | 		console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |')); | ||||||
| 		console.log(themeColor(' |_|_|_|_|___|___|_,_|___|_  |')); | 		console.log(themeColor(' |_|_|_|_|___|___|_,_|___|_  |')); | ||||||
| 		console.log(' ' + chalk.gray(v) + themeColor('                        |___|\n'.substr(v.length))); | 		console.log(' ' + chalk.gray(v) + themeColor('                        |___|\n'.substring(v.length))); | ||||||
| 		//#endregion | 		//#endregion | ||||||
|  |  | ||||||
| 		console.log(' Misskey is an open-source decentralized microblogging platform.'); | 		console.log(' Misskey is an open-source decentralized microblogging platform.'); | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -29,6 +29,121 @@ const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe => | |||||||
| const isDirect = (recipe: IRecipe): recipe is IDirectRecipe => | const isDirect = (recipe: IRecipe): recipe is IDirectRecipe => | ||||||
| 	recipe.type === 'Direct'; | 	recipe.type === 'Direct'; | ||||||
|  |  | ||||||
|  | class DeliverManager { | ||||||
|  | 	private actor: ThinUser; | ||||||
|  | 	private activity: IActivity | null; | ||||||
|  | 	private recipes: IRecipe[] = []; | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Constructor | ||||||
|  | 	 * @param userEntityService | ||||||
|  | 	 * @param followingsRepository | ||||||
|  | 	 * @param queueService | ||||||
|  | 	 * @param actor Actor | ||||||
|  | 	 * @param activity Activity to deliver | ||||||
|  | 	 */ | ||||||
|  | 	constructor( | ||||||
|  | 		private userEntityService: UserEntityService, | ||||||
|  | 		private followingsRepository: FollowingsRepository, | ||||||
|  | 		private queueService: QueueService, | ||||||
|  |  | ||||||
|  | 		actor: { id: User['id']; host: 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'); | ||||||
|  |  | ||||||
|  | 		// パフォーマンス向上のためキューに突っ込むのはidのみに絞る | ||||||
|  | 		this.actor = { | ||||||
|  | 			id: actor.id, | ||||||
|  | 		}; | ||||||
|  | 		this.activity = activity; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Add recipe for followers deliver | ||||||
|  | 	 */ | ||||||
|  | 	@bindThis | ||||||
|  | 	public addFollowersRecipe(): void { | ||||||
|  | 		const deliver: IFollowersRecipe = { | ||||||
|  | 			type: 'Followers', | ||||||
|  | 		}; | ||||||
|  |  | ||||||
|  | 		this.addRecipe(deliver); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Add recipe for direct deliver | ||||||
|  | 	 * @param to To | ||||||
|  | 	 */ | ||||||
|  | 	@bindThis | ||||||
|  | 	public addDirectRecipe(to: RemoteUser): void { | ||||||
|  | 		const recipe: IDirectRecipe = { | ||||||
|  | 			type: 'Direct', | ||||||
|  | 			to, | ||||||
|  | 		}; | ||||||
|  |  | ||||||
|  | 		this.addRecipe(recipe); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Add recipe | ||||||
|  | 	 * @param recipe Recipe | ||||||
|  | 	 */ | ||||||
|  | 	@bindThis | ||||||
|  | 	public addRecipe(recipe: IRecipe): void { | ||||||
|  | 		this.recipes.push(recipe); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Execute delivers | ||||||
|  | 	 */ | ||||||
|  | 	@bindThis | ||||||
|  | 	public async execute(): Promise<void> { | ||||||
|  | 		// The value flags whether it is shared or not. | ||||||
|  | 		// key: inbox URL, value: whether it is sharedInbox | ||||||
|  | 		const inboxes = new Map<string, boolean>(); | ||||||
|  |  | ||||||
|  | 		// build inbox list | ||||||
|  | 		// Process follower recipes first to avoid duplication when processing direct recipes later. | ||||||
|  | 		if (this.recipes.some(r => isFollowers(r))) { | ||||||
|  | 			// followers deliver | ||||||
|  | 			// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう | ||||||
|  | 			// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう? | ||||||
|  | 			const followers = await this.followingsRepository.find({ | ||||||
|  | 				where: { | ||||||
|  | 					followeeId: this.actor.id, | ||||||
|  | 					followerHost: Not(IsNull()), | ||||||
|  | 				}, | ||||||
|  | 				select: { | ||||||
|  | 					followerSharedInbox: true, | ||||||
|  | 					followerInbox: true, | ||||||
|  | 				}, | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			for (const following of followers) { | ||||||
|  | 				const inbox = following.followerSharedInbox ?? following.followerInbox; | ||||||
|  | 				if (inbox === null) throw new Error('inbox is null'); | ||||||
|  | 				inboxes.set(inbox, following.followerSharedInbox != null); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for (const recipe of this.recipes.filter(isDirect)) { | ||||||
|  | 			// check that shared inbox has not been added yet | ||||||
|  | 			if (recipe.to.sharedInbox !== null && inboxes.has(recipe.to.sharedInbox)) continue; | ||||||
|  |  | ||||||
|  | 			// check that they actually have an inbox | ||||||
|  | 			if (recipe.to.inbox === null) continue; | ||||||
|  |  | ||||||
|  | 			inboxes.set(recipe.to.inbox, false); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// deliver | ||||||
|  | 		this.queueService.deliverMany(this.actor, this.activity, inboxes); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class ApDeliverManagerService { | export class ApDeliverManagerService { | ||||||
| 	constructor( | 	constructor( | ||||||
| @@ -52,7 +167,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 +186,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 +199,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, | ||||||
| @@ -95,123 +210,3 @@ export class ApDeliverManagerService { | |||||||
| 		); | 		); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| class DeliverManager { |  | ||||||
| 	private actor: ThinUser; |  | ||||||
| 	private activity: IActivity | null; |  | ||||||
| 	private recipes: IRecipe[] = []; |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Constructor |  | ||||||
| 	 * @param userEntityService |  | ||||||
| 	 * @param followingsRepository |  | ||||||
| 	 * @param queueService |  | ||||||
| 	 * @param actor Actor |  | ||||||
| 	 * @param activity Activity to deliver |  | ||||||
| 	 */ |  | ||||||
| 	constructor( |  | ||||||
| 		private userEntityService: UserEntityService, |  | ||||||
| 		private followingsRepository: FollowingsRepository, |  | ||||||
| 		private queueService: QueueService, |  | ||||||
|  |  | ||||||
| 		actor: { id: User['id']; host: null; }, |  | ||||||
| 		activity: IActivity | null, |  | ||||||
| 	) { |  | ||||||
| 		// 型で弾いてはいるが一応ローカルユーザーかチェック |  | ||||||
| 		if (actor.host != null) throw new Error('actor.host must be null'); |  | ||||||
|  |  | ||||||
| 		// パフォーマンス向上のためキューに突っ込むのはidのみに絞る |  | ||||||
| 		this.actor = { |  | ||||||
| 			id: actor.id, |  | ||||||
| 		}; |  | ||||||
| 		this.activity = activity; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Add recipe for followers deliver |  | ||||||
| 	 */ |  | ||||||
| 	@bindThis |  | ||||||
| 	public addFollowersRecipe() { |  | ||||||
| 		const deliver = { |  | ||||||
| 			type: 'Followers', |  | ||||||
| 		} as IFollowersRecipe; |  | ||||||
|  |  | ||||||
| 		this.addRecipe(deliver); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Add recipe for direct deliver |  | ||||||
| 	 * @param to To |  | ||||||
| 	 */ |  | ||||||
| 	@bindThis |  | ||||||
| 	public addDirectRecipe(to: RemoteUser) { |  | ||||||
| 		const recipe = { |  | ||||||
| 			type: 'Direct', |  | ||||||
| 			to, |  | ||||||
| 		} as IDirectRecipe; |  | ||||||
|  |  | ||||||
| 		this.addRecipe(recipe); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Add recipe |  | ||||||
| 	 * @param recipe Recipe |  | ||||||
| 	 */ |  | ||||||
| 	@bindThis |  | ||||||
| 	public addRecipe(recipe: IRecipe) { |  | ||||||
| 		this.recipes.push(recipe); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Execute delivers |  | ||||||
| 	 */ |  | ||||||
| 	@bindThis |  | ||||||
| 	public async execute() { |  | ||||||
| 		// The value flags whether it is shared or not. |  | ||||||
| 		// key: inbox URL, value: whether it is sharedInbox |  | ||||||
| 		const inboxes = new Map<string, boolean>(); |  | ||||||
|  |  | ||||||
| 		/* |  | ||||||
| 		build inbox list |  | ||||||
|  |  | ||||||
| 		Process follower recipes first to avoid duplication when processing |  | ||||||
| 		direct recipes later. |  | ||||||
| 		*/ |  | ||||||
| 		if (this.recipes.some(r => isFollowers(r))) { |  | ||||||
| 			// followers deliver |  | ||||||
| 			// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう |  | ||||||
| 			// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう? |  | ||||||
| 			const followers = await this.followingsRepository.find({ |  | ||||||
| 				where: { |  | ||||||
| 					followeeId: this.actor.id, |  | ||||||
| 					followerHost: Not(IsNull()), |  | ||||||
| 				}, |  | ||||||
| 				select: { |  | ||||||
| 					followerSharedInbox: true, |  | ||||||
| 					followerInbox: true, |  | ||||||
| 				}, |  | ||||||
| 			}) as { |  | ||||||
| 				followerSharedInbox: string | null; |  | ||||||
| 				followerInbox: string; |  | ||||||
| 			}[]; |  | ||||||
|  |  | ||||||
| 			for (const following of followers) { |  | ||||||
| 				const inbox = following.followerSharedInbox ?? following.followerInbox; |  | ||||||
| 				inboxes.set(inbox, following.followerSharedInbox != null); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		this.recipes.filter((recipe): recipe is IDirectRecipe => |  | ||||||
| 			// followers recipes have already been processed |  | ||||||
| 			isDirect(recipe) |  | ||||||
| 			// check that shared inbox has not been added yet |  | ||||||
| 			&& !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox)) |  | ||||||
| 			// check that they actually have an inbox |  | ||||||
| 			&& recipe.to.inbox != null, |  | ||||||
| 		) |  | ||||||
| 			.forEach(recipe => inboxes.set(recipe.to.inbox!, false)); |  | ||||||
|  |  | ||||||
| 		// deliver |  | ||||||
| 		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'); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -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', |  | ||||||
| 			name: string, |  | ||||||
| 			value: string, |  | ||||||
| 			identifier?: IIdentifier, |  | ||||||
| 		}[] = []; |  | ||||||
|  |  | ||||||
| 		if (profile.fields) { |  | ||||||
| 			for (const field of profile.fields) { |  | ||||||
| 				attachment.push({ |  | ||||||
| 			type: 'PropertyValue', | 			type: 'PropertyValue', | ||||||
| 			name: field.name, | 			name: field.name, | ||||||
| 					value: (field.value != null && field.value.match(/^https?:/)) | 			value: /^https?:/.test(field.value) | ||||||
| 				? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>` | 				? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>` | ||||||
| 				: field.value, | 				: 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( | ||||||
|  | 			url, | ||||||
|  | 			{ | ||||||
| 				headers: { | 				headers: { | ||||||
| 					Accept: 'application/ld+json, application/json', | 					Accept: 'application/ld+json, application/json', | ||||||
| 				}, | 				}, | ||||||
| 				timeout: this.loderTimeout, | 				timeout: this.loderTimeout, | ||||||
| 		}, { throwErrorWhenResponseNotOk: false }).then(res => { | 			}, | ||||||
|  | 			{ throwErrorWhenResponseNotOk: false }, | ||||||
|  | 		).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 && | ||||||
|   | |||||||
| @@ -254,7 +254,7 @@ export default abstract class Chart<T extends Schema> { | |||||||
| 	private convertRawRecord(x: RawRecord<T>): KVs<T> { | 	private convertRawRecord(x: RawRecord<T>): KVs<T> { | ||||||
| 		const kvs = {} as Record<string, number>; | 		const kvs = {} as Record<string, number>; | ||||||
| 		for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns<T>)[]) { | 		for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns<T>)[]) { | ||||||
| 			kvs[(k as string).substr(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number; | 			kvs[(k as string).substring(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number; | ||||||
| 		} | 		} | ||||||
| 		return kvs as KVs<T>; | 		return kvs as KVs<T>; | ||||||
| 	} | 	} | ||||||
| @@ -627,7 +627,7 @@ export default abstract class Chart<T extends Schema> { | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 		// 要求された範囲の最も古い箇所に位置するログが存在しなかったら | 		// 要求された範囲の最も古い箇所に位置するログが存在しなかったら | ||||||
| 		} else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) { | 		} else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) { | ||||||
| 			// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する | 			// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する | ||||||
| 			// (隙間埋めできないため) | 			// (隙間埋めできないため) | ||||||
| 			const outdatedLog = await repository.findOne({ | 			const outdatedLog = await repository.findOne({ | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ export type Acct = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export function parse(acct: string): Acct { | export function parse(acct: string): Acct { | ||||||
| 	if (acct.startsWith('@')) acct = acct.substr(1); | 	if (acct.startsWith('@')) acct = acct.substring(1); | ||||||
| 	const split = acct.split('@', 2); | 	const split = acct.split('@', 2); | ||||||
| 	return { username: split[0], host: split[1] ?? null }; | 	return { username: split[0], host: split[1] ?? null }; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -67,8 +67,9 @@ export function maximum(xs: number[]): number { | |||||||
| export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] { | export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] { | ||||||
| 	const groups = [] as T[][]; | 	const groups = [] as T[][]; | ||||||
| 	for (const x of xs) { | 	for (const x of xs) { | ||||||
| 		if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { | 		const lastGroup = groups.at(-1); | ||||||
| 			groups[groups.length - 1].push(x); | 		if (lastGroup !== undefined && f(lastGroup[0], x)) { | ||||||
|  | 			lastGroup.push(x); | ||||||
| 		} else { | 		} else { | ||||||
| 			groups.push([x]); | 			groups.push([x]); | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { Inject, Injectable } from '@nestjs/common'; | import { Inject, Injectable } from '@nestjs/common'; | ||||||
| import { IsNull, MoreThan, Not } from 'typeorm'; | import { IsNull, MoreThan, Not } from 'typeorm'; | ||||||
| import { DI } from '@/di-symbols.js'; | import { DI } from '@/di-symbols.js'; | ||||||
| import type { DriveFilesRepository } from '@/models/index.js'; | import type { DriveFile, DriveFilesRepository } from '@/models/index.js'; | ||||||
| import type { Config } from '@/config.js'; | import type { Config } from '@/config.js'; | ||||||
| import type Logger from '@/logger.js'; | import type Logger from '@/logger.js'; | ||||||
| import { DriveService } from '@/core/DriveService.js'; | import { DriveService } from '@/core/DriveService.js'; | ||||||
| @@ -31,7 +31,7 @@ export class CleanRemoteFilesProcessorService { | |||||||
| 		this.logger.info('Deleting cached remote files...'); | 		this.logger.info('Deleting cached remote files...'); | ||||||
|  |  | ||||||
| 		let deletedCount = 0; | 		let deletedCount = 0; | ||||||
| 		let cursor: any = null; | 		let cursor: DriveFile['id'] | null = null; | ||||||
|  |  | ||||||
| 		while (true) { | 		while (true) { | ||||||
| 			const files = await this.driveFilesRepository.find({ | 			const files = await this.driveFilesRepository.find({ | ||||||
| @@ -51,7 +51,7 @@ export class CleanRemoteFilesProcessorService { | |||||||
| 				break; | 				break; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			cursor = files[files.length - 1].id; | 			cursor = files.at(-1)?.id ?? null; | ||||||
|  |  | ||||||
| 			await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true))); | 			await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true))); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ export class DeleteAccountProcessorService { | |||||||
| 					break; | 					break; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				cursor = notes[notes.length - 1].id; | 				cursor = notes.at(-1)?.id ?? null; | ||||||
|  |  | ||||||
| 				await this.notesRepository.delete(notes.map(note => note.id)); | 				await this.notesRepository.delete(notes.map(note => note.id)); | ||||||
|  |  | ||||||
| @@ -101,7 +101,7 @@ export class DeleteAccountProcessorService { | |||||||
| 					break; | 					break; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				cursor = files[files.length - 1].id; | 				cursor = files.at(-1)?.id ?? null; | ||||||
|  |  | ||||||
| 				for (const file of files) { | 				for (const file of files) { | ||||||
| 					await this.driveService.deleteFileSync(file); | 					await this.driveService.deleteFileSync(file); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { Inject, Injectable } from '@nestjs/common'; | import { Inject, Injectable } from '@nestjs/common'; | ||||||
| import { MoreThan } from 'typeorm'; | import { MoreThan } from 'typeorm'; | ||||||
| import { DI } from '@/di-symbols.js'; | import { DI } from '@/di-symbols.js'; | ||||||
| import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; | import type { UsersRepository, DriveFilesRepository, DriveFile } from '@/models/index.js'; | ||||||
| import type { Config } from '@/config.js'; | import type { Config } from '@/config.js'; | ||||||
| import type Logger from '@/logger.js'; | import type Logger from '@/logger.js'; | ||||||
| import { DriveService } from '@/core/DriveService.js'; | import { DriveService } from '@/core/DriveService.js'; | ||||||
| @@ -40,7 +40,7 @@ export class DeleteDriveFilesProcessorService { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		let deletedCount = 0; | 		let deletedCount = 0; | ||||||
| 		let cursor: any = null; | 		let cursor: DriveFile['id'] | null = null; | ||||||
|  |  | ||||||
| 		while (true) { | 		while (true) { | ||||||
| 			const files = await this.driveFilesRepository.find({ | 			const files = await this.driveFilesRepository.find({ | ||||||
| @@ -59,7 +59,7 @@ export class DeleteDriveFilesProcessorService { | |||||||
| 				break; | 				break; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			cursor = files[files.length - 1].id; | 			cursor = files.at(-1)?.id ?? null; | ||||||
|  |  | ||||||
| 			for (const file of files) { | 			for (const file of files) { | ||||||
| 				await this.driveService.deleteFileSync(file); | 				await this.driveService.deleteFileSync(file); | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common'; | |||||||
| import { MoreThan } from 'typeorm'; | import { MoreThan } from 'typeorm'; | ||||||
| import { format as dateFormat } from 'date-fns'; | import { format as dateFormat } from 'date-fns'; | ||||||
| import { DI } from '@/di-symbols.js'; | import { DI } from '@/di-symbols.js'; | ||||||
| import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; | import type { UsersRepository, BlockingsRepository, Blocking } from '@/models/index.js'; | ||||||
| import type { Config } from '@/config.js'; | import type { Config } from '@/config.js'; | ||||||
| import type Logger from '@/logger.js'; | import type Logger from '@/logger.js'; | ||||||
| import { DriveService } from '@/core/DriveService.js'; | import { DriveService } from '@/core/DriveService.js'; | ||||||
| @@ -53,7 +53,7 @@ export class ExportBlockingProcessorService { | |||||||
| 			const stream = fs.createWriteStream(path, { flags: 'a' }); | 			const stream = fs.createWriteStream(path, { flags: 'a' }); | ||||||
|  |  | ||||||
| 			let exportedCount = 0; | 			let exportedCount = 0; | ||||||
| 			let cursor: any = null; | 			let cursor: Blocking['id'] | null = null; | ||||||
|  |  | ||||||
| 			while (true) { | 			while (true) { | ||||||
| 				const blockings = await this.blockingsRepository.find({ | 				const blockings = await this.blockingsRepository.find({ | ||||||
| @@ -72,7 +72,7 @@ export class ExportBlockingProcessorService { | |||||||
| 					break; | 					break; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				cursor = blockings[blockings.length - 1].id; | 				cursor = blockings.at(-1)?.id ?? null; | ||||||
|  |  | ||||||
| 				for (const block of blockings) { | 				for (const block of blockings) { | ||||||
| 					const u = await this.usersRepository.findOneBy({ id: block.blockeeId }); | 					const u = await this.usersRepository.findOneBy({ id: block.blockeeId }); | ||||||
|   | |||||||
| @@ -94,7 +94,7 @@ export class ExportFavoritesProcessorService { | |||||||
| 					break; | 					break; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				cursor = favorites[favorites.length - 1].id; | 				cursor = favorites.at(-1)?.id ?? null; | ||||||
|  |  | ||||||
| 				for (const favorite of favorites) { | 				for (const favorite of favorites) { | ||||||
| 					let poll: Poll | undefined; | 					let poll: Poll | undefined; | ||||||
|   | |||||||
| @@ -79,7 +79,7 @@ export class ExportFollowingProcessorService { | |||||||
| 					break; | 					break; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				cursor = followings[followings.length - 1].id; | 				cursor = followings.at(-1)?.id ?? null; | ||||||
|  |  | ||||||
| 				for (const following of followings) { | 				for (const following of followings) { | ||||||
| 					const u = await this.usersRepository.findOneBy({ id: following.followeeId }); | 					const u = await this.usersRepository.findOneBy({ id: following.followeeId }); | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common'; | |||||||
| import { IsNull, MoreThan } from 'typeorm'; | import { IsNull, MoreThan } from 'typeorm'; | ||||||
| import { format as dateFormat } from 'date-fns'; | import { format as dateFormat } from 'date-fns'; | ||||||
| import { DI } from '@/di-symbols.js'; | import { DI } from '@/di-symbols.js'; | ||||||
| import type { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js'; | import type { MutingsRepository, UsersRepository, BlockingsRepository, Muting } from '@/models/index.js'; | ||||||
| import type { Config } from '@/config.js'; | import type { Config } from '@/config.js'; | ||||||
| import type Logger from '@/logger.js'; | import type Logger from '@/logger.js'; | ||||||
| import { DriveService } from '@/core/DriveService.js'; | import { DriveService } from '@/core/DriveService.js'; | ||||||
| @@ -56,7 +56,7 @@ export class ExportMutingProcessorService { | |||||||
| 			const stream = fs.createWriteStream(path, { flags: 'a' }); | 			const stream = fs.createWriteStream(path, { flags: 'a' }); | ||||||
|  |  | ||||||
| 			let exportedCount = 0; | 			let exportedCount = 0; | ||||||
| 			let cursor: any = null; | 			let cursor: Muting['id'] | null = null; | ||||||
|  |  | ||||||
| 			while (true) { | 			while (true) { | ||||||
| 				const mutes = await this.mutingsRepository.find({ | 				const mutes = await this.mutingsRepository.find({ | ||||||
| @@ -76,7 +76,7 @@ export class ExportMutingProcessorService { | |||||||
| 					break; | 					break; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				cursor = mutes[mutes.length - 1].id; | 				cursor = mutes.at(-1)?.id ?? null; | ||||||
|  |  | ||||||
| 				for (const mute of mutes) { | 				for (const mute of mutes) { | ||||||
| 					const u = await this.usersRepository.findOneBy({ id: mute.muteeId }); | 					const u = await this.usersRepository.findOneBy({ id: mute.muteeId }); | ||||||
|   | |||||||
| @@ -90,7 +90,7 @@ export class ExportNotesProcessorService { | |||||||
| 					break; | 					break; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				cursor = notes[notes.length - 1].id; | 				cursor = notes.at(-1)?.id ?? null; | ||||||
|  |  | ||||||
| 				for (const note of notes) { | 				for (const note of notes) { | ||||||
| 					let poll: Poll | undefined; | 					let poll: Poll | undefined; | ||||||
|   | |||||||
| @@ -181,7 +181,7 @@ export class ActivityPubServerService { | |||||||
| 				undefined, | 				undefined, | ||||||
| 				inStock ? `${partOf}?${url.query({ | 				inStock ? `${partOf}?${url.query({ | ||||||
| 					page: 'true', | 					page: 'true', | ||||||
| 					cursor: followings[followings.length - 1].id, | 					cursor: followings.at(-1)!.id, | ||||||
| 				})}` : undefined, | 				})}` : undefined, | ||||||
| 			); | 			); | ||||||
|  |  | ||||||
| @@ -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)); | ||||||
| @@ -269,7 +273,7 @@ export class ActivityPubServerService { | |||||||
| 				undefined, | 				undefined, | ||||||
| 				inStock ? `${partOf}?${url.query({ | 				inStock ? `${partOf}?${url.query({ | ||||||
| 					page: 'true', | 					page: 'true', | ||||||
| 					cursor: followings[followings.length - 1].id, | 					cursor: followings.at(-1)!.id, | ||||||
| 				})}` : undefined, | 				})}` : undefined, | ||||||
| 			); | 			); | ||||||
|  |  | ||||||
| @@ -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'); | ||||||
| @@ -387,7 +398,7 @@ export class ActivityPubServerService { | |||||||
| 				})}` : undefined, | 				})}` : undefined, | ||||||
| 				notes.length ? `${partOf}?${url.query({ | 				notes.length ? `${partOf}?${url.query({ | ||||||
| 					page: 'true', | 					page: 'true', | ||||||
| 					until_id: notes[notes.length - 1].id, | 					until_id: notes.at(-1)!.id, | ||||||
| 				})}` : undefined, | 				})}` : undefined, | ||||||
| 			); | 			); | ||||||
|  |  | ||||||
| @@ -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`, | ||||||
| 			); | 			); | ||||||
|   | |||||||
| @@ -447,12 +447,12 @@ export async function testPaginationConsistency<Entity extends { id: string, cre | |||||||
| 	for (const limit of [1, 5, 10, 100, undefined]) { | 	for (const limit of [1, 5, 10, 100, undefined]) { | ||||||
| 		// 1. sinceId/DateとuntilId/Dateで両端を指定して取得した結果が期待通りになっていること | 		// 1. sinceId/DateとuntilId/Dateで両端を指定して取得した結果が期待通りになっていること | ||||||
| 		if (ordering === 'desc') { | 		if (ordering === 'desc') { | ||||||
| 			const end = expected[expected.length - 1]; | 			const end = expected.at(-1)!; | ||||||
| 			let last = await fetchEntities(rangeToParam({ limit, since: end })); | 			let last = await fetchEntities(rangeToParam({ limit, since: end })); | ||||||
| 			const actual: Entity[] = []; | 			const actual: Entity[] = []; | ||||||
| 			while (last.length !== 0) { | 			while (last.length !== 0) { | ||||||
| 				actual.push(...last); | 				actual.push(...last); | ||||||
| 				last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1], since: end })); | 				last = await fetchEntities(rangeToParam({ limit, until: last.at(-1), since: end })); | ||||||
| 			} | 			} | ||||||
| 			actual.push(end); | 			actual.push(end); | ||||||
| 			assert.deepStrictEqual( | 			assert.deepStrictEqual( | ||||||
| @@ -467,7 +467,7 @@ export async function testPaginationConsistency<Entity extends { id: string, cre | |||||||
| 			const actual: Entity[] = []; | 			const actual: Entity[] = []; | ||||||
| 			while (last.length !== 0) { | 			while (last.length !== 0) { | ||||||
| 				actual.push(...last); | 				actual.push(...last); | ||||||
| 				last = await fetchEntities(rangeToParam({ limit, since: last[last.length - 1] })); | 				last = await fetchEntities(rangeToParam({ limit, since: last.at(-1) })); | ||||||
| 			} | 			} | ||||||
| 			assert.deepStrictEqual( | 			assert.deepStrictEqual( | ||||||
| 				actual.map(({ id, createdAt }) => id + ':' + createdAt), | 				actual.map(({ id, createdAt }) => id + ':' + createdAt), | ||||||
| @@ -480,7 +480,7 @@ export async function testPaginationConsistency<Entity extends { id: string, cre | |||||||
| 			const actual: Entity[] = []; | 			const actual: Entity[] = []; | ||||||
| 			while (last.length !== 0) { | 			while (last.length !== 0) { | ||||||
| 				actual.push(...last); | 				actual.push(...last); | ||||||
| 				last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1] })); | 				last = await fetchEntities(rangeToParam({ limit, until: last.at(-1) })); | ||||||
| 			} | 			} | ||||||
| 			assert.deepStrictEqual( | 			assert.deepStrictEqual( | ||||||
| 				actual.map(({ id, createdAt }) => id + ':' + createdAt), | 				actual.map(({ id, createdAt }) => id + ':' + createdAt), | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ | |||||||
| 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> | 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> | ||||||
| 		</template> | 		</template> | ||||||
| 	</MkFolder> | 	</MkFolder> | ||||||
| 	<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }"> | 	<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align ?? null, backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }"> | ||||||
| 		<template v-for="child in c.children" :key="child"> | 		<template v-for="child in c.children" :key="child"> | ||||||
| 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/> | 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/> | ||||||
| 		</template> | 		</template> | ||||||
| @@ -102,10 +102,6 @@ function openPostForm() { | |||||||
| 	gap: 12px; | 	gap: 12px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .containerCenter { |  | ||||||
| 	text-align: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .fontSerif { | .fontSerif { | ||||||
| 	font-family: serif; | 	font-family: serif; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -25,8 +25,8 @@ | |||||||
| 		<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/> | 		<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/> | ||||||
|  |  | ||||||
| 		<p :class="$style.name"> | 		<p :class="$style.name"> | ||||||
| 			<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> | 			<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substring(0, file.name.lastIndexOf('.')) : file.name }}</span> | ||||||
| 			<span v-if="file.name.lastIndexOf('.') != -1" style="opacity: 0.5;">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> | 			<span v-if="file.name.lastIndexOf('.') != -1" style="opacity: 0.5;">{{ file.name.substring(file.name.lastIndexOf('.')) }}</span> | ||||||
| 		</p> | 		</p> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -568,7 +568,7 @@ function fetchMoreFolders() { | |||||||
| 	os.api('drive/folders', { | 	os.api('drive/folders', { | ||||||
| 		folderId: folder.value ? folder.value.id : null, | 		folderId: folder.value ? folder.value.id : null, | ||||||
| 		type: props.type, | 		type: props.type, | ||||||
| 		untilId: folders.value[folders.value.length - 1].id, | 		untilId: folders.value.at(-1)?.id, | ||||||
| 		limit: max + 1, | 		limit: max + 1, | ||||||
| 	}).then(folders => { | 	}).then(folders => { | ||||||
| 		if (folders.length === max + 1) { | 		if (folders.length === max + 1) { | ||||||
| @@ -591,7 +591,7 @@ function fetchMoreFiles() { | |||||||
| 	os.api('drive/files', { | 	os.api('drive/files', { | ||||||
| 		folderId: folder.value ? folder.value.id : null, | 		folderId: folder.value ? folder.value.id : null, | ||||||
| 		type: props.type, | 		type: props.type, | ||||||
| 		untilId: files.value[files.value.length - 1].id, | 		untilId: files.value.at(-1)?.id, | ||||||
| 		limit: max + 1, | 		limit: max + 1, | ||||||
| 	}).then(files => { | 	}).then(files => { | ||||||
| 		if (files.length === max + 1) { | 		if (files.length === max + 1) { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <component | <component | ||||||
| 	:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target" | 	:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel" :target="target" | ||||||
| 	:title="url" | 	:title="url" | ||||||
| > | > | ||||||
| 	<slot></slot> | 	<slot></slot> | ||||||
|   | |||||||
| @@ -59,8 +59,8 @@ function draw(): void { | |||||||
|  |  | ||||||
| 	polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`; | 	polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`; | ||||||
|  |  | ||||||
| 	headX = _polylinePoints[_polylinePoints.length - 1][0]; | 	headX = _polylinePoints.at(-1)![0]; | ||||||
| 	headY = _polylinePoints[_polylinePoints.length - 1][1]; | 	headY = _polylinePoints.at(-1)![1]; | ||||||
| } | } | ||||||
|  |  | ||||||
| watch(() => props.src, draw, { immediate: true }); | watch(() => props.src, draw, { immediate: true }); | ||||||
|   | |||||||
| @@ -120,7 +120,7 @@ const contextmenu = $computed(() => ([{ | |||||||
|  |  | ||||||
| function back() { | function back() { | ||||||
| 	history.pop(); | 	history.pop(); | ||||||
| 	router.replace(history[history.length - 1].path, history[history.length - 1].key); | 	router.replace(history.at(-1)!.path, history.at(-1)!.key); | ||||||
| } | } | ||||||
|  |  | ||||||
| function reload() { | function reload() { | ||||||
|   | |||||||
| @@ -564,7 +564,7 @@ async function onPaste(ev: ClipboardEvent) { | |||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; | 			quoteId = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ | |||||||
| 	:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.large]: defaultStore.state.largeNoteReactions }]" | 	:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.large]: defaultStore.state.largeNoteReactions }]" | ||||||
| 	@click="toggleReaction()" | 	@click="toggleReaction()" | ||||||
| > | > | ||||||
| 	<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substr(1, reaction.length - 2)]"/> | 	<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> | ||||||
| 	<span :class="$style.count">{{ count }}</span> | 	<span :class="$style.count">{{ count }}</span> | ||||||
| </button> | </button> | ||||||
| </template> | </template> | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ | |||||||
| 	</div> | 	</div> | ||||||
| </template> | </template> | ||||||
| <div v-else> | <div v-else> | ||||||
| 	<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> | 	<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> | ||||||
| 		<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`"> | 		<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`"> | ||||||
| 		</div> | 		</div> | ||||||
| 		<article :class="$style.body"> | 		<article :class="$style.body"> | ||||||
|   | |||||||
| @@ -88,7 +88,7 @@ onMounted(() => { | |||||||
| 		user = props.q; | 		user = props.q; | ||||||
| 	} else { | 	} else { | ||||||
| 		const query = props.q.startsWith('@') ? | 		const query = props.q.startsWith('@') ? | ||||||
| 			Acct.parse(props.q.substr(1)) : | 			Acct.parse(props.q.substring(1)) : | ||||||
| 			{ userId: props.q }; | 			{ userId: props.q }; | ||||||
|  |  | ||||||
| 		os.api('users/show', query).then(res => { | 		os.api('users/show', query).then(res => { | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ const props = defineProps<{ | |||||||
| 	useOriginalSize?: boolean; | 	useOriginalSize?: boolean; | ||||||
| }>(); | }>(); | ||||||
|  |  | ||||||
| const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substr(1, props.name.length - 2) : props.name).replace('@.', '')); | const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', '')); | ||||||
| const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@'))); | const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@'))); | ||||||
|  |  | ||||||
| const rawUrl = computed(() => { | const rawUrl = computed(() => { | ||||||
|   | |||||||
| @@ -11,13 +11,13 @@ export default function(props: { src: string; tag?: string; textTag?: string; }, | |||||||
| 			parsed.push(str); | 			parsed.push(str); | ||||||
| 			break; | 			break; | ||||||
| 		} else { | 		} else { | ||||||
| 			if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen)); | 			if (nextBracketOpen > 0) parsed.push(str.substring(0, nextBracketOpen)); | ||||||
| 			parsed.push({ | 			parsed.push({ | ||||||
| 				arg: str.substring(nextBracketOpen + 1, nextBracketClose), | 				arg: str.substring(nextBracketOpen + 1, nextBracketClose), | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		str = str.substr(nextBracketClose + 1); | 		str = str.substring(nextBracketClose + 1); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]())); | 	return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]())); | ||||||
|   | |||||||
| @@ -88,10 +88,13 @@ | |||||||
| 					<template #label>Special thanks</template> | 					<template #label>Special thanks</template> | ||||||
| 					<div class="_gaps" style="text-align: center;"> | 					<div class="_gaps" style="text-align: center;"> | ||||||
| 						<div> | 						<div> | ||||||
| 							<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a> | 							<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a> | ||||||
| 						</div> | 						</div> | ||||||
| 						<div> | 						<div> | ||||||
| 							<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a> | 							<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a> | ||||||
|  | 						</div> | ||||||
|  | 						<div> | ||||||
|  | 							<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="100" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a> | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 				</FormSection> | 				</FormSection> | ||||||
| @@ -161,6 +164,9 @@ const patronsWithIcon = [{ | |||||||
| }, { | }, { | ||||||
| 	name: '清遊あみ', | 	name: '清遊あみ', | ||||||
| 	icon: 'https://misskey-hub.net/patrons/de25195b88e940a388388bea2e7637d8.jpg', | 	icon: 'https://misskey-hub.net/patrons/de25195b88e940a388388bea2e7637d8.jpg', | ||||||
|  | }, { | ||||||
|  | 	name: 'Nagi8410', | ||||||
|  | 	icon: 'https://misskey-hub.net/patrons/31b102ab4fc540ed806b0461575d38be.jpg', | ||||||
| }]; | }]; | ||||||
|  |  | ||||||
| const patrons = [ | const patrons = [ | ||||||
| @@ -256,6 +262,7 @@ const patrons = [ | |||||||
| 	'binvinyl', | 	'binvinyl', | ||||||
| 	'渡志郎', | 	'渡志郎', | ||||||
| 	'ぷーざ', | 	'ぷーざ', | ||||||
|  | 	'越貝鯛丸', | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure')); | let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure')); | ||||||
|   | |||||||
| @@ -85,7 +85,7 @@ onMounted(() => { | |||||||
| 	connection.on('stats', onStats); | 	connection.on('stats', onStats); | ||||||
| 	connection.on('statsLog', onStatsLog); | 	connection.on('statsLog', onStatsLog); | ||||||
| 	connection.send('requestLog', { | 	connection.send('requestLog', { | ||||||
| 		id: Math.random().toString().substr(2, 8), | 		id: Math.random().toString().substring(2, 10), | ||||||
| 		length: 100, | 		length: 100, | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -156,7 +156,7 @@ onMounted(async () => { | |||||||
|  |  | ||||||
| 	nextTick(() => { | 	nextTick(() => { | ||||||
| 		queueStatsConnection.send('requestLog', { | 		queueStatsConnection.send('requestLog', { | ||||||
| 			id: Math.random().toString().substr(2, 8), | 			id: Math.random().toString().substring(2, 10), | ||||||
| 			length: 100, | 			length: 100, | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
|   | |||||||
| @@ -106,7 +106,7 @@ onMounted(() => { | |||||||
| 	connection.on('stats', onStats); | 	connection.on('stats', onStats); | ||||||
| 	connection.on('statsLog', onStatsLog); | 	connection.on('statsLog', onStatsLog); | ||||||
| 	connection.send('requestLog', { | 	connection.send('requestLog', { | ||||||
| 		id: Math.random().toString().substr(2, 8), | 		id: Math.random().toString().substring(2, 10), | ||||||
| 		length: 200, | 		length: 200, | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ export function createAiScriptEnv(opts) { | |||||||
| 		USER_NAME: $i ? values.STR($i.name) : values.NULL, | 		USER_NAME: $i ? values.STR($i.name) : values.NULL, | ||||||
| 		USER_USERNAME: $i ? values.STR($i.username) : values.NULL, | 		USER_USERNAME: $i ? values.STR($i.username) : values.NULL, | ||||||
| 		CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value), | 		CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value), | ||||||
|  | 		CURRENT_URL: values.STR(window.location.href), | ||||||
| 		'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { | 		'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { | ||||||
| 			await os.alert({ | 			await os.alert({ | ||||||
| 				type: type ? type.value : 'info', | 				type: type ? type.value : 'info', | ||||||
|   | |||||||
| @@ -78,8 +78,9 @@ export function maximum(xs: number[]): number { | |||||||
| export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] { | export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] { | ||||||
| 	const groups = [] as T[][]; | 	const groups = [] as T[][]; | ||||||
| 	for (const x of xs) { | 	for (const x of xs) { | ||||||
| 		if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { | 		const lastGroup = groups.at(-1); | ||||||
| 			groups[groups.length - 1].push(x); | 		if (lastGroup !== undefined && f(lastGroup[0], x)) { | ||||||
|  | 			lastGroup.push(x); | ||||||
| 		} else { | 		} else { | ||||||
| 			groups.push([x]); | 			groups.push([x]); | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -65,7 +65,7 @@ export class Autocomplete { | |||||||
| 	 */ | 	 */ | ||||||
| 	private onInput() { | 	private onInput() { | ||||||
| 		const caretPos = this.textarea.selectionStart; | 		const caretPos = this.textarea.selectionStart; | ||||||
| 		const text = this.text.substr(0, caretPos).split('\n').pop()!; | 		const text = this.text.substring(0, caretPos).split('\n').pop()!; | ||||||
|  |  | ||||||
| 		const mentionIndex = text.lastIndexOf('@'); | 		const mentionIndex = text.lastIndexOf('@'); | ||||||
| 		const hashtagIndex = text.lastIndexOf('#'); | 		const hashtagIndex = text.lastIndexOf('#'); | ||||||
| @@ -91,7 +91,7 @@ export class Autocomplete { | |||||||
| 		let opened = false; | 		let opened = false; | ||||||
|  |  | ||||||
| 		if (isMention) { | 		if (isMention) { | ||||||
| 			const username = text.substr(mentionIndex + 1); | 			const username = text.substring(mentionIndex + 1); | ||||||
| 			if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) { | 			if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) { | ||||||
| 				this.open('user', username); | 				this.open('user', username); | ||||||
| 				opened = true; | 				opened = true; | ||||||
| @@ -102,7 +102,7 @@ export class Autocomplete { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if (isHashtag && !opened) { | 		if (isHashtag && !opened) { | ||||||
| 			const hashtag = text.substr(hashtagIndex + 1); | 			const hashtag = text.substring(hashtagIndex + 1); | ||||||
| 			if (!hashtag.includes(' ')) { | 			if (!hashtag.includes(' ')) { | ||||||
| 				this.open('hashtag', hashtag); | 				this.open('hashtag', hashtag); | ||||||
| 				opened = true; | 				opened = true; | ||||||
| @@ -110,7 +110,7 @@ export class Autocomplete { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if (isEmoji && !opened) { | 		if (isEmoji && !opened) { | ||||||
| 			const emoji = text.substr(emojiIndex + 1); | 			const emoji = text.substring(emojiIndex + 1); | ||||||
| 			if (!emoji.includes(' ')) { | 			if (!emoji.includes(' ')) { | ||||||
| 				this.open('emoji', emoji); | 				this.open('emoji', emoji); | ||||||
| 				opened = true; | 				opened = true; | ||||||
| @@ -118,7 +118,7 @@ export class Autocomplete { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if (isMfmTag && !opened) { | 		if (isMfmTag && !opened) { | ||||||
| 			const mfmTag = text.substr(mfmTagIndex + 1); | 			const mfmTag = text.substring(mfmTagIndex + 1); | ||||||
| 			if (!mfmTag.includes(' ')) { | 			if (!mfmTag.includes(' ')) { | ||||||
| 				this.open('mfmTag', mfmTag.replace('[', '')); | 				this.open('mfmTag', mfmTag.replace('[', '')); | ||||||
| 				opened = true; | 				opened = true; | ||||||
| @@ -208,9 +208,9 @@ export class Autocomplete { | |||||||
| 		if (type === 'user') { | 		if (type === 'user') { | ||||||
| 			const source = this.text; | 			const source = this.text; | ||||||
|  |  | ||||||
| 			const before = source.substr(0, caret); | 			const before = source.substring(0, caret); | ||||||
| 			const trimmedBefore = before.substring(0, before.lastIndexOf('@')); | 			const trimmedBefore = before.substring(0, before.lastIndexOf('@')); | ||||||
| 			const after = source.substr(caret); | 			const after = source.substring(caret); | ||||||
|  |  | ||||||
| 			const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`; | 			const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`; | ||||||
|  |  | ||||||
| @@ -226,9 +226,9 @@ export class Autocomplete { | |||||||
| 		} else if (type === 'hashtag') { | 		} else if (type === 'hashtag') { | ||||||
| 			const source = this.text; | 			const source = this.text; | ||||||
|  |  | ||||||
| 			const before = source.substr(0, caret); | 			const before = source.substring(0, caret); | ||||||
| 			const trimmedBefore = before.substring(0, before.lastIndexOf('#')); | 			const trimmedBefore = before.substring(0, before.lastIndexOf('#')); | ||||||
| 			const after = source.substr(caret); | 			const after = source.substring(caret); | ||||||
|  |  | ||||||
| 			// 挿入 | 			// 挿入 | ||||||
| 			this.text = `${trimmedBefore}#${value} ${after}`; | 			this.text = `${trimmedBefore}#${value} ${after}`; | ||||||
| @@ -242,9 +242,9 @@ export class Autocomplete { | |||||||
| 		} else if (type === 'emoji') { | 		} else if (type === 'emoji') { | ||||||
| 			const source = this.text; | 			const source = this.text; | ||||||
|  |  | ||||||
| 			const before = source.substr(0, caret); | 			const before = source.substring(0, caret); | ||||||
| 			const trimmedBefore = before.substring(0, before.lastIndexOf(':')); | 			const trimmedBefore = before.substring(0, before.lastIndexOf(':')); | ||||||
| 			const after = source.substr(caret); | 			const after = source.substring(caret); | ||||||
|  |  | ||||||
| 			// 挿入 | 			// 挿入 | ||||||
| 			this.text = trimmedBefore + value + after; | 			this.text = trimmedBefore + value + after; | ||||||
| @@ -258,9 +258,9 @@ export class Autocomplete { | |||||||
| 		} else if (type === 'mfmTag') { | 		} else if (type === 'mfmTag') { | ||||||
| 			const source = this.text; | 			const source = this.text; | ||||||
|  |  | ||||||
| 			const before = source.substr(0, caret); | 			const before = source.substring(0, caret); | ||||||
| 			const trimmedBefore = before.substring(0, before.lastIndexOf('$')); | 			const trimmedBefore = before.substring(0, before.lastIndexOf('$')); | ||||||
| 			const after = source.substr(caret); | 			const after = source.substring(caret); | ||||||
|  |  | ||||||
| 			// 挿入 | 			// 挿入 | ||||||
| 			this.text = `${trimmedBefore}$[${value} ]${after}`; | 			this.text = `${trimmedBefore}$[${value} ]${after}`; | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ export async function genSearchQuery(v: any, q: string) { | |||||||
| 	let host: string; | 	let host: string; | ||||||
| 	let userId: string; | 	let userId: string; | ||||||
| 	if (q.split(' ').some(x => x.startsWith('@'))) { | 	if (q.split(' ').some(x => x.startsWith('@'))) { | ||||||
| 		for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) { | 		for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substring(1))) { | ||||||
| 			if (at.includes('.')) { | 			if (at.includes('.')) { | ||||||
| 				if (at === localHost || at === '.') { | 				if (at === localHost || at === '.') { | ||||||
| 					host = null; | 					host = null; | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ export async function lookup(router?: Router) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (query.startsWith('#')) { | 	if (query.startsWith('#')) { | ||||||
| 		_router.push(`/tags/${encodeURIComponent(query.substr(1))}`); | 		_router.push(`/tags/${encodeURIComponent(query.substring(1))}`); | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ export const fromThemeString = (str?: string) : ThemeValue => { | |||||||
| 	} else if (str.startsWith('"')) { | 	} else if (str.startsWith('"')) { | ||||||
| 		return { | 		return { | ||||||
| 			type: 'css', | 			type: 'css', | ||||||
| 			value: str.substr(1).trim(), | 			value: str.substring(1).trim(), | ||||||
| 		}; | 		}; | ||||||
| 	} else { | 	} else { | ||||||
| 		return str; | 		return str; | ||||||
|   | |||||||
| @@ -98,7 +98,7 @@ function compile(theme: Theme): Record<string, string> { | |||||||
| 	function getColor(val: string): tinycolor.Instance { | 	function getColor(val: string): tinycolor.Instance { | ||||||
| 		// ref (prop) | 		// ref (prop) | ||||||
| 		if (val[0] === '@') { | 		if (val[0] === '@') { | ||||||
| 			return getColor(theme.props[val.substr(1)]); | 			return getColor(theme.props[val.substring(1)]); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// ref (const) | 		// ref (const) | ||||||
| @@ -109,7 +109,7 @@ function compile(theme: Theme): Record<string, string> { | |||||||
| 		// func | 		// func | ||||||
| 		else if (val[0] === ':') { | 		else if (val[0] === ':') { | ||||||
| 			const parts = val.split('<'); | 			const parts = val.split('<'); | ||||||
| 			const func = parts.shift().substr(1); | 			const func = parts.shift().substring(1); | ||||||
| 			const arg = parseFloat(parts.shift()); | 			const arg = parseFloat(parts.shift()); | ||||||
| 			const color = getColor(parts.join('<')); | 			const color = getColor(parts.join('<')); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -124,7 +124,7 @@ connection.on('stats', onStats); | |||||||
| connection.on('statsLog', onStatsLog); | connection.on('statsLog', onStatsLog); | ||||||
|  |  | ||||||
| connection.send('requestLog', { | connection.send('requestLog', { | ||||||
| 	id: Math.random().toString().substr(2, 8), | 	id: Math.random().toString().substring(2, 10), | ||||||
| 	length: 1, | 	length: 1, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -100,7 +100,7 @@ onMounted(() => { | |||||||
| 	props.connection.on('stats', onStats); | 	props.connection.on('stats', onStats); | ||||||
| 	props.connection.on('statsLog', onStatsLog); | 	props.connection.on('statsLog', onStatsLog); | ||||||
| 	props.connection.send('requestLog', { | 	props.connection.send('requestLog', { | ||||||
| 		id: Math.random().toString().substr(2, 8), | 		id: Math.random().toString().substring(2, 10), | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| @@ -121,10 +121,10 @@ function onStats(connStats) { | |||||||
| 	cpuPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${cpuPolylinePoints} ${viewBoxX},${viewBoxY}`; | 	cpuPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${cpuPolylinePoints} ${viewBoxX},${viewBoxY}`; | ||||||
| 	memPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${memPolylinePoints} ${viewBoxX},${viewBoxY}`; | 	memPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${memPolylinePoints} ${viewBoxX},${viewBoxY}`; | ||||||
|  |  | ||||||
| 	cpuHeadX = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][0]; | 	cpuHeadX = cpuPolylinePointsStats.at(-1)![0]; | ||||||
| 	cpuHeadY = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][1]; | 	cpuHeadY = cpuPolylinePointsStats.at(-1)![1]; | ||||||
| 	memHeadX = memPolylinePointsStats[memPolylinePointsStats.length - 1][0]; | 	memHeadX = memPolylinePointsStats.at(-1)![0]; | ||||||
| 	memHeadY = memPolylinePointsStats[memPolylinePointsStats.length - 1][1]; | 	memHeadY = memPolylinePointsStats.at(-1)![1]; | ||||||
|  |  | ||||||
| 	cpuP = (connStats.cpu * 100).toFixed(0); | 	cpuP = (connStats.cpu * 100).toFixed(0); | ||||||
| 	memP = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0); | 	memP = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0); | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ onMounted(() => { | |||||||
| 	props.connection.on('stats', onStats); | 	props.connection.on('stats', onStats); | ||||||
| 	props.connection.on('statsLog', onStatsLog); | 	props.connection.on('statsLog', onStatsLog); | ||||||
| 	props.connection.send('requestLog', { | 	props.connection.send('requestLog', { | ||||||
| 		id: Math.random().toString().substr(2, 8), | 		id: Math.random().toString().substring(2, 10), | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| @@ -94,10 +94,10 @@ function onStats(connStats) { | |||||||
| 	inPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${inPolylinePoints} ${viewBoxX},${viewBoxY}`; | 	inPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${inPolylinePoints} ${viewBoxX},${viewBoxY}`; | ||||||
| 	outPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${outPolylinePoints} ${viewBoxX},${viewBoxY}`; | 	outPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${outPolylinePoints} ${viewBoxX},${viewBoxY}`; | ||||||
|  |  | ||||||
| 	inHeadX = inPolylinePointsStats[inPolylinePointsStats.length - 1][0]; | 	inHeadX = inPolylinePointsStats.at(-1)![0]; | ||||||
| 	inHeadY = inPolylinePointsStats[inPolylinePointsStats.length - 1][1]; | 	inHeadY = inPolylinePointsStats.at(-1)![1]; | ||||||
| 	outHeadX = outPolylinePointsStats[outPolylinePointsStats.length - 1][0]; | 	outHeadX = outPolylinePointsStats.at(-1)![0]; | ||||||
| 	outHeadY = outPolylinePointsStats[outPolylinePointsStats.length - 1][1]; | 	outHeadY = outPolylinePointsStats.at(-1)![1]; | ||||||
|  |  | ||||||
| 	inRecent = connStats.net.rx; | 	inRecent = connStats.net.rx; | ||||||
| 	outRecent = connStats.net.tx; | 	outRecent = connStats.net.tx; | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ export type Acct = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export function parse(acct: string): Acct { | export function parse(acct: string): Acct { | ||||||
| 	if (acct.startsWith('@')) acct = acct.substr(1); | 	if (acct.startsWith('@')) acct = acct.substring(1); | ||||||
| 	const split = acct.split('@', 2); | 	const split = acct.split('@', 2); | ||||||
| 	return { username: split[0], host: split[1] || null }; | 	return { username: split[0], host: split[1] || null }; | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 tamaina
					tamaina