refactor(api): simplify & improve username suggestion logic (#44)

This commit is contained in:
hampus-fluxer
2026-01-06 03:39:19 +01:00
committed by GitHub
parent 8ca99424ce
commit 1cef2290fe
4 changed files with 120 additions and 53 deletions

View File

@@ -412,7 +412,7 @@ export const AuthController = (app: HonoApp) => {
Validator('json', UsernameSuggestionsRequest),
async (ctx) => {
const {global_name} = ctx.req.valid('json');
const suggestions = generateUsernameSuggestions(global_name, 5);
const suggestions = generateUsernameSuggestions(global_name);
return ctx.json({suggestions});
},
);

View File

@@ -45,6 +45,7 @@ import * as AgeUtils from '~/utils/AgeUtils';
import * as IpUtils from '~/utils/IpUtils';
import {parseAcceptLanguage} from '~/utils/LocaleUtils';
import {generateRandomUsername} from '~/utils/UsernameGenerator';
import {deriveUsernameFromDisplayName} from '~/utils/UsernameSuggestionUtils';
const MINIMUM_AGE_BY_COUNTRY: Record<string, number> = {
KR: 14,
@@ -202,8 +203,32 @@ export class AuthRegistrationService {
if (emailTaken) throw InputValidationError.create('email', 'Email already in use');
}
const username = data.username || generateRandomUsername();
const discriminator = await this.allocateDiscriminator(username);
let usernameCandidate: string | undefined = data.username ?? undefined;
let discriminator: number | null = null;
if (!usernameCandidate) {
const derivedUsername = deriveUsernameFromDisplayName(data.global_name ?? '');
if (derivedUsername) {
try {
discriminator = await this.allocateDiscriminator(derivedUsername);
usernameCandidate = derivedUsername;
} catch (error) {
if (!(error instanceof InputValidationError)) {
throw error;
}
}
}
}
if (!usernameCandidate) {
usernameCandidate = generateRandomUsername();
discriminator = await this.allocateDiscriminator(usernameCandidate);
} else if (discriminator === null) {
discriminator = await this.allocateDiscriminator(usernameCandidate);
}
const username = usernameCandidate!;
const userId = this.generateUserId(emailKey);
const acceptLanguage = request.headers.get('accept-language');

View File

@@ -17,64 +17,36 @@
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {UsernameType} from '~/Schema';
import {transliterate as tr} from 'transliteration';
import {generateRandomUsername} from '~/utils/UsernameGenerator';
function sanitizeForFluxerTag(input: string): string {
let result = tr(input.trim());
const MAX_USERNAME_LENGTH = 32;
result = result.replace(/[\s\-.]+/g, '_');
function sanitizeDisplayName(globalName: string): string | null {
const trimmed = globalName.trim();
if (!trimmed) return null;
result = result.replace(/[^a-zA-Z0-9_]/g, '');
if (!result) {
result = 'user';
let sanitized = tr(trimmed);
sanitized = sanitized.replace(/[\s\-.]+/g, '_');
sanitized = sanitized.replace(/[^a-zA-Z0-9_]/g, '');
if (!sanitized) return null;
if (sanitized.length > MAX_USERNAME_LENGTH) {
sanitized = sanitized.substring(0, MAX_USERNAME_LENGTH);
}
if (result.length > 32) {
result = result.substring(0, 32);
const validation = UsernameType.safeParse(sanitized);
if (!validation.success) {
return null;
}
return result.toLowerCase();
return sanitized;
}
export function generateUsernameSuggestions(globalName: string, count: number = 5): Array<string> {
const suggestions: Array<string> = [];
const transliterated = tr(globalName.trim());
const hasMeaningfulContent = /[a-zA-Z]/.test(transliterated);
if (!hasMeaningfulContent) {
for (let i = 0; i < count; i++) {
const randomUsername = generateRandomUsername();
const sanitizedRandom = sanitizeForFluxerTag(randomUsername);
if (sanitizedRandom && sanitizedRandom.length <= 32) {
suggestions.push(sanitizedRandom.toLowerCase());
}
}
return Array.from(new Set(suggestions)).slice(0, count);
}
const baseUsername = sanitizeForFluxerTag(globalName);
suggestions.push(baseUsername);
const suffixes = ['_', '__', '___', '123', '_1', '_official', '_real'];
for (const suffix of suffixes) {
if (suggestions.length >= count) break;
const suggestion = baseUsername + suffix;
if (suggestion.length <= 32) {
suggestions.push(suggestion);
}
}
let counter = 2;
while (suggestions.length < count) {
const suggestion = `${baseUsername}${counter}`;
if (suggestion.length <= 32) {
suggestions.push(suggestion);
}
counter++;
}
return Array.from(new Set(suggestions)).slice(0, count);
export function deriveUsernameFromDisplayName(globalName: string): string | null {
return sanitizeDisplayName(globalName);
}
export function generateUsernameSuggestions(globalName: string): Array<string> {
const candidate = deriveUsernameFromDisplayName(globalName);
return candidate ? [candidate] : [];
}