feat: add fluxer upstream source and self-hosting documentation
- 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
This commit is contained in:
178
fluxer/packages/schema/src/primitives/ChannelValidators.tsx
Normal file
178
fluxer/packages/schema/src/primitives/ChannelValidators.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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;
|
||||
});
|
||||
Reference in New Issue
Block a user