.gear/rules | 3 + .gear/tags/list | 1 + .gear/zabbix-in-telegram.spec | 65 +++ LICENSE.txt | 2 +- README.md | 51 +-- ZbxTgDaemon.py | 174 ++++++++ bash-old-version/tg_vars.cfg.example | 11 + bash-old-version/zbxtg.sh | 142 +++++++ requirements.txt | 3 + tg_vars.cfg.example | 10 - zbxtg.py | 747 +++++++++++++++++++++++++++++------ zbxtg.sh | 111 ------ zbxtg_settings.example.py | 57 ++- 13 files changed, 1094 insertions(+), 283 deletions(-) diff --git a/.gear/rules b/.gear/rules new file mode 100644 index 0000000..2319eee --- /dev/null +++ b/.gear/rules @@ -0,0 +1,3 @@ +tar: upstream:. +diff: upstream:. . +spec: .gear/zabbix-in-telegram.spec diff --git a/.gear/tags/list b/.gear/tags/list new file mode 100644 index 0000000..71c721b --- /dev/null +++ b/.gear/tags/list @@ -0,0 +1 @@ +c23673b50fa2f10c40ee53ad5cad6f90212a071a upstream diff --git a/.gear/zabbix-in-telegram.spec b/.gear/zabbix-in-telegram.spec new file mode 100644 index 0000000..90270c3 --- /dev/null +++ b/.gear/zabbix-in-telegram.spec @@ -0,0 +1,65 @@ +%define z_dir %_sysconfdir/zabbix/alertscripts + +Name: zabbix-in-telegram +Version: 20200425 +Release: alt1 + +Summary: Zabbix Notifications with graphs in Telegram + +License: MIT +Group: Monitoring +URL: https://github.com/ableev/Zabbix-in-Telegram + +BuildArch: noarch + +# Source-git: https://github.com/ableev/Zabbix-in-Telegram.git +Source: %name-%version.tar +Patch: %name-%version-%release.patch + +BuildRequires(pre): rpm-build-python3 +BuildRequires(pre): rpm-build-intro + +Requires: zabbix-server-common > 3.0.0 + +%add_python3_req_skip zbxtg_settings + +# generated by 'epm restore --dry-run' from zabbix-in-telegram/requirements.txt +%py3_use socks >= 1.6.8 +%py3_use requests >= 2.20.0 +%py3_use requests-oauthlib >= 0.6.2 + +%description +Zabbix Notifications with graphs in Telegram. + +%prep +%setup +sed -i 's|#!/usr/bin/env python|#!/usr/bin/env python3|' \ + $(find ./ -name '*.py') + +%build + +%install +mkdir -p %buildroot%z_dir +install -p -m 755 zbxtg.py %buildroot%z_dir/zbxtg.py +install -p -m 644 zbxtg_settings.example.py %buildroot%z_dir/zbxtg_settings.py + +%files +%z_dir/zbxtg.py +%config(noreplace) %z_dir/zbxtg_settings.py +%doc README.md LICENSE.txt + + +%changelog +* Sat Jul 03 2021 Vitaly Lipatov 20200425-alt1 +- update to 4ca3585b4b568060370f17a864a8cbceb14438ca +- add requires + +* Fri Nov 15 2019 Andrey Bychkov 20160607-alt3 +- python2 -> python3 + +* Tue Jun 7 2016 Terechkov Evgenii 20160607-alt2 +- c23673b + +* Tue Jun 7 2016 Terechkov Evgenii 20160607-alt1 +- Initial build for ALT Linux Sisyphus (77e163b) + diff --git a/LICENSE.txt b/LICENSE.txt index 10a8634..d3023c7 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Ilya Ableev +Copyright (c) 2019 Ilya Ableev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0147ae7..193d3b2 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,39 @@ # Zabbix-in-Telegram Zabbix Notifications with graphs in Telegram -Join us in our **Telegram group** via this link: https://telegram.me/ZbxTg +Join us in our **Telegram group** via this link: https://t.me/ZbxTg -Subscribe to our channel: https://telegram.me/Zabbix_in_Telegram +Subscribe to our channel: https://t.me/Zabbix_in_Telegram Rate on [share.zabbix.com](https://share.zabbix.com): https://share.zabbix.com/cat-notifications/zabbix-in-telegram ### Features - [x] Graphs based on latest data are sent directly to your messenger -- [x] You can send messages both in private and group chats -- [x] Channels support +- [x] You can send messages both in private and group/supergroup chats +- [x] Channels support (only public, but you can do it for private as well with dirty hack) - [x] Saves chatid as a temporary file - [x] Simple markdown and HTML are supported -- [x] Emoji in messages +- [x] Emoji (you can use emoji instead of severity, see [the wiki article](https://github.com/ableev/Zabbix-in-Telegram/wiki/Trigger-severity-as-Emoji)) (zabbix doesn't support utf8mb4 encoding yet) +- [x] Location map ### TODOs -- Simple zabbix's management via bot's commands +- Simple zabbix's management via bot's commands – in dev state - Ability to send complex graph or part of screen ### Configuration / Installation -**First of all**: You need to install the `requests` module for python, this is required for operation!
- To do so, enter `pip install requests` in your commandline! +**READ WIKI IF YOU HAVE PROBLEM WITH SOMETHING**: https://github.com/ableev/Zabbix-in-Telegram/wiki - * Put `zbxtg.py` in your `AlertScriptsPath` directory, the path is set inside your zabbix_server.conf - * Put `zbxtg_group.py` in the same location if you want to send messages to the group chat - * Create `zbxtg_settings.py` with your settings and save them in the same directory as the script, see example for layout - * Create a bot in Telegram and get API key - * Create readonly user in Zabbix (for getting graphs images from zabbix) - * Set proxy host:port in `zbxtg_settings.py` if you need an internet proxy +**First of all**: You need to install the appropriate modules for python, this is required for operation!
+ To do so, enter `pip install -r requirements.txt` in your commandline! + + * Put `zbxtg.py` in your `AlertScriptsPath` directory, the path is set inside your `zabbix_server.conf` + * Put `zbxtg_group.py` in the same location if you want to send messages to the group chat (if you are using Zabbix 2.x version) + * Create `zbxtg_settings.py` (copy it from `zbxtg_settings.example.py`) with your settings and save them in the same directory as the script, see example for layout + * Create a bot in Telegram and get API key: https://core.telegram.org/bots#creating-a-new-bot + * Create readonly user in Zabbix web interface (for getting graphs from zabbix) + * Set proxy host:port in `zbxtg_settings.py` if you need an internet proxy (socks5 supported as well, the wiki will help you) * Add new media for Telegram in Zabbix web interface with these settings: @@ -41,7 +44,7 @@ Rate on [share.zabbix.com](https://share.zabbix.com): https://share.zabbix.com/c * **Note that Zabbix 3.0 has different settings for that step, see it there**: https://github.com/ableev/Zabbix-in-Telegram/wiki/Working-with-Zabbix-3.0 * Send a message to your bot via Telegram, e.g. "/start" - * If you are in group chat, just mention your bot, e.g. `@ZbxTgDevBot ping` + * If you are in a group chat, start a conversation with your bot: `/start@ZbxTgDevBot` * Create a new action like this: ``` Last value: {ITEM.LASTVALUE1} ({TIME}) @@ -55,8 +58,9 @@ zbxtg;title:{HOST.HOST} - {TRIGGER.NAME} * Add the appropriate Media Type to your user * The username is **CASE-SENSITIVE** + * If you don't have a username, you can use your chatid directly (and you need to google how to get it) * Group chats don't have URLs, so you need to put group's name in media type - * Messages for channels should be sent as for private chats + * Messages for channels should be sent as for private chats (simply add bot to your channel first and use channel's username as if it was a real user) * Private: @@ -73,9 +77,12 @@ zbxtg;graphs_period=10800 -- set graphs period (default - 3600 seconds) zbxtg;graphs_width=700 -- set graphs width (default - 900px) zbxtg;graphs_height=300 -- set graphs height (default - 300px) zbxtg;itemid:{ITEM.ID1} -- define itemid (from trigger) for attach -zbxtg;title:{HOST.HOST} - {TRIGGER.NAME} -- graph title +zbxtg;itemid:{ITEM.ID1},{ITEM.ID2},{ITEM.ID3} -- same, but if you want to send two or more graphs, use complex trigger +zbxtg;title:{HOST.HOST} - {TRIGGER.NAME} -- graph's title zbxtg;debug -- enables debug mode, some logs and images will be saved in the tmp dir (temporary doesn't affect python version) zbxtg;channel -- enables sending to channels +zbxtg;to:username1,username2,username3 -- now you don't need to create dedicated profiles and add media for them, use this option in action to send messages to those user(s) +zbxtg;to_group:Group Name One,Group Name Two -- the same but for groups ``` You can use markdown or html formatting in your action: https://core.telegram.org/bots/api#markdown-style + https://core.telegram.org/bots/api#html-style. @@ -83,10 +90,10 @@ You can use markdown or html formatting in your action: https://core.telegram.or #### Debug * You can use the following command to send a message from your command line:
-`./zbxtg.py "" "" "" --debug` - * For `` substitute your Telegram username, NOT that of your bot (case-sensitive) - * For `` and `` just substitute something like "test" "test" (for Telegram it's doesn't matter between subject and body - * You can omit the `"`, these are optional +`./zbxtg.py "@username" "first part of a message" "second part of a message" --debug` + * For `@username` substitute your Telegram username, **NOT that of your bot** (case-sensitive) OR chatid + * For `first part of a message` and `second part of a message` just substitute something like "test" "test" (for Telegram it's doesn't matter between subject and body) + * You can skip the `"` if it's one word for every parameter, these are optional --- @@ -99,5 +106,5 @@ You can use markdown or html formatting in your action: https://core.telegram.or If you see this error, it means that you rich the limit of caption with 200 symbols in it (Telegram API's limitaion). Such captions will be automatically cut to 200 symbols. -#### Zabbix 3.0 +#### Zabbix 3.0 and higher (3.2, 3.4, 4.0, 4.2, 4.4) https://github.com/ableev/Zabbix-in-Telegram/wiki/Working-with-Zabbix-3.0 diff --git a/ZbxTgDaemon.py b/ZbxTgDaemon.py new file mode 100644 index 0000000..d18a4d0 --- /dev/null +++ b/ZbxTgDaemon.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python +# coding: utf-8 + +import sys +import os +import hashlib +import re +import time +from os.path import dirname +import zbxtg_settings +import zbxtg +from pyzabbix import ZabbixAPI, ZabbixAPIException + + +class zabbixApi(): + def __init__(self, server, user, password): + self.api = ZabbixAPI(server) + self.user = user + self.password = password + + def login(self): + self.api.login(self.user, self.password) + + def triggers_active(self): + return self.api.trigger.get(output="extend", monitored=True, filter={"value": 1}, sortfield="priority", sortorder="DESC", + selectHosts="extend") + + + +def print_message(string): + string = str(string) + "\n" + filename = sys.argv[0].split("/")[-1] + sys.stderr.write(filename + ": " + string) + + +def file_write(filename, text): + with open(filename, "w") as fd: + fd.write(str(text)) + return True + + +def file_read(filename): + with open(filename, 'r') as fd: + text = fd.readlines() + return text + + +def main(): + TelegramAPI = zbxtg.TelegramAPI + ZabbixWeb = zbxtg.ZabbixWeb + tmp_dir = zbxtg_settings.zbx_tg_tmp_dir + + if not zbxtg_settings.zbx_tg_daemon_enabled: + print("You should enable daemon by adding 'zbx_tg_remote_control' in the configuration file") + sys.exit(1) + + tmp_uids = tmp_dir + "/uids.txt" + tmp_ts = { + "message_id": tmp_dir + "/daemon_message_id.txt", + "update_offset": tmp_dir + "/update_offset.txt", + } + + for i, v in tmp_ts.iteritems(): + if not os.path.exists(v): + print_message("{0} doesn't exist, creating new one...".format(v)) + file_write(v, "0") + print_message("{0} successfully created".format(v)) + + message_id_last = file_read(tmp_ts["message_id"])[0].strip() + if message_id_last: + message_id_last = int(message_id_last) + + update_id = file_read(tmp_ts["update_offset"]) + + tg = TelegramAPI(key=zbxtg_settings.tg_key) + if zbxtg_settings.proxy_to_tg: + proxy_to_tg = zbxtg_settings.proxy_to_tg + if not proxy_to_tg.find("http") and not proxy_to_tg.find("socks"): + proxy_to_tg = "https://" + proxy_to_tg + tg.proxies = { + "https": "{0}".format(zbxtg_settings.proxy_to_tg), + } + zbx = ZabbixWeb(server=zbxtg_settings.zbx_server, username=zbxtg_settings.zbx_api_user, + password=zbxtg_settings.zbx_api_pass) + if zbxtg_settings.proxy_to_zbx: + zbx.proxies = {"http": "http://{0}/".format(zbxtg_settings.proxy_to_zbx)} + + try: + zbx_api_verify = zbxtg_settings.zbx_api_verify + zbx.verify = zbx_api_verify + except: + pass + + zbxapi = zabbixApi(zbxtg_settings.zbx_server, zbxtg_settings.zbx_api_user, zbxtg_settings.zbx_api_pass) + zbxapi.login() + + print(tg.get_me()) + + #hosts = zbxdb.db_query("SELECT hostid, host FROM hosts") + + commands = [ + "/triggers", + "/help", + # "/graph", + # "/history", + # "/screen" + ] + + def md5(fname): + hash_md5 = hashlib.md5() + with open(fname, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + md5sum = md5("ZbxTgDaemon.py") + print md5sum + + try: + while True: + time.sleep(1) + md5sum_new = md5("ZbxTgDaemon.py") + if md5sum != md5sum_new: + sys.exit(1) + tg.update_offset = update_id + updates = tg.get_updates() + if not updates["result"]: + continue + for m in updates["result"]: + if "message" not in m: + continue + update_id_last = m["update_id"] + tg.update_offset = update_id_last + if m["message"]["from"]["id"] not in zbxtg_settings.zbx_tg_daemon_enabled_ids: + file_write(tmp_ts["update_offset"], update_id_last) + continue + print("Fuck this shit, I'm not going to answer to someone not from the whitelist") + else: + if not "text" in m["message"]: + continue + text = m["message"]["text"] + to = m["message"]["from"]["id"] + reply_text = list() + if m["message"]["message_id"] > message_id_last: + if re.search(r"^/(start|help)", text): + reply_text.append("Hey, this is ZbxTgDaemon bot.") + reply_text.append("https://github.com/ableev/Zabbix-in-Telegram") + reply_text.append("If you need help, you can ask it in @ZbxTg group\n") + reply_text.append("Available commands:") + reply_text.append("\n".join(commands)) + tg.disable_web_page_preview = True + if re.search(r"^/triggers", text): + triggers = zbxapi.triggers_active() + if triggers: + for t in triggers: + reply_text.append("Severity: {0}, Host: {1}, Trigger: {2}".format( + t["priority"], t["hosts"][0]["host"].encode('utf-8'), t["description"].encode('utf-8') + )) + else: + reply_text.append("There are no triggers, have a nice day!") + if not reply_text: + reply_text = ["I don't know what to do about it"] + if tg.send_message(to, reply_text): + with open(tmp_ts["message_id"], "w") as message_id_file: + message_id_file.write(str(m["message"]["message_id"])) + message_id_last = m["message"]["message_id"] + tg.disable_web_page_preview = False + file_write(tmp_ts["update_offset"], update_id_last) + except KeyboardInterrupt: + print("Exiting...") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/bash-old-version/tg_vars.cfg.example b/bash-old-version/tg_vars.cfg.example new file mode 100644 index 0000000..6494278 --- /dev/null +++ b/bash-old-version/tg_vars.cfg.example @@ -0,0 +1,11 @@ +TG_KEY="000:AAAAA_bbbb" + +ZBX_TG_PREFIX="zbxtg" # variable for separating text from script info +ZBX_TG_SIGN="TRUE" + +ZBX_SERVER="http://zabbix.local" # zabbix server url +ZBX_API_USER="api" # zabbix user; user must have at least read access to get graphs +ZBX_API_PASS="apisecret" + +CURL="curl -s" # if you are using proxy server, it's time to add it right here +# CURL="curl -x proxy.local:3128 -s" diff --git a/bash-old-version/zbxtg.sh b/bash-old-version/zbxtg.sh new file mode 100755 index 0000000..6afad26 --- /dev/null +++ b/bash-old-version/zbxtg.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +. $(dirname "$0")/tg_vars.cfg + +CURL_TG="${CURL} https://api.telegram.org/bot${TG_KEY}" + +TMP_DIR="/tmp/${ZBX_TG_PREFIX}" +[ ! -d "${TMP_DIR}" ] && (mkdir -p ${TMP_DIR} || TMP_DIR="/tmp") +TMP_COOKIE="${TMP_DIR}/cookie.txt" +TMP_UIDS="${TMP_DIR}/uids.txt" + +TS="`date +%s_%N`_$RANDOM" +LOG="/dev/null" + +IS_DEBUG () { + if [ "${ISDEBUG}" == "TRUE" ] + then + return 0 + else + return 1 + fi +} + + +login() { + # grab cookie for downloading image + IS_DEBUG && echo "${CURL} --cookie-jar ${TMP_COOKIE} --request POST --data \"name=${ZBX_API_USER}&password=${ZBX_API_PASS}&enter=Sign%20in\" ${ZBX_SERVER}/" >>${LOG} + ${CURL} --cookie-jar ${TMP_COOKIE} --request POST --data "name=${ZBX_API_USER}&password=${ZBX_API_PASS}&enter=Sign%20in" ${ZBX_SERVER}/ +} + +get_image() { + URL=$1 + URL=$(echo "${URL}" | sed -e 's/\ /%20/g') + IMG_NAME=$2 + # downloads png graph and saves it to temporary path + IS_DEBUG && echo "${CURL} --cookie ${TMP_COOKIE} --globoff \"${URL}\" -o ${IMG_NAME}" >>${LOG} + ${CURL} --cookie ${TMP_COOKIE} --globoff "${URL}" -o ${IMG_NAME} +} + +TO=$1 +SUBJECT=$2 +BODY=$3 + +TG_GROUP=0 # send message to chat or to private chat to user +TG_CHANNEL=0 # send message to channel +METHOD="txt" # sendMessage (simple text) or sendPhoto (attached image) + +echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};graphs" && METHOD="image" +echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};chat" && TG_GROUP=1 +echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};group" && TG_GROUP=1 +echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};debug" && ISDEBUG="TRUE" +echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};channel" && TG_CHANNEL=1 + +IS_DEBUG && LOG="${TMP_DIR}/debug.${TS}.log" +IS_DEBUG && echo -e "TMP_DIR=${TMP_DIR}\nTMP_COOKIE=${TMP_COOKIE}\nTMP_UIDS=${TMP_UIDS}" >>${LOG} + +if [ "${TG_GROUP}" -eq 1 ] +then + TG_CONTACT_TYPE="group" +else + TG_CONTACT_TYPE="private" +fi + +TG_CHAT_ID=$(cat ${TMP_UIDS} | awk -F ';' '{if ($1 == "'${TO}'" && $2 == "'${TG_CONTACT_TYPE}'") print $3}' | tail -1) + +if [ "${TG_CHANNEL}" -eq 1 ] +then + TG_CHAT_ID="${TO}" +fi + +if [ -z "${TG_CHAT_ID}" ] +then + TG_UPDATES=$(${CURL_TG}/getUpdates | sed -e 's/},{/\n/') + for (( idx=${#TG_UPDATES[@]}-1 ; idx>=0 ; idx-- )) + do + UPDATE="${TG_UPDATES[idx]}" + echo "${UPDATE}" + if [ "${TG_GROUP}" -eq 1 ] + then + TG_CHAT_ID=$(echo "${UPDATE}" | sed -e 's/["}{]//g' | awk -F ',' '{if ($8 == "type:group" && $7 == "title:'${TO}'") {gsub("chat:id:", "", $6); print $6}}' | tail -1) + if [ "$(echo ${TG_CHAT_ID} | grep -Eq '\-[0-9]+' && echo 1 || echo 0)" -eq 1 ] + then + break + fi + else + TG_CHAT_ID=$(echo "${UPDATE}" | sed -e 's/["}{]//g' | awk -F ',' '{if ($10 == "type:private" && $5 == "username:'${TO}'") {gsub("chat:id:", "", $6); print $6}}' | tail -1) + if [ "$(echo ${TG_CHAT_ID} | grep -Eq '[0-9]+' && echo 1 || echo 0)" -eq 1 ] + then + break + fi + fi + done + echo "${TO};${TG_CONTACT_TYPE};${TG_CHAT_ID}" >>${TMP_UIDS} +fi + +IS_DEBUG && echo "TG_CHAT_ID: ${TG_CHAT_ID}" >>${LOG} + +TG_TEXT=$(echo "${BODY}" | grep -vE "^${ZBX_TG_PREFIX};") +if [ "${ZBX_TG_SIGN}" != "FALSE" ] +then + TG_TEXT=$(echo ${TG_TEXT}; echo "--"; echo "${ZBX_SERVER}") +fi + +case "${METHOD}" in + + "txt") + TG_MESSAGE=$(echo -e "${SUBJECT}\n${TG_TEXT}") + IS_DEBUG && echo "${CURL_TG}/sendMessage -F \"chat_id=${TG_CHAT_ID}\" -F \"text=${TG_MESSAGE}\"" >>${LOG} + ANSWER=$(${CURL_TG}/sendMessage?chat_id=${TG_CHAT_ID} --form "text=${TG_MESSAGE}" 2>&1) + if [ "$(echo "${ANSWER}" | grep -Ec 'migrated.*supergroup')" -eq 1 ] + then + migrate_to_chat_id=$(echo "${ANSWER}" | sed -e 's/["}{]//g' | grep -Eo '\-[0-9]+$') + echo "${TO};${TG_CONTACT_TYPE};${migrate_to_chat_id}" >>${TMP_UIDS} + ANSWER=$(${CURL_TG}/sendMessage?chat_id=${migrate_to_chat_id} --form "text=${TG_MESSAGE}" 2>&1) + fi + ;; + + "image") + PERIOD=3600 # default period + echo "${BODY}" | grep -q "^${ZBX_TG_PREFIX};graphs_period" && PERIOD=$(echo "${BODY}" | awk -F "${ZBX_TG_PREFIX};graphs_period=" '{if ($2 != "") print $2}' | tail -1 | grep -Eo '[0-9]+' || echo 3600) + ZBX_ITEMID=$(echo "${BODY}" | awk -F "${ZBX_TG_PREFIX};itemid:" '{if ($2 != "") print $2}' | tail -1 | grep -Eo '[0-9]+') + ZBX_TITLE=$(echo "${BODY}" | awk -F "${ZBX_TG_PREFIX};title:" '{if ($2 != "") print $2}' | tail -1) + URL="${ZBX_SERVER}/chart3.php?period=${PERIOD}&name=${ZBX_TITLE}&width=900&height=200&graphtype=0&legend=1&items[0][itemid]=${ZBX_ITEMID}&items[0][sortorder]=0&items[0][drawtype]=5&items[0][color]=00CC00" + IS_DEBUG && echo "Zabbix graph URL: ${URL}" >> ${LOG} + login + CACHE_IMAGE="${TMP_DIR}/graph.${ZBX_ITEMID}.png" + IS_DEBUG && echo "Image cached to ${CACHE_IMAGE} and wasn't deleted" >> ${LOG} + get_image "${URL}" ${CACHE_IMAGE} + TG_CAPTION_ORIG=$(echo -e "${SUBJECT}\n${TG_TEXT}") + TG_CAPTION=$(echo -e $(echo "${TG_CAPTION_ORIG}" | sed ':a;N;$!ba;s/\n/\\n/g' | awk '{print substr( $0, 0, 200 )}')) + if [ "${TG_CAPTION}" != "${TG_CAPTION_ORIG}" ] + then + echo "${ZBX_TG_PREFIX}: probably you will see MEDIA_CAPTION_TOO_LONG error, the message has been cut to 200 symbols, https://github.com/ableev/Zabbix-in-Telegram/issues/9#issuecomment-166895044" + fi + IS_DEBUG && echo "${CURL_TG}/sendPhoto?chat_id=${TG_CHAT_ID}\" --form \"caption=${TG_CAPTION}\" -F \"photo=@${CACHE_IMAGE}\"" >>${LOG} + ANSWER=$(${CURL_TG}/sendPhoto?chat_id=${TG_CHAT_ID} --form "caption=${TG_CAPTION}" -F "photo=@${CACHE_IMAGE}") + IS_DEBUG || rm ${CACHE_IMAGE} + ;; + +esac + +echo >>${LOG} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..63b178b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +PySocks==1.6.8 +requests==2.20.0 +requests-oauthlib==0.6.2 diff --git a/tg_vars.cfg.example b/tg_vars.cfg.example deleted file mode 100644 index 938ab1d..0000000 --- a/tg_vars.cfg.example +++ /dev/null @@ -1,10 +0,0 @@ -TG_KEY="000:AAAAA_bbbb" - -ZBX_TG_PREFIX="zbxtg" # variable for separating text from script info - -ZBX_SERVER="http://zabbix.local" # zabbix server url -ZBX_API_USER="api" # zabbix user; user must have at least read access to get graphs -ZBX_API_PASS="apisecret" - -CURL="curl -s" # if you are using proxy server, it's time to add it right here -# CURL="curl -x proxy.local:3128 -s" diff --git a/zbxtg.py b/zbxtg.py index 5099de9..3689ab5 100755 --- a/zbxtg.py +++ b/zbxtg.py @@ -5,22 +5,34 @@ import sys import os import time import random +import string import requests import json import re import stat +import hashlib +import subprocess +#import sqlite3 from os.path import dirname import zbxtg_settings -class TelegramAPI(): +class Cache: + def __init__(self, database): + self.database = database + + def create_db(self, database): + pass + + +class TelegramAPI: tg_url_bot_general = "https://api.telegram.org/bot" def http_get(self, url): - res = requests.get(url, proxies=self.proxies) - answer = res.text - answer_json = json.loads(answer.decode('utf8')) - return answer_json + answer = requests.get(url, proxies=self.proxies) + self.result = answer.json() + self.ok_update() + return self.result def __init__(self, key): self.debug = False @@ -32,7 +44,15 @@ class TelegramAPI(): self.disable_web_page_preview = False self.disable_notification = False self.reply_to_message_id = 0 + self.tmp_dir = None self.tmp_uids = None + self.location = {"latitude": None, "longitude": None} + self.update_offset = 0 + self.image_buttons = False + self.result = None + self.ok = None + self.error = None + self.get_updates_from_file = False def get_me(self): url = self.tg_url_bot_general + self.key + "/getMe" @@ -41,17 +61,19 @@ class TelegramAPI(): def get_updates(self): url = self.tg_url_bot_general + self.key + "/getUpdates" + params = {"offset": self.update_offset} if self.debug: print_message(url) - updates = self.http_get(url) + answer = requests.post(url, params=params, proxies=self.proxies) + self.result = answer.json() + if self.get_updates_from_file: + print_message("Getting updated from file getUpdates.txt") + self.result = json.loads("".join(file_read("getUpdates.txt"))) if self.debug: print_message("Content of /getUpdates:") - print_message(updates) - if not updates["ok"]: - print_message(updates) - return updates - else: - return updates + print_message(json.dumps(self.result)) + self.ok_update() + return self.result def send_message(self, to, message): url = self.tg_url_bot_general + self.key + "/sendMessage" @@ -69,19 +91,50 @@ class TelegramAPI(): print_message("Trying to /sendMessage:") print_message(url) print_message("post params: " + str(params)) - res = requests.post(url, params=params, proxies=self.proxies) - answer = res.text - answer_json = json.loads(answer.decode('utf8')) - if not answer_json["ok"]: - print_message(answer_json) - return answer_json + answer = requests.post(url, params=params, proxies=self.proxies) + if answer.status_code == 414: + self.result = {"ok": False, "description": "414 URI Too Long"} else: - return answer_json + self.result = answer.json() + self.ok_update() + return self.result + + def update_message(self, to, message_id, message): + url = self.tg_url_bot_general + self.key + "/editMessageText" + message = "\n".join(message) + params = {"chat_id": to, "message_id": message_id, "text": message, + "disable_web_page_preview": self.disable_web_page_preview, + "disable_notification": self.disable_notification} + if self.markdown or self.html: + parse_mode = "HTML" + if self.markdown: + parse_mode = "Markdown" + params["parse_mode"] = parse_mode + if self.debug: + print_message("Trying to /editMessageText:") + print_message(url) + print_message("post params: " + str(params)) + answer = requests.post(url, params=params, proxies=self.proxies) + self.result = answer.json() + self.ok_update() + return self.result def send_photo(self, to, message, path): url = self.tg_url_bot_general + self.key + "/sendPhoto" message = "\n".join(message) - params = {"chat_id": to, "caption": message, "disable_notification": self.disable_notification} + if self.image_buttons: + reply_markup = json.dumps({"inline_keyboard": [[ + {"text": "R", "callback_data": "graph_refresh"}, + {"text": "1h", "callback_data": "graph_period_3600"}, + {"text": "3h", "callback_data": "graph_period_10800"}, + {"text": "6h", "callback_data": "graph_period_21600"}, + {"text": "12h", "callback_data": "graph_period_43200"}, + {"text": "24h", "callback_data": "graph_period_86400"}, + ], ]}) + else: + reply_markup = json.dumps({}) + params = {"chat_id": to, "caption": message, "disable_notification": self.disable_notification, + "reply_markup": reply_markup} if self.reply_to_message_id: params["reply_to_message_id"] = self.reply_to_message_id files = {"photo": open(path, 'rb')} @@ -90,14 +143,34 @@ class TelegramAPI(): print_message(url) print_message(params) print_message("files: " + str(files)) - res = requests.post(url, params=params, files=files, proxies=self.proxies) - answer = res.text - answer_json = json.loads(answer.decode('utf8')) - if not answer_json["ok"]: - print_message(answer_json) - return answer_json + answer = requests.post(url, params=params, files=files, proxies=self.proxies) + self.result = answer.json() + self.ok_update() + return self.result + + def send_txt(self, to, text, text_name=None): + path = self.tmp_dir + "/" + "zbxtg_txt_" + url = self.tg_url_bot_general + self.key + "/sendDocument" + text = "\n".join(text) + if not text_name: + path += "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10)) else: - return answer_json + path += text_name + path += ".txt" + file_write(path, text) + params = {"chat_id": to, "caption": path.split("/")[-1], "disable_notification": self.disable_notification} + if self.reply_to_message_id: + params["reply_to_message_id"] = self.reply_to_message_id + files = {"document": open(path, 'rb')} + if self.debug: + print_message("Trying to /sendDocument:") + print_message(url) + print_message(params) + print_message("files: " + str(files)) + answer = requests.post(url, params=params, files=files, proxies=self.proxies) + self.result = answer.json() + self.ok_update() + return self.result def get_uid(self, name): uid = 0 @@ -109,21 +182,28 @@ class TelegramAPI(): chat = m["message"]["chat"] elif "edited_message" in m: chat = m["edited_message"]["chat"] + else: + continue if chat["type"] == self.type == "private": if "username" in chat: if chat["username"] == name: uid = chat["id"] if (chat["type"] == "group" or chat["type"] == "supergroup") and self.type == "group": if "title" in chat: - if chat["title"] == name.decode("utf-8"): - uid = chat["id"] + if sys.version_info[0] < 3: + if chat["title"] == name.decode("utf-8"): + uid = chat["id"] + else: + if chat["title"] == name: + uid = chat["id"] return uid def error_need_to_contact(self, to): if self.type == "private": print_message("User '{0}' needs to send some text bot in private".format(to)) if self.type == "group": - print_message("You need to mention your bot in '{0}' group chat (i.e. type @YourBot)".format(to)) + print_message("You need start a conversation with your bot first in '{0}' group chat, type '/start@{1}'" + .format(to, self.get_me()["result"]["username"])) def update_cache_uid(self, name, uid, message="Add new string to cache file"): cache_string = "{0};{1};{2}\n".format(name, self.type, str(uid).rstrip()) @@ -132,22 +212,68 @@ class TelegramAPI(): print_message("{0}: {1}".format(message, cache_string)) with open(self.tmp_uids, "a") as cache_file_uids: cache_file_uids.write(cache_string) + return True def get_uid_from_cache(self, name): + if self.debug: + print_message("Trying to read cached uid for {0}, {1}, from {2}".format(name, self.type, self.tmp_uids)) uid = 0 if os.path.isfile(self.tmp_uids): with open(self.tmp_uids, 'r') as cache_file_uids: cache_uids_old = cache_file_uids.readlines() - for u in cache_uids_old: u_splitted = u.split(";") if name == u_splitted[0] and self.type == u_splitted[1]: uid = u_splitted[2] - return uid + def send_location(self, to, coordinates): + url = self.tg_url_bot_general + self.key + "/sendLocation" + params = {"chat_id": to, "disable_notification": self.disable_notification, + "latitude": coordinates["latitude"], "longitude": coordinates["longitude"]} + if self.reply_to_message_id: + params["reply_to_message_id"] = self.reply_to_message_id + if self.debug: + print_message("Trying to /sendLocation:") + print_message(url) + print_message("post params: " + str(params)) + answer = requests.post(url, params=params, proxies=self.proxies) + self.result = answer.json() + self.ok_update() + return self.result + + def answer_callback_query(self, callback_query_id, text=None): + url = self.tg_url_bot_general + self.key + "/answerCallbackQuery" + if not text: + params = {"callback_query_id": callback_query_id} + else: + params = {"callback_query_id": callback_query_id, "text": text} + answer = requests.post(url, params=params, proxies=self.proxies) + self.result = answer.json() + self.ok_update() + return self.result + + def ok_update(self): + self.ok = self.result["ok"] + if self.ok: + self.error = None + else: + self.error = self.result["description"] + print_message(self.error) + return True + + +def markdown_fix(message, offset, emoji=False): + offset = int(offset) + if emoji: # https://github.com/ableev/Zabbix-in-Telegram/issues/152 + offset -= 2 + message = "\n".join(message) + message = message[:offset] + message[offset+1:] + message = message.split("\n") + return message -class ZabbixAPI(): + +class ZabbixWeb: def __init__(self, server, username, password): self.debug = False self.server = server @@ -156,16 +282,19 @@ class ZabbixAPI(): self.proxies = {} self.verify = True self.cookie = None + self.basic_auth_user = None + self.basic_auth_pass = None + self.tmp_dir = None def login(self): - if not self.verify: requests.packages.urllib3.disable_warnings() data_api = {"name": self.username, "password": self.password, "enter": "Sign in"} - req_cookie = requests.post(self.server + "/", data=data_api, proxies=self.proxies, verify=self.verify) - cookie = req_cookie.cookies - if len(req_cookie.history) > 1 and req_cookie.history[0].status_code == 302: + answer = requests.post(self.server + "/", data=data_api, proxies=self.proxies, verify=self.verify, + auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass)) + cookie = answer.cookies + if len(answer.history) > 1 and answer.history[0].status_code == 302: print_message("probably the server in your config file has not full URL (for example " "'{0}' instead of '{1}')".format(self.server, self.server + "/zabbix")) if not cookie: @@ -174,26 +303,49 @@ class ZabbixAPI(): self.cookie = cookie - def graph_get(self, itemid, period, title, width, height, tmp_dir): - file_img = tmp_dir + "/{0}.png".format(itemid) + def graph_get(self, itemid, period, title, width, height, version=3): + file_img = "{0}/{1}.png".format(self.tmp_dir, + "".join(random.choice(string.ascii_letters) for e in range(10))) title = requests.utils.quote(title) - zbx_img_url = self.server + "/chart3.php?period={1}&name={2}" \ - "&width={3}&height={4}&graphtype=0&legend=1" \ - "&items[0][itemid]={0}&items[0][sortorder]=0" \ - "&items[0][drawtype]=5&items[0][color]=00CC00".format(itemid, period, title, - width, height) + colors = { + 0: "00CC00", + 1: "CC0000", + 2: "0000CC", + 3: "CCCC00", + 4: "00CCCC", + 5: "CC00CC", + } + + drawtype = 5 + if len(itemid) > 1: + drawtype = 2 + + zbx_img_url_itemids = [] + for i in range(0, len(itemid)): + itemid_url = "&items[{0}][itemid]={1}&items[{0}][sortorder]={0}&" \ + "items[{0}][drawtype]={3}&items[{0}][color]={2}".format(i, itemid[i], colors[i], drawtype) + zbx_img_url_itemids.append(itemid_url) + + zbx_img_url = self.server + "/chart3.php?" + if version < 4: + zbx_img_url += "period={0}".format(period) + else: + zbx_img_url += "from=now-{0}&to=now".format(period) + zbx_img_url += "&name={0}&width={1}&height={2}&graphtype=0&legend=1".format(title, width, height) + zbx_img_url += "".join(zbx_img_url_itemids) + if self.debug: print_message(zbx_img_url) - res = requests.get(zbx_img_url, cookies=self.cookie, proxies=self.proxies, verify=self.verify) - res_code = res.status_code - if res_code == 404: + answer = requests.get(zbx_img_url, cookies=self.cookie, proxies=self.proxies, verify=self.verify, + auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass)) + status_code = answer.status_code + if status_code == 404: print_message("can't get image from '{0}'".format(zbx_img_url)) return False - res_img = res.content - with open(file_img, 'wb') as fp: - fp.write(res_img) + res_img = answer.content + file_bwrite(file_img, res_img) return file_img def api_test(self): @@ -205,16 +357,15 @@ class ZabbixAPI(): return api.text -def print_message(string): - string = str(string) + "\n" +def print_message(message): + message = str(message) + "\n" filename = sys.argv[0].split("/")[-1] - sys.stderr.write(filename + ": " + string) + sys.stderr.write(filename + ": " + message) def list_cut(elements, symbols_limit): symbols_count = symbols_count_now = 0 elements_new = [] - element_last = None element_last_list = [] for e in elements: symbols_count_now = symbols_count + len(e) @@ -238,9 +389,93 @@ def list_cut(elements, symbols_limit): return elements_new, True +class Maps: + # https://developers.google.com/maps/documentation/geocoding/intro + def __init__(self): + self.key = None + self.proxies = {} + + def get_coordinates_by_address(self, address): + coordinates = {"latitude": 0, "longitude": 0} + url_api = "https://maps.googleapis.com/maps/api/geocode/json?key={0}&address={1}".format(self.key, address) + url = url_api + answer = requests.get(url, proxies=self.proxies) + result = answer.json() + try: + coordinates_dict = result["results"][0]["geometry"]["location"] + except: + if "error_message" in result: + print_message("[" + result["status"] + "]: " + result["error_message"]) + return coordinates + coordinates = {"latitude": coordinates_dict["lat"], "longitude": coordinates_dict["lng"]} + return coordinates + + +def file_write(filename, text): + with open(filename, "w") as fd: + fd.write(str(text)) + return True + + +def file_bwrite(filename, data): + with open(filename, "wb") as fd: + fd.write(data) + return True + + +def file_read(filename): + with open(filename, "r") as fd: + text = fd.readlines() + return text + + +def file_append(filename, text): + with open(filename, "a") as fd: + fd.write(str(text)) + return True + + +def external_image_get(url, tmp_dir, timeout=6): + image_hash = hashlib.md5() + image_hash.update(url.encode()) + file_img = tmp_dir + "/external_{0}.png".format(image_hash.hexdigest()) + try: + answer = requests.get(url, timeout=timeout, allow_redirects=True) + except requests.exceptions.ReadTimeout as ex: + print_message("Can't get external image from '{0}': timeout".format(url)) + return False + status_code = answer.status_code + if status_code == 404: + print_message("Can't get external image from '{0}': HTTP 404 error".format(url)) + return False + answer_image = answer.content + file_bwrite(file_img, answer_image) + return file_img + + +def age2sec(age_str): + age_sec = 0 + age_regex = "([0-9]+d)?\s?([0-9]+h)?\s?([0-9]+m)?" + age_pattern = re.compile(age_regex) + intervals = age_pattern.match(age_str).groups() + for i in intervals: + if i: + metric = i[-1] + if metric == "d": + age_sec += int(i[0:-1])*86400 + if metric == "h": + age_sec += int(i[0:-1])*3600 + if metric == "m": + age_sec += int(i[0:-1])*60 + return age_sec + + def main(): tmp_dir = zbxtg_settings.zbx_tg_tmp_dir + if tmp_dir == "/tmp/" + zbxtg_settings.zbx_tg_prefix: + print_message("WARNING: it is strongly recommended to change `zbx_tg_tmp_dir` variable in config!!!") + print_message("https://github.com/ableev/Zabbix-in-Telegram/wiki/Change-zbx_tg_tmp_dir-in-settings") tmp_cookie = tmp_dir + "/cookie.py.txt" tmp_uids = tmp_dir + "/uids.txt" @@ -252,28 +487,150 @@ def main(): log_file = "/dev/null" - zbx_to = sys.argv[1] - zbx_subject = sys.argv[2] - zbx_body = sys.argv[3] + args = sys.argv + + settings = { + "zbxtg_itemid": "0", # itemid for graph + "zbxtg_title": None, # title for graph + "zbxtg_image_period": None, + "zbxtg_image_age": "3600", + "zbxtg_image_width": "900", + "zbxtg_image_height": "200", + "tg_method_image": False, # if True - default send images, False - send text + "tg_chat": False, # send message to chat or in private + "tg_group": False, # send message to chat or in private + "is_debug": False, + "is_channel": False, + "disable_web_page_preview": False, + "location": None, # address + "lat": 0, # latitude + "lon": 0, # longitude + "is_single_message": False, + "markdown": False, + "html": False, + "signature": None, + "signature_disable": False, + "graph_buttons": False, + "extimg": None, + "to": None, + "to_group": None, + "forked": False, + } + + url_github = "https://github.com/ableev/Zabbix-in-Telegram" + url_wiki_base = "https://github.com/ableev/Zabbix-in-Telegram/wiki" + url_tg_group = "https://t.me/ZbxTg" + url_tg_channel = "https://t.me/Zabbix_in_Telegram" + + settings_description = { + "itemid": {"name": "zbxtg_itemid", "type": "list", + "help": "script will attach a graph with that itemid (could be multiple)", "url": "Graphs"}, + "title": {"name": "zbxtg_title", "type": "str", "help": "title for attached graph", "url": "Graphs"}, + "graphs_period": {"name": "zbxtg_image_period", "type": "int", "help": "graph period", "url": "Graphs"}, + "graphs_age": {"name": "zbxtg_image_age", "type": "str", "help": "graph period as age", "url": "Graphs"}, + "graphs_width": {"name": "zbxtg_image_width", "type": "int", "help": "graph width", "url": "Graphs"}, + "graphs_height": {"name": "zbxtg_image_height", "type": "int", "help": "graph height", "url": "Graphs"}, + "graphs": {"name": "tg_method_image", "type": "bool", "help": "enables graph sending", "url": "Graphs"}, + "chat": {"name": "tg_chat", "type": "bool", "help": "deprecated, don't use it, see 'group'", + "url": "How-to-send-message-to-the-group-chat"}, + "group": {"name": "tg_group", "type": "bool", "help": "sends message to a group", + "url": "How-to-send-message-to-the-group-chat"}, + "debug": {"name": "is_debug", "type": "bool", "help": "enables 'debug'", + "url": "How-to-test-script-in-command-line"}, + "channel": {"name": "is_channel", "type": "bool", "help": "sends message to a channel", + "url": "Channel-support"}, + "disable_web_page_preview": {"name": "disable_web_page_preview", "type": "bool", + "help": "disable web page preview", "url": "Disable-web-page-preview"}, + "location": {"name": "location", "type": "str", "help": "address of location", "url": "Location"}, + "lat": {"name": "lat", "type": "str", "help": "specify latitude (and lon too!)", "url": "Location"}, + "lon": {"name": "lon", "type": "str", "help": "specify longitude (and lat too!)", "url": "Location"}, + "single_message": {"name": "is_single_message", "type": "bool", "help": "do not split message and graph", + "url": "Why-am-I-getting-two-messages-instead-of-one"}, + "markdown": {"name": "markdown", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"}, + "html": {"name": "html", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"}, + "signature": {"name": "signature", "type": "str", + "help": "bot's signature", "url": "Bot-signature"}, + "signature_disable": {"name": "signature_disable", "type": "bool", + "help": "enables/disables bot's signature", "url": "Bot-signature"}, + "graph_buttons": {"name": "graph_buttons", "type": "bool", + "help": "activates buttons under graph, could be using in ZbxTgDaemon", + "url": "Interactive-bot"}, + "external_image": {"name": "extimg", "type": "str", + "help": "should be url; attaches external image from different source", + "url": "External-image-as-graph"}, + "to": {"name": "to", "type": "str", "help": "rewrite zabbix username, use that instead of arguments", + "url": "Custom-to-and-to_group"}, + "to_group": {"name": "to_group", "type": "str", + "help": "rewrite zabbix username, use that instead of arguments", "url": "Custom-to-and-to_group"}, + "forked": {"name": "forked", "type": "bool", "help": "internal variable, do not use it. Ever.", "url": ""}, + } + + if len(args) < 4: + do_not_exit = False + if "--features" in args: + print(("List of available settings, see {0}/Settings\n---".format(url_wiki_base))) + for sett, proprt in list(settings_description.items()): + print(("{0}: {1}\ndoc: {2}/{3}\n--".format(sett, proprt["help"], url_wiki_base, proprt["url"]))) + + elif "--show-settings" in args: + do_not_exit = True + print_message("Settings: " + str(json.dumps(settings, indent=2))) + + else: + print(("Hi. You should provide at least three arguments.\n" + "zbxtg.py [TO] [SUBJECT] [BODY]\n\n" + "1. Read main page and/or wiki: {0} + {1}\n" + "2. Public Telegram group (discussion): {2}\n" + "3. Public Telegram channel: {3}\n" + "4. Try dev branch for test purposes (new features, etc): {0}/tree/dev" + .format(url_github, url_wiki_base, url_tg_group, url_tg_channel))) + if not do_not_exit: + sys.exit(0) + + + zbx_to = args[1] + zbx_subject = args[2] + zbx_body = args[3] tg = TelegramAPI(key=zbxtg_settings.tg_key) + tg.tmp_dir = tmp_dir tg.tmp_uids = tmp_uids if zbxtg_settings.proxy_to_tg: + proxy_to_tg = zbxtg_settings.proxy_to_tg + if not proxy_to_tg.find("http") and not proxy_to_tg.find("socks"): + proxy_to_tg = "https://" + proxy_to_tg tg.proxies = { - "http": "http://{0}/".format(zbxtg_settings.proxy_to_tg), - "https": "https://{0}/".format(zbxtg_settings.proxy_to_tg) - } + "https": "{0}".format(proxy_to_tg), + } - zbx = ZabbixAPI(server=zbxtg_settings.zbx_server, username=zbxtg_settings.zbx_api_user, + zbx = ZabbixWeb(server=zbxtg_settings.zbx_server, username=zbxtg_settings.zbx_api_user, password=zbxtg_settings.zbx_api_pass) + zbx.tmp_dir = tmp_dir + + # workaround for Zabbix 4.x + zbx_version = 3 + + try: + zbx_version = zbxtg_settings.zbx_server_version + except: + pass + if zbxtg_settings.proxy_to_zbx: zbx.proxies = { "http": "http://{0}/".format(zbxtg_settings.proxy_to_zbx), "https": "https://{0}/".format(zbxtg_settings.proxy_to_zbx) - } + } + + # https://github.com/ableev/Zabbix-in-Telegram/issues/55 + try: + if zbxtg_settings.zbx_basic_auth: + zbx.basic_auth_user = zbxtg_settings.zbx_basic_auth_user + zbx.basic_auth_pass = zbxtg_settings.zbx_basic_auth_pass + except: + pass try: zbx_api_verify = zbxtg_settings.zbx_api_verify @@ -281,42 +638,38 @@ def main(): except: pass + map = Maps() + # api key to resolve address to coordinates via google api + try: + if zbxtg_settings.google_maps_api_key: + map.key = zbxtg_settings.google_maps_api_key + if zbxtg_settings.proxy_to_tg: + map.proxies = { + "http": "http://{0}/".format(zbxtg_settings.proxy_to_tg), + "https": "https://{0}/".format(zbxtg_settings.proxy_to_tg) + } + except: + pass + zbxtg_body = (zbx_subject + "\n" + zbx_body).splitlines() zbxtg_body_text = [] - settings = { - "zbxtg_itemid": "0", # itemid for graph - "zbxtg_title": None, # title for graph - "zbxtg_image_period": "3600", - "zbxtg_image_width": "900", - "zbxtg_image_height": "200", - "tg_method_image": False, # if True - default send images, False - send text - "tg_chat": False, # send message to chat or in private - "is_debug": False, - "is_channel": False, - "disable_web_page_preview": False, - } - settings_description = { - "itemid": {"name": "zbxtg_itemid", "type": "int"}, - "title": {"name": "zbxtg_title", "type": "str"}, - "graphs_period": {"name": "zbxtg_image_period", "type": "int"}, - "graphs_width": {"name": "zbxtg_image_width", "type": "int"}, - "graphs_height": {"name": "zbxtg_image_height", "type": "int"}, - "graphs": {"name": "tg_method_image", "type": "bool"}, - "chat": {"name": "tg_chat", "type": "bool"}, - "debug": {"name": "is_debug", "type": "bool"}, - "channel": {"name": "is_channel", "type": "bool"}, - "disable_web_page_preview": {"name": "disable_web_page_preview", "type": "bool"}, - } - for line in zbxtg_body: if line.find(zbxtg_settings.zbx_tg_prefix) > -1: - setting = re.split("[\s\:\=]+", line, maxsplit=1) + setting = re.split("[\s:=]+", line, maxsplit=1) key = setting[0].replace(zbxtg_settings.zbx_tg_prefix + ";", "") - if len(setting) > 1 and len(setting[1]) > 0: + if key not in settings_description: + if "--debug" in args: + print_message("[ERROR] There is no '{0}' method, use --features to get help".format(key)) + continue + if settings_description[key]["type"] == "list": + value = setting[1].split(",") + elif len(setting) > 1 and len(setting[1]) > 0: value = setting[1] - else: + elif settings_description[key]["type"] == "bool": value = True + else: + value = settings[settings_description[key]["name"]] if key in settings_description: settings[settings_description[key]["name"]] = value else: @@ -324,16 +677,19 @@ def main(): tg_method_image = bool(settings["tg_method_image"]) tg_chat = bool(settings["tg_chat"]) + tg_group = bool(settings["tg_group"]) is_debug = bool(settings["is_debug"]) is_channel = bool(settings["is_channel"]) disable_web_page_preview = bool(settings["disable_web_page_preview"]) + is_single_message = bool(settings["is_single_message"]) # experimental way to send message to the group https://github.com/ableev/Zabbix-in-Telegram/issues/15 - if sys.argv[0].split("/")[-1] == "zbxtg_group.py" or "--group" in sys.argv or tg_chat: + if args[0].split("/")[-1] == "zbxtg_group.py" or "--group" in args or tg_chat or tg_group: tg_chat = True + tg_group = True tg.type = "group" - if "--debug" in sys.argv or is_debug: + if "--debug" in args or is_debug: is_debug = True tg.debug = True zbx.debug = True @@ -342,20 +698,41 @@ def main(): log_file = tmp_dir + ".debug." + hash_ts + ".log" #print_message(log_file) - if "--markdown" in sys.argv: + if "--markdown" in args or settings["markdown"]: tg.markdown = True - if "--html" in sys.argv: + if "--html" in args or settings["html"]: tg.html = True - if "--channel" in sys.argv or is_channel: + if "--channel" in args or is_channel: tg.type = "channel" - if "--disable_web_page_preview" in sys.argv or disable_web_page_preview: + if "--disable_web_page_preview" in args or disable_web_page_preview: if is_debug: print_message("'disable_web_page_preview' option has been enabled") tg.disable_web_page_preview = True + if "--graph_buttons" in args or settings["graph_buttons"]: + tg.image_buttons = True + + if "--forked" in args: + settings["forked"] = True + + if "--tg-key" in args: + tg.key = args[args.index("--tg-key") + 1] + + location_coordinates = {"latitude": None, "longitude": None} + if settings["lat"] > 0 and settings["lat"] > 0: + location_coordinates = {"latitude": settings["lat"], "longitude": settings["lon"]} + tg.location = location_coordinates + else: + if settings["location"]: + location_coordinates = map.get_coordinates_by_address(settings["location"]) + if location_coordinates: + settings["lat"] = location_coordinates["latitude"] + settings["lon"] = location_coordinates["longitude"] + tg.location = location_coordinates + if not os.path.isdir(tmp_dir): if is_debug: print_message("Tmp dir doesn't exist, creating new one...") @@ -369,13 +746,62 @@ def main(): if is_debug: print_message("Using {0} as a temporary dir".format(tmp_dir)) + done_all_work_in_the_fork = False + # issue75 + + to_types = ["to", "to_group", "to_channel"] + to_types_to_telegram = {"to": "private", "to_group": "group", "to_channel": "channel"} + multiple_to = {} + for i in to_types: + multiple_to[i]=[] + + for t in to_types: + try: + if settings[t] and not settings["forked"]: + # zbx_to = settings["to"] + multiple_to[t] = re.split(",", settings[t]) + except KeyError: + pass + + # example: + # {'to_channel': [], 'to': ['usr1', 'usr2', 'usr3'], 'to_group': []} + + if (sum([len(v) for k, v in list(multiple_to.items())])) == 1: + # if we have only one recipient, we don't need fork to send message, just re-write "to" vaiable + tmp_max = 0 + for t in to_types: + if len(multiple_to[t]) > tmp_max: + tmp_max = len(multiple_to[t]) + tg.type = to_types_to_telegram[t] + zbx_to = multiple_to[t][0] + else: + for t in to_types: + for i in multiple_to[t]: + args_new = list(args) + args_new[1] = i + if t == "to_group": + args_new.append("--group") + args_new.append("--forked") + args_new.insert(0, sys.executable) + if is_debug: + print_message("Fork for custom recipient ({1}), new args: {0}".format(args_new, + to_types_to_telegram[t])) + subprocess.call(args_new) + done_all_work_in_the_fork = True + + if done_all_work_in_the_fork: + sys.exit(0) + uid = None if tg.type == "channel": uid = zbx_to - else: + if tg.type == "private": zbx_to = zbx_to.replace("@", "") + if zbx_to.isdigit(): + uid = zbx_to + if not uid: uid = tg.get_uid_from_cache(zbx_to) @@ -395,59 +821,120 @@ def main(): # add signature, turned off by default, you can turn it on in config try: - if zbxtg_settings.zbx_tg_signature: + if "--signature" in args or settings["signature"] or zbxtg_settings.zbx_tg_signature\ + and not "--signature_disable" in args and not settings["signature_disable"]: + if "--signature" in args: + settings["signature"] = args[args.index("--signature") + 1] + if not settings["signature"]: + settings["signature"] = zbxtg_settings.zbx_server zbxtg_body_text.append("--") - zbxtg_body_text.append(zbxtg_settings.zbx_server) + zbxtg_body_text.append(settings["signature"]) except: pass # replace text with emojis + internal_using_emoji = False # I hate that, but... https://github.com/ableev/Zabbix-in-Telegram/issues/152 if hasattr(zbxtg_settings, "emoji_map"): zbxtg_body_text_emoji_support = [] for l in zbxtg_body_text: l_new = l - for k, v in zbxtg_settings.emoji_map.iteritems(): + for k, v in list(zbxtg_settings.emoji_map.items()): l_new = l_new.replace("{{" + k + "}}", v) zbxtg_body_text_emoji_support.append(l_new) + if len("".join(zbxtg_body_text)) - len("".join(zbxtg_body_text_emoji_support)): + internal_using_emoji = True zbxtg_body_text = zbxtg_body_text_emoji_support - if not tg_method_image: - result = tg.send_message(uid, zbxtg_body_text) - if not result["ok"]: - if result["description"] == "[Error]: Bad Request: group chat is migrated to supergroup chat": - migrate_to_chat_id = result["parameters"]["migrate_to_chat_id"] - tg.update_cache_uid(zbx_to, uid, message="Group chat is migrated to supergroup, updating cache file") + if not is_single_message: + tg.send_message(uid, zbxtg_body_text) + if not tg.ok: + # first case – if group has been migrated to a supergroup, we need to update chat_id of that group + if tg.error.find("migrated") > -1 and tg.error.find("supergroup") > -1: + migrate_to_chat_id = tg.result["parameters"]["migrate_to_chat_id"] + tg.update_cache_uid(zbx_to, migrate_to_chat_id, message="Group chat is migrated to supergroup, " + "updating cache file") uid = migrate_to_chat_id - result = tg.send_message(uid, zbxtg_body_text) - else: + tg.send_message(uid, zbxtg_body_text) + + # another case if markdown is enabled and we got parse error, try to remove "bad" symbols from message + if tg.markdown and tg.error.find("Can't find end of the entity starting at byte offset") > -1: + markdown_warning = "Original message has been fixed due to {0}. " \ + "Please, fix the markdown, it's slowing down messages sending."\ + .format(url_wiki_base + "/" + settings_description["markdown"]["url"]) + markdown_fix_attempts = 0 + while not tg.ok and markdown_fix_attempts != 3: + offset = re.search("Can't find end of the entity starting at byte offset ([0-9]+)", tg.error).group(1) + zbxtg_body_text = markdown_fix(zbxtg_body_text, offset, emoji=internal_using_emoji) + \ + ["\n"] + [markdown_warning] + tg.disable_web_page_preview = True + tg.send_message(uid, zbxtg_body_text) + markdown_fix_attempts += 1 + if tg.ok: + print_message(markdown_warning) + + if is_debug: + print((tg.result)) + + if settings["zbxtg_image_age"]: + age_sec = age2sec(settings["zbxtg_image_age"]) + if age_sec > 0 and age_sec > 3600: + settings["zbxtg_image_period"] = age_sec + + message_id = 0 + if tg_method_image: zbx.login() if not zbx.cookie: - print_message("Login to Zabbix web UI has failed, check manually...") + text_warn = "Login to Zabbix web UI has failed (web url, user or password are incorrect), "\ + "unable to send graphs check manually" + tg.send_message(uid, [text_warn]) + print_message(text_warn) else: - zbxtg_file_img = zbx.graph_get(settings["zbxtg_itemid"], settings["zbxtg_image_period"], - settings["zbxtg_title"], settings["zbxtg_image_width"], - settings["zbxtg_image_height"], tmp_dir) - #zbxtg_body_text, is_modified = list_cut(zbxtg_body_text, 200) - result = tg.send_message(uid, zbxtg_body_text) - message_id = result["result"]["message_id"] + if not settings["extimg"]: + zbxtg_file_img = zbx.graph_get(settings["zbxtg_itemid"], settings["zbxtg_image_period"], + settings["zbxtg_title"], settings["zbxtg_image_width"], + settings["zbxtg_image_height"], version=zbx_version) + else: + zbxtg_file_img = external_image_get(settings["extimg"], tmp_dir=zbx.tmp_dir) + zbxtg_body_text, is_modified = list_cut(zbxtg_body_text, 200) + if tg.ok: + message_id = tg.result["result"]["message_id"] tg.reply_to_message_id = message_id - tg.disable_notification = True if not zbxtg_file_img: - tg.send_message(uid, ["Can't get graph image, check script manually, see logs, or disable graphs"]) - print_message("Can't get image, check URL manually") + text_warn = "Can't get graph image, check script manually, see logs, or disable graphs" + tg.send_message(uid, [text_warn]) + print_message(text_warn) else: - - zbxtg_body_text = "" - """ - if is_modified: - print_message("probably you will see MEDIA_CAPTION_TOO_LONG error, " - "the message has been cut to 200 symbols, " - "https://github.com/ableev/Zabbix-in-Telegram/issues/9" - "#issuecomment-166895044") - """ - if tg.send_photo(uid, zbxtg_body_text, zbxtg_file_img): + if not is_single_message: + zbxtg_body_text = "" + else: + if is_modified: + text_warn = "probably you will see MEDIA_CAPTION_TOO_LONG error, "\ + "the message has been cut to 200 symbols, "\ + "https://github.com/ableev/Zabbix-in-Telegram/issues/9"\ + "#issuecomment-166895044" + print_message(text_warn) + if not is_single_message: + tg.disable_notification = True + tg.send_photo(uid, zbxtg_body_text, zbxtg_file_img) + if tg.ok: + settings["zbxtg_body_text"] = zbxtg_body_text os.remove(zbxtg_file_img) - + else: + if tg.error.find("PHOTO_INVALID_DIMENSIONS") > -1: + if not tg.disable_web_page_preview: + tg.disable_web_page_preview = True + text_warn = "Zabbix user couldn't get graph (probably has no rights to get data from host), " \ + "check script manually, see {0}".format(url_wiki_base + "/" + + settings_description["graphs"]["url"]) + tg.send_message(uid, [text_warn]) + print_message(text_warn) + if tg.location and location_coordinates["latitude"] and location_coordinates["longitude"]: + tg.reply_to_message_id = message_id + tg.disable_notification = True + tg.send_location(to=uid, coordinates=location_coordinates) + + if "--show-settings" in args: + print_message("Settings: " + str(json.dumps(settings, indent=2))) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/zbxtg.sh b/zbxtg.sh deleted file mode 100755 index f9c9663..0000000 --- a/zbxtg.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/bin/bash - -. $(dirname "$0")/tg_vars.cfg - -CURL_TG="${CURL} https://api.telegram.org/bot${TG_KEY}" - -TMP_DIR="/tmp/${ZBX_TG_PREFIX}" -[ ! -d "${TMP_DIR}" ] && (mkdir -p ${TMP_DIR} || TMP_DIR="/tmp") -TMP_COOKIE="${TMP_DIR}/cookie.txt" -TMP_UIDS="${TMP_DIR}/uids.txt" - -TS="`date +%s_%N`_$RANDOM" -LOG="/dev/null" - -IS_DEBUG () { - if [ "${ISDEBUG}" == "TRUE" ] - then - return 0 - else - return 1 - fi -} - - -login() { - # grab cookie for downloading image - IS_DEBUG && echo "${CURL} --cookie-jar ${TMP_COOKIE} --request POST --data \"name=${ZBX_API_USER}&password=${ZBX_API_PASS}&enter=Sign%20in\" ${ZBX_SERVER}/" >>${LOG} - ${CURL} --cookie-jar ${TMP_COOKIE} --request POST --data "name=${ZBX_API_USER}&password=${ZBX_API_PASS}&enter=Sign%20in" ${ZBX_SERVER}/ -} - -get_image() { - URL=$1 - URL=$(echo "${URL}" | sed -e 's/\ /%20/g') - IMG_NAME=$2 - # downloads png graph and saves it to temporary path - IS_DEBUG && echo "${CURL} --cookie ${TMP_COOKIE} --globoff \"${URL}\" -o ${IMG_NAME}" >>${LOG} - ${CURL} --cookie ${TMP_COOKIE} --globoff "${URL}" -o ${IMG_NAME} -} - -TO=$1 -SUBJECT=$2 -BODY=$3 - -TG_CHAT=0 # send message to chat or to private chat to user -METHOD="txt" # sendMessage (simple text) or sendPhoto (attached image) - -echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};graphs" && METHOD="image" -echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};chat" && TG_CHAT=1 -echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};debug" && ISDEBUG="TRUE" - -IS_DEBUG && LOG="${TMP_DIR}/debug.${TS}.log" -IS_DEBUG && echo -e "TMP_DIR=${TMP_DIR}\nTMP_COOKIE=${TMP_COOKIE}\nTMP_UIDS=${TMP_UIDS}" >>${LOG} - -if [ "${TG_CHAT}" -eq 1 ] -then - TG_CONTACT_TYPE="chat" -else - TG_CONTACT_TYPE="user" -fi - -TG_CHAT_ID=$(cat ${TMP_UIDS} | awk -F ';' '{if ($1 == "'${TO}'" && $2 == "'${TG_CONTACT_TYPE}'") print $3}' | tail -1) - -if [ -z "${TG_CHAT_ID}" ] -then - TG_UPDATES=$(${CURL_TG}/getUpdates) - if [ "${TG_CHAT}" -eq 1 ] - then - TG_CHAT_ID=$(echo "${TG_UPDATES}" | sed -e 's/["}{]//g' | awk -F ',' '{if ($8 == "type:group" && $7 == "title:'${TO}'") {gsub("chat:id:", "", $6); print $6}}' | tail -1) - else - TG_CHAT_ID=$(echo "${TG_UPDATES}" | sed -e 's/["}{]//g' | awk -F ',' '{if ($10 == "type:private" && $5 == "username:'${TO}'") {gsub("chat:id:", "", $6); print $6}}' | tail -1) - fi - echo "${TO};${TG_CONTACT_TYPE};${TG_CHAT_ID}" >>${TMP_UIDS} -fi - -IS_DEBUG && echo "TG_CHAT_ID: ${TG_CHAT_ID}" >>${LOG} - -TG_TEXT=$(echo "${BODY}" | grep -vE "^${ZBX_TG_PREFIX};"; echo "--"; echo "${ZBX_SERVER}") - -case "${METHOD}" in - - "txt") - TG_MESSAGE=$(echo -e "${SUBJECT}\n${TG_TEXT}") - IS_DEBUG && echo "${CURL_TG}/sendMessage -F \"chat_id=${TG_CHAT_ID}\" -F \"text=${TG_MESSAGE}\"" >>${LOG} - ${CURL_TG}/sendMessage -F "chat_id=${TG_CHAT_ID}" -F "text=${TG_MESSAGE}" >>${LOG} 2>&1 - ;; - - "image") - PERIOD=3600 # default period - echo "${BODY}" | grep -q "^${ZBX_TG_PREFIX};graphs_period" && PERIOD=$(echo "${BODY}" | awk -F "${ZBX_TG_PREFIX};graphs_period=" '{if ($2 != "") print $2}' | tail -1 | grep -Eo '[0-9]+' || echo 3600) - ZBX_ITEMID=$(echo "${BODY}" | awk -F "${ZBX_TG_PREFIX};itemid:" '{if ($2 != "") print $2}' | tail -1 | grep -Eo '[0-9]+') - ZBX_TITLE=$(echo "${BODY}" | awk -F "${ZBX_TG_PREFIX};title:" '{if ($2 != "") print $2}' | tail -1) - URL="${ZBX_SERVER}/chart3.php?period=${PERIOD}&name=${ZBX_TITLE}&width=900&height=200&graphtype=0&legend=1&items[0][itemid]=${ZBX_ITEMID}&items[0][sortorder]=0&items[0][drawtype]=5&items[0][color]=00CC00" - IS_DEBUG && echo "Zabbix graph URL: ${URL}" >> ${LOG} - login - CACHE_IMAGE="${TMP_DIR}/graph.${ZBX_ITEMID}.png" - IS_DEBUG && echo "Image cached to ${CACHE_IMAGE} and wasn't deleted" >> ${LOG} - get_image "${URL}" ${CACHE_IMAGE} - TG_CAPTION_ORIG=$(echo -e "${SUBJECT}\n${TG_TEXT}") - TG_CAPTION=$(echo -e $(echo "${TG_CAPTION_ORIG}" | sed ':a;N;$!ba;s/\n/\\n/g' | awk '{print substr( $0, 0, 200 )}')) - if [ "${TG_CAPTION}" != "${TG_CAPTION_ORIG}" ] - then - echo "${ZBX_TG_PREFIX}: probably you will see MEDIA_CAPTION_TOO_LONG error, the message has been cut to 200 symbols, https://github.com/ableev/Zabbix-in-Telegram/issues/9#issuecomment-166895044" - fi - IS_DEBUG && echo "${CURL_TG}/sendPhoto -F \"chat_id=${TG_CHAT_ID}\" -F \"caption=${TG_CAPTION}\" -F \"photo=@${CACHE_IMAGE}\"" >>${LOG} - ${CURL_TG}/sendPhoto -F "chat_id=${TG_CHAT_ID}" -F "caption=${TG_CAPTION}" -F "photo=@${CACHE_IMAGE}" >>${LOG} 2>&1 - IS_DEBUG || rm ${CACHE_IMAGE} - ;; - -esac - -echo >>${LOG} diff --git a/zbxtg_settings.example.py b/zbxtg_settings.example.py index 4b313df..4b9e656 100644 --- a/zbxtg_settings.example.py +++ b/zbxtg_settings.example.py @@ -3,27 +3,66 @@ tg_key = "XYZ" # telegram bot api key zbx_tg_prefix = "zbxtg" # variable for separating text from script info -zbx_tg_tmp_dir = "/tmp/" + zbx_tg_prefix # directory for saving caches, uids, cookies, etc. +zbx_tg_tmp_dir = "/var/tmp/" + zbx_tg_prefix # directory for saving caches, uids, cookies, etc. zbx_tg_signature = False -zbx_server = "http://localhost" # zabbix server full url +zbx_tg_update_messages = True +zbx_tg_matches = { + "problem": "PROBLEM: ", + "ok": "OK: " +} + +zbx_server = "http://127.0.0.1/zabbix/" # zabbix server full url zbx_api_user = "api" zbx_api_pass = "api" zbx_api_verify = True # True - do not ignore self signed certificates, False - ignore +#zbx_server_version = 2 # for Zabbix 2.x version +zbx_server_version = 3 # for Zabbix 3.x version, by default, not everyone updated to 4.x yet +#zbx_server_version = 4 # for Zabbix 4.x version, default will be changed in the future with this + +zbx_basic_auth = False +zbx_basic_auth_user = "zabbix" +zbx_basic_auth_pass = "zabbix" + proxy_to_zbx = None proxy_to_tg = None -#proxy_to_zbx = "proxy.local:3128" -#proxy_to_tg = "proxy.local:3128" +# proxy_to_zbx = "http://proxy.local:3128" +# proxy_to_tg = "https://proxy.local:3128" + +# proxy_to_tg = "socks5://user1:password2@hostname:port" # socks5 with username and password +# proxy_to_tg = "socks5://hostname:port" # socks5 without username and password +# proxy_to_tg = "socks5h://hostname:port" # hostname resolution on SOCKS proxy. + # This helps when internet provider alter DNS queries. + # Found here: https://stackoverflow.com/a/43266186/957508 + +google_maps_api_key = None # get your key, see https://developers.google.com/maps/documentation/geocoding/intro + +zbx_tg_daemon_enabled = False +zbx_tg_daemon_enabled_ids = [6931850, ] +zbx_tg_daemon_enabled_users = ["ableev", ] +zbx_tg_daemon_enabled_chats = ["Zabbix in Telegram Script", ] + +zbx_db_host = "localhost" +zbx_db_database = "zabbix" +zbx_db_user = "zbxtg" +zbx_db_password = "zbxtg" + emoji_map = { - "ok": "✅", - "problem": "❗", + "Disaster": "đŸ”Ĩ", + "High": "🛑", + "Average": "❗", + "Warning": "⚠ī¸", + "Information": "ℹī¸", + "Not classified": "🔘", + "OK": "✅", + "PROBLEM": "❗", "info": "ℹī¸", - "warning": "⚠ī¸", - "disaster": "❌", + "WARNING": "⚠ī¸", + "DISASTER": "❌", "bomb": "đŸ’Ŗ", "fire": "đŸ”Ĩ", "hankey": "💩", -} \ No newline at end of file +}