diff options
Diffstat (limited to 'packages/backend/src/server/api/endpoints/i')
5 files changed, 80 insertions, 36 deletions
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index d4098458d7..931c8d69b0 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- try { await this.userAuthService.twoFactorAuthenticate(profile, token); } catch (e) { - throw new Error('authentication failed'); + throw new Error('authentication failed', { cause: e }); } } diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 7852b5a2e1..e2a14b61af 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -17,7 +17,7 @@ import { ApiLoggerService } from '@/server/api/ApiLoggerService.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; - +import { renderInlineError } from '@/misc/render-inline-error.js'; import * as Acct from '@/misc/acct.js'; import { DI } from '@/di-symbols.js'; import { MiMeta } from '@/models/_.js'; @@ -105,7 +105,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const { username, host } = Acct.parse(ps.moveToAccount); // retrieve the destination account let moveTo = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => { - this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`); + this.apiLoggerService.logger.warn(`failed to resolve remote user: ${renderInlineError(e)}`); throw new ApiError(meta.errors.noSuchUser); }); const destination = await this.getterService.getUser(moveTo.id) as MiLocalUser | MiRemoteUser; diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts index b9c41b057d..444734070f 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts @@ -104,53 +104,88 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } // grouping - let groupedNotifications = [notifications[0]] as MiGroupedNotification[]; - for (let i = 1; i < notifications.length; i++) { + const groupedNotifications : MiGroupedNotification[] = []; + // keep track of where reaction / renote notifications are, by note id + const reactionIdxByNoteId = new Map<string, number>(); + const renoteIdxByNoteId = new Map<string, number>(); + + // group notifications by type+note; notice that we don't try to + // split groups if they span a long stretch of time, because + // it's probably overkill: if the user has very few + // notifications, there should be very little difference; if the + // user has many notifications, the pagination will break the + // groups + + // scan `notifications` newest-to-oldest + for (let i = 0; i < notifications.length; i++) { const notification = notifications[i]; - const prev = notifications[i - 1]; - let prevGroupedNotification = groupedNotifications.at(-1)!; - if (prev.type === 'reaction' && notification.type === 'reaction' && prev.noteId === notification.noteId) { - if (prevGroupedNotification.type !== 'reaction:grouped') { - groupedNotifications[groupedNotifications.length - 1] = { + if (notification.type === 'reaction') { + const reactionIdx = reactionIdxByNoteId.get(notification.noteId); + if (reactionIdx === undefined) { + // first reaction to this note that we see, add it as-is + // and remember where we put it + groupedNotifications.push(notification); + reactionIdxByNoteId.set(notification.noteId, groupedNotifications.length - 1); + continue; + } + + let prevReaction = groupedNotifications[reactionIdx] as FilterUnionByProperty<MiGroupedNotification, 'type', 'reaction:grouped'> | FilterUnionByProperty<MiGroupedNotification, 'type', 'reaction'>; + // if the previous reaction is not a group, make it into one + if (prevReaction.type !== 'reaction:grouped') { + prevReaction = groupedNotifications[reactionIdx] = { type: 'reaction:grouped', - id: '', - createdAt: prev.createdAt, - noteId: prev.noteId!, + id: prevReaction.id, // this will be the newest id in this group + createdAt: prevReaction.createdAt, + noteId: prevReaction.noteId!, reactions: [{ - userId: prev.notifierId!, - reaction: prev.reaction!, + userId: prevReaction.notifierId!, + reaction: prevReaction.reaction!, }], }; - prevGroupedNotification = groupedNotifications.at(-1)!; } - (prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'reaction:grouped'>).reactions.push({ + // add this new reaction to the existing group + (prevReaction as FilterUnionByProperty<MiGroupedNotification, 'type', 'reaction:grouped'>).reactions.push({ userId: notification.notifierId!, reaction: notification.reaction!, }); - prevGroupedNotification.id = notification.id; continue; } - if (prev.type === 'renote' && notification.type === 'renote' && prev.targetNoteId === notification.targetNoteId) { - if (prevGroupedNotification.type !== 'renote:grouped') { - groupedNotifications[groupedNotifications.length - 1] = { + + if (notification.type === 'renote') { + const renoteIdx = renoteIdxByNoteId.get(notification.targetNoteId); + if (renoteIdx === undefined) { + // first renote of this note that we see, add it as-is and + // remember where we put it + groupedNotifications.push(notification); + renoteIdxByNoteId.set(notification.targetNoteId, groupedNotifications.length - 1); + continue; + } + + let prevRenote = groupedNotifications[renoteIdx] as FilterUnionByProperty<MiGroupedNotification, 'type', 'renote:grouped'> | FilterUnionByProperty<MiGroupedNotification, 'type', 'renote'>; + // if the previous renote is not a group, make it into one + if (prevRenote.type !== 'renote:grouped') { + prevRenote = groupedNotifications[renoteIdx] = { type: 'renote:grouped', - id: '', - createdAt: notification.createdAt, - noteId: prev.noteId!, - userIds: [prev.notifierId!], + id: prevRenote.id, // this will be the newest id in this group + createdAt: prevRenote.createdAt, + noteId: prevRenote.noteId!, + userIds: [prevRenote.notifierId!], }; - prevGroupedNotification = groupedNotifications.at(-1)!; } - (prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'renote:grouped'>).userIds.push(notification.notifierId!); - prevGroupedNotification.id = notification.id; + // add this new renote to the existing group + (prevRenote as FilterUnionByProperty<MiGroupedNotification, 'type', 'renote:grouped'>).userIds.push(notification.notifierId!); continue; } + // not a groupable notification, just push it groupedNotifications.push(notification); } - groupedNotifications = groupedNotifications.slice(0, ps.limit); + // sort the groups by their id, newest first + groupedNotifications.sort( + (a, b) => a.id < b.id ? 1 : a.id > b.id ? -1 : 0, + ); return await this.notificationEntityService.packGroupedMany(groupedNotifications, me.id); }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index dff33016e0..d284334834 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -20,9 +20,7 @@ export const meta = { }, }, - res: { - type: 'object', - }, + res: {}, // 10 calls per 5 seconds limit: { diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index f35e395841..5767880531 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as mfm from '@transfem-org/sfm-js'; +import * as mfm from 'mfm-js'; import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; @@ -34,6 +34,7 @@ import { verifyFieldLinks } from '@/misc/verify-field-link.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { notificationRecieveConfig } from '@/models/json-schema/user.js'; import { userUnsignedFetchOptions } from '@/const.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -263,6 +264,15 @@ export const paramDef = { enum: userUnsignedFetchOptions, nullable: false, }, + attributionDomains: { + type: 'array', + items: { + type: 'string', + minLength: 1, + maxLength: 128, + }, + maxItems: 32, + }, }, } as const; @@ -373,6 +383,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; + if (ps.attributionDomains !== undefined) updates.attributionDomains = ps.attributionDomains; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; @@ -506,7 +517,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // Retrieve the old account const knownAs = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => { - this.apiLoggerService.logger.warn(`failed to resolve dstination user: ${e}`); + this.apiLoggerService.logger.warn(`failed to resolve destination user: ${renderInlineError(e)}`); throw new ApiError(meta.errors.noSuchUser); }); if (knownAs.id === _user.id) throw new ApiError(meta.errors.forbiddenToSetYourself); @@ -606,7 +617,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const updatedProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - this.cacheService.userProfileCache.set(user.id, updatedProfile); + await this.cacheService.userProfileCache.set(user.id, updatedProfile); // Publish meUpdated event this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj); @@ -663,7 +674,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // these two methods need to be kept in sync with // `ApRendererService.renderPerson` private userNeedsPublishing(oldUser: MiLocalUser, newUser: Partial<MiUser>): boolean { - const basicFields: (keyof MiUser)[] = ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs', 'hideOnlineStatus', 'enableRss', 'requireSigninToViewContents', 'makeNotesFollowersOnlyBefore', 'makeNotesHiddenBefore']; + const basicFields: (keyof MiUser)[] = ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs', 'hideOnlineStatus', 'enableRss', 'requireSigninToViewContents', 'makeNotesFollowersOnlyBefore', 'makeNotesHiddenBefore', 'attributionDomains']; for (const field of basicFields) { if ((field in newUser) && oldUser[field] !== newUser[field]) { return true; |