Merge branch 'develop' into ed25519
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.10.0"
|
||||
"node": "^20.10.0 || ^22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./built/boot/entry.js",
|
||||
@@ -159,7 +159,7 @@
|
||||
"qrcode": "1.5.3",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.20.10",
|
||||
"re2": "1.21.2",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
@@ -177,7 +177,7 @@
|
||||
"tsc-alias": "1.8.8",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.20",
|
||||
"typescript": "5.4.5",
|
||||
"typescript": "5.5.2",
|
||||
"ulid": "2.3.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
@@ -207,7 +207,6 @@
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.12.7",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.15",
|
||||
"@types/oauth": "0.9.4",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
|
@@ -10,7 +10,6 @@ import sanitizeHtml from 'sanitize-html';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import type {
|
||||
AbuseReportNotificationRecipientRepository,
|
||||
MiAbuseReportNotificationRecipient,
|
||||
@@ -91,7 +90,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||
const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
|
||||
.filter(it => it.isActive && it.userProfile?.emailVerified)
|
||||
.map(it => it.userProfile?.email)
|
||||
.filter(isNotNull),
|
||||
.filter(x => x != null),
|
||||
);
|
||||
|
||||
// 送信先の鮮度を保つため、毎回取得する
|
||||
@@ -138,7 +137,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||
.then(it => it
|
||||
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
|
||||
.map(it => it.systemWebhookId)
|
||||
.filter(isNotNull));
|
||||
.filter(x => x != null));
|
||||
for (const webhookId of recipientWebhookIds) {
|
||||
await Promise.all(
|
||||
abuseReports.map(it => {
|
||||
@@ -340,7 +339,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||
@bindThis
|
||||
private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||
const userRecipients = recipients.filter(it => it.userId !== null);
|
||||
const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(isNotNull));
|
||||
const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
|
||||
if (recipientUserIds.size <= 0) {
|
||||
// ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
|
||||
return recipients;
|
||||
|
@@ -41,7 +41,7 @@ export class ClipService {
|
||||
const currentCount = await this.clipsRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) {
|
||||
throw new ClipService.TooManyClipsError();
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export class ClipService {
|
||||
const currentCount = await this.clipNotesRepository.countBy({
|
||||
clipId: clip.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
|
||||
throw new ClipService.TooManyClipNotesError();
|
||||
}
|
||||
|
||||
|
@@ -55,9 +55,6 @@ export class FanoutTimelineEndpointService {
|
||||
|
||||
@bindThis
|
||||
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
|
||||
let noteIds: string[];
|
||||
let shouldFallbackToDb = false;
|
||||
|
||||
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
|
||||
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
|
||||
|
||||
@@ -67,12 +64,11 @@ export class FanoutTimelineEndpointService {
|
||||
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
|
||||
|
||||
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
|
||||
const redisResultIds = Array.from(new Set(redisResult.flat(1)));
|
||||
const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
|
||||
|
||||
redisResultIds.sort(idCompare);
|
||||
noteIds = redisResultIds.slice(0, ps.limit);
|
||||
|
||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||
let noteIds = redisResultIds.slice(0, ps.limit);
|
||||
const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
|
||||
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
let filter = ps.noteFilter ?? (_note => true);
|
||||
|
@@ -59,7 +59,6 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
@@ -839,7 +838,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
const mentions = extractMentions(tokens);
|
||||
let mentionedUsers = (await Promise.all(mentions.map(m =>
|
||||
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
|
||||
))).filter(isNotNull);
|
||||
))).filter(x => x != null);
|
||||
|
||||
// Drop duplicate users
|
||||
mentionedUsers = mentionedUsers.filter((u, i, self) =>
|
||||
|
@@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
|
||||
const FALLBACK = '\u2764';
|
||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||
@@ -117,11 +118,16 @@ export class ReactionService {
|
||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
// Check if note is Renote
|
||||
if (isRenote(note) && !isQuote(note)) {
|
||||
throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
|
||||
}
|
||||
|
||||
let reaction = _reaction ?? FALLBACK;
|
||||
|
||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
||||
reaction = '\u2764';
|
||||
} else if (_reaction) {
|
||||
} else if (_reaction != null) {
|
||||
const custom = reaction.match(isCustomEmojiRegexp);
|
||||
if (custom) {
|
||||
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
||||
|
@@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
|
||||
const currentCount = await this.userListMembershipsRepository.countBy({
|
||||
userListId: list.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||
throw new UserListService.TooManyUsersError();
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
|
||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import { concat, unique } from '@/misc/prelude/array.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { getApIds } from './type.js';
|
||||
import { ApPersonService } from './models/ApPersonService.js';
|
||||
import type { ApObject } from './type.js';
|
||||
@@ -41,7 +40,7 @@ export class ApAudienceService {
|
||||
const limit = promiseLimit<MiUser | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
|
||||
)).filter(isNotNull);
|
||||
)).filter(x => x != null);
|
||||
|
||||
if (toGroups.public.length > 0) {
|
||||
return {
|
||||
|
@@ -27,7 +27,6 @@ import { QueueService } from '@/core/QueueService.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { AbuseReportService } from '@/core/AbuseReportService.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';
|
||||
@@ -531,7 +530,7 @@ export class ApInboxService {
|
||||
const userIds = uris
|
||||
.filter(uri => uri.startsWith(this.config.url + '/users/'))
|
||||
.map(uri => uri.split('/').at(-1))
|
||||
.filter(isNotNull);
|
||||
.filter(x => x != null);
|
||||
const users = await this.usersRepository.findBy({
|
||||
id: In(userIds),
|
||||
});
|
||||
|
@@ -25,7 +25,6 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
|
||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { JsonLdService } from './JsonLdService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
@@ -317,7 +316,7 @@ export class ApRendererService {
|
||||
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||
return ids.map(id => items.find(item => item.id === id)).filter(isNotNull);
|
||||
return ids.map(id => items.find(item => item.id === id)).filter(x => x != null);
|
||||
};
|
||||
|
||||
let inReplyTo;
|
||||
@@ -689,7 +688,7 @@ export class ApRendererService {
|
||||
if (names.length === 0) return [];
|
||||
|
||||
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(x => x != null);
|
||||
|
||||
return emojis;
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
|
||||
import type { MiUser } from '@/models/_.js';
|
||||
import { toArray, unique } from '@/misc/prelude/array.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { isMention } from '../type.js';
|
||||
import { Resolver } from '../ApResolverService.js';
|
||||
import { ApPersonService } from './ApPersonService.js';
|
||||
@@ -28,7 +27,7 @@ export class ApMentionService {
|
||||
const limit = promiseLimit<MiUser | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
|
||||
)).filter(isNotNull);
|
||||
)).filter(x => x != null);
|
||||
|
||||
return mentionedUsers;
|
||||
}
|
||||
|
@@ -24,7 +24,6 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { ApMfmService } from '../ApMfmService.js';
|
||||
@@ -253,7 +252,7 @@ export class ApNoteService {
|
||||
}
|
||||
};
|
||||
|
||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull));
|
||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
|
||||
const results = await Promise.all(uris.map(tryResolveNote));
|
||||
|
||||
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
||||
|
@@ -39,7 +39,6 @@ import { MetaService } from '@/core/MetaService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { REMOTE_USER_CACHE_TTL, REMOTE_USER_MOVE_COOLDOWN } from '@/const.js';
|
||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
@@ -711,7 +710,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
// とりあえずidを別の時間で生成して順番を維持
|
||||
let td = 0;
|
||||
for (const note of featuredNotes.filter(isNotNull)) {
|
||||
for (const note of featuredNotes.filter(x => x != null)) {
|
||||
td -= 1000;
|
||||
transactionalEntityManager.insert(MiUserNotePining, {
|
||||
id: this.idService.gen(Date.now() + td),
|
||||
|
@@ -10,7 +10,6 @@ import type { Config } from '@/config.js';
|
||||
import type { IPoll } from '@/models/Poll.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { isQuestion } from '../type.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { ApResolverService } from '../ApResolverService.js';
|
||||
@@ -52,7 +51,7 @@ export class ApQuestionService {
|
||||
|
||||
const choices = question[multiple ? 'anyOf' : 'oneOf']
|
||||
?.map((x) => x.name)
|
||||
.filter(isNotNull)
|
||||
.filter(x => x != null)
|
||||
?? [];
|
||||
|
||||
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
|
||||
|
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { toArray } from '@/misc/prelude/array.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { isHashtag } from '../type.js';
|
||||
import type { IObject, IApHashtag } from '../type.js';
|
||||
|
||||
@@ -16,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
|
||||
return hashtags.map(tag => {
|
||||
const m = tag.name.match(/^#(.+)/);
|
||||
return m ? m[1] : null;
|
||||
}).filter(isNotNull);
|
||||
}).filter(x => x != null);
|
||||
}
|
||||
|
||||
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
|
||||
|
@@ -11,7 +11,6 @@ import { bindThis } from '@/decorators.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
|
||||
@Injectable()
|
||||
export class AbuseReportNotificationRecipientEntityService {
|
||||
@@ -66,13 +65,13 @@ export class AbuseReportNotificationRecipientEntityService {
|
||||
);
|
||||
}
|
||||
|
||||
const userIds = objs.map(it => it.userId).filter(isNotNull);
|
||||
const userIds = objs.map(it => it.userId).filter(x => x != null);
|
||||
const users: Map<string, Packed<'UserLite'>> = (userIds.length > 0)
|
||||
? await this.userEntityService.packMany(userIds)
|
||||
.then(it => new Map(it.map(it => [it.id, it])))
|
||||
: new Map();
|
||||
|
||||
const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(isNotNull);
|
||||
const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(x => x != null);
|
||||
const systemWebhooks: Map<string, Packed<'SystemWebhook'>> = (systemWebhookIds.length > 0)
|
||||
? await this.systemWebhookEntityService.packMany(systemWebhookIds)
|
||||
.then(it => new Map(it.map(it => [it.id, it])))
|
||||
|
@@ -10,7 +10,6 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@@ -63,7 +62,7 @@ export class AbuseUserReportEntityService {
|
||||
) {
|
||||
const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
|
||||
const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
|
||||
const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull);
|
||||
const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null);
|
||||
const _userMap = await this.userEntityService.packMany(
|
||||
[..._reporters, ..._targetUsers, ..._assignees],
|
||||
null,
|
||||
|
@@ -53,7 +53,7 @@ export class ClipEntityService {
|
||||
isPublic: clip.isPublic,
|
||||
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
||||
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
||||
notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
|
||||
notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,6 @@ import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UtilityService } from '../UtilityService.js';
|
||||
import { VideoProcessingService } from '../VideoProcessingService.js';
|
||||
@@ -261,11 +260,11 @@ export class DriveFileEntityService {
|
||||
files: MiDriveFile[],
|
||||
options?: PackOptions,
|
||||
): Promise<Packed<'DriveFile'>[]> {
|
||||
const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull);
|
||||
const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
|
||||
const _userMap = await this.userEntityService.packMany(_user)
|
||||
.then(users => new Map(users.map(user => [user.id, user])));
|
||||
const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
|
||||
return items.filter(isNotNull);
|
||||
return items.filter(x => x != null);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -290,6 +289,6 @@ export class DriveFileEntityService {
|
||||
): Promise<Packed<'DriveFile'>[]> {
|
||||
if (fileIds.length === 0) return [];
|
||||
const filesMap = await this.packManyByIdsMap(fileIds, options);
|
||||
return fileIds.map(id => filesMap.get(id)).filter(isNotNull);
|
||||
return fileIds.map(id => filesMap.get(id)).filter(x => x != null);
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ import type { MiUser } from '@/models/User.js';
|
||||
import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -59,8 +58,8 @@ export class InviteCodeEntityService {
|
||||
tickets: MiRegistrationTicket[],
|
||||
me: { id: MiUser['id'] },
|
||||
) {
|
||||
const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull);
|
||||
const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull);
|
||||
const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(x => x != null);
|
||||
const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(x => x != null);
|
||||
const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(
|
||||
|
@@ -14,7 +14,6 @@ import type { MiNote } from '@/models/Note.js';
|
||||
import type { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { DebounceLoader } from '@/misc/loader.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
@@ -276,7 +275,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
packedFiles.set(k, v);
|
||||
}
|
||||
}
|
||||
return fileIds.map(id => packedFiles.get(id)).filter(isNotNull);
|
||||
return fileIds.map(id => packedFiles.get(id)).filter(x => x != null);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -449,12 +448,12 @@ export class NoteEntityService implements OnModuleInit {
|
||||
|
||||
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
|
||||
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
|
||||
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
|
||||
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
|
||||
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
|
||||
const users = [
|
||||
...notes.map(({ user, userId }) => user ?? userId),
|
||||
...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull),
|
||||
...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull),
|
||||
...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null),
|
||||
...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null),
|
||||
];
|
||||
const packedUsers = await this.userEntityService.packMany(users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
|
@@ -13,7 +13,6 @@ import type { MiGroupedNotification, MiNotification } from '@/models/Notificatio
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { RoleEntityService } from './RoleEntityService.js';
|
||||
@@ -103,7 +102,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
user,
|
||||
reaction: reaction.reaction,
|
||||
};
|
||||
}))).filter(r => isNotNull(r.user));
|
||||
}))).filter(r => r.user != null);
|
||||
// if all users have been deleted, don't show this notification
|
||||
if (reactions.length === 0) {
|
||||
return null;
|
||||
@@ -124,7 +123,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
return this.userEntityService.pack(userId, { id: meId });
|
||||
}))).filter(isNotNull);
|
||||
}))).filter(x => x != null);
|
||||
// if all users have been deleted, don't show this notification
|
||||
if (users.length === 0) {
|
||||
return null;
|
||||
@@ -181,7 +180,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
|
||||
validNotifications = await this.#filterValidNotifier(validNotifications, meId);
|
||||
|
||||
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
|
||||
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null);
|
||||
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
||||
where: { id: In(noteIds) },
|
||||
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
|
||||
@@ -223,7 +222,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
);
|
||||
});
|
||||
|
||||
return (await Promise.all(packPromises)).filter(isNotNull);
|
||||
return (await Promise.all(packPromises)).filter(x => x != null);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -305,7 +304,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
|
||||
]);
|
||||
|
||||
const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull);
|
||||
const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null);
|
||||
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
|
||||
where: { id: In(notifierIds) },
|
||||
}) : [];
|
||||
@@ -313,7 +312,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
|
||||
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
|
||||
return isValid ? notification : null;
|
||||
}))) as [T | null] ).filter(isNotNull);
|
||||
}))) as [T | null] ).filter(x => x != null);
|
||||
|
||||
return filteredNotifications;
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@ import type { MiPage } from '@/models/Page.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
|
||||
@@ -106,7 +105,7 @@ export class PageEntityService {
|
||||
script: page.script,
|
||||
eyeCatchingImageId: page.eyeCatchingImageId,
|
||||
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
|
||||
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)),
|
||||
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)),
|
||||
likedCount: page.likedCount,
|
||||
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
|
||||
});
|
||||
|
@@ -47,7 +47,6 @@ import { IdService } from '@/core/IdService.js';
|
||||
import type { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { NoteEntityService } from './NoteEntityService.js';
|
||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
@@ -514,7 +513,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
|
||||
alsoKnownAs: user.alsoKnownAs
|
||||
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
|
||||
.then(xs => xs.length === 0 ? null : xs.filter(isNotNull))
|
||||
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
|
||||
: null,
|
||||
createdAt: this.idService.parse(user.id).date.toISOString(),
|
||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||
|
@@ -1,8 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function isNotNull<T extends NonNullable<unknown>>(input: T | undefined | null): input is T {
|
||||
return input != null;
|
||||
}
|
@@ -65,44 +65,6 @@ export function maximum(xs: number[]): number {
|
||||
return Math.max(...xs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an array based on the equivalence relation.
|
||||
* The concatenation of the result is equal to the argument.
|
||||
*/
|
||||
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
|
||||
const groups = [] as T[][];
|
||||
for (const x of xs) {
|
||||
const lastGroup = groups.at(-1);
|
||||
if (lastGroup !== undefined && f(lastGroup[0], x)) {
|
||||
lastGroup.push(x);
|
||||
} else {
|
||||
groups.push([x]);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an array based on the equivalence relation induced by the function.
|
||||
* The concatenation of the result is equal to the argument.
|
||||
*/
|
||||
export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
|
||||
return groupBy((a, b) => f(a) === f(b), xs);
|
||||
}
|
||||
|
||||
export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
|
||||
return collections.reduce((obj: Record<string, T[]>, item: T) => {
|
||||
const key = keySelector(item);
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
obj[key] = [];
|
||||
}
|
||||
|
||||
obj[key].push(item);
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two arrays by lexicographical order
|
||||
*/
|
||||
|
@@ -1,8 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function gcd(a: number, b: number): number {
|
||||
return b === 0 ? a : gcd(b, a % b);
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface IMaybe<T> {
|
||||
isJust(): this is IJust<T>;
|
||||
}
|
||||
|
||||
export interface IJust<T> extends IMaybe<T> {
|
||||
get(): T;
|
||||
}
|
||||
|
||||
export function just<T>(value: T): IJust<T> {
|
||||
return {
|
||||
isJust: () => true,
|
||||
get: () => value,
|
||||
};
|
||||
}
|
||||
|
||||
export function nothing<T>(): IMaybe<T> {
|
||||
return {
|
||||
isJust: () => false,
|
||||
};
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function concat(xs: string[]): string {
|
||||
return xs.join('');
|
||||
}
|
||||
|
||||
export function capitalize(s: string): string {
|
||||
return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
|
||||
}
|
||||
|
||||
export function toUpperCase(s: string): string {
|
||||
return s.toUpperCase();
|
||||
}
|
||||
|
||||
export function toLowerCase(s: string): string {
|
||||
return s.toLowerCase();
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const fallback = Symbol('fallback');
|
@@ -82,34 +82,14 @@ import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
||||
|
||||
export interface MiRepository<T extends ObjectLiteral> {
|
||||
createTableColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
|
||||
createTableColumnNamesWithPrimaryKey(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
|
||||
createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
|
||||
insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>;
|
||||
selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void;
|
||||
}
|
||||
|
||||
export const miRepository = {
|
||||
createTableColumnNames(queryBuilder) {
|
||||
// @ts-expect-error -- protected
|
||||
const insertedColumns = queryBuilder.getInsertedColumns();
|
||||
if (insertedColumns.length) {
|
||||
return insertedColumns.map(column => column.databaseName);
|
||||
}
|
||||
if (!queryBuilder.expressionMap.mainAlias?.hasMetadata && !queryBuilder.expressionMap.insertColumns.length) {
|
||||
// @ts-expect-error -- protected
|
||||
const valueSets = queryBuilder.getValueSets();
|
||||
if (valueSets.length === 1) {
|
||||
return Object.keys(valueSets[0]);
|
||||
}
|
||||
}
|
||||
return queryBuilder.expressionMap.insertColumns;
|
||||
},
|
||||
createTableColumnNamesWithPrimaryKey(queryBuilder) {
|
||||
const columnNames = this.createTableColumnNames(queryBuilder);
|
||||
if (!columnNames.includes('id')) {
|
||||
columnNames.unshift('id');
|
||||
}
|
||||
return columnNames;
|
||||
createTableColumnNames() {
|
||||
return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName);
|
||||
},
|
||||
async insertOne(entity, findOptions?) {
|
||||
const queryBuilder = this.createQueryBuilder().insert().values(entity);
|
||||
@@ -117,7 +97,7 @@ export const miRepository = {
|
||||
const mainAlias = queryBuilder.expressionMap.mainAlias!;
|
||||
const name = mainAlias.name;
|
||||
mainAlias.name = 't';
|
||||
const columnNames = this.createTableColumnNamesWithPrimaryKey(queryBuilder);
|
||||
const columnNames = this.createTableColumnNames();
|
||||
queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
|
||||
const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames });
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
@@ -138,7 +118,7 @@ export const miRepository = {
|
||||
selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
|
||||
return builder.select(selection, selectionAliasName);
|
||||
};
|
||||
for (const columnName of this.createTableColumnNamesWithPrimaryKey(queryBuilder)) {
|
||||
for (const columnName of this.createTableColumnNames()) {
|
||||
selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
|
||||
}
|
||||
},
|
||||
|
@@ -117,6 +117,12 @@ export class DeliverProcessorService {
|
||||
suspensionState: 'autoSuspendedForNotResponding',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// isNotRespondingがtrueでnotRespondingSinceがnullの場合はnotRespondingSinceをセット
|
||||
// notRespondingSinceは新たな機能なので、それ以前のデータにはnotRespondingSinceがない場合がある
|
||||
this.federatedInstanceService.update(i.id, {
|
||||
notRespondingSince: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
this.apRequestChart.deliverFail();
|
||||
|
@@ -73,6 +73,16 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
|
||||
}
|
||||
statusCode = statusCode ?? 403;
|
||||
} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
|
||||
const info: unknown = err.info;
|
||||
const unixEpochInSeconds = Date.now();
|
||||
if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') {
|
||||
const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000);
|
||||
// もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
|
||||
reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
|
||||
} else {
|
||||
this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
|
||||
}
|
||||
} else if (!statusCode) {
|
||||
statusCode = 500;
|
||||
}
|
||||
@@ -308,12 +318,17 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
if (factor > 0) {
|
||||
// Rate limit
|
||||
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
|
||||
throw new ApiError({
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
||||
httpStatusCode: 429,
|
||||
});
|
||||
if ('info' in err) {
|
||||
// errはLimiter.LimiterInfoであることが期待される
|
||||
throw new ApiError({
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
||||
httpStatusCode: 429,
|
||||
}, err.info);
|
||||
} else {
|
||||
throw new TypeError('information must be a rate-limiter information.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -32,11 +32,13 @@ export class RateLimiterService {
|
||||
|
||||
@bindThis
|
||||
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
|
||||
return new Promise<void>((ok, reject) => {
|
||||
if (this.disabled) ok();
|
||||
{
|
||||
if (this.disabled) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Short-term limit
|
||||
const min = (): void => {
|
||||
const min = new Promise<void>((ok, reject) => {
|
||||
const minIntervalLimiter = new Limiter({
|
||||
id: `${actor}:${limitation.key}:min`,
|
||||
duration: limitation.minInterval! * factor,
|
||||
@@ -46,25 +48,25 @@ export class RateLimiterService {
|
||||
|
||||
minIntervalLimiter.get((err, info) => {
|
||||
if (err) {
|
||||
return reject('ERR');
|
||||
return reject({ code: 'ERR', info });
|
||||
}
|
||||
|
||||
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
|
||||
|
||||
if (info.remaining === 0) {
|
||||
reject('BRIEF_REQUEST_INTERVAL');
|
||||
return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
|
||||
} else {
|
||||
if (hasLongTermLimit) {
|
||||
max();
|
||||
return max.then(ok, reject);
|
||||
} else {
|
||||
ok();
|
||||
return ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Long term limit
|
||||
const max = (): void => {
|
||||
const max = new Promise<void>((ok, reject) => {
|
||||
const limiter = new Limiter({
|
||||
id: `${actor}:${limitation.key}`,
|
||||
duration: limitation.duration! * factor,
|
||||
@@ -74,18 +76,18 @@ export class RateLimiterService {
|
||||
|
||||
limiter.get((err, info) => {
|
||||
if (err) {
|
||||
return reject('ERR');
|
||||
return reject({ code: 'ERR', info });
|
||||
}
|
||||
|
||||
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
|
||||
|
||||
if (info.remaining === 0) {
|
||||
reject('RATE_LIMIT_EXCEEDED');
|
||||
return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
|
||||
} else {
|
||||
ok();
|
||||
return ok();
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const hasShortTermLimit = typeof limitation.minInterval === 'number';
|
||||
|
||||
@@ -94,12 +96,12 @@ export class RateLimiterService {
|
||||
typeof limitation.max === 'number';
|
||||
|
||||
if (hasShortTermLimit) {
|
||||
min();
|
||||
return min;
|
||||
} else if (hasLongTermLimit) {
|
||||
max();
|
||||
return max;
|
||||
} else {
|
||||
ok();
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ export const paramDef = {
|
||||
startsAt: { type: 'integer' },
|
||||
dayOfWeek: { type: 'integer' },
|
||||
},
|
||||
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'],
|
||||
required: ['id'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
@@ -63,8 +63,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
ratio: ps.ratio,
|
||||
memo: ps.memo,
|
||||
imageUrl: ps.imageUrl,
|
||||
expiresAt: new Date(ps.expiresAt),
|
||||
startsAt: new Date(ps.startsAt),
|
||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
|
||||
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
|
||||
dayOfWeek: ps.dayOfWeek,
|
||||
});
|
||||
|
||||
|
@@ -6,7 +6,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RolesRepository } from '@/models/_.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
@@ -50,19 +49,6 @@ export const paramDef = {
|
||||
},
|
||||
required: [
|
||||
'roleId',
|
||||
'name',
|
||||
'description',
|
||||
'color',
|
||||
'iconUrl',
|
||||
'target',
|
||||
'condFormula',
|
||||
'isPublic',
|
||||
'isModerator',
|
||||
'isAdministrator',
|
||||
'asBadge',
|
||||
'canEditMembersByModerator',
|
||||
'displayOrder',
|
||||
'policies',
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
@@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
const currentAntennasCount = await this.antennasRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
|
||||
if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
|
||||
throw new ApiError(meta.errors.tooManyAntennas);
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||
import { ClipService } from '@/core/ClipService.js';
|
||||
@@ -41,7 +41,7 @@ export const paramDef = {
|
||||
isPublic: { type: 'boolean' },
|
||||
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
|
||||
},
|
||||
required: ['clipId', 'name'],
|
||||
required: ['clipId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
|
@@ -95,15 +95,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
// Check if the circular reference will occur
|
||||
const checkCircle = async (folderId: string): Promise<boolean> => {
|
||||
// Fetch folder
|
||||
const folder2 = await this.driveFoldersRepository.findOneBy({
|
||||
const folder2 = await this.driveFoldersRepository.findOneByOrFail({
|
||||
id: folderId,
|
||||
});
|
||||
|
||||
if (folder2!.id === folder!.id) {
|
||||
if (folder2.id === folder.id) {
|
||||
return true;
|
||||
} else if (folder2!.parentId) {
|
||||
return await checkCircle(folder2!.parentId);
|
||||
} else if (folder2.parentId) {
|
||||
return await checkCircle(folder2.parentId);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
@@ -70,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
id: fileId,
|
||||
userId: me.id,
|
||||
}),
|
||||
))).filter(isNotNull);
|
||||
))).filter(x => x != null);
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error();
|
||||
|
@@ -10,7 +10,6 @@ import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
@@ -48,7 +47,7 @@ export const paramDef = {
|
||||
} },
|
||||
isSensitive: { type: 'boolean', default: false },
|
||||
},
|
||||
required: ['postId', 'title', 'fileIds'],
|
||||
required: ['postId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
@@ -63,15 +62,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private galleryPostEntityService: GalleryPostEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const files = (await Promise.all(ps.fileIds.map(fileId =>
|
||||
this.driveFilesRepository.findOneBy({
|
||||
id: fileId,
|
||||
userId: me.id,
|
||||
}),
|
||||
))).filter(isNotNull);
|
||||
let files: Array<MiDriveFile> | undefined;
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error();
|
||||
if (ps.fileIds) {
|
||||
files = (await Promise.all(ps.fileIds.map(fileId =>
|
||||
this.driveFilesRepository.findOneBy({
|
||||
id: fileId,
|
||||
userId: me.id,
|
||||
}),
|
||||
))).filter(x => x != null);
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
await this.galleryPostsRepository.update({
|
||||
@@ -82,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
title: ps.title,
|
||||
description: ps.description,
|
||||
isSensitive: ps.isSensitive,
|
||||
fileIds: files.map(file => file.id),
|
||||
fileIds: files ? files.map(file => file.id) : undefined,
|
||||
});
|
||||
|
||||
const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId });
|
||||
|
@@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
||||
const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url));
|
||||
const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id });
|
||||
if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
|
||||
if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
|
||||
throw new ApiError(meta.errors.tooManyAntennas);
|
||||
}
|
||||
this.queueService.createImportAntennasJob(me, antennas);
|
||||
|
@@ -85,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
const currentWebhooksCount = await this.webhooksRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
|
||||
if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
|
||||
throw new ApiError(meta.errors.tooManyWebhooks);
|
||||
}
|
||||
|
||||
|
@@ -34,13 +34,13 @@ export const paramDef = {
|
||||
webhookId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
url: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', maxLength: 1024, default: '' },
|
||||
secret: { type: 'string', nullable: true, maxLength: 1024 },
|
||||
on: { type: 'array', items: {
|
||||
type: 'string', enum: webhookEventTypes,
|
||||
} },
|
||||
active: { type: 'boolean' },
|
||||
},
|
||||
required: ['webhookId', 'name', 'url', 'on', 'active'],
|
||||
required: ['webhookId'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
@@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
await this.webhooksRepository.update(webhook.id, {
|
||||
name: ps.name,
|
||||
url: ps.url,
|
||||
secret: ps.secret,
|
||||
secret: ps.secret === null ? '' : ps.secret,
|
||||
on: ps.on,
|
||||
active: ps.active,
|
||||
});
|
||||
|
@@ -36,6 +36,12 @@ export const meta = {
|
||||
code: 'YOU_HAVE_BEEN_BLOCKED',
|
||||
id: '20ef5475-9f38-4e4c-bd33-de6d979498ec',
|
||||
},
|
||||
|
||||
cannotReactToRenote: {
|
||||
message: 'You cannot react to Renote.',
|
||||
code: 'CANNOT_REACT_TO_RENOTE',
|
||||
id: 'eaccdc08-ddef-43fe-908f-d108faad57f5',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -62,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
await this.reactionService.create(me, note, ps.reaction).catch(err => {
|
||||
if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
|
||||
if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote);
|
||||
throw err;
|
||||
});
|
||||
return;
|
||||
|
@@ -70,7 +70,7 @@ export const paramDef = {
|
||||
alignCenter: { type: 'boolean' },
|
||||
hideTitleWhenPinned: { type: 'boolean' },
|
||||
},
|
||||
required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
|
||||
required: ['pageId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
@@ -91,9 +91,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
let eyeCatchingImage = null;
|
||||
if (ps.eyeCatchingImageId != null) {
|
||||
eyeCatchingImage = await this.driveFilesRepository.findOneBy({
|
||||
const eyeCatchingImage = await this.driveFilesRepository.findOneBy({
|
||||
id: ps.eyeCatchingImageId,
|
||||
userId: me.id,
|
||||
});
|
||||
@@ -116,23 +115,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
await this.pagesRepository.update(page.id, {
|
||||
updatedAt: new Date(),
|
||||
title: ps.title,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
name: ps.name === undefined ? page.name : ps.name,
|
||||
name: ps.name,
|
||||
summary: ps.summary === undefined ? page.summary : ps.summary,
|
||||
content: ps.content,
|
||||
variables: ps.variables,
|
||||
script: ps.script,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
font: ps.font === undefined ? page.font : ps.font,
|
||||
eyeCatchingImageId: ps.eyeCatchingImageId === null
|
||||
? null
|
||||
: ps.eyeCatchingImageId === undefined
|
||||
? page.eyeCatchingImageId
|
||||
: eyeCatchingImage!.id,
|
||||
alignCenter: ps.alignCenter,
|
||||
hideTitleWhenPinned: ps.hideTitleWhenPinned,
|
||||
font: ps.font,
|
||||
eyeCatchingImageId: ps.eyeCatchingImageId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
@@ -53,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
host: acct.host ?? IsNull(),
|
||||
})));
|
||||
|
||||
return await this.userEntityService.packMany(users.filter(isNotNull), me, { schema: 'UserDetailed' });
|
||||
return await this.userEntityService.packMany(users.filter(x => x != null), me, { schema: 'UserDetailed' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -100,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
const currentCount = await this.userListsRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) {
|
||||
throw new ApiError(meta.errors.tooManyUserLists);
|
||||
}
|
||||
|
||||
|
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
const currentCount = await this.userListsRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) {
|
||||
throw new ApiError(meta.errors.tooManyUserLists);
|
||||
}
|
||||
|
||||
|
@@ -14,6 +14,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { MfmService } from "@/core/MfmService.js";
|
||||
import { parse as mfmParse } from 'mfm-js';
|
||||
|
||||
@Injectable()
|
||||
export class FeedService {
|
||||
@@ -33,6 +35,7 @@ export class FeedService {
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private idService: IdService,
|
||||
private mfmService: MfmService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -76,13 +79,14 @@ export class FeedService {
|
||||
id: In(note.fileIds),
|
||||
}) : [];
|
||||
const file = files.find(file => file.type.startsWith('image/'));
|
||||
const text = note.text;
|
||||
|
||||
feed.addItem({
|
||||
title: `New note by ${author.name}`,
|
||||
link: `${this.config.url}/notes/${note.id}`,
|
||||
date: this.idService.parse(note.id).date,
|
||||
description: note.cw ?? undefined,
|
||||
content: note.text ?? undefined,
|
||||
content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined,
|
||||
image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined,
|
||||
});
|
||||
}
|
||||
|
@@ -1,5 +1,3 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
redistest:
|
||||
image: redis:7
|
@@ -163,8 +163,7 @@ describe('アンテナ', () => {
|
||||
});
|
||||
|
||||
test('が上限いっぱいまで作成できること', async () => {
|
||||
// antennaLimit + 1まで作れるのがキモ
|
||||
const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({
|
||||
const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit)].map(() => successfulApiCall({
|
||||
endpoint: 'antennas/create',
|
||||
parameters: { ...defaultParam },
|
||||
user: alice,
|
||||
|
@@ -153,8 +153,7 @@ describe('クリップ', () => {
|
||||
});
|
||||
|
||||
test('の作成はポリシーで定められた数以上はできない。', async () => {
|
||||
// ポリシー + 1まで作れるという所がミソ
|
||||
const clipLimit = DEFAULT_POLICIES.clipLimit + 1;
|
||||
const clipLimit = DEFAULT_POLICIES.clipLimit;
|
||||
for (let i = 0; i < clipLimit; i++) {
|
||||
await create();
|
||||
}
|
||||
@@ -327,7 +326,7 @@ describe('クリップ', () => {
|
||||
});
|
||||
|
||||
test('の一覧(clips/list)が取得できる(上限いっぱい)', async () => {
|
||||
const clipLimit = DEFAULT_POLICIES.clipLimit + 1;
|
||||
const clipLimit = DEFAULT_POLICIES.clipLimit;
|
||||
const clips = await createMany({}, clipLimit);
|
||||
const res = await list({
|
||||
parameters: { limit: 1 }, // FIXME: 無視されて11全部返ってくる
|
||||
@@ -705,7 +704,7 @@ describe('クリップ', () => {
|
||||
|
||||
// TODO: 17000msくらいかかる...
|
||||
test('をポリシーで定められた上限いっぱい(200)を超えて追加はできない。', async () => {
|
||||
const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1;
|
||||
const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit;
|
||||
const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, {
|
||||
text: `test ${i}`,
|
||||
}) as unknown)) as Misskey.entities.Note[];
|
||||
|
@@ -266,6 +266,67 @@ describe('Endpoints', () => {
|
||||
assert.strictEqual(res.status, 400);
|
||||
});
|
||||
|
||||
test('リノートにリアクションできない', async () => {
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
const bobRenote = await post(bob, { renoteId: bobNote.id });
|
||||
|
||||
const res = await api('notes/reactions/create', {
|
||||
noteId: bobRenote.id,
|
||||
reaction: '🚀',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'CANNOT_REACT_TO_RENOTE');
|
||||
});
|
||||
|
||||
test('引用にリアクションできる', async () => {
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
const bobRenote = await post(bob, { text: 'hi again', renoteId: bobNote.id });
|
||||
|
||||
const res = await api('notes/reactions/create', {
|
||||
noteId: bobRenote.id,
|
||||
reaction: '🚀',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
});
|
||||
|
||||
test('空文字列のリアクションは\u2764にフォールバックされる', async () => {
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
|
||||
const res = await api('notes/reactions/create', {
|
||||
noteId: bobNote.id,
|
||||
reaction: '',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
|
||||
const reaction = await api('notes/reactions', {
|
||||
noteId: bobNote.id,
|
||||
});
|
||||
|
||||
assert.strictEqual(reaction.body.length, 1);
|
||||
assert.strictEqual(reaction.body[0].type, '\u2764');
|
||||
});
|
||||
|
||||
test('絵文字ではない文字列のリアクションは\u2764にフォールバックされる', async () => {
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
|
||||
const res = await api('notes/reactions/create', {
|
||||
noteId: bobNote.id,
|
||||
reaction: 'Hello!',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
|
||||
const reaction = await api('notes/reactions', {
|
||||
noteId: bobNote.id,
|
||||
});
|
||||
|
||||
assert.strictEqual(reaction.body.length, 1);
|
||||
assert.strictEqual(reaction.body[0].type, '\u2764');
|
||||
});
|
||||
|
||||
test('空のパラメータで怒られる', async () => {
|
||||
// @ts-expect-error param must not be empty
|
||||
const res = await api('notes/reactions/create', {}, alice);
|
||||
|
@@ -153,6 +153,23 @@ describe('Webリソース', () => {
|
||||
path: path('nonexisting'),
|
||||
status: 404,
|
||||
}));
|
||||
|
||||
describe(' has entry such ', () => {
|
||||
beforeEach(() => {
|
||||
post(alice, { text: "**a**" })
|
||||
});
|
||||
|
||||
test('MFMを含まない。', async () => {
|
||||
const content = await simpleGet(path(alice.username), "*/*", undefined, res => res.text());
|
||||
const _body: unknown = content.body;
|
||||
// JSONフィードのときは改めて文字列化する
|
||||
const body: string = typeof (_body) === "object" ? JSON.stringify(_body) : _body as string;
|
||||
|
||||
if (body.includes("**a**")) {
|
||||
throw new Error("MFM shouldn't be included");
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe.each([{ path: '/api/foo' }])('$path', ({ path }) => {
|
||||
|
@@ -7,6 +7,8 @@
|
||||
// pnpm jest -- e2e/timelines.ts
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Redis } from 'ioredis';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js';
|
||||
|
||||
function genHost() {
|
||||
@@ -17,7 +19,13 @@ function waitForPushToTl() {
|
||||
return sleep(500);
|
||||
}
|
||||
|
||||
let redisForTimelines: Redis;
|
||||
|
||||
describe('Timelines', () => {
|
||||
beforeAll(() => {
|
||||
redisForTimelines = new Redis(loadConfig().redisForTimelines);
|
||||
});
|
||||
|
||||
describe('Home TL', () => {
|
||||
test.concurrent('自分の visibility: followers なノートが含まれる', async () => {
|
||||
const [alice] = await Promise.all([signup()]);
|
||||
@@ -1272,6 +1280,33 @@ describe('Timelines', () => {
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
/** @see https://github.com/misskey-dev/misskey/issues/14000 */
|
||||
test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId による絞り込みが正しく動作する', async () => {
|
||||
const alice = await signup();
|
||||
const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' });
|
||||
const note1 = await post(alice, { text: '1' });
|
||||
const note2 = await post(alice, { text: '2' });
|
||||
await redisForTimelines.del('list:userTimeline:' + alice.id);
|
||||
const note3 = await post(alice, { text: '3' });
|
||||
|
||||
const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id });
|
||||
assert.deepStrictEqual(res.body, [note1, note2, note3]);
|
||||
});
|
||||
|
||||
test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId と untilId による絞り込みが正しく動作する', async () => {
|
||||
const alice = await signup();
|
||||
const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' });
|
||||
const note1 = await post(alice, { text: '1' });
|
||||
const note2 = await post(alice, { text: '2' });
|
||||
await redisForTimelines.del('list:userTimeline:' + alice.id);
|
||||
const note3 = await post(alice, { text: '3' });
|
||||
const noteUntil = await post(alice, { text: 'Note where id will be `untilId`.' });
|
||||
await post(alice, { text: '4' });
|
||||
|
||||
const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id, untilId: noteUntil.id });
|
||||
assert.deepStrictEqual(res.body, [note3, note2, note1]);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: リノートミュート済みユーザーのテスト
|
||||
|
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { just, nothing } from '../../src/misc/prelude/maybe.js';
|
||||
|
||||
describe('just', () => {
|
||||
test('has a value', () => {
|
||||
assert.deepStrictEqual(just(3).isJust(), true);
|
||||
});
|
||||
|
||||
test('has the inverse called get', () => {
|
||||
assert.deepStrictEqual(just(3).get(), 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nothing', () => {
|
||||
test('has no value', () => {
|
||||
assert.deepStrictEqual(nothing().isJust(), false);
|
||||
});
|
||||
});
|
@@ -17,6 +17,7 @@ import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/val
|
||||
import { entities } from '../src/postgres.js';
|
||||
import { loadConfig } from '../src/config.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import { type Response } from 'node-fetch';
|
||||
|
||||
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
|
||||
|
||||
@@ -454,7 +455,7 @@ export type SimpleGetResponse = {
|
||||
type: string | null,
|
||||
location: string | null
|
||||
};
|
||||
export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined): Promise<SimpleGetResponse> => {
|
||||
export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined, bodyExtractor: (res: Response) => Promise<string | null> = _ => Promise.resolve(null)): Promise<SimpleGetResponse> => {
|
||||
const res = await relativeFetch(path, {
|
||||
headers: {
|
||||
Accept: accept,
|
||||
@@ -482,7 +483,7 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
|
||||
const body =
|
||||
jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() :
|
||||
htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) :
|
||||
null;
|
||||
await bodyExtractor(res);
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
|
Reference in New Issue
Block a user