Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 48 additions & 129 deletions lib/src/shared/utils/instance.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:thunder/src/core/models/models.dart';
import 'package:thunder/src/features/instance/instance.dart';
import 'package:thunder/src/features/search/search.dart';
import 'package:thunder/src/shared/pages/loading_page.dart';
import 'package:thunder/src/shared/utils/link_utils.dart';

String? fetchInstanceNameFromUrl(String? url) {
if (url == null) {
Expand All @@ -21,160 +22,78 @@ String? fetchInstanceNameFromUrl(String? url) {
return uri.host;
}

/// Matches instance.tld/c/community@otherinstance.tld
/// Puts community in group 3 and otherinstance.tld in group 4
/// https://regex101.com/r/sE8SmL/1
final RegExp fullCommunityUrl = RegExp(r'^!?(https?:\/\/)?(.*)\/c\/(.*)@(.*)$');

/// Matches instance.tld/c/community
/// Puts community in group 3 and instance.tld in group 2
/// https://regex101.com/r/AW2qTr/1
final RegExp shortCommunityUrl = RegExp(r'^!?(https?:\/\/)?(.*)\/c\/([^@\n]*)$');

/// Matches community@instance.tld
/// Puts community in group 2 and instance.tld in group 3
/// https://regex101.com/r/1VrXgX/1
final RegExp instanceName = RegExp(r'^!?(https?:\/\/)?((?:(?!\/c\/c).)*)@(.*)$');

/// Checks if the given text references a community on a valid Lemmy server.
/// Checks if the given text references a community on a valid Lemmy/PieFed server.
/// If so, returns the community name in the format community@instance.tld.
/// Otherwise, returns null.
Future<String?> getLemmyCommunity(String text) async {
// Do an initial check for usernames in the format /u/user@instance.tld or @user@instance.tld.
// These can accidentally trip our community name detection.
if (text.toLowerCase().startsWith('/u/') || text.toLowerCase().startsWith('@')) {
return null;
}

final RegExpMatch? fullCommunityUrlMatch = fullCommunityUrl.firstMatch(text);
if (fullCommunityUrlMatch != null && fullCommunityUrlMatch.groupCount >= 4) {
return '${fullCommunityUrlMatch.group(3)}@${fullCommunityUrlMatch.group(4)}';
}

final RegExpMatch? shortCommunityUrlMatch = shortCommunityUrl.firstMatch(text);
if (shortCommunityUrlMatch != null && shortCommunityUrlMatch.groupCount >= 3) {
return '${shortCommunityUrlMatch.group(3)}@${shortCommunityUrlMatch.group(2)}';
}

final RegExpMatch? instanceNameMatch = instanceName.firstMatch(text);
if (instanceNameMatch != null && instanceNameMatch.groupCount >= 3) {
return '${instanceNameMatch.group(2)}@${instanceNameMatch.group(3)}';
}

return null;
final result = parseCommunity(text);
return result?.qualified;
}

/// Matches instance.tld/u/username@otherinstance.tld
/// Puts username in group 3 and otherinstance.tld in group 4
final RegExp fullUsernameUrl = RegExp(r'^@?(https?:\/\/)?(.*)\/u\/(.*)@(.*)$');

/// Matches instance.tld/u/username
/// Puts username in group 3 and instance.tld in group 2
final RegExp shortUsernameUrl = RegExp(r'^@?(https?:\/\/)?(.*)\/u\/([^@\n]*)$');

/// Matches username@instance.tld
/// Puts username in group 2 and instance.tld in group 3
final RegExp username = RegExp(r'^@?(https?:\/\/)?((?:(?!\/u\/u).)*)@(.*)$');

/// Checks if the given text references a user on a valid Lemmy server.
/// If so, returns the username name in the format username@instance.tld.
/// Checks if the given text references a user on a valid Lemmy/PieFed server.
/// If so, returns the username in the format username@instance.tld.
/// Otherwise, returns null.
Future<String?> getLemmyUser(String text) async {
// Do an initial check for communities in the format /c/community@instance.tld or !community@instance.tld.
// These can accidentally trip our user name detection.
if (text.toLowerCase().startsWith('/c/') || text.toLowerCase().startsWith('!')) {
return null;
}
final result = parseUser(text);
return result?.qualified;
}

final RegExpMatch? fullUsernameUrlMatch = fullUsernameUrl.firstMatch(text);
if (fullUsernameUrlMatch != null && fullUsernameUrlMatch.groupCount >= 4) {
return '${fullUsernameUrlMatch.group(3)}@${fullUsernameUrlMatch.group(4)}';
/// Gets the post ID from a Lemmy/PieFed URL.
/// If the URL is from a different instance, it will attempt to resolve it.
Future<int?> getLemmyPostId(BuildContext context, String text) async {
final parsed = parsePostId(text);
if (parsed == null) {
return null;
}

final RegExpMatch? shortUsernameUrlMatch = shortUsernameUrl.firstMatch(text);
if (shortUsernameUrlMatch != null && shortUsernameUrlMatch.groupCount >= 3) {
return '${shortUsernameUrlMatch.group(3)}@${shortUsernameUrlMatch.group(2)}';
}
final account = context.read<ProfileBloc>().state.account;
final postId = int.tryParse(parsed.value);

final RegExpMatch? usernameMatch = username.firstMatch(text);
if (usernameMatch != null && usernameMatch.groupCount >= 3) {
return '${usernameMatch.group(2)}@${usernameMatch.group(3)}';
if (postId == null) {
return null;
}

return null;
}

final RegExp _post = RegExp(r'^(https?:\/\/)(.*)\/post\/([0-9]*).*$');
Future<int?> getLemmyPostId(BuildContext context, String text) async {
final account = context.read<ProfileBloc>().state.account;

final RegExpMatch? postMatch = _post.firstMatch(text);
if (postMatch != null) {
final String? instance = postMatch.group(2);
final int? postId = int.tryParse(postMatch.group(3)!);
if (postId != null) {
if (instance == account.instance) {
return postId;
} else {
// This is a post on another instance. Try to resolve it
try {
// Show the loading page while we resolve the post
showLoadingPage(context);

final response = await SearchRepositoryImpl(account: account).resolve(query: text);
return response['post']?.id;
} catch (e) {
return null;
}
}
if (parsed.instance == account.instance) {
return postId;
} else {
// This is a post on another instance. Try to resolve it
try {
showLoadingPage(context);
final response = await SearchRepositoryImpl(account: account).resolve(query: text);
return response['post']?.id;
} catch (e) {
return null;
}
}

return null;
}

final RegExp _comment = RegExp(r'^(https?:\/\/)(.*)\/comment\/([0-9]*).*$');
final RegExp _commentAlternate = RegExp(r'^(https?:\/\/)(.*)\/post\/([0-9]*)\/([0-9]*).*$');
/// Gets the comment ID from a Lemmy/PieFed URL.
/// If the URL is from a different instance, it will attempt to resolve it.
Future<int?> getLemmyCommentId(BuildContext context, String text) async {
String? instance;
int? commentId;
final parsed = parseCommentId(text);
if (parsed == null) {
return null;
}

final account = context.read<ProfileBloc>().state.account;
final commentId = int.tryParse(parsed.value);

// Try legacy comment link format
RegExpMatch? commentMatch = _comment.firstMatch(text);
if (commentMatch != null) {
// It's a match!
instance = commentMatch.group(2);
commentId = int.tryParse(commentMatch.group(3)!);
} else {
// Otherwise, try the new format
commentMatch = _commentAlternate.firstMatch(text);
if (commentMatch != null) {
// It's a match!
instance = commentMatch.group(2);
commentId = int.tryParse(commentMatch.group(4)!);
}
if (commentId == null) {
return null;
}

if (commentId != null) {
if (instance == account.instance) {
return commentId;
} else {
// This is a comment on another instance. Try to resolve it
try {
// Show the loading page while we resolve the post
showLoadingPage(context);

final response = await SearchRepositoryImpl(account: account).resolve(query: text);
return response['comment']?.id;
} catch (e) {
return null;
}
if (parsed.instance == account.instance) {
return commentId;
} else {
// This is a comment on another instance. Try to resolve it
try {
showLoadingPage(context);
final response = await SearchRepositoryImpl(account: account).resolve(query: text);
return response['comment']?.id;
} catch (e) {
return null;
}
}

return null;
}

/// Fetches the instance info for a given URL.
Expand Down
Loading