Merge branch 'develop' into upstream/2025.5.0

This commit is contained in:
dakkar 2025-06-20 10:44:34 +01:00
commit 886160bdec
52 changed files with 1519 additions and 630 deletions

View file

@ -221,6 +221,10 @@ export const meta = {
},
},
},
signupReason: {
type: 'string',
optional: false, nullable: true,
},
},
},
} as const;

View file

@ -793,9 +793,29 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const after = await this.metaService.fetch(true);
this.moderationLogService.log(me, 'updateServerSettings', {
before,
after,
before: sanitize(before),
after: sanitize(after),
});
});
}
}
function sanitize(meta: Partial<MiMeta>): Partial<MiMeta> {
return {
...meta,
hcaptchaSecretKey: '<redacted>',
mcaptchaSecretKey: '<redacted>',
recaptchaSecretKey: '<redacted>',
turnstileSecretKey: '<redacted>',
fcSecretKey: '<redacted>',
smtpPass: '<redacted>',
swPrivateKey: '<redacted>',
objectStorageAccessKey: '<redacted>',
objectStorageSecretKey: '<redacted>',
deeplAuthKey: '<redacted>',
libreTranslateKey: '<redacted>',
verifymailAuthKey: '<redacted>',
truemailAuthKey: '<redacted>',
};
}

View file

@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { getJsonSchema } from '@/core/chart/core.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { schema } from '@/core/chart/charts/entities/per-user-following.js';
import { CacheService } from '@/core/CacheService.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['charts', 'users', 'following'],
@ -40,9 +42,84 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private perUserFollowingChart: PerUserFollowingChart,
private readonly cacheService: CacheService,
private readonly roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
return await this.perUserFollowingChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId);
const profile = await this.cacheService.userProfileCache.fetch(ps.userId);
// These are structured weird to avoid un-necessary calls to roleService and cacheService
const iAmModeratorOrTarget = me && (me.id === ps.userId || await this.roleService.isModerator(me));
const iAmFollowingOrTarget = me && (me.id === ps.userId || await this.cacheService.isFollowing(me.id, ps.userId));
const canViewFollowing =
profile.followingVisibility === 'public'
|| iAmModeratorOrTarget
|| (profile.followingVisibility === 'followers' && iAmFollowingOrTarget);
const canViewFollowers =
profile.followersVisibility === 'public'
|| iAmModeratorOrTarget
|| (profile.followersVisibility === 'followers' && iAmFollowingOrTarget);
if (!canViewFollowing && !canViewFollowers) {
return {
local: {
followings: {
total: [],
inc: [],
dec: [],
},
followers: {
total: [],
inc: [],
dec: [],
},
},
remote: {
followings: {
total: [],
inc: [],
dec: [],
},
followers: {
total: [],
inc: [],
dec: [],
},
},
};
}
const chart = await this.perUserFollowingChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId);
if (!canViewFollowers) {
chart.local.followers = {
total: [],
inc: [],
dec: [],
};
chart.remote.followers = {
total: [],
inc: [],
dec: [],
};
}
if (!canViewFollowing) {
chart.local.followings = {
total: [],
inc: [],
dec: [],
};
chart.remote.followings = {
total: [],
inc: [],
dec: [],
};
}
return chart;
});
}
}

View file

@ -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 notification = notifications[i];
const prev = notifications[i - 1];
let prevGroupedNotification = groupedNotifications.at(-1)!;
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>();
if (prev.type === 'reaction' && notification.type === 'reaction' && prev.noteId === notification.noteId) {
if (prevGroupedNotification.type !== 'reaction:grouped') {
groupedNotifications[groupedNotifications.length - 1] = {
// 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];
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] = {
type: 'renote:grouped',
id: '',
createdAt: notification.createdAt,
noteId: prev.noteId!,
userIds: [prev.notifierId!],
};
prevGroupedNotification = groupedNotifications.at(-1)!;
}
(prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'renote:grouped'>).userIds.push(notification.notifierId!);
prevGroupedNotification.id = notification.id;
continue;
}
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: prevRenote.id, // this will be the newest id in this group
createdAt: prevRenote.createdAt,
noteId: prevRenote.noteId!,
userIds: [prevRenote.notifierId!],
};
}
// 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);
});