Merge branch 'develop' into upstream/2025.5.0
This commit is contained in:
commit
886160bdec
52 changed files with 1519 additions and 630 deletions
|
|
@ -33,7 +33,7 @@ import type Logger from '@/logger.js';
|
|||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IActivity, IAnnounce, ICreate } from '@/core/activitypub/type.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
||||
|
|
@ -571,7 +571,7 @@ export class ActivityPubServerService {
|
|||
|
||||
const pinnedNotes = (await Promise.all(pinings.map(pining =>
|
||||
this.notesRepository.findOneByOrFail({ id: pining.noteId }))))
|
||||
.filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility));
|
||||
.filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility) && !isPureRenote(note));
|
||||
|
||||
const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note, user)));
|
||||
|
||||
|
|
@ -791,6 +791,10 @@ export class ActivityPubServerService {
|
|||
reply.header('Access-Control-Allow-Origin', '*');
|
||||
reply.header('Access-Control-Expose-Headers', 'Vary');
|
||||
|
||||
// Tell crawlers not to index AP endpoints.
|
||||
// https://developers.google.com/search/docs/crawling-indexing/block-indexing
|
||||
reply.header('X-Robots-Tag', 'noindex');
|
||||
|
||||
/* tell any caching proxy that they should not cache these
|
||||
responses: we wouldn't want the proxy to return a 403 to
|
||||
someone presenting a valid signature, or return a cached
|
||||
|
|
@ -838,6 +842,11 @@ export class ActivityPubServerService {
|
|||
return;
|
||||
}
|
||||
|
||||
// Boosts don't federate directly - they should only be referenced as an activity
|
||||
if (isPureRenote(note)) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
this.setResponseType(request, reply);
|
||||
|
||||
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ export class FileServerService {
|
|||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\'');
|
||||
reply.header('Access-Control-Allow-Origin', '*');
|
||||
|
||||
// Tell crawlers not to index files endpoints.
|
||||
// https://developers.google.com/search/docs/crawling-indexing/block-indexing
|
||||
reply.header('X-Robots-Tag', 'noindex');
|
||||
done();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,10 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||
reply: FastifyReply,
|
||||
): void {
|
||||
// Tell crawlers not to index API endpoints.
|
||||
// https://developers.google.com/search/docs/crawling-indexing/block-indexing
|
||||
reply.header('X-Robots-Tag', 'noindex');
|
||||
|
||||
const body = request.method === 'GET'
|
||||
? request.query
|
||||
: request.body;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -71,6 +71,13 @@ export class MastodonApiServerService {
|
|||
done();
|
||||
});
|
||||
|
||||
// Tell crawlers not to index API endpoints.
|
||||
// https://developers.google.com/search/docs/crawling-indexing/block-indexing
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.header('X-Robots-Tag', 'noindex');
|
||||
done();
|
||||
});
|
||||
|
||||
// External endpoints
|
||||
this.apiAccountMastodon.register(fastify);
|
||||
this.apiAppsMastodon.register(fastify);
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@ export class UrlPreviewService {
|
|||
reply: FastifyReply,
|
||||
): Promise<void> {
|
||||
if (!this.meta.urlPreviewEnabled) {
|
||||
// Tell crawlers not to index URL previews.
|
||||
// https://developers.google.com/search/docs/crawling-indexing/block-indexing
|
||||
reply.header('X-Robots-Tag', 'noindex');
|
||||
|
||||
return reply.code(403).send({
|
||||
error: {
|
||||
message: 'URL preview is disabled',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue