#!/bin/bash # this is just a library # you will need to provide the following in the implementation: # 1. the api url with it's corresponding token as # api_url="api.telegram.org/bot[insert your api token]/" # pay attention to the trailing forward slash # 2. the following files to read and write from # updates.txt # sentmsgs.txt # delmsgs.txt # 3. (optional) a directory for the bot's files as # bot_tmpdir="/some/path/" # pay attention to the trailing forward slash # IMPORTANT: this library is not asynchronous, only one message # is read at a time and is done so in order # IMPORTANT: this library uses the getUpdates method with # tcp_keepalive,NOT webhooks # attempts to grab the last update's update_id and prints it to stdout calc_offset() { local current="$(bc -l <<< "$(tail -n 1 "${bot_tmpdir}updates.txt" | \ grep -om1 "\"update_id\":[0-9]\+" | \ grep -om1 "[0-9]\+") + 1" 2>/dev/null)" [ ! -z "${current}" ] && printf "%d" "${current}" } # captures output of calc_offset to use as offset and fetches a single update with that offset # uses tcp_keepalive method for 300 seconds, but telegram only keeps the connection open for 60 seconds # prints result of fetching update if not empty and formats it for better use with the other functions getupd() { local offset="$(calc_offset)" local update="$(curl -sX GET --keepalive-time 300 "${api_url}getUpdates" -d "offset=${offset}" \ -d "limit=1" -d "timeout=300")" update="$(sed 's/{"ok":true,"result":\[\]}\|^{"ok":false.*\|^{"ok":true,"result":\[{\?//g;:a;N;$!ba;s/\n//g;s/}\]}$//g;' <<< "${update}")" [ ! -z "${update}" ] && printf "%s\n" "${update}" >> "${bot_tmpdir}updates.txt" } getmsg_id() { tail -n1 "${bot_tmpdir}${1}" | grep -om1\ "\"message\":{\"message_id\":[0-9]\+\|^\"message_id\":[0-9]\+" | grep -om1 "[0-9]\+" } getusr_id() { tail -n1 "${bot_tmpdir}${1}" | grep -om1\ "\"from\":{\"id\":[0-9]\+" | grep -om1 "[0-9]\+" } getchat_id() { tail -n1 "${bot_tmpdir}${1}" | grep -om1\ "\"chat\":{\"id\":-\?[0-9]\+" | grep -om1 "\-\?[0-9]\+" } # makes decoding emojis possible, important for sending them too utf-16-surrogate-pair-decode() { # shamelessly stolen from stackoverflow local out="$1" local remain="" local regexp='(.*)\\u[dD]([0-9a-fA-F]{3})\\u[dD]([0-9a-fA-F]{3})(.*)' while [[ "${out}" =~ $regexp ]] ; do # match 2 \udxxx hex values, calculate new U, then split and replace local W1="$(( ( 0xd${BASH_REMATCH[2]} & 0x3ff) <<10 ))" local W2="$(( 0xd${BASH_REMATCH[3]} & 0x3ff ))" U="$(( ( W1 | W2 ) + 0x10000 ))" remain="$(printf '\\U%8.8x' "${U}")${BASH_REMATCH[4]}${remain}" out="${BASH_REMATCH[1]}" done echo -e "${out}${remain}" } gettext() { local text="$(tail -n 1 "${bot_tmpdir}${1}" | grep -om1\ '{\?\("text"\|"caption"\):"\(\\"\|[^"]*\)*"\(,\|}\)' | grep -v "^{" | tail -n1)" grep "^\"text\"" <<< "${text}" >/dev/null && utf-16-surrogate-pair-decode "${text:8:-2}"\ || { grep "^\"caption\"" <<< "${text}" >/dev/null && utf-16-surrogate-pair-decode "${text:11:-2}" } } getusr_name(){ local user_name="$(tail -n 1 "${bot_tmpdir}${1}" | grep -om1\ '"first_name":"\(\\"\|[^"]*\)*",\("username"\|"language_code"\)' | head -n1)" grep "\"username\"$" <<< "${user_name}" >/dev/null && utf-16-surrogate-pair-decode "${user_name:14:-12}"\ || { grep "\"language_code\"$" <<< "${user_name}" >/dev/null && utf-16-surrogate-pair-decode "${user_name:14:-17}" } } getusrname(){ local username="$(tail -n 1 "${bot_tmpdir}${1}" | grep -om1\ '"username":"\(\\"\|[^"]*\)*"\(,"language_code"\|},"chat"\)' | head -n1)" grep "\"language_code\"$" <<< "${username}" >/dev/null && utf-16-surrogate-pair-decode "${username:12:-17}"\ || { grep "},\"chat\"$" <<< "${username}" >/dev/null && utf-16-surrogate-pair-decode "${username:12:-9}" } } getchat_title(){ local chat_title="$(tail -n 1 "${bot_tmpdir}${1}" | grep -om1\ '"title":"\(\\"\|[^"]*\)*","type":"group"' | head -n1)" [ ! -z "${chat_title}" ] && utf-16-surrogate-pair-decode "${chat_title:9:-16}"\ || getusr_name "${1}" } getcbk_data() { local callbacks="$(tail -n 1 "${bot_tmpdir}${1}"\ | grep "\"callback_query\"" | grep -om1 '"data":"\(\\"\|[^"]*\)*"}')" [ ! -z "${callbacks}" ] && utf-16-surrogate-pair-decode "${callbacks:8:-2}" } # $1 is name of a bash array with the number of columns in every row such as ( 1 1 3 ) for a 3 row keyboard in which the first and second row have 1 column, and the third has 3 # $2 is name of a bash array with as many text entries as there are columns, they are added left to right, up to down, to the rows/columns configuration passed as first arg # $3 is name of a bash array with as many data entries as there are columns, they are added left to right, up to down, to the rows/columns configuration passed as first arg mkinline_kbd(){ local -n rows="${1}" local -n text="${2}" local -n data="${3}" local kbd="{\"inline_keyboard\":[]}" local unit="\"text\":\"\",\"callback_data\":\"\"" local rowformat="[]" local colformat="{}" local rowfirstpass=1 local colfirstpass=1 for (( i=0; i<"${#rows[@]}"; i++ )); do [ "${rowfirstpass}" -eq 1 ] && { kbd="$(sed "s/]}$/${rowformat}]}/" <<< "${kbd}")" rowfirstpass=0 } || kbd="$(sed "s/]}$/,${rowformat}]}/" <<< "${kbd}")" for (( j=0; j<"${rows[${i}]}"; j++)); do [ "${colfirstpass}" -eq 1 ] && { kbd="$(sed "s/]]}$/${colformat}]]}/" <<< "${kbd}")" colfirstpass=0 } || kbd="$(sed "s/]]}$/,${colformat}]]}/" <<< "${kbd}")" kbd="$(sed "s/}]]}$/${unit}}]]}/" <<< "${kbd}")" done colfirstpass=1 done for i in "${text[@]}"; do kbd="$(sed "s/\"text\":\"\"/\"text\":\"${i}\"/" <<< "${kbd}")" done for i in "${data[@]}"; do kbd="$(sed "s/\"callback_data\":\"\"/\"callback_data\":\"${i}\"/" <<< "${kbd}")" done sed "s/ /+/g;:a;N;$!ba;s/\n/%0A/g" <<< "${kbd}" } # $1 is target chat_id # $2 is text # $3 is inline keyboard (optional) sendmsg() { local sentmsg [ -z "${3}" ] && sentmsg="$(curl -sX GET "${api_url}sendMessage" -d "chat_id=${1}" \ -d "text=${2}")" \ || sentmsg="$(curl -sX GET "${api_url}sendMessage" -d "chat_id=${1}" \ -d "text=${2}" \ -d "reply_markup=${3}")" sentmsg="$(sed 's/^{"ok":false.*\|^{"ok":true,"result":{\|}$//g' <<< "${sentmsg}")" [ ! -z "${sentmsg}" ] && printf "%s\n" "${sentmsg}" >> "${bot_tmpdir}sentmsgs.txt" } # $1 is target chat_id # $2 is target msg_id # $3 is text # $4 is inline keyboard (optional) replymsg() { local sentmsg [ -z "${4}" ] &&\ sentmsg="$(curl -sX GET "${api_url}sendMessage" -d "chat_id=${1}" -d "reply_to_message_id=${2}"\ -d "text=${3}")" || sentmsg="$(curl -sX GET "${api_url}sendMessage" -d "chat_id=${1}" -d "reply_to_message_id=${2}"\ -d "text=${3}" -d "reply_markup=${4}")" sentmsg="$(sed 's/^{"ok":false.*\|^{"ok":true,"result":{\|}$//g' <<< "${sentmsg}")" [ ! -z "${sentmsg}" ] && printf "%s\n" "${sentmsg}" >> "${bot_tmpdir}sentmsgs.txt" } # $1 is target chat_id # $2 is target msg_id # $3 is text # $4 is inline keyboard (optional) editmsg() { local editmsg [ -z "${4}" ] && editmsg="$(curl -sX GET "${api_url}editMessageText" -d "chat_id=${1}" -d "message_id=${2}" \ -d "text=${3}")" || editmsg="$(curl -sX GET "${api_url}editMessageText" -d "chat_id=${1}" -d "message_id=${2}" \ -d "text=${3}" -d "reply_markup=${4}")" editmsg="$(sed 's/^{"ok":false.*\|^{"ok":true,"result":{\|}$//g' <<< "${editmsg}")" [ ! -z "${editmsg}" ] && printf "%s\n" "${editmsg}" >> "${bot_tmpdir}sentmsgs.txt" } # $1 is target chat_id # $2 is target msg_id delmsg() { local delmsg delmsg="$(curl -sX GET "${api_url}deleteMessage" -d "chat_id=${1}"\ -d "message_id=${2}")" [ ! -z "${delmsg}" ] && printf "deleted message %s from chat %s\n" "${2}" "${1}" >> "${bot_tmpdir}delmsgs.txt" } # $1 is the name of a bash associative array to be filled with the message contents getmsg_content(){ local -n _REF="${1}" _REF[user_id]="$(getusr_id "${2}")" _REF[user_name]="$(getusr_name "${2}")" _REF[username]="$(getusrname "${2}")" _REF[chat_id]="$(getchat_id "${2}")" _REF[chat_title]="$(getchat_title "${2}")" _REF[msg_id]="$(getmsg_id "${2}")" _REF[text]="$(gettext "${2}")" _REF[callback]="$(getcbk_data "${2}")" }