Merge branch 'develop' into upstream/2025.5.0
This commit is contained in:
commit
886160bdec
52 changed files with 1519 additions and 630 deletions
|
|
@ -221,6 +221,10 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
},
|
||||
signupReason: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -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>',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue