aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaria Lisina <sekoohaka.sarisan@gmail.com>2025-04-14 05:29:28 +0300
committerMaria Lisina <sekoohaka.sarisan@gmail.com>2025-04-14 05:29:28 +0300
commit570cd31efa5194c57bc9ed59018a345d667cb075 (patch)
tree153e1d28578f3138e3681fad43b76d241ef46388
parent290c81bbd47f4ec35f1b3990305e4a4b645959cf (diff)
bot: Add initial SauceNAO support
Signed-off-by: Maria Lisina <sekoohaka.sarisan@gmail.com>
-rw-r--r--README.md3
-rw-r--r--files/help.txt.default2
-rw-r--r--modules/commands.sh3
-rw-r--r--submodules/callback_stop.sh2
-rw-r--r--submodules/command_saucenao.sh35
-rw-r--r--units/sn_auth.sh61
-rw-r--r--units/sn_get.sh77
-rw-r--r--units/sn_result.sh82
-rw-r--r--units/sn_search.sh63
9 files changed, 327 insertions, 1 deletions
diff --git a/README.md b/README.md
index dee84db..0bc0c3a 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ Telegram Image Board Bot written in POSIX Shell (dash)
* Post information view
* Post tags view
* MD5 hash lookup
+* SauceNAO reverse image search
* Inline shortcuts
* Inline shortcuts saving
* Inline shortcuts search
@@ -102,6 +103,7 @@ Caching modes:
* `[md5]` - MD5 hash
* `[login]` - Image Board login or username
* `[key]` - Image Board API key or password
+* `[snkey]` - SauceNAO API Key
### Inline search
@@ -134,6 +136,7 @@ Caching modes:
* `/original [b] [id/md5]` - Get original file of post
* `/post [b] [id/md5]` - Get information of post
* `/hash [md5]` - Get posts IDs by hash lookup
+* `/saucenao [snkey]` - Reverse search image
* `/short [query]` - Create inline shortcut
* `/shorts` - Manage saved shortcuts
* `/stop` - Remove all user data
diff --git a/files/help.txt.default b/files/help.txt.default
index 30b644d..427b644 100644
--- a/files/help.txt.default
+++ b/files/help.txt.default
@@ -12,6 +12,7 @@
[md5] - MD5 hash
[login] - Image Board login or username
[key] - Image Board API key or password
+[snkey] - SauceNAO API Key
<b>Inline search:</b>
<code>pools</code> [b] [page] [options] [name] - Pools search
@@ -40,6 +41,7 @@
/original [b] [id/md5] - Get original file of post
/post [b] [id/md5] - Get information of post
/hash [md5] - Get posts IDs by hash lookup
+/saucenao [snkey] - Reverse search image
/short [query] - Create inline shortcut
/shorts - Manage saved shortcuts
/stop - Remove all your data
diff --git a/modules/commands.sh b/modules/commands.sh
index e1f7f38..8457321 100644
--- a/modules/commands.sh
+++ b/modules/commands.sh
@@ -50,6 +50,9 @@ case "${command}" in
("/prpr" | "/prpr@${username}")
. "${submodules}/command_prpr.sh"
;;
+ ("/saucenao" | "/saucenao@${username}")
+ . "${submodules}/command_saucenao.sh"
+ ;;
("/short" | "/short@${username}")
. "${submodules}/command_short.sh"
;;
diff --git a/submodules/callback_stop.sh b/submodules/callback_stop.sh
index 311cd77..dc3e9c5 100644
--- a/submodules/callback_stop.sh
+++ b/submodules/callback_stop.sh
@@ -8,7 +8,7 @@ then
return 0
fi
-locks="auth short"
+locks="auth saucenao short"
for lock in ${locks}
do
diff --git a/submodules/command_saucenao.sh b/submodules/command_saucenao.sh
new file mode 100644
index 0000000..471d8ca
--- /dev/null
+++ b/submodules/command_saucenao.sh
@@ -0,0 +1,35 @@
+# Copyright (C) 2024-2025 Maria Lisina
+# Copyright (C) 2024-2025 Danil Lisin
+# SPDX-License-Identifier: Apache-2.0
+
+until mkdir "${user_config}_saucenao.lock"
+do
+ sleep 1
+done
+
+. "${units}/sn_auth.sh"
+
+if [ -n "${output_text}" ]
+then
+ rm -fr "${user_config}_saucenao.lock"
+ return 0
+fi
+
+. "${units}/sn_get.sh"
+
+if [ -n "${output_text}" ]
+then
+ rm -fr "${user_config}_saucenao.lock"
+ return 0
+fi
+
+. "${units}/sn_search.sh"
+
+if [ -n "${output_text}" ]
+then
+ rm -fr "${user_config}_saucenao.lock"
+ return 0
+fi
+
+. "${units}/sn_result.sh"
+rm -fr "${user_config}_saucenao.lock"
diff --git a/units/sn_auth.sh b/units/sn_auth.sh
new file mode 100644
index 0000000..0d595ac
--- /dev/null
+++ b/units/sn_auth.sh
@@ -0,0 +1,61 @@
+# Copyright (C) 2024-2025 Maria Lisina
+# Copyright (C) 2024-2025 Danil Lisin
+# SPDX-License-Identifier: Apache-2.0
+
+if [ -n "${1}" ]
+then
+ sn_key="${1}"
+elif ! [ -s "${user_config}/saucenao" ]
+then
+ output_text="You must provide your SauceNAO API Key before you can use SauceNAO"
+ return 0
+else
+ return 0
+fi
+
+sn_file="${cache}/${update_id}_search.json"
+dump="${dump} ${sn_file##*/}"
+
+if ! curl --data-urlencode "output_type=2" \
+ --data-urlencode "api_key=${sn_key}" \
+ --get \
+ --max-time ${external_timeout} \
+ --output "${sn_file}" \
+ --proxy "${external_proxy}" \
+ --silent \
+ --user-agent "Sekoohaka" \
+ "https://saucenao.com/search.php"
+then
+ output_text="Failed to process request"
+ log_text="sn_auth (${update_id}): ${output_text}"
+
+ . "${units}/log.sh"
+ . "${units}/dump.sh"
+
+ return 0
+fi
+
+if ! jq -e '.' "${sn_file}" > /dev/null
+then
+ output_text="An unknown error occurred"
+ log_text="sn_auth (${update_id}): ${output_text}"
+
+ . "${units}/log.sh"
+ . "${units}/dump.sh"
+
+ return 0
+fi
+
+if [ "$(jq -r '.header.user_id' "${sn_file}")" = "null" ]
+then
+ output_text="Invalid API Key"
+ log_text="sn_auth (${update_id}): ${output_text}"
+
+ . "${units}/log.sh"
+ . "${units}/dump.sh"
+
+ return 0
+fi
+
+output_text="Authorized successfully"
+printf "%s\n" "${sn_key}" > "${user_config}/saucenao"
diff --git a/units/sn_get.sh b/units/sn_get.sh
new file mode 100644
index 0000000..88a5da8
--- /dev/null
+++ b/units/sn_get.sh
@@ -0,0 +1,77 @@
+# Copyright (C) 2024-2025 Maria Lisina
+# Copyright (C) 2024-2025 Danil Lisin
+# SPDX-License-Identifier: Apache-2.0
+
+message_id="$(jq -r '.message.reply_to_message.message_id' "${update}")"
+file_id="$(jq -r '.message.reply_to_message.photo.[1].file_id' "${update}")"
+
+if [ "${message_id}" = "null" ]
+then
+ output_text="You must use this command in reply to an image"
+ return 0
+fi
+
+if [ "${file_id}" = "null" ]
+then
+ output_text="Could not find an image in replied message"
+ return 0
+fi
+
+output_file="${cache}/${update_id}_getFile.json"
+dump="${dump} ${output_file##*/}"
+
+if ! curl --data-urlencode "file_id=${file_id}" \
+ --get \
+ --max-time ${internal_timeout} \
+ --output "${output_file}" \
+ --proxy "${internal_proxy}" \
+ --silent \
+ "${api_address}/bot${api_token}/getFile"
+then
+ output_text="An unknown error occurred"
+ log_text="getFile (${update_id}): An unknown error occurred"
+
+ . "${units}/log.sh"
+ . "${units}/dump.sh"
+
+ return 0
+fi
+
+if ! jq -e '.' "${output_file}" > /dev/null
+then
+ output_text="An unknown error occurred"
+ log_text="getFile (${update_id}): An unknown error occurred"
+
+ . "${units}/log.sh"
+ . "${units}/dump.sh"
+
+ return 0
+fi
+
+if [ "$(jq -r '.ok' "${output_file}")" != "true" ]
+then
+ output_text="An unknown error occurred"
+ error_description="$(jq -r '.description' "${output_file}")"
+
+ if [ "${error_description}" != "null" ]
+ then
+ log_text="getFile (${update_id}): ${error_description}"
+ else
+ log_text="getFile (${update_id}): An unknown error occurred"
+ fi
+
+ . "${units}/log.sh"
+ . "${units}/dump.sh"
+
+ return 0
+fi
+
+file_path="$(jq -r '.result.file_path' "${output_file}")"
+
+if [ "${api_address}" = "127.0.0.1:8081" ] && ! ls "${file_path}" > /dev/null
+then
+ output_text="This command cannot be used, contact bot deployer"
+ log_text="Error: sn_get: Cannot access Bot API working directory"
+
+ . "${units}/log.sh"
+fi
diff --git a/units/sn_result.sh b/units/sn_result.sh
new file mode 100644
index 0000000..84f429c
--- /dev/null
+++ b/units/sn_result.sh
@@ -0,0 +1,82 @@
+# Copyright (C) 2024-2025 Maria Lisina
+# Copyright (C) 2024-2025 Danil Lisin
+# SPDX-License-Identifier: Apache-2.0
+
+minimum_similarity="$(jq -r ".header.index.minimum_similarity" "${sn_file}" | sed 's/\.//')"
+highest_similarity=0
+highest_index=0
+array_count=0
+
+while [ -n "${array_count}" ]
+do
+ similarity="$(jq -r ".results.[${array_count}].header.similarity" "${sn_file}" | sed 's/\.//')"
+
+ if [ "${similarity}" = "null" ]
+ then
+ break
+ fi
+
+ if [ ${highest_similarity} -lt ${similarity} ]
+ then
+ highest_similarity=${similarity}
+ highest_index=${array_count}
+ fi
+
+ array_count=$((array_count + 1))
+done
+
+if [ ${highest_similarity} -lt ${minimum_similarity} ]
+then
+ output_text="No results found"
+ return 0
+fi
+
+thumbnail="$(jq -r ".results.[${highest_index}].header.thumbnail" "${sn_file}")"
+similarity="$(printf "%.2f" "$(printf "%s" "${highest_similarity} / 100" | bc -l)")"
+
+link_preview_options="$(jq --null-input --compact-output \
+ --arg url "${thumbnail}" \
+ '{"url": $url, "prefer_small_media": true, "show_above_text": true}')"
+
+danbooru_id="$(jq -r ".results.[${highest_index}].data.danbooru_id" "${sn_file}")"
+gelbooru_id="$(jq -r ".results.[${highest_index}].data.gelbooru_id" "${sn_file}")"
+idol_id="$(jq -r ".results.[${highest_index}].data.idol_id" "${sn_file}")"
+konachan_id="$(jq -r ".results.[${highest_index}].data.konachan_id" "${sn_file}")"
+sankaku_id="$(jq -r ".results.[${highest_index}].data.sankaku_id" "${sn_file}")"
+yandere_id="$(jq -r ".results.[${highest_index}].data.yandere_id" "${sn_file}")"
+
+output_text="<b>SauceNAO</b>"
+output_text="$(printf "%s\n<b>Similarity:</b> %s%%" "${output_text}" "${similarity}")"
+
+if [ "${danbooru_id}" != "null" ]
+then
+ output_text="$(printf "%s\n<b>Danbooru ID:</b> <code>%s</code>" "${output_text}" "${danbooru_id}")"
+fi
+
+if [ "${gelbooru_id}" != "null" ]
+then
+ output_text="$(printf "%s\n<b>Gelbooru ID:</b> <code>%s</code>" "${output_text}" "${gelbooru_id}")"
+fi
+
+if [ "${idol_id}" != "null" ]
+then
+ idol_md5="$(jq -r ".results.[${highest_index}].header.index_name" "${sn_file}" | parameter 6 | cut -d '_' -f 1)"
+
+ output_text="$(printf "%s\n<b>Idol Complex ID:</b> <code>%s</code>" "${output_text}" "${idol_id}")"
+ output_text="$(printf "%s\n<b>Idol Complex MD5:</b> <code>%s</code>" "${output_text}" "${idol_md5}")"
+fi
+
+if [ "${konachan_id}" != "null" ]
+then
+ output_text="$(printf "%s\n<b>Konachan.com ID:</b> <code>%s</code>" "${output_text}" "${konachan_id}")"
+fi
+
+if [ "${sankaku_id}" != "null" ]
+then
+ output_text="$(printf "%s\n<b>Sankaku Channel ID:</b> <code>%s</code>" "${output_text}" "${sankaku_id}")"
+fi
+
+if [ "${yandere_id}" != "null" ]
+then
+ output_text="$(printf "%s\n<b>yande.re ID:</b> <code>%s</code>" "${output_text}" "${yandere_id}")"
+fi
diff --git a/units/sn_search.sh b/units/sn_search.sh
new file mode 100644
index 0000000..4070ed4
--- /dev/null
+++ b/units/sn_search.sh
@@ -0,0 +1,63 @@
+# Copyright (C) 2024-2025 Maria Lisina
+# Copyright (C) 2024-2025 Danil Lisin
+# SPDX-License-Identifier: Apache-2.0
+
+if [ "${api_address}" = "127.0.0.1:8081" ]
+then
+ sn_query="file=@${file_path}"
+else
+ sn_query="url=${api_address}/file/bot${api_token}/${file_path}"
+fi
+
+sn_key="$(cat "${user_config}/saucenao")"
+sn_file="${cache}/${update_id}_search.json"
+dump="${dump} ${sn_file##*/}"
+
+if ! curl --form "output_type=2" \
+ --form "api_key=${sn_key}" \
+ --form "dbs[]=9" \
+ --form "dbs[]=12" \
+ --form "dbs[]=25" \
+ --form "dbs[]=26" \
+ --form "dbs[]=27" \
+ --form "dbs[]=30" \
+ --form "dedupe=2" \
+ --form "${sn_query}" \
+ --get \
+ --max-time ${external_timeout} \
+ --output "${sn_file}" \
+ --proxy "${external_proxy}" \
+ --silent \
+ --user-agent "Sekoohaka" \
+ "https://saucenao.com/search.php"
+then
+ output_text="Failed to process request"
+ log_text="sn_search (${update_id}): ${output_text}"
+
+ . "${units}/log.sh"
+ . "${units}/dump.sh"
+
+ return 0
+fi
+
+if ! jq -e '.' "${sn_file}" > /dev/null
+then
+ output_text="An unknown error occurred"
+ log_text="sn_search (${update_id}): ${output_text}"
+
+ . "${units}/log.sh"
+ . "${units}/dump.sh"
+
+ return 0
+fi
+
+if [ "$(jq -r '.header.status' "${sn_file}")" != "0" ]
+then
+ output_text="Error: $(jq -r '.header.message' "${sn_file}")"
+ log_text="sn_search (${update_id}): $(jq -r '.header.status' "${sn_file}")"
+
+ . "${units}/log.sh"
+ . "${units}/dump.sh"
+
+ return 0
+fi