mirror of
https://github.com/Stypox/dicio-android.git
synced 2026-01-09 06:12:01 +08:00
[Search] Reorganize skill output and detect recaptchas
This commit is contained in:
parent
e2c5dc9808
commit
c16625f7f3
@ -21,65 +21,21 @@ import org.dicio.skill.context.SkillContext
|
||||
import org.dicio.skill.skill.InteractionPlan
|
||||
import org.dicio.skill.skill.SkillOutput
|
||||
import org.stypox.dicio.R
|
||||
import org.stypox.dicio.io.graphical.Headline
|
||||
import org.stypox.dicio.io.graphical.HeadlineSpeechSkillOutput
|
||||
import org.stypox.dicio.sentences.Sentences
|
||||
import org.stypox.dicio.skills.search.SearchOutput.Data
|
||||
import org.stypox.dicio.util.RecognizeEverythingSkill
|
||||
import org.stypox.dicio.util.ShareUtils
|
||||
import org.stypox.dicio.util.getString
|
||||
|
||||
class SearchOutput(
|
||||
private val results: List<Data>?,
|
||||
private val askAgain: Boolean,
|
||||
) : SkillOutput {
|
||||
class Data (
|
||||
val title: String,
|
||||
val thumbnailUrl: String,
|
||||
val url: String,
|
||||
val description: String,
|
||||
)
|
||||
sealed interface SearchOutput : SkillOutput {
|
||||
|
||||
override fun getSpeechOutput(ctx: SkillContext): String = ctx.getString(
|
||||
if (results == null)
|
||||
R.string.skill_search_what_question
|
||||
else if (results.isNotEmpty())
|
||||
R.string.skill_search_here_is_what_i_found
|
||||
else if (askAgain)
|
||||
R.string.skill_search_no_results
|
||||
else
|
||||
// if the search continues to return 0 results, don't keep asking
|
||||
R.string.skill_search_no_results_stop
|
||||
)
|
||||
data class Results(private val results: List<Data>) : SearchOutput {
|
||||
override fun getSpeechOutput(ctx: SkillContext): String =
|
||||
ctx.getString(R.string.skill_search_here_is_what_i_found)
|
||||
|
||||
override fun getInteractionPlan(ctx: SkillContext): InteractionPlan {
|
||||
if (!results.isNullOrEmpty() || !askAgain) {
|
||||
return InteractionPlan.FinishInteraction
|
||||
}
|
||||
|
||||
val searchAnythingSkill = object : RecognizeEverythingSkill(SearchInfo) {
|
||||
override suspend fun generateOutput(
|
||||
ctx: SkillContext,
|
||||
inputData: String
|
||||
): SkillOutput {
|
||||
// ask again only if this is the first time we ask the user to provide what
|
||||
// to search for, otherwise we could continue asking indefinitely
|
||||
return SearchOutput(searchOnDuckDuckGo(ctx, inputData), results == null)
|
||||
}
|
||||
}
|
||||
|
||||
return InteractionPlan.StartSubInteraction(
|
||||
reopenMicrophone = true,
|
||||
nextSkills = listOf(
|
||||
SearchSkill(SearchInfo, Sentences.Search[ctx.sentencesLanguage]!!),
|
||||
searchAnythingSkill,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun GraphicalOutput(ctx: SkillContext) {
|
||||
if (results.isNullOrEmpty()) {
|
||||
Headline(text = getSpeechOutput(ctx))
|
||||
} else {
|
||||
@Composable
|
||||
override fun GraphicalOutput(ctx: SkillContext) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
@ -89,15 +45,70 @@ class SearchOutput(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object NoSearchTerm : SearchOutput, HeadlineSpeechSkillOutput {
|
||||
override fun getSpeechOutput(ctx: SkillContext): String =
|
||||
ctx.getString(R.string.skill_search_what_question)
|
||||
|
||||
override fun getInteractionPlan(ctx: SkillContext): InteractionPlan =
|
||||
getRetryInteractionPlan(ctx)
|
||||
}
|
||||
|
||||
object NoResultAskAgain : SearchOutput, HeadlineSpeechSkillOutput {
|
||||
override fun getSpeechOutput(ctx: SkillContext): String =
|
||||
ctx.getString(R.string.skill_search_no_results)
|
||||
|
||||
override fun getInteractionPlan(ctx: SkillContext): InteractionPlan =
|
||||
getRetryInteractionPlan(ctx)
|
||||
}
|
||||
|
||||
object NoResultStop : SearchOutput, HeadlineSpeechSkillOutput {
|
||||
override fun getSpeechOutput(ctx: SkillContext): String =
|
||||
ctx.getString(R.string.skill_search_no_results_stop)
|
||||
}
|
||||
|
||||
object RecaptchaRequested : SearchOutput, HeadlineSpeechSkillOutput {
|
||||
override fun getSpeechOutput(ctx: SkillContext): String =
|
||||
ctx.getString(R.string.skill_search_duckduckgo_recaptcha)
|
||||
}
|
||||
|
||||
|
||||
class Data (
|
||||
val title: String,
|
||||
val thumbnailUrl: String,
|
||||
val url: String,
|
||||
val description: String,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun getRetryInteractionPlan(ctx: SkillContext): InteractionPlan {
|
||||
val searchAnythingSkill = object : RecognizeEverythingSkill(SearchInfo) {
|
||||
override suspend fun generateOutput(
|
||||
ctx: SkillContext,
|
||||
inputData: String
|
||||
): SkillOutput {
|
||||
// ask again only if this is the first time we ask the user to provide what
|
||||
// to search for, otherwise we could continue asking indefinitely
|
||||
return searchOnDuckDuckGo(ctx, inputData, askAgainIfNoResult = false)
|
||||
}
|
||||
}
|
||||
|
||||
return InteractionPlan.StartSubInteraction(
|
||||
reopenMicrophone = true,
|
||||
nextSkills = listOf(
|
||||
SearchSkill(SearchInfo, Sentences.Search[ctx.sentencesLanguage]!!),
|
||||
searchAnythingSkill,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchResult(data: SearchOutput.Data) {
|
||||
private fun SearchResult(data: Data) {
|
||||
val context = LocalContext.current
|
||||
Column(
|
||||
modifier = Modifier.clickable {
|
||||
ShareUtils.openUrlInBrowser(context, data.url)
|
||||
}
|
||||
modifier = Modifier.clickable { ShareUtils.openUrlInBrowser(context, data.url) }
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
@ -15,10 +15,8 @@ import org.stypox.dicio.util.LocaleUtils
|
||||
class SearchSkill(correspondingSkillInfo: SkillInfo, data: StandardRecognizerData<Search>)
|
||||
: StandardRecognizerSkill<Search>(correspondingSkillInfo, data) {
|
||||
override suspend fun generateOutput(ctx: SkillContext, inputData: Search): SkillOutput {
|
||||
val query = when (inputData) {
|
||||
is Search.Query -> inputData.what ?: return SearchOutput(null, true)
|
||||
}
|
||||
return SearchOutput(searchOnDuckDuckGo(ctx, query), true)
|
||||
val query = when (inputData) { is Search.Query -> inputData.what }
|
||||
return searchOnDuckDuckGo(ctx, query, true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,17 +29,25 @@ private val DUCK_DUCK_GO_SUPPORTED_LOCALES = listOf(
|
||||
"it-it", "jp-jp", "kr-kr", "lv-lv", "lt-lt", "my-en", "mx-es", "nl-nl", "nz-en",
|
||||
"no-no", "pk-en", "pe-es", "ph-en", "pl-pl", "pt-pt", "ro-ro", "ru-ru", "xa-ar",
|
||||
"sg-en", "sk-sk", "sl-sl", "za-en", "es-ca", "es-es", "se-sv", "ch-de", "ch-fr",
|
||||
"tw-tz", "th-en", "tr-tr", /*"us-en",*/ "us-es", "ua-uk", "uk-en", "vn-en"
|
||||
"tw-tz", "th-en", "tr-tr", "us-en", "us-es", "ua-uk", "uk-en", "vn-en"
|
||||
)
|
||||
|
||||
internal fun searchOnDuckDuckGo(ctx: SkillContext, query: String): List<SearchOutput.Data> {
|
||||
internal fun searchOnDuckDuckGo(
|
||||
ctx: SkillContext,
|
||||
query: String?,
|
||||
askAgainIfNoResult: Boolean,
|
||||
): SearchOutput {
|
||||
if (query.isNullOrBlank()) {
|
||||
return SearchOutput.NoSearchTerm
|
||||
}
|
||||
|
||||
// find the locale supported by DuckDuckGo that matches the user locale the most
|
||||
val locale = LocaleUtils.resolveValueForSupportedLocale(
|
||||
ctx.locale,
|
||||
DUCK_DUCK_GO_SUPPORTED_LOCALES.associateBy {
|
||||
// DuckDuckGo locale names have first the country and then the language, but the locale
|
||||
// selection function assumes the opposite
|
||||
it.split("-").reversed().joinToString(separator = "_")
|
||||
it.split("-").reversed().joinToString(separator = "-")
|
||||
}
|
||||
// default to English when no locale is supported
|
||||
) ?: "us-en"
|
||||
@ -57,16 +63,21 @@ internal fun searchOnDuckDuckGo(ctx: SkillContext, query: String): List<SearchOu
|
||||
)
|
||||
)
|
||||
|
||||
// Sometimes DuckDuckGo replies with a recaptcha request, and no results are provided then
|
||||
if (html.contains("anomaly-modal__title")) {
|
||||
return SearchOutput.RecaptchaRequested
|
||||
}
|
||||
|
||||
val document: Document = Jsoup.parse(html)
|
||||
val elements = document.select("div[class=links_main links_deep result__body]")
|
||||
val result: MutableList<SearchOutput.Data> = ArrayList()
|
||||
val results: MutableList<SearchOutput.Data> = ArrayList()
|
||||
for (element in elements) {
|
||||
try {
|
||||
// the url is under the "uddg" query parameter
|
||||
val ddgUrl = element.select("a[class=result__a]").first()!!.attr("href")
|
||||
val url = ddgUrl.toUri().getQueryParameter("uddg")!!
|
||||
|
||||
result.add(
|
||||
results.add(
|
||||
SearchOutput.Data(
|
||||
title = element.select("a[class=result__a]").first()!!.text(),
|
||||
thumbnailUrl = "https:" + element.select("img[class=result__icon__img]")
|
||||
@ -79,5 +90,13 @@ internal fun searchOnDuckDuckGo(ctx: SkillContext, query: String): List<SearchOu
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return if (results.isEmpty()) {
|
||||
if (askAgainIfNoResult) {
|
||||
SearchOutput.NoResultAskAgain
|
||||
} else {
|
||||
SearchOutput.NoResultStop
|
||||
}
|
||||
} else {
|
||||
SearchOutput.Results(results)
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,4 +245,5 @@
|
||||
<string name="skill_weather_miles_per_hour">mph</string>
|
||||
<string name="failed_to_copy">Failed to copy to clipboard</string>
|
||||
<string name="skill_translation_auto">Auto</string>
|
||||
<string name="skill_search_duckduckgo_recaptcha">DuckDuckGo did not provide results, asking for a Captcha to be solved</string>
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user