#!/usr/bin/python # coding:utf-8 # ServerSan - main.py # 2018/3/15 13:43 # __author__ = 'Benny <benny@bennythink.com>' __version__ = '1.0.0' import os import sys import time from base64 import b64encode from typing import List, Any, Union import pymongo import telebot from telebot import types from apscheduler.schedulers.background import BackgroundScheduler try: bot = telebot.TeleBot(sys.argv[1]) except IndexError as e: print('Usage: python main.py token') sys.exit(1) client = pymongo.MongoClient() db = client['ServerSan'] user_col = db['user'] sysinfo_col = db['sysinfo'] log_col = db['loginfo'] @bot.message_handler(commands=['start']) def bot_start(message): bot.send_chat_action(message.chat.id, 'typing') bot.send_message(message.chat.id, u'Welcome %s!đ\n' 'ServerSan is a server status monitor Telegram Bot. ' 'This bot is under actively development, ' 'and *DOES NOT* provide any reliability for now.\n' 'Please click /help for support.' % message.chat.first_name, parse_mode='Markdown') @bot.message_handler(commands=['help']) def bot_help(message): bot.send_chat_action(message.chat.id, 'typing') bot.send_message(message.chat.id, "Developer @BennyThink \n" "GitHub https://github.com/BennyThink/ServerSan \n" "Feel free to open an issue or talk to me directly!") @bot.message_handler(commands=['add']) def add_server(message): server_token = generate_token() if message.chat.last_name is None: message.chat.last_name = '' user_info = user_col.find_one({'userID': message.chat.id}) if user_info is None: user_info = dict(userID=message.chat.id, username=message.chat.username, nickname=message.chat.first_name + message.chat.last_name, role=1000, server=[server_token], notify=1) else: server_block = user_info['server'] server_block.append(server_token) user_info.update(server=server_block) if user_col.update_one({'userID': message.chat.id}, {'$set': user_info}, upsert=True).acknowledged: bot.send_chat_action(message.chat.id, 'typing') bot.send_message(message.chat.id, "Execute the following command(unused server will be deleted within 30 minutes):\n\n" "`wget https://raw.githubusercontent.com/BennyThink/ServerSan/master/install.sh" "&& bash install.sh %s`" % server_token, parse_mode='Markdown') else: bot.send_chat_action(message.chat.id, 'typing') bot.send_message(message.chat.id, 'Something is going wrong') @bot.message_handler(commands=['stat']) def stat(message): markup = create_server_markup(message.chat.id, 'stat') if markup.to_dic()['inline_keyboard']: bot.send_message(message.chat.id, 'Which server do you want to view?', reply_markup=markup) else: bot.send_message(message.chat.id, 'Oww you don\'t have any servers, click /add to add one.') @bot.message_handler(commands=['delete']) def delete(message): markup = create_server_markup(message.chat.id, 'delete') if markup.to_dic()['inline_keyboard']: bot.send_message(message.chat.id, 'Which server do you want to delete?', reply_markup=markup) else: bot.send_message(message.chat.id, 'Oww you don\'t have any servers.') @bot.message_handler(commands=['settings']) def settings(message): markup = types.ReplyKeyboardMarkup() itembtn1 = types.KeyboardButton('âYes') itembtn2 = types.KeyboardButton('đNo') markup.add(itembtn1, itembtn2) bot.send_message(message.chat.id, 'Do you wish to enable notifications?', reply_markup=markup) @bot.message_handler() def common(message): markup = types.ReplyKeyboardRemove(selective=False) if message.text == u'âYes': user_col.update_one({'userID': message.chat.id}, {"$set": {'notify': 1}}) bot.send_message(message.chat.id, 'Your settings has been saved.', reply_markup=markup) elif message.text == u'đNo': user_col.update_one({'userID': message.chat.id}, {"$set": {'notify': 0}}) bot.send_message(message.chat.id, 'Your settings has been saved.', reply_markup=markup) else: bot.send_message(message.chat.id, message.text, reply_markup=markup, parse_mode='HTML') @bot.callback_query_handler(func=lambda call: True) def callback_handle(call): if 'stat' in call.data: info, notify = parse_data(get_user_server(call.message.chat.id)[int(call.data.split()[1])]) bot.answer_callback_query(call.id, 'Your info for %s' % notify) bot.send_message(call.message.chat.id, info, parse_mode='HTML') elif 'delete' in call.data: # delete is two step, delete server block and systat info = get_user_server(call.message.chat.id)[int(call.data.split()[1])] # type: Union[dict] user_id = call.message.chat.id auth_code = info['auth'] bot.answer_callback_query(call.id, 'Deleting your server %s' % info['hostname']) msg = 'Your server has been deleted. Run the following commands on your server:\n\n' + \ '''<code>rm -R /etc/serversan && (crontab -u serversan -l | grep -v "/etc/serversan/ss-agent.py") \ |crontab -u serversan - && userdel serversan</code>''' \ if delete_server(user_id, auth_code) else 'Something\'s wrong' markup = create_server_markup(user_id, 'delete') bot.edit_message_reply_markup(user_id, call.message.message_id, reply_markup=markup) bot.send_message(call.message.chat.id, msg, parse_mode='HTML') def generate_token(): return b64encode(os.urandom(16)) def create_server_markup(chat_id, op): """ create server information markup based on different operation :param chat_id: chat_id from which conversation :param op: delete or stat :return: 2 lines in a row markup """ one_latest_server = get_user_server(chat_id) # type: List[Union[dict, Any]] btn_list = [] count = get_effective_count(chat_id) size = 2 markup = types.InlineKeyboardMarkup(size) for index in range(0, count): btn_list.append(types.InlineKeyboardButton( "%s %s" % (one_latest_server[index]['hostname'], one_latest_server[index]['ip']), callback_data='%s %s' % (op, index))) for i in range(0, len(btn_list), size): part = btn_list[i:i + size] if len(part) == 3: markup.add(part[0], part[1], part[2]) elif len(part) == 2: markup.add(part[0], part[1]) else: markup.add(part[0]) return markup def delete_server(_id, auth_code): server_block = user_col.find_one({'userID': _id})['server'] server_block.pop(server_block.index(auth_code)) r1 = sysinfo_col.delete_many({'auth': auth_code}).acknowledged r2 = user_col.update_one({'userID': _id}, {"$set": {'server': server_block}}).acknowledged if r1 and r2: return True def parse_data(latest_info): """ parse dict to string. :param latest_info: the latest info about one server :return: string to be sent """ return u'''Your server <code>%s</code> on %s as of %s info:\n System Uptime: %s OS: %s Kernel: %s Process: %s Sessions: <code>%s</code> CPU Model: %s CPU Speed: %sx %s GHz Network Activityââ:%s KiB/s %s KiB/s CPU utilization: <b>%s</b> %% RAM: %s MiB of %s MiB , <b>%s%%</b> SWAP: %s MiB of %s MiB , <b>%s%%</b> Disk: %s GiB of %s GiB, <b>%s%%</b> Top Process: \n%s''' % (latest_info['hostname'], latest_info['ip'], time.strftime('%Y-%m-%d %H:%M', time.localtime(latest_info['timestamp'])), parse_uptime(latest_info['uptime']), latest_info['os'][0], latest_info['os'][1], latest_info['pro'], latest_info['session'], latest_info['cpu'][0], latest_info['cpu'][1], latest_info['cpu'][2], latest_info['network'][0], latest_info['network'][1], # TODO: calculate total incoming and outgoing. latest_info['percent'], latest_info['mem'][0], latest_info['mem'][1], latest_info['mem'][2], latest_info['swap'][0], latest_info['swap'][1], latest_info['swap'][2], latest_info['disk'][0], latest_info['disk'][1], latest_info['disk'][2], latest_info['top']), latest_info['hostname'] def server_group(auth_code): """ return all the server information under one auth code :param auth_code: auth code from user_info :return: [{block1},{block2}] """ return [i for i in sysinfo_col.find({'auth': auth_code})] def get_effective_count(_id): return len([auth for auth in user_col.find_one({'userID': _id})['server'] if sysinfo_col.find_one({'auth': auth})]) def get_user_server(_id): """ get designated user's all latest server :param _id: user_id :return: [{},{}] for the newest information on one server """ info = 'No information' all_server = [] # user_server contains an user's all auth token user_server = [t for i in user_col.find({'userID': _id}) for t in i.get('server')] for token in user_server: for info in sysinfo_col.find({'auth': token}): pass all_server.append(info) return all_server def parse_uptime(boot_time): p1 = time.strftime("%Y-%m-%d %H:%M", time.localtime(boot_time)) + ' up, ' uptime = time.time() - boot_time day = uptime // 3600 // 24 hour = uptime - day * 3600 * 24 minute = hour - hour // 3600 * 3600 return p1 + '%d days %d:%d' % (day, hour // 3600, minute // 60) def del_unused_server(): blocks = [(j, i['userID']) for i in user_col.find() for j in i['server']] for i in blocks: if sysinfo_col.find_one({'auth': i[0]}) is None: delete_server(i[1], i[0]) def resource_warning(): auth_list = list(set([i['auth'] for i in sysinfo_col.find()])) data = None latest_each = [] for i in auth_list: for j in sysinfo_col.find({'auth': i}): data = j latest_each.append(data) for each in latest_each: if time.time() - each['timestamp'] > 900: warning_send(u'â Warningâ \nYour server %s has lost connection for 900 seconds' % each['hostname'], each['auth']) elif each['percent'] > 95.0 or each['mem'][2] > 90.0 or each['swap'][2] > 60.0: warning_send(u'''â Warningâ \nYour server %s is consuming a lot of resources. CPU utilization: %s %% Ram usage: %s %% Swap usage: %s %% Disk usage: %s %% ------------------- Top process:\n%s''' % ( each['hostname'], each['percent'], each['mem'][2], each['swap'][2], each['disk'][2], each['top']), each['auth']) elif each['disk'][2] > 80.0: warning_send(u'''â Warningâ \nYour server %s is not having enough free disk space. Current disk status is showing as follows: Used: %s GiB All: %s GiB Percentage: %s %%''' % ( each['hostname'], each['disk'][0], each['disk'][1], each['disk'][2]), each['auth']) def warning_send(msg, auth): log = log_col.find_one({'auth': auth}) or {'timestamp': 0} last_sent = log['timestamp'] for i in user_col.find(): # 14400 seconds: sent alert , 4 hours if auth in i['server'] and i['notify'] and (time.time() - last_sent) > 14400: bot.send_message(i['userID'], msg) log_col.update_one({'auth': auth}, {'$set': {'timestamp': time.time()}}, upsert=True) def del_old_records(sec): for i in sysinfo_col.find(): if time.time() - i['timestamp'] > sec: sysinfo_col.delete_one({'timestamp': i['timestamp']}) if __name__ == '__main__': scheduler = BackgroundScheduler() scheduler.add_job(del_unused_server, 'interval', minutes=30) scheduler.add_job(resource_warning, 'interval', minutes=10) # scheduler.add_job(del_old_records, 'cron', day='*/180', args=[180 * 24 * 3600]) scheduler.start() bot.polling(none_stop=True)