├── 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 | 
101 | 
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 |
--------------------------------------------------------------------------------