- Clone of github.com/fluxerapp/fluxer (official upstream) - SELF_HOSTING.md: full VM rebuild procedure, architecture overview, service reference, step-by-step setup, troubleshooting, seattle reference - dev/.env.example: all env vars with secrets redacted and generation instructions - dev/livekit.yaml: LiveKit config template with placeholder keys - fluxer-seattle/: existing seattle deployment setup scripts
179 lines
5.5 KiB
TypeScript
179 lines
5.5 KiB
TypeScript
/*
|
|
* 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 {
|
|
ChannelOverwriteTypes,
|
|
ChannelOverwriteTypesDescriptions,
|
|
ChannelTypes,
|
|
} from '@fluxer/constants/src/ChannelConstants';
|
|
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
|
import {
|
|
createInt32EnumType,
|
|
createNamedLiteralUnion,
|
|
MAX_STRING_PROCESSING_LENGTH,
|
|
normalizeString,
|
|
normalizeWhitespace,
|
|
stripInvisibles,
|
|
withOpenApiType,
|
|
withStringLengthRangeValidation,
|
|
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
|
import {z} from 'zod';
|
|
|
|
export const ChannelTypeSchema = withOpenApiType(
|
|
createInt32EnumType(
|
|
[
|
|
[ChannelTypes.GUILD_TEXT, 'GUILD_TEXT', 'A text channel within a guild'],
|
|
[ChannelTypes.DM, 'DM', 'A direct message between users'],
|
|
[ChannelTypes.GUILD_VOICE, 'GUILD_VOICE', 'A voice channel within a guild'],
|
|
[ChannelTypes.GROUP_DM, 'GROUP_DM', 'A group direct message between users'],
|
|
[ChannelTypes.GUILD_CATEGORY, 'GUILD_CATEGORY', 'A category that contains channels'],
|
|
[ChannelTypes.GUILD_LINK, 'GUILD_LINK', 'A link channel for external resources'],
|
|
[ChannelTypes.DM_PERSONAL_NOTES, 'DM_PERSONAL_NOTES', 'Personal notes DM channel'],
|
|
],
|
|
'The type of the channel',
|
|
),
|
|
'ChannelType',
|
|
);
|
|
|
|
export const ChannelOverwriteTypeSchema = withOpenApiType(
|
|
createNamedLiteralUnion(
|
|
[
|
|
[ChannelOverwriteTypes.ROLE, 'ROLE', ChannelOverwriteTypesDescriptions.ROLE],
|
|
[ChannelOverwriteTypes.MEMBER, 'MEMBER', ChannelOverwriteTypesDescriptions.MEMBER],
|
|
] as const,
|
|
'The type of entity the overwrite applies to',
|
|
),
|
|
'ChannelOverwriteType',
|
|
);
|
|
|
|
const WHITESPACE_REGEX = /\s+/g;
|
|
const MULTIPLE_HYPHENS_REGEX = /-{2,}/g;
|
|
const VANITY_URL_REGEX = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
|
|
const DISALLOWED_CHARS = new Set(' !"#$%&\'()*+,/:;<=>?@[\\]^`{|}~');
|
|
|
|
function sanitizeChannelName(value: string): string {
|
|
if (value.length > MAX_STRING_PROCESSING_LENGTH) {
|
|
throw new Error(ValidationErrorCodes.STRING_LENGTH_INVALID);
|
|
}
|
|
let s = normalizeString(value);
|
|
s = stripInvisibles(s);
|
|
s = normalizeWhitespace(s);
|
|
return s;
|
|
}
|
|
|
|
export const ChannelNameType = z
|
|
.string()
|
|
.superRefine((value, ctx) => {
|
|
if (value.length > MAX_STRING_PROCESSING_LENGTH) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
message: ValidationErrorCodes.STRING_LENGTH_INVALID,
|
|
params: {min: 1, max: 100},
|
|
});
|
|
return z.NEVER;
|
|
}
|
|
const normalized = normalizeString(value);
|
|
const processed =
|
|
normalized
|
|
.toLowerCase()
|
|
.replace(WHITESPACE_REGEX, '-')
|
|
.split('')
|
|
.filter((char) => !DISALLOWED_CHARS.has(char))
|
|
.join('') || '-';
|
|
if (processed.length < 1) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
message: ValidationErrorCodes.CHANNEL_NAME_EMPTY_AFTER_NORMALIZATION,
|
|
});
|
|
return z.NEVER;
|
|
}
|
|
})
|
|
.transform((value) => {
|
|
if (value.length > MAX_STRING_PROCESSING_LENGTH) {
|
|
throw new Error(ValidationErrorCodes.STRING_LENGTH_INVALID);
|
|
}
|
|
const normalized = normalizeString(value);
|
|
return (
|
|
normalized
|
|
.toLowerCase()
|
|
.replace(WHITESPACE_REGEX, '-')
|
|
.split('')
|
|
.filter((char) => !DISALLOWED_CHARS.has(char))
|
|
.join('') || '-'
|
|
);
|
|
})
|
|
.pipe(withStringLengthRangeValidation(z.string(), 1, 100, ValidationErrorCodes.STRING_LENGTH_INVALID));
|
|
|
|
export const GeneralChannelNameType = z
|
|
.string()
|
|
.superRefine((value, ctx) => {
|
|
if (value.length > MAX_STRING_PROCESSING_LENGTH) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
message: ValidationErrorCodes.STRING_LENGTH_INVALID,
|
|
params: {min: 1, max: 100},
|
|
});
|
|
return z.NEVER;
|
|
}
|
|
})
|
|
.transform((value) => {
|
|
let sanitized = sanitizeChannelName(value);
|
|
sanitized = sanitized.replace(WHITESPACE_REGEX, ' ');
|
|
return sanitized;
|
|
})
|
|
.refine((v) => v.trim().length > 0, ValidationErrorCodes.NAME_EMPTY_AFTER_NORMALIZATION)
|
|
.pipe(withStringLengthRangeValidation(z.string(), 1, 100, ValidationErrorCodes.STRING_LENGTH_INVALID));
|
|
|
|
export const VanityURLCodeType = z
|
|
.string()
|
|
.superRefine((value, ctx) => {
|
|
const normalized = normalizeString(value);
|
|
const processed = normalized.toLowerCase().replace(WHITESPACE_REGEX, '-').replace(MULTIPLE_HYPHENS_REGEX, '-');
|
|
if (!VANITY_URL_REGEX.test(processed)) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
message: ValidationErrorCodes.VANITY_URL_INVALID_CHARACTERS,
|
|
});
|
|
return z.NEVER;
|
|
}
|
|
})
|
|
.transform((value) => {
|
|
const normalized = normalizeString(value);
|
|
return normalized.toLowerCase().replace(WHITESPACE_REGEX, '-').replace(MULTIPLE_HYPHENS_REGEX, '-');
|
|
})
|
|
.pipe(withStringLengthRangeValidation(z.string(), 2, 32, ValidationErrorCodes.VANITY_URL_CODE_LENGTH_INVALID));
|
|
|
|
const AUDIT_LOG_REASON_MAX_LENGTH = 512;
|
|
|
|
export const AuditLogReasonType = z
|
|
.string()
|
|
.nullable()
|
|
.optional()
|
|
.transform((value) => {
|
|
if (!value || value.trim().length === 0) {
|
|
return null;
|
|
}
|
|
const normalized = normalizeString(value);
|
|
if (normalized.length < 1 || normalized.length > AUDIT_LOG_REASON_MAX_LENGTH) {
|
|
return null;
|
|
}
|
|
return normalized;
|
|
});
|