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:
Vish
2026-03-13 00:55:14 -07:00
parent 5ceda343b8
commit 3b9d759b4b
5859 changed files with 1923440 additions and 0 deletions

View 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;
});