refactor progress
This commit is contained in:
181
packages/api/src/channel/controllers/CallController.tsx
Normal file
181
packages/api/src/channel/controllers/CallController.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createChannelID, createUserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {DefaultUserOnly, LoginRequired} from '@fluxer/api/src/middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {Validator} from '@fluxer/api/src/Validator';
|
||||
import {CallRingBodySchema, CallUpdateBodySchema} from '@fluxer/schema/src/domains/channel/ChannelRequestSchemas';
|
||||
import {CallEligibilityResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import {ChannelIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
|
||||
export function CallController(app: HonoApp) {
|
||||
app.get(
|
||||
'/channels/:channel_id/call',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_CALL_GET),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', ChannelIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'get_call_eligibility',
|
||||
summary: 'Get call eligibility status',
|
||||
description:
|
||||
'Checks whether a call can be initiated in the channel and if there is an active call. Returns ringable status and silent mode flag.',
|
||||
responseSchema: CallEligibilityResponse,
|
||||
statusCode: 200,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const channelService = ctx.get('channelService');
|
||||
|
||||
const {ringable, silent} = await channelService.checkCallEligibility({userId, channelId});
|
||||
return ctx.json({ringable, silent: !!silent});
|
||||
},
|
||||
);
|
||||
|
||||
app.patch(
|
||||
'/channels/:channel_id/call',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_CALL_UPDATE),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', ChannelIdParam),
|
||||
Validator('json', CallUpdateBodySchema),
|
||||
OpenAPI({
|
||||
operationId: 'update_call_region',
|
||||
summary: 'Update call region',
|
||||
description: 'Changes the voice server region for an active call to optimise latency and connection quality.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const {region} = ctx.req.valid('json');
|
||||
const channelService = ctx.get('channelService');
|
||||
|
||||
await channelService.updateCall({userId, channelId, region});
|
||||
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/channels/:channel_id/call/ring',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_CALL_RING),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', ChannelIdParam),
|
||||
Validator('json', CallRingBodySchema),
|
||||
OpenAPI({
|
||||
operationId: 'ring_call_recipients',
|
||||
summary: 'Ring call recipients',
|
||||
description:
|
||||
'Sends ringing notifications to specified users in a call. If no recipients are specified, rings all channel members.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const {recipients} = ctx.req.valid('json');
|
||||
const channelService = ctx.get('channelService');
|
||||
const requestCache = ctx.get('requestCache');
|
||||
|
||||
const recipientIds = recipients ? recipients.map(createUserID) : undefined;
|
||||
|
||||
await channelService.ringCallRecipients({
|
||||
userId,
|
||||
channelId,
|
||||
recipients: recipientIds,
|
||||
requestCache,
|
||||
});
|
||||
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/channels/:channel_id/call/stop-ringing',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_CALL_STOP_RINGING),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', ChannelIdParam),
|
||||
Validator('json', CallRingBodySchema),
|
||||
OpenAPI({
|
||||
operationId: 'stop_ringing_call_recipients',
|
||||
summary: 'Stop ringing call recipients',
|
||||
description:
|
||||
'Stops ringing notifications for specified users in a call. Allows callers to stop notifying users who have declined or not responded.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const {recipients} = ctx.req.valid('json');
|
||||
const channelService = ctx.get('channelService');
|
||||
|
||||
const recipientIds = recipients ? recipients.map(createUserID) : undefined;
|
||||
|
||||
await channelService.stopRingingCallRecipients({
|
||||
userId,
|
||||
channelId,
|
||||
recipients: recipientIds,
|
||||
});
|
||||
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/channels/:channel_id/call/end',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_CALL_UPDATE),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', ChannelIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'end_call',
|
||||
summary: 'End call session',
|
||||
description: 'Terminates an active voice call in the channel. Records the call end state for all participants.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
|
||||
ctx.get('channelService').recordCallEnded({channelId});
|
||||
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
}
|
||||
305
packages/api/src/channel/controllers/ChannelController.tsx
Normal file
305
packages/api/src/channel/controllers/ChannelController.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createChannelID, createUserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {DefaultUserOnly, LoginRequired} from '@fluxer/api/src/middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
|
||||
import type {HonoApp, HonoEnv} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {Validator} from '@fluxer/api/src/Validator';
|
||||
import {
|
||||
ChannelUpdateRequest,
|
||||
DeleteChannelQuery,
|
||||
PermissionOverwriteCreateRequest,
|
||||
} from '@fluxer/schema/src/domains/channel/ChannelRequestSchemas';
|
||||
import {ChannelResponse, RtcRegionResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import {
|
||||
ChannelIdOverwriteIdParam,
|
||||
ChannelIdParam,
|
||||
ChannelIdUserIdParam,
|
||||
} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import type {Context} from 'hono';
|
||||
import {z} from 'zod';
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function ChannelController(app: HonoApp) {
|
||||
app.get(
|
||||
'/channels/:channel_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_GET),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'get_channel',
|
||||
summary: 'Fetch a channel',
|
||||
description:
|
||||
'Retrieves the channel object including metadata, member list, and settings. Requires the user to be a member of the channel with view permissions.',
|
||||
responseSchema: ChannelResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
const channelRequestService = ctx.get('channelRequestService');
|
||||
return ctx.json(
|
||||
await channelRequestService.getChannelResponse({
|
||||
userId,
|
||||
channelId,
|
||||
requestCache,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
'/channels/:channel_id/rtc-regions',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_GET),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', ChannelIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'list_rtc_regions',
|
||||
summary: 'List RTC regions',
|
||||
description:
|
||||
'Returns available voice and video calling regions for the channel, used to optimise connection quality. Requires membership with call permissions.',
|
||||
responseSchema: z.array(RtcRegionResponse),
|
||||
statusCode: 200,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const channelRequestService = ctx.get('channelRequestService');
|
||||
return ctx.json(await channelRequestService.listRtcRegions({userId, channelId}));
|
||||
},
|
||||
);
|
||||
|
||||
app.patch(
|
||||
'/channels/:channel_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_UPDATE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam, {
|
||||
post: async (result, ctx: Context<HonoEnv>) => {
|
||||
if (!result.success) {
|
||||
return undefined;
|
||||
}
|
||||
const channelId = createChannelID(result.data.channel_id);
|
||||
const existing = await ctx.get('channelService').getChannel({
|
||||
userId: ctx.get('user').id,
|
||||
channelId,
|
||||
});
|
||||
ctx.set('channelUpdateType', existing.type);
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
Validator('json', ChannelUpdateRequest, {
|
||||
pre: async (raw: unknown, ctx: Context<HonoEnv>) => {
|
||||
const channelType = ctx.get('channelUpdateType');
|
||||
if (channelType === undefined) {
|
||||
throw new Error('Missing channel type for update validation');
|
||||
}
|
||||
const body = isPlainObject(raw) ? raw : {};
|
||||
return {...body, type: channelType};
|
||||
},
|
||||
}),
|
||||
OpenAPI({
|
||||
operationId: 'update_channel',
|
||||
summary: 'Update channel settings',
|
||||
description:
|
||||
'Modifies channel properties such as name, description, topic, nsfw flag, and slowmode. Requires management permissions in the channel.',
|
||||
responseSchema: ChannelResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const data = ctx.req.valid('json');
|
||||
const requestCache = ctx.get('requestCache');
|
||||
const channelRequestService = ctx.get('channelRequestService');
|
||||
return ctx.json(
|
||||
await channelRequestService.updateChannel({
|
||||
userId,
|
||||
channelId,
|
||||
data,
|
||||
requestCache,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_DELETE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam),
|
||||
Validator('query', DeleteChannelQuery),
|
||||
OpenAPI({
|
||||
operationId: 'delete_channel',
|
||||
summary: 'Delete a channel',
|
||||
description:
|
||||
'Permanently removes a channel and all its content. Only server administrators or the channel owner can delete channels.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const {silent} = ctx.req.valid('query');
|
||||
const requestCache = ctx.get('requestCache');
|
||||
const channelRequestService = ctx.get('channelRequestService');
|
||||
await channelRequestService.deleteChannel({userId, channelId, requestCache, silent});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.put(
|
||||
'/channels/:channel_id/recipients/:user_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_UPDATE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdUserIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'add_group_dm_recipient',
|
||||
summary: 'Add recipient to group DM',
|
||||
description:
|
||||
'Adds a user to a group direct message channel. The requesting user must be a member of the group DM.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const recipientId = createUserID(ctx.req.valid('param').user_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
|
||||
await ctx.get('channelService').groupDms.addRecipientToChannel({
|
||||
userId,
|
||||
channelId,
|
||||
recipientId,
|
||||
requestCache,
|
||||
});
|
||||
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id/recipients/:user_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_UPDATE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdUserIdParam),
|
||||
Validator('query', DeleteChannelQuery),
|
||||
OpenAPI({
|
||||
operationId: 'remove_group_dm_recipient',
|
||||
summary: 'Remove recipient from group DM',
|
||||
description:
|
||||
'Removes a user from a group direct message channel. The requesting user must be a member with appropriate permissions.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const recipientId = createUserID(ctx.req.valid('param').user_id);
|
||||
const {silent} = ctx.req.valid('query');
|
||||
const requestCache = ctx.get('requestCache');
|
||||
await ctx
|
||||
.get('channelService')
|
||||
.removeRecipientFromChannel({userId, channelId, recipientId, requestCache, silent});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.put(
|
||||
'/channels/:channel_id/permissions/:overwrite_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_UPDATE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdOverwriteIdParam),
|
||||
Validator('json', PermissionOverwriteCreateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'set_channel_permission_overwrite',
|
||||
summary: 'Set permission overwrite for channel',
|
||||
description:
|
||||
'Creates or updates permission overrides for a role or user in the channel. Allows fine-grained control over who can view, send messages, or manage the channel.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const overwriteId = ctx.req.valid('param').overwrite_id;
|
||||
const data = ctx.req.valid('json');
|
||||
const requestCache = ctx.get('requestCache');
|
||||
|
||||
await ctx.get('channelService').setChannelPermissionOverwrite({
|
||||
userId,
|
||||
channelId,
|
||||
overwriteId,
|
||||
overwrite: {
|
||||
type: data.type,
|
||||
allow_: data.allow ? data.allow : 0n,
|
||||
deny_: data.deny ? data.deny : 0n,
|
||||
},
|
||||
requestCache,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id/permissions/:overwrite_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_UPDATE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdOverwriteIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'delete_channel_permission_overwrite',
|
||||
summary: 'Delete permission overwrite',
|
||||
description:
|
||||
'Removes a permission override from a role or user in the channel, restoring default permissions. Requires channel management rights.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const overwriteId = ctx.req.valid('param').overwrite_id;
|
||||
const requestCache = ctx.get('requestCache');
|
||||
await ctx.get('channelService').deleteChannelPermissionOverwrite({userId, channelId, overwriteId, requestCache});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
}
|
||||
386
packages/api/src/channel/controllers/MessageController.tsx
Normal file
386
packages/api/src/channel/controllers/MessageController.tsx
Normal file
@@ -0,0 +1,386 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createAttachmentID, createChannelID, createMessageID} from '@fluxer/api/src/BrandedTypes';
|
||||
import type {MessageRequest, MessageUpdateRequest} from '@fluxer/api/src/channel/MessageTypes';
|
||||
import {parseMultipartMessageData} from '@fluxer/api/src/channel/services/message/MessageRequestParser';
|
||||
import {DefaultUserOnly, LoginRequired} from '@fluxer/api/src/middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {parseJsonPreservingLargeIntegers} from '@fluxer/api/src/utils/LosslessJsonParser';
|
||||
import {Validator} from '@fluxer/api/src/Validator';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
|
||||
import {
|
||||
ChannelIdMessageIdAttachmentIdParam,
|
||||
ChannelIdMessageIdParam,
|
||||
ChannelIdParam,
|
||||
} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {
|
||||
BulkDeleteMessagesRequest,
|
||||
MessageAckRequest,
|
||||
MessageRequestSchema,
|
||||
MessagesQuery,
|
||||
MessageUpdateRequestSchema,
|
||||
} from '@fluxer/schema/src/domains/message/MessageRequestSchemas';
|
||||
import {MessageResponseSchema} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import {z} from 'zod';
|
||||
|
||||
export function MessageController(app: HonoApp) {
|
||||
app.get(
|
||||
'/channels/:channel_id/messages',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_MESSAGES_GET),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam),
|
||||
Validator('query', MessagesQuery),
|
||||
OpenAPI({
|
||||
operationId: 'list_messages',
|
||||
summary: 'List messages in a channel',
|
||||
responseSchema: z.array(MessageResponseSchema),
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Channels', 'Messages'],
|
||||
description:
|
||||
'Retrieves a paginated list of messages from a channel. User must have permission to view the channel. Supports pagination via limit, before, after, and around parameters. Returns messages in reverse chronological order (newest first).',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const {limit, before, after, around} = ctx.req.valid('query');
|
||||
const requestCache = ctx.get('requestCache');
|
||||
const messageRequestService = ctx.get('messageRequestService');
|
||||
return ctx.json(
|
||||
await messageRequestService.listMessages({
|
||||
userId,
|
||||
channelId,
|
||||
query: {
|
||||
limit,
|
||||
before: before ? createMessageID(before) : undefined,
|
||||
after: after ? createMessageID(after) : undefined,
|
||||
around: around ? createMessageID(around) : undefined,
|
||||
},
|
||||
requestCache,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
'/channels/:channel_id/messages/:message_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_MESSAGE_GET),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'get_message',
|
||||
summary: 'Fetch a message',
|
||||
responseSchema: MessageResponseSchema,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Channels', 'Messages'],
|
||||
description:
|
||||
'Retrieves a specific message by ID. User must have permission to view the channel and the message must exist. Returns full message details including content, author, reactions, and attachments.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id} = ctx.req.valid('param');
|
||||
const user = ctx.get('user');
|
||||
const userId = user.id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
const messageRequestService = ctx.get('messageRequestService');
|
||||
return ctx.json(
|
||||
await messageRequestService.getMessage({
|
||||
userId,
|
||||
channelId,
|
||||
messageId,
|
||||
requestCache,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/channels/:channel_id/messages',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_MESSAGE_CREATE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'send_message',
|
||||
summary: 'Send a message',
|
||||
responseSchema: MessageResponseSchema,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Channels', 'Messages'],
|
||||
description:
|
||||
'Sends a new message to a channel. Requires permission to send messages in the target channel. Supports text content, embeds, attachments (multipart), and mentions. Returns the created message object with full details.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
const messageRequestService = ctx.get('messageRequestService');
|
||||
|
||||
const contentType = ctx.req.header('content-type');
|
||||
const validatedData = contentType?.includes('multipart/form-data')
|
||||
? ((await parseMultipartMessageData(ctx, user, channelId, MessageRequestSchema)) as MessageRequest)
|
||||
: await (async () => {
|
||||
let data: unknown;
|
||||
try {
|
||||
const raw = await ctx.req.text();
|
||||
data = raw.trim().length === 0 ? {} : parseJsonPreservingLargeIntegers(raw);
|
||||
} catch {
|
||||
throw InputValidationError.fromCode('message_data', ValidationErrorCodes.INVALID_MESSAGE_DATA);
|
||||
}
|
||||
const validationResult = MessageRequestSchema.safeParse(data);
|
||||
if (!validationResult.success) {
|
||||
throw InputValidationError.fromCode('message_data', ValidationErrorCodes.INVALID_MESSAGE_DATA);
|
||||
}
|
||||
return validationResult.data;
|
||||
})();
|
||||
return ctx.json(
|
||||
await messageRequestService.sendMessage({
|
||||
user,
|
||||
channelId,
|
||||
data: validatedData as MessageRequest,
|
||||
requestCache,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
app.patch(
|
||||
'/channels/:channel_id/messages/:message_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_MESSAGE_UPDATE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'edit_message',
|
||||
summary: 'Edit a message',
|
||||
responseSchema: MessageResponseSchema,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Channels', 'Messages'],
|
||||
description:
|
||||
'Updates an existing message. Only the message author can edit messages (or admins with proper permissions). Supports updating content, embeds, and attachments. Returns the updated message object. Maintains original message ID and timestamps.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id} = ctx.req.valid('param');
|
||||
const user = ctx.get('user');
|
||||
const userId = user.id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
const messageRequestService = ctx.get('messageRequestService');
|
||||
|
||||
const contentType = ctx.req.header('content-type');
|
||||
const validatedData = contentType?.includes('multipart/form-data')
|
||||
? ((await parseMultipartMessageData(ctx, user, channelId, MessageUpdateRequestSchema)) as MessageUpdateRequest)
|
||||
: await (async () => {
|
||||
let data: unknown;
|
||||
try {
|
||||
const raw = await ctx.req.text();
|
||||
data = raw.trim().length === 0 ? {} : parseJsonPreservingLargeIntegers(raw);
|
||||
} catch {
|
||||
throw InputValidationError.fromCode('message_data', ValidationErrorCodes.INVALID_MESSAGE_DATA);
|
||||
}
|
||||
const validationResult = MessageUpdateRequestSchema.safeParse(data);
|
||||
if (!validationResult.success) {
|
||||
throw InputValidationError.fromCode('message_data', ValidationErrorCodes.INVALID_MESSAGE_DATA);
|
||||
}
|
||||
return validationResult.data;
|
||||
})();
|
||||
return ctx.json(
|
||||
await messageRequestService.editMessage({
|
||||
userId,
|
||||
channelId,
|
||||
messageId,
|
||||
data: validatedData as MessageUpdateRequest,
|
||||
requestCache,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id/messages/ack',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_READ_STATE_DELETE),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', ChannelIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'clear_channel_read_state',
|
||||
summary: 'Clear channel read state',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Channels', 'Messages'],
|
||||
description:
|
||||
'Clears all read state and acknowledgement records for a channel, marking all messages as unread. Only available for regular user accounts. Returns 204 No Content on success.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
await ctx.get('channelService').deleteReadState({userId, channelId});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id/messages/:message_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_MESSAGE_DELETE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'delete_message',
|
||||
summary: 'Delete a message',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Channels', 'Messages'],
|
||||
description:
|
||||
'Deletes a message permanently. Only the message author can delete messages (or admins/moderators with proper permissions). Cannot be undone. Returns 204 No Content on success.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id} = ctx.req.valid('param');
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
await ctx.get('channelService').deleteMessage({userId, channelId, messageId, requestCache});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id/messages/:message_id/attachments/:attachment_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_MESSAGE_DELETE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdAttachmentIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'delete_message_attachment',
|
||||
summary: 'Delete a message attachment',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Channels', 'Messages'],
|
||||
description:
|
||||
'Removes a specific attachment from a message while keeping the message intact. Only the message author can remove attachments (or admins/moderators). Returns 204 No Content on success.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id, attachment_id} = ctx.req.valid('param');
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const attachmentId = createAttachmentID(attachment_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
await ctx.get('channelService').deleteAttachment({
|
||||
userId,
|
||||
channelId,
|
||||
messageId: messageId,
|
||||
attachmentId,
|
||||
requestCache,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/channels/:channel_id/messages/bulk-delete',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_MESSAGE_BULK_DELETE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam),
|
||||
Validator('json', BulkDeleteMessagesRequest),
|
||||
OpenAPI({
|
||||
operationId: 'bulk_delete_messages',
|
||||
summary: 'Bulk delete messages',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Channels', 'Messages'],
|
||||
description:
|
||||
'Deletes multiple messages at once. Requires moderation or admin permissions. Commonly used for message cleanup. Messages from different authors can be deleted together. Returns 204 No Content on success.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const messageIds = ctx.req.valid('json').message_ids.map(createMessageID);
|
||||
await ctx.get('channelService').bulkDeleteMessages({userId, channelId, messageIds});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/channels/:channel_id/typing',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_TYPING),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'indicate_typing',
|
||||
summary: 'Indicate typing activity',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Channels', 'Messages'],
|
||||
description:
|
||||
'Notifies other users in the channel that you are actively typing. Typing indicators typically expire after a short period (usually 10 seconds). Returns 204 No Content. Commonly called repeatedly while the user is composing a message.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
await ctx.get('channelService').startTyping({userId, channelId});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/channels/:channel_id/messages/:message_id/ack',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_MESSAGE_ACK),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', ChannelIdMessageIdParam),
|
||||
Validator('json', MessageAckRequest),
|
||||
OpenAPI({
|
||||
operationId: 'acknowledge_message',
|
||||
summary: 'Acknowledge a message',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Channels', 'Messages'],
|
||||
description:
|
||||
'Marks a message as read and records acknowledgement state. Only available for regular user accounts. Updates mention count if provided. Returns 204 No Content on success.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id} = ctx.req.valid('param');
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const {mention_count: mentionCount, manual} = ctx.req.valid('json');
|
||||
await ctx.get('channelService').ackMessage({
|
||||
userId,
|
||||
channelId,
|
||||
messageId,
|
||||
mentionCount: mentionCount ?? 0,
|
||||
manual,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createChannelID, createMessageID, createUserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {isPersonalNotesChannel} from '@fluxer/api/src/channel/services/message/MessageHelpers';
|
||||
import {LoginRequired} from '@fluxer/api/src/middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {Validator} from '@fluxer/api/src/Validator';
|
||||
import {UnclaimedAccountCannotAddReactionsError} from '@fluxer/errors/src/domains/channel/UnclaimedAccountCannotAddReactionsError';
|
||||
import {
|
||||
ChannelIdMessageIdEmojiParam,
|
||||
ChannelIdMessageIdEmojiTargetIdParam,
|
||||
ChannelIdMessageIdParam,
|
||||
ChannelIdParam,
|
||||
SessionIdQuerySchema,
|
||||
} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {
|
||||
ChannelPinsQuerySchema,
|
||||
ReactionUsersQuerySchema,
|
||||
} from '@fluxer/schema/src/domains/message/MessageRequestSchemas';
|
||||
import {
|
||||
ChannelPinsResponse,
|
||||
ReactionUsersListResponse,
|
||||
} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
|
||||
export function MessageInteractionController(app: HonoApp) {
|
||||
app.get(
|
||||
'/channels/:channel_id/messages/pins',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_PINS),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam),
|
||||
Validator('query', ChannelPinsQuerySchema),
|
||||
OpenAPI({
|
||||
operationId: 'list_pinned_messages',
|
||||
summary: 'List pinned messages',
|
||||
description:
|
||||
'Retrieves a paginated list of messages pinned in a channel. User must have permission to view the channel. Supports pagination via limit and before parameters. Returns pinned messages with their pin timestamps.',
|
||||
responseSchema: ChannelPinsResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
const {limit, before} = ctx.req.valid('query');
|
||||
return ctx.json(
|
||||
await ctx
|
||||
.get('channelService')
|
||||
.getChannelPins({userId, channelId, requestCache, limit, beforeTimestamp: before}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/channels/:channel_id/pins/ack',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_PINS),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'acknowledge_pins',
|
||||
summary: 'Acknowledge new pin notifications',
|
||||
description:
|
||||
'Marks all new pin notifications in a channel as acknowledged. Clears the notification badge for pinned messages. Returns 204 No Content on success.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const {channel} = await ctx.get('channelService').getChannelAuthenticated({userId, channelId});
|
||||
const timestamp = channel.lastPinTimestamp;
|
||||
if (timestamp != null) {
|
||||
await ctx.get('channelService').ackPins({userId, channelId, timestamp});
|
||||
}
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.put(
|
||||
'/channels/:channel_id/pins/:message_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_PINS),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'pin_message',
|
||||
summary: 'Pin a message',
|
||||
description:
|
||||
'Pins a message to the channel. Requires permission to manage pins (typically moderator or higher). Pinned messages are highlighted and searchable. Returns 204 No Content on success.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id} = ctx.req.valid('param');
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
await ctx.get('channelService').pinMessage({
|
||||
userId,
|
||||
channelId,
|
||||
messageId,
|
||||
requestCache,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id/pins/:message_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_PINS),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'unpin_message',
|
||||
summary: 'Unpin a message',
|
||||
description:
|
||||
'Unpins a message from the channel. Requires permission to manage pins. The message remains in the channel but is no longer highlighted. Returns 204 No Content on success.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id} = ctx.req.valid('param');
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
await ctx.get('channelService').unpinMessage({
|
||||
userId,
|
||||
channelId,
|
||||
messageId,
|
||||
requestCache,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
'/channels/:channel_id/messages/:message_id/reactions/:emoji',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_REACTIONS),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdEmojiParam),
|
||||
Validator('query', ReactionUsersQuerySchema),
|
||||
OpenAPI({
|
||||
operationId: 'list_reaction_users',
|
||||
summary: 'List users who reacted with emoji',
|
||||
description:
|
||||
'Retrieves a paginated list of users who reacted to a message with a specific emoji. Supports pagination via limit and after parameters. Returns user objects for each reaction.',
|
||||
responseSchema: ReactionUsersListResponse,
|
||||
statusCode: 200,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id, emoji} = ctx.req.valid('param');
|
||||
const {limit, after} = ctx.req.valid('query');
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const afterUserId = after ? createUserID(after) : undefined;
|
||||
return ctx.json(
|
||||
await ctx
|
||||
.get('channelService')
|
||||
.getUsersForReaction({userId, channelId, messageId, emoji, limit, after: afterUserId}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
app.put(
|
||||
'/channels/:channel_id/messages/:message_id/reactions/:emoji/@me',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_REACTIONS),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdEmojiParam),
|
||||
Validator('query', SessionIdQuerySchema),
|
||||
OpenAPI({
|
||||
operationId: 'add_reaction',
|
||||
summary: 'Add reaction to message',
|
||||
description:
|
||||
'Adds an emoji reaction to a message. Each user can react once with each emoji. Cannot be used from unclaimed accounts outside personal notes. Returns 204 No Content on success.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id, emoji} = ctx.req.valid('param');
|
||||
const user = ctx.get('user');
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const sessionId = ctx.req.valid('query').session_id;
|
||||
const requestCache = ctx.get('requestCache');
|
||||
|
||||
if (user.isUnclaimedAccount() && !isPersonalNotesChannel({userId: user.id, channelId})) {
|
||||
throw new UnclaimedAccountCannotAddReactionsError();
|
||||
}
|
||||
|
||||
await ctx.get('channelService').addReaction({
|
||||
userId: user.id,
|
||||
sessionId,
|
||||
channelId,
|
||||
messageId,
|
||||
emoji,
|
||||
requestCache,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id/messages/:message_id/reactions/:emoji/@me',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_REACTIONS),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdEmojiParam),
|
||||
Validator('query', SessionIdQuerySchema),
|
||||
OpenAPI({
|
||||
operationId: 'remove_own_reaction',
|
||||
summary: 'Remove own reaction from message',
|
||||
description:
|
||||
"Removes your own emoji reaction from a message. Returns 204 No Content on success. Has no effect if you haven't reacted with that emoji.",
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id, emoji} = ctx.req.valid('param');
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const sessionId = ctx.req.valid('query').session_id;
|
||||
const requestCache = ctx.get('requestCache');
|
||||
await ctx.get('channelService').removeOwnReaction({
|
||||
userId,
|
||||
sessionId,
|
||||
channelId,
|
||||
messageId,
|
||||
emoji,
|
||||
requestCache,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id/messages/:message_id/reactions/:emoji/:target_id',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_REACTIONS),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdEmojiTargetIdParam),
|
||||
Validator('query', SessionIdQuerySchema),
|
||||
OpenAPI({
|
||||
operationId: 'remove_reaction',
|
||||
summary: 'Remove reaction from message',
|
||||
description:
|
||||
"Removes a specific user's emoji reaction from a message. Requires moderator or higher permissions to remove reactions from other users. Returns 204 No Content on success.",
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id, emoji, target_id} = ctx.req.valid('param');
|
||||
const userId = ctx.get('user').id;
|
||||
const targetId = createUserID(target_id);
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
const sessionId = ctx.req.valid('query').session_id;
|
||||
const requestCache = ctx.get('requestCache');
|
||||
await ctx.get('channelService').removeReaction({
|
||||
userId,
|
||||
sessionId,
|
||||
channelId,
|
||||
messageId,
|
||||
emoji,
|
||||
targetId,
|
||||
requestCache,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id/messages/:message_id/reactions/:emoji',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_REACTIONS),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdEmojiParam),
|
||||
OpenAPI({
|
||||
operationId: 'remove_all_reactions_for_emoji',
|
||||
summary: 'Remove all reactions with emoji',
|
||||
description:
|
||||
"Removes all emoji reactions of a specific type from a message. All users' reactions with that emoji are deleted. Requires moderator or higher permissions. Returns 204 No Content on success.",
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id, emoji} = ctx.req.valid('param');
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
await ctx.get('channelService').removeAllReactionsForEmoji({userId, channelId, messageId, emoji});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
'/channels/:channel_id/messages/:message_id/reactions',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_REACTIONS),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdMessageIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'remove_all_reactions',
|
||||
summary: 'Remove all reactions from message',
|
||||
description:
|
||||
'Removes all emoji reactions from a message, regardless of emoji type or user. All reactions are permanently deleted. Requires moderator or higher permissions. Returns 204 No Content on success.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {channel_id, message_id} = ctx.req.valid('param');
|
||||
const userId = ctx.get('user').id;
|
||||
const channelId = createChannelID(channel_id);
|
||||
const messageId = createMessageID(message_id);
|
||||
await ctx.get('channelService').removeAllReactions({userId, channelId, messageId});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createChannelID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {parseScheduledMessageInput} from '@fluxer/api/src/channel/controllers/ScheduledMessageParsing';
|
||||
import {DefaultUserOnly, LoginRequired} from '@fluxer/api/src/middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {Validator} from '@fluxer/api/src/Validator';
|
||||
import {ChannelIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {ScheduledMessageResponseSchema} from '@fluxer/schema/src/domains/message/ScheduledMessageSchemas';
|
||||
|
||||
export function ScheduledMessageController(app: HonoApp) {
|
||||
app.post(
|
||||
'/channels/:channel_id/messages/schedule',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_MESSAGE_CREATE),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', ChannelIdParam),
|
||||
OpenAPI({
|
||||
operationId: 'schedule_message',
|
||||
summary: 'Schedule a message to send later',
|
||||
description:
|
||||
'Schedules a message to be sent at a specified time. Only available for regular user accounts. Requires permission to send messages in the target channel. Message is sent automatically at the scheduled time. Returns the scheduled message object with delivery time.',
|
||||
responseSchema: ScheduledMessageResponseSchema,
|
||||
statusCode: 201,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const scheduledMessageService = ctx.get('scheduledMessageService');
|
||||
|
||||
const {message, scheduledLocalAt, timezone} = await parseScheduledMessageInput({
|
||||
ctx,
|
||||
user,
|
||||
channelId,
|
||||
});
|
||||
|
||||
const scheduledMessage = await scheduledMessageService.createScheduledMessage({
|
||||
user,
|
||||
channelId,
|
||||
data: message,
|
||||
scheduledLocalAt,
|
||||
timezone,
|
||||
});
|
||||
|
||||
return ctx.json(scheduledMessage.toResponse(), 201);
|
||||
},
|
||||
);
|
||||
}
|
||||
102
packages/api/src/channel/controllers/ScheduledMessageParsing.tsx
Normal file
102
packages/api/src/channel/controllers/ScheduledMessageParsing.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {ChannelID} from '@fluxer/api/src/BrandedTypes';
|
||||
import type {MessageRequest} from '@fluxer/api/src/channel/MessageTypes';
|
||||
import {parseMultipartMessageData} from '@fluxer/api/src/channel/services/message/MessageRequestParser';
|
||||
import type {User} from '@fluxer/api/src/models/User';
|
||||
import type {HonoEnv} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {parseJsonPreservingLargeIntegers} from '@fluxer/api/src/utils/LosslessJsonParser';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
|
||||
import {MessageRequestSchema} from '@fluxer/schema/src/domains/message/MessageRequestSchemas';
|
||||
import {createStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import type {Context} from 'hono';
|
||||
import {ms} from 'itty-time';
|
||||
import type {z} from 'zod';
|
||||
|
||||
export const ScheduledMessageSchema = MessageRequestSchema.extend({
|
||||
scheduled_local_at: createStringType(1, 64),
|
||||
timezone: createStringType(1, 128),
|
||||
});
|
||||
|
||||
export type ScheduledMessageSchemaType = z.infer<typeof ScheduledMessageSchema>;
|
||||
|
||||
export function extractScheduleFields(data: ScheduledMessageSchemaType): {
|
||||
scheduled_local_at: string;
|
||||
timezone: string;
|
||||
message: MessageRequest;
|
||||
} {
|
||||
const {scheduled_local_at, timezone, ...messageData} = data;
|
||||
return {
|
||||
scheduled_local_at,
|
||||
timezone,
|
||||
message: messageData as MessageRequest,
|
||||
};
|
||||
}
|
||||
|
||||
export async function parseScheduledMessageInput({
|
||||
ctx,
|
||||
user,
|
||||
channelId,
|
||||
}: {
|
||||
ctx: Context<HonoEnv>;
|
||||
user: User;
|
||||
channelId: ChannelID;
|
||||
}): Promise<{message: MessageRequest; scheduledLocalAt: string; timezone: string}> {
|
||||
const contentType = ctx.req.header('content-type') ?? '';
|
||||
const isMultipart = contentType.includes('multipart/form-data');
|
||||
|
||||
if (isMultipart) {
|
||||
let parsedPayload: unknown = null;
|
||||
const message = (await parseMultipartMessageData(ctx, user, channelId, MessageRequestSchema, {
|
||||
uploadExpiresAt: new Date(Date.now() + ms('32 days')),
|
||||
onPayloadParsed(payload) {
|
||||
parsedPayload = payload;
|
||||
},
|
||||
})) as MessageRequest;
|
||||
|
||||
if (!parsedPayload) {
|
||||
throw InputValidationError.fromCode('scheduled_message', ValidationErrorCodes.FAILED_TO_PARSE_MULTIPART_PAYLOAD);
|
||||
}
|
||||
|
||||
const validation = ScheduledMessageSchema.safeParse(parsedPayload);
|
||||
if (!validation.success) {
|
||||
throw InputValidationError.fromCode('scheduled_message', ValidationErrorCodes.INVALID_SCHEDULED_MESSAGE_PAYLOAD);
|
||||
}
|
||||
|
||||
const {scheduled_local_at, timezone} = extractScheduleFields(validation.data);
|
||||
return {message, scheduledLocalAt: scheduled_local_at, timezone};
|
||||
}
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
const raw = await ctx.req.text();
|
||||
body = raw.trim().length === 0 ? {} : parseJsonPreservingLargeIntegers(raw);
|
||||
} catch {
|
||||
throw InputValidationError.fromCode('scheduled_message', ValidationErrorCodes.INVALID_SCHEDULED_MESSAGE_PAYLOAD);
|
||||
}
|
||||
const validation = ScheduledMessageSchema.safeParse(body);
|
||||
if (!validation.success) {
|
||||
throw InputValidationError.fromCode('scheduled_message', ValidationErrorCodes.INVALID_SCHEDULED_MESSAGE_PAYLOAD);
|
||||
}
|
||||
|
||||
const {scheduled_local_at, timezone, message} = extractScheduleFields(validation.data);
|
||||
return {message, scheduledLocalAt: scheduled_local_at, timezone};
|
||||
}
|
||||
123
packages/api/src/channel/controllers/StreamController.tsx
Normal file
123
packages/api/src/channel/controllers/StreamController.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createChannelID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {DefaultUserOnly, LoginRequired} from '@fluxer/api/src/middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {Validator} from '@fluxer/api/src/Validator';
|
||||
import {
|
||||
StreamPreviewUploadBodySchema,
|
||||
StreamUpdateBodySchema,
|
||||
} from '@fluxer/schema/src/domains/channel/ChannelRequestSchemas';
|
||||
import {StreamKeyParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
|
||||
export function StreamController(app: HonoApp) {
|
||||
app.patch(
|
||||
'/streams/:stream_key/stream',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_STREAM_UPDATE),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('json', StreamUpdateBodySchema),
|
||||
Validator('param', StreamKeyParam),
|
||||
OpenAPI({
|
||||
operationId: 'update_stream_region',
|
||||
summary: 'Update stream region',
|
||||
description:
|
||||
'Changes the media server region for an active stream. Used to optimise bandwidth and latency for streaming.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {region} = ctx.req.valid('json');
|
||||
const streamKey = ctx.req.valid('param').stream_key;
|
||||
await ctx.get('streamService').updateStreamRegion({streamKey, region});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
'/streams/:stream_key/preview',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_STREAM_PREVIEW_GET),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('param', StreamKeyParam),
|
||||
OpenAPI({
|
||||
operationId: 'get_stream_preview',
|
||||
summary: 'Get stream preview image',
|
||||
description:
|
||||
'Retrieves the current preview thumbnail for a stream. Returns the image with no-store cache headers to ensure freshness.',
|
||||
responseSchema: null,
|
||||
statusCode: 200,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const streamKey = ctx.req.valid('param').stream_key;
|
||||
const preview = await ctx.get('streamService').getPreview({streamKey, userId: user.id});
|
||||
if (!preview) {
|
||||
return ctx.body(null, 404);
|
||||
}
|
||||
const payload: ArrayBuffer = preview.buffer.slice().buffer;
|
||||
const headers = {
|
||||
'Content-Type': preview.contentType || 'image/jpeg',
|
||||
'Cache-Control': 'no-store, private',
|
||||
Pragma: 'no-cache',
|
||||
};
|
||||
return ctx.newResponse(payload, 200, headers);
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/streams/:stream_key/preview',
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_STREAM_PREVIEW_POST),
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
Validator('json', StreamPreviewUploadBodySchema),
|
||||
Validator('param', StreamKeyParam),
|
||||
OpenAPI({
|
||||
operationId: 'upload_stream_preview',
|
||||
summary: 'Upload stream preview image',
|
||||
description:
|
||||
'Uploads a custom thumbnail image for the stream. The image is scanned for content policy violations and stored securely.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
security: ['bearerToken', 'sessionToken'],
|
||||
tags: 'Channels',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const {thumbnail, channel_id, content_type} = ctx.req.valid('json');
|
||||
const streamKey = ctx.req.valid('param').stream_key;
|
||||
await ctx.get('streamService').uploadPreview({
|
||||
streamKey,
|
||||
channelId: createChannelID(channel_id),
|
||||
userId: user.id,
|
||||
thumbnail,
|
||||
contentType: content_type,
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
}
|
||||
35
packages/api/src/channel/controllers/index.tsx
Normal file
35
packages/api/src/channel/controllers/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {CallController} from '@fluxer/api/src/channel/controllers/CallController';
|
||||
import {ChannelController} from '@fluxer/api/src/channel/controllers/ChannelController';
|
||||
import {MessageController} from '@fluxer/api/src/channel/controllers/MessageController';
|
||||
import {MessageInteractionController} from '@fluxer/api/src/channel/controllers/MessageInteractionController';
|
||||
import {ScheduledMessageController} from '@fluxer/api/src/channel/controllers/ScheduledMessageController';
|
||||
import {StreamController} from '@fluxer/api/src/channel/controllers/StreamController';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
|
||||
export function registerChannelControllers(app: HonoApp) {
|
||||
ChannelController(app);
|
||||
MessageInteractionController(app);
|
||||
MessageController(app);
|
||||
ScheduledMessageController(app);
|
||||
CallController(app);
|
||||
StreamController(app);
|
||||
}
|
||||
Reference in New Issue
Block a user