Skip to content

Instantly share code, notes, and snippets.

@alxgrk
Last active May 7, 2020 14:53
Show Gist options
  • Save alxgrk/ace9aa6adf00c2e54a002ab888e50409 to your computer and use it in GitHub Desktop.
Save alxgrk/ace9aa6adf00c2e54a002ab888e50409 to your computer and use it in GitHub Desktop.
Telegram Quiz Bot for Bash
#!/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