-
-
Notifications
You must be signed in to change notification settings - Fork 23
Validate Mbin community names when creating a community #420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /// Helpers for validating and repairing Mbin magazine (community) names. | ||
| /// | ||
| /// Mbin restricts magazine names to 2-25 characters consisting only of | ||
| /// letters, digits and underscores (`RegPatterns::MAGAZINE_NAME` / | ||
| /// `/^[a-zA-Z0-9_]{2,25}$/` upstream). Lemmy and PieFed use different rules, | ||
| /// so callers should only apply these checks when talking to an Mbin server. | ||
| library; | ||
|
|
||
| const int mbinCommunityNameMinLength = 2; | ||
| const int mbinCommunityNameMaxLength = 25; | ||
|
|
||
| final RegExp _mbinCommunityNameRegExp = RegExp(r'^[a-zA-Z0-9_]{2,25}$'); | ||
| final RegExp _mbinCommunityNameInvalidChars = RegExp(r'[^a-zA-Z0-9_]'); | ||
|
|
||
| /// Whether [name] is a valid Mbin magazine name that can be submitted as-is. | ||
| bool isValidMbinCommunityName(String name) => | ||
| _mbinCommunityNameRegExp.hasMatch(name); | ||
|
|
||
| /// The reason [name] is not a valid Mbin magazine name, or `null` when it is | ||
| /// valid (or still empty, which is treated as "not entered yet"). | ||
| MbinCommunityNameIssue? mbinCommunityNameIssue(String name) { | ||
| if (name.isEmpty) return null; | ||
| if (_mbinCommunityNameInvalidChars.hasMatch(name)) { | ||
| return MbinCommunityNameIssue.invalidCharacters; | ||
| } | ||
| if (name.length < mbinCommunityNameMinLength) { | ||
| return MbinCommunityNameIssue.tooShort; | ||
| } | ||
| if (name.length > mbinCommunityNameMaxLength) { | ||
| return MbinCommunityNameIssue.tooLong; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| enum MbinCommunityNameIssue { invalidCharacters, tooShort, tooLong } | ||
|
|
||
| /// A best-effort valid name derived from [name], or `null` when nothing | ||
| /// usable can be salvaged (e.g. the input has no letters/digits at all) or | ||
| /// when [name] is already valid. | ||
| String? suggestMbinCommunityName(String name) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One addition that would be nice for this function is to also make the suggested name all lowercase, as that seems to be the general convention for community names. |
||
| if (isValidMbinCommunityName(name)) return null; | ||
|
|
||
| // Replace every run of unsupported characters (whitespace, punctuation, | ||
| // accented letters, ...) with a single underscore, then tidy up the | ||
| // underscores so the result reads naturally. | ||
| var suggestion = name | ||
| .replaceAll(_mbinCommunityNameInvalidChars, '_') | ||
| .replaceAll(RegExp(r'_+'), '_') | ||
| .replaceAll(RegExp(r'^_+|_+$'), ''); | ||
|
|
||
| if (suggestion.length > mbinCommunityNameMaxLength) { | ||
| suggestion = suggestion | ||
| .substring(0, mbinCommunityNameMaxLength) | ||
| .replaceAll(RegExp(r'_+$'), ''); | ||
| } | ||
|
Comment on lines
+46
to
+55
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it work to move the |
||
|
|
||
| if (suggestion.length < mbinCommunityNameMinLength) return null; | ||
| if (suggestion == name) return null; | ||
|
|
||
| return suggestion; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import 'package:flutter_test/flutter_test.dart'; | ||
| import 'package:interstellar/src/utils/mbin_community_name.dart'; | ||
|
|
||
| void main() { | ||
| group('isValidMbinCommunityName', () { | ||
| test('accepts letters, digits and underscores within 2-25 chars', () { | ||
| expect(isValidMbinCommunityName('ab'), isTrue); | ||
| expect(isValidMbinCommunityName('under_score'), isTrue); | ||
| expect(isValidMbinCommunityName('Mixed_Case_123'), isTrue); | ||
| expect(isValidMbinCommunityName('a' * 25), isTrue); | ||
| }); | ||
|
|
||
| test('rejects empty, too short and too long names', () { | ||
| expect(isValidMbinCommunityName(''), isFalse); | ||
| expect(isValidMbinCommunityName('a'), isFalse); | ||
| expect(isValidMbinCommunityName('a' * 26), isFalse); | ||
| }); | ||
|
|
||
| test('rejects unsupported characters', () { | ||
| expect(isValidMbinCommunityName('with space'), isFalse); | ||
| expect(isValidMbinCommunityName('with-hyphen'), isFalse); | ||
| expect(isValidMbinCommunityName('dot.separated'), isFalse); | ||
| expect(isValidMbinCommunityName('accenté'), isFalse); | ||
| }); | ||
| }); | ||
|
|
||
| group('mbinCommunityNameIssue', () { | ||
| const invalid = MbinCommunityNameIssue.invalidCharacters; | ||
| const tooShort = MbinCommunityNameIssue.tooShort; | ||
| const tooLong = MbinCommunityNameIssue.tooLong; | ||
|
|
||
| test('returns null for an empty (not yet entered) name', () { | ||
| expect(mbinCommunityNameIssue(''), isNull); | ||
| }); | ||
|
|
||
| test('returns null for a valid name', () { | ||
| expect(mbinCommunityNameIssue('valid_Name_123'), isNull); | ||
| }); | ||
|
|
||
| test('reports invalid characters ahead of length problems', () { | ||
| expect(mbinCommunityNameIssue('hello world'), invalid); | ||
| expect(mbinCommunityNameIssue('!'), invalid); | ||
| }); | ||
|
|
||
| test('reports names that are too short', () { | ||
| expect(mbinCommunityNameIssue('a'), tooShort); | ||
| }); | ||
|
|
||
| test('reports names longer than 25 characters', () { | ||
| expect(mbinCommunityNameIssue('a' * 26), tooLong); | ||
| }); | ||
| }); | ||
|
|
||
| group('suggestMbinCommunityName', () { | ||
| test('returns null when the name is already valid', () { | ||
| expect(suggestMbinCommunityName('already_valid'), isNull); | ||
| }); | ||
|
|
||
| test('replaces unsupported runs with a single underscore', () { | ||
| final result = suggestMbinCommunityName('My Cool Community!'); | ||
| expect(result, 'My_Cool_Community'); | ||
| expect(suggestMbinCommunityName('a...b---c'), 'a_b_c'); | ||
| }); | ||
|
|
||
| test('trims leading and trailing underscores', () { | ||
| expect(suggestMbinCommunityName(' hello!! '), 'hello'); | ||
| }); | ||
|
|
||
| test('truncates to 25 characters without a trailing underscore', () { | ||
| final s = suggestMbinCommunityName('abcdefghijklmnopqrstuvwx yz'); | ||
| expect(s, 'abcdefghijklmnopqrstuvwx'); | ||
| expect(isValidMbinCommunityName(s!), isTrue); | ||
| }); | ||
|
|
||
| test('returns null when nothing usable can be salvaged', () { | ||
| expect(suggestMbinCommunityName('a'), isNull); | ||
| expect(suggestMbinCommunityName(' '), isNull); | ||
| expect(suggestMbinCommunityName('日本語'), isNull); | ||
| }); | ||
|
|
||
| test('always produces a valid name when it returns one', () { | ||
| const inputs = [ | ||
| 'hello world', | ||
| 'Trailing punctuation???', | ||
| '***leading', | ||
| 'lots of spaces', | ||
| 'cafe-society #2', | ||
| ]; | ||
| for (final input in inputs) { | ||
| final suggestion = suggestMbinCommunityName(input); | ||
| if (suggestion == null) continue; | ||
| expect(isValidMbinCommunityName(suggestion), isTrue, reason: input); | ||
| } | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of enforcing the max community length as an error, why don't you use the
maxLengthfield on theTextEditorwidget? It would display the current and max length of the text as the user is typing, and prevent the user from typing over that length.