<filename>SpicePyBot.py
# ======================
# general python modules
# ======================
import time
import logging
from functools import wraps
import os
import sys
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from threading import Thread

# ===================
# module from SpicePy
# ===================
import spicepy.netlist as ntl
from spicepy.netsolve import net_solve

# ==========================
# python-temegam-bot modules
# ==========================
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
import telegram as telegram

# ===============================
# create necessary folders
# ===============================
if not os.path.exists('users'):
    os.makedirs('users')

# ===============================
# admin list
# ===============================
fid = open('./admin_only/admin_list.txt', 'r')
LIST_OF_ADMINS = [int(adm) for adm in fid.readline().split()]
fid.close()


# ==========================
# Logging
# ==========================
# define filter to log only one level
class MyFilter(object):
    def __init__(self, level):
        self.__level = level

    def filter(self, logRecord):
        return logRecord.levelno <= self.__level


# formatter
fmt = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# StatLog: log how the log is used (only INFO)
StatLog = logging.getLogger('StatLog')
h1 = logging.FileHandler('StatBot.log')
h1.setFormatter(fmt)
StatLog.addHandler(h1)
StatLog.setLevel(logging.INFO)
StatLog.addFilter(MyFilter(logging.INFO))

# SolverLog: catch error in the netlist (>= WARNING)
SolverLog = logging.getLogger('SolverLog')
h2 = logging.FileHandler('SolverLog.log')
h2.setFormatter(fmt)
SolverLog.addHandler(h2)

# OtherLog: catch all other error using the dispatcher (>= WARNING)
OtherLog = logging.getLogger('OtherLog')
h3 = logging.FileHandler('OtherLog.log')
h3.setFormatter(fmt)
OtherLog.addHandler(h3)


# ==========================
# useful functions
# ==========================
# The following function reads the TOKEN from a file.
# This file is not incuded in the github-repo for obvious reasons
def read_token(filename):
    """
    'read_token' reads the bot token from a text file.

    :param filename: filename of the file including the token
    :return: string with the token
    """
    with open(filename) as f:
        token = f.readline().replace('\n', '')
    return token


# error_callback to log uncaught error
def error_callback(update, context):
    """
    'error_callback' log uncaught error

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    OtherLog.error("Update {} caused error {}".format(update, context.error))


# compute the solution
def get_solution(fname, update, context):
    """
    'get_solution' computes the solution of a network using SpicePy

    :param fname: filename with the netlist
    :param update: bot update
    :param context: CallbackContext
    :return:
        * mex:  solution formatted in a string
    """
    try:
        # create network and solve it
        net = ntl.Network(fname)

        # check if number of nodes exceeds the limit
        NMAX = 40
        if net.node_num > NMAX:
            mex = "Your netlist includes more than {:d} nodes.\n".format(net.node_num)
            mex += "*The maximum allowed number on this bot is {:d}.*\n".format(NMAX)
            mex += "Please reduce the number of nodes or take a look to the computational core of this bot "
            mex += "that does not have this limitation:\n"
            mex += "[SpicePy project](https://github.com/giaccone/SpicePy)"

        else:

            # limit sample for .tran to 2000 (max)
            if net.analysis[0].lower() == '.tran':
                Nsamples = float(net.convert_unit(net.analysis[2])) / float(net.convert_unit(net.analysis[1]))
                if Nsamples > 2000:
                    step = float(net.convert_unit(net.analysis[2]))/1999
                    net.analysis[1] = '{:.3e}'.format(step)

                    mex = "Your netlits defines a '.tran' analysis with *{:d}* samples\n".format(int(Nsamples))
                    mex += "Since this bot runs on a limited hardware shared by many users\n"
                    mex += "The analysis has been limited to *2000* samples:\n"
                    mex += "`.tran " + net.analysis[1] + " " + net.analysis[-1] + "`"


                    context.bot.send_message(chat_id=update.message.chat_id,
                                     text=mex,
                                     parse_mode=telegram.ParseMode.MARKDOWN)

            # limit sample for .ac to 2000 (max)
            elif net.analysis[0].lower() == '.ac':
                # get frequencies
                net.frequency_span()

                if not np.isscalar(net.f):
                    # get Nsamples
                    Nsamples = len(net.f)
                    # limit di 2000 max
                    if Nsamples > 2000:
                        scale = 2000 / Nsamples
                        old_analysys = "`" + " ".join(net.analysis) + "`"
                        net.analysis[2] = str(int(np.ceil(scale * float(net.convert_unit(net.analysis[2])))))
                        new_analysys = "`" + " ".join(net.analysis) + "`"

                        mex = "Your netlits defines a '.tran' analysis with *{:d}* samples\n".format(int(Nsamples))
                        mex += "Since this bot runs on a limited hardware shared by many users\n"
                        mex += "The analysis has been limited to *2000* samples:\n"
                        mex += "original analysis: " + old_analysys + "\n"
                        mex += "ner analysis: " + new_analysys + "\n"

                        context.bot.send_message(chat_id=update.message.chat_id,
                                         text=mex,
                                         parse_mode=telegram.ParseMode.MARKDOWN)

            # get configurations
            fname = './users/' + str(update.message.chat_id) + '.cnf'
            fid = open(fname, 'r')
            flag = fid.readline()[:-1]  # read nodal_pot conf
            nodal_pot = flag == 'True'
            flag = fid.readline()[:-1]  # read polar conf
            polar = flag == 'True'
            flag = fid.readline()  # read dB conf
            dB = flag == 'True'

            if net.analysis[0] == '.op':
                # forcepolar to False for .op problems
                polar = False

            # solve the network
            net_solve(net)

            # .op and .ac (single-frequency): prepare mex to be printed
            if (net.analysis[0].lower() == '.op') | ((net.analysis[0].lower() == '.ac') & (np.isscalar(net.f))):
                # get branch quantities
                net.branch_voltage()
                net.branch_current()
                net.branch_power()

                # prepare message
                mex = net.print(polar=polar, message=True)
                mex = mex.replace('==============================================\n'
                                  '               branch quantities'
                                  '\n==============================================\n', '*branch quantities*\n`')
                mex = mex.replace('----------------------------------------------', '')
                mex += '`'

                # if the user wants node potentials add it to mex
                if nodal_pot:
                    # create local dictionary node-number 2 node-label
                    num2node_label = {num: name for name, num in net.node_label2num.items() if name != '0'}

                    # compute the node potentials
                    mex0 = '*node potentials*\n`'
                    for num in num2node_label:
                        voltage = net.get_voltage('(' + num2node_label[num] + ')')[0]
                        if polar:
                            mex0 += 'v(' + num2node_label[num] + ') = {:10.4f} V < {:10.4f}°\n'.format(np.abs(voltage),np.angle(voltage) * 180 / np.pi)
                        else:
                            mex0 += 'v(' + num2node_label[num] + ') = {:10.4f} V\n'.format(voltage)


                    # add newline
                    mex0 += '`\n\n'

                    # add node potentials before branch quantities
                    mex = mex0 + mex

            elif net.analysis[0].lower() == '.tran':
                hf = net.plot(to_file=True, filename='./users/tran_plot_' + str(update.message.chat_id) + '.png',dpi_value=150)
                mex = None
                plt.close(hf)

            elif net.analysis[0].lower() == '.ac':
                hf = net.bode(to_file=True, decibel=dB, filename='./users/bode_plot_' + str(update.message.chat_id) + '.png', dpi_value=150)
                mex = None
                if isinstance(hf, list):
                    for fig in hf:
                        plt.close(fig)
                else:
                    plt.close(hf)

            # Log every time a network is solved
            # To make stat it is saved the type of network and the UserID
            StatLog.info('Analysis: ' + net.analysis[0] + ' - UserID: ' + str(update.effective_user.id))

        return net, mex

    except:
        # set network to None
        net = None
        # read network with issues
        wrong_net = ''
        with open(fname) as f:
            for line in f:
                wrong_net += line
        wrong_net = wrong_net.replace('\n', '  /  ')

        # log error
        SolverLog.error('UserID: ' + str(update.effective_user.id) + ' - Netlist error: ' + wrong_net)
        return net, "*Something went wrong with your netlist*.\nPlease check the netlist format."


# ==========================
# restriction decorator
# ==========================
def restricted(func):
    """
    'restricted' decorates a function so that it can be used only to allowed users

    :param func: function to be decorated
    :return: function wrapper
    """
    @wraps(func)
    def wrapped(update, context, *args, **kwargs):
        user_id = update.effective_user.id
        if user_id not in LIST_OF_ADMINS:
            print("Unauthorized access denied for {}.".format(user_id))
            context.bot.send_message(chat_id=update.message.chat_id, text="You are not authorized to run this command")
            return
        return func(update, context, *args, **kwargs)
    return wrapped


# ==========================
# block group decorator
# ==========================
def block_group(func):
    """
    'block_group' decorates functions so that they can't be used in telegram groups

    :param func: function to be decorated
    :return: function wrapper
    """
    @wraps(func)
    def wrapped(update, context, *args, **kwargs):
        # skip requests from groups
        if update.message.chat_id < 0:
            mex = "This bot is for personal use only.\n"
            mex += "*Please remove it from this group*\n"
            context.bot.send_message(chat_id=update.message.chat_id, text=mex,
                             parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)
            return
        return func(update, context, *args, **kwargs)
    return wrapped


# ==========================
# start - welcome message
# ==========================
@block_group
def start(update, context):
    """
    'start' provides the start message

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    msg = "*Welcome to SpicePyBot*.\n\n"
    msg += "It allows you to solve linear:\n"
    msg += "  \* DC networks (.op)\n"
    msg += "  \* AC networks (.ac)\n"
    msg += "  \* dynamic networks (.tran)\n\n"
    msg += "Run the code:\n"
    msg += "`/help`:  to have a short guide.\n\n"
    msg += "or\n\n"
    msg += "Read the full [tutorial](https://github.com/giaccone/SpicePyBot/wiki) if "
    msg += "you are completely new to this subject."

    context.bot.send_message(chat_id=update.message.chat_id,
                     text=msg,
                     parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)
    fname = './users/' + str(update.message.chat_id) + '.cnf'
    fid = open(fname, 'w')
    fid.write('False\n')  # this is for the node potential
    fid.write('False\n')  # this is for the polar flag
    fid.write('False')    # this is for the decibel flag
    fid.close()


# =========================================
# catch netlist from a file sent to the bot
# =========================================
@block_group
def catch_netlist(update, context):
    """
    'catch_netlist' get a netlist in a text file and provide the results.

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    # if current user don't have cnf file create it
    if not os.path.exists('./users/' + str(update.message.chat_id) + '.cnf'):
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write('False\n')  # this is for the node potential
        fid.write('False\n')  # this is for the polar flag
        fid.write('False')    # this is for the decibel flag
        fid.close()

    # catch the netlist from file
    file = context.bot.getFile(update.message.document.file_id)
    fname = './users/' + str(update.message.chat_id) + '.txt'
    file.download(fname)

    # send the netlist for double check to user
    mex = 'This is your netlist:\n\n'
    with open(fname) as f:
        for line in f:
            mex += line
    context.bot.send_message(chat_id=update.message.chat_id, text=mex)

    # compute solution
    net, mex = get_solution(fname, update, context)

    # typing
    context.bot.send_chat_action(chat_id=update.message.chat_id, action=telegram.ChatAction.TYPING)

    if mex is None:    # in case of .tran or .ac-multi-freq mex is none, hence send the plot
        if net.analysis[0].lower() == '.tran':
            context.bot.send_photo(chat_id=update.message.chat_id,
                           photo=open('./users/tran_plot_' + str(update.message.chat_id) + '.png', 'rb'))
        elif net.analysis[0].lower() == '.ac':
            N = int(len(net.tf_cmd.split()[1:]) / 2)
            if N == 1:
                context.bot.send_photo(chat_id=update.message.chat_id,
                               photo=open('./users/bode_plot_' + str(update.message.chat_id) + '.png', 'rb'))
            else:
                for k in range(N):
                    context.bot.send_photo(chat_id=update.message.chat_id,
                                   photo=open(
                                       './users/bode_plot_' + str(update.message.chat_id) + '_' + str(k) + '.png',
                                       'rb'))

    else:    # otherwise print results
        mex = 'Please remember that all components are analyzed with *passive sign convention*.\nHere you have  ' \
              '*the circuit solution*.\n\n' + mex
        context.bot.send_message(chat_id=update.message.chat_id, text=mex,
                         parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)


# ==========================
# help - short guide
# ==========================
@block_group
def help(update, context):
    """
    'help' provides information about the use of the bot

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    msg = "*Very short guide*.\n\n" #1)upload a file with the netlist (don't know what a netlist is? Run `/tutorial` in the bot)\n2) enjoy\n\n\n*If you need a more detailed guide*\nRun `/tutorial` in the bot"
    msg += "The Bot makes use of netlists to describe circuits. If you do not know what "
    msg += "a netlist is, please refer to  SpicePy "
    msg += "[documentation](https://github.com/giaccone/SpicePy/wiki/User's-guide)"
    msg += " and [examples](https://github.com/giaccone/SpicePy/wiki/Examples).\n\n"
    msg += "Assuming that you know how to describe a circuit by means of a netlist, you can either:\n\n"
    msg += "1) use the command `/netlist` and write the netlist directly to the Bot (i.e. chatting with the BOT)\n\n"
    msg += "or\n\n"
    msg += "2) send a text file to the Bot including the netlist. The Bot will catch it and it'll solve it.\n\n"
    msg += "*Finally*\n"
    msg += "read the full [tutorial](https://github.com/giaccone/SpicePyBot/wiki) if "
    msg += "you are completely new to this subject."
    context.bot.send_message(chat_id=update.message.chat_id,
                     text=msg,
                     parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)


# =========================================
# netlist - write te netlist in the BOT
# =========================================
@block_group
def netlist(update, context):
    """
    'netlist' tell to the bot that the used intend to send a netlist via text message

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    # if current user don't have cnf file create it
    if not os.path.exists('./users/' + str(update.message.chat_id) + '.cnf'):
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write('False\n')  # this is for the node potential
        fid.write('False\n')  # this is for the polar flag
        fid.write('False')    # this is for the decibel flag
        fid.close()

    open("./users/" + str(update.message.chat_id) + "_waitnetlist", 'w').close()
    context.bot.send_message(chat_id=update.message.chat_id, text="Please write the netlist\nAll in one message.")


# =========================================
# reply - catch any message and reply to it
# =========================================
@block_group
def reply(update, context):
    """
    'reply' provides the result to a netlist send via text message. If /netlist is not
    used before sending the netlist, a funny message is sent.

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    # check call to /netlist
    if os.path.exists("./users/" + str(update.message.chat_id) + "_waitnetlist"):
        # write the netlist
        fname = "./users/" + str(update.message.chat_id) + ".txt"
        fid = open(fname, "w")
        fid.write(str(update.message.text) + '\n')
        fid.close()

        # remove waitnetlist file for this user
        os.remove("./users/" + str(update.message.chat_id) + "_waitnetlist")

        # send the netlist for double check to user
        mex = 'This is your netlist:\n\n'
        with open(fname) as f:
            for line in f:
                mex += line
        context.bot.send_message(chat_id=update.message.chat_id, text=mex)

        # compute solution
        net, mex = get_solution(fname, update, context)

        # typing
        context.bot.send_chat_action(chat_id=update.message.chat_id, action=telegram.ChatAction.TYPING)

        if mex is None:  # in case of .tran or .ac-multi-freq mex is none, hence send the plot
            if net.analysis[0].lower() == '.tran':
                context.bot.send_photo(chat_id=update.message.chat_id,
                               photo=open('./users/tran_plot_' + str(update.message.chat_id) + '.png', 'rb'))
            elif net.analysis[0].lower() == '.ac':
                N = int(len(net.tf_cmd.split()[1:]) / 2)
                if N == 1:
                    context.bot.send_photo(chat_id=update.message.chat_id,
                                   photo=open('./users/bode_plot_' + str(update.message.chat_id) + '.png', 'rb'))
                else:
                    for k in range(N):
                        context.bot.send_photo(chat_id=update.message.chat_id,
                                       photo=open(
                                           './users/bode_plot_' + str(update.message.chat_id) + '_' + str(k) + '.png',
                                           'rb'))

        else:    # otherwise print results
            mex = 'Please remember that all components are analyzed with *passive sign convention*.\nHere you have  ' \
                  '*the circuit solution*.\n\n' + mex
            context.bot.send_message(chat_id=update.message.chat_id, text=mex,
                             parse_mode=telegram.ParseMode.MARKDOWN)

    else:    # ironic answer if the user send a random mesage to the Bot
        update.message.reply_text("Come on! We are here to solve circuits and not to chat! 😀\n"
                                  "Please provide me a netlist.", quote=True)


# =========================================
# complex_repr - toggle polar/cartesian
# =========================================
@block_group
def complex_repr(update, context):
    """
    'complex_repr' switch from cartesian to polar representation for a complex number

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    if os.path.exists('./users/' + str(update.message.chat_id) + '.cnf'):
        # get configurations
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'r')
        flag = fid.readline()[:-1]  # read nodal_pot conf
        nodal_pot = flag == 'True'
        flag = fid.readline()[:-1]  # read polar conf
        polar = flag == 'True'
        flag = fid.readline()  # read dB conf
        dB = flag == 'True'

        # keep nodal pot and toggle polar
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write(str(nodal_pot) + '\n')
        fid.write(str(not polar) + '\n')
        fid.write(str(dB))
        fid.close()
    else:
        polar = False
        # Initialize config file with polar = True (everything else False)
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write('False\n')  # this is for the node potential
        fid.write(str(not polar) + '\n')  # this is for the polar flag
        fid.write('False')  # this is for the decibel flag
        fid.close()

    # notify user
    if polar:
        context.bot.send_message(chat_id=update.message.chat_id, text="Switched to cartesian representation")
    else:
        context.bot.send_message(chat_id=update.message.chat_id, text="Switched to polar representation")


# =========================================
# nodal_pot - toggle node potentials in output
# =========================================
@block_group
def nodal_pot(update, context):
    """
    'nodal_pot' enable/disable node potentials in the results

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    if os.path.exists('./users/' + str(update.message.chat_id) + '.cnf'):
        # get configurations
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'r')
        flag = fid.readline()[:-1]  # read nodal_pot conf
        nodal_pot = flag == 'True'
        flag = fid.readline()[:-1]  # read polar conf
        polar = flag == 'True'
        flag = fid.readline()  # read dB conf
        dB = flag == 'True'

        # switch nodal pot keep polar
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write(str(not nodal_pot) + '\n')
        fid.write(str(polar) + '\n')
        fid.write(str(dB))
        fid.close()
    else:
        nodal_pot = False

        # Initialize config file with nodal_pot = True (everything else False)
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write(str(not nodal_pot) + '\n')   # this is for the node potential
        fid.write('False\n')   # this is for the polar flag
        fid.write('False')  # this is for the decibel flag
        fid.close()

    # notify user
    if nodal_pot:
        context.bot.send_message(chat_id=update.message.chat_id, text="Node potentials removed from results")
    else:
        context.bot.send_message(chat_id=update.message.chat_id, text="Node potentials included in results")


# =========================================
# decibel - toggle decibel in bode plot
# =========================================
@block_group
def decibel(update, context):
    """
    'decibel' enable/disable decibel representation in Bode plots

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    if os.path.exists('./users/' + str(update.message.chat_id) + '.cnf'):
        # get configurations
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'r')
        flag = fid.readline()[:-1]  # read nodal_pot conf
        nodal_pot = flag == 'True'
        flag = fid.readline()[:-1]  # read polar conf
        polar = flag == 'True'
        flag = fid.readline()  # read dB conf
        dB = flag == 'True'

        # switch nodal pot keep polar
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write(str(nodal_pot) + '\n')
        fid.write(str(polar) + '\n')
        fid.write(str(not dB))
        fid.close()
    else:
        dB = False

        # Initialize config file with dB = True (everything else False)
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write('False\n')   # this is for the node potential
        fid.write('False\n')   # this is for the polar flag
        fid.write(str(not dB))  # this is for the decibel flag
        fid.close()

    # notify user
    if dB:
        context.bot.send_message(chat_id=update.message.chat_id, text="bode plot: decibel disabled")
    else:
        context.bot.send_message(chat_id=update.message.chat_id, text="bode plot: decibel enabled")


# =========================================
# log - get log
# =========================================
@block_group
@restricted
def log(update, context):
    """
    'log' sends log files in the chat

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    context.bot.send_document(chat_id=update.message.chat_id, document=open('./SolverLog.log', 'rb'))
    context.bot.send_document(chat_id=update.message.chat_id, document=open('./OtherLog.log', 'rb'))


# =========================================
# stat - get stat
# =========================================
@block_group
@restricted
def stat(update, context):
    """
    'stat' computes statistical information about the bot use

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    context.bot.send_document(chat_id=update.message.chat_id, document=open('./StatBot.log', 'rb'))

    # initialize list
    analysis = []
    user = []

    fid = open('./admin_only/admin_list.txt', 'r')
    ADMIN_LIST = [int(adm) for adm in fid.readline().split()]
    fid.close()

    with open('./StatBot.log') as fid:
        for line in fid:
            ele = line.split(' - ')

            if int(ele[-1].replace('UserID: ','')) not in ADMIN_LIST:
                analysis.append(ele[3].replace('Analysis: ', '').lower())
                user.append(int(ele[4].replace('UserID: ', '')))

    # convert to numpy array
    analysis = np.array(analysis)
    user = np.array(user)

    # percentages
    x = []
    labels = '.op', '.ac', '.tran'
    x.append(np.sum(analysis == labels[0]))
    x.append(np.sum(analysis == labels[1]))
    x.append(np.sum(analysis == labels[2]))

    # create mex
    mex = ''
    mex += '*# of Users*: {}\n'.format(np.unique(user).size)
    mex += '*# of Analyses*: {}\n'.format(analysis.size)
    mex += '    *.op*: {:.2f} %\n'.format(x[0]/np.sum(x)*100)
    mex += '    *.ac*: {:.2f} %\n'.format(x[1] / np.sum(x) * 100)
    mex += '    *.tran*: {:.2f} %\n'.format(x[2] / np.sum(x) * 100)

    context.bot.send_message(chat_id=update.message.chat_id, text=mex,
                     parse_mode=telegram.ParseMode.MARKDOWN)


# =========================================
# send2all - send message to all users
# =========================================
@block_group
@restricted
def send2all(update, context):
    """
    'send2all' sends a message to all users

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    # read all users from StatBot.log
    user = []
    with open('./StatBot.log') as fid:
        for line in fid:
            ele = line.split(' - ')
            user.append(int(ele[4].replace('UserID: ', '')))

    # convert to numpy array
    user = np.unique(np.array(user))

    # merge them with the user database
    if os.path.exists('./users/users_database.db'):
        user_db = []
        with open('./users/users_database.db', 'r') as fid:
            for line in fid:
                user_db.append(int(line))

        user_db = np.unique(np.array(user_db))
        user = np.unique(np.concatenate((user, user_db)))
        np.savetxt('./users/users_database.db', user, fmt="%s")
    else:
        np.savetxt('./users/users_database.db', user, fmt="%s")

    # get the message to be sent
    fid = open('./admin_only/message.txt')
    msg = fid.read()
    fid.close()

    # send to all user
    cnt_sent = 0
    cnt_not_sent = 0
    for id in user:
        chat_id = int(id)
        # try to send the message
        try:
            context.bot.send_message(chat_id=chat_id,
                             text=msg,
                             parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)
            cnt_sent += 1

        # if the user closed the bot, cacth exception and update cnt_not_sent
        except telegram.error.TelegramError:
            cnt_not_sent += 1

    # print on screen
    msg = "*{} users* notified with the above message.\n".format(cnt_sent)
    msg += "*{} users* not notified (bot is inactive).".format(cnt_not_sent)

    # get admin list
    fid = open('./admin_only/admin_list.txt', 'r')
    ADMIN_LIST = [int(adm) for adm in fid.readline().split()]
    fid.close()

    # send to all admins stat about message sent
    for id in ADMIN_LIST:
        chat_id = int(id)

        # try to send the message
        try:
            context.bot.send_message(chat_id=chat_id,
                             text=msg,
                             parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)

        # if the admin closed the bot don't care about the exception
        except telegram.error.TelegramError:
            pass


# =========================================
# send2admin - send message to all admins
# =========================================
@block_group
@restricted
def send2admin(update, context):
    """
    'send2admin' sends a message to all admins

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    # get admin list
    fid = open('./admin_only/admin_list.txt', 'r')
    ADMIN_LIST = [int(adm) for adm in fid.readline().split()]
    fid.close()

    # get the message to be sent
    fid = open('./admin_only/message.txt')
    msg = fid.read()
    fid.close()

    # send to all admins
    for id in ADMIN_LIST:
        chat_id = int(id)
        # try to send the message
        try:
            context.bot.send_message(chat_id=chat_id,
                             text=msg,
                             parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)

        # if the admin closed the bot don't care about the exception
        except telegram.error.TelegramError:
            pass

# =========================================
# who - retrieve user info from user id
# =========================================
@block_group
@restricted
def who(update, context):
    """
    'who' retreive user info from user id

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    # get admin list (to send them user info)
    fid = open('./admin_only/admin_list.txt', 'r')
    ADMIN_LIST = [int(adm) for adm in fid.readline().split()]
    fid.close()
    
    # get user id provided with the command
    userID = int(update.message.text.replace('/who ','').strip())

    # get and send data
    try:
        # try to get data
        chat = context.bot.get_chat(chat_id=userID)
        # build messagge                                                                                                       update.message.from_user.last_name)
        msg = "results for userID {}:\n  * username: @{}\n  * first name: {}\n  * last name: {}\n\n".format(userID,
                                                                                        chat.username,
                                                                                        chat.first_name,
                                                                                        chat.last_name)
        # check if user has profile picture
        if hasattr(chat.photo, 'small_file_id'):
            photo = True
        else:
            photo = False
            msg += "\n\n The user has no profile picture."

        # send information
        for admin in ADMIN_LIST:
            admin_id = int(admin)
            context.bot.send_message(chat_id=admin_id, text=msg)
            if photo:
                file = context.bot.getFile(chat.photo.small_file_id)
                fname = './users/propic.png'
                file.download(fname)
                context.bot.send_photo(chat_id=admin_id, photo=open('./users/propic.png', 'rb'))
                os.remove('./users/propic.png')    
            

    # send message when user if not found
    except telegram.TelegramError:
        msg += "\n\nuser {} not found".format(userID)
        for admin in ADMIN_LIST:
            admin_id = int(admin)
            context.bot.send_message(chat_id=admin_id, text=msg)

# =========================================
# unknown - catch any wrong command
# =========================================
@block_group
def unknown(update, context):
    """
    'unknown' catch unknown commands

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    context.bot.send_message(chat_id=update.message.chat_id, text="Sorry, I didn't understand that command.")


# =========================================
# bot - main
# =========================================
def main():
    # set TOKEN and initialization
    fname = './admin_only/SpicePyBot_token.txt'
    updater = Updater(token=read_token(fname), use_context=True)
    dispatcher = updater.dispatcher

    # restart - restart the BOT
    # -------------------------
    def stop_and_restart():
        """Gracefully stop the Updater and replace the current process with a new one"""
        updater.stop()
        os.execl(sys.executable, sys.executable, *sys.argv)

    @block_group
    @restricted
    def restart(update, context):
        update.message.reply_text('Bot is restarting...')
        Thread(target=stop_and_restart).start()

    # /r - restart the bot
    dispatcher.add_handler(CommandHandler('r', restart))

    # /start handler
    start_handler = CommandHandler('start', start)
    dispatcher.add_handler(start_handler)

    # catch netlist when sent to the BOT
    dispatcher.add_handler(MessageHandler(Filters.document, catch_netlist))

    # /help handler
    help_handler = CommandHandler('help', help)
    dispatcher.add_handler(help_handler)

    # /netlist handler
    netlist_handler = CommandHandler('netlist', netlist)
    dispatcher.add_handler(netlist_handler)

    # /complex_repr handler
    complex_repr_handler = CommandHandler('complex_repr', complex_repr)
    dispatcher.add_handler(complex_repr_handler)

    # /nodal_pot handler
    nodal_pot_handler = CommandHandler('nodal_pot', nodal_pot)
    dispatcher.add_handler(nodal_pot_handler)

    # /decibel handler
    decibel_handler = CommandHandler('decibel', decibel)
    dispatcher.add_handler(decibel_handler)

    # /log - get log file
    dispatcher.add_handler(CommandHandler('log', log))

    # /stat - get stat file
    dispatcher.add_handler(CommandHandler('stat', stat))

    # /send2all - send message to all users
    dispatcher.add_handler(CommandHandler('send2all', send2all))

    # /send2admin - send message to all admins
    dispatcher.add_handler(CommandHandler('send2admin', send2admin))

    # /who - retrieve user info from id
    dispatcher.add_handler(CommandHandler('who', who))

    # reply to unknown commands
    unknown_handler = MessageHandler(Filters.command, unknown)
    dispatcher.add_handler(unknown_handler)

    # reply to random message or get netlist after /netlist
    reply_handler = MessageHandler(Filters.text, reply)
    dispatcher.add_handler(reply_handler)

    # log every uncaught error with error handler
    dispatcher.add_error_handler(error_callback)

    # start the BOT
    updater.start_polling()
    # Block until you press Ctrl-C or the process receives SIGINT, SIGTERM or
    # SIGABRT. This should be used most of the time, since start_polling() is
    # non-blocking and will stop the bot gracefully.
    updater.idle()


if __name__ == '__main__':
    main()

<filename>SpicePyBot.py
# ======================
# general python modules
# ======================
import time
import logging
from functools import wraps
import os
import sys
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from threading import Thread

# ===================
# module from SpicePy
# ===================
import spicepy.netlist as ntl
from spicepy.netsolve import net_solve

# ==========================
# python-temegam-bot modules
# ==========================
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
import telegram as telegram

# ===============================
# create necessary folders
# ===============================
if not os.path.exists('users'):
    os.makedirs('users')

# ===============================
# admin list
# ===============================
fid = open('./admin_only/admin_list.txt', 'r')
LIST_OF_ADMINS = [int(adm) for adm in fid.readline().split()]
fid.close()


# ==========================
# Logging
# ==========================
# define filter to log only one level
class MyFilter(object):
    def __init__(self, level):
        self.__level = level

    def filter(self, logRecord):
        return logRecord.levelno <= self.__level


# formatter
fmt = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# StatLog: log how the log is used (only INFO)
StatLog = logging.getLogger('StatLog')
h1 = logging.FileHandler('StatBot.log')
h1.setFormatter(fmt)
StatLog.addHandler(h1)
StatLog.setLevel(logging.INFO)
StatLog.addFilter(MyFilter(logging.INFO))

# SolverLog: catch error in the netlist (>= WARNING)
SolverLog = logging.getLogger('SolverLog')
h2 = logging.FileHandler('SolverLog.log')
h2.setFormatter(fmt)
SolverLog.addHandler(h2)

# OtherLog: catch all other error using the dispatcher (>= WARNING)
OtherLog = logging.getLogger('OtherLog')
h3 = logging.FileHandler('OtherLog.log')
h3.setFormatter(fmt)
OtherLog.addHandler(h3)


# ==========================
# useful functions
# ==========================
# The following function reads the TOKEN from a file.
# This file is not incuded in the github-repo for obvious reasons
def read_token(filename):
    """
    'read_token' reads the bot token from a text file.

    :param filename: filename of the file including the token
    :return: string with the token
    """
    with open(filename) as f:
        token = f.readline().replace('\n', '')
    return token


# error_callback to log uncaught error
def error_callback(update, context):
    """
    'error_callback' log uncaught error

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    OtherLog.error("Update {} caused error {}".format(update, context.error))


# compute the solution
def get_solution(fname, update, context):
    """
    'get_solution' computes the solution of a network using SpicePy

    :param fname: filename with the netlist
    :param update: bot update
    :param context: CallbackContext
    :return:
        * mex:  solution formatted in a string
    """
    try:
        # create network and solve it
        net = ntl.Network(fname)

        # check if number of nodes exceeds the limit
        NMAX = 40
        if net.node_num > NMAX:
            mex = "Your netlist includes more than {:d} nodes.\n".format(net.node_num)
            mex += "*The maximum allowed number on this bot is {:d}.*\n".format(NMAX)
            mex += "Please reduce the number of nodes or take a look to the computational core of this bot "
            mex += "that does not have this limitation:\n"
            mex += "[SpicePy project](https://github.com/giaccone/SpicePy)"

        else:

            # limit sample for .tran to 2000 (max)
            if net.analysis[0].lower() == '.tran':
                Nsamples = float(net.convert_unit(net.analysis[2])) / float(net.convert_unit(net.analysis[1]))
                if Nsamples > 2000:
                    step = float(net.convert_unit(net.analysis[2]))/1999
                    net.analysis[1] = '{:.3e}'.format(step)

                    mex = "Your netlits defines a '.tran' analysis with *{:d}* samples\n".format(int(Nsamples))
                    mex += "Since this bot runs on a limited hardware shared by many users\n"
                    mex += "The analysis has been limited to *2000* samples:\n"
                    mex += "`.tran " + net.analysis[1] + " " + net.analysis[-1] + "`"


                    context.bot.send_message(chat_id=update.message.chat_id,
                                     text=mex,
                                     parse_mode=telegram.ParseMode.MARKDOWN)

            # limit sample for .ac to 2000 (max)
            elif net.analysis[0].lower() == '.ac':
                # get frequencies
                net.frequency_span()

                if not np.isscalar(net.f):
                    # get Nsamples
                    Nsamples = len(net.f)
                    # limit di 2000 max
                    if Nsamples > 2000:
                        scale = 2000 / Nsamples
                        old_analysys = "`" + " ".join(net.analysis) + "`"
                        net.analysis[2] = str(int(np.ceil(scale * float(net.convert_unit(net.analysis[2])))))
                        new_analysys = "`" + " ".join(net.analysis) + "`"

                        mex = "Your netlits defines a '.tran' analysis with *{:d}* samples\n".format(int(Nsamples))
                        mex += "Since this bot runs on a limited hardware shared by many users\n"
                        mex += "The analysis has been limited to *2000* samples:\n"
                        mex += "original analysis: " + old_analysys + "\n"
                        mex += "ner analysis: " + new_analysys + "\n"

                        context.bot.send_message(chat_id=update.message.chat_id,
                                         text=mex,
                                         parse_mode=telegram.ParseMode.MARKDOWN)

            # get configurations
            fname = './users/' + str(update.message.chat_id) + '.cnf'
            fid = open(fname, 'r')
            flag = fid.readline()[:-1]  # read nodal_pot conf
            nodal_pot = flag == 'True'
            flag = fid.readline()[:-1]  # read polar conf
            polar = flag == 'True'
            flag = fid.readline()  # read dB conf
            dB = flag == 'True'

            if net.analysis[0] == '.op':
                # forcepolar to False for .op problems
                polar = False

            # solve the network
            net_solve(net)

            # .op and .ac (single-frequency): prepare mex to be printed
            if (net.analysis[0].lower() == '.op') | ((net.analysis[0].lower() == '.ac') & (np.isscalar(net.f))):
                # get branch quantities
                net.branch_voltage()
                net.branch_current()
                net.branch_power()

                # prepare message
                mex = net.print(polar=polar, message=True)
                mex = mex.replace('==============================================\n'
                                  '               branch quantities'
                                  '\n==============================================\n', '*branch quantities*\n`')
                mex = mex.replace('----------------------------------------------', '')
                mex += '`'

                # if the user wants node potentials add it to mex
                if nodal_pot:
                    # create local dictionary node-number 2 node-label
                    num2node_label = {num: name for name, num in net.node_label2num.items() if name != '0'}

                    # compute the node potentials
                    mex0 = '*node potentials*\n`'
                    for num in num2node_label:
                        voltage = net.get_voltage('(' + num2node_label[num] + ')')[0]
                        if polar:
                            mex0 += 'v(' + num2node_label[num] + ') = {:10.4f} V < {:10.4f}°\n'.format(np.abs(voltage),np.angle(voltage) * 180 / np.pi)
                        else:
                            mex0 += 'v(' + num2node_label[num] + ') = {:10.4f} V\n'.format(voltage)


                    # add newline
                    mex0 += '`\n\n'

                    # add node potentials before branch quantities
                    mex = mex0 + mex

            elif net.analysis[0].lower() == '.tran':
                hf = net.plot(to_file=True, filename='./users/tran_plot_' + str(update.message.chat_id) + '.png',dpi_value=150)
                mex = None
                plt.close(hf)

            elif net.analysis[0].lower() == '.ac':
                hf = net.bode(to_file=True, decibel=dB, filename='./users/bode_plot_' + str(update.message.chat_id) + '.png', dpi_value=150)
                mex = None
                if isinstance(hf, list):
                    for fig in hf:
                        plt.close(fig)
                else:
                    plt.close(hf)

            # Log every time a network is solved
            # To make stat it is saved the type of network and the UserID
            StatLog.info('Analysis: ' + net.analysis[0] + ' - UserID: ' + str(update.effective_user.id))

        return net, mex

    except:
        # set network to None
        net = None
        # read network with issues
        wrong_net = ''
        with open(fname) as f:
            for line in f:
                wrong_net += line
        wrong_net = wrong_net.replace('\n', '  /  ')

        # log error
        SolverLog.error('UserID: ' + str(update.effective_user.id) + ' - Netlist error: ' + wrong_net)
        return net, "*Something went wrong with your netlist*.\nPlease check the netlist format."


# ==========================
# restriction decorator
# ==========================
def restricted(func):
    """
    'restricted' decorates a function so that it can be used only to allowed users

    :param func: function to be decorated
    :return: function wrapper
    """
    @wraps(func)
    def wrapped(update, context, *args, **kwargs):
        user_id = update.effective_user.id
        if user_id not in LIST_OF_ADMINS:
            print("Unauthorized access denied for {}.".format(user_id))
            context.bot.send_message(chat_id=update.message.chat_id, text="You are not authorized to run this command")
            return
        return func(update, context, *args, **kwargs)
    return wrapped


# ==========================
# block group decorator
# ==========================
def block_group(func):
    """
    'block_group' decorates functions so that they can't be used in telegram groups

    :param func: function to be decorated
    :return: function wrapper
    """
    @wraps(func)
    def wrapped(update, context, *args, **kwargs):
        # skip requests from groups
        if update.message.chat_id < 0:
            mex = "This bot is for personal use only.\n"
            mex += "*Please remove it from this group*\n"
            context.bot.send_message(chat_id=update.message.chat_id, text=mex,
                             parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)
            return
        return func(update, context, *args, **kwargs)
    return wrapped


# ==========================
# start - welcome message
# ==========================
@block_group
def start(update, context):
    """
    'start' provides the start message

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    msg = "*Welcome to SpicePyBot*.\n\n"
    msg += "It allows you to solve linear:\n"
    msg += "  \* DC networks (.op)\n"
    msg += "  \* AC networks (.ac)\n"
    msg += "  \* dynamic networks (.tran)\n\n"
    msg += "Run the code:\n"
    msg += "`/help`:  to have a short guide.\n\n"
    msg += "or\n\n"
    msg += "Read the full [tutorial](https://github.com/giaccone/SpicePyBot/wiki) if "
    msg += "you are completely new to this subject."

    context.bot.send_message(chat_id=update.message.chat_id,
                     text=msg,
                     parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)
    fname = './users/' + str(update.message.chat_id) + '.cnf'
    fid = open(fname, 'w')
    fid.write('False\n')  # this is for the node potential
    fid.write('False\n')  # this is for the polar flag
    fid.write('False')    # this is for the decibel flag
    fid.close()


# =========================================
# catch netlist from a file sent to the bot
# =========================================
@block_group
def catch_netlist(update, context):
    """
    'catch_netlist' get a netlist in a text file and provide the results.

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    # if current user don't have cnf file create it
    if not os.path.exists('./users/' + str(update.message.chat_id) + '.cnf'):
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write('False\n')  # this is for the node potential
        fid.write('False\n')  # this is for the polar flag
        fid.write('False')    # this is for the decibel flag
        fid.close()

    # catch the netlist from file
    file = context.bot.getFile(update.message.document.file_id)
    fname = './users/' + str(update.message.chat_id) + '.txt'
    file.download(fname)

    # send the netlist for double check to user
    mex = 'This is your netlist:\n\n'
    with open(fname) as f:
        for line in f:
            mex += line
    context.bot.send_message(chat_id=update.message.chat_id, text=mex)

    # compute solution
    net, mex = get_solution(fname, update, context)

    # typing
    context.bot.send_chat_action(chat_id=update.message.chat_id, action=telegram.ChatAction.TYPING)

    if mex is None:    # in case of .tran or .ac-multi-freq mex is none, hence send the plot
        if net.analysis[0].lower() == '.tran':
            context.bot.send_photo(chat_id=update.message.chat_id,
                           photo=open('./users/tran_plot_' + str(update.message.chat_id) + '.png', 'rb'))
        elif net.analysis[0].lower() == '.ac':
            N = int(len(net.tf_cmd.split()[1:]) / 2)
            if N == 1:
                context.bot.send_photo(chat_id=update.message.chat_id,
                               photo=open('./users/bode_plot_' + str(update.message.chat_id) + '.png', 'rb'))
            else:
                for k in range(N):
                    context.bot.send_photo(chat_id=update.message.chat_id,
                                   photo=open(
                                       './users/bode_plot_' + str(update.message.chat_id) + '_' + str(k) + '.png',
                                       'rb'))

    else:    # otherwise print results
        mex = 'Please remember that all components are analyzed with *passive sign convention*.\nHere you have  ' \
              '*the circuit solution*.\n\n' + mex
        context.bot.send_message(chat_id=update.message.chat_id, text=mex,
                         parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)


# ==========================
# help - short guide
# ==========================
@block_group
def help(update, context):
    """
    'help' provides information about the use of the bot

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    msg = "*Very short guide*.\n\n" #1)upload a file with the netlist (don't know what a netlist is? Run `/tutorial` in the bot)\n2) enjoy\n\n\n*If you need a more detailed guide*\nRun `/tutorial` in the bot"
    msg += "The Bot makes use of netlists to describe circuits. If you do not know what "
    msg += "a netlist is, please refer to  SpicePy "
    msg += "[documentation](https://github.com/giaccone/SpicePy/wiki/User's-guide)"
    msg += " and [examples](https://github.com/giaccone/SpicePy/wiki/Examples).\n\n"
    msg += "Assuming that you know how to describe a circuit by means of a netlist, you can either:\n\n"
    msg += "1) use the command `/netlist` and write the netlist directly to the Bot (i.e. chatting with the BOT)\n\n"
    msg += "or\n\n"
    msg += "2) send a text file to the Bot including the netlist. The Bot will catch it and it'll solve it.\n\n"
    msg += "*Finally*\n"
    msg += "read the full [tutorial](https://github.com/giaccone/SpicePyBot/wiki) if "
    msg += "you are completely new to this subject."
    context.bot.send_message(chat_id=update.message.chat_id,
                     text=msg,
                     parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)


# =========================================
# netlist - write te netlist in the BOT
# =========================================
@block_group
def netlist(update, context):
    """
    'netlist' tell to the bot that the used intend to send a netlist via text message

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    # if current user don't have cnf file create it
    if not os.path.exists('./users/' + str(update.message.chat_id) + '.cnf'):
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write('False\n')  # this is for the node potential
        fid.write('False\n')  # this is for the polar flag
        fid.write('False')    # this is for the decibel flag
        fid.close()

    open("./users/" + str(update.message.chat_id) + "_waitnetlist", 'w').close()
    context.bot.send_message(chat_id=update.message.chat_id, text="Please write the netlist\nAll in one message.")


# =========================================
# reply - catch any message and reply to it
# =========================================
@block_group
def reply(update, context):
    """
    'reply' provides the result to a netlist send via text message. If /netlist is not
    used before sending the netlist, a funny message is sent.

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    # check call to /netlist
    if os.path.exists("./users/" + str(update.message.chat_id) + "_waitnetlist"):
        # write the netlist
        fname = "./users/" + str(update.message.chat_id) + ".txt"
        fid = open(fname, "w")
        fid.write(str(update.message.text) + '\n')
        fid.close()

        # remove waitnetlist file for this user
        os.remove("./users/" + str(update.message.chat_id) + "_waitnetlist")

        # send the netlist for double check to user
        mex = 'This is your netlist:\n\n'
        with open(fname) as f:
            for line in f:
                mex += line
        context.bot.send_message(chat_id=update.message.chat_id, text=mex)

        # compute solution
        net, mex = get_solution(fname, update, context)

        # typing
        context.bot.send_chat_action(chat_id=update.message.chat_id, action=telegram.ChatAction.TYPING)

        if mex is None:  # in case of .tran or .ac-multi-freq mex is none, hence send the plot
            if net.analysis[0].lower() == '.tran':
                context.bot.send_photo(chat_id=update.message.chat_id,
                               photo=open('./users/tran_plot_' + str(update.message.chat_id) + '.png', 'rb'))
            elif net.analysis[0].lower() == '.ac':
                N = int(len(net.tf_cmd.split()[1:]) / 2)
                if N == 1:
                    context.bot.send_photo(chat_id=update.message.chat_id,
                                   photo=open('./users/bode_plot_' + str(update.message.chat_id) + '.png', 'rb'))
                else:
                    for k in range(N):
                        context.bot.send_photo(chat_id=update.message.chat_id,
                                       photo=open(
                                           './users/bode_plot_' + str(update.message.chat_id) + '_' + str(k) + '.png',
                                           'rb'))

        else:    # otherwise print results
            mex = 'Please remember that all components are analyzed with *passive sign convention*.\nHere you have  ' \
                  '*the circuit solution*.\n\n' + mex
            context.bot.send_message(chat_id=update.message.chat_id, text=mex,
                             parse_mode=telegram.ParseMode.MARKDOWN)

    else:    # ironic answer if the user send a random mesage to the Bot
        update.message.reply_text("Come on! We are here to solve circuits and not to chat! 😀\n"
                                  "Please provide me a netlist.", quote=True)


# =========================================
# complex_repr - toggle polar/cartesian
# =========================================
@block_group
def complex_repr(update, context):
    """
    'complex_repr' switch from cartesian to polar representation for a complex number

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    if os.path.exists('./users/' + str(update.message.chat_id) + '.cnf'):
        # get configurations
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'r')
        flag = fid.readline()[:-1]  # read nodal_pot conf
        nodal_pot = flag == 'True'
        flag = fid.readline()[:-1]  # read polar conf
        polar = flag == 'True'
        flag = fid.readline()  # read dB conf
        dB = flag == 'True'

        # keep nodal pot and toggle polar
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write(str(nodal_pot) + '\n')
        fid.write(str(not polar) + '\n')
        fid.write(str(dB))
        fid.close()
    else:
        polar = False
        # Initialize config file with polar = True (everything else False)
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write('False\n')  # this is for the node potential
        fid.write(str(not polar) + '\n')  # this is for the polar flag
        fid.write('False')  # this is for the decibel flag
        fid.close()

    # notify user
    if polar:
        context.bot.send_message(chat_id=update.message.chat_id, text="Switched to cartesian representation")
    else:
        context.bot.send_message(chat_id=update.message.chat_id, text="Switched to polar representation")


# =========================================
# nodal_pot - toggle node potentials in output
# =========================================
@block_group
def nodal_pot(update, context):
    """
    'nodal_pot' enable/disable node potentials in the results

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    if os.path.exists('./users/' + str(update.message.chat_id) + '.cnf'):
        # get configurations
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'r')
        flag = fid.readline()[:-1]  # read nodal_pot conf
        nodal_pot = flag == 'True'
        flag = fid.readline()[:-1]  # read polar conf
        polar = flag == 'True'
        flag = fid.readline()  # read dB conf
        dB = flag == 'True'

        # switch nodal pot keep polar
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write(str(not nodal_pot) + '\n')
        fid.write(str(polar) + '\n')
        fid.write(str(dB))
        fid.close()
    else:
        nodal_pot = False

        # Initialize config file with nodal_pot = True (everything else False)
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write(str(not nodal_pot) + '\n')   # this is for the node potential
        fid.write('False\n')   # this is for the polar flag
        fid.write('False')  # this is for the decibel flag
        fid.close()

    # notify user
    if nodal_pot:
        context.bot.send_message(chat_id=update.message.chat_id, text="Node potentials removed from results")
    else:
        context.bot.send_message(chat_id=update.message.chat_id, text="Node potentials included in results")


# =========================================
# decibel - toggle decibel in bode plot
# =========================================
@block_group
def decibel(update, context):
    """
    'decibel' enable/disable decibel representation in Bode plots

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    if os.path.exists('./users/' + str(update.message.chat_id) + '.cnf'):
        # get configurations
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'r')
        flag = fid.readline()[:-1]  # read nodal_pot conf
        nodal_pot = flag == 'True'
        flag = fid.readline()[:-1]  # read polar conf
        polar = flag == 'True'
        flag = fid.readline()  # read dB conf
        dB = flag == 'True'

        # switch nodal pot keep polar
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write(str(nodal_pot) + '\n')
        fid.write(str(polar) + '\n')
        fid.write(str(not dB))
        fid.close()
    else:
        dB = False

        # Initialize config file with dB = True (everything else False)
        fname = './users/' + str(update.message.chat_id) + '.cnf'
        fid = open(fname, 'w')
        fid.write('False\n')   # this is for the node potential
        fid.write('False\n')   # this is for the polar flag
        fid.write(str(not dB))  # this is for the decibel flag
        fid.close()

    # notify user
    if dB:
        context.bot.send_message(chat_id=update.message.chat_id, text="bode plot: decibel disabled")
    else:
        context.bot.send_message(chat_id=update.message.chat_id, text="bode plot: decibel enabled")


# =========================================
# log - get log
# =========================================
@block_group
@restricted
def log(update, context):
    """
    'log' sends log files in the chat

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    context.bot.send_document(chat_id=update.message.chat_id, document=open('./SolverLog.log', 'rb'))
    context.bot.send_document(chat_id=update.message.chat_id, document=open('./OtherLog.log', 'rb'))


# =========================================
# stat - get stat
# =========================================
@block_group
@restricted
def stat(update, context):
    """
    'stat' computes statistical information about the bot use

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    context.bot.send_document(chat_id=update.message.chat_id, document=open('./StatBot.log', 'rb'))

    # initialize list
    analysis = []
    user = []

    fid = open('./admin_only/admin_list.txt', 'r')
    ADMIN_LIST = [int(adm) for adm in fid.readline().split()]
    fid.close()

    with open('./StatBot.log') as fid:
        for line in fid:
            ele = line.split(' - ')

            if int(ele[-1].replace('UserID: ','')) not in ADMIN_LIST:
                analysis.append(ele[3].replace('Analysis: ', '').lower())
                user.append(int(ele[4].replace('UserID: ', '')))

    # convert to numpy array
    analysis = np.array(analysis)
    user = np.array(user)

    # percentages
    x = []
    labels = '.op', '.ac', '.tran'
    x.append(np.sum(analysis == labels[0]))
    x.append(np.sum(analysis == labels[1]))
    x.append(np.sum(analysis == labels[2]))

    # create mex
    mex = ''
    mex += '*# of Users*: {}\n'.format(np.unique(user).size)
    mex += '*# of Analyses*: {}\n'.format(analysis.size)
    mex += '    *.op*: {:.2f} %\n'.format(x[0]/np.sum(x)*100)
    mex += '    *.ac*: {:.2f} %\n'.format(x[1] / np.sum(x) * 100)
    mex += '    *.tran*: {:.2f} %\n'.format(x[2] / np.sum(x) * 100)

    context.bot.send_message(chat_id=update.message.chat_id, text=mex,
                     parse_mode=telegram.ParseMode.MARKDOWN)


# =========================================
# send2all - send message to all users
# =========================================
@block_group
@restricted
def send2all(update, context):
    """
    'send2all' sends a message to all users

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    # read all users from StatBot.log
    user = []
    with open('./StatBot.log') as fid:
        for line in fid:
            ele = line.split(' - ')
            user.append(int(ele[4].replace('UserID: ', '')))

    # convert to numpy array
    user = np.unique(np.array(user))

    # merge them with the user database
    if os.path.exists('./users/users_database.db'):
        user_db = []
        with open('./users/users_database.db', 'r') as fid:
            for line in fid:
                user_db.append(int(line))

        user_db = np.unique(np.array(user_db))
        user = np.unique(np.concatenate((user, user_db)))
        np.savetxt('./users/users_database.db', user, fmt="%s")
    else:
        np.savetxt('./users/users_database.db', user, fmt="%s")

    # get the message to be sent
    fid = open('./admin_only/message.txt')
    msg = fid.read()
    fid.close()

    # send to all user
    cnt_sent = 0
    cnt_not_sent = 0
    for id in user:
        chat_id = int(id)
        # try to send the message
        try:
            context.bot.send_message(chat_id=chat_id,
                             text=msg,
                             parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)
            cnt_sent += 1

        # if the user closed the bot, cacth exception and update cnt_not_sent
        except telegram.error.TelegramError:
            cnt_not_sent += 1

    # print on screen
    msg = "*{} users* notified with the above message.\n".format(cnt_sent)
    msg += "*{} users* not notified (bot is inactive).".format(cnt_not_sent)

    # get admin list
    fid = open('./admin_only/admin_list.txt', 'r')
    ADMIN_LIST = [int(adm) for adm in fid.readline().split()]
    fid.close()

    # send to all admins stat about message sent
    for id in ADMIN_LIST:
        chat_id = int(id)

        # try to send the message
        try:
            context.bot.send_message(chat_id=chat_id,
                             text=msg,
                             parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)

        # if the admin closed the bot don't care about the exception
        except telegram.error.TelegramError:
            pass


# =========================================
# send2admin - send message to all admins
# =========================================
@block_group
@restricted
def send2admin(update, context):
    """
    'send2admin' sends a message to all admins

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """

    # get admin list
    fid = open('./admin_only/admin_list.txt', 'r')
    ADMIN_LIST = [int(adm) for adm in fid.readline().split()]
    fid.close()

    # get the message to be sent
    fid = open('./admin_only/message.txt')
    msg = fid.read()
    fid.close()

    # send to all admins
    for id in ADMIN_LIST:
        chat_id = int(id)
        # try to send the message
        try:
            context.bot.send_message(chat_id=chat_id,
                             text=msg,
                             parse_mode=telegram.ParseMode.MARKDOWN, disable_web_page_preview=True)

        # if the admin closed the bot don't care about the exception
        except telegram.error.TelegramError:
            pass

# =========================================
# who - retrieve user info from user id
# =========================================
@block_group
@restricted
def who(update, context):
    """
    'who' retreive user info from user id

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    # get admin list (to send them user info)
    fid = open('./admin_only/admin_list.txt', 'r')
    ADMIN_LIST = [int(adm) for adm in fid.readline().split()]
    fid.close()
    
    # get user id provided with the command
    userID = int(update.message.text.replace('/who ','').strip())

    # get and send data
    try:
        # try to get data
        chat = context.bot.get_chat(chat_id=userID)
        # build messagge                                                                                                       update.message.from_user.last_name)
        msg = "results for userID {}:\n  * username: @{}\n  * first name: {}\n  * last name: {}\n\n".format(userID,
                                                                                        chat.username,
                                                                                        chat.first_name,
                                                                                        chat.last_name)
        # check if user has profile picture
        if hasattr(chat.photo, 'small_file_id'):
            photo = True
        else:
            photo = False
            msg += "\n\n The user has no profile picture."

        # send information
        for admin in ADMIN_LIST:
            admin_id = int(admin)
            context.bot.send_message(chat_id=admin_id, text=msg)
            if photo:
                file = context.bot.getFile(chat.photo.small_file_id)
                fname = './users/propic.png'
                file.download(fname)
                context.bot.send_photo(chat_id=admin_id, photo=open('./users/propic.png', 'rb'))
                os.remove('./users/propic.png')    
            

    # send message when user if not found
    except telegram.TelegramError:
        msg += "\n\nuser {} not found".format(userID)
        for admin in ADMIN_LIST:
            admin_id = int(admin)
            context.bot.send_message(chat_id=admin_id, text=msg)

# =========================================
# unknown - catch any wrong command
# =========================================
@block_group
def unknown(update, context):
    """
    'unknown' catch unknown commands

    :param update: bot update
    :param context: CallbackContext
    :return: None
    """
    context.bot.send_message(chat_id=update.message.chat_id, text="Sorry, I didn't understand that command.")


# =========================================
# bot - main
# =========================================
def main():
    # set TOKEN and initialization
    fname = './admin_only/SpicePyBot_token.txt'
    updater = Updater(token=read_token(fname), use_context=True)
    dispatcher = updater.dispatcher

    # restart - restart the BOT
    # -------------------------
    def stop_and_restart():
        """Gracefully stop the Updater and replace the current process with a new one"""
        updater.stop()
        os.execl(sys.executable, sys.executable, *sys.argv)

    @block_group
    @restricted
    def restart(update, context):
        update.message.reply_text('Bot is restarting...')
        Thread(target=stop_and_restart).start()

    # /r - restart the bot
    dispatcher.add_handler(CommandHandler('r', restart))

    # /start handler
    start_handler = CommandHandler('start', start)
    dispatcher.add_handler(start_handler)

    # catch netlist when sent to the BOT
    dispatcher.add_handler(MessageHandler(Filters.document, catch_netlist))

    # /help handler
    help_handler = CommandHandler('help', help)
    dispatcher.add_handler(help_handler)

    # /netlist handler
    netlist_handler = CommandHandler('netlist', netlist)
    dispatcher.add_handler(netlist_handler)

    # /complex_repr handler
    complex_repr_handler = CommandHandler('complex_repr', complex_repr)
    dispatcher.add_handler(complex_repr_handler)

    # /nodal_pot handler
    nodal_pot_handler = CommandHandler('nodal_pot', nodal_pot)
    dispatcher.add_handler(nodal_pot_handler)

    # /decibel handler
    decibel_handler = CommandHandler('decibel', decibel)
    dispatcher.add_handler(decibel_handler)

    # /log - get log file
    dispatcher.add_handler(CommandHandler('log', log))

    # /stat - get stat file
    dispatcher.add_handler(CommandHandler('stat', stat))

    # /send2all - send message to all users
    dispatcher.add_handler(CommandHandler('send2all', send2all))

    # /send2admin - send message to all admins
    dispatcher.add_handler(CommandHandler('send2admin', send2admin))

    # /who - retrieve user info from id
    dispatcher.add_handler(CommandHandler('who', who))

    # reply to unknown commands
    unknown_handler = MessageHandler(Filters.command, unknown)
    dispatcher.add_handler(unknown_handler)

    # reply to random message or get netlist after /netlist
    reply_handler = MessageHandler(Filters.text, reply)
    dispatcher.add_handler(reply_handler)

    # log every uncaught error with error handler
    dispatcher.add_error_handler(error_callback)

    # start the BOT
    updater.start_polling()
    # Block until you press Ctrl-C or the process receives SIGINT, SIGTERM or
    # SIGABRT. This should be used most of the time, since start_polling() is
    # non-blocking and will stop the bot gracefully.
    updater.idle()


if __name__ == '__main__':
    main()

# ====================== # general python modules # ====================== # =================== # module from SpicePy # =================== # ========================== # python-temegam-bot modules # ========================== # =============================== # create necessary folders # =============================== # =============================== # admin list # =============================== # ========================== # Logging # ========================== # define filter to log only one level # formatter # StatLog: log how the log is used (only INFO) # SolverLog: catch error in the netlist (>= WARNING) # OtherLog: catch all other error using the dispatcher (>= WARNING) # ========================== # useful functions # ========================== # The following function reads the TOKEN from a file. # This file is not incuded in the github-repo for obvious reasons 'read_token' reads the bot token from a text file.      :param filename: filename of the file including the token     :return: string with the token # error_callback to log uncaught error 'error_callback' log uncaught error      :param update: bot update     :param context: CallbackContext     :return: None # compute the solution 'get_solution' computes the solution of a network using SpicePy      :param fname: filename with the netlist     :param update: bot update     :param context: CallbackContext     :return:         * mex:  solution formatted in a string # create network and solve it # check if number of nodes exceeds the limit # limit sample for .tran to 2000 (max) # limit sample for .ac to 2000 (max) # get frequencies # get Nsamples # limit di 2000 max # get configurations # read nodal_pot conf # read polar conf # read dB conf # forcepolar to False for .op problems # solve the network # .op and .ac (single-frequency): prepare mex to be printed # get branch quantities # prepare message # if the user wants node potentials add it to mex # create local dictionary node-number 2 node-label # compute the node potentials # add newline # add node potentials before branch quantities # Log every time a network is solved # To make stat it is saved the type of network and the UserID # set network to None # read network with issues # log error # ========================== # restriction decorator # ========================== 'restricted' decorates a function so that it can be used only to allowed users      :param func: function to be decorated     :return: function wrapper # ========================== # block group decorator # ========================== 'block_group' decorates functions so that they can't be used in telegram groups      :param func: function to be decorated     :return: function wrapper # skip requests from groups # ========================== # start - welcome message # ========================== 'start' provides the start message      :param update: bot update     :param context: CallbackContext     :return: None # this is for the node potential # this is for the polar flag # this is for the decibel flag # ========================================= # catch netlist from a file sent to the bot # ========================================= 'catch_netlist' get a netlist in a text file and provide the results.      :param update: bot update     :param context: CallbackContext     :return: None # if current user don't have cnf file create it # this is for the node potential # this is for the polar flag # this is for the decibel flag # catch the netlist from file # send the netlist for double check to user # compute solution # typing # in case of .tran or .ac-multi-freq mex is none, hence send the plot # otherwise print results # ========================== # help - short guide # ========================== 'help' provides information about the use of the bot      :param update: bot update     :param context: CallbackContext     :return: None #1)upload a file with the netlist (don't know what a netlist is? Run `/tutorial` in the bot)\n2) enjoy\n\n\n*If you need a more detailed guide*\nRun `/tutorial` in the bot" # ========================================= # netlist - write te netlist in the BOT # ========================================= 'netlist' tell to the bot that the used intend to send a netlist via text message      :param update: bot update     :param context: CallbackContext     :return: None # if current user don't have cnf file create it # this is for the node potential # this is for the polar flag # this is for the decibel flag # ========================================= # reply - catch any message and reply to it # ========================================= 'reply' provides the result to a netlist send via text message. If /netlist is not     used before sending the netlist, a funny message is sent.      :param update: bot update     :param context: CallbackContext     :return: None # check call to /netlist # write the netlist # remove waitnetlist file for this user # send the netlist for double check to user # compute solution # typing # in case of .tran or .ac-multi-freq mex is none, hence send the plot # otherwise print results # ironic answer if the user send a random mesage to the Bot # ========================================= # complex_repr - toggle polar/cartesian # ========================================= 'complex_repr' switch from cartesian to polar representation for a complex number      :param update: bot update     :param context: CallbackContext     :return: None # get configurations # read nodal_pot conf # read polar conf # read dB conf # keep nodal pot and toggle polar # Initialize config file with polar = True (everything else False) # this is for the node potential # this is for the polar flag # this is for the decibel flag # notify user # ========================================= # nodal_pot - toggle node potentials in output # ========================================= 'nodal_pot' enable/disable node potentials in the results      :param update: bot update     :param context: CallbackContext     :return: None # get configurations # read nodal_pot conf # read polar conf # read dB conf # switch nodal pot keep polar # Initialize config file with nodal_pot = True (everything else False) # this is for the node potential # this is for the polar flag # this is for the decibel flag # notify user # ========================================= # decibel - toggle decibel in bode plot # ========================================= 'decibel' enable/disable decibel representation in Bode plots      :param update: bot update     :param context: CallbackContext     :return: None # get configurations # read nodal_pot conf # read polar conf # read dB conf # switch nodal pot keep polar # Initialize config file with dB = True (everything else False) # this is for the node potential # this is for the polar flag # this is for the decibel flag # notify user # ========================================= # log - get log # ========================================= 'log' sends log files in the chat      :param update: bot update     :param context: CallbackContext     :return: None # ========================================= # stat - get stat # ========================================= 'stat' computes statistical information about the bot use      :param update: bot update     :param context: CallbackContext     :return: None # initialize list # convert to numpy array # percentages # create mex # of Users*: {}\n'.format(np.unique(user).size) # of Analyses*: {}\n'.format(analysis.size) # ========================================= # send2all - send message to all users # ========================================= 'send2all' sends a message to all users      :param update: bot update     :param context: CallbackContext     :return: None # read all users from StatBot.log # convert to numpy array # merge them with the user database # get the message to be sent # send to all user # try to send the message # if the user closed the bot, cacth exception and update cnt_not_sent # print on screen # get admin list # send to all admins stat about message sent # try to send the message # if the admin closed the bot don't care about the exception # ========================================= # send2admin - send message to all admins # ========================================= 'send2admin' sends a message to all admins      :param update: bot update     :param context: CallbackContext     :return: None # get admin list # get the message to be sent # send to all admins # try to send the message # if the admin closed the bot don't care about the exception # ========================================= # who - retrieve user info from user id # ========================================= 'who' retreive user info from user id      :param update: bot update     :param context: CallbackContext     :return: None # get admin list (to send them user info) # get user id provided with the command # get and send data # try to get data # build messagge                                                                                                       update.message.from_user.last_name) # check if user has profile picture # send information # send message when user if not found # ========================================= # unknown - catch any wrong command # ========================================= 'unknown' catch unknown commands      :param update: bot update     :param context: CallbackContext     :return: None # ========================================= # bot - main # ========================================= # set TOKEN and initialization # restart - restart the BOT # ------------------------- Gracefully stop the Updater and replace the current process with a new one # /r - restart the bot # /start handler # catch netlist when sent to the BOT # /help handler # /netlist handler # /complex_repr handler # /nodal_pot handler # /decibel handler # /log - get log file # /stat - get stat file # /send2all - send message to all users # /send2admin - send message to all admins # /who - retrieve user info from id # reply to unknown commands # reply to random message or get netlist after /netlist # log every uncaught error with error handler # start the BOT # Block until you press Ctrl-C or the process receives SIGINT, SIGTERM or # SIGABRT. This should be used most of the time, since start_polling() is # non-blocking and will stop the bot gracefully.