├── zbxtg_group.py ├── requirements.txt ├── bash-old-version ├── tg_vars.cfg.example └── zbxtg.sh ├── LICENSE.txt ├── zbxtg_settings.example.py ├── README.md ├── ZbxTgDaemon.py └── zbxtg.py /zbxtg_group.py: -------------------------------------------------------------------------------- 1 | zbxtg.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PySocks==1.6.8 2 | requests==2.20.0 3 | requests-oauthlib==0.6.2 4 | -------------------------------------------------------------------------------- /bash-old-version/tg_vars.cfg.example: -------------------------------------------------------------------------------- 1 | TG_KEY="000:AAAAA_bbbb" 2 | 3 | ZBX_TG_PREFIX="zbxtg" # variable for separating text from script info 4 | ZBX_TG_SIGN="TRUE" 5 | 6 | ZBX_SERVER="http://zabbix.local" # zabbix server url 7 | ZBX_API_USER="api" # zabbix user; user must have at least read access to get graphs 8 | ZBX_API_PASS="apisecret" 9 | 10 | CURL="curl -s" # if you are using proxy server, it's time to add it right here 11 | # CURL="curl -x proxy.local:3128 -s" 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Ilya Ableev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /zbxtg_settings.example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | tg_key = "XYZ" # telegram bot api key 4 | 5 | zbx_tg_prefix = "zbxtg" # variable for separating text from script info 6 | zbx_tg_tmp_dir = "/var/tmp/" + zbx_tg_prefix # directory for saving caches, uids, cookies, etc. 7 | zbx_tg_signature = False 8 | 9 | zbx_tg_update_messages = True 10 | zbx_tg_matches = { 11 | "problem": "PROBLEM: ", 12 | "ok": "OK: " 13 | } 14 | 15 | zbx_server = "http://127.0.0.1/zabbix/" # zabbix server full url 16 | zbx_api_user = "api" 17 | zbx_api_pass = "api" 18 | zbx_api_verify = True # True - do not ignore self signed certificates, False - ignore 19 | 20 | #zbx_server_version = 2 # for Zabbix 2.x version 21 | zbx_server_version = 3 # for Zabbix 3.x version, by default, not everyone updated to 4.x yet 22 | #zbx_server_version = 4 # for Zabbix 4.x version, default will be changed in the future with this 23 | 24 | zbx_basic_auth = False 25 | zbx_basic_auth_user = "zabbix" 26 | zbx_basic_auth_pass = "zabbix" 27 | 28 | proxy_to_zbx = None 29 | proxy_to_tg = None 30 | 31 | # proxy_to_zbx = "http://proxy.local:3128" 32 | # proxy_to_tg = "https://proxy.local:3128" 33 | 34 | # proxy_to_tg = "socks5://user1:password2@hostname:port" # socks5 with username and password 35 | # proxy_to_tg = "socks5://hostname:port" # socks5 without username and password 36 | # proxy_to_tg = "socks5h://hostname:port" # hostname resolution on SOCKS proxy. 37 | # This helps when internet provider alter DNS queries. 38 | # Found here: https://stackoverflow.com/a/43266186/957508 39 | 40 | google_maps_api_key = None # get your key, see https://developers.google.com/maps/documentation/geocoding/intro 41 | 42 | zbx_tg_daemon_enabled = False 43 | zbx_tg_daemon_enabled_ids = [6931850, ] 44 | zbx_tg_daemon_enabled_users = ["ableev", ] 45 | zbx_tg_daemon_enabled_chats = ["Zabbix in Telegram Script", ] 46 | 47 | zbx_db_host = "localhost" 48 | zbx_db_database = "zabbix" 49 | zbx_db_user = "zbxtg" 50 | zbx_db_password = "zbxtg" 51 | 52 | 53 | emoji_map = { 54 | "Disaster": "🔥", 55 | "High": "🛑", 56 | "Average": "❗", 57 | "Warning": "⚠️", 58 | "Information": "ℹ️", 59 | "Not classified": "🔘", 60 | "OK": "✅", 61 | "PROBLEM": "❗", 62 | "info": "ℹ️", 63 | "WARNING": "⚠️", 64 | "DISASTER": "❌", 65 | "bomb": "💣", 66 | "fire": "🔥", 67 | "hankey": "💩", 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zabbix-in-Telegram 2 | Zabbix Notifications with graphs in Telegram 3 | 4 | Join us in our **Telegram group** via this link: https://t.me/ZbxTg 5 | 6 | Subscribe to our channel: https://t.me/Zabbix_in_Telegram 7 | 8 | Rate on [share.zabbix.com](https://share.zabbix.com): https://share.zabbix.com/cat-notifications/zabbix-in-telegram 9 | 10 | ### Features 11 | - [x] Graphs based on latest data are sent directly to your messenger 12 | - [x] You can send messages both in private and group/supergroup chats 13 | - [x] Channels support (only public, but you can do it for private as well with dirty hack) 14 | - [x] Saves chatid as a temporary file 15 | - [x] Simple markdown and HTML are supported 16 | - [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) 17 | - [x] Location map 18 | 19 | ### TODOs 20 | - Simple zabbix's management via bot's commands – in dev state 21 | - Ability to send complex graph or part of screen 22 | 23 | 24 | ### Configuration / Installation 25 | 26 | **READ WIKI IF YOU HAVE PROBLEM WITH SOMETHING**: https://github.com/ableev/Zabbix-in-Telegram/wiki 27 | 28 | **First of all**: You need to install the appropriate modules for python, this is required for operation!
29 | To do so, enter `pip install -r requirements.txt` in your commandline! 30 | 31 | * Put `zbxtg.py` in your `AlertScriptsPath` directory, the path is set inside your `zabbix_server.conf` 32 | * 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) 33 | * 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 34 | * Create a bot in Telegram and get API key: https://core.telegram.org/bots#creating-a-new-bot 35 | * Create readonly user in Zabbix web interface (for getting graphs from zabbix) 36 | * Set proxy host:port in `zbxtg_settings.py` if you need an internet proxy (socks5 supported as well, the wiki will help you) 37 | * Add new media for Telegram in Zabbix web interface with these settings: 38 | 39 | 40 | 41 | * Add another one if you want to send messages to the group 42 | 43 | 44 | 45 | * **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 46 | * Send a message to your bot via Telegram, e.g. "/start" 47 | * If you are in a group chat, start a conversation with your bot: `/start@ZbxTgDevBot` 48 | * Create a new action like this: 49 | ``` 50 | Last value: {ITEM.LASTVALUE1} ({TIME}) 51 | zbxtg;graphs 52 | zbxtg;graphs_period=10800 53 | zbxtg;itemid:{ITEM.ID1} 54 | zbxtg;title:{HOST.HOST} - {TRIGGER.NAME} 55 | ``` 56 | 57 | 58 | 59 | * Add the appropriate Media Type to your user 60 | * The username is **CASE-SENSITIVE** 61 | * If you don't have a username, you can use your chatid directly (and you need to google how to get it) 62 | * Group chats don't have URLs, so you need to put group's name in media type 63 | * 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) 64 | 65 | * Private: 66 | 67 | 68 | 69 | * Group: 70 | 71 | 72 | 73 | #### Annotations 74 | ``` 75 | zbxtg;graphs -- enables attached graphs 76 | zbxtg;graphs_period=10800 -- set graphs period (default - 3600 seconds) 77 | zbxtg;graphs_width=700 -- set graphs width (default - 900px) 78 | zbxtg;graphs_height=300 -- set graphs height (default - 300px) 79 | zbxtg;itemid:{ITEM.ID1} -- define itemid (from trigger) for attach 80 | zbxtg;itemid:{ITEM.ID1},{ITEM.ID2},{ITEM.ID3} -- same, but if you want to send two or more graphs, use complex trigger 81 | zbxtg;title:{HOST.HOST} - {TRIGGER.NAME} -- graph's title 82 | zbxtg;debug -- enables debug mode, some logs and images will be saved in the tmp dir (temporary doesn't affect python version) 83 | zbxtg;channel -- enables sending to channels 84 | 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) 85 | zbxtg;to_group:Group Name One,Group Name Two -- the same but for groups 86 | ``` 87 | 88 | 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. 89 | 90 | #### Debug 91 | 92 | * You can use the following command to send a message from your command line:
93 | `./zbxtg.py "@username" "first part of a message" "second part of a message" --debug` 94 | * For `@username` substitute your Telegram username, **NOT that of your bot** (case-sensitive) OR chatid 95 | * 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) 96 | * You can skip the `"` if it's one word for every parameter, these are optional 97 | 98 | --- 99 | 100 | ![](http://i.imgur.com/1T4aHuf.png) 101 | ![](http://i.imgur.com/5ZPyvoe.png) 102 | 103 | ### Known issues 104 | 105 | #### MEDIA_CAPTION_TOO_LONG 106 | If you see this error, it means that you rich the limit of caption with 200 symbols in it (Telegram API's limitaion). 107 | Such captions will be automatically cut to 200 symbols. 108 | 109 | #### Zabbix 3.0 and higher (3.2, 3.4, 4.0, 4.2, 4.4) 110 | https://github.com/ableev/Zabbix-in-Telegram/wiki/Working-with-Zabbix-3.0 111 | -------------------------------------------------------------------------------- /bash-old-version/zbxtg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . $(dirname "$0")/tg_vars.cfg 4 | 5 | CURL_TG="${CURL} https://api.telegram.org/bot${TG_KEY}" 6 | 7 | TMP_DIR="/tmp/${ZBX_TG_PREFIX}" 8 | [ ! -d "${TMP_DIR}" ] && (mkdir -p ${TMP_DIR} || TMP_DIR="/tmp") 9 | TMP_COOKIE="${TMP_DIR}/cookie.txt" 10 | TMP_UIDS="${TMP_DIR}/uids.txt" 11 | 12 | TS="`date +%s_%N`_$RANDOM" 13 | LOG="/dev/null" 14 | 15 | IS_DEBUG () { 16 | if [ "${ISDEBUG}" == "TRUE" ] 17 | then 18 | return 0 19 | else 20 | return 1 21 | fi 22 | } 23 | 24 | 25 | login() { 26 | # grab cookie for downloading image 27 | 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} 28 | ${CURL} --cookie-jar ${TMP_COOKIE} --request POST --data "name=${ZBX_API_USER}&password=${ZBX_API_PASS}&enter=Sign%20in" ${ZBX_SERVER}/ 29 | } 30 | 31 | get_image() { 32 | URL=$1 33 | URL=$(echo "${URL}" | sed -e 's/\ /%20/g') 34 | IMG_NAME=$2 35 | # downloads png graph and saves it to temporary path 36 | IS_DEBUG && echo "${CURL} --cookie ${TMP_COOKIE} --globoff \"${URL}\" -o ${IMG_NAME}" >>${LOG} 37 | ${CURL} --cookie ${TMP_COOKIE} --globoff "${URL}" -o ${IMG_NAME} 38 | } 39 | 40 | TO=$1 41 | SUBJECT=$2 42 | BODY=$3 43 | 44 | TG_GROUP=0 # send message to chat or to private chat to user 45 | TG_CHANNEL=0 # send message to channel 46 | METHOD="txt" # sendMessage (simple text) or sendPhoto (attached image) 47 | 48 | echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};graphs" && METHOD="image" 49 | echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};chat" && TG_GROUP=1 50 | echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};group" && TG_GROUP=1 51 | echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};debug" && ISDEBUG="TRUE" 52 | echo "${BODY}" | grep -q "${ZBX_TG_PREFIX};channel" && TG_CHANNEL=1 53 | 54 | IS_DEBUG && LOG="${TMP_DIR}/debug.${TS}.log" 55 | IS_DEBUG && echo -e "TMP_DIR=${TMP_DIR}\nTMP_COOKIE=${TMP_COOKIE}\nTMP_UIDS=${TMP_UIDS}" >>${LOG} 56 | 57 | if [ "${TG_GROUP}" -eq 1 ] 58 | then 59 | TG_CONTACT_TYPE="group" 60 | else 61 | TG_CONTACT_TYPE="private" 62 | fi 63 | 64 | TG_CHAT_ID=$(cat ${TMP_UIDS} | awk -F ';' '{if ($1 == "'${TO}'" && $2 == "'${TG_CONTACT_TYPE}'") print $3}' | tail -1) 65 | 66 | if [ "${TG_CHANNEL}" -eq 1 ] 67 | then 68 | TG_CHAT_ID="${TO}" 69 | fi 70 | 71 | if [ -z "${TG_CHAT_ID}" ] 72 | then 73 | TG_UPDATES=$(${CURL_TG}/getUpdates | sed -e 's/},{/\n/') 74 | for (( idx=${#TG_UPDATES[@]}-1 ; idx>=0 ; idx-- )) 75 | do 76 | UPDATE="${TG_UPDATES[idx]}" 77 | echo "${UPDATE}" 78 | if [ "${TG_GROUP}" -eq 1 ] 79 | then 80 | 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) 81 | if [ "$(echo ${TG_CHAT_ID} | grep -Eq '\-[0-9]+' && echo 1 || echo 0)" -eq 1 ] 82 | then 83 | break 84 | fi 85 | else 86 | 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) 87 | if [ "$(echo ${TG_CHAT_ID} | grep -Eq '[0-9]+' && echo 1 || echo 0)" -eq 1 ] 88 | then 89 | break 90 | fi 91 | fi 92 | done 93 | echo "${TO};${TG_CONTACT_TYPE};${TG_CHAT_ID}" >>${TMP_UIDS} 94 | fi 95 | 96 | IS_DEBUG && echo "TG_CHAT_ID: ${TG_CHAT_ID}" >>${LOG} 97 | 98 | TG_TEXT=$(echo "${BODY}" | grep -vE "^${ZBX_TG_PREFIX};") 99 | if [ "${ZBX_TG_SIGN}" != "FALSE" ] 100 | then 101 | TG_TEXT=$(echo ${TG_TEXT}; echo "--"; echo "${ZBX_SERVER}") 102 | fi 103 | 104 | case "${METHOD}" in 105 | 106 | "txt") 107 | TG_MESSAGE=$(echo -e "${SUBJECT}\n${TG_TEXT}") 108 | IS_DEBUG && echo "${CURL_TG}/sendMessage -F \"chat_id=${TG_CHAT_ID}\" -F \"text=${TG_MESSAGE}\"" >>${LOG} 109 | ANSWER=$(${CURL_TG}/sendMessage?chat_id=${TG_CHAT_ID} --form "text=${TG_MESSAGE}" 2>&1) 110 | if [ "$(echo "${ANSWER}" | grep -Ec 'migrated.*supergroup')" -eq 1 ] 111 | then 112 | migrate_to_chat_id=$(echo "${ANSWER}" | sed -e 's/["}{]//g' | grep -Eo '\-[0-9]+$') 113 | echo "${TO};${TG_CONTACT_TYPE};${migrate_to_chat_id}" >>${TMP_UIDS} 114 | ANSWER=$(${CURL_TG}/sendMessage?chat_id=${migrate_to_chat_id} --form "text=${TG_MESSAGE}" 2>&1) 115 | fi 116 | ;; 117 | 118 | "image") 119 | PERIOD=3600 # default period 120 | 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) 121 | ZBX_ITEMID=$(echo "${BODY}" | awk -F "${ZBX_TG_PREFIX};itemid:" '{if ($2 != "") print $2}' | tail -1 | grep -Eo '[0-9]+') 122 | ZBX_TITLE=$(echo "${BODY}" | awk -F "${ZBX_TG_PREFIX};title:" '{if ($2 != "") print $2}' | tail -1) 123 | 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" 124 | IS_DEBUG && echo "Zabbix graph URL: ${URL}" >> ${LOG} 125 | login 126 | CACHE_IMAGE="${TMP_DIR}/graph.${ZBX_ITEMID}.png" 127 | IS_DEBUG && echo "Image cached to ${CACHE_IMAGE} and wasn't deleted" >> ${LOG} 128 | get_image "${URL}" ${CACHE_IMAGE} 129 | TG_CAPTION_ORIG=$(echo -e "${SUBJECT}\n${TG_TEXT}") 130 | TG_CAPTION=$(echo -e $(echo "${TG_CAPTION_ORIG}" | sed ':a;N;$!ba;s/\n/\\n/g' | awk '{print substr( $0, 0, 200 )}')) 131 | if [ "${TG_CAPTION}" != "${TG_CAPTION_ORIG}" ] 132 | then 133 | 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" 134 | fi 135 | IS_DEBUG && echo "${CURL_TG}/sendPhoto?chat_id=${TG_CHAT_ID}\" --form \"caption=${TG_CAPTION}\" -F \"photo=@${CACHE_IMAGE}\"" >>${LOG} 136 | ANSWER=$(${CURL_TG}/sendPhoto?chat_id=${TG_CHAT_ID} --form "caption=${TG_CAPTION}" -F "photo=@${CACHE_IMAGE}") 137 | IS_DEBUG || rm ${CACHE_IMAGE} 138 | ;; 139 | 140 | esac 141 | 142 | echo >>${LOG} 143 | -------------------------------------------------------------------------------- /ZbxTgDaemon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import sys 5 | import os 6 | import hashlib 7 | import re 8 | import time 9 | from os.path import dirname 10 | import zbxtg_settings 11 | import zbxtg 12 | from pyzabbix import ZabbixAPI, ZabbixAPIException 13 | 14 | 15 | class zabbixApi(): 16 | def __init__(self, server, user, password): 17 | self.api = ZabbixAPI(server) 18 | self.user = user 19 | self.password = password 20 | 21 | def login(self): 22 | self.api.login(self.user, self.password) 23 | 24 | def triggers_active(self): 25 | return self.api.trigger.get(output="extend", monitored=True, filter={"value": 1}, sortfield="priority", sortorder="DESC", 26 | selectHosts="extend") 27 | 28 | 29 | 30 | def print_message(string): 31 | string = str(string) + "\n" 32 | filename = sys.argv[0].split("/")[-1] 33 | sys.stderr.write(filename + ": " + string) 34 | 35 | 36 | def file_write(filename, text): 37 | with open(filename, "w") as fd: 38 | fd.write(str(text)) 39 | return True 40 | 41 | 42 | def file_read(filename): 43 | with open(filename, 'r') as fd: 44 | text = fd.readlines() 45 | return text 46 | 47 | 48 | def main(): 49 | TelegramAPI = zbxtg.TelegramAPI 50 | ZabbixWeb = zbxtg.ZabbixWeb 51 | tmp_dir = zbxtg_settings.zbx_tg_tmp_dir 52 | 53 | if not zbxtg_settings.zbx_tg_daemon_enabled: 54 | print("You should enable daemon by adding 'zbx_tg_remote_control' in the configuration file") 55 | sys.exit(1) 56 | 57 | tmp_uids = tmp_dir + "/uids.txt" 58 | tmp_ts = { 59 | "message_id": tmp_dir + "/daemon_message_id.txt", 60 | "update_offset": tmp_dir + "/update_offset.txt", 61 | } 62 | 63 | for i, v in tmp_ts.iteritems(): 64 | if not os.path.exists(v): 65 | print_message("{0} doesn't exist, creating new one...".format(v)) 66 | file_write(v, "0") 67 | print_message("{0} successfully created".format(v)) 68 | 69 | message_id_last = file_read(tmp_ts["message_id"])[0].strip() 70 | if message_id_last: 71 | message_id_last = int(message_id_last) 72 | 73 | update_id = file_read(tmp_ts["update_offset"]) 74 | 75 | tg = TelegramAPI(key=zbxtg_settings.tg_key) 76 | if zbxtg_settings.proxy_to_tg: 77 | proxy_to_tg = zbxtg_settings.proxy_to_tg 78 | if not proxy_to_tg.find("http") and not proxy_to_tg.find("socks"): 79 | proxy_to_tg = "https://" + proxy_to_tg 80 | tg.proxies = { 81 | "https": "{0}".format(zbxtg_settings.proxy_to_tg), 82 | } 83 | zbx = ZabbixWeb(server=zbxtg_settings.zbx_server, username=zbxtg_settings.zbx_api_user, 84 | password=zbxtg_settings.zbx_api_pass) 85 | if zbxtg_settings.proxy_to_zbx: 86 | zbx.proxies = {"http": "http://{0}/".format(zbxtg_settings.proxy_to_zbx)} 87 | 88 | try: 89 | zbx_api_verify = zbxtg_settings.zbx_api_verify 90 | zbx.verify = zbx_api_verify 91 | except: 92 | pass 93 | 94 | zbxapi = zabbixApi(zbxtg_settings.zbx_server, zbxtg_settings.zbx_api_user, zbxtg_settings.zbx_api_pass) 95 | zbxapi.login() 96 | 97 | print(tg.get_me()) 98 | 99 | #hosts = zbxdb.db_query("SELECT hostid, host FROM hosts") 100 | 101 | commands = [ 102 | "/triggers", 103 | "/help", 104 | # "/graph", 105 | # "/history", 106 | # "/screen" 107 | ] 108 | 109 | def md5(fname): 110 | hash_md5 = hashlib.md5() 111 | with open(fname, "rb") as f: 112 | for chunk in iter(lambda: f.read(4096), b""): 113 | hash_md5.update(chunk) 114 | return hash_md5.hexdigest() 115 | 116 | md5sum = md5("ZbxTgDaemon.py") 117 | print md5sum 118 | 119 | try: 120 | while True: 121 | time.sleep(1) 122 | md5sum_new = md5("ZbxTgDaemon.py") 123 | if md5sum != md5sum_new: 124 | sys.exit(1) 125 | tg.update_offset = update_id 126 | updates = tg.get_updates() 127 | if not updates["result"]: 128 | continue 129 | for m in updates["result"]: 130 | if "message" not in m: 131 | continue 132 | update_id_last = m["update_id"] 133 | tg.update_offset = update_id_last 134 | if m["message"]["from"]["id"] not in zbxtg_settings.zbx_tg_daemon_enabled_ids: 135 | file_write(tmp_ts["update_offset"], update_id_last) 136 | continue 137 | print("Fuck this shit, I'm not going to answer to someone not from the whitelist") 138 | else: 139 | if not "text" in m["message"]: 140 | continue 141 | text = m["message"]["text"] 142 | to = m["message"]["from"]["id"] 143 | reply_text = list() 144 | if m["message"]["message_id"] > message_id_last: 145 | if re.search(r"^/(start|help)", text): 146 | reply_text.append("Hey, this is ZbxTgDaemon bot.") 147 | reply_text.append("https://github.com/ableev/Zabbix-in-Telegram") 148 | reply_text.append("If you need help, you can ask it in @ZbxTg group\n") 149 | reply_text.append("Available commands:") 150 | reply_text.append("\n".join(commands)) 151 | tg.disable_web_page_preview = True 152 | if re.search(r"^/triggers", text): 153 | triggers = zbxapi.triggers_active() 154 | if triggers: 155 | for t in triggers: 156 | reply_text.append("Severity: {0}, Host: {1}, Trigger: {2}".format( 157 | t["priority"], t["hosts"][0]["host"].encode('utf-8'), t["description"].encode('utf-8') 158 | )) 159 | else: 160 | reply_text.append("There are no triggers, have a nice day!") 161 | if not reply_text: 162 | reply_text = ["I don't know what to do about it"] 163 | if tg.send_message(to, reply_text): 164 | with open(tmp_ts["message_id"], "w") as message_id_file: 165 | message_id_file.write(str(m["message"]["message_id"])) 166 | message_id_last = m["message"]["message_id"] 167 | tg.disable_web_page_preview = False 168 | file_write(tmp_ts["update_offset"], update_id_last) 169 | except KeyboardInterrupt: 170 | print("Exiting...") 171 | 172 | 173 | if __name__ == "__main__": 174 | main() -------------------------------------------------------------------------------- /zbxtg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import sys 5 | import os 6 | import time 7 | import random 8 | import string 9 | import requests 10 | import json 11 | import re 12 | import stat 13 | import hashlib 14 | import subprocess 15 | #import sqlite3 16 | from os.path import dirname 17 | import zbxtg_settings 18 | 19 | 20 | class Cache: 21 | def __init__(self, database): 22 | self.database = database 23 | 24 | def create_db(self, database): 25 | pass 26 | 27 | 28 | class TelegramAPI: 29 | tg_url_bot_general = "https://api.telegram.org/bot" 30 | 31 | def http_get(self, url): 32 | answer = requests.get(url, proxies=self.proxies) 33 | self.result = answer.json() 34 | self.ok_update() 35 | return self.result 36 | 37 | def __init__(self, key): 38 | self.debug = False 39 | self.key = key 40 | self.proxies = {} 41 | self.type = "private" # 'private' for private chats or 'group' for group chats 42 | self.markdown = False 43 | self.html = False 44 | self.disable_web_page_preview = False 45 | self.disable_notification = False 46 | self.reply_to_message_id = 0 47 | self.tmp_dir = None 48 | self.tmp_uids = None 49 | self.location = {"latitude": None, "longitude": None} 50 | self.update_offset = 0 51 | self.image_buttons = False 52 | self.result = None 53 | self.ok = None 54 | self.error = None 55 | self.get_updates_from_file = False 56 | 57 | def get_me(self): 58 | url = self.tg_url_bot_general + self.key + "/getMe" 59 | me = self.http_get(url) 60 | return me 61 | 62 | def get_updates(self): 63 | url = self.tg_url_bot_general + self.key + "/getUpdates" 64 | params = {"offset": self.update_offset} 65 | if self.debug: 66 | print_message(url) 67 | answer = requests.post(url, params=params, proxies=self.proxies) 68 | self.result = answer.json() 69 | if self.get_updates_from_file: 70 | print_message("Getting updated from file getUpdates.txt") 71 | self.result = json.loads("".join(file_read("getUpdates.txt"))) 72 | if self.debug: 73 | print_message("Content of /getUpdates:") 74 | print_message(json.dumps(self.result)) 75 | self.ok_update() 76 | return self.result 77 | 78 | def send_message(self, to, message): 79 | url = self.tg_url_bot_general + self.key + "/sendMessage" 80 | message = "\n".join(message) 81 | params = {"chat_id": to, "text": message, "disable_web_page_preview": self.disable_web_page_preview, 82 | "disable_notification": self.disable_notification} 83 | if self.reply_to_message_id: 84 | params["reply_to_message_id"] = self.reply_to_message_id 85 | if self.markdown or self.html: 86 | parse_mode = "HTML" 87 | if self.markdown: 88 | parse_mode = "Markdown" 89 | params["parse_mode"] = parse_mode 90 | if self.debug: 91 | print_message("Trying to /sendMessage:") 92 | print_message(url) 93 | print_message("post params: " + str(params)) 94 | answer = requests.post(url, params=params, proxies=self.proxies) 95 | if answer.status_code == 414: 96 | self.result = {"ok": False, "description": "414 URI Too Long"} 97 | else: 98 | self.result = answer.json() 99 | self.ok_update() 100 | return self.result 101 | 102 | def update_message(self, to, message_id, message): 103 | url = self.tg_url_bot_general + self.key + "/editMessageText" 104 | message = "\n".join(message) 105 | params = {"chat_id": to, "message_id": message_id, "text": message, 106 | "disable_web_page_preview": self.disable_web_page_preview, 107 | "disable_notification": self.disable_notification} 108 | if self.markdown or self.html: 109 | parse_mode = "HTML" 110 | if self.markdown: 111 | parse_mode = "Markdown" 112 | params["parse_mode"] = parse_mode 113 | if self.debug: 114 | print_message("Trying to /editMessageText:") 115 | print_message(url) 116 | print_message("post params: " + str(params)) 117 | answer = requests.post(url, params=params, proxies=self.proxies) 118 | self.result = answer.json() 119 | self.ok_update() 120 | return self.result 121 | 122 | def send_photo(self, to, message, path): 123 | url = self.tg_url_bot_general + self.key + "/sendPhoto" 124 | message = "\n".join(message) 125 | if self.image_buttons: 126 | reply_markup = json.dumps({"inline_keyboard": [[ 127 | {"text": "R", "callback_data": "graph_refresh"}, 128 | {"text": "1h", "callback_data": "graph_period_3600"}, 129 | {"text": "3h", "callback_data": "graph_period_10800"}, 130 | {"text": "6h", "callback_data": "graph_period_21600"}, 131 | {"text": "12h", "callback_data": "graph_period_43200"}, 132 | {"text": "24h", "callback_data": "graph_period_86400"}, 133 | ], ]}) 134 | else: 135 | reply_markup = json.dumps({}) 136 | params = {"chat_id": to, "caption": message, "disable_notification": self.disable_notification, 137 | "reply_markup": reply_markup} 138 | if self.reply_to_message_id: 139 | params["reply_to_message_id"] = self.reply_to_message_id 140 | files = {"photo": open(path, 'rb')} 141 | if self.debug: 142 | print_message("Trying to /sendPhoto:") 143 | print_message(url) 144 | print_message(params) 145 | print_message("files: " + str(files)) 146 | answer = requests.post(url, params=params, files=files, proxies=self.proxies) 147 | self.result = answer.json() 148 | self.ok_update() 149 | return self.result 150 | 151 | def send_txt(self, to, text, text_name=None): 152 | path = self.tmp_dir + "/" + "zbxtg_txt_" 153 | url = self.tg_url_bot_general + self.key + "/sendDocument" 154 | text = "\n".join(text) 155 | if not text_name: 156 | path += "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10)) 157 | else: 158 | path += text_name 159 | path += ".txt" 160 | file_write(path, text) 161 | params = {"chat_id": to, "caption": path.split("/")[-1], "disable_notification": self.disable_notification} 162 | if self.reply_to_message_id: 163 | params["reply_to_message_id"] = self.reply_to_message_id 164 | files = {"document": open(path, 'rb')} 165 | if self.debug: 166 | print_message("Trying to /sendDocument:") 167 | print_message(url) 168 | print_message(params) 169 | print_message("files: " + str(files)) 170 | answer = requests.post(url, params=params, files=files, proxies=self.proxies) 171 | self.result = answer.json() 172 | self.ok_update() 173 | return self.result 174 | 175 | def get_uid(self, name): 176 | uid = 0 177 | if self.debug: 178 | print_message("Getting uid from /getUpdates...") 179 | updates = self.get_updates() 180 | for m in updates["result"]: 181 | if "message" in m: 182 | chat = m["message"]["chat"] 183 | elif "edited_message" in m: 184 | chat = m["edited_message"]["chat"] 185 | else: 186 | continue 187 | if chat["type"] == self.type == "private": 188 | if "username" in chat: 189 | if chat["username"] == name: 190 | uid = chat["id"] 191 | if (chat["type"] == "group" or chat["type"] == "supergroup") and self.type == "group": 192 | if "title" in chat: 193 | if sys.version_info[0] < 3: 194 | if chat["title"] == name.decode("utf-8"): 195 | uid = chat["id"] 196 | else: 197 | if chat["title"] == name: 198 | uid = chat["id"] 199 | return uid 200 | 201 | def error_need_to_contact(self, to): 202 | if self.type == "private": 203 | print_message("User '{0}' needs to send some text bot in private".format(to)) 204 | if self.type == "group": 205 | print_message("You need start a conversation with your bot first in '{0}' group chat, type '/start@{1}'" 206 | .format(to, self.get_me()["result"]["username"])) 207 | 208 | def update_cache_uid(self, name, uid, message="Add new string to cache file"): 209 | cache_string = "{0};{1};{2}\n".format(name, self.type, str(uid).rstrip()) 210 | # FIXME 211 | if self.debug: 212 | print_message("{0}: {1}".format(message, cache_string)) 213 | with open(self.tmp_uids, "a") as cache_file_uids: 214 | cache_file_uids.write(cache_string) 215 | return True 216 | 217 | def get_uid_from_cache(self, name): 218 | if self.debug: 219 | print_message("Trying to read cached uid for {0}, {1}, from {2}".format(name, self.type, self.tmp_uids)) 220 | uid = 0 221 | if os.path.isfile(self.tmp_uids): 222 | with open(self.tmp_uids, 'r') as cache_file_uids: 223 | cache_uids_old = cache_file_uids.readlines() 224 | for u in cache_uids_old: 225 | u_splitted = u.split(";") 226 | if name == u_splitted[0] and self.type == u_splitted[1]: 227 | uid = u_splitted[2] 228 | return uid 229 | 230 | def send_location(self, to, coordinates): 231 | url = self.tg_url_bot_general + self.key + "/sendLocation" 232 | params = {"chat_id": to, "disable_notification": self.disable_notification, 233 | "latitude": coordinates["latitude"], "longitude": coordinates["longitude"]} 234 | if self.reply_to_message_id: 235 | params["reply_to_message_id"] = self.reply_to_message_id 236 | if self.debug: 237 | print_message("Trying to /sendLocation:") 238 | print_message(url) 239 | print_message("post params: " + str(params)) 240 | answer = requests.post(url, params=params, proxies=self.proxies) 241 | self.result = answer.json() 242 | self.ok_update() 243 | return self.result 244 | 245 | def answer_callback_query(self, callback_query_id, text=None): 246 | url = self.tg_url_bot_general + self.key + "/answerCallbackQuery" 247 | if not text: 248 | params = {"callback_query_id": callback_query_id} 249 | else: 250 | params = {"callback_query_id": callback_query_id, "text": text} 251 | answer = requests.post(url, params=params, proxies=self.proxies) 252 | self.result = answer.json() 253 | self.ok_update() 254 | return self.result 255 | 256 | def ok_update(self): 257 | self.ok = self.result["ok"] 258 | if self.ok: 259 | self.error = None 260 | else: 261 | self.error = self.result["description"] 262 | print_message(self.error) 263 | return True 264 | 265 | 266 | def markdown_fix(message, offset, emoji=False): 267 | offset = int(offset) 268 | if emoji: # https://github.com/ableev/Zabbix-in-Telegram/issues/152 269 | offset -= 2 270 | message = "\n".join(message) 271 | message = message[:offset] + message[offset+1:] 272 | message = message.split("\n") 273 | return message 274 | 275 | 276 | class ZabbixWeb: 277 | def __init__(self, server, username, password): 278 | self.debug = False 279 | self.server = server 280 | self.username = username 281 | self.password = password 282 | self.proxies = {} 283 | self.verify = True 284 | self.cookie = None 285 | self.basic_auth_user = None 286 | self.basic_auth_pass = None 287 | self.tmp_dir = None 288 | 289 | def login(self): 290 | if not self.verify: 291 | requests.packages.urllib3.disable_warnings() 292 | 293 | data_api = {"name": self.username, "password": self.password, "enter": "Sign in"} 294 | answer = requests.post(self.server + "/", data=data_api, proxies=self.proxies, verify=self.verify, 295 | auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass)) 296 | cookie = answer.cookies 297 | if len(answer.history) > 1 and answer.history[0].status_code == 302: 298 | print_message("probably the server in your config file has not full URL (for example " 299 | "'{0}' instead of '{1}')".format(self.server, self.server + "/zabbix")) 300 | if not cookie: 301 | print_message("authorization has failed, url: {0}".format(self.server + "/")) 302 | cookie = None 303 | 304 | self.cookie = cookie 305 | 306 | def graph_get(self, itemid, period, title, width, height, version=3): 307 | file_img = "{0}/{1}.png".format(self.tmp_dir, 308 | "".join(random.choice(string.ascii_letters) for e in range(10))) 309 | 310 | title = requests.utils.quote(title) 311 | 312 | colors = { 313 | 0: "00CC00", 314 | 1: "CC0000", 315 | 2: "0000CC", 316 | 3: "CCCC00", 317 | 4: "00CCCC", 318 | 5: "CC00CC", 319 | } 320 | 321 | drawtype = 5 322 | if len(itemid) > 1: 323 | drawtype = 2 324 | 325 | zbx_img_url_itemids = [] 326 | for i in range(0, len(itemid)): 327 | itemid_url = "&items[{0}][itemid]={1}&items[{0}][sortorder]={0}&" \ 328 | "items[{0}][drawtype]={3}&items[{0}][color]={2}".format(i, itemid[i], colors[i], drawtype) 329 | zbx_img_url_itemids.append(itemid_url) 330 | 331 | zbx_img_url = self.server + "/chart3.php?" 332 | if version < 4: 333 | zbx_img_url += "period={0}".format(period) 334 | else: 335 | zbx_img_url += "from=now-{0}&to=now".format(period) 336 | zbx_img_url += "&name={0}&width={1}&height={2}&graphtype=0&legend=1".format(title, width, height) 337 | zbx_img_url += "".join(zbx_img_url_itemids) 338 | 339 | if self.debug: 340 | print_message(zbx_img_url) 341 | answer = requests.get(zbx_img_url, cookies=self.cookie, proxies=self.proxies, verify=self.verify, 342 | auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass)) 343 | status_code = answer.status_code 344 | if status_code == 404: 345 | print_message("can't get image from '{0}'".format(zbx_img_url)) 346 | return False 347 | res_img = answer.content 348 | file_bwrite(file_img, res_img) 349 | return file_img 350 | 351 | def api_test(self): 352 | headers = {'Content-type': 'application/json'} 353 | api_data = json.dumps({"jsonrpc": "2.0", "method": "user.login", "params": 354 | {"user": self.username, "password": self.password}, "id": 1}) 355 | api_url = self.server + "/api_jsonrpc.php" 356 | api = requests.post(api_url, data=api_data, proxies=self.proxies, headers=headers) 357 | return api.text 358 | 359 | 360 | def print_message(message): 361 | message = str(message) + "\n" 362 | filename = sys.argv[0].split("/")[-1] 363 | sys.stderr.write(filename + ": " + message) 364 | 365 | 366 | def list_cut(elements, symbols_limit): 367 | symbols_count = symbols_count_now = 0 368 | elements_new = [] 369 | element_last_list = [] 370 | for e in elements: 371 | symbols_count_now = symbols_count + len(e) 372 | if symbols_count_now > symbols_limit: 373 | limit_idx = symbols_limit - symbols_count 374 | e_list = list(e) 375 | for idx, ee in enumerate(e_list): 376 | if idx < limit_idx: 377 | element_last_list.append(ee) 378 | else: 379 | break 380 | break 381 | else: 382 | symbols_count = symbols_count_now + 1 383 | elements_new.append(e) 384 | if symbols_count_now < symbols_limit: 385 | return elements, False 386 | else: 387 | element_last = "".join(element_last_list) 388 | elements_new.append(element_last) 389 | return elements_new, True 390 | 391 | 392 | class Maps: 393 | # https://developers.google.com/maps/documentation/geocoding/intro 394 | def __init__(self): 395 | self.key = None 396 | self.proxies = {} 397 | 398 | def get_coordinates_by_address(self, address): 399 | coordinates = {"latitude": 0, "longitude": 0} 400 | url_api = "https://maps.googleapis.com/maps/api/geocode/json?key={0}&address={1}".format(self.key, address) 401 | url = url_api 402 | answer = requests.get(url, proxies=self.proxies) 403 | result = answer.json() 404 | try: 405 | coordinates_dict = result["results"][0]["geometry"]["location"] 406 | except: 407 | if "error_message" in result: 408 | print_message("[" + result["status"] + "]: " + result["error_message"]) 409 | return coordinates 410 | coordinates = {"latitude": coordinates_dict["lat"], "longitude": coordinates_dict["lng"]} 411 | return coordinates 412 | 413 | 414 | def file_write(filename, text): 415 | with open(filename, "w") as fd: 416 | fd.write(str(text)) 417 | return True 418 | 419 | 420 | def file_bwrite(filename, data): 421 | with open(filename, "wb") as fd: 422 | fd.write(data) 423 | return True 424 | 425 | 426 | def file_read(filename): 427 | with open(filename, "r") as fd: 428 | text = fd.readlines() 429 | return text 430 | 431 | 432 | def file_append(filename, text): 433 | with open(filename, "a") as fd: 434 | fd.write(str(text)) 435 | return True 436 | 437 | 438 | def external_image_get(url, tmp_dir, timeout=6): 439 | image_hash = hashlib.md5() 440 | image_hash.update(url.encode()) 441 | file_img = tmp_dir + "/external_{0}.png".format(image_hash.hexdigest()) 442 | try: 443 | answer = requests.get(url, timeout=timeout, allow_redirects=True) 444 | except requests.exceptions.ReadTimeout as ex: 445 | print_message("Can't get external image from '{0}': timeout".format(url)) 446 | return False 447 | status_code = answer.status_code 448 | if status_code == 404: 449 | print_message("Can't get external image from '{0}': HTTP 404 error".format(url)) 450 | return False 451 | answer_image = answer.content 452 | file_bwrite(file_img, answer_image) 453 | return file_img 454 | 455 | 456 | def age2sec(age_str): 457 | age_sec = 0 458 | age_regex = "([0-9]+d)?\s?([0-9]+h)?\s?([0-9]+m)?" 459 | age_pattern = re.compile(age_regex) 460 | intervals = age_pattern.match(age_str).groups() 461 | for i in intervals: 462 | if i: 463 | metric = i[-1] 464 | if metric == "d": 465 | age_sec += int(i[0:-1])*86400 466 | if metric == "h": 467 | age_sec += int(i[0:-1])*3600 468 | if metric == "m": 469 | age_sec += int(i[0:-1])*60 470 | return age_sec 471 | 472 | 473 | def main(): 474 | 475 | tmp_dir = zbxtg_settings.zbx_tg_tmp_dir 476 | if tmp_dir == "/tmp/" + zbxtg_settings.zbx_tg_prefix: 477 | print_message("WARNING: it is strongly recommended to change `zbx_tg_tmp_dir` variable in config!!!") 478 | print_message("https://github.com/ableev/Zabbix-in-Telegram/wiki/Change-zbx_tg_tmp_dir-in-settings") 479 | 480 | tmp_cookie = tmp_dir + "/cookie.py.txt" 481 | tmp_uids = tmp_dir + "/uids.txt" 482 | tmp_need_update = False # do we need to update cache file with uids or not 483 | 484 | rnd = random.randint(0, 999) 485 | ts = time.time() 486 | hash_ts = str(ts) + "." + str(rnd) 487 | 488 | log_file = "/dev/null" 489 | 490 | args = sys.argv 491 | 492 | settings = { 493 | "zbxtg_itemid": "0", # itemid for graph 494 | "zbxtg_title": None, # title for graph 495 | "zbxtg_image_period": None, 496 | "zbxtg_image_age": "3600", 497 | "zbxtg_image_width": "900", 498 | "zbxtg_image_height": "200", 499 | "tg_method_image": False, # if True - default send images, False - send text 500 | "tg_chat": False, # send message to chat or in private 501 | "tg_group": False, # send message to chat or in private 502 | "is_debug": False, 503 | "is_channel": False, 504 | "disable_web_page_preview": False, 505 | "location": None, # address 506 | "lat": 0, # latitude 507 | "lon": 0, # longitude 508 | "is_single_message": False, 509 | "markdown": False, 510 | "html": False, 511 | "signature": None, 512 | "signature_disable": False, 513 | "graph_buttons": False, 514 | "extimg": None, 515 | "to": None, 516 | "to_group": None, 517 | "forked": False, 518 | } 519 | 520 | url_github = "https://github.com/ableev/Zabbix-in-Telegram" 521 | url_wiki_base = "https://github.com/ableev/Zabbix-in-Telegram/wiki" 522 | url_tg_group = "https://t.me/ZbxTg" 523 | url_tg_channel = "https://t.me/Zabbix_in_Telegram" 524 | 525 | settings_description = { 526 | "itemid": {"name": "zbxtg_itemid", "type": "list", 527 | "help": "script will attach a graph with that itemid (could be multiple)", "url": "Graphs"}, 528 | "title": {"name": "zbxtg_title", "type": "str", "help": "title for attached graph", "url": "Graphs"}, 529 | "graphs_period": {"name": "zbxtg_image_period", "type": "int", "help": "graph period", "url": "Graphs"}, 530 | "graphs_age": {"name": "zbxtg_image_age", "type": "str", "help": "graph period as age", "url": "Graphs"}, 531 | "graphs_width": {"name": "zbxtg_image_width", "type": "int", "help": "graph width", "url": "Graphs"}, 532 | "graphs_height": {"name": "zbxtg_image_height", "type": "int", "help": "graph height", "url": "Graphs"}, 533 | "graphs": {"name": "tg_method_image", "type": "bool", "help": "enables graph sending", "url": "Graphs"}, 534 | "chat": {"name": "tg_chat", "type": "bool", "help": "deprecated, don't use it, see 'group'", 535 | "url": "How-to-send-message-to-the-group-chat"}, 536 | "group": {"name": "tg_group", "type": "bool", "help": "sends message to a group", 537 | "url": "How-to-send-message-to-the-group-chat"}, 538 | "debug": {"name": "is_debug", "type": "bool", "help": "enables 'debug'", 539 | "url": "How-to-test-script-in-command-line"}, 540 | "channel": {"name": "is_channel", "type": "bool", "help": "sends message to a channel", 541 | "url": "Channel-support"}, 542 | "disable_web_page_preview": {"name": "disable_web_page_preview", "type": "bool", 543 | "help": "disable web page preview", "url": "Disable-web-page-preview"}, 544 | "location": {"name": "location", "type": "str", "help": "address of location", "url": "Location"}, 545 | "lat": {"name": "lat", "type": "str", "help": "specify latitude (and lon too!)", "url": "Location"}, 546 | "lon": {"name": "lon", "type": "str", "help": "specify longitude (and lat too!)", "url": "Location"}, 547 | "single_message": {"name": "is_single_message", "type": "bool", "help": "do not split message and graph", 548 | "url": "Why-am-I-getting-two-messages-instead-of-one"}, 549 | "markdown": {"name": "markdown", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"}, 550 | "html": {"name": "html", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"}, 551 | "signature": {"name": "signature", "type": "str", 552 | "help": "bot's signature", "url": "Bot-signature"}, 553 | "signature_disable": {"name": "signature_disable", "type": "bool", 554 | "help": "enables/disables bot's signature", "url": "Bot-signature"}, 555 | "graph_buttons": {"name": "graph_buttons", "type": "bool", 556 | "help": "activates buttons under graph, could be using in ZbxTgDaemon", 557 | "url": "Interactive-bot"}, 558 | "external_image": {"name": "extimg", "type": "str", 559 | "help": "should be url; attaches external image from different source", 560 | "url": "External-image-as-graph"}, 561 | "to": {"name": "to", "type": "str", "help": "rewrite zabbix username, use that instead of arguments", 562 | "url": "Custom-to-and-to_group"}, 563 | "to_group": {"name": "to_group", "type": "str", 564 | "help": "rewrite zabbix username, use that instead of arguments", "url": "Custom-to-and-to_group"}, 565 | "forked": {"name": "forked", "type": "bool", "help": "internal variable, do not use it. Ever.", "url": ""}, 566 | } 567 | 568 | if len(args) < 4: 569 | do_not_exit = False 570 | if "--features" in args: 571 | print(("List of available settings, see {0}/Settings\n---".format(url_wiki_base))) 572 | for sett, proprt in list(settings_description.items()): 573 | print(("{0}: {1}\ndoc: {2}/{3}\n--".format(sett, proprt["help"], url_wiki_base, proprt["url"]))) 574 | 575 | elif "--show-settings" in args: 576 | do_not_exit = True 577 | print_message("Settings: " + str(json.dumps(settings, indent=2))) 578 | 579 | else: 580 | print(("Hi. You should provide at least three arguments.\n" 581 | "zbxtg.py [TO] [SUBJECT] [BODY]\n\n" 582 | "1. Read main page and/or wiki: {0} + {1}\n" 583 | "2. Public Telegram group (discussion): {2}\n" 584 | "3. Public Telegram channel: {3}\n" 585 | "4. Try dev branch for test purposes (new features, etc): {0}/tree/dev" 586 | .format(url_github, url_wiki_base, url_tg_group, url_tg_channel))) 587 | if not do_not_exit: 588 | sys.exit(0) 589 | 590 | 591 | zbx_to = args[1] 592 | zbx_subject = args[2] 593 | zbx_body = args[3] 594 | 595 | tg = TelegramAPI(key=zbxtg_settings.tg_key) 596 | 597 | tg.tmp_dir = tmp_dir 598 | tg.tmp_uids = tmp_uids 599 | 600 | if zbxtg_settings.proxy_to_tg: 601 | proxy_to_tg = zbxtg_settings.proxy_to_tg 602 | if not proxy_to_tg.find("http") and not proxy_to_tg.find("socks"): 603 | proxy_to_tg = "https://" + proxy_to_tg 604 | tg.proxies = { 605 | "https": "{0}".format(proxy_to_tg), 606 | } 607 | 608 | zbx = ZabbixWeb(server=zbxtg_settings.zbx_server, username=zbxtg_settings.zbx_api_user, 609 | password=zbxtg_settings.zbx_api_pass) 610 | 611 | zbx.tmp_dir = tmp_dir 612 | 613 | # workaround for Zabbix 4.x 614 | zbx_version = 3 615 | 616 | try: 617 | zbx_version = zbxtg_settings.zbx_server_version 618 | except: 619 | pass 620 | 621 | if zbxtg_settings.proxy_to_zbx: 622 | zbx.proxies = { 623 | "http": "http://{0}/".format(zbxtg_settings.proxy_to_zbx), 624 | "https": "https://{0}/".format(zbxtg_settings.proxy_to_zbx) 625 | } 626 | 627 | # https://github.com/ableev/Zabbix-in-Telegram/issues/55 628 | try: 629 | if zbxtg_settings.zbx_basic_auth: 630 | zbx.basic_auth_user = zbxtg_settings.zbx_basic_auth_user 631 | zbx.basic_auth_pass = zbxtg_settings.zbx_basic_auth_pass 632 | except: 633 | pass 634 | 635 | try: 636 | zbx_api_verify = zbxtg_settings.zbx_api_verify 637 | zbx.verify = zbx_api_verify 638 | except: 639 | pass 640 | 641 | map = Maps() 642 | # api key to resolve address to coordinates via google api 643 | try: 644 | if zbxtg_settings.google_maps_api_key: 645 | map.key = zbxtg_settings.google_maps_api_key 646 | if zbxtg_settings.proxy_to_tg: 647 | map.proxies = { 648 | "http": "http://{0}/".format(zbxtg_settings.proxy_to_tg), 649 | "https": "https://{0}/".format(zbxtg_settings.proxy_to_tg) 650 | } 651 | except: 652 | pass 653 | 654 | zbxtg_body = (zbx_subject + "\n" + zbx_body).splitlines() 655 | zbxtg_body_text = [] 656 | 657 | for line in zbxtg_body: 658 | if line.find(zbxtg_settings.zbx_tg_prefix) > -1: 659 | setting = re.split("[\s:=]+", line, maxsplit=1) 660 | key = setting[0].replace(zbxtg_settings.zbx_tg_prefix + ";", "") 661 | if key not in settings_description: 662 | if "--debug" in args: 663 | print_message("[ERROR] There is no '{0}' method, use --features to get help".format(key)) 664 | continue 665 | if settings_description[key]["type"] == "list": 666 | value = setting[1].split(",") 667 | elif len(setting) > 1 and len(setting[1]) > 0: 668 | value = setting[1] 669 | elif settings_description[key]["type"] == "bool": 670 | value = True 671 | else: 672 | value = settings[settings_description[key]["name"]] 673 | if key in settings_description: 674 | settings[settings_description[key]["name"]] = value 675 | else: 676 | zbxtg_body_text.append(line) 677 | 678 | tg_method_image = bool(settings["tg_method_image"]) 679 | tg_chat = bool(settings["tg_chat"]) 680 | tg_group = bool(settings["tg_group"]) 681 | is_debug = bool(settings["is_debug"]) 682 | is_channel = bool(settings["is_channel"]) 683 | disable_web_page_preview = bool(settings["disable_web_page_preview"]) 684 | is_single_message = bool(settings["is_single_message"]) 685 | 686 | # experimental way to send message to the group https://github.com/ableev/Zabbix-in-Telegram/issues/15 687 | if args[0].split("/")[-1] == "zbxtg_group.py" or "--group" in args or tg_chat or tg_group: 688 | tg_chat = True 689 | tg_group = True 690 | tg.type = "group" 691 | 692 | if "--debug" in args or is_debug: 693 | is_debug = True 694 | tg.debug = True 695 | zbx.debug = True 696 | print_message(tg.get_me()) 697 | print_message("Cache file with uids: " + tg.tmp_uids) 698 | log_file = tmp_dir + ".debug." + hash_ts + ".log" 699 | #print_message(log_file) 700 | 701 | if "--markdown" in args or settings["markdown"]: 702 | tg.markdown = True 703 | 704 | if "--html" in args or settings["html"]: 705 | tg.html = True 706 | 707 | if "--channel" in args or is_channel: 708 | tg.type = "channel" 709 | 710 | if "--disable_web_page_preview" in args or disable_web_page_preview: 711 | if is_debug: 712 | print_message("'disable_web_page_preview' option has been enabled") 713 | tg.disable_web_page_preview = True 714 | 715 | if "--graph_buttons" in args or settings["graph_buttons"]: 716 | tg.image_buttons = True 717 | 718 | if "--forked" in args: 719 | settings["forked"] = True 720 | 721 | if "--tg-key" in args: 722 | tg.key = args[args.index("--tg-key") + 1] 723 | 724 | location_coordinates = {"latitude": None, "longitude": None} 725 | if settings["lat"] > 0 and settings["lat"] > 0: 726 | location_coordinates = {"latitude": settings["lat"], "longitude": settings["lon"]} 727 | tg.location = location_coordinates 728 | else: 729 | if settings["location"]: 730 | location_coordinates = map.get_coordinates_by_address(settings["location"]) 731 | if location_coordinates: 732 | settings["lat"] = location_coordinates["latitude"] 733 | settings["lon"] = location_coordinates["longitude"] 734 | tg.location = location_coordinates 735 | 736 | if not os.path.isdir(tmp_dir): 737 | if is_debug: 738 | print_message("Tmp dir doesn't exist, creating new one...") 739 | try: 740 | os.makedirs(tmp_dir) 741 | open(tg.tmp_uids, "a").close() 742 | os.chmod(tmp_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) 743 | os.chmod(tg.tmp_uids, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) 744 | except: 745 | tmp_dir = "/tmp" 746 | if is_debug: 747 | print_message("Using {0} as a temporary dir".format(tmp_dir)) 748 | 749 | done_all_work_in_the_fork = False 750 | # issue75 751 | 752 | to_types = ["to", "to_group", "to_channel"] 753 | to_types_to_telegram = {"to": "private", "to_group": "group", "to_channel": "channel"} 754 | multiple_to = {} 755 | for i in to_types: 756 | multiple_to[i]=[] 757 | 758 | for t in to_types: 759 | try: 760 | if settings[t] and not settings["forked"]: 761 | # zbx_to = settings["to"] 762 | multiple_to[t] = re.split(",", settings[t]) 763 | except KeyError: 764 | pass 765 | 766 | # example: 767 | # {'to_channel': [], 'to': ['usr1', 'usr2', 'usr3'], 'to_group': []} 768 | 769 | if (sum([len(v) for k, v in list(multiple_to.items())])) == 1: 770 | # if we have only one recipient, we don't need fork to send message, just re-write "to" vaiable 771 | tmp_max = 0 772 | for t in to_types: 773 | if len(multiple_to[t]) > tmp_max: 774 | tmp_max = len(multiple_to[t]) 775 | tg.type = to_types_to_telegram[t] 776 | zbx_to = multiple_to[t][0] 777 | else: 778 | for t in to_types: 779 | for i in multiple_to[t]: 780 | args_new = list(args) 781 | args_new[1] = i 782 | if t == "to_group": 783 | args_new.append("--group") 784 | args_new.append("--forked") 785 | args_new.insert(0, sys.executable) 786 | if is_debug: 787 | print_message("Fork for custom recipient ({1}), new args: {0}".format(args_new, 788 | to_types_to_telegram[t])) 789 | subprocess.call(args_new) 790 | done_all_work_in_the_fork = True 791 | 792 | if done_all_work_in_the_fork: 793 | sys.exit(0) 794 | 795 | uid = None 796 | 797 | if tg.type == "channel": 798 | uid = zbx_to 799 | if tg.type == "private": 800 | zbx_to = zbx_to.replace("@", "") 801 | 802 | if zbx_to.isdigit(): 803 | uid = zbx_to 804 | 805 | if not uid: 806 | uid = tg.get_uid_from_cache(zbx_to) 807 | 808 | if not uid: 809 | uid = tg.get_uid(zbx_to) 810 | if uid: 811 | tmp_need_update = True 812 | if not uid: 813 | tg.error_need_to_contact(zbx_to) 814 | sys.exit(1) 815 | 816 | if tmp_need_update: 817 | tg.update_cache_uid(zbx_to, str(uid).rstrip()) 818 | 819 | if is_debug: 820 | print_message("Telegram uid of {0} '{1}': {2}".format(tg.type, zbx_to, uid)) 821 | 822 | # add signature, turned off by default, you can turn it on in config 823 | try: 824 | if "--signature" in args or settings["signature"] or zbxtg_settings.zbx_tg_signature\ 825 | and not "--signature_disable" in args and not settings["signature_disable"]: 826 | if "--signature" in args: 827 | settings["signature"] = args[args.index("--signature") + 1] 828 | if not settings["signature"]: 829 | settings["signature"] = zbxtg_settings.zbx_server 830 | zbxtg_body_text.append("--") 831 | zbxtg_body_text.append(settings["signature"]) 832 | except: 833 | pass 834 | 835 | # replace text with emojis 836 | internal_using_emoji = False # I hate that, but... https://github.com/ableev/Zabbix-in-Telegram/issues/152 837 | if hasattr(zbxtg_settings, "emoji_map"): 838 | zbxtg_body_text_emoji_support = [] 839 | for l in zbxtg_body_text: 840 | l_new = l 841 | for k, v in list(zbxtg_settings.emoji_map.items()): 842 | l_new = l_new.replace("{{" + k + "}}", v) 843 | zbxtg_body_text_emoji_support.append(l_new) 844 | if len("".join(zbxtg_body_text)) - len("".join(zbxtg_body_text_emoji_support)): 845 | internal_using_emoji = True 846 | zbxtg_body_text = zbxtg_body_text_emoji_support 847 | 848 | if not is_single_message: 849 | tg.send_message(uid, zbxtg_body_text) 850 | if not tg.ok: 851 | # first case – if group has been migrated to a supergroup, we need to update chat_id of that group 852 | if tg.error.find("migrated") > -1 and tg.error.find("supergroup") > -1: 853 | migrate_to_chat_id = tg.result["parameters"]["migrate_to_chat_id"] 854 | tg.update_cache_uid(zbx_to, migrate_to_chat_id, message="Group chat is migrated to supergroup, " 855 | "updating cache file") 856 | uid = migrate_to_chat_id 857 | tg.send_message(uid, zbxtg_body_text) 858 | 859 | # another case if markdown is enabled and we got parse error, try to remove "bad" symbols from message 860 | if tg.markdown and tg.error.find("Can't find end of the entity starting at byte offset") > -1: 861 | markdown_warning = "Original message has been fixed due to {0}. " \ 862 | "Please, fix the markdown, it's slowing down messages sending."\ 863 | .format(url_wiki_base + "/" + settings_description["markdown"]["url"]) 864 | markdown_fix_attempts = 0 865 | while not tg.ok and markdown_fix_attempts != 3: 866 | offset = re.search("Can't find end of the entity starting at byte offset ([0-9]+)", tg.error).group(1) 867 | zbxtg_body_text = markdown_fix(zbxtg_body_text, offset, emoji=internal_using_emoji) + \ 868 | ["\n"] + [markdown_warning] 869 | tg.disable_web_page_preview = True 870 | tg.send_message(uid, zbxtg_body_text) 871 | markdown_fix_attempts += 1 872 | if tg.ok: 873 | print_message(markdown_warning) 874 | 875 | if is_debug: 876 | print((tg.result)) 877 | 878 | if settings["zbxtg_image_age"]: 879 | age_sec = age2sec(settings["zbxtg_image_age"]) 880 | if age_sec > 0 and age_sec > 3600: 881 | settings["zbxtg_image_period"] = age_sec 882 | 883 | message_id = 0 884 | if tg_method_image: 885 | zbx.login() 886 | if not zbx.cookie: 887 | text_warn = "Login to Zabbix web UI has failed (web url, user or password are incorrect), "\ 888 | "unable to send graphs check manually" 889 | tg.send_message(uid, [text_warn]) 890 | print_message(text_warn) 891 | else: 892 | if not settings["extimg"]: 893 | zbxtg_file_img = zbx.graph_get(settings["zbxtg_itemid"], settings["zbxtg_image_period"], 894 | settings["zbxtg_title"], settings["zbxtg_image_width"], 895 | settings["zbxtg_image_height"], version=zbx_version) 896 | else: 897 | zbxtg_file_img = external_image_get(settings["extimg"], tmp_dir=zbx.tmp_dir) 898 | zbxtg_body_text, is_modified = list_cut(zbxtg_body_text, 200) 899 | if tg.ok: 900 | message_id = tg.result["result"]["message_id"] 901 | tg.reply_to_message_id = message_id 902 | if not zbxtg_file_img: 903 | text_warn = "Can't get graph image, check script manually, see logs, or disable graphs" 904 | tg.send_message(uid, [text_warn]) 905 | print_message(text_warn) 906 | else: 907 | if not is_single_message: 908 | zbxtg_body_text = "" 909 | else: 910 | if is_modified: 911 | text_warn = "probably you will see MEDIA_CAPTION_TOO_LONG error, "\ 912 | "the message has been cut to 200 symbols, "\ 913 | "https://github.com/ableev/Zabbix-in-Telegram/issues/9"\ 914 | "#issuecomment-166895044" 915 | print_message(text_warn) 916 | if not is_single_message: 917 | tg.disable_notification = True 918 | tg.send_photo(uid, zbxtg_body_text, zbxtg_file_img) 919 | if tg.ok: 920 | settings["zbxtg_body_text"] = zbxtg_body_text 921 | os.remove(zbxtg_file_img) 922 | else: 923 | if tg.error.find("PHOTO_INVALID_DIMENSIONS") > -1: 924 | if not tg.disable_web_page_preview: 925 | tg.disable_web_page_preview = True 926 | text_warn = "Zabbix user couldn't get graph (probably has no rights to get data from host), " \ 927 | "check script manually, see {0}".format(url_wiki_base + "/" + 928 | settings_description["graphs"]["url"]) 929 | tg.send_message(uid, [text_warn]) 930 | print_message(text_warn) 931 | if tg.location and location_coordinates["latitude"] and location_coordinates["longitude"]: 932 | tg.reply_to_message_id = message_id 933 | tg.disable_notification = True 934 | tg.send_location(to=uid, coordinates=location_coordinates) 935 | 936 | if "--show-settings" in args: 937 | print_message("Settings: " + str(json.dumps(settings, indent=2))) 938 | 939 | if __name__ == "__main__": 940 | main() 941 | --------------------------------------------------------------------------------