Merge branch 'develop' into pag-back

This commit is contained in:
tamaina 2023-07-14 07:21:05 +00:00
commit 2a434c63df
54 changed files with 352 additions and 341 deletions

View File

@ -27,6 +27,7 @@
- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
- 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように - 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
- プロフィールURLをコピーできるボタンを追加 #11190 - プロフィールURLをコピーできるボタンを追加 #11190
- `CURRENT_URL`で現在表示中のURLを取得できるように(AiScript)
- ユーザーのContextMenuに「アンテナに追加」ボタンを追加 - ユーザーのContextMenuに「アンテナに追加」ボタンを追加
- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように - フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように - 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように

View File

@ -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.');

View File

@ -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`)
);
} }
} }

View File

@ -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,

View File

@ -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);
}
}

View File

@ -21,10 +21,10 @@ import { CacheService } from '@/core/CacheService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { RemoteUser } from '@/models/entities/User.js'; import type { RemoteUser } from '@/models/entities/User.js';
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js'; import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js'; import { ApLoggerService } from './ApLoggerService.js';
import { ApDbResolverService } from './ApDbResolverService.js'; import { ApDbResolverService } from './ApDbResolverService.js';
@ -86,7 +86,7 @@ export class ApInboxService {
} }
@bindThis @bindThis
public async performActivity(actor: RemoteUser, activity: IObject) { public async performActivity(actor: RemoteUser, activity: IObject): Promise<void> {
if (isCollectionOrOrderedCollection(activity)) { if (isCollectionOrOrderedCollection(activity)) {
const resolver = this.apResolverService.createResolver(); const resolver = this.apResolverService.createResolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
@ -107,7 +107,7 @@ export class ApInboxService {
if (actor.uri) { if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
setImmediate(() => { setImmediate(() => {
this.apPersonService.updatePerson(actor.uri!); this.apPersonService.updatePerson(actor.uri);
}); });
} }
} }
@ -229,7 +229,7 @@ export class ApInboxService {
@bindThis @bindThis
private async add(actor: RemoteUser, activity: IAdd): Promise<void> { private async add(actor: RemoteUser, activity: IAdd): Promise<void> {
if ('actor' in activity && actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }
@ -273,7 +273,7 @@ export class ApInboxService {
const unlock = await this.appLockService.getApLock(uri); const unlock = await this.appLockService.getApLock(uri);
try { try {
// 既に同じURIを持つものが登録されていないかチェック // 既に同じURIを持つものが登録されていないかチェック
const exist = await this.apNoteService.fetchNote(uri); const exist = await this.apNoteService.fetchNote(uri);
if (exist) { if (exist) {
return; return;
@ -292,7 +292,7 @@ export class ApInboxService {
return; return;
} }
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode ?? err}`); this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
} }
throw err; throw err;
} }
@ -409,7 +409,7 @@ export class ApInboxService {
@bindThis @bindThis
private async delete(actor: RemoteUser, activity: IDelete): Promise<string> { private async delete(actor: RemoteUser, activity: IDelete): Promise<string> {
if ('actor' in activity && actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }
@ -420,7 +420,7 @@ export class ApInboxService {
// typeが不明だけど、どうせ消えてるのでremote resolveしない // typeが不明だけど、どうせ消えてるのでremote resolveしない
formerType = undefined; formerType = undefined;
} else { } else {
const object = activity.object as IObject; const object = activity.object;
if (isTombstone(object)) { if (isTombstone(object)) {
formerType = toSingle(object.formerType); formerType = toSingle(object.formerType);
} else { } else {
@ -503,7 +503,10 @@ export class ApInboxService {
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
const uris = getApIds(activity.object); const uris = getApIds(activity.object);
const userIds = uris.filter(uri => uri.startsWith(this.config.url + '/users/')).map(uri => uri.split('/').pop()!); const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1))
.filter((userId): userId is string => userId !== undefined);
const users = await this.usersRepository.findBy({ const users = await this.usersRepository.findBy({
id: In(userIds), id: In(userIds),
}); });
@ -566,7 +569,7 @@ export class ApInboxService {
@bindThis @bindThis
private async remove(actor: RemoteUser, activity: IRemove): Promise<void> { private async remove(actor: RemoteUser, activity: IRemove): Promise<void> {
if ('actor' in activity && actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }
@ -586,7 +589,7 @@ export class ApInboxService {
@bindThis @bindThis
private async undo(actor: RemoteUser, activity: IUndo): Promise<string> { private async undo(actor: RemoteUser, activity: IUndo): Promise<string> {
if ('actor' in activity && actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }
@ -719,7 +722,7 @@ export class ApInboxService {
@bindThis @bindThis
private async update(actor: RemoteUser, activity: IUpdate): Promise<string> { private async update(actor: RemoteUser, activity: IUpdate): Promise<string> {
if ('actor' in activity && actor.uri !== activity.actor) { if (actor.uri !== activity.actor) {
return 'skip: invalid actor'; return 'skip: invalid actor';
} }
@ -733,7 +736,7 @@ export class ApInboxService {
}); });
if (isActor(object)) { if (isActor(object)) {
await this.apPersonService.updatePerson(actor.uri!, resolver, object); await this.apPersonService.updatePerson(actor.uri, resolver, object);
return 'ok: Person updated'; return 'ok: Person updated';
} else if (getApType(object) === 'Question') { } else if (getApType(object) === 'Question') {
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err)); await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));

View File

@ -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));
} }

View File

@ -1,6 +1,6 @@
import { createPublicKey } from 'node:crypto'; import { createPublicKey } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In, IsNull } from 'typeorm'; import { In } from 'typeorm';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -26,7 +26,6 @@ import { isNotNull } from '@/misc/is-not-null.js';
import { LdSignatureService } from './LdSignatureService.js'; import { LdSignatureService } from './LdSignatureService.js';
import { ApMfmService } from './ApMfmService.js'; import { ApMfmService } from './ApMfmService.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
import type { IIdentifier } from './models/identifier.js';
@Injectable() @Injectable()
export class ApRendererService { export class ApRendererService {
@ -63,7 +62,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept { public renderAccept(object: string | IObject, user: { id: User['id']; host: null }): IAccept {
return { return {
type: 'Accept', type: 'Accept',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -72,7 +71,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderAdd(user: LocalUser, target: any, object: any): IAdd { public renderAdd(user: LocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
return { return {
type: 'Add', type: 'Add',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -82,7 +81,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderAnnounce(object: any, note: Note): IAnnounce { public renderAnnounce(object: string | IObject, note: Note): IAnnounce {
const attributedTo = this.userEntityService.genLocalUserUri(note.userId); const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
let to: string[] = []; let to: string[] = [];
@ -133,13 +132,13 @@ export class ApRendererService {
@bindThis @bindThis
public renderCreate(object: IObject, note: Note): ICreate { public renderCreate(object: IObject, note: Note): ICreate {
const activity = { const activity: ICreate = {
id: `${this.config.url}/notes/${note.id}/activity`, id: `${this.config.url}/notes/${note.id}/activity`,
actor: this.userEntityService.genLocalUserUri(note.userId), actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Create', type: 'Create',
published: note.createdAt.toISOString(), published: note.createdAt.toISOString(),
object, object,
} as ICreate; };
if (object.to) activity.to = object.to; if (object.to) activity.to = object.to;
if (object.cc) activity.cc = object.cc; if (object.cc) activity.cc = object.cc;
@ -209,7 +208,7 @@ export class ApRendererService {
* @param id Follower|Followee ID * @param id Follower|Followee ID
*/ */
@bindThis @bindThis
public async renderFollowUser(id: User['id']) { public async renderFollowUser(id: User['id']): Promise<string> {
const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser; const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
return this.userEntityService.getUserUri(user); return this.userEntityService.getUserUri(user);
} }
@ -223,8 +222,8 @@ export class ApRendererService {
return { return {
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`, id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
type: 'Follow', type: 'Follow',
actor: this.userEntityService.getUserUri(follower)!, actor: this.userEntityService.getUserUri(follower),
object: this.userEntityService.getUserUri(followee)!, object: this.userEntityService.getUserUri(followee),
}; };
} }
@ -264,14 +263,14 @@ export class ApRendererService {
public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> { public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> {
const reaction = noteReaction.reaction; const reaction = noteReaction.reaction;
const object = { const object: ILike = {
type: 'Like', type: 'Like',
id: `${this.config.url}/likes/${noteReaction.id}`, id: `${this.config.url}/likes/${noteReaction.id}`,
actor: `${this.config.url}/users/${noteReaction.userId}`, actor: `${this.config.url}/users/${noteReaction.userId}`,
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`, object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
content: reaction, content: reaction,
_misskey_reaction: reaction, _misskey_reaction: reaction,
} as ILike; };
if (reaction.startsWith(':')) { if (reaction.startsWith(':')) {
const name = reaction.replaceAll(':', ''); const name = reaction.replaceAll(':', '');
@ -287,7 +286,7 @@ export class ApRendererService {
public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention { public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
return { return {
type: 'Mention', type: 'Mention',
href: this.userEntityService.getUserUri(mention)!, href: this.userEntityService.getUserUri(mention),
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`, name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
}; };
} }
@ -297,8 +296,8 @@ export class ApRendererService {
src: PartialLocalUser | PartialRemoteUser, src: PartialLocalUser | PartialRemoteUser,
dst: PartialLocalUser | PartialRemoteUser, dst: PartialLocalUser | PartialRemoteUser,
): IMove { ): IMove {
const actor = this.userEntityService.getUserUri(src)!; const actor = this.userEntityService.getUserUri(src);
const target = this.userEntityService.getUserUri(dst)!; const target = this.userEntityService.getUserUri(dst);
return { return {
id: `${this.config.url}/moves/${src.id}/${dst.id}`, id: `${this.config.url}/moves/${src.id}/${dst.id}`,
actor, actor,
@ -310,10 +309,10 @@ export class ApRendererService {
@bindThis @bindThis
public async renderNote(note: Note, dive = true): Promise<IPost> { public async renderNote(note: Note, dive = true): Promise<IPost> {
const getPromisedFiles = async (ids: string[]) => { const getPromisedFiles = async (ids: string[]): Promise<DriveFile[]> => {
if (!ids || ids.length === 0) return []; if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) }); const items = await this.driveFilesRepository.findBy({ id: In(ids) });
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[]; return ids.map(id => items.find(item => item.id === id)).filter((item): item is DriveFile => item != null);
}; };
let inReplyTo; let inReplyTo;
@ -375,7 +374,7 @@ export class ApRendererService {
id: In(note.mentions), id: In(note.mentions),
}) : []; }) : [];
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag)); const hashtagTags = note.tags.map(tag => this.renderHashtag(tag));
const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser)); const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
const files = await getPromisedFiles(note.fileIds); const files = await getPromisedFiles(note.fileIds);
@ -451,37 +450,26 @@ export class ApRendererService {
@bindThis @bindThis
public async renderPerson(user: LocalUser) { public async renderPerson(user: LocalUser) {
const id = this.userEntityService.genLocalUserUri(user.id); const id = this.userEntityService.genLocalUserUri(user.id);
const isSystem = !!user.username.match(/\./); const isSystem = user.username.includes('.');
const [avatar, banner, profile] = await Promise.all([ const [avatar, banner, profile] = await Promise.all([
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined), user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined), user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
this.userProfilesRepository.findOneByOrFail({ userId: user.id }), this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
]); ]);
const attachment: { const attachment = profile.fields.map(field => ({
type: 'PropertyValue', type: 'PropertyValue',
name: string, name: field.name,
value: string, value: /^https?:/.test(field.value)
identifier?: IIdentifier, ? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
}[] = []; : field.value,
}));
if (profile.fields) {
for (const field of profile.fields) {
attachment.push({
type: 'PropertyValue',
name: field.name,
value: (field.value != null && field.value.match(/^https?:/))
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
: field.value,
});
}
}
const emojis = await this.getEmojis(user.emojis); const emojis = await this.getEmojis(user.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag)); const hashtagTags = user.tags.map(tag => this.renderHashtag(tag));
const tag = [ const tag = [
...apemojis, ...apemojis,
@ -490,7 +478,7 @@ export class ApRendererService {
const keypair = await this.userKeypairService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
const person = { const person: any = {
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
id, id,
inbox: `${id}/inbox`, inbox: `${id}/inbox`,
@ -508,11 +496,11 @@ export class ApRendererService {
image: banner ? this.renderImage(banner) : null, image: banner ? this.renderImage(banner) : null,
tag, tag,
manuallyApprovesFollowers: user.isLocked, manuallyApprovesFollowers: user.isLocked,
discoverable: !!user.isExplorable, discoverable: user.isExplorable,
publicKey: this.renderKey(user, keypair, '#main-key'), publicKey: this.renderKey(user, keypair, '#main-key'),
isCat: user.isCat, isCat: user.isCat,
attachment: attachment.length ? attachment : undefined, attachment: attachment.length ? attachment : undefined,
} as any; };
if (user.movedToUri) { if (user.movedToUri) {
person.movedTo = user.movedToUri; person.movedTo = user.movedToUri;
@ -552,7 +540,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderReject(object: any, user: { id: User['id'] }): IReject { public renderReject(object: string | IObject, user: { id: User['id'] }): IReject {
return { return {
type: 'Reject', type: 'Reject',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -561,7 +549,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove { public renderRemove(user: { id: User['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
return { return {
type: 'Remove', type: 'Remove',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -579,8 +567,8 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderUndo(object: any, user: { id: User['id'] }): IUndo { public renderUndo(object: string | IObject, user: { id: User['id'] }): IUndo {
const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
return { return {
type: 'Undo', type: 'Undo',
@ -592,7 +580,7 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate { public renderUpdate(object: string | IObject, user: { id: User['id'] }): IUpdate {
return { return {
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`, id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
@ -658,7 +646,7 @@ export class ApRendererService {
vcard: 'http://www.w3.org/2006/vcard/ns#', vcard: 'http://www.w3.org/2006/vcard/ns#',
}, },
], ],
}, x as T & { id: string; }); }, x as T & { id: string });
} }
@bindThis @bindThis
@ -683,13 +671,13 @@ export class ApRendererService {
*/ */
@bindThis @bindThis
public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
const page = { const page: any = {
id, id,
partOf, partOf,
type: 'OrderedCollectionPage', type: 'OrderedCollectionPage',
totalItems, totalItems,
orderedItems, orderedItems,
} as any; };
if (prev) page.prev = prev; if (prev) page.prev = prev;
if (next) page.next = next; if (next) page.next = next;
@ -706,7 +694,7 @@ export class ApRendererService {
* @param orderedItems attached objects (optional) * @param orderedItems attached objects (optional)
*/ */
@bindThis @bindThis
public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) { public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) {
const page: any = { const page: any = {
id, id,
type: 'OrderedCollection', type: 'OrderedCollection',
@ -722,7 +710,7 @@ export class ApRendererService {
@bindThis @bindThis
private async getEmojis(names: string[]): Promise<Emoji[]> { private async getEmojis(names: string[]): Promise<Emoji[]> {
if (names == null || names.length === 0) return []; if (names.length === 0) return [];
const allEmojis = await this.customEmojiService.localEmojisCache.fetch(); const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull); const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);

View File

@ -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({

View File

@ -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');
} }

View File

@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CONTEXTS } from './misc/contexts.js'; import { CONTEXTS } from './misc/contexts.js';
import type { JsonLdDocument } from 'jsonld';
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 // RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
@ -18,22 +20,21 @@ class LdSignature {
@bindThis @bindThis
public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> { public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
const options = { const options: {
type: 'RsaSignature2017',
creator,
domain,
nonce: crypto.randomBytes(16).toString('hex'),
created: (created ?? new Date()).toISOString(),
} as {
type: string; type: string;
creator: string; creator: string;
domain?: string; domain?: string;
nonce: string; nonce: string;
created: string; created: string;
} = {
type: 'RsaSignature2017',
creator,
nonce: crypto.randomBytes(16).toString('hex'),
created: (created ?? new Date()).toISOString(),
}; };
if (!domain) { if (domain) {
delete options.domain; options.domain = domain;
} }
const toBeSigned = await this.createVerifyData(data, options); const toBeSigned = await this.createVerifyData(data, options);
@ -62,7 +63,7 @@ class LdSignature {
} }
@bindThis @bindThis
public async createVerifyData(data: any, options: any) { public async createVerifyData(data: any, options: any): Promise<string> {
const transformedOptions = { const transformedOptions = {
...options, ...options,
'@context': 'https://w3id.org/identity/v1', '@context': 'https://w3id.org/identity/v1',
@ -82,7 +83,7 @@ class LdSignature {
} }
@bindThis @bindThis
public async normalize(data: any) { public async normalize(data: JsonLdDocument): Promise<string> {
const customLoader = this.getLoader(); const customLoader = this.getLoader();
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically // XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595 // https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
@ -93,14 +94,14 @@ class LdSignature {
@bindThis @bindThis
private getLoader() { private getLoader() {
return async (url: string): Promise<any> => { return async (url: string): Promise<RemoteDocument> => {
if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`); if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
if (this.preLoad) { if (this.preLoad) {
if (url in CONTEXTS) { if (url in CONTEXTS) {
if (this.debug) console.debug(`HIT: ${url}`); if (this.debug) console.debug(`HIT: ${url}`);
return { return {
contextUrl: null, contextUrl: undefined,
document: CONTEXTS[url], document: CONTEXTS[url],
documentUrl: url, documentUrl: url,
}; };
@ -110,7 +111,7 @@ class LdSignature {
if (this.debug) console.debug(`MISS: ${url}`); if (this.debug) console.debug(`MISS: ${url}`);
const document = await this.fetchDocument(url); const document = await this.fetchDocument(url);
return { return {
contextUrl: null, contextUrl: undefined,
document: document, document: document,
documentUrl: url, documentUrl: url,
}; };
@ -118,13 +119,17 @@ class LdSignature {
} }
@bindThis @bindThis
private async fetchDocument(url: string) { private async fetchDocument(url: string): Promise<JsonLd> {
const json = await this.httpRequestService.send(url, { const json = await this.httpRequestService.send(
headers: { url,
Accept: 'application/ld+json, application/json', {
headers: {
Accept: 'application/ld+json, application/json',
},
timeout: this.loderTimeout,
}, },
timeout: this.loderTimeout, { throwErrorWhenResponseNotOk: false },
}, { throwErrorWhenResponseNotOk: false }).then(res => { ).then(res => {
if (!res.ok) { if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`); throw new Error(`${res.status} ${res.statusText}`);
} else { } else {
@ -132,7 +137,7 @@ class LdSignature {
} }
}); });
return json; return json as JsonLd;
} }
@bindThis @bindThis

View File

@ -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,

View File

@ -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' };

View File

@ -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 &&

View File

@ -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({

View File

@ -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 };
} }

View File

@ -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]);
} }

View File

@ -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)));

View File

@ -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);

View 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);

View 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 });

View File

@ -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;

View File

@ -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 });

View File

@ -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 });

View File

@ -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;

View File

@ -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`,
); );

View File

@ -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),

View File

@ -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;
} }

View File

@ -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>

View File

@ -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) {

View File

@ -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>

View File

@ -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 });

View File

@ -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() {

View File

@ -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];
}); });
} }
} }

View File

@ -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>

View File

@ -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">

View File

@ -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 => {

View File

@ -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(() => {

View File

@ -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]()));

View File

@ -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'));

View File

@ -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,
}); });
}); });

View File

@ -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,
}); });
}); });

View File

@ -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,
}); });
}); });

View File

@ -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',

View File

@ -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]);
} }

View File

@ -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}`;

View File

@ -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;

View File

@ -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;
} }

View File

@ -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;

View File

@ -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('<'));

View File

@ -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,
}); });

View File

@ -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);

View File

@ -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;

View File

@ -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 };
} }