/[smecontribs]/rpms/smeserver-zabbix-server/contribs10/smeserver-zabbix-server-0.1-bz10802-checkcert-telegram.patch
ViewVC logotype

Annotation of /rpms/smeserver-zabbix-server/contribs10/smeserver-zabbix-server-0.1-bz10802-checkcert-telegram.patch

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.1 - (hide annotations) (download)
Tue Nov 9 04:28:17 2021 UTC (2 years, 6 months ago) by jpp
Branch: MAIN
CVS Tags: smeserver-zabbix-server-0_1-31_el7_sme, smeserver-zabbix-server-0_1-29_el7_sme, smeserver-zabbix-server-0_1-30_el7_sme, smeserver-zabbix-server-0_1-28_el7_sme, HEAD
* Mon Nov 08 2021 Jean-Philippe Pialasse <tests@pialasse.com> 0.1-28.sme
- add check cert scripts and telegram [SME: 10802]
  no template provided to use with

1 jpp 1.1 diff -Nur --no-dereference smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/cert_expire.pl smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/cert_expire.pl
2     --- smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/cert_expire.pl 1969-12-31 19:00:00.000000000 -0500
3     +++ smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/cert_expire.pl 2021-11-08 22:00:50.335000000 -0500
4     @@ -0,0 +1,139 @@
5     +#!/usr/bin/perl -w
6     +# Check peer certificate validity for Zabbix
7     +# Require perl module : IO::Socket, Net::SSLeay, Date::Parse
8     +# Require unix programs : openssl, echo, sendmail
9     +#
10     +# Based on sslexpire from Emmanuel Lacour <elacour@home-dn.net>
11     +#
12     +# This file is free software; you can redistribute it and/or modify it
13     +# under the terms of the GNU General Public License as published by the
14     +# Free Software Foundation; either version 2, or (at your option) any
15     +# later version.
16     +#
17     +# This file is distributed in the hope that it will be
18     +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty
19     +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20     +# General Public License for more details.
21     +#
22     +# You should have received a copy of the GNU General Public License
23     +# along with this file; see the file COPYING. If not, write to the Free
24     +# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
25     +# 02110-1301, USA.
26     +#
27     +
28     +
29     +use strict;
30     +use IO::Socket;
31     +use Net::SSLeay;
32     +use Getopt::Long;
33     +use Date::Parse;
34     +
35     +Net::SSLeay::SSLeay_add_ssl_algorithms();
36     +Net::SSLeay::randomize();
37     +
38     +# Default values
39     +my $opensslpath = "/usr/bin/openssl";
40     +my $host = '127.0.0.1';
41     +my $port = '443';
42     +
43     +my %opts;
44     +GetOptions (\%opts,
45     + 'host|h=s',
46     + 'port|p=s',
47     + 'help',
48     +);
49     +
50     +if ($opts{'host'}) {
51     + $host = $opts{'host'};
52     +}
53     +if ($opts{'port'}){
54     + $port = $opts{'port'};
55     +}
56     +
57     +if ($opts{'help'}) {
58     + &usage;
59     +}
60     +
61     +# Print program usage
62     +sub usage {
63     + print "Usage: sslexpire [OPTION]...
64     +-h, --host=HOST check this host
65     +-p, --port=TCPPORT check this port on the previous host
66     + --help print this help, then exit
67     +";
68     + exit;
69     +}
70     +
71     +# This will return the expiration date
72     +sub getExpire {
73     +
74     + my ($l_host,$l_port) = @_;
75     + my ($l_expdate,$l_comment);
76     +
77     + # Connect to $l_host:$l_port
78     + my $socket = IO::Socket::INET->new(
79     + Proto => "tcp",
80     + PeerAddr => $l_host,
81     + PeerPort => $l_port
82     + );
83     + # If we connected successfully
84     + if ($socket) {
85     + # Intiate ssl
86     + my $l_ctx = Net::SSLeay::CTX_new();
87     + my $l_ssl = Net::SSLeay::new($l_ctx);
88     +
89     + Net::SSLeay::set_fd($l_ssl, fileno($socket));
90     + my $res = Net::SSLeay::connect($l_ssl);
91     +
92     + # Get peer certificate
93     + my $l_x509 = Net::SSLeay::get_peer_certificate($l_ssl);
94     + if ($l_x509) {
95     + my $l_string = Net::SSLeay::PEM_get_string_X509($l_x509);
96     + # Get the expiration date, using openssl
97     + $l_expdate = `echo "$l_string" | $opensslpath x509 -enddate -noout 2>&1`;
98     + $l_expdate =~ s/.*=//;
99     + chomp($l_expdate);
100     + }
101     + else {
102     + $l_expdate = 1;
103     + }
104     +
105     + # Close and cleanup
106     + Net::SSLeay::free($l_ssl);
107     + Net::SSLeay::CTX_free($l_ctx);
108     + close $socket;
109     + }
110     + else {
111     + $l_expdate = 1;
112     + }
113     + return $l_expdate;
114     +}
115     +
116     +
117     +# Print remaining days before expiration
118     +sub report {
119     + # Convert date into epoch using date command
120     + my ($l_expdate) = @_;
121     +
122     + if ($l_expdate ne "1") {
123     + # The current date
124     + my $l_today = time;
125     + my $l_epochdate = str2time($l_expdate);
126     +
127     + # Calculate diff between expiration date and today
128     + my $l_diff = ($l_epochdate - $l_today)/(3600*24);
129     +
130     + # Report if needed
131     + printf "%.0f\n", $l_diff;
132     + }
133     + else {
134     + print "Unable to read certificate!\n";
135     + exit (1);
136     + }
137     +}
138     +
139     +# Get expiration date
140     +my $expdate = getExpire($host,$port);
141     +
142     +# Report
143     +report("$expdate");
144     diff -Nur --no-dereference smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/check_cert.pl smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/check_cert.pl
145     --- smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/check_cert.pl 1969-12-31 19:00:00.000000000 -0500
146     +++ smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/check_cert.pl 2021-11-08 22:00:50.719000000 -0500
147     @@ -0,0 +1,109 @@
148     +#!/usr/bin/perl
149     +
150     +=head1 SYNOPSIS
151     +
152     + check_ssl_certificate.pl
153     + --url,-u URL
154     + --sni,-s HOSTNAME SNI servername (SSL vhost) that will be requested during SSL handshake.
155     + This tells the server which certificate to return.
156     + Default to the host passed with --url
157     +
158     +=cut
159     +
160     +use strict;
161     +use warnings;
162     +use IO::Socket::SSL;
163     +use LWP::UserAgent;
164     +use URI::URL;
165     +use DateTime::Format::ISO8601;
166     +use Getopt::Long qw/:config auto_help/;
167     +use Pod::Usage;
168     +use JSON qw(to_json);
169     +
170     +use constant TIMEOUT => 10;
171     +
172     +my ($url, $sni, $status, @san);
173     +
174     +sub ssl_opts {
175     + my ($sni, $expiration_date_ref, $status_ref, $san_ref) = @_;
176     + return (
177     + 'verify_hostname' => 0,
178     + 'SSL_ca_file' => '/etc/pki/tls/certs/ca-bundle.crt',
179     + 'SSL_hostname' => $sni,
180     + 'SSL_verifycn_name' => $sni,
181     + 'SSL_verify_scheme' => 'http',
182     + 'SSL_verify_callback' => sub {
183     + my (undef, $ctx_store) = @_;
184     + # Get the error message from openssl verification
185     + $$status_ref = Net::SSLeay::X509_verify_cert_error_string(Net::SSLeay::X509_STORE_CTX_get_error($ctx_store));
186     + # Get the raw cert, to extract the expiration
187     + my $cert = Net::SSLeay::X509_STORE_CTX_get_current_cert($ctx_store);
188     + $$expiration_date_ref = Net::SSLeay::P_ASN1_TIME_get_isotime(Net::SSLeay::X509_get_notAfter($cert));
189     + # Get Alt names so we can check later if the hostname match
190     + @$san_ref = Net::SSLeay::X509_get_subjectAltNames($cert);
191     + # Keep only odd elements. Even ones contains subject types which we're not interested in
192     + @$san_ref = @$san_ref[grep $_ % 2, 0..scalar(@$san_ref)];
193     + # Always return success
194     + return 1;
195     + }
196     + )
197     +}
198     +
199     +sub https_get {
200     + my ($url, $sni, $expiration_date_ref, $status_ref, $san_ref) = @_;
201     +
202     + my $ua = LWP::UserAgent->new();
203     + $ua->timeout(TIMEOUT);
204     + $ua->ssl_opts( ssl_opts($sni, $expiration_date_ref, $status_ref, $san_ref) );
205     + my $request = HTTP::Request->new('GET', $url);
206     + $request->header(Host => $sni);
207     + my $response = $ua->simple_request($request);
208     + return $response;
209     +}
210     +
211     +sub wildcard_match {
212     + my ($cn, $host) = @_;
213     + my $match = 0;
214     + return 0 if $cn !~ m/^\*\.(.*)$/;
215     + my $cn_dom = $1;
216     + my $host_dom = ($sni =~ m/^[^\.]+\.(.*)$/)[0];
217     + return ($cn_dom eq $host_dom);
218     +}
219     +
220     +GetOptions ("url|u=s" => \$url,
221     + "sni|s=s" => \$sni) or pod2usage(1);
222     +if (@ARGV) {
223     + print "This script takes no arguments...\n";
224     + pod2usage(1);
225     +}
226     +pod2usage(1) if (!$url);
227     +
228     +my $expiration_date;
229     +my $uri = URI->new($url);
230     +die "Only https urls are supported\n" unless $uri->scheme eq 'https';
231     +$sni ||= $uri->host;
232     +my $response = https_get($url, $sni, \$expiration_date, \$status, \@san);
233     +
234     +my $out = {
235     + code => $response->code,
236     + status => $response->message,
237     + days_left => undef,
238     + cert_cn => undef,
239     + issuer => undef
240     +};
241     +
242     +if ($response->code != 500) { # Even a 404 is good enough, as far as cert validation goes...
243     + my $now = DateTime->now;
244     + $expiration_date = DateTime::Format::ISO8601->parse_datetime( $expiration_date );
245     +
246     + $out->{issuer} = $response->headers->{'client-ssl-cert-issuer'};
247     + $out->{cert_cn} = ($response->headers->{'client-ssl-cert-subject'} =~ m/CN=(.*)$/)[0];
248     + $status = "no common name" if !$out->{cert_cn};
249     + $out->{status} = ($status eq 'ok' and !grep { $sni eq $_ } @san and !wildcard_match($out->{cert_cn},$sni)) ?
250     + $out->{status} = "hostname mismatch ($sni doesn't match any of " . join(" ", @san) . ")" :
251     + $status;
252     + $out->{days_left} = ($expiration_date < $now) ? -1 * $expiration_date->delta_days($now)->delta_days :
253     + $expiration_date->delta_days($now)->delta_days
254     +}
255     +
256     +print to_json($out, { pretty => 1 });
257     diff -Nur --no-dereference smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg_group.py smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg_group.py
258     --- smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg_group.py 1969-12-31 19:00:00.000000000 -0500
259     +++ smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg_group.py 2021-11-08 22:00:51.153000000 -0500
260     @@ -0,0 +1,939 @@
261     +#!/usr/bin/env python
262     +# coding: utf-8
263     +
264     +import sys
265     +import os
266     +import time
267     +import random
268     +import string
269     +import requests
270     +import json
271     +import re
272     +import stat
273     +import hashlib
274     +import subprocess
275     +#import sqlite3
276     +from os.path import dirname
277     +import zbxtg_settings
278     +
279     +
280     +class Cache:
281     + def __init__(self, database):
282     + self.database = database
283     +
284     + def create_db(self, database):
285     + pass
286     +
287     +
288     +class TelegramAPI:
289     + tg_url_bot_general = "https://api.telegram.org/bot"
290     +
291     + def http_get(self, url):
292     + answer = requests.get(url, proxies=self.proxies)
293     + self.result = answer.json()
294     + self.ok_update()
295     + return self.result
296     +
297     + def __init__(self, key):
298     + self.debug = False
299     + self.key = key
300     + self.proxies = {}
301     + self.type = "private" # 'private' for private chats or 'group' for group chats
302     + self.markdown = False
303     + self.html = False
304     + self.disable_web_page_preview = False
305     + self.disable_notification = False
306     + self.reply_to_message_id = 0
307     + self.tmp_dir = None
308     + self.tmp_uids = None
309     + self.location = {"latitude": None, "longitude": None}
310     + self.update_offset = 0
311     + self.image_buttons = False
312     + self.result = None
313     + self.ok = None
314     + self.error = None
315     + self.get_updates_from_file = False
316     +
317     + def get_me(self):
318     + url = self.tg_url_bot_general + self.key + "/getMe"
319     + me = self.http_get(url)
320     + return me
321     +
322     + def get_updates(self):
323     + url = self.tg_url_bot_general + self.key + "/getUpdates"
324     + params = {"offset": self.update_offset}
325     + if self.debug:
326     + print_message(url)
327     + answer = requests.post(url, params=params, proxies=self.proxies)
328     + self.result = answer.json()
329     + if self.get_updates_from_file:
330     + print_message("Getting updated from file getUpdates.txt")
331     + self.result = json.loads("".join(file_read("getUpdates.txt")))
332     + if self.debug:
333     + print_message("Content of /getUpdates:")
334     + print_message(json.dumps(self.result))
335     + self.ok_update()
336     + return self.result
337     +
338     + def send_message(self, to, message):
339     + url = self.tg_url_bot_general + self.key + "/sendMessage"
340     + message = "\n".join(message)
341     + params = {"chat_id": to, "text": message, "disable_web_page_preview": self.disable_web_page_preview,
342     + "disable_notification": self.disable_notification}
343     + if self.reply_to_message_id:
344     + params["reply_to_message_id"] = self.reply_to_message_id
345     + if self.markdown or self.html:
346     + parse_mode = "HTML"
347     + if self.markdown:
348     + parse_mode = "Markdown"
349     + params["parse_mode"] = parse_mode
350     + if self.debug:
351     + print_message("Trying to /sendMessage:")
352     + print_message(url)
353     + print_message("post params: " + str(params))
354     + answer = requests.post(url, params=params, proxies=self.proxies)
355     + if answer.status_code == 414:
356     + self.result = {"ok": False, "description": "414 URI Too Long"}
357     + else:
358     + self.result = answer.json()
359     + self.ok_update()
360     + return self.result
361     +
362     + def update_message(self, to, message_id, message):
363     + url = self.tg_url_bot_general + self.key + "/editMessageText"
364     + message = "\n".join(message)
365     + params = {"chat_id": to, "message_id": message_id, "text": message,
366     + "disable_web_page_preview": self.disable_web_page_preview,
367     + "disable_notification": self.disable_notification}
368     + if self.markdown or self.html:
369     + parse_mode = "HTML"
370     + if self.markdown:
371     + parse_mode = "Markdown"
372     + params["parse_mode"] = parse_mode
373     + if self.debug:
374     + print_message("Trying to /editMessageText:")
375     + print_message(url)
376     + print_message("post params: " + str(params))
377     + answer = requests.post(url, params=params, proxies=self.proxies)
378     + self.result = answer.json()
379     + self.ok_update()
380     + return self.result
381     +
382     + def send_photo(self, to, message, path):
383     + url = self.tg_url_bot_general + self.key + "/sendPhoto"
384     + message = "\n".join(message)
385     + if self.image_buttons:
386     + reply_markup = json.dumps({"inline_keyboard": [[
387     + {"text": "R", "callback_data": "graph_refresh"},
388     + {"text": "1h", "callback_data": "graph_period_3600"},
389     + {"text": "3h", "callback_data": "graph_period_10800"},
390     + {"text": "6h", "callback_data": "graph_period_21600"},
391     + {"text": "12h", "callback_data": "graph_period_43200"},
392     + {"text": "24h", "callback_data": "graph_period_86400"},
393     + ], ]})
394     + else:
395     + reply_markup = json.dumps({})
396     + params = {"chat_id": to, "caption": message, "disable_notification": self.disable_notification,
397     + "reply_markup": reply_markup}
398     + if self.reply_to_message_id:
399     + params["reply_to_message_id"] = self.reply_to_message_id
400     + files = {"photo": open(path, 'rb')}
401     + if self.debug:
402     + print_message("Trying to /sendPhoto:")
403     + print_message(url)
404     + print_message(params)
405     + print_message("files: " + str(files))
406     + answer = requests.post(url, params=params, files=files, proxies=self.proxies)
407     + self.result = answer.json()
408     + self.ok_update()
409     + return self.result
410     +
411     + def send_txt(self, to, text, text_name=None):
412     + path = self.tmp_dir + "/" + "zbxtg_txt_"
413     + url = self.tg_url_bot_general + self.key + "/sendDocument"
414     + text = "\n".join(text)
415     + if not text_name:
416     + path += "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
417     + else:
418     + path += text_name
419     + path += ".txt"
420     + file_write(path, text)
421     + params = {"chat_id": to, "caption": path.split("/")[-1], "disable_notification": self.disable_notification}
422     + if self.reply_to_message_id:
423     + params["reply_to_message_id"] = self.reply_to_message_id
424     + files = {"document": open(path, 'rb')}
425     + if self.debug:
426     + print_message("Trying to /sendDocument:")
427     + print_message(url)
428     + print_message(params)
429     + print_message("files: " + str(files))
430     + answer = requests.post(url, params=params, files=files, proxies=self.proxies)
431     + self.result = answer.json()
432     + self.ok_update()
433     + return self.result
434     +
435     + def get_uid(self, name):
436     + uid = 0
437     + if self.debug:
438     + print_message("Getting uid from /getUpdates...")
439     + updates = self.get_updates()
440     + for m in updates["result"]:
441     + if "message" in m:
442     + chat = m["message"]["chat"]
443     + elif "edited_message" in m:
444     + chat = m["edited_message"]["chat"]
445     + else:
446     + continue
447     + if chat["type"] == self.type == "private":
448     + if "username" in chat:
449     + if chat["username"] == name:
450     + uid = chat["id"]
451     + if (chat["type"] == "group" or chat["type"] == "supergroup") and self.type == "group":
452     + if "title" in chat:
453     + if sys.version_info[0] < 3:
454     + if chat["title"] == name.decode("utf-8"):
455     + uid = chat["id"]
456     + else:
457     + if chat["title"] == name:
458     + uid = chat["id"]
459     + return uid
460     +
461     + def error_need_to_contact(self, to):
462     + if self.type == "private":
463     + print_message("User '{0}' needs to send some text bot in private".format(to))
464     + if self.type == "group":
465     + print_message("You need start a conversation with your bot first in '{0}' group chat, type '/start@{1}'"
466     + .format(to, self.get_me()["result"]["username"]))
467     +
468     + def update_cache_uid(self, name, uid, message="Add new string to cache file"):
469     + cache_string = "{0};{1};{2}\n".format(name, self.type, str(uid).rstrip())
470     + # FIXME
471     + if self.debug:
472     + print_message("{0}: {1}".format(message, cache_string))
473     + with open(self.tmp_uids, "a") as cache_file_uids:
474     + cache_file_uids.write(cache_string)
475     + return True
476     +
477     + def get_uid_from_cache(self, name):
478     + if self.debug:
479     + print_message("Trying to read cached uid for {0}, {1}, from {2}".format(name, self.type, self.tmp_uids))
480     + uid = 0
481     + if os.path.isfile(self.tmp_uids):
482     + with open(self.tmp_uids, 'r') as cache_file_uids:
483     + cache_uids_old = cache_file_uids.readlines()
484     + for u in cache_uids_old:
485     + u_splitted = u.split(";")
486     + if name == u_splitted[0] and self.type == u_splitted[1]:
487     + uid = u_splitted[2]
488     + return uid
489     +
490     + def send_location(self, to, coordinates):
491     + url = self.tg_url_bot_general + self.key + "/sendLocation"
492     + params = {"chat_id": to, "disable_notification": self.disable_notification,
493     + "latitude": coordinates["latitude"], "longitude": coordinates["longitude"]}
494     + if self.reply_to_message_id:
495     + params["reply_to_message_id"] = self.reply_to_message_id
496     + if self.debug:
497     + print_message("Trying to /sendLocation:")
498     + print_message(url)
499     + print_message("post params: " + str(params))
500     + answer = requests.post(url, params=params, proxies=self.proxies)
501     + self.result = answer.json()
502     + self.ok_update()
503     + return self.result
504     +
505     + def answer_callback_query(self, callback_query_id, text=None):
506     + url = self.tg_url_bot_general + self.key + "/answerCallbackQuery"
507     + if not text:
508     + params = {"callback_query_id": callback_query_id}
509     + else:
510     + params = {"callback_query_id": callback_query_id, "text": text}
511     + answer = requests.post(url, params=params, proxies=self.proxies)
512     + self.result = answer.json()
513     + self.ok_update()
514     + return self.result
515     +
516     + def ok_update(self):
517     + self.ok = self.result["ok"]
518     + if self.ok:
519     + self.error = None
520     + else:
521     + self.error = self.result["description"]
522     + print_message(self.error)
523     + return True
524     +
525     +
526     +def markdown_fix(message, offset, emoji=False):
527     + offset = int(offset)
528     + if emoji: # https://github.com/ableev/Zabbix-in-Telegram/issues/152
529     + offset -= 2
530     + message = "\n".join(message)
531     + message = message[:offset] + message[offset+1:]
532     + message = message.split("\n")
533     + return message
534     +
535     +
536     +class ZabbixWeb:
537     + def __init__(self, server, username, password):
538     + self.debug = False
539     + self.server = server
540     + self.username = username
541     + self.password = password
542     + self.proxies = {}
543     + self.verify = True
544     + self.cookie = None
545     + self.basic_auth_user = None
546     + self.basic_auth_pass = None
547     + self.tmp_dir = None
548     +
549     + def login(self):
550     + if not self.verify:
551     + requests.packages.urllib3.disable_warnings()
552     +
553     + data_api = {"name": self.username, "password": self.password, "enter": "Sign in"}
554     + answer = requests.post(self.server + "/", data=data_api, proxies=self.proxies, verify=self.verify,
555     + auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass))
556     + cookie = answer.cookies
557     + if len(answer.history) > 1 and answer.history[0].status_code == 302:
558     + print_message("probably the server in your config file has not full URL (for example "
559     + "'{0}' instead of '{1}')".format(self.server, self.server + "/zabbix"))
560     + if not cookie:
561     + print_message("authorization has failed, url: {0}".format(self.server + "/"))
562     + cookie = None
563     +
564     + self.cookie = cookie
565     +
566     + def graph_get(self, itemid, period, title, width, height, version=3):
567     + file_img = self.tmp_dir + "/{0}.png".format("".join(itemid))
568     +
569     + title = requests.utils.quote(title)
570     +
571     + colors = {
572     + 0: "00CC00",
573     + 1: "CC0000",
574     + 2: "0000CC",
575     + 3: "CCCC00",
576     + 4: "00CCCC",
577     + 5: "CC00CC",
578     + }
579     +
580     + drawtype = 5
581     + if len(itemid) > 1:
582     + drawtype = 2
583     +
584     + zbx_img_url_itemids = []
585     + for i in range(0, len(itemid)):
586     + itemid_url = "&items[{0}][itemid]={1}&items[{0}][sortorder]={0}&" \
587     + "items[{0}][drawtype]={3}&items[{0}][color]={2}".format(i, itemid[i], colors[i], drawtype)
588     + zbx_img_url_itemids.append(itemid_url)
589     +
590     + zbx_img_url = self.server + "/chart3.php?"
591     + if version < 4:
592     + zbx_img_url += "period={0}".format(period)
593     + else:
594     + zbx_img_url += "from=now-{0}&to=now".format(period)
595     + zbx_img_url += "&name={0}&width={1}&height={2}&graphtype=0&legend=1".format(title, width, height)
596     + zbx_img_url += "".join(zbx_img_url_itemids)
597     +
598     + if self.debug:
599     + print_message(zbx_img_url)
600     + answer = requests.get(zbx_img_url, cookies=self.cookie, proxies=self.proxies, verify=self.verify,
601     + auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass))
602     + status_code = answer.status_code
603     + if status_code == 404:
604     + print_message("can't get image from '{0}'".format(zbx_img_url))
605     + return False
606     + res_img = answer.content
607     + file_bwrite(file_img, res_img)
608     + return file_img
609     +
610     + def api_test(self):
611     + headers = {'Content-type': 'application/json'}
612     + api_data = json.dumps({"jsonrpc": "2.0", "method": "user.login", "params":
613     + {"user": self.username, "password": self.password}, "id": 1})
614     + api_url = self.server + "/api_jsonrpc.php"
615     + api = requests.post(api_url, data=api_data, proxies=self.proxies, headers=headers)
616     + return api.text
617     +
618     +
619     +def print_message(message):
620     + message = str(message) + "\n"
621     + filename = sys.argv[0].split("/")[-1]
622     + sys.stderr.write(filename + ": " + message)
623     +
624     +
625     +def list_cut(elements, symbols_limit):
626     + symbols_count = symbols_count_now = 0
627     + elements_new = []
628     + element_last_list = []
629     + for e in elements:
630     + symbols_count_now = symbols_count + len(e)
631     + if symbols_count_now > symbols_limit:
632     + limit_idx = symbols_limit - symbols_count
633     + e_list = list(e)
634     + for idx, ee in enumerate(e_list):
635     + if idx < limit_idx:
636     + element_last_list.append(ee)
637     + else:
638     + break
639     + break
640     + else:
641     + symbols_count = symbols_count_now + 1
642     + elements_new.append(e)
643     + if symbols_count_now < symbols_limit:
644     + return elements, False
645     + else:
646     + element_last = "".join(element_last_list)
647     + elements_new.append(element_last)
648     + return elements_new, True
649     +
650     +
651     +class Maps:
652     + # https://developers.google.com/maps/documentation/geocoding/intro
653     + def __init__(self):
654     + self.key = None
655     + self.proxies = {}
656     +
657     + def get_coordinates_by_address(self, address):
658     + coordinates = {"latitude": 0, "longitude": 0}
659     + url_api = "https://maps.googleapis.com/maps/api/geocode/json?key={0}&address={1}".format(self.key, address)
660     + url = url_api
661     + answer = requests.get(url, proxies=self.proxies)
662     + result = answer.json()
663     + try:
664     + coordinates_dict = result["results"][0]["geometry"]["location"]
665     + except:
666     + if "error_message" in result:
667     + print_message("[" + result["status"] + "]: " + result["error_message"])
668     + return coordinates
669     + coordinates = {"latitude": coordinates_dict["lat"], "longitude": coordinates_dict["lng"]}
670     + return coordinates
671     +
672     +
673     +def file_write(filename, text):
674     + with open(filename, "w") as fd:
675     + fd.write(str(text))
676     + return True
677     +
678     +
679     +def file_bwrite(filename, data):
680     + with open(filename, "wb") as fd:
681     + fd.write(data)
682     + return True
683     +
684     +
685     +def file_read(filename):
686     + with open(filename, "r") as fd:
687     + text = fd.readlines()
688     + return text
689     +
690     +
691     +def file_append(filename, text):
692     + with open(filename, "a") as fd:
693     + fd.write(str(text))
694     + return True
695     +
696     +
697     +def external_image_get(url, tmp_dir, timeout=6):
698     + image_hash = hashlib.md5()
699     + image_hash.update(url.encode())
700     + file_img = tmp_dir + "/external_{0}.png".format(image_hash.hexdigest())
701     + try:
702     + answer = requests.get(url, timeout=timeout, allow_redirects=True)
703     + except requests.exceptions.ReadTimeout as ex:
704     + print_message("Can't get external image from '{0}': timeout".format(url))
705     + return False
706     + status_code = answer.status_code
707     + if status_code == 404:
708     + print_message("Can't get external image from '{0}': HTTP 404 error".format(url))
709     + return False
710     + answer_image = answer.content
711     + file_bwrite(file_img, answer_image)
712     + return file_img
713     +
714     +
715     +def age2sec(age_str):
716     + age_sec = 0
717     + age_regex = "([0-9]+d)?\s?([0-9]+h)?\s?([0-9]+m)?"
718     + age_pattern = re.compile(age_regex)
719     + intervals = age_pattern.match(age_str).groups()
720     + for i in intervals:
721     + if i:
722     + metric = i[-1]
723     + if metric == "d":
724     + age_sec += int(i[0:-1])*86400
725     + if metric == "h":
726     + age_sec += int(i[0:-1])*3600
727     + if metric == "m":
728     + age_sec += int(i[0:-1])*60
729     + return age_sec
730     +
731     +
732     +def main():
733     +
734     + tmp_dir = zbxtg_settings.zbx_tg_tmp_dir
735     + if tmp_dir == "/tmp/" + zbxtg_settings.zbx_tg_prefix:
736     + print_message("WARNING: it is strongly recommended to change `zbx_tg_tmp_dir` variable in config!!!")
737     + print_message("https://github.com/ableev/Zabbix-in-Telegram/wiki/Change-zbx_tg_tmp_dir-in-settings")
738     +
739     + tmp_cookie = tmp_dir + "/cookie.py.txt"
740     + tmp_uids = tmp_dir + "/uids.txt"
741     + tmp_need_update = False # do we need to update cache file with uids or not
742     +
743     + rnd = random.randint(0, 999)
744     + ts = time.time()
745     + hash_ts = str(ts) + "." + str(rnd)
746     +
747     + log_file = "/dev/null"
748     +
749     + args = sys.argv
750     +
751     + settings = {
752     + "zbxtg_itemid": "0", # itemid for graph
753     + "zbxtg_title": None, # title for graph
754     + "zbxtg_image_period": None,
755     + "zbxtg_image_age": "3600",
756     + "zbxtg_image_width": "900",
757     + "zbxtg_image_height": "200",
758     + "tg_method_image": False, # if True - default send images, False - send text
759     + "tg_chat": False, # send message to chat or in private
760     + "tg_group": False, # send message to chat or in private
761     + "is_debug": False,
762     + "is_channel": False,
763     + "disable_web_page_preview": False,
764     + "location": None, # address
765     + "lat": 0, # latitude
766     + "lon": 0, # longitude
767     + "is_single_message": False,
768     + "markdown": False,
769     + "html": False,
770     + "signature": None,
771     + "signature_disable": False,
772     + "graph_buttons": False,
773     + "extimg": None,
774     + "to": None,
775     + "to_group": None,
776     + "forked": False,
777     + }
778     +
779     + url_github = "https://github.com/ableev/Zabbix-in-Telegram"
780     + url_wiki_base = "https://github.com/ableev/Zabbix-in-Telegram/wiki"
781     + url_tg_group = "https://t.me/ZbxTg"
782     + url_tg_channel = "https://t.me/Zabbix_in_Telegram"
783     +
784     + settings_description = {
785     + "itemid": {"name": "zbxtg_itemid", "type": "list",
786     + "help": "script will attach a graph with that itemid (could be multiple)", "url": "Graphs"},
787     + "title": {"name": "zbxtg_title", "type": "str", "help": "title for attached graph", "url": "Graphs"},
788     + "graphs_period": {"name": "zbxtg_image_period", "type": "int", "help": "graph period", "url": "Graphs"},
789     + "graphs_age": {"name": "zbxtg_image_age", "type": "str", "help": "graph period as age", "url": "Graphs"},
790     + "graphs_width": {"name": "zbxtg_image_width", "type": "int", "help": "graph width", "url": "Graphs"},
791     + "graphs_height": {"name": "zbxtg_image_height", "type": "int", "help": "graph height", "url": "Graphs"},
792     + "graphs": {"name": "tg_method_image", "type": "bool", "help": "enables graph sending", "url": "Graphs"},
793     + "chat": {"name": "tg_chat", "type": "bool", "help": "deprecated, don't use it, see 'group'",
794     + "url": "How-to-send-message-to-the-group-chat"},
795     + "group": {"name": "tg_group", "type": "bool", "help": "sends message to a group",
796     + "url": "How-to-send-message-to-the-group-chat"},
797     + "debug": {"name": "is_debug", "type": "bool", "help": "enables 'debug'",
798     + "url": "How-to-test-script-in-command-line"},
799     + "channel": {"name": "is_channel", "type": "bool", "help": "sends message to a channel",
800     + "url": "Channel-support"},
801     + "disable_web_page_preview": {"name": "disable_web_page_preview", "type": "bool",
802     + "help": "disable web page preview", "url": "Disable-web-page-preview"},
803     + "location": {"name": "location", "type": "str", "help": "address of location", "url": "Location"},
804     + "lat": {"name": "lat", "type": "str", "help": "specify latitude (and lon too!)", "url": "Location"},
805     + "lon": {"name": "lon", "type": "str", "help": "specify longitude (and lat too!)", "url": "Location"},
806     + "single_message": {"name": "is_single_message", "type": "bool", "help": "do not split message and graph",
807     + "url": "Why-am-I-getting-two-messages-instead-of-one"},
808     + "markdown": {"name": "markdown", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"},
809     + "html": {"name": "html", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"},
810     + "signature": {"name": "signature", "type": "str",
811     + "help": "bot's signature", "url": "Bot-signature"},
812     + "signature_disable": {"name": "signature_disable", "type": "bool",
813     + "help": "enables/disables bot's signature", "url": "Bot-signature"},
814     + "graph_buttons": {"name": "graph_buttons", "type": "bool",
815     + "help": "activates buttons under graph, could be using in ZbxTgDaemon",
816     + "url": "Interactive-bot"},
817     + "external_image": {"name": "extimg", "type": "str",
818     + "help": "should be url; attaches external image from different source",
819     + "url": "External-image-as-graph"},
820     + "to": {"name": "to", "type": "str", "help": "rewrite zabbix username, use that instead of arguments",
821     + "url": "Custom-to-and-to_group"},
822     + "to_group": {"name": "to_group", "type": "str",
823     + "help": "rewrite zabbix username, use that instead of arguments", "url": "Custom-to-and-to_group"},
824     + "forked": {"name": "forked", "type": "bool", "help": "internal variable, do not use it. Ever.", "url": ""},
825     + }
826     +
827     + if len(args) < 4:
828     + do_not_exit = False
829     + if "--features" in args:
830     + print(("List of available settings, see {0}/Settings\n---".format(url_wiki_base)))
831     + for sett, proprt in list(settings_description.items()):
832     + print(("{0}: {1}\ndoc: {2}/{3}\n--".format(sett, proprt["help"], url_wiki_base, proprt["url"])))
833     +
834     + elif "--show-settings" in args:
835     + do_not_exit = True
836     + print_message("Settings: " + str(json.dumps(settings, indent=2)))
837     +
838     + else:
839     + print(("Hi. You should provide at least three arguments.\n"
840     + "zbxtg.py [TO] [SUBJECT] [BODY]\n\n"
841     + "1. Read main page and/or wiki: {0} + {1}\n"
842     + "2. Public Telegram group (discussion): {2}\n"
843     + "3. Public Telegram channel: {3}\n"
844     + "4. Try dev branch for test purposes (new features, etc): {0}/tree/dev"
845     + .format(url_github, url_wiki_base, url_tg_group, url_tg_channel)))
846     + if not do_not_exit:
847     + sys.exit(0)
848     +
849     +
850     + zbx_to = args[1]
851     + zbx_subject = args[2]
852     + zbx_body = args[3]
853     +
854     + tg = TelegramAPI(key=zbxtg_settings.tg_key)
855     +
856     + tg.tmp_dir = tmp_dir
857     + tg.tmp_uids = tmp_uids
858     +
859     + if zbxtg_settings.proxy_to_tg:
860     + proxy_to_tg = zbxtg_settings.proxy_to_tg
861     + if not proxy_to_tg.find("http") and not proxy_to_tg.find("socks"):
862     + proxy_to_tg = "https://" + proxy_to_tg
863     + tg.proxies = {
864     + "https": "{0}".format(proxy_to_tg),
865     + }
866     +
867     + zbx = ZabbixWeb(server=zbxtg_settings.zbx_server, username=zbxtg_settings.zbx_api_user,
868     + password=zbxtg_settings.zbx_api_pass)
869     +
870     + zbx.tmp_dir = tmp_dir
871     +
872     + # workaround for Zabbix 4.x
873     + zbx_version = 3
874     +
875     + try:
876     + zbx_version = zbxtg_settings.zbx_server_version
877     + except:
878     + pass
879     +
880     + if zbxtg_settings.proxy_to_zbx:
881     + zbx.proxies = {
882     + "http": "http://{0}/".format(zbxtg_settings.proxy_to_zbx),
883     + "https": "https://{0}/".format(zbxtg_settings.proxy_to_zbx)
884     + }
885     +
886     + # https://github.com/ableev/Zabbix-in-Telegram/issues/55
887     + try:
888     + if zbxtg_settings.zbx_basic_auth:
889     + zbx.basic_auth_user = zbxtg_settings.zbx_basic_auth_user
890     + zbx.basic_auth_pass = zbxtg_settings.zbx_basic_auth_pass
891     + except:
892     + pass
893     +
894     + try:
895     + zbx_api_verify = zbxtg_settings.zbx_api_verify
896     + zbx.verify = zbx_api_verify
897     + except:
898     + pass
899     +
900     + map = Maps()
901     + # api key to resolve address to coordinates via google api
902     + try:
903     + if zbxtg_settings.google_maps_api_key:
904     + map.key = zbxtg_settings.google_maps_api_key
905     + if zbxtg_settings.proxy_to_tg:
906     + map.proxies = {
907     + "http": "http://{0}/".format(zbxtg_settings.proxy_to_tg),
908     + "https": "https://{0}/".format(zbxtg_settings.proxy_to_tg)
909     + }
910     + except:
911     + pass
912     +
913     + zbxtg_body = (zbx_subject + "\n" + zbx_body).splitlines()
914     + zbxtg_body_text = []
915     +
916     + for line in zbxtg_body:
917     + if line.find(zbxtg_settings.zbx_tg_prefix) > -1:
918     + setting = re.split("[\s:=]+", line, maxsplit=1)
919     + key = setting[0].replace(zbxtg_settings.zbx_tg_prefix + ";", "")
920     + if key not in settings_description:
921     + if "--debug" in args:
922     + print_message("[ERROR] There is no '{0}' method, use --features to get help".format(key))
923     + continue
924     + if settings_description[key]["type"] == "list":
925     + value = setting[1].split(",")
926     + elif len(setting) > 1 and len(setting[1]) > 0:
927     + value = setting[1]
928     + elif settings_description[key]["type"] == "bool":
929     + value = True
930     + else:
931     + value = settings[settings_description[key]["name"]]
932     + if key in settings_description:
933     + settings[settings_description[key]["name"]] = value
934     + else:
935     + zbxtg_body_text.append(line)
936     +
937     + tg_method_image = bool(settings["tg_method_image"])
938     + tg_chat = bool(settings["tg_chat"])
939     + tg_group = bool(settings["tg_group"])
940     + is_debug = bool(settings["is_debug"])
941     + is_channel = bool(settings["is_channel"])
942     + disable_web_page_preview = bool(settings["disable_web_page_preview"])
943     + is_single_message = bool(settings["is_single_message"])
944     +
945     + # experimental way to send message to the group https://github.com/ableev/Zabbix-in-Telegram/issues/15
946     + if args[0].split("/")[-1] == "zbxtg_group.py" or "--group" in args or tg_chat or tg_group:
947     + tg_chat = True
948     + tg_group = True
949     + tg.type = "group"
950     +
951     + if "--debug" in args or is_debug:
952     + is_debug = True
953     + tg.debug = True
954     + zbx.debug = True
955     + print_message(tg.get_me())
956     + print_message("Cache file with uids: " + tg.tmp_uids)
957     + log_file = tmp_dir + ".debug." + hash_ts + ".log"
958     + #print_message(log_file)
959     +
960     + if "--markdown" in args or settings["markdown"]:
961     + tg.markdown = True
962     +
963     + if "--html" in args or settings["html"]:
964     + tg.html = True
965     +
966     + if "--channel" in args or is_channel:
967     + tg.type = "channel"
968     +
969     + if "--disable_web_page_preview" in args or disable_web_page_preview:
970     + if is_debug:
971     + print_message("'disable_web_page_preview' option has been enabled")
972     + tg.disable_web_page_preview = True
973     +
974     + if "--graph_buttons" in args or settings["graph_buttons"]:
975     + tg.image_buttons = True
976     +
977     + if "--forked" in args:
978     + settings["forked"] = True
979     +
980     + if "--tg-key" in args:
981     + tg.key = args[args.index("--tg-key") + 1]
982     +
983     + location_coordinates = {"latitude": None, "longitude": None}
984     + if settings["lat"] > 0 and settings["lat"] > 0:
985     + location_coordinates = {"latitude": settings["lat"], "longitude": settings["lon"]}
986     + tg.location = location_coordinates
987     + else:
988     + if settings["location"]:
989     + location_coordinates = map.get_coordinates_by_address(settings["location"])
990     + if location_coordinates:
991     + settings["lat"] = location_coordinates["latitude"]
992     + settings["lon"] = location_coordinates["longitude"]
993     + tg.location = location_coordinates
994     +
995     + if not os.path.isdir(tmp_dir):
996     + if is_debug:
997     + print_message("Tmp dir doesn't exist, creating new one...")
998     + try:
999     + os.makedirs(tmp_dir)
1000     + open(tg.tmp_uids, "a").close()
1001     + os.chmod(tmp_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
1002     + os.chmod(tg.tmp_uids, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
1003     + except:
1004     + tmp_dir = "/tmp"
1005     + if is_debug:
1006     + print_message("Using {0} as a temporary dir".format(tmp_dir))
1007     +
1008     + done_all_work_in_the_fork = False
1009     + # issue75
1010     +
1011     + to_types = ["to", "to_group", "to_channel"]
1012     + to_types_to_telegram = {"to": "private", "to_group": "group", "to_channel": "channel"}
1013     + multiple_to = {}
1014     + for i in to_types:
1015     + multiple_to[i]=[]
1016     +
1017     + for t in to_types:
1018     + try:
1019     + if settings[t] and not settings["forked"]:
1020     + # zbx_to = settings["to"]
1021     + multiple_to[t] = re.split(",", settings[t])
1022     + except KeyError:
1023     + pass
1024     +
1025     + # example:
1026     + # {'to_channel': [], 'to': ['usr1', 'usr2', 'usr3'], 'to_group': []}
1027     +
1028     + if (sum([len(v) for k, v in list(multiple_to.items())])) == 1:
1029     + # if we have only one recipient, we don't need fork to send message, just re-write "to" vaiable
1030     + tmp_max = 0
1031     + for t in to_types:
1032     + if len(multiple_to[t]) > tmp_max:
1033     + tmp_max = len(multiple_to[t])
1034     + tg.type = to_types_to_telegram[t]
1035     + zbx_to = multiple_to[t][0]
1036     + else:
1037     + for t in to_types:
1038     + for i in multiple_to[t]:
1039     + args_new = list(args)
1040     + args_new[1] = i
1041     + if t == "to_group":
1042     + args_new.append("--group")
1043     + args_new.append("--forked")
1044     + args_new.insert(0, sys.executable)
1045     + if is_debug:
1046     + print_message("Fork for custom recipient ({1}), new args: {0}".format(args_new,
1047     + to_types_to_telegram[t]))
1048     + subprocess.call(args_new)
1049     + done_all_work_in_the_fork = True
1050     +
1051     + if done_all_work_in_the_fork:
1052     + sys.exit(0)
1053     +
1054     + uid = None
1055     +
1056     + if tg.type == "channel":
1057     + uid = zbx_to
1058     + if tg.type == "private":
1059     + zbx_to = zbx_to.replace("@", "")
1060     +
1061     + if zbx_to.isdigit():
1062     + uid = zbx_to
1063     +
1064     + if not uid:
1065     + uid = tg.get_uid_from_cache(zbx_to)
1066     +
1067     + if not uid:
1068     + uid = tg.get_uid(zbx_to)
1069     + if uid:
1070     + tmp_need_update = True
1071     + if not uid:
1072     + tg.error_need_to_contact(zbx_to)
1073     + sys.exit(1)
1074     +
1075     + if tmp_need_update:
1076     + tg.update_cache_uid(zbx_to, str(uid).rstrip())
1077     +
1078     + if is_debug:
1079     + print_message("Telegram uid of {0} '{1}': {2}".format(tg.type, zbx_to, uid))
1080     +
1081     + # add signature, turned off by default, you can turn it on in config
1082     + try:
1083     + if "--signature" in args or settings["signature"] or zbxtg_settings.zbx_tg_signature\
1084     + and not "--signature_disable" in args and not settings["signature_disable"]:
1085     + if "--signature" in args:
1086     + settings["signature"] = args[args.index("--signature") + 1]
1087     + if not settings["signature"]:
1088     + settings["signature"] = zbxtg_settings.zbx_server
1089     + zbxtg_body_text.append("--")
1090     + zbxtg_body_text.append(settings["signature"])
1091     + except:
1092     + pass
1093     +
1094     + # replace text with emojis
1095     + internal_using_emoji = False # I hate that, but... https://github.com/ableev/Zabbix-in-Telegram/issues/152
1096     + if hasattr(zbxtg_settings, "emoji_map"):
1097     + zbxtg_body_text_emoji_support = []
1098     + for l in zbxtg_body_text:
1099     + l_new = l
1100     + for k, v in list(zbxtg_settings.emoji_map.items()):
1101     + l_new = l_new.replace("{{" + k + "}}", v)
1102     + zbxtg_body_text_emoji_support.append(l_new)
1103     + if len("".join(zbxtg_body_text)) - len("".join(zbxtg_body_text_emoji_support)):
1104     + internal_using_emoji = True
1105     + zbxtg_body_text = zbxtg_body_text_emoji_support
1106     +
1107     + if not is_single_message:
1108     + tg.send_message(uid, zbxtg_body_text)
1109     + if not tg.ok:
1110     + # first case – if group has been migrated to a supergroup, we need to update chat_id of that group
1111     + if tg.error.find("migrated") > -1 and tg.error.find("supergroup") > -1:
1112     + migrate_to_chat_id = tg.result["parameters"]["migrate_to_chat_id"]
1113     + tg.update_cache_uid(zbx_to, migrate_to_chat_id, message="Group chat is migrated to supergroup, "
1114     + "updating cache file")
1115     + uid = migrate_to_chat_id
1116     + tg.send_message(uid, zbxtg_body_text)
1117     +
1118     + # another case if markdown is enabled and we got parse error, try to remove "bad" symbols from message
1119     + if tg.markdown and tg.error.find("Can't find end of the entity starting at byte offset") > -1:
1120     + markdown_warning = "Original message has been fixed due to {0}. " \
1121     + "Please, fix the markdown, it's slowing down messages sending."\
1122     + .format(url_wiki_base + "/" + settings_description["markdown"]["url"])
1123     + markdown_fix_attempts = 0
1124     + while not tg.ok and markdown_fix_attempts != 3:
1125     + offset = re.search("Can't find end of the entity starting at byte offset ([0-9]+)", tg.error).group(1)
1126     + zbxtg_body_text = markdown_fix(zbxtg_body_text, offset, emoji=internal_using_emoji) + \
1127     + ["\n"] + [markdown_warning]
1128     + tg.disable_web_page_preview = True
1129     + tg.send_message(uid, zbxtg_body_text)
1130     + markdown_fix_attempts += 1
1131     + if tg.ok:
1132     + print_message(markdown_warning)
1133     +
1134     + if is_debug:
1135     + print((tg.result))
1136     +
1137     + if settings["zbxtg_image_age"]:
1138     + age_sec = age2sec(settings["zbxtg_image_age"])
1139     + if age_sec > 0 and age_sec > 3600:
1140     + settings["zbxtg_image_period"] = age_sec
1141     +
1142     + message_id = 0
1143     + if tg_method_image:
1144     + zbx.login()
1145     + if not zbx.cookie:
1146     + text_warn = "Login to Zabbix web UI has failed (web url, user or password are incorrect), "\
1147     + "unable to send graphs check manually"
1148     + tg.send_message(uid, [text_warn])
1149     + print_message(text_warn)
1150     + else:
1151     + if not settings["extimg"]:
1152     + zbxtg_file_img = zbx.graph_get(settings["zbxtg_itemid"], settings["zbxtg_image_period"],
1153     + settings["zbxtg_title"], settings["zbxtg_image_width"],
1154     + settings["zbxtg_image_height"], version=zbx_version)
1155     + else:
1156     + zbxtg_file_img = external_image_get(settings["extimg"], tmp_dir=zbx.tmp_dir)
1157     + zbxtg_body_text, is_modified = list_cut(zbxtg_body_text, 200)
1158     + if tg.ok:
1159     + message_id = tg.result["result"]["message_id"]
1160     + tg.reply_to_message_id = message_id
1161     + if not zbxtg_file_img:
1162     + text_warn = "Can't get graph image, check script manually, see logs, or disable graphs"
1163     + tg.send_message(uid, [text_warn])
1164     + print_message(text_warn)
1165     + else:
1166     + if not is_single_message:
1167     + zbxtg_body_text = ""
1168     + else:
1169     + if is_modified:
1170     + text_warn = "probably you will see MEDIA_CAPTION_TOO_LONG error, "\
1171     + "the message has been cut to 200 symbols, "\
1172     + "https://github.com/ableev/Zabbix-in-Telegram/issues/9"\
1173     + "#issuecomment-166895044"
1174     + print_message(text_warn)
1175     + if not is_single_message:
1176     + tg.disable_notification = True
1177     + tg.send_photo(uid, zbxtg_body_text, zbxtg_file_img)
1178     + if tg.ok:
1179     + settings["zbxtg_body_text"] = zbxtg_body_text
1180     + os.remove(zbxtg_file_img)
1181     + else:
1182     + if tg.error.find("PHOTO_INVALID_DIMENSIONS") > -1:
1183     + if not tg.disable_web_page_preview:
1184     + tg.disable_web_page_preview = True
1185     + text_warn = "Zabbix user couldn't get graph (probably has no rights to get data from host), " \
1186     + "check script manually, see {0}".format(url_wiki_base + "/" +
1187     + settings_description["graphs"]["url"])
1188     + tg.send_message(uid, [text_warn])
1189     + print_message(text_warn)
1190     + if tg.location and location_coordinates["latitude"] and location_coordinates["longitude"]:
1191     + tg.reply_to_message_id = message_id
1192     + tg.disable_notification = True
1193     + tg.send_location(to=uid, coordinates=location_coordinates)
1194     +
1195     + if "--show-settings" in args:
1196     + print_message("Settings: " + str(json.dumps(settings, indent=2)))
1197     +
1198     +if __name__ == "__main__":
1199     + main()
1200     diff -Nur --no-dereference smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg.py smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg.py
1201     --- smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg.py 1969-12-31 19:00:00.000000000 -0500
1202     +++ smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg.py 2021-11-08 22:00:51.689000000 -0500
1203     @@ -0,0 +1,939 @@
1204     +#!/usr/bin/env python
1205     +# coding: utf-8
1206     +
1207     +import sys
1208     +import os
1209     +import time
1210     +import random
1211     +import string
1212     +import requests
1213     +import json
1214     +import re
1215     +import stat
1216     +import hashlib
1217     +import subprocess
1218     +#import sqlite3
1219     +from os.path import dirname
1220     +import zbxtg_settings
1221     +
1222     +
1223     +class Cache:
1224     + def __init__(self, database):
1225     + self.database = database
1226     +
1227     + def create_db(self, database):
1228     + pass
1229     +
1230     +
1231     +class TelegramAPI:
1232     + tg_url_bot_general = "https://api.telegram.org/bot"
1233     +
1234     + def http_get(self, url):
1235     + answer = requests.get(url, proxies=self.proxies)
1236     + self.result = answer.json()
1237     + self.ok_update()
1238     + return self.result
1239     +
1240     + def __init__(self, key):
1241     + self.debug = False
1242     + self.key = key
1243     + self.proxies = {}
1244     + self.type = "private" # 'private' for private chats or 'group' for group chats
1245     + self.markdown = False
1246     + self.html = False
1247     + self.disable_web_page_preview = False
1248     + self.disable_notification = False
1249     + self.reply_to_message_id = 0
1250     + self.tmp_dir = None
1251     + self.tmp_uids = None
1252     + self.location = {"latitude": None, "longitude": None}
1253     + self.update_offset = 0
1254     + self.image_buttons = False
1255     + self.result = None
1256     + self.ok = None
1257     + self.error = None
1258     + self.get_updates_from_file = False
1259     +
1260     + def get_me(self):
1261     + url = self.tg_url_bot_general + self.key + "/getMe"
1262     + me = self.http_get(url)
1263     + return me
1264     +
1265     + def get_updates(self):
1266     + url = self.tg_url_bot_general + self.key + "/getUpdates"
1267     + params = {"offset": self.update_offset}
1268     + if self.debug:
1269     + print_message(url)
1270     + answer = requests.post(url, params=params, proxies=self.proxies)
1271     + self.result = answer.json()
1272     + if self.get_updates_from_file:
1273     + print_message("Getting updated from file getUpdates.txt")
1274     + self.result = json.loads("".join(file_read("getUpdates.txt")))
1275     + if self.debug:
1276     + print_message("Content of /getUpdates:")
1277     + print_message(json.dumps(self.result))
1278     + self.ok_update()
1279     + return self.result
1280     +
1281     + def send_message(self, to, message):
1282     + url = self.tg_url_bot_general + self.key + "/sendMessage"
1283     + message = "\n".join(message)
1284     + params = {"chat_id": to, "text": message, "disable_web_page_preview": self.disable_web_page_preview,
1285     + "disable_notification": self.disable_notification}
1286     + if self.reply_to_message_id:
1287     + params["reply_to_message_id"] = self.reply_to_message_id
1288     + if self.markdown or self.html:
1289     + parse_mode = "HTML"
1290     + if self.markdown:
1291     + parse_mode = "Markdown"
1292     + params["parse_mode"] = parse_mode
1293     + if self.debug:
1294     + print_message("Trying to /sendMessage:")
1295     + print_message(url)
1296     + print_message("post params: " + str(params))
1297     + answer = requests.post(url, params=params, proxies=self.proxies)
1298     + if answer.status_code == 414:
1299     + self.result = {"ok": False, "description": "414 URI Too Long"}
1300     + else:
1301     + self.result = answer.json()
1302     + self.ok_update()
1303     + return self.result
1304     +
1305     + def update_message(self, to, message_id, message):
1306     + url = self.tg_url_bot_general + self.key + "/editMessageText"
1307     + message = "\n".join(message)
1308     + params = {"chat_id": to, "message_id": message_id, "text": message,
1309     + "disable_web_page_preview": self.disable_web_page_preview,
1310     + "disable_notification": self.disable_notification}
1311     + if self.markdown or self.html:
1312     + parse_mode = "HTML"
1313     + if self.markdown:
1314     + parse_mode = "Markdown"
1315     + params["parse_mode"] = parse_mode
1316     + if self.debug:
1317     + print_message("Trying to /editMessageText:")
1318     + print_message(url)
1319     + print_message("post params: " + str(params))
1320     + answer = requests.post(url, params=params, proxies=self.proxies)
1321     + self.result = answer.json()
1322     + self.ok_update()
1323     + return self.result
1324     +
1325     + def send_photo(self, to, message, path):
1326     + url = self.tg_url_bot_general + self.key + "/sendPhoto"
1327     + message = "\n".join(message)
1328     + if self.image_buttons:
1329     + reply_markup = json.dumps({"inline_keyboard": [[
1330     + {"text": "R", "callback_data": "graph_refresh"},
1331     + {"text": "1h", "callback_data": "graph_period_3600"},
1332     + {"text": "3h", "callback_data": "graph_period_10800"},
1333     + {"text": "6h", "callback_data": "graph_period_21600"},
1334     + {"text": "12h", "callback_data": "graph_period_43200"},
1335     + {"text": "24h", "callback_data": "graph_period_86400"},
1336     + ], ]})
1337     + else:
1338     + reply_markup = json.dumps({})
1339     + params = {"chat_id": to, "caption": message, "disable_notification": self.disable_notification,
1340     + "reply_markup": reply_markup}
1341     + if self.reply_to_message_id:
1342     + params["reply_to_message_id"] = self.reply_to_message_id
1343     + files = {"photo": open(path, 'rb')}
1344     + if self.debug:
1345     + print_message("Trying to /sendPhoto:")
1346     + print_message(url)
1347     + print_message(params)
1348     + print_message("files: " + str(files))
1349     + answer = requests.post(url, params=params, files=files, proxies=self.proxies)
1350     + self.result = answer.json()
1351     + self.ok_update()
1352     + return self.result
1353     +
1354     + def send_txt(self, to, text, text_name=None):
1355     + path = self.tmp_dir + "/" + "zbxtg_txt_"
1356     + url = self.tg_url_bot_general + self.key + "/sendDocument"
1357     + text = "\n".join(text)
1358     + if not text_name:
1359     + path += "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
1360     + else:
1361     + path += text_name
1362     + path += ".txt"
1363     + file_write(path, text)
1364     + params = {"chat_id": to, "caption": path.split("/")[-1], "disable_notification": self.disable_notification}
1365     + if self.reply_to_message_id:
1366     + params["reply_to_message_id"] = self.reply_to_message_id
1367     + files = {"document": open(path, 'rb')}
1368     + if self.debug:
1369     + print_message("Trying to /sendDocument:")
1370     + print_message(url)
1371     + print_message(params)
1372     + print_message("files: " + str(files))
1373     + answer = requests.post(url, params=params, files=files, proxies=self.proxies)
1374     + self.result = answer.json()
1375     + self.ok_update()
1376     + return self.result
1377     +
1378     + def get_uid(self, name):
1379     + uid = 0
1380     + if self.debug:
1381     + print_message("Getting uid from /getUpdates...")
1382     + updates = self.get_updates()
1383     + for m in updates["result"]:
1384     + if "message" in m:
1385     + chat = m["message"]["chat"]
1386     + elif "edited_message" in m:
1387     + chat = m["edited_message"]["chat"]
1388     + else:
1389     + continue
1390     + if chat["type"] == self.type == "private":
1391     + if "username" in chat:
1392     + if chat["username"] == name:
1393     + uid = chat["id"]
1394     + if (chat["type"] == "group" or chat["type"] == "supergroup") and self.type == "group":
1395     + if "title" in chat:
1396     + if sys.version_info[0] < 3:
1397     + if chat["title"] == name.decode("utf-8"):
1398     + uid = chat["id"]
1399     + else:
1400     + if chat["title"] == name:
1401     + uid = chat["id"]
1402     + return uid
1403     +
1404     + def error_need_to_contact(self, to):
1405     + if self.type == "private":
1406     + print_message("User '{0}' needs to send some text bot in private".format(to))
1407     + if self.type == "group":
1408     + print_message("You need start a conversation with your bot first in '{0}' group chat, type '/start@{1}'"
1409     + .format(to, self.get_me()["result"]["username"]))
1410     +
1411     + def update_cache_uid(self, name, uid, message="Add new string to cache file"):
1412     + cache_string = "{0};{1};{2}\n".format(name, self.type, str(uid).rstrip())
1413     + # FIXME
1414     + if self.debug:
1415     + print_message("{0}: {1}".format(message, cache_string))
1416     + with open(self.tmp_uids, "a") as cache_file_uids:
1417     + cache_file_uids.write(cache_string)
1418     + return True
1419     +
1420     + def get_uid_from_cache(self, name):
1421     + if self.debug:
1422     + print_message("Trying to read cached uid for {0}, {1}, from {2}".format(name, self.type, self.tmp_uids))
1423     + uid = 0
1424     + if os.path.isfile(self.tmp_uids):
1425     + with open(self.tmp_uids, 'r') as cache_file_uids:
1426     + cache_uids_old = cache_file_uids.readlines()
1427     + for u in cache_uids_old:
1428     + u_splitted = u.split(";")
1429     + if name == u_splitted[0] and self.type == u_splitted[1]:
1430     + uid = u_splitted[2]
1431     + return uid
1432     +
1433     + def send_location(self, to, coordinates):
1434     + url = self.tg_url_bot_general + self.key + "/sendLocation"
1435     + params = {"chat_id": to, "disable_notification": self.disable_notification,
1436     + "latitude": coordinates["latitude"], "longitude": coordinates["longitude"]}
1437     + if self.reply_to_message_id:
1438     + params["reply_to_message_id"] = self.reply_to_message_id
1439     + if self.debug:
1440     + print_message("Trying to /sendLocation:")
1441     + print_message(url)
1442     + print_message("post params: " + str(params))
1443     + answer = requests.post(url, params=params, proxies=self.proxies)
1444     + self.result = answer.json()
1445     + self.ok_update()
1446     + return self.result
1447     +
1448     + def answer_callback_query(self, callback_query_id, text=None):
1449     + url = self.tg_url_bot_general + self.key + "/answerCallbackQuery"
1450     + if not text:
1451     + params = {"callback_query_id": callback_query_id}
1452     + else:
1453     + params = {"callback_query_id": callback_query_id, "text": text}
1454     + answer = requests.post(url, params=params, proxies=self.proxies)
1455     + self.result = answer.json()
1456     + self.ok_update()
1457     + return self.result
1458     +
1459     + def ok_update(self):
1460     + self.ok = self.result["ok"]
1461     + if self.ok:
1462     + self.error = None
1463     + else:
1464     + self.error = self.result["description"]
1465     + print_message(self.error)
1466     + return True
1467     +
1468     +
1469     +def markdown_fix(message, offset, emoji=False):
1470     + offset = int(offset)
1471     + if emoji: # https://github.com/ableev/Zabbix-in-Telegram/issues/152
1472     + offset -= 2
1473     + message = "\n".join(message)
1474     + message = message[:offset] + message[offset+1:]
1475     + message = message.split("\n")
1476     + return message
1477     +
1478     +
1479     +class ZabbixWeb:
1480     + def __init__(self, server, username, password):
1481     + self.debug = False
1482     + self.server = server
1483     + self.username = username
1484     + self.password = password
1485     + self.proxies = {}
1486     + self.verify = True
1487     + self.cookie = None
1488     + self.basic_auth_user = None
1489     + self.basic_auth_pass = None
1490     + self.tmp_dir = None
1491     +
1492     + def login(self):
1493     + if not self.verify:
1494     + requests.packages.urllib3.disable_warnings()
1495     +
1496     + data_api = {"name": self.username, "password": self.password, "enter": "Sign in"}
1497     + answer = requests.post(self.server + "/", data=data_api, proxies=self.proxies, verify=self.verify,
1498     + auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass))
1499     + cookie = answer.cookies
1500     + if len(answer.history) > 1 and answer.history[0].status_code == 302:
1501     + print_message("probably the server in your config file has not full URL (for example "
1502     + "'{0}' instead of '{1}')".format(self.server, self.server + "/zabbix"))
1503     + if not cookie:
1504     + print_message("authorization has failed, url: {0}".format(self.server + "/"))
1505     + cookie = None
1506     +
1507     + self.cookie = cookie
1508     +
1509     + def graph_get(self, itemid, period, title, width, height, version=3):
1510     + file_img = self.tmp_dir + "/{0}.png".format("".join(itemid))
1511     +
1512     + title = requests.utils.quote(title)
1513     +
1514     + colors = {
1515     + 0: "00CC00",
1516     + 1: "CC0000",
1517     + 2: "0000CC",
1518     + 3: "CCCC00",
1519     + 4: "00CCCC",
1520     + 5: "CC00CC",
1521     + }
1522     +
1523     + drawtype = 5
1524     + if len(itemid) > 1:
1525     + drawtype = 2
1526     +
1527     + zbx_img_url_itemids = []
1528     + for i in range(0, len(itemid)):
1529     + itemid_url = "&items[{0}][itemid]={1}&items[{0}][sortorder]={0}&" \
1530     + "items[{0}][drawtype]={3}&items[{0}][color]={2}".format(i, itemid[i], colors[i], drawtype)
1531     + zbx_img_url_itemids.append(itemid_url)
1532     +
1533     + zbx_img_url = self.server + "/chart3.php?"
1534     + if version < 4:
1535     + zbx_img_url += "period={0}".format(period)
1536     + else:
1537     + zbx_img_url += "from=now-{0}&to=now".format(period)
1538     + zbx_img_url += "&name={0}&width={1}&height={2}&graphtype=0&legend=1".format(title, width, height)
1539     + zbx_img_url += "".join(zbx_img_url_itemids)
1540     +
1541     + if self.debug:
1542     + print_message(zbx_img_url)
1543     + answer = requests.get(zbx_img_url, cookies=self.cookie, proxies=self.proxies, verify=self.verify,
1544     + auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass))
1545     + status_code = answer.status_code
1546     + if status_code == 404:
1547     + print_message("can't get image from '{0}'".format(zbx_img_url))
1548     + return False
1549     + res_img = answer.content
1550     + file_bwrite(file_img, res_img)
1551     + return file_img
1552     +
1553     + def api_test(self):
1554     + headers = {'Content-type': 'application/json'}
1555     + api_data = json.dumps({"jsonrpc": "2.0", "method": "user.login", "params":
1556     + {"user": self.username, "password": self.password}, "id": 1})
1557     + api_url = self.server + "/api_jsonrpc.php"
1558     + api = requests.post(api_url, data=api_data, proxies=self.proxies, headers=headers)
1559     + return api.text
1560     +
1561     +
1562     +def print_message(message):
1563     + message = str(message) + "\n"
1564     + filename = sys.argv[0].split("/")[-1]
1565     + sys.stderr.write(filename + ": " + message)
1566     +
1567     +
1568     +def list_cut(elements, symbols_limit):
1569     + symbols_count = symbols_count_now = 0
1570     + elements_new = []
1571     + element_last_list = []
1572     + for e in elements:
1573     + symbols_count_now = symbols_count + len(e)
1574     + if symbols_count_now > symbols_limit:
1575     + limit_idx = symbols_limit - symbols_count
1576     + e_list = list(e)
1577     + for idx, ee in enumerate(e_list):
1578     + if idx < limit_idx:
1579     + element_last_list.append(ee)
1580     + else:
1581     + break
1582     + break
1583     + else:
1584     + symbols_count = symbols_count_now + 1
1585     + elements_new.append(e)
1586     + if symbols_count_now < symbols_limit:
1587     + return elements, False
1588     + else:
1589     + element_last = "".join(element_last_list)
1590     + elements_new.append(element_last)
1591     + return elements_new, True
1592     +
1593     +
1594     +class Maps:
1595     + # https://developers.google.com/maps/documentation/geocoding/intro
1596     + def __init__(self):
1597     + self.key = None
1598     + self.proxies = {}
1599     +
1600     + def get_coordinates_by_address(self, address):
1601     + coordinates = {"latitude": 0, "longitude": 0}
1602     + url_api = "https://maps.googleapis.com/maps/api/geocode/json?key={0}&address={1}".format(self.key, address)
1603     + url = url_api
1604     + answer = requests.get(url, proxies=self.proxies)
1605     + result = answer.json()
1606     + try:
1607     + coordinates_dict = result["results"][0]["geometry"]["location"]
1608     + except:
1609     + if "error_message" in result:
1610     + print_message("[" + result["status"] + "]: " + result["error_message"])
1611     + return coordinates
1612     + coordinates = {"latitude": coordinates_dict["lat"], "longitude": coordinates_dict["lng"]}
1613     + return coordinates
1614     +
1615     +
1616     +def file_write(filename, text):
1617     + with open(filename, "w") as fd:
1618     + fd.write(str(text))
1619     + return True
1620     +
1621     +
1622     +def file_bwrite(filename, data):
1623     + with open(filename, "wb") as fd:
1624     + fd.write(data)
1625     + return True
1626     +
1627     +
1628     +def file_read(filename):
1629     + with open(filename, "r") as fd:
1630     + text = fd.readlines()
1631     + return text
1632     +
1633     +
1634     +def file_append(filename, text):
1635     + with open(filename, "a") as fd:
1636     + fd.write(str(text))
1637     + return True
1638     +
1639     +
1640     +def external_image_get(url, tmp_dir, timeout=6):
1641     + image_hash = hashlib.md5()
1642     + image_hash.update(url.encode())
1643     + file_img = tmp_dir + "/external_{0}.png".format(image_hash.hexdigest())
1644     + try:
1645     + answer = requests.get(url, timeout=timeout, allow_redirects=True)
1646     + except requests.exceptions.ReadTimeout as ex:
1647     + print_message("Can't get external image from '{0}': timeout".format(url))
1648     + return False
1649     + status_code = answer.status_code
1650     + if status_code == 404:
1651     + print_message("Can't get external image from '{0}': HTTP 404 error".format(url))
1652     + return False
1653     + answer_image = answer.content
1654     + file_bwrite(file_img, answer_image)
1655     + return file_img
1656     +
1657     +
1658     +def age2sec(age_str):
1659     + age_sec = 0
1660     + age_regex = "([0-9]+d)?\s?([0-9]+h)?\s?([0-9]+m)?"
1661     + age_pattern = re.compile(age_regex)
1662     + intervals = age_pattern.match(age_str).groups()
1663     + for i in intervals:
1664     + if i:
1665     + metric = i[-1]
1666     + if metric == "d":
1667     + age_sec += int(i[0:-1])*86400
1668     + if metric == "h":
1669     + age_sec += int(i[0:-1])*3600
1670     + if metric == "m":
1671     + age_sec += int(i[0:-1])*60
1672     + return age_sec
1673     +
1674     +
1675     +def main():
1676     +
1677     + tmp_dir = zbxtg_settings.zbx_tg_tmp_dir
1678     + if tmp_dir == "/tmp/" + zbxtg_settings.zbx_tg_prefix:
1679     + print_message("WARNING: it is strongly recommended to change `zbx_tg_tmp_dir` variable in config!!!")
1680     + print_message("https://github.com/ableev/Zabbix-in-Telegram/wiki/Change-zbx_tg_tmp_dir-in-settings")
1681     +
1682     + tmp_cookie = tmp_dir + "/cookie.py.txt"
1683     + tmp_uids = tmp_dir + "/uids.txt"
1684     + tmp_need_update = False # do we need to update cache file with uids or not
1685     +
1686     + rnd = random.randint(0, 999)
1687     + ts = time.time()
1688     + hash_ts = str(ts) + "." + str(rnd)
1689     +
1690     + log_file = "/dev/null"
1691     +
1692     + args = sys.argv
1693     +
1694     + settings = {
1695     + "zbxtg_itemid": "0", # itemid for graph
1696     + "zbxtg_title": None, # title for graph
1697     + "zbxtg_image_period": None,
1698     + "zbxtg_image_age": "3600",
1699     + "zbxtg_image_width": "900",
1700     + "zbxtg_image_height": "200",
1701     + "tg_method_image": False, # if True - default send images, False - send text
1702     + "tg_chat": False, # send message to chat or in private
1703     + "tg_group": False, # send message to chat or in private
1704     + "is_debug": False,
1705     + "is_channel": False,
1706     + "disable_web_page_preview": False,
1707     + "location": None, # address
1708     + "lat": 0, # latitude
1709     + "lon": 0, # longitude
1710     + "is_single_message": False,
1711     + "markdown": False,
1712     + "html": False,
1713     + "signature": None,
1714     + "signature_disable": False,
1715     + "graph_buttons": False,
1716     + "extimg": None,
1717     + "to": None,
1718     + "to_group": None,
1719     + "forked": False,
1720     + }
1721     +
1722     + url_github = "https://github.com/ableev/Zabbix-in-Telegram"
1723     + url_wiki_base = "https://github.com/ableev/Zabbix-in-Telegram/wiki"
1724     + url_tg_group = "https://t.me/ZbxTg"
1725     + url_tg_channel = "https://t.me/Zabbix_in_Telegram"
1726     +
1727     + settings_description = {
1728     + "itemid": {"name": "zbxtg_itemid", "type": "list",
1729     + "help": "script will attach a graph with that itemid (could be multiple)", "url": "Graphs"},
1730     + "title": {"name": "zbxtg_title", "type": "str", "help": "title for attached graph", "url": "Graphs"},
1731     + "graphs_period": {"name": "zbxtg_image_period", "type": "int", "help": "graph period", "url": "Graphs"},
1732     + "graphs_age": {"name": "zbxtg_image_age", "type": "str", "help": "graph period as age", "url": "Graphs"},
1733     + "graphs_width": {"name": "zbxtg_image_width", "type": "int", "help": "graph width", "url": "Graphs"},
1734     + "graphs_height": {"name": "zbxtg_image_height", "type": "int", "help": "graph height", "url": "Graphs"},
1735     + "graphs": {"name": "tg_method_image", "type": "bool", "help": "enables graph sending", "url": "Graphs"},
1736     + "chat": {"name": "tg_chat", "type": "bool", "help": "deprecated, don't use it, see 'group'",
1737     + "url": "How-to-send-message-to-the-group-chat"},
1738     + "group": {"name": "tg_group", "type": "bool", "help": "sends message to a group",
1739     + "url": "How-to-send-message-to-the-group-chat"},
1740     + "debug": {"name": "is_debug", "type": "bool", "help": "enables 'debug'",
1741     + "url": "How-to-test-script-in-command-line"},
1742     + "channel": {"name": "is_channel", "type": "bool", "help": "sends message to a channel",
1743     + "url": "Channel-support"},
1744     + "disable_web_page_preview": {"name": "disable_web_page_preview", "type": "bool",
1745     + "help": "disable web page preview", "url": "Disable-web-page-preview"},
1746     + "location": {"name": "location", "type": "str", "help": "address of location", "url": "Location"},
1747     + "lat": {"name": "lat", "type": "str", "help": "specify latitude (and lon too!)", "url": "Location"},
1748     + "lon": {"name": "lon", "type": "str", "help": "specify longitude (and lat too!)", "url": "Location"},
1749     + "single_message": {"name": "is_single_message", "type": "bool", "help": "do not split message and graph",
1750     + "url": "Why-am-I-getting-two-messages-instead-of-one"},
1751     + "markdown": {"name": "markdown", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"},
1752     + "html": {"name": "html", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"},
1753     + "signature": {"name": "signature", "type": "str",
1754     + "help": "bot's signature", "url": "Bot-signature"},
1755     + "signature_disable": {"name": "signature_disable", "type": "bool",
1756     + "help": "enables/disables bot's signature", "url": "Bot-signature"},
1757     + "graph_buttons": {"name": "graph_buttons", "type": "bool",
1758     + "help": "activates buttons under graph, could be using in ZbxTgDaemon",
1759     + "url": "Interactive-bot"},
1760     + "external_image": {"name": "extimg", "type": "str",
1761     + "help": "should be url; attaches external image from different source",
1762     + "url": "External-image-as-graph"},
1763     + "to": {"name": "to", "type": "str", "help": "rewrite zabbix username, use that instead of arguments",
1764     + "url": "Custom-to-and-to_group"},
1765     + "to_group": {"name": "to_group", "type": "str",
1766     + "help": "rewrite zabbix username, use that instead of arguments", "url": "Custom-to-and-to_group"},
1767     + "forked": {"name": "forked", "type": "bool", "help": "internal variable, do not use it. Ever.", "url": ""},
1768     + }
1769     +
1770     + if len(args) < 4:
1771     + do_not_exit = False
1772     + if "--features" in args:
1773     + print(("List of available settings, see {0}/Settings\n---".format(url_wiki_base)))
1774     + for sett, proprt in list(settings_description.items()):
1775     + print(("{0}: {1}\ndoc: {2}/{3}\n--".format(sett, proprt["help"], url_wiki_base, proprt["url"])))
1776     +
1777     + elif "--show-settings" in args:
1778     + do_not_exit = True
1779     + print_message("Settings: " + str(json.dumps(settings, indent=2)))
1780     +
1781     + else:
1782     + print(("Hi. You should provide at least three arguments.\n"
1783     + "zbxtg.py [TO] [SUBJECT] [BODY]\n\n"
1784     + "1. Read main page and/or wiki: {0} + {1}\n"
1785     + "2. Public Telegram group (discussion): {2}\n"
1786     + "3. Public Telegram channel: {3}\n"
1787     + "4. Try dev branch for test purposes (new features, etc): {0}/tree/dev"
1788     + .format(url_github, url_wiki_base, url_tg_group, url_tg_channel)))
1789     + if not do_not_exit:
1790     + sys.exit(0)
1791     +
1792     +
1793     + zbx_to = args[1]
1794     + zbx_subject = args[2]
1795     + zbx_body = args[3]
1796     +
1797     + tg = TelegramAPI(key=zbxtg_settings.tg_key)
1798     +
1799     + tg.tmp_dir = tmp_dir
1800     + tg.tmp_uids = tmp_uids
1801     +
1802     + if zbxtg_settings.proxy_to_tg:
1803     + proxy_to_tg = zbxtg_settings.proxy_to_tg
1804     + if not proxy_to_tg.find("http") and not proxy_to_tg.find("socks"):
1805     + proxy_to_tg = "https://" + proxy_to_tg
1806     + tg.proxies = {
1807     + "https": "{0}".format(proxy_to_tg),
1808     + }
1809     +
1810     + zbx = ZabbixWeb(server=zbxtg_settings.zbx_server, username=zbxtg_settings.zbx_api_user,
1811     + password=zbxtg_settings.zbx_api_pass)
1812     +
1813     + zbx.tmp_dir = tmp_dir
1814     +
1815     + # workaround for Zabbix 4.x
1816     + zbx_version = 3
1817     +
1818     + try:
1819     + zbx_version = zbxtg_settings.zbx_server_version
1820     + except:
1821     + pass
1822     +
1823     + if zbxtg_settings.proxy_to_zbx:
1824     + zbx.proxies = {
1825     + "http": "http://{0}/".format(zbxtg_settings.proxy_to_zbx),
1826     + "https": "https://{0}/".format(zbxtg_settings.proxy_to_zbx)
1827     + }
1828     +
1829     + # https://github.com/ableev/Zabbix-in-Telegram/issues/55
1830     + try:
1831     + if zbxtg_settings.zbx_basic_auth:
1832     + zbx.basic_auth_user = zbxtg_settings.zbx_basic_auth_user
1833     + zbx.basic_auth_pass = zbxtg_settings.zbx_basic_auth_pass
1834     + except:
1835     + pass
1836     +
1837     + try:
1838     + zbx_api_verify = zbxtg_settings.zbx_api_verify
1839     + zbx.verify = zbx_api_verify
1840     + except:
1841     + pass
1842     +
1843     + map = Maps()
1844     + # api key to resolve address to coordinates via google api
1845     + try:
1846     + if zbxtg_settings.google_maps_api_key:
1847     + map.key = zbxtg_settings.google_maps_api_key
1848     + if zbxtg_settings.proxy_to_tg:
1849     + map.proxies = {
1850     + "http": "http://{0}/".format(zbxtg_settings.proxy_to_tg),
1851     + "https": "https://{0}/".format(zbxtg_settings.proxy_to_tg)
1852     + }
1853     + except:
1854     + pass
1855     +
1856     + zbxtg_body = (zbx_subject + "\n" + zbx_body).splitlines()
1857     + zbxtg_body_text = []
1858     +
1859     + for line in zbxtg_body:
1860     + if line.find(zbxtg_settings.zbx_tg_prefix) > -1:
1861     + setting = re.split("[\s:=]+", line, maxsplit=1)
1862     + key = setting[0].replace(zbxtg_settings.zbx_tg_prefix + ";", "")
1863     + if key not in settings_description:
1864     + if "--debug" in args:
1865     + print_message("[ERROR] There is no '{0}' method, use --features to get help".format(key))
1866     + continue
1867     + if settings_description[key]["type"] == "list":
1868     + value = setting[1].split(",")
1869     + elif len(setting) > 1 and len(setting[1]) > 0:
1870     + value = setting[1]
1871     + elif settings_description[key]["type"] == "bool":
1872     + value = True
1873     + else:
1874     + value = settings[settings_description[key]["name"]]
1875     + if key in settings_description:
1876     + settings[settings_description[key]["name"]] = value
1877     + else:
1878     + zbxtg_body_text.append(line)
1879     +
1880     + tg_method_image = bool(settings["tg_method_image"])
1881     + tg_chat = bool(settings["tg_chat"])
1882     + tg_group = bool(settings["tg_group"])
1883     + is_debug = bool(settings["is_debug"])
1884     + is_channel = bool(settings["is_channel"])
1885     + disable_web_page_preview = bool(settings["disable_web_page_preview"])
1886     + is_single_message = bool(settings["is_single_message"])
1887     +
1888     + # experimental way to send message to the group https://github.com/ableev/Zabbix-in-Telegram/issues/15
1889     + if args[0].split("/")[-1] == "zbxtg_group.py" or "--group" in args or tg_chat or tg_group:
1890     + tg_chat = True
1891     + tg_group = True
1892     + tg.type = "group"
1893     +
1894     + if "--debug" in args or is_debug:
1895     + is_debug = True
1896     + tg.debug = True
1897     + zbx.debug = True
1898     + print_message(tg.get_me())
1899     + print_message("Cache file with uids: " + tg.tmp_uids)
1900     + log_file = tmp_dir + ".debug." + hash_ts + ".log"
1901     + #print_message(log_file)
1902     +
1903     + if "--markdown" in args or settings["markdown"]:
1904     + tg.markdown = True
1905     +
1906     + if "--html" in args or settings["html"]:
1907     + tg.html = True
1908     +
1909     + if "--channel" in args or is_channel:
1910     + tg.type = "channel"
1911     +
1912     + if "--disable_web_page_preview" in args or disable_web_page_preview:
1913     + if is_debug:
1914     + print_message("'disable_web_page_preview' option has been enabled")
1915     + tg.disable_web_page_preview = True
1916     +
1917     + if "--graph_buttons" in args or settings["graph_buttons"]:
1918     + tg.image_buttons = True
1919     +
1920     + if "--forked" in args:
1921     + settings["forked"] = True
1922     +
1923     + if "--tg-key" in args:
1924     + tg.key = args[args.index("--tg-key") + 1]
1925     +
1926     + location_coordinates = {"latitude": None, "longitude": None}
1927     + if settings["lat"] > 0 and settings["lat"] > 0:
1928     + location_coordinates = {"latitude": settings["lat"], "longitude": settings["lon"]}
1929     + tg.location = location_coordinates
1930     + else:
1931     + if settings["location"]:
1932     + location_coordinates = map.get_coordinates_by_address(settings["location"])
1933     + if location_coordinates:
1934     + settings["lat"] = location_coordinates["latitude"]
1935     + settings["lon"] = location_coordinates["longitude"]
1936     + tg.location = location_coordinates
1937     +
1938     + if not os.path.isdir(tmp_dir):
1939     + if is_debug:
1940     + print_message("Tmp dir doesn't exist, creating new one...")
1941     + try:
1942     + os.makedirs(tmp_dir)
1943     + open(tg.tmp_uids, "a").close()
1944     + os.chmod(tmp_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
1945     + os.chmod(tg.tmp_uids, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
1946     + except:
1947     + tmp_dir = "/tmp"
1948     + if is_debug:
1949     + print_message("Using {0} as a temporary dir".format(tmp_dir))
1950     +
1951     + done_all_work_in_the_fork = False
1952     + # issue75
1953     +
1954     + to_types = ["to", "to_group", "to_channel"]
1955     + to_types_to_telegram = {"to": "private", "to_group": "group", "to_channel": "channel"}
1956     + multiple_to = {}
1957     + for i in to_types:
1958     + multiple_to[i]=[]
1959     +
1960     + for t in to_types:
1961     + try:
1962     + if settings[t] and not settings["forked"]:
1963     + # zbx_to = settings["to"]
1964     + multiple_to[t] = re.split(",", settings[t])
1965     + except KeyError:
1966     + pass
1967     +
1968     + # example:
1969     + # {'to_channel': [], 'to': ['usr1', 'usr2', 'usr3'], 'to_group': []}
1970     +
1971     + if (sum([len(v) for k, v in list(multiple_to.items())])) == 1:
1972     + # if we have only one recipient, we don't need fork to send message, just re-write "to" vaiable
1973     + tmp_max = 0
1974     + for t in to_types:
1975     + if len(multiple_to[t]) > tmp_max:
1976     + tmp_max = len(multiple_to[t])
1977     + tg.type = to_types_to_telegram[t]
1978     + zbx_to = multiple_to[t][0]
1979     + else:
1980     + for t in to_types:
1981     + for i in multiple_to[t]:
1982     + args_new = list(args)
1983     + args_new[1] = i
1984     + if t == "to_group":
1985     + args_new.append("--group")
1986     + args_new.append("--forked")
1987     + args_new.insert(0, sys.executable)
1988     + if is_debug:
1989     + print_message("Fork for custom recipient ({1}), new args: {0}".format(args_new,
1990     + to_types_to_telegram[t]))
1991     + subprocess.call(args_new)
1992     + done_all_work_in_the_fork = True
1993     +
1994     + if done_all_work_in_the_fork:
1995     + sys.exit(0)
1996     +
1997     + uid = None
1998     +
1999     + if tg.type == "channel":
2000     + uid = zbx_to
2001     + if tg.type == "private":
2002     + zbx_to = zbx_to.replace("@", "")
2003     +
2004     + if zbx_to.isdigit():
2005     + uid = zbx_to
2006     +
2007     + if not uid:
2008     + uid = tg.get_uid_from_cache(zbx_to)
2009     +
2010     + if not uid:
2011     + uid = tg.get_uid(zbx_to)
2012     + if uid:
2013     + tmp_need_update = True
2014     + if not uid:
2015     + tg.error_need_to_contact(zbx_to)
2016     + sys.exit(1)
2017     +
2018     + if tmp_need_update:
2019     + tg.update_cache_uid(zbx_to, str(uid).rstrip())
2020     +
2021     + if is_debug:
2022     + print_message("Telegram uid of {0} '{1}': {2}".format(tg.type, zbx_to, uid))
2023     +
2024     + # add signature, turned off by default, you can turn it on in config
2025     + try:
2026     + if "--signature" in args or settings["signature"] or zbxtg_settings.zbx_tg_signature\
2027     + and not "--signature_disable" in args and not settings["signature_disable"]:
2028     + if "--signature" in args:
2029     + settings["signature"] = args[args.index("--signature") + 1]
2030     + if not settings["signature"]:
2031     + settings["signature"] = zbxtg_settings.zbx_server
2032     + zbxtg_body_text.append("--")
2033     + zbxtg_body_text.append(settings["signature"])
2034     + except:
2035     + pass
2036     +
2037     + # replace text with emojis
2038     + internal_using_emoji = False # I hate that, but... https://github.com/ableev/Zabbix-in-Telegram/issues/152
2039     + if hasattr(zbxtg_settings, "emoji_map"):
2040     + zbxtg_body_text_emoji_support = []
2041     + for l in zbxtg_body_text:
2042     + l_new = l
2043     + for k, v in list(zbxtg_settings.emoji_map.items()):
2044     + l_new = l_new.replace("{{" + k + "}}", v)
2045     + zbxtg_body_text_emoji_support.append(l_new)
2046     + if len("".join(zbxtg_body_text)) - len("".join(zbxtg_body_text_emoji_support)):
2047     + internal_using_emoji = True
2048     + zbxtg_body_text = zbxtg_body_text_emoji_support
2049     +
2050     + if not is_single_message:
2051     + tg.send_message(uid, zbxtg_body_text)
2052     + if not tg.ok:
2053     + # first case – if group has been migrated to a supergroup, we need to update chat_id of that group
2054     + if tg.error.find("migrated") > -1 and tg.error.find("supergroup") > -1:
2055     + migrate_to_chat_id = tg.result["parameters"]["migrate_to_chat_id"]
2056     + tg.update_cache_uid(zbx_to, migrate_to_chat_id, message="Group chat is migrated to supergroup, "
2057     + "updating cache file")
2058     + uid = migrate_to_chat_id
2059     + tg.send_message(uid, zbxtg_body_text)
2060     +
2061     + # another case if markdown is enabled and we got parse error, try to remove "bad" symbols from message
2062     + if tg.markdown and tg.error.find("Can't find end of the entity starting at byte offset") > -1:
2063     + markdown_warning = "Original message has been fixed due to {0}. " \
2064     + "Please, fix the markdown, it's slowing down messages sending."\
2065     + .format(url_wiki_base + "/" + settings_description["markdown"]["url"])
2066     + markdown_fix_attempts = 0
2067     + while not tg.ok and markdown_fix_attempts != 3:
2068     + offset = re.search("Can't find end of the entity starting at byte offset ([0-9]+)", tg.error).group(1)
2069     + zbxtg_body_text = markdown_fix(zbxtg_body_text, offset, emoji=internal_using_emoji) + \
2070     + ["\n"] + [markdown_warning]
2071     + tg.disable_web_page_preview = True
2072     + tg.send_message(uid, zbxtg_body_text)
2073     + markdown_fix_attempts += 1
2074     + if tg.ok:
2075     + print_message(markdown_warning)
2076     +
2077     + if is_debug:
2078     + print((tg.result))
2079     +
2080     + if settings["zbxtg_image_age"]:
2081     + age_sec = age2sec(settings["zbxtg_image_age"])
2082     + if age_sec > 0 and age_sec > 3600:
2083     + settings["zbxtg_image_period"] = age_sec
2084     +
2085     + message_id = 0
2086     + if tg_method_image:
2087     + zbx.login()
2088     + if not zbx.cookie:
2089     + text_warn = "Login to Zabbix web UI has failed (web url, user or password are incorrect), "\
2090     + "unable to send graphs check manually"
2091     + tg.send_message(uid, [text_warn])
2092     + print_message(text_warn)
2093     + else:
2094     + if not settings["extimg"]:
2095     + zbxtg_file_img = zbx.graph_get(settings["zbxtg_itemid"], settings["zbxtg_image_period"],
2096     + settings["zbxtg_title"], settings["zbxtg_image_width"],
2097     + settings["zbxtg_image_height"], version=zbx_version)
2098     + else:
2099     + zbxtg_file_img = external_image_get(settings["extimg"], tmp_dir=zbx.tmp_dir)
2100     + zbxtg_body_text, is_modified = list_cut(zbxtg_body_text, 200)
2101     + if tg.ok:
2102     + message_id = tg.result["result"]["message_id"]
2103     + tg.reply_to_message_id = message_id
2104     + if not zbxtg_file_img:
2105     + text_warn = "Can't get graph image, check script manually, see logs, or disable graphs"
2106     + tg.send_message(uid, [text_warn])
2107     + print_message(text_warn)
2108     + else:
2109     + if not is_single_message:
2110     + zbxtg_body_text = ""
2111     + else:
2112     + if is_modified:
2113     + text_warn = "probably you will see MEDIA_CAPTION_TOO_LONG error, "\
2114     + "the message has been cut to 200 symbols, "\
2115     + "https://github.com/ableev/Zabbix-in-Telegram/issues/9"\
2116     + "#issuecomment-166895044"
2117     + print_message(text_warn)
2118     + if not is_single_message:
2119     + tg.disable_notification = True
2120     + tg.send_photo(uid, zbxtg_body_text, zbxtg_file_img)
2121     + if tg.ok:
2122     + settings["zbxtg_body_text"] = zbxtg_body_text
2123     + os.remove(zbxtg_file_img)
2124     + else:
2125     + if tg.error.find("PHOTO_INVALID_DIMENSIONS") > -1:
2126     + if not tg.disable_web_page_preview:
2127     + tg.disable_web_page_preview = True
2128     + text_warn = "Zabbix user couldn't get graph (probably has no rights to get data from host), " \
2129     + "check script manually, see {0}".format(url_wiki_base + "/" +
2130     + settings_description["graphs"]["url"])
2131     + tg.send_message(uid, [text_warn])
2132     + print_message(text_warn)
2133     + if tg.location and location_coordinates["latitude"] and location_coordinates["longitude"]:
2134     + tg.reply_to_message_id = message_id
2135     + tg.disable_notification = True
2136     + tg.send_location(to=uid, coordinates=location_coordinates)
2137     +
2138     + if "--show-settings" in args:
2139     + print_message("Settings: " + str(json.dumps(settings, indent=2)))
2140     +
2141     +if __name__ == "__main__":
2142     + main()
2143     diff -Nur --no-dereference smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg_settings.example.py smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg_settings.example.py
2144     --- smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg_settings.example.py 1969-12-31 19:00:00.000000000 -0500
2145     +++ smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg_settings.example.py 2021-11-08 22:03:56.386000000 -0500
2146     @@ -0,0 +1,68 @@
2147     +# -*- coding: utf-8 -*-
2148     +
2149     +tg_key = "XYZ" # telegram bot api key
2150     +
2151     +zbx_tg_prefix = "zbxtg" # variable for separating text from script info
2152     +zbx_tg_tmp_dir = "/var/tmp/" + zbx_tg_prefix # directory for saving caches, uids, cookies, etc.
2153     +zbx_tg_signature = False
2154     +
2155     +zbx_tg_update_messages = True
2156     +zbx_tg_matches = {
2157     + "problem": "PROBLEM: ",
2158     + "ok": "OK: "
2159     +}
2160     +
2161     +zbx_server = "http://127.0.0.1/zabbix/" # zabbix server full url
2162     +zbx_api_user = "api"
2163     +zbx_api_pass = "api"
2164     +zbx_api_verify = True # True - do not ignore self signed certificates, False - ignore
2165     +
2166     +#zbx_server_version = 2 # for Zabbix 2.x version
2167     +zbx_server_version = 3 # for Zabbix 3.x version, by default, not everyone updated to 4.x yet
2168     +#zbx_server_version = 4 # for Zabbix 4.x version, default will be changed in the future with this
2169     +
2170     +zbx_basic_auth = False
2171     +zbx_basic_auth_user = "zabbix"
2172     +zbx_basic_auth_pass = "zabbix"
2173     +
2174     +proxy_to_zbx = None
2175     +proxy_to_tg = None
2176     +
2177     +# proxy_to_zbx = "http://proxy.local:3128"
2178     +# proxy_to_tg = "https://proxy.local:3128"
2179     +
2180     +# proxy_to_tg = "socks5://user1:password2@hostname:port" # socks5 with username and password
2181     +# proxy_to_tg = "socks5://hostname:port" # socks5 without username and password
2182     +# proxy_to_tg = "socks5h://hostname:port" # hostname resolution on SOCKS proxy.
2183     + # This helps when internet provider alter DNS queries.
2184     + # Found here: https://stackoverflow.com/a/43266186/957508
2185     +
2186     +google_maps_api_key = None # get your key, see https://developers.google.com/maps/documentation/geocoding/intro
2187     +
2188     +zbx_tg_daemon_enabled = False
2189     +zbx_tg_daemon_enabled_ids = [6931850, ]
2190     +zbx_tg_daemon_enabled_users = ["ableev", ]
2191     +zbx_tg_daemon_enabled_chats = ["Zabbix in Telegram Script", ]
2192     +
2193     +zbx_db_host = "localhost"
2194     +zbx_db_database = "zabbix"
2195     +zbx_db_user = "zbxtg"
2196     +zbx_db_password = "zbxtg"
2197     +
2198     +
2199     +emoji_map = {
2200     + "Disaster": "đŸ”Ĩ",
2201     + "High": "🛑",
2202     + "Average": "❗",
2203     + "Warning": "⚠ī¸",
2204     + "Information": "ℹī¸",
2205     + "Not classified": "🔘",
2206     + "OK": "✅",
2207     + "PROBLEM": "❗",
2208     + "info": "ℹī¸",
2209     + "WARNING": "⚠ī¸",
2210     + "DISASTER": "❌",
2211     + "bomb": "đŸ’Ŗ",
2212     + "fire": "đŸ”Ĩ",
2213     + "hankey": "💩",
2214     +}

admin@koozali.org
ViewVC Help
Powered by ViewVC 1.2.1 RSS 2.0 feed