Repository: tuskyapp/Tusky (language: kotlin)

Issue:
Missing support for GoToSocial and microblog.pub links in looksLikeMastodonUrl
Tusky currently supports automated lookup of posts (i.e., URLs inside other posts) from Mastodon, Pleroma, Friendica, Misskey, and Pixelfed, but it fails when it comes to [GoToSocial](https://gotosocial.org/) or [microblog.pub](https://docs.microblog.pub/). I have absolutely no experience with Kotlin or the workflow here, so I'm not really comfortable with creating a pull request myself. But if someone is willing to do it, I believe the correct regexes are as follows:

* `"^/@[^/]+/statuses/[0-9ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz]{26}$"` for GoToSocial posts
  * It uses ULID, so it's guaranteed to be 26 symbols in Crockford's Base 32. And it seems to be case-insensitive.
  * See https://github.com/superseriousbusiness/gotosocial/blob/25edd57eaf3eecf0b449a4dabb5b5fc5d6ae7687/internal/id/ulid.go#L35 and https://github.com/oklog/ulid
* `"^/o/[0-9a-f]{32}$"` for microblog.pub posts
  * It uses UUID, so it's guaranteed to be 32 hex symbols. It is case-sensitive (lowercase).
  * See https://git.sr.ht/~tsileo/microblog.pub/tree/v2/item/app/boxes.py#L51
* Optionally `"^/articles/[0-9a-f]{7}/[-\\w]+$"` for microblog.pub articles
  * Articles use a combination of the short id (currently first 7 characters of the actual id) and a slug.
  * Maybe something more general like `"^/articles/[0-9a-f]+/[-\\w]+$"` should be used in case they decide to change the length of the short id.
  * This is not necessary as it would complicate getting to the actual article. Users would be forced to use the "Open in browser" button to see the whole content. (Mastodon shows only the name of the article.)
  * See https://git.sr.ht/~tsileo/microblog.pub/tree/v2/item/app/boxes.py#L658 and https://git.sr.ht/~tsileo/microblog.pub/tree/v2/item/app/utils/text.py#L5

It's supposed to go in Tusky/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt.

* * * *
- Tusky Version: 19.0
- Android Version: 11
- Android Device: FP4
- Mastodon instance (if applicable):

- [X] I searched or browsed the repo’s other issues to ensure this is not a duplicate.



Discussion hints:
nan

Pull request: Load goto social and microblog.pub urls in the app
- goto social status urls
- goto social profile urls
- microblog.pub status urls

(microblog.pub profile urls are just site urls, so 🤷 )

I also moved the utility function into `LinkHelper`, it felt weird to keep it in `BottomSheetActivity`

 #2893 

Test command: ./gradlew :app:testBlueDebugUnitTest --tests "com.keylesspalace.tusky.BottomSheetActivityTest" --tests "com.keylesspalace.tusky.util.LinkHelperTest"

Fix patch:
diff --git a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt
index 36a476f2fb..60d1966d1c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt
@@ -29,10 +29,9 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
 import com.keylesspalace.tusky.components.account.AccountActivity
 import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
 import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.looksLikeMastodonUrl
 import com.keylesspalace.tusky.util.openLink
 import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
-import java.net.URI
-import java.net.URISyntaxException
 import javax.inject.Inject
 
 /** this is the base class for all activities that open links
@@ -173,45 +172,6 @@ abstract class BottomSheetActivity : BaseActivity() {
     }
 }
 
-// https://mastodon.foo.bar/@User
-// https://mastodon.foo.bar/@User/43456787654678
-// https://pleroma.foo.bar/users/User
-// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0
-// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc
-// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
-// https://friendica.foo.bar/profile/user
-// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
-// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
-// https://pixelfed.social/p/connyduck/391263492998670833
-// https://pixelfed.social/connyduck
-fun looksLikeMastodonUrl(urlString: String): Boolean {
-    val uri: URI
-    try {
-        uri = URI(urlString)
-    } catch (e: URISyntaxException) {
-        return false
-    }
-
-    if (uri.query != null ||
-        uri.fragment != null ||
-        uri.path == null
-    ) {
-        return false
-    }
-
-    val path = uri.path
-    return path.matches("^/@[^/]+$".toRegex()) ||
-        path.matches("^/@[^/]+/\\d+$".toRegex()) ||
-        path.matches("^/users/\\w+$".toRegex()) ||
-        path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) ||
-        path.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
-        path.matches("^/notes/[a-z0-9]+$".toRegex()) ||
-        path.matches("^/display/[-a-f0-9]+$".toRegex()) ||
-        path.matches("^/profile/\\w+$".toRegex()) ||
-        path.matches("^/p/\\w+/\\d+$".toRegex()) ||
-        path.matches("^/\\w+$".toRegex())
-}
-
 enum class PostLookupFallbackBehavior {
     OPEN_IN_BROWSER,
     DISPLAY_ERROR,
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
index 39489e4a0c..69f84593ca 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
@@ -37,6 +37,8 @@ import com.keylesspalace.tusky.R
 import com.keylesspalace.tusky.entity.HashTag
 import com.keylesspalace.tusky.entity.Status.Mention
 import com.keylesspalace.tusky.interfaces.LinkListener
+import java.net.URI
+import java.net.URISyntaxException
 
 fun getDomain(urlString: String?): String {
     val host = urlString?.toUri()?.host
@@ -270,4 +272,49 @@ private fun openLinkInCustomTab(uri: Uri, context: Context) {
     }
 }
 
+// https://mastodon.foo.bar/@User
+// https://mastodon.foo.bar/@User/43456787654678
+// https://pleroma.foo.bar/users/User
+// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0
+// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc
+// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
+// https://friendica.foo.bar/profile/user
+// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
+// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
+// https://pixelfed.social/p/connyduck/391263492998670833
+// https://pixelfed.social/connyduck
+// https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2
+// https://gts.foo.bar/@goblin
+// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5
+fun looksLikeMastodonUrl(urlString: String): Boolean {
+    val uri: URI
+    try {
+        uri = URI(urlString)
+    } catch (e: URISyntaxException) {
+        return false
+    }
+
+    if (uri.query != null ||
+        uri.fragment != null ||
+        uri.path == null
+    ) {
+        return false
+    }
+
+    return uri.path.let {
+        it.matches("^/@[^/]+$".toRegex()) ||
+            it.matches("^/@[^/]+/\\d+$".toRegex()) ||
+            it.matches("^/users/\\w+$".toRegex()) ||
+            it.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) ||
+            it.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
+            it.matches("^/notes/[a-z0-9]+$".toRegex()) ||
+            it.matches("^/display/[-a-f0-9]+$".toRegex()) ||
+            it.matches("^/profile/\\w+$".toRegex()) ||
+            it.matches("^/p/\\w+/\\d+$".toRegex()) ||
+            it.matches("^/\\w+$".toRegex()) ||
+            it.matches("^/@[^/]+/statuses/[a-zA-Z0-9]+$".toRegex()) ||
+            it.matches("^/o/[a-f0-9]+$".toRegex())
+    }
+}
+
 private const val TAG = "LinkHelper"


Test patch:
diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt
index 4c77799976..142893cee7 100644
--- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt
@@ -30,8 +30,6 @@ import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.Mockito.eq
 import org.mockito.kotlin.doReturn
@@ -108,70 +106,6 @@ class BottomSheetActivityTest {
         activity = FakeBottomSheetActivity(apiMock)
     }
 
-    @RunWith(Parameterized::class)
-    class UrlMatchingTests(private val url: String, private val expectedResult: Boolean) {
-        companion object {
-            @Parameterized.Parameters(name = "match_{0}")
-            @JvmStatic
-            fun data(): Iterable<Any> {
-                return listOf(
-                    arrayOf("https://mastodon.foo.bar/@User", true),
-                    arrayOf("http://mastodon.foo.bar/@abc123", true),
-                    arrayOf("https://mastodon.foo.bar/@user/345667890345678", true),
-                    arrayOf("https://mastodon.foo.bar/@user/3", true),
-                    arrayOf("https://pleroma.foo.bar/users/meh3223", true),
-                    arrayOf("https://pleroma.foo.bar/users/meh3223_bruh", true),
-                    arrayOf("https://pleroma.foo.bar/users/2345", true),
-                    arrayOf("https://pleroma.foo.bar/notice/9", true),
-                    arrayOf("https://pleroma.foo.bar/notice/9345678", true),
-                    arrayOf("https://pleroma.foo.bar/notice/wat", true),
-                    arrayOf("https://pleroma.foo.bar/notice/9qTHT2ANWUdXzENqC0", true),
-                    arrayOf("https://pleroma.foo.bar/objects/abcdef-123-abcd-9876543", true),
-                    arrayOf("https://misskey.foo.bar/notes/mew", true),
-                    arrayOf("https://misskey.foo.bar/notes/1421564653", true),
-                    arrayOf("https://misskey.foo.bar/notes/qwer615985ddf", true),
-                    arrayOf("https://friendica.foo.bar/profile/user", true),
-                    arrayOf("https://friendica.foo.bar/profile/uSeR", true),
-                    arrayOf("https://friendica.foo.bar/profile/user_user", true),
-                    arrayOf("https://friendica.foo.bar/profile/123", true),
-                    arrayOf("https://friendica.foo.bar/display/abcdef-123-abcd-9876543", true),
-                    arrayOf("https://google.com/", false),
-                    arrayOf("https://mastodon.foo.bar/@User?foo=bar", false),
-                    arrayOf("https://mastodon.foo.bar/@User#foo", false),
-                    arrayOf("http://mastodon.foo.bar/@", false),
-                    arrayOf("http://mastodon.foo.bar/@/345678", false),
-                    arrayOf("https://mastodon.foo.bar/@user/345667890345678/", false),
-                    arrayOf("https://mastodon.foo.bar/@user/3abce", false),
-                    arrayOf("https://pleroma.foo.bar/users/", false),
-                    arrayOf("https://pleroma.foo.bar/users/meow/", false),
-                    arrayOf("https://pleroma.foo.bar/users/@meow", false),
-                    arrayOf("https://pleroma.foo.bar/user/2345", false),
-                    arrayOf("https://pleroma.foo.bar/notices/123456", false),
-                    arrayOf("https://pleroma.foo.bar/notice/@neverhappen/", false),
-                    arrayOf("https://pleroma.foo.bar/object/abcdef-123-abcd-9876543", false),
-                    arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543", false),
-                    arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543/", false),
-                    arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd_9876543", false),
-                    arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543", false),
-                    arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543/", false),
-                    arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd_9876543", false),
-                    arrayOf("https://friendica.foo.bar/profile/@mew", false),
-                    arrayOf("https://friendica.foo.bar/profile/@mew/", false),
-                    arrayOf("https://misskey.foo.bar/notes/@nyan", false),
-                    arrayOf("https://misskey.foo.bar/notes/NYAN123", false),
-                    arrayOf("https://misskey.foo.bar/notes/meow123/", false),
-                    arrayOf("https://pixelfed.social/p/connyduck/391263492998670833", true),
-                    arrayOf("https://pixelfed.social/connyduck", true)
-                )
-            }
-        }
-
-        @Test
-        fun test() {
-            assertEquals(expectedResult, looksLikeMastodonUrl(url))
-        }
-    }
-
     @Test
     fun beginEndSearch_setIsSearching_isSearchingAfterBegin() {
         activity.onBeginSearch("https://mastodon.foo.bar/@User")
diff --git a/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt
index 6264d86b95..85be92dc7b 100644
--- a/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt
@@ -12,6 +12,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener
 import org.junit.Assert
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 import org.robolectric.annotation.Config
 
 @Config(sdk = [28])
@@ -308,4 +309,71 @@ class LinkHelperTest {
             Assert.assertFalse(markedUpContent.contains("${getDomain(tag.url)})"))
         }
     }
+
+    @RunWith(Parameterized::class)
+    class UrlMatchingTests(private val url: String, private val expectedResult: Boolean) {
+        companion object {
+            @Parameterized.Parameters(name = "match_{0}")
+            @JvmStatic
+            fun data(): Iterable<Any> {
+                return listOf(
+                    arrayOf("https://mastodon.foo.bar/@User", true),
+                    arrayOf("http://mastodon.foo.bar/@abc123", true),
+                    arrayOf("https://mastodon.foo.bar/@user/345667890345678", true),
+                    arrayOf("https://mastodon.foo.bar/@user/3", true),
+                    arrayOf("https://pleroma.foo.bar/users/meh3223", true),
+                    arrayOf("https://pleroma.foo.bar/users/meh3223_bruh", true),
+                    arrayOf("https://pleroma.foo.bar/users/2345", true),
+                    arrayOf("https://pleroma.foo.bar/notice/9", true),
+                    arrayOf("https://pleroma.foo.bar/notice/9345678", true),
+                    arrayOf("https://pleroma.foo.bar/notice/wat", true),
+                    arrayOf("https://pleroma.foo.bar/notice/9qTHT2ANWUdXzENqC0", true),
+                    arrayOf("https://pleroma.foo.bar/objects/abcdef-123-abcd-9876543", true),
+                    arrayOf("https://misskey.foo.bar/notes/mew", true),
+                    arrayOf("https://misskey.foo.bar/notes/1421564653", true),
+                    arrayOf("https://misskey.foo.bar/notes/qwer615985ddf", true),
+                    arrayOf("https://friendica.foo.bar/profile/user", true),
+                    arrayOf("https://friendica.foo.bar/profile/uSeR", true),
+                    arrayOf("https://friendica.foo.bar/profile/user_user", true),
+                    arrayOf("https://friendica.foo.bar/profile/123", true),
+                    arrayOf("https://friendica.foo.bar/display/abcdef-123-abcd-9876543", true),
+                    arrayOf("https://google.com/", false),
+                    arrayOf("https://mastodon.foo.bar/@User?foo=bar", false),
+                    arrayOf("https://mastodon.foo.bar/@User#foo", false),
+                    arrayOf("http://mastodon.foo.bar/@", false),
+                    arrayOf("http://mastodon.foo.bar/@/345678", false),
+                    arrayOf("https://mastodon.foo.bar/@user/345667890345678/", false),
+                    arrayOf("https://mastodon.foo.bar/@user/3abce", false),
+                    arrayOf("https://pleroma.foo.bar/users/", false),
+                    arrayOf("https://pleroma.foo.bar/users/meow/", false),
+                    arrayOf("https://pleroma.foo.bar/users/@meow", false),
+                    arrayOf("https://pleroma.foo.bar/user/2345", false),
+                    arrayOf("https://pleroma.foo.bar/notices/123456", false),
+                    arrayOf("https://pleroma.foo.bar/notice/@neverhappen/", false),
+                    arrayOf("https://pleroma.foo.bar/object/abcdef-123-abcd-9876543", false),
+                    arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543", false),
+                    arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543/", false),
+                    arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd_9876543", false),
+                    arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543", false),
+                    arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543/", false),
+                    arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd_9876543", false),
+                    arrayOf("https://friendica.foo.bar/profile/@mew", false),
+                    arrayOf("https://friendica.foo.bar/profile/@mew/", false),
+                    arrayOf("https://misskey.foo.bar/notes/@nyan", false),
+                    arrayOf("https://misskey.foo.bar/notes/NYAN123", false),
+                    arrayOf("https://misskey.foo.bar/notes/meow123/", false),
+                    arrayOf("https://pixelfed.social/p/connyduck/391263492998670833", true),
+                    arrayOf("https://pixelfed.social/connyduck", true),
+                    arrayOf("https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2", true),
+                    arrayOf("https://gts.foo.bar/@goblin", true),
+                    arrayOf("https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5", true),
+                )
+            }
+        }
+
+        @Test
+        fun test() {
+            Assert.assertEquals(expectedResult, looksLikeMastodonUrl(url))
+        }
+    }
 }