summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/endpoints/i
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src/server/api/endpoints/i')
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/move.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications-grouped.ts87
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/get.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts19
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;