feat(threads): thread and forum API, admin, schemas and docs (#3251)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-07 15:25:12 +02:00
1 parent 5f4295e399
commit 3f4160b138
495 files changed
+39232 -1215

No files matched your search

@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {viewerFromCtx} from '@app/api/experiment/ChannelThreadsGate';
import {RequireEmailAccountIdentity} from '@app/api/middleware/AccountIdentityMiddleware';
import {DefaultUserOnly, LoginRequired} from '@app/api/middleware/AuthMiddleware';
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
@@ -40,6 +41,7 @@ export function ReportController(app: HonoApp) {
return ctx.json(
await ctx.get('reportRequestService').reportMessage({
user: ctx.get('user'),
viewer: viewerFromCtx(ctx),
data: ctx.req.valid('json'),
}),
);
@@ -2,6 +2,7 @@
import {requireEmailVerified} from '@app/api/auth/EmailVerificationUtils';
import {createChannelID, createGuildID, createInviteCode, createMessageID, createUserID} from '@app/api/BrandedTypes';
import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate';
import type {User} from '@app/api/models/User';
import {type ReportStatus, reportStatusToString} from '@app/api/report/IReportRepository';
import type {ReportService} from '@app/api/report/ReportService';
@@ -36,10 +37,15 @@ interface ReportRecord {
export class ReportRequestService {
constructor(private reportService: ReportService) {}
async reportMessage({user, data}: ReportUserRequestContext<ReportMessageRequest>): Promise<ReportResponse> {
async reportMessage({
user,
viewer,
data,
}: ReportUserRequestContext<ReportMessageRequest> & {viewer: ThreadViewer}): Promise<ReportResponse> {
this.requireVerifiedAccount(user);
const report = await this.reportService.reportMessage(
this.createReporter(user),
viewer,
createChannelID(data.channel_id),
createMessageID(data.message_id),
data.category,
+18 -5
View File
@@ -23,8 +23,10 @@ import {
computeEffectiveContentWarning,
guildToContentWarningView,
} from '@app/api/channel/utils/EffectiveContentWarning';
import {resolveNsfwScopeChannel} from '@app/api/channel/utils/ThreadNsfwScope';
import type {MessageAttachment} from '@app/api/database/types/MessageTypes';
import type {DSAReportTicketRow} from '@app/api/database/types/ReportTypes';
import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import {emitActivity} from '@app/api/infrastructure/activity/ActivityEvents';
import type {ReportOutcome, ReportTarget, ResolvedBy} from '@app/api/infrastructure/activity/Contract.generated';
@@ -148,6 +150,7 @@ export class ReportService {
async reportMessage(
reporter: ReporterMetadata,
viewer: ThreadViewer,
channelId: ChannelID,
messageId: MessageID,
category: string,
@@ -157,6 +160,7 @@ export class ReportService {
await this.ensureReportRateLimit(this.createReportRateLimitIdentifier(reporterKey), REPORT_RATE_LIMIT_MAX, false);
const {authChannel, channel, message} = await this.getReportableMessageForReporter({
reporterId: reporter.id,
viewer,
channelId,
messageId,
});
@@ -628,10 +632,12 @@ export class ReportService {
private async getReportableMessageForReporter({
reporterId,
viewer,
channelId,
messageId,
}: {
reporterId: UserID | null;
viewer: ThreadViewer;
channelId: ChannelID;
messageId: MessageID;
}): Promise<{
@@ -642,7 +648,11 @@ export class ReportService {
if (!reporterId) {
throw new UnknownChannelError();
}
const authChannel = await this.messageChannelAuthService.getChannelAuthenticated({userId: reporterId, channelId});
const authChannel = await this.messageChannelAuthService.getChannelAuthenticated({
userId: reporterId,
channelId,
viewer,
});
if (!(await this.canAccessMessage(authChannel, messageId))) {
throw new UnknownMessageError();
}
@@ -720,9 +730,12 @@ export class ReportService {
};
}
const guildView = guildToContentWarningView(guild);
const scope = channel
? await resolveNsfwScopeChannel(channel, (channelId) => this.channelRepository.findUnique(channelId))
: null;
let parentCategoryView: ContentWarningChannelLike | null = null;
if (channel?.parentId) {
const parent = await this.channelRepository.findUnique(channel.parentId);
if (scope?.parentId) {
const parent = await this.channelRepository.findUnique(scope.parentId);
if (parent) {
parentCategoryView = channelToContentWarningView(parent);
}
@@ -730,8 +743,8 @@ export class ReportService {
let effectiveNsfw: boolean | null = null;
let effectiveLevel: number | null = null;
let effectiveText: string | null = null;
if (channel) {
const channelView = channelToContentWarningView(channel);
if (scope) {
const channelView = channelToContentWarningView(scope);
effectiveNsfw = computeEffectiveChannelNsfw(channelView, parentCategoryView, guildView);
const effective = computeEffectiveContentWarning(channelView, parentCategoryView, guildView);
effectiveLevel = effective.level;