Skip to content

Commit 9ead9e7

Browse files
committed
fix fanbox link
1 parent 48bbb7e commit 9ead9e7

10 files changed

Lines changed: 301 additions & 74 deletions

File tree

shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMatcher.kt

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,63 +21,75 @@ internal class DeepLinkMatcher<T>(
2121

2222
val args = mutableMapOf<String, Any>()
2323
// match the host
24-
if (request.uri.host != deepLinkPattern.uriPattern.host) return null
24+
if (!matchSegments(request.uri.host.split('.'), deepLinkPattern.hostSegments, args, "host")) return null
2525

2626
// match the path
27-
request.pathSegments
27+
if (!matchSegments(request.pathSegments, deepLinkPattern.pathSegments, args, "path")) return null
28+
// match queries (if any)
29+
request.queries.forEach { query ->
30+
val name = query.key
31+
// if the query param is not part of the pattern, we just ignore it
32+
val queryStringParser = deepLinkPattern.queryValueParsers[name] ?: return@forEach
33+
val queryParsedValue =
34+
try {
35+
queryStringParser.invoke(query.value.first())
36+
} catch (e: IllegalArgumentException) {
37+
DebugRepository.log(
38+
"${TAG_LOG_ERROR}: Failed to parse query name:[$name] value:[${query.value}]." +
39+
" ${e.stackTraceToString()}",
40+
)
41+
return null
42+
}
43+
args[name] = queryParsedValue
44+
}
45+
// provide the serializer of the matching key and map of arg names to parsed arg values
46+
return DeepLinkMatchResult(deepLinkPattern.serializer, args)
47+
}
48+
49+
private fun matchSegments(
50+
requestedSegments: List<String>,
51+
candidateSegments: List<DeepLinkPattern.PathSegment>,
52+
args: MutableMap<String, Any>,
53+
componentName: String,
54+
): Boolean {
55+
if (requestedSegments.size != candidateSegments.size) return false
56+
57+
requestedSegments
2858
.asSequence()
2959
// zip to compare the two objects side by side, order matters here so we
3060
// need to make sure the compared segments are at the same position within the url
31-
.zip(deepLinkPattern.pathSegments.asSequence())
61+
.zip(candidateSegments.asSequence())
3262
.forEach { it ->
33-
// retrieve the two path segments to compare
63+
// retrieve the two segments to compare
3464
val requestedSegment = it.first
3565
val candidateSegment = it.second
36-
// if the potential match expects a path arg for this segment, try to parse the
66+
// if the potential match expects an arg for this segment, try to parse the
3767
// requested segment into the expected type
3868
if (candidateSegment.isParamArg) {
3969
var valueToParse = requestedSegment
40-
if (!valueToParse.startsWith(candidateSegment.prefix)) return null
70+
if (!valueToParse.startsWith(candidateSegment.prefix)) return false
4171
valueToParse = valueToParse.removePrefix(candidateSegment.prefix)
4272

43-
if (!valueToParse.endsWith(candidateSegment.suffix)) return null
73+
if (!valueToParse.endsWith(candidateSegment.suffix)) return false
4474
valueToParse = valueToParse.removeSuffix(candidateSegment.suffix)
4575

4676
val parsedValue =
4777
try {
4878
candidateSegment.typeParser.invoke(valueToParse)
4979
} catch (e: IllegalArgumentException) {
5080
DebugRepository.log(
51-
"${TAG_LOG_ERROR}: Failed to parse path value:[$requestedSegment]" +
81+
"${TAG_LOG_ERROR}: Failed to parse $componentName value:[$requestedSegment]" +
5282
". ${e.stackTraceToString()}",
5383
)
54-
return null
84+
return false
5585
}
5686
args[candidateSegment.stringValue] = parsedValue
5787
} else if (requestedSegment != candidateSegment.stringValue) {
58-
// if it's path arg is not the expected type, its not a match
59-
return null
88+
// if it's not the expected segment, its not a match
89+
return false
6090
}
6191
}
62-
// match queries (if any)
63-
request.queries.forEach { query ->
64-
val name = query.key
65-
// if the query param is not part of the pattern, we just ignore it
66-
val queryStringParser = deepLinkPattern.queryValueParsers[name] ?: return@forEach
67-
val queryParsedValue =
68-
try {
69-
queryStringParser.invoke(query.value.first())
70-
} catch (e: IllegalArgumentException) {
71-
DebugRepository.log(
72-
"${TAG_LOG_ERROR}: Failed to parse query name:[$name] value:[${query.value}]." +
73-
" ${e.stackTraceToString()}",
74-
)
75-
return null
76-
}
77-
args[name] = queryParsedValue
78-
}
79-
// provide the serializer of the matching key and map of arg names to parsed arg values
80-
return DeepLinkMatchResult(deepLinkPattern.serializer, args)
92+
return true
8193
}
8294
}
8395

shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkPattern.kt

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ internal class DeepLinkPattern<T>(
4343
*/
4444
private val regexPatternFillIn = Regex("\\{(.+?)\\}")
4545

46+
/**
47+
* parse the host into a list of [PathSegment]
48+
*/
49+
val hostSegments: List<PathSegment> = uriPattern.host.split('.').map(::parseSegment)
50+
4651
/**
4752
* parse the path into a list of [PathSegment]
4853
*
@@ -51,26 +56,8 @@ internal class DeepLinkPattern<T>(
5156
*/
5257
val pathSegments: List<PathSegment> =
5358
buildList {
54-
uriPattern.rawSegments.forEach { segment ->
55-
// first, check if it is a path arg
56-
var result = regexPatternFillIn.find(segment)
57-
if (result != null) {
58-
// if so, extract the path arg name (the string value within the curly braces)
59-
val argName = result.groups[1]!!.value
60-
// from [T], read the primitive type of this argument to get the correct type parser
61-
val elementIndex = serializer.descriptor.getElementIndex(argName)
62-
val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex)
63-
// Calculate prefix and suffix
64-
val range = result.range
65-
val prefix = segment.substring(0, range.first)
66-
val suffix = segment.substring(range.last + 1)
67-
68-
// finally, add the arg name and its respective type parser to the map
69-
add(PathSegment(argName, true, getTypeParser(elementDescriptor.kind), prefix, suffix))
70-
} else {
71-
// if its not a path arg, then its just a static string path segment
72-
add(PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING)))
73-
}
59+
uriPattern.rawSegments.normalizeRootPathSegments().forEach { segment ->
60+
add(parseSegment(segment))
7461
}
7562
}
7663

@@ -98,6 +85,28 @@ internal class DeepLinkPattern<T>(
9885
val prefix: String = "",
9986
val suffix: String = "",
10087
)
88+
89+
private fun parseSegment(segment: String): PathSegment {
90+
// first, check if it is an arg
91+
val result = regexPatternFillIn.find(segment)
92+
return if (result != null) {
93+
// if so, extract the arg name (the string value within the curly braces)
94+
val argName = result.groups[1]!!.value
95+
// from [T], read the primitive type of this argument to get the correct type parser
96+
val elementIndex = serializer.descriptor.getElementIndex(argName)
97+
val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex)
98+
// Calculate prefix and suffix
99+
val range = result.range
100+
val prefix = segment.substring(0, range.first)
101+
val suffix = segment.substring(range.last + 1)
102+
103+
// finally, add the arg name and its respective type parser to the map
104+
PathSegment(argName, true, getTypeParser(elementDescriptor.kind), prefix, suffix)
105+
} else {
106+
// if its not an arg, then its just a static string segment
107+
PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING))
108+
}
109+
}
101110
}
102111

103112
/**
@@ -129,3 +138,5 @@ private fun getTypeParser(kind: SerialKind): TypeParser =
129138
"Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive.",
130139
)
131140
}
141+
142+
private fun List<String>.normalizeRootPathSegments(): List<String> = takeUnless { size == 1 && single().isEmpty() }.orEmpty()

shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkRequest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal class DeepLinkRequest(
1414
/**
1515
* A list of path segments
1616
*/
17-
val pathSegments: List<String> = uri.rawSegments
17+
val pathSegments: List<String> = uri.rawSegments.normalizeRootPathSegments()
1818

1919
/**
2020
* A map of query name to query value
@@ -23,3 +23,5 @@ internal class DeepLinkRequest(
2323

2424
// TODO add parsing for other Uri components, i.e. fragments, mimeType, action
2525
}
26+
27+
private fun List<String>.normalizeRootPathSegments(): List<String> = takeUnless { size == 1 && single().isEmpty() }.orEmpty()

shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/PlatformDeepLinkMatcher.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ internal object PlatformDeepLinkMatcher {
4141
)
4242
val match = DeepLinkMatcher(request, pattern).match() ?: return null
4343
val data = KeyDecoder(match.args).decodeSerializableValue(match.serializer)
44+
if (!deepLink.matcher(data)) return null
4445
return Match(
4546
route = deepLink.callback(data),
46-
host = uriPattern.host,
47+
host = request.uri.host,
4748
)
4849
}
4950

shared/src/commonMain/kotlin/dev/dimension/flare/model/PlatformSpec.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public interface PlatformDataSourceContext {
6666
public data class PlatformDeepLink<T>(
6767
public val uriPattern: String,
6868
public val serializer: KSerializer<T>,
69+
public val matcher: (T) -> Boolean = { true },
6970
public val callback: (T) -> DeeplinkRoute,
7071
)
7172

shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/settings/LinkOpenDefaultsPresenter.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ public class LinkOpenDefaultsPresenter : PresenterBase<LinkOpenDefaultsPresenter
116116
spec
117117
.deepLinks(service.accountKey)
118118
.mapNotNull { deepLink ->
119-
Url(deepLink.uriPattern).host.takeIf { it.isNotBlank() }
119+
Url(deepLink.uriPattern).host.takeIf { host ->
120+
host.isNotBlank() && !host.contains('{') && !host.contains('}')
121+
}
120122
}.distinct()
121123
if (hosts.isEmpty()) {
122124
return@mapNotNull null

shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/PlatformDeepLinkMatcherTest.kt

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import dev.dimension.flare.ui.model.UiAccount
1010
import dev.dimension.flare.ui.route.DeeplinkRoute
1111
import kotlinx.collections.immutable.ImmutableList
1212
import kotlinx.collections.immutable.persistentMapOf
13+
import kotlinx.serialization.SerialName
14+
import kotlinx.serialization.Serializable
1315
import kotlin.test.Test
1416
import kotlin.test.assertEquals
1517
import kotlin.test.assertTrue
@@ -182,6 +184,106 @@ class PlatformDeepLinkMatcherTest {
182184
assertTrue(PlatformDeepLinkMatcher.matches("https://mastodon.social/about", mapping).isEmpty())
183185
}
184186

187+
@Test
188+
fun matchesHostArguments() {
189+
val account =
190+
UiAccount(
191+
accountKey = MicroBlogKey(id = "1", host = "fanbox.cc"),
192+
platformType = PlatformType.Fanbox,
193+
)
194+
val mapping =
195+
mapOf(
196+
account to
197+
listOf(
198+
PlatformDeepLink(
199+
uriPattern = "https://{creatorid}.fanbox.cc/posts/{id}",
200+
serializer = TestFanboxPostDeepLink.serializer(),
201+
callback = { data ->
202+
DeeplinkRoute.Article(
203+
accountType = AccountType.Specific(account.accountKey),
204+
articleKey = MicroBlogKey(id = data.id, host = "fanbox.cc"),
205+
)
206+
},
207+
),
208+
),
209+
)
210+
211+
val matches = PlatformDeepLinkMatcher.matches("https://turisasu.fanbox.cc/posts/11771479", mapping)
212+
213+
assertMatch(
214+
DeeplinkRoute.Article(
215+
accountType = AccountType.Specific(account.accountKey),
216+
articleKey = MicroBlogKey(id = "11771479", host = "fanbox.cc"),
217+
),
218+
"turisasu.fanbox.cc",
219+
matches[account],
220+
)
221+
}
222+
223+
@Test
224+
fun matchesHostArgumentsWithoutPath() {
225+
val account =
226+
UiAccount(
227+
accountKey = MicroBlogKey(id = "1", host = "fanbox.cc"),
228+
platformType = PlatformType.Fanbox,
229+
)
230+
val mapping =
231+
mapOf(
232+
account to
233+
listOf(
234+
PlatformDeepLink(
235+
uriPattern = "https://{creatorid}.fanbox.cc",
236+
serializer = TestFanboxCreatorDeepLink.serializer(),
237+
callback = { data ->
238+
DeeplinkRoute.Profile.User(
239+
accountType = AccountType.Specific(account.accountKey),
240+
userKey = MicroBlogKey(id = data.creatorId, host = "fanbox.cc"),
241+
)
242+
},
243+
),
244+
),
245+
)
246+
247+
val matches = PlatformDeepLinkMatcher.matches("https://turisasu.fanbox.cc/", mapping)
248+
249+
assertMatch(
250+
DeeplinkRoute.Profile.User(
251+
accountType = AccountType.Specific(account.accountKey),
252+
userKey = MicroBlogKey(id = "turisasu", host = "fanbox.cc"),
253+
),
254+
"turisasu.fanbox.cc",
255+
matches[account],
256+
)
257+
}
258+
259+
@Test
260+
fun hostArgumentMatcherRejectsWwwAsFanboxCreatorId() {
261+
val account =
262+
UiAccount(
263+
accountKey = MicroBlogKey(id = "1", host = "fanbox.cc"),
264+
platformType = PlatformType.Fanbox,
265+
)
266+
val mapping =
267+
mapOf(
268+
account to
269+
listOf(
270+
PlatformDeepLink(
271+
uriPattern = "https://{creatorid}.fanbox.cc",
272+
serializer = TestFanboxCreatorDeepLink.serializer(),
273+
matcher = { data -> data.creatorId != "www" },
274+
callback = { data ->
275+
DeeplinkRoute.Profile.User(
276+
accountType = AccountType.Specific(account.accountKey),
277+
userKey = MicroBlogKey(id = data.creatorId, host = "fanbox.cc"),
278+
)
279+
},
280+
),
281+
),
282+
)
283+
284+
assertTrue(PlatformDeepLinkMatcher.matches("https://www.fanbox.cc/", mapping).isEmpty())
285+
}
286+
185287
@Test
186288
fun matchesRealWorldLinks() {
187289
val mastodonAccount =
@@ -349,3 +451,16 @@ private fun deepLinkMapping(vararg accounts: UiAccount): Map<UiAccount, List<Pla
349451
}.build()
350452

351453
private val platformRegistry = testPlatformRegistry()
454+
455+
@Serializable
456+
private data class TestFanboxPostDeepLink(
457+
@SerialName("creatorid")
458+
val creatorId: String,
459+
val id: String,
460+
)
461+
462+
@Serializable
463+
private data class TestFanboxCreatorDeepLink(
464+
@SerialName("creatorid")
465+
val creatorId: String,
466+
)

0 commit comments

Comments
 (0)