Skip to content

Commit

Permalink
Fix chat not loading with Trusted Types enforcement
Browse files Browse the repository at this point in the history
  • Loading branch information
arkon committed Aug 11, 2024
1 parent d07d59e commit e67674a
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 8 deletions.
2 changes: 1 addition & 1 deletion app/src/main/assets/ChatInjector.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ window.fetch = async (...args) => {
window.addEventListener('messageReceive', d => messageReceiveCallback(d.detail));

// Send processed data back to app
window.addEventListener('messagePostProcess', d => window.Android.receiveMessages(d.detail))
window.addEventListener('messagePostProcess', d => window.livetl.receiveMessages(d.detail))

const isReplay = window.location.href.startsWith('https://www.youtube.com/live_chat_replay');

Expand Down
47 changes: 46 additions & 1 deletion app/src/main/kotlin/com/livetl/android/data/chat/ChatService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package com.livetl.android.data.chat

import android.content.Context
import android.webkit.JavascriptInterface
import android.webkit.MimeTypeMap
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.ui.util.fastForEach
Expand All @@ -13,6 +16,11 @@ import com.livetl.android.util.setDefaultSettings
import com.livetl.android.util.toDebugTimestampString
import com.livetl.android.util.withUIContext
import dagger.hilt.android.qualifiers.ApplicationContext
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.util.flattenEntries
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
Expand All @@ -26,8 +34,10 @@ import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import logcat.logcat
import java.nio.charset.StandardCharsets
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.microseconds
Expand All @@ -38,13 +48,48 @@ class ChatService @Inject constructor(
@ApplicationContext context: Context,
private val json: Json,
private val chatUrlFetcher: ChatUrlFetcher,
private val client: HttpClient,
) {

private val webview by lazy {
WebView(context).apply {
setDefaultSettings()
addJavascriptInterface(this@ChatService, "Android")
addJavascriptInterface(this@ChatService, "livetl")
webViewClient = object : WebViewClient() {
// Overwrite "Content-Security-Policy": "require-trusted-types-for 'script'"
// so that we can inject our interceptor script to get the chat data
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?,
): WebResourceResponse? {
if (request == null || request.url == null) {
return null
}

val url = request.url.toString()

if (ChatUrlFetcher.EMBED_SUFFIX !in url) {
return super.shouldInterceptRequest(view, request)
}

return runBlocking {
val result = client.get(url) {
headers {
request.requestHeaders.forEach { (name, value) -> set(name, value) }
}
}

WebResourceResponse(
MimeTypeMap.getFileExtensionFromUrl(url),
StandardCharsets.UTF_8.name(),
result.status.value,
result.status.description,
result.headers.flattenEntries().toMap() - "content-security-policy",
result.body(),
)
}
}

override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
injectScript(context.readAssetFile("ChatInjector.js"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ChatUrlFetcher @Inject constructor(private val client: HttpClient) {
val urlPrefix = "https://www.youtube.com/live_chat"

if (isLive) {
return "$urlPrefix?v=$videoId&embed_domain=www.livetl.app"
return "$urlPrefix?v=$videoId&$EMBED_SUFFIX"
}

val result = client.get("https://www.youtube.com/watch?v=$videoId") {
Expand All @@ -27,7 +27,11 @@ class ChatUrlFetcher @Inject constructor(private val client: HttpClient) {
}

val continuation = matches.group(1)
return "${urlPrefix}_replay?continuation=$continuation&embed_domain=www.livetl.app"
return "${urlPrefix}_replay?continuation=$continuation&$EMBED_SUFFIX"
}

companion object {
const val EMBED_SUFFIX = "embed_domain=www.livetl.app"
}
}

Expand Down
6 changes: 2 additions & 4 deletions app/src/main/kotlin/com/livetl/android/util/WebViewUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ import android.webkit.WebSettings
import android.webkit.WebView
import com.livetl.android.BuildConfig

fun createScriptTag(js: String): String = """<script type="text/javascript">$js</script>""".trimIndent()

fun WebView.injectScript(js: String) {
val encodedJs = Base64.encodeToString(js.toByteArray(), Base64.NO_WRAP)
loadUrl(
"""
javascript:(function() {
var parent = document.getElementsByTagName('head').item(0);
var script = document.createElement('script');
const parent = document.getElementsByTagName('head').item(0);
const script = document.createElement('script');
script.type = 'text/javascript';
script.innerHTML = window.atob('$encodedJs');
parent.appendChild(script);
Expand Down

0 comments on commit e67674a

Please sign in to comment.