Last active
May 7, 2020 14:53
-
-
Save alxgrk/ace9aa6adf00c2e54a002ab888e50409 to your computer and use it in GitHub Desktop.
Telegram Quiz Bot for Bash
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
############################################################################################################ | |
# # | |
# To run this script on startup (tested on Raspbian), do the following: # | |
# # | |
# 1. add /lib/systemd/system/quiz-bot-telegram.service file using `sudo` # | |
# ``` # | |
# Description=Quiz-Bot for Telegram # | |
# After=multi-user.target # | |
# # | |
# [Service] # | |
# Type=idle # | |
# User=pi # | |
# WorkingDirectory=/path/to/your/script # | |
# ExecStart=/bin/bash ./start-bot.sh -t {{YOUR_TOKEN}}" # | |
# # | |
# [Install] # | |
# WantedBy=multi-user.target # | |
# ``` # | |
# # | |
# 2. exec `sudo chmod 644 /lib/systemd/system/quiz-bot-telegram.service` # | |
# # | |
# 3. tell systemd to run it on boot # | |
# ``` # | |
# sudo systemctl daemon-reload # | |
# sudo systemctl enable sample.service # | |
# ``` # | |
# # | |
# To view logs run `sudo journalctl -u quiz-bot-telegram.service` | |
############################################################################################################ | |
[[ -z $DEBUG ]] && set -x && set -o functrace | |
TOKEN="" | |
BASE_URL="" | |
OFFSET="" | |
CURRENTLY_UPDATED_CHATS=() | |
##################### | |
# kvstore | |
##################### | |
function install_kvstore() { | |
# from https://github.com/ccarpita/kvstore | |
[[ ! -f ./kvstore ]] \ | |
&& curl -o kvstore https://raw.githubusercontent.com/ccarpita/kvstore/master/kvstore.sh \ | |
&& chmod +x kvstore | |
source kvstore load | |
} | |
function kvget() { | |
./kvstore get $1 $2 | |
} | |
export -f kvget | |
function kvset() { | |
./kvstore set $1 $2 $3 | |
} | |
export -f kvset | |
##################### | |
# util | |
##################### | |
function read_questions() { | |
# read all questions and store in global indexed array | |
mapfile QUESTIONS < <(find data/ -name "*.json") | |
} | |
function update_offset() { | |
local last_update_id=$1 | |
if [[ -n $last_update_id && $last_update_id != "null" ]] | |
then | |
kvset last-update-id "last-update-id" $((++last_update_id)) | |
export OFFSET="?offset=$last_update_id" | |
echo "New offset: $OFFSET" | |
fi | |
} | |
export -f update_offset | |
function send_message() { | |
local chat_id="$1" | |
local text="$2" | |
local additional_params=",$3" | |
local request_body='{ | |
"chat_id": "'$chat_id'", | |
"text": "'"$text"'"'"$additional_params"' | |
}' | |
curl -s $BASE_URL/sendMessage -H "Content-Type: application/json" -d "$request_body" | |
} | |
export -f send_message | |
function send_markdown_message() { | |
send_message "$1" "$2" '"parse_mode": "MarkdownV2"' | |
} | |
function send_silent_message() { | |
send_message "$1" "$2" '"disable_notification": true' | |
} | |
##################### | |
# initialization | |
##################### | |
function validate_token() { | |
if [[ -z $TOKEN ]]; then | |
echo "Please set '-t' for the bot's token." | |
exit 1 | |
fi | |
export BASE_URL=https://api.telegram.org/bot${TOKEN} | |
echo "Who is this bot we are talking about?" | |
curl -s $BASE_URL/getMe | jq . | |
echo -e \\n | |
} | |
function set_commands() { | |
local request_body='{ | |
"commands": [ { | |
"command": "start", | |
"description": "Start Bot." | |
}, { | |
"command": "scores", | |
"description": "Show scores per user." | |
}] | |
}' | |
curl -s $BASE_URL/setMyCommands -H "Content-Type: application/json" -d "$request_body" | |
} | |
###################### | |
# scores | |
##################### | |
function show_scores() { | |
local chat_id=$1 | |
local scores=$(kvget scores-per-chat $chat_id) | |
while read -r line | |
do | |
[[ -z $line ]] && continue | |
local user=$(echo $line | cut -d'=' -f1) | |
local value=$(echo $line | cut -d'=' -f2) | |
local valueInPercent=$(($(echo $value | cut -d'/' -f1) * 100 / $(echo $value | cut -d'/' -f2))) | |
local intermediate="$intermediate$user $value $valueInPercent\n" | |
done < <(echo $scores | sed 's/;/\n/g') | |
local count=1 | |
while read -r line | |
do | |
[[ -z $line ]] && continue | |
local user=$(echo $line | cut -d' ' -f1) | |
local value=$(echo $line | cut -d' ' -f2) | |
local valueInPercent=$(echo $line | cut -d' ' -f3) | |
local text="$text$((count++))\\\\. *$user:* $valueInPercent% _\\\\($value\\\\)_\n" | |
done < <(echo -e $intermediate | sort -rn -k3) | |
send_markdown_message $chat_id "$text" | |
} | |
export -f show_scores | |
function update_scores_per_chat() { | |
local chat_id=$1 | |
local correct_answer=$2 | |
local poll_answer="$3" | |
local poll_id=$(echo "$poll_answer" | jq -r ".poll_id") | |
local name=$(echo "$poll_answer" | jq -r ".user.first_name" | sed 's/[[:space:]]//g') # only required field in user | |
local answer=$(echo "$poll_answer" | jq -r ".option_ids[0]") | |
local was_correct=$([[ $answer =~ $correct_answer ]] && echo 1 || echo 0) | |
# of pattern: name1=m/n name2=… | |
local scores=$(kvget scores-per-chat $chat_id) | |
local updated=$([[ -n $(echo $scores | grep ".*$name") ]] \ | |
&& echo $scores | sed -r 's|(.*)'$name'=([[:digit:]]+)/([[:digit:]]+)(.*)|echo "\1'$name'=$((\2+'$was_correct'))/$((\3+1))\4"|e' \ | |
|| echo "$scores;$name=$was_correct/1") | |
kvset scores-per-chat $chat_id "$updated" | |
} | |
export -f update_scores_per_chat | |
##################### | |
# poll creation | |
##################### | |
function create_poll() { | |
local chat_id=$1 | |
local next_question_file_id=$(kvget chat-question-map "$chat_id") | |
local next_question_file=${QUESTIONS[${next_question_file_id:-0}]} | |
local question=$(jq ".question" $next_question_file) | |
local answers=$(jq ".answers" $next_question_file) | |
local correct_option=$(jq ".correctOption" $next_question_file) | |
kvset chat-question-map "$chat_id" $((++next_question_file_id)) | |
local request_body='{ | |
"chat_id": "'$chat_id'", | |
"question": '"$question"', | |
"options": '"$answers"', | |
"is_anonymous": false, | |
"type": "quiz", | |
"correct_option_id": '$correct_option', | |
"disable_notification": true | |
}' | |
local poll_created=$(curl -s $BASE_URL/sendPoll -H "Content-Type: application/json" -d "$request_body") | |
local poll_id=$(echo $poll_created | jq '.result.poll.id') | |
[[ -n $poll_id || $poll_id != "null" ]] \ | |
&& kvset poll-correct-answer-map $poll_id $correct_option | |
echo $poll_id | |
} | |
export -f create_poll | |
function send_poll() { | |
local chat_id=$1 | |
[[ -z $chat_id ]] && echo "Could not send poll for empty chat_id" >&2 \ | |
&& return | |
local poll_id=$(create_poll $chat_id) | |
[[ -z $poll_id || $poll_id = "null" ]] && echo "Could not extract poll_id" >&2 \ | |
&& return | |
echo "Created poll with id "$poll_id >&2 | |
kvset poll-chat-map $poll_id "$chat_id" | |
kvset chat-latest-poll-map "$chat_id" $poll_id | |
echo "Stored poll-chat-mapping $poll_id=$(kvget poll-chat-map $poll_id) sucessfully" >&2 | |
} | |
export -f send_poll | |
#################### | |
# react on changes | |
##################### | |
function on_start() { | |
local updates=$1 | |
local chat_ids=$(echo $updates | jq '.result[].message | select(.text=="/start") | .chat.id') | |
for chat_id in $(echo $chat_ids | uniq) | |
do | |
echo "Chat started: "$chat_id | |
send_poll "$chat_id" | |
CURRENTLY_UPDATED_CHATS+=($chat_id) | |
done | |
} | |
function on_poll_answer() { | |
local updates=$1 | |
local poll_answers=$(echo $updates | jq -c '.result[] | select(.poll_answer!=null) | .poll_answer') | |
for poll_answer in $poll_answers | |
do | |
local poll_answer_id=$(echo $poll_answer | jq '.poll_id' | sort | uniq) | |
local chat_id_from_poll_id=$(kvget poll-chat-map "$poll_answer_id" &2>/dev/null) | |
local latest_poll_id_for_chat_id=$(kvget chat-latest-poll-map "$chat_id_from_poll_id" &2>/dev/null) | |
[[ -z $chat_id_from_poll_id ]] \ | |
&& echo "Could not send next poll for previous poll with id $poll_answer_id, because there is no chat_id mapping" \ | |
&& continue | |
echo "Retrieved chat_id $chat_id_from_poll_id for poll_id $poll_answer_id" | |
local poll_id=$(echo "$poll_answer" | jq -r ".poll_id") | |
local correct_option=$(echo $updates | jq -r '.result[] | select(.poll.correct_option_id!=null) | select(.poll.id=="'$poll_id'") | .poll.correct_option_id') | |
update_scores_per_chat $chat_id_from_poll_id $correct_option "$poll_answer" | |
# only send new poll, if this hasn't happened in this run before | |
if [[ ! " ${CURRENTLY_UPDATED_CHATS[@]} " =~ " ${chat_id_from_poll_id} " ]] | |
then | |
[[ -z $latest_poll_id_for_chat_id || $latest_poll_id_for_chat_id == $poll_answer_id ]] \ | |
&& send_poll "$chat_id_from_poll_id" \ | |
&& CURRENTLY_UPDATED_CHATS+=("$chat_id_from_poll_id") | |
fi | |
# TODO: store timestamp and regularly remove stale entries | |
#./kvstore rm poll-chat-map "$poll_answer_id" &2>/dev/null | |
#echo "Removed poll-chat-mapping for poll_id $poll_answer_id" | |
done | |
} | |
function on_scores() { | |
local updates=$1 | |
local chat_ids=$(echo $updates | jq '.result[].message | select(.text | startswith("/scores")) | .chat.id') | |
for chat_id in $(echo $chat_ids | uniq) | |
do | |
echo "Showing scores for: "$chat_id | |
show_scores $chat_id | |
done | |
} | |
##################### | |
# | |
# MAIN ROUTINE | |
# | |
##################### | |
while getopts ":t:" opt; do | |
case $opt in | |
t) export TOKEN="$OPTARG" | |
;; | |
\?) echo "Invalid option -$opt" >&2 | |
;; | |
esac | |
done | |
install_kvstore | |
read_questions | |
validate_token | |
update_offset $[$(kvget last-update-id "last-update-id")-1] | |
set_commands | |
while : | |
do | |
sleep 5 & | |
timer=$! | |
updates=$(curl -s $BASE_URL/getUpdates$OFFSET) | |
echo "Got update for offset ${OFFSET:-0}" | |
echo $updates | jq . | |
echo -e \\n | |
on_start "$updates" | |
on_poll_answer "$updates" | |
on_scores "$updates" | |
update_offset $(echo $updates | jq ".result[-1].update_id") | |
echo "Waiting for timer to finish..." | |
wait $timer | |
CURRENTLY_UPDATED_CHATS=() | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment