diff --git a/api/README.md b/api/README.md index 035e097..e9daaa2 100644 --- a/api/README.md +++ b/api/README.md @@ -3,26 +3,24 @@ Aktienbot API ## Development - 1. Create virtual environment `python -m venv venv env/Scripts/activate` 2. Install requirements `pip install -r api/requirements.txt` 3. Set environment variables (see list below) - 1. Use `.env`-file in `api` directory like `.env.example` - 2. Or set variables using `export` or `set` commands. (Windows `set`, Linux `export`) -4. Run api `python api/app.py` + 1. Use `.env`-file in `api` directory like `.env.example` + 2. Or set variables using `export` or `set` commands. (Windows `set`, Linux `export`) +4. Run api `python api/app.py` + ## Testing - 1. Create virtual environment `python -m venv venv env/Scripts/activate` 2. Install requirements `pip install -r api/requirements.txt` 3. Set environment variables (see list below) - 1. Use `.env`-file in `api` directory like `.env.example` - 2. Or set variables using `export` or `set` commands. (Windows `set`, Linux `export`) + 1. Use `.env`-file in `api` directory like `.env.example` + 2. Or set variables using `export` or `set` commands. (Windows `set`, Linux `export`) 4. Change directory: `cd api/` 5. Run tests: `python -m pytest -v --cov-report term-missing --cov=app` ## Environment variables - ``` # Flask secret key SECRET_KEY= @@ -36,7 +34,6 @@ Aktienbot API ``` ## Docker - ``` docker run -d \ --name aktienbot_api \ @@ -51,5 +48,4 @@ docker run -d \ --restart unless-stopped \ registry.flokaiser.com/aktienbot/api:latest ``` - or load environment variables from file by using `--env-file ` \ No newline at end of file diff --git a/api/app/__init__.py b/api/app/__init__.py index aee0786..30822d3 100644 --- a/api/app/__init__.py +++ b/api/app/__init__.py @@ -4,19 +4,21 @@ __credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin P __license__ = "GPL 3.0" __version__ = "1.0.0" +from flask import current_app from apiflask import APIFlask + +from dotenv import load_dotenv +from flask_cors import CORS + from app.blueprints.keyword import keyword_blueprint from app.blueprints.portfolio import portfolio_blueprint -from app.blueprints.share_price import share_price_blueprint from app.blueprints.shares import shares_blueprint -from app.blueprints.telegram import telegram_blueprint +from app.blueprints.share_price import share_price_blueprint from app.blueprints.transactions import transaction_blueprint +from app.blueprints.telegram import telegram_blueprint from app.blueprints.user import users_blueprint from app.helper_functions import hash_password from app.models import * -from dotenv import load_dotenv -from flask import current_app -from flask_cors import CORS def create_app(config_filename=None): diff --git a/api/app/auth.py b/api/app/auth.py index 2e88111..e1a4bba 100644 --- a/api/app/auth.py +++ b/api/app/auth.py @@ -4,9 +4,10 @@ __credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin P __license__ = "GPL 3.0" __version__ = "1.0.0" +from flask import current_app + import jwt from apiflask import HTTPTokenAuth -from flask import current_app auth = HTTPTokenAuth() diff --git a/api/app/blueprints/keyword.py b/api/app/blueprints/keyword.py index 3588c50..e3edc21 100644 --- a/api/app/blueprints/keyword.py +++ b/api/app/blueprints/keyword.py @@ -7,11 +7,12 @@ __version__ = "1.0.0" import os from apiflask import APIBlueprint, abort -from app.auth import auth + from app.db import database as db from app.helper_functions import make_response, get_email_or_abort_401 -from app.models import Keyword +from app.auth import auth from app.schema import KeywordResponseSchema, KeywordSchema, DeleteSuccessfulSchema +from app.models import Keyword keyword_blueprint = APIBlueprint('keyword', __name__, url_prefix='/api') __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) diff --git a/api/app/blueprints/portfolio.py b/api/app/blueprints/portfolio.py index 386e83b..9ead66b 100644 --- a/api/app/blueprints/portfolio.py +++ b/api/app/blueprints/portfolio.py @@ -33,23 +33,20 @@ def get_portfolio(): # Otherwise calculate portfolio if transactions is not None: for row in transactions: - if row[2] == 0: - continue - else: - data = { - "isin": row[0], - "comment": row[1], - "count": row[2], - # "calculated_price": row[3], - "last_transaction": row[4], - 'current_price': 0 - } + data = { + "isin": row[0], + "comment": row[1], + "count": row[2], + # "calculated_price": row[3], + "last_transaction": row[4], + 'current_price': 0 + } - # Add current share value to portfolio - query_share_price = db.session.query(SharePrice).filter_by(isin=row[0]).order_by(SharePrice.date.desc()).first() - if query_share_price is not None: - data['current_price'] = query_share_price.as_dict()['price'] + # Add current share value to portfolio + query_share_price = db.session.query(SharePrice).filter_by(isin=row[0]).order_by(SharePrice.date.desc()).first() + if query_share_price is not None: + data['current_price'] = query_share_price.as_dict()['price'] - return_portfolio.append(data) + return_portfolio.append(data) return make_response(return_portfolio, 200, "Successfully loaded symbols") diff --git a/api/app/blueprints/transactions.py b/api/app/blueprints/transactions.py index 0759215..d34c799 100644 --- a/api/app/blueprints/transactions.py +++ b/api/app/blueprints/transactions.py @@ -67,7 +67,7 @@ def get_transaction(): return_transactions = [] # Get all transactions - transactions = db.session.query(Transaction).filter_by(email=email).order_by(Transaction.time.desc()).all() + transactions = db.session.query(Transaction).filter_by(email=email).all() # Iterate over transactions and add them to return_transactions if transactions is not None: diff --git a/api/app/config/flask.cfg b/api/app/config/flask.cfg index 4e738a2..2fec222 100644 --- a/api/app/config/flask.cfg +++ b/api/app/config/flask.cfg @@ -15,9 +15,9 @@ SECRET_KEY = os.getenv('SECRET_KEY', 'secret string') SQLALCHEMY_DATABASE_URI = "mysql+pymysql://" + os.getenv('MYSQL_USER') + ":" + os.getenv('MYSQL_PASSWORD') + "@" + os.getenv('MYSQL_HOST') + ":" + os.getenv('MYSQL_PORT', '3306') + "/" + os.getenv('MYSQL_NAME', 'aktienbot') SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning SQLALCHEMY_ENGINE_OPTIONS = { - 'pool_size': 10, - 'pool_recycle': 60, - 'pool_pre_ping': True + 'pool_size': 10, + 'pool_recycle': 60, + 'pool_pre_ping': True } # openapi/Swagger config diff --git a/api/app/config/flask_test.cfg b/api/app/config/flask_test.cfg index 2c82b5c..fc8ac7f 100644 --- a/api/app/config/flask_test.cfg +++ b/api/app/config/flask_test.cfg @@ -15,8 +15,8 @@ SECRET_KEY = os.getenv('SECRET_KEY', 'secret string') SQLALCHEMY_DATABASE_URI = "mysql+pymysql://" + os.getenv('MYSQL_USER') + ":" + os.getenv('MYSQL_PASSWORD') + "@" + os.getenv('MYSQL_HOST') + ":" + os.getenv('MYSQL_PORT', '3306') + "/" + os.getenv('MYSQL_NAME', 'aktienbot_test') SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning SQLALCHEMY_ENGINE_OPTIONS = { - 'pool_size': 100, - 'pool_recycle': 240 # 4 minutes + 'pool_size': 100, + 'pool_recycle': 240 # 4 minutes } # openapi/Swagger config diff --git a/api/load_share_price.py b/api/load_share_price.py index 80bf4a3..bd77ee4 100644 --- a/api/load_share_price.py +++ b/api/load_share_price.py @@ -9,42 +9,19 @@ import os import threading import time -import investpy -import pandas import requests -from currency_converter import CurrencyConverter +import yfinance from dotenv import load_dotenv def thread_function(s): - try: - search_result = investpy.search_quotes(text=s, products=['stocks'], countries=['germany'], n_results=1) - - recent_data = pandas.DataFrame(search_result.retrieve_recent_data()) - - stock_price = round(float(recent_data.iloc[-1]["Close"]), 2) + my_share_info = yfinance.Ticker(s) + my_share_data = my_share_info.info + if my_share_data['regularMarketPrice'] is not None: payload = { - "isin": s, - "price": round(float(stock_price), 2), - "time": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.000Z") - } - - requests.post(os.getenv("API_URL") + '/symbol', json=payload, headers={'Authorization': 'Bearer ' + token}) - except RuntimeError: - my_converter = CurrencyConverter() - - search_result = investpy.search_quotes(text=s, products=['stocks'], n_results=1) - - currency = str(search_result.retrieve_currency()) - - recent_data = pandas.DataFrame(search_result.retrieve_recent_data()) - - stock_price = my_converter.convert(float(recent_data.iloc[-1]["Close"]), str(currency), 'EUR') - - payload = { - "isin": s, - "price": round(float(stock_price), 2), + "symbol": s, + "price": float(my_share_data['regularMarketPrice']), "time": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.000Z") } diff --git a/api/requirements.txt b/api/requirements.txt index f4af07f..4768e71 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -11,8 +11,6 @@ bcrypt==3.2.2 pytest~=7.1.2 pytest-cov marshmallow~=3.15.0 -faker~=13.11.0 -requests~=2.27.1 -investpy~=1.0.8 -pandas~=1.4.2 -currencyconverter~=0.16.12 \ No newline at end of file +faker~=13.7.0 +yfinance~=0.1.70 +requests~=2.27.1 \ No newline at end of file diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 16fe73f..be4933c 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -6,9 +6,10 @@ __version__ = "1.0.0" import pytest from app import create_app, db -from app.helper_functions import hash_password from app.models import User, Transaction, Keyword, Share +from app.helper_functions import hash_password + @pytest.fixture(scope='module') def new_user(): diff --git a/api/tests/functional/test_keyword.py b/api/tests/functional/test_keyword.py index a3251b9..3ce610d 100644 --- a/api/tests/functional/test_keyword.py +++ b/api/tests/functional/test_keyword.py @@ -8,7 +8,6 @@ __version__ = "1.0.0" This file (test_keyword.py) contains the functional tests for the `keyword` blueprint. """ import json - from tests.functional.helper_functions import get_token diff --git a/api/tests/functional/test_portfolio.py b/api/tests/functional/test_portfolio.py index c2d1e6f..b2278ba 100644 --- a/api/tests/functional/test_portfolio.py +++ b/api/tests/functional/test_portfolio.py @@ -8,7 +8,6 @@ __version__ = "1.0.0" This file (test_portfolio.py) contains the functional tests for the `portfolio` blueprint. """ import json - from tests.functional.helper_functions import get_token diff --git a/api/tests/functional/test_share.py b/api/tests/functional/test_share.py index f3b16cc..875bc0b 100644 --- a/api/tests/functional/test_share.py +++ b/api/tests/functional/test_share.py @@ -8,7 +8,6 @@ __version__ = "1.0.0" This file (test_share.py) contains the functional tests for the `share` blueprint. """ import json - from tests.functional.helper_functions import get_token diff --git a/api/tests/functional/test_telegram.py b/api/tests/functional/test_telegram.py index 6086321..17679df 100644 --- a/api/tests/functional/test_telegram.py +++ b/api/tests/functional/test_telegram.py @@ -8,7 +8,6 @@ __version__ = "1.0.0" This file (test_telegram.py) contains the functional tests for the `telegram` blueprint. """ import json - from tests.functional.helper_functions import get_token diff --git a/api/tests/functional/test_transaction.py b/api/tests/functional/test_transaction.py index 47d255a..2f1917d 100644 --- a/api/tests/functional/test_transaction.py +++ b/api/tests/functional/test_transaction.py @@ -8,7 +8,6 @@ __version__ = "1.0.0" This file (test_transaction.py) contains the functional tests for the `transaction` blueprint. """ import json - from tests.functional.helper_functions import get_token diff --git a/documentation/Kurzdokumentation.docx b/documentation/Kurzdokumentation.docx new file mode 100644 index 0000000..82f2aa3 Binary files /dev/null and b/documentation/Kurzdokumentation.docx differ diff --git a/documentation/Kurzdokumentation.pdf b/documentation/Kurzdokumentation.pdf deleted file mode 100644 index cf4d102..0000000 Binary files a/documentation/Kurzdokumentation.pdf and /dev/null differ diff --git a/documentation/telegram_bot/api_handling/api_handler.html b/documentation/telegram_bot/api_handling/api_handler.html deleted file mode 100644 index 84fbb60..0000000 --- a/documentation/telegram_bot/api_handling/api_handler.html +++ /dev/null @@ -1,1571 +0,0 @@ - - - - - - -telegram_bot.api_handling.api_handler API documentation - - - - - - - - - - - -
-
-
-

Module telegram_bot.api_handling.api_handler

-
-
-

script for communicating with webservice to get data from database

-
- -Expand source code - -
"""
-script for communicating with webservice to get data from database
-"""
-__author__ = "Florian Kellermann, Linus Eickhoff"
-__date__ = "10.05.2022"
-__version__ = "1.0.2"
-__license__ = "None"
-
-# side-dependencies: none
-# Work in Progress
-
-import os
-import sys
-
-import requests as r
-from croniter import croniter  # used for checking cron formatting
-from dotenv import load_dotenv
-
-load_dotenv()  # loads environment vars
-
-
-# note: for more information about the api visit swagger documentation on https://gruppe1.testsites.info/api/docs#/
-
-class API_Handler:
-    """class for interacting with the api webservice
-    
-    Attributes:
-        db_adress (string): adress of the database
-        token (string): auth token for api
-
-    Methods:
-        reauthorize(email, password): set new credentials
-        get_user_keywords(user_id): gets the keywords of the user
-        set_keyword(user_id, keyword): sets the keyword of the user
-        delete_keyword(user_id, keyword): deletes the keyword of the user
-        get_user_shares(user_id): gets the shares of the user
-        set_share(user_id, symbol): sets the share of the user
-        delete_share(user_id, symbol): deletes the share of the user
-        get_user_transactions(user_id): gets the transactions of the user
-        set_transaction(user_id, transaction): sets the transaction of the user
-        get_user_portfolio(user_id): gets the portfolio of the user
-        set_portfolio(user_id, portfolio): sets the portfolio of the user
-        delete_portfolio(user_id, portfolio): deletes the portfolio of the user
-        set_cron_interval(user_id, interval): sets the cron interval of the user
-        set_admin(email, is_admin): sets the admin status of the user with the given email
-    """
-
-    def __init__(self, db_adress, email, password):
-        """initializes the API_Handler class
-
-        Args:
-            db_adress (string): adress of the database
-            email (string): email of the user
-            password (string): password of the user
-        """
-        self.db_adress = db_adress
-
-        payload = {'email': email, 'password': password}  # credentials for admin account that has all permissions to get and set data (in this case bot account)
-        with r.Session() as s:  # open session
-            p = s.post(self.db_adress + "/user/login", json=payload)  # login to webservice
-            if p.status_code == 200:
-                self.token = p.json()["data"]['token']  # store token for further authentication of requests
-            else:
-                print("Error: " + str(p.status_code) + " invalid credentials")
-                self.token = None
-
-    def reauthorize(self, email, password):  # can be used if token expired
-        """set new credentials
-
-        Args:
-            email (string): email of the user
-            password (string): password of the user
-
-        Returns:
-            token (string): new token or None if not 200
-
-        Raises:
-            None
-        """
-        payload = {'email': email, 'password': password}
-        with r.Session() as s:
-            p = s.post(self.db_adress + "/user/login", json=payload)
-            if p.status_code == 200:
-                self.token = p.json()["data"]['token']
-                return p.json()["data"]['token']
-            else:
-                self.token = None
-                return None
-
-    def get_user(self, user_id, max_retries=10):  # max retries are used recursively if the request fails
-        """gets the shares of the user
-
-        Args:
-            user_id (int): id of the user
-            max_retries (int): max retries for the request
-
-        Returns:
-            json: json of user infos
-        
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}  # authorization is bot_token:user_id (user_id is the id of the user you want to get data from)
-            req = s.get(self.db_adress + "/user", headers=headers)
-            if (req.status_code == 200):
-                return req.json()["data"]
-
-            else:
-                return self.get_user(user_id, max_retries - 1)  # if request fails try again recursively
-
-    def get_all_users(self, max_retries=10):
-        """gets all users
-
-        Args:
-            max_retries (int): max retries for the request
-
-        Returns:
-            list: list of users
-        
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token}
-            req = s.get(self.db_adress + "/users", headers=headers)
-            if (req.status_code == 200):
-                return req.json()["data"]
-
-            else:
-                return self.get_all_users(max_retries - 1)
-
-    def get_user_keywords(self, user_id, max_retries=10):
-        """gets the keywords of the user
-        
-        Args:
-            user_id (int): id of the user
-            max_retries (int): max retries for the request
-
-        Returns:
-            list: list of keywords
-        
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        keywords = []
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.get(self.db_adress + "/keywords", headers=headers)
-            if (req.status_code == 200):
-                keywords_json = req.json()["data"]
-                for keyword in keywords_json:  # keywords_json is a list of dictionaries
-                    keywords.append(keyword["keyword"])
-
-                return keywords  # will be empty if no keywords are set
-
-            else:
-                return self.get_user_keywords(user_id, max_retries - 1)
-
-    def set_keyword(self, user_id, keyword):
-        """sets the keyword of the user
-
-        Args:
-            user_id (int): id of the user
-            keyword (int): keyword of the user
-
-        Returns:
-            int: status code
-
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.post(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers)
-
-            return req.status_code
-
-    def delete_keyword(self, user_id, keyword):
-        """deletes the keyword of the user
-
-        Args:
-            user_id (int): id of the user
-            keyword (string): keyword of the user
-
-        Returns:
-            int: status code
-
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.delete(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers)
-
-            return req.status_code
-
-    def get_user_shares(self, user_id, max_retries=10):
-        """gets the shares of the user
-
-        Args:
-            user_id (int): id of the user
-            max_retries (int): max retries for the request
-
-        Returns:
-            list: list of shares
-
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.get(self.db_adress + "/shares", headers=headers)
-            if (req.status_code == 200):
-                shares_json = req.json()["data"]
-                shares = []
-                for share in shares_json:
-                    shares.append(share["isin"])  # we only want the isin of the shares
-
-                return shares
-
-            else:
-                return self.get_user_shares(user_id, max_retries - 1)
-
-    def set_share(self, user_id, isin, comment):
-        """sets the share of the user
-
-        Args:
-            user_id (int): id of the user
-            isin (string): identifier of the share (standard is isin)
-            comment (string): comment of the share
-        
-        Returns:
-            int: status code
-
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.post(self.db_adress + "/share", json={"comment": comment, "isin": isin},
-                         headers=headers)  # set share by setting comment and isin, comment can be the real name of the share e.g. "Apple Inc."
-            return req.status_code
-
-    def delete_share(self, user_id, isin):
-        """deletes the share of the user
-
-        Args:
-            user_id (int): id of the user
-            isin (string): identifier of the share (standard is isin)
-        
-        Returns:
-            int: status code
-
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.delete(self.db_adress + "/share", json={"isin": str(isin)}, headers=headers)  # to delete a share only the isin is needed because it is unique, shares are not transactions!
-            return req.status_code
-
-    def get_user_transactions(self, user_id, max_retries=10):
-        """gets the transactions of the user
-        
-        Args:
-            user_id (int): id of the user
-            max_retries (int): max retries for the request
-        
-        Returns:
-            dict: dictionary of transactions
-        
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.get(self.db_adress + "/transactions", headers=headers)
-
-            if req.status_code == 200:
-                transactions_dict = req.json()["data"]
-                return transactions_dict
-            else:
-                return self.get_user_transactions(user_id, max_retries - 1)
-
-    def set_transaction(self, user_id, comment, isin, count, price, time):
-        """sets the transaction of the user
-
-        Args:
-            user_id (int): id of the user
-            comment (string): comment of the transaction
-            isin (string): isin of the transaction
-            count (float): count of the transaction
-            price (float): price of the transaction
-            time (string): time of the transaction formatted like e.g. "2011-10-05T14:48:00.000Z"
-        
-        Returns:
-            int: status code
-        
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            time = time[:-3] + "Z"  # remove last character and add Z to make it a valid date for db
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            transaction = {"comment": str(comment), "count": float(count), "isin": str(isin), "price": float(price),
-                           "time": str(time)}  # set transaction as JSON with all the attributes needed according to Swagger docs
-            req = s.post(self.db_adress + "/transaction", json=transaction, headers=headers)
-            return req.status_code
-
-    def get_user_portfolio(self, user_id, max_retries=10):
-        """gets the portfolio of the user
-
-        Args:
-            user_id (int): id of the user
-            max_retries (int): max retries for the request
-        
-        Returns:
-            dict: dictionary of portfolio
-        
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.get(self.db_adress + "/portfolio", headers=headers)  # get portfolio as JSON
-            if req.status_code == 200:
-                portfolio_dict = req.json()["data"]  # get the data of the JSON
-                return portfolio_dict
-            else:
-                return self.get_user_portfolio(user_id, max_retries - 1)
-
-    def set_cron_interval(self, user_id, cron_interval):
-        """sets the cron interval of the user
-
-        Args:
-            user_id (int): id of the user
-            cron_interval (String): Update interval in cron format => see https://crontab.guru/ for formatting
-        
-        Returns:
-            int: status code
-        
-        Raises:
-            None
-        """
-        if not croniter.is_valid(cron_interval):  # check if cron_interval is in valid format
-            print("Error: Invalid cron format")
-            return -1  # return error code -1 if invalid cron format
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.put(self.db_adress + "/user/setCron", json={"cron": str(cron_interval)}, headers=headers)  # put not post (see swagger docs)
-            return req.status_code
-
-    def set_admin(self, email, is_admin):
-        """sets the admin of the user
-
-        Args:
-            email (string): email of the user
-            is_admin (bool): "true" if user should be Admin, "false" if not
-        
-        Returns:
-            int: status code
-        
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token}  # only bot token is needed, user is chosen by email
-            req = s.put(self.db_adress + "/user/setAdmin", json={"admin": is_admin, "email": str(email)}, headers=headers)
-            return req.status_code
-
-
-if __name__ == "__main__":  # editable, just for basic on the go testing of new functions
-
-    print("This is a module for the telegram bot. It is not intended to be run directly.")
-    handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD")))  # get creds from env
-    print(handler.token)
-    keywords = handler.get_user_keywords(user_id=1709356058)  # user_id here is currently mine (Linus)
-    print(keywords)
-    shares = handler.get_user_portfolio(user_id=1709356058)
-    print("set cron with status: " + str(handler.set_cron_interval(user_id=1709356058, cron_interval="0 0 * * *")))
-    user = handler.get_user(user_id=1709356058)
-    print(user)
-    all_users = handler.get_all_users()
-    admin_status = handler.set_admin("test@test.com", "true")
-    print(admin_status)
-    print(all_users)
-    print(shares)
-    sys.exit(1)
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class API_Handler -(db_adress, email, password) -
-
-

class for interacting with the api webservice

-

Attributes

-
-
db_adress : string
-
adress of the database
-
token : string
-
auth token for api
-
-

Methods

-

reauthorize(email, password): set new credentials -get_user_keywords(user_id): gets the keywords of the user -set_keyword(user_id, keyword): sets the keyword of the user -delete_keyword(user_id, keyword): deletes the keyword of the user -get_user_shares(user_id): gets the shares of the user -set_share(user_id, symbol): sets the share of the user -delete_share(user_id, symbol): deletes the share of the user -get_user_transactions(user_id): gets the transactions of the user -set_transaction(user_id, transaction): sets the transaction of the user -get_user_portfolio(user_id): gets the portfolio of the user -set_portfolio(user_id, portfolio): sets the portfolio of the user -delete_portfolio(user_id, portfolio): deletes the portfolio of the user -set_cron_interval(user_id, interval): sets the cron interval of the user -set_admin(email, is_admin): sets the admin status of the user with the given email

-

initializes the API_Handler class

-

Args

-
-
db_adress : string
-
adress of the database
-
email : string
-
email of the user
-
password : string
-
password of the user
-
-
- -Expand source code - -
class API_Handler:
-    """class for interacting with the api webservice
-    
-    Attributes:
-        db_adress (string): adress of the database
-        token (string): auth token for api
-
-    Methods:
-        reauthorize(email, password): set new credentials
-        get_user_keywords(user_id): gets the keywords of the user
-        set_keyword(user_id, keyword): sets the keyword of the user
-        delete_keyword(user_id, keyword): deletes the keyword of the user
-        get_user_shares(user_id): gets the shares of the user
-        set_share(user_id, symbol): sets the share of the user
-        delete_share(user_id, symbol): deletes the share of the user
-        get_user_transactions(user_id): gets the transactions of the user
-        set_transaction(user_id, transaction): sets the transaction of the user
-        get_user_portfolio(user_id): gets the portfolio of the user
-        set_portfolio(user_id, portfolio): sets the portfolio of the user
-        delete_portfolio(user_id, portfolio): deletes the portfolio of the user
-        set_cron_interval(user_id, interval): sets the cron interval of the user
-        set_admin(email, is_admin): sets the admin status of the user with the given email
-    """
-
-    def __init__(self, db_adress, email, password):
-        """initializes the API_Handler class
-
-        Args:
-            db_adress (string): adress of the database
-            email (string): email of the user
-            password (string): password of the user
-        """
-        self.db_adress = db_adress
-
-        payload = {'email': email, 'password': password}  # credentials for admin account that has all permissions to get and set data (in this case bot account)
-        with r.Session() as s:  # open session
-            p = s.post(self.db_adress + "/user/login", json=payload)  # login to webservice
-            if p.status_code == 200:
-                self.token = p.json()["data"]['token']  # store token for further authentication of requests
-            else:
-                print("Error: " + str(p.status_code) + " invalid credentials")
-                self.token = None
-
-    def reauthorize(self, email, password):  # can be used if token expired
-        """set new credentials
-
-        Args:
-            email (string): email of the user
-            password (string): password of the user
-
-        Returns:
-            token (string): new token or None if not 200
-
-        Raises:
-            None
-        """
-        payload = {'email': email, 'password': password}
-        with r.Session() as s:
-            p = s.post(self.db_adress + "/user/login", json=payload)
-            if p.status_code == 200:
-                self.token = p.json()["data"]['token']
-                return p.json()["data"]['token']
-            else:
-                self.token = None
-                return None
-
-    def get_user(self, user_id, max_retries=10):  # max retries are used recursively if the request fails
-        """gets the shares of the user
-
-        Args:
-            user_id (int): id of the user
-            max_retries (int): max retries for the request
-
-        Returns:
-            json: json of user infos
-        
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}  # authorization is bot_token:user_id (user_id is the id of the user you want to get data from)
-            req = s.get(self.db_adress + "/user", headers=headers)
-            if (req.status_code == 200):
-                return req.json()["data"]
-
-            else:
-                return self.get_user(user_id, max_retries - 1)  # if request fails try again recursively
-
-    def get_all_users(self, max_retries=10):
-        """gets all users
-
-        Args:
-            max_retries (int): max retries for the request
-
-        Returns:
-            list: list of users
-        
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token}
-            req = s.get(self.db_adress + "/users", headers=headers)
-            if (req.status_code == 200):
-                return req.json()["data"]
-
-            else:
-                return self.get_all_users(max_retries - 1)
-
-    def get_user_keywords(self, user_id, max_retries=10):
-        """gets the keywords of the user
-        
-        Args:
-            user_id (int): id of the user
-            max_retries (int): max retries for the request
-
-        Returns:
-            list: list of keywords
-        
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        keywords = []
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.get(self.db_adress + "/keywords", headers=headers)
-            if (req.status_code == 200):
-                keywords_json = req.json()["data"]
-                for keyword in keywords_json:  # keywords_json is a list of dictionaries
-                    keywords.append(keyword["keyword"])
-
-                return keywords  # will be empty if no keywords are set
-
-            else:
-                return self.get_user_keywords(user_id, max_retries - 1)
-
-    def set_keyword(self, user_id, keyword):
-        """sets the keyword of the user
-
-        Args:
-            user_id (int): id of the user
-            keyword (int): keyword of the user
-
-        Returns:
-            int: status code
-
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.post(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers)
-
-            return req.status_code
-
-    def delete_keyword(self, user_id, keyword):
-        """deletes the keyword of the user
-
-        Args:
-            user_id (int): id of the user
-            keyword (string): keyword of the user
-
-        Returns:
-            int: status code
-
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.delete(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers)
-
-            return req.status_code
-
-    def get_user_shares(self, user_id, max_retries=10):
-        """gets the shares of the user
-
-        Args:
-            user_id (int): id of the user
-            max_retries (int): max retries for the request
-
-        Returns:
-            list: list of shares
-
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.get(self.db_adress + "/shares", headers=headers)
-            if (req.status_code == 200):
-                shares_json = req.json()["data"]
-                shares = []
-                for share in shares_json:
-                    shares.append(share["isin"])  # we only want the isin of the shares
-
-                return shares
-
-            else:
-                return self.get_user_shares(user_id, max_retries - 1)
-
-    def set_share(self, user_id, isin, comment):
-        """sets the share of the user
-
-        Args:
-            user_id (int): id of the user
-            isin (string): identifier of the share (standard is isin)
-            comment (string): comment of the share
-        
-        Returns:
-            int: status code
-
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.post(self.db_adress + "/share", json={"comment": comment, "isin": isin},
-                         headers=headers)  # set share by setting comment and isin, comment can be the real name of the share e.g. "Apple Inc."
-            return req.status_code
-
-    def delete_share(self, user_id, isin):
-        """deletes the share of the user
-
-        Args:
-            user_id (int): id of the user
-            isin (string): identifier of the share (standard is isin)
-        
-        Returns:
-            int: status code
-
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.delete(self.db_adress + "/share", json={"isin": str(isin)}, headers=headers)  # to delete a share only the isin is needed because it is unique, shares are not transactions!
-            return req.status_code
-
-    def get_user_transactions(self, user_id, max_retries=10):
-        """gets the transactions of the user
-        
-        Args:
-            user_id (int): id of the user
-            max_retries (int): max retries for the request
-        
-        Returns:
-            dict: dictionary of transactions
-        
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.get(self.db_adress + "/transactions", headers=headers)
-
-            if req.status_code == 200:
-                transactions_dict = req.json()["data"]
-                return transactions_dict
-            else:
-                return self.get_user_transactions(user_id, max_retries - 1)
-
-    def set_transaction(self, user_id, comment, isin, count, price, time):
-        """sets the transaction of the user
-
-        Args:
-            user_id (int): id of the user
-            comment (string): comment of the transaction
-            isin (string): isin of the transaction
-            count (float): count of the transaction
-            price (float): price of the transaction
-            time (string): time of the transaction formatted like e.g. "2011-10-05T14:48:00.000Z"
-        
-        Returns:
-            int: status code
-        
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            time = time[:-3] + "Z"  # remove last character and add Z to make it a valid date for db
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            transaction = {"comment": str(comment), "count": float(count), "isin": str(isin), "price": float(price),
-                           "time": str(time)}  # set transaction as JSON with all the attributes needed according to Swagger docs
-            req = s.post(self.db_adress + "/transaction", json=transaction, headers=headers)
-            return req.status_code
-
-    def get_user_portfolio(self, user_id, max_retries=10):
-        """gets the portfolio of the user
-
-        Args:
-            user_id (int): id of the user
-            max_retries (int): max retries for the request
-        
-        Returns:
-            dict: dictionary of portfolio
-        
-        Raises:
-            None
-        """
-        if max_retries <= 0:
-            return None
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.get(self.db_adress + "/portfolio", headers=headers)  # get portfolio as JSON
-            if req.status_code == 200:
-                portfolio_dict = req.json()["data"]  # get the data of the JSON
-                return portfolio_dict
-            else:
-                return self.get_user_portfolio(user_id, max_retries - 1)
-
-    def set_cron_interval(self, user_id, cron_interval):
-        """sets the cron interval of the user
-
-        Args:
-            user_id (int): id of the user
-            cron_interval (String): Update interval in cron format => see https://crontab.guru/ for formatting
-        
-        Returns:
-            int: status code
-        
-        Raises:
-            None
-        """
-        if not croniter.is_valid(cron_interval):  # check if cron_interval is in valid format
-            print("Error: Invalid cron format")
-            return -1  # return error code -1 if invalid cron format
-
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-            req = s.put(self.db_adress + "/user/setCron", json={"cron": str(cron_interval)}, headers=headers)  # put not post (see swagger docs)
-            return req.status_code
-
-    def set_admin(self, email, is_admin):
-        """sets the admin of the user
-
-        Args:
-            email (string): email of the user
-            is_admin (bool): "true" if user should be Admin, "false" if not
-        
-        Returns:
-            int: status code
-        
-        Raises:
-            None
-        """
-        with r.Session() as s:
-            headers = {'Authorization': 'Bearer ' + self.token}  # only bot token is needed, user is chosen by email
-            req = s.put(self.db_adress + "/user/setAdmin", json={"admin": is_admin, "email": str(email)}, headers=headers)
-            return req.status_code
-
-

Methods

-
-
-def delete_keyword(self, user_id, keyword) -
-
-

deletes the keyword of the user

-

Args

-
-
user_id : int
-
id of the user
-
keyword : string
-
keyword of the user
-
-

Returns

-
-
int
-
status code
-
-

Raises

-

None

-
- -Expand source code - -
def delete_keyword(self, user_id, keyword):
-    """deletes the keyword of the user
-
-    Args:
-        user_id (int): id of the user
-        keyword (string): keyword of the user
-
-    Returns:
-        int: status code
-
-    Raises:
-        None
-    """
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-        req = s.delete(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers)
-
-        return req.status_code
-
-
-
-def delete_share(self, user_id, isin) -
-
-

deletes the share of the user

-

Args

-
-
user_id : int
-
id of the user
-
isin : string
-
identifier of the share (standard is isin)
-
-

Returns

-
-
int
-
status code
-
-

Raises

-

None

-
- -Expand source code - -
def delete_share(self, user_id, isin):
-    """deletes the share of the user
-
-    Args:
-        user_id (int): id of the user
-        isin (string): identifier of the share (standard is isin)
-    
-    Returns:
-        int: status code
-
-    Raises:
-        None
-    """
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-        req = s.delete(self.db_adress + "/share", json={"isin": str(isin)}, headers=headers)  # to delete a share only the isin is needed because it is unique, shares are not transactions!
-        return req.status_code
-
-
-
-def get_all_users(self, max_retries=10) -
-
-

gets all users

-

Args

-
-
max_retries : int
-
max retries for the request
-
-

Returns

-
-
list
-
list of users
-
-

Raises

-

None

-
- -Expand source code - -
def get_all_users(self, max_retries=10):
-    """gets all users
-
-    Args:
-        max_retries (int): max retries for the request
-
-    Returns:
-        list: list of users
-    
-    Raises:
-        None
-    """
-    if max_retries <= 0:
-        return None
-
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token}
-        req = s.get(self.db_adress + "/users", headers=headers)
-        if (req.status_code == 200):
-            return req.json()["data"]
-
-        else:
-            return self.get_all_users(max_retries - 1)
-
-
-
-def get_user(self, user_id, max_retries=10) -
-
-

gets the shares of the user

-

Args

-
-
user_id : int
-
id of the user
-
max_retries : int
-
max retries for the request
-
-

Returns

-
-
json
-
json of user infos
-
-

Raises

-

None

-
- -Expand source code - -
def get_user(self, user_id, max_retries=10):  # max retries are used recursively if the request fails
-    """gets the shares of the user
-
-    Args:
-        user_id (int): id of the user
-        max_retries (int): max retries for the request
-
-    Returns:
-        json: json of user infos
-    
-    Raises:
-        None
-    """
-    if max_retries <= 0:
-        return None
-
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}  # authorization is bot_token:user_id (user_id is the id of the user you want to get data from)
-        req = s.get(self.db_adress + "/user", headers=headers)
-        if (req.status_code == 200):
-            return req.json()["data"]
-
-        else:
-            return self.get_user(user_id, max_retries - 1)  # if request fails try again recursively
-
-
-
-def get_user_keywords(self, user_id, max_retries=10) -
-
-

gets the keywords of the user

-

Args

-
-
user_id : int
-
id of the user
-
max_retries : int
-
max retries for the request
-
-

Returns

-
-
list
-
list of keywords
-
-

Raises

-

None

-
- -Expand source code - -
def get_user_keywords(self, user_id, max_retries=10):
-    """gets the keywords of the user
-    
-    Args:
-        user_id (int): id of the user
-        max_retries (int): max retries for the request
-
-    Returns:
-        list: list of keywords
-    
-    Raises:
-        None
-    """
-    if max_retries <= 0:
-        return None
-
-    keywords = []
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-        req = s.get(self.db_adress + "/keywords", headers=headers)
-        if (req.status_code == 200):
-            keywords_json = req.json()["data"]
-            for keyword in keywords_json:  # keywords_json is a list of dictionaries
-                keywords.append(keyword["keyword"])
-
-            return keywords  # will be empty if no keywords are set
-
-        else:
-            return self.get_user_keywords(user_id, max_retries - 1)
-
-
-
-def get_user_portfolio(self, user_id, max_retries=10) -
-
-

gets the portfolio of the user

-

Args

-
-
user_id : int
-
id of the user
-
max_retries : int
-
max retries for the request
-
-

Returns

-
-
dict
-
dictionary of portfolio
-
-

Raises

-

None

-
- -Expand source code - -
def get_user_portfolio(self, user_id, max_retries=10):
-    """gets the portfolio of the user
-
-    Args:
-        user_id (int): id of the user
-        max_retries (int): max retries for the request
-    
-    Returns:
-        dict: dictionary of portfolio
-    
-    Raises:
-        None
-    """
-    if max_retries <= 0:
-        return None
-
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-        req = s.get(self.db_adress + "/portfolio", headers=headers)  # get portfolio as JSON
-        if req.status_code == 200:
-            portfolio_dict = req.json()["data"]  # get the data of the JSON
-            return portfolio_dict
-        else:
-            return self.get_user_portfolio(user_id, max_retries - 1)
-
-
-
-def get_user_shares(self, user_id, max_retries=10) -
-
-

gets the shares of the user

-

Args

-
-
user_id : int
-
id of the user
-
max_retries : int
-
max retries for the request
-
-

Returns

-
-
list
-
list of shares
-
-

Raises

-

None

-
- -Expand source code - -
def get_user_shares(self, user_id, max_retries=10):
-    """gets the shares of the user
-
-    Args:
-        user_id (int): id of the user
-        max_retries (int): max retries for the request
-
-    Returns:
-        list: list of shares
-
-    Raises:
-        None
-    """
-    if max_retries <= 0:
-        return None
-
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-        req = s.get(self.db_adress + "/shares", headers=headers)
-        if (req.status_code == 200):
-            shares_json = req.json()["data"]
-            shares = []
-            for share in shares_json:
-                shares.append(share["isin"])  # we only want the isin of the shares
-
-            return shares
-
-        else:
-            return self.get_user_shares(user_id, max_retries - 1)
-
-
-
-def get_user_transactions(self, user_id, max_retries=10) -
-
-

gets the transactions of the user

-

Args

-
-
user_id : int
-
id of the user
-
max_retries : int
-
max retries for the request
-
-

Returns

-
-
dict
-
dictionary of transactions
-
-

Raises

-

None

-
- -Expand source code - -
def get_user_transactions(self, user_id, max_retries=10):
-    """gets the transactions of the user
-    
-    Args:
-        user_id (int): id of the user
-        max_retries (int): max retries for the request
-    
-    Returns:
-        dict: dictionary of transactions
-    
-    Raises:
-        None
-    """
-    if max_retries <= 0:
-        return None
-
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-        req = s.get(self.db_adress + "/transactions", headers=headers)
-
-        if req.status_code == 200:
-            transactions_dict = req.json()["data"]
-            return transactions_dict
-        else:
-            return self.get_user_transactions(user_id, max_retries - 1)
-
-
-
-def reauthorize(self, email, password) -
-
-

set new credentials

-

Args

-
-
email : string
-
email of the user
-
password : string
-
password of the user
-
-

Returns

-

token (string): new token or None if not 200

-

Raises

-

None

-
- -Expand source code - -
def reauthorize(self, email, password):  # can be used if token expired
-    """set new credentials
-
-    Args:
-        email (string): email of the user
-        password (string): password of the user
-
-    Returns:
-        token (string): new token or None if not 200
-
-    Raises:
-        None
-    """
-    payload = {'email': email, 'password': password}
-    with r.Session() as s:
-        p = s.post(self.db_adress + "/user/login", json=payload)
-        if p.status_code == 200:
-            self.token = p.json()["data"]['token']
-            return p.json()["data"]['token']
-        else:
-            self.token = None
-            return None
-
-
-
-def set_admin(self, email, is_admin) -
-
-

sets the admin of the user

-

Args

-
-
email : string
-
email of the user
-
is_admin : bool
-
"true" if user should be Admin, "false" if not
-
-

Returns

-
-
int
-
status code
-
-

Raises

-

None

-
- -Expand source code - -
def set_admin(self, email, is_admin):
-    """sets the admin of the user
-
-    Args:
-        email (string): email of the user
-        is_admin (bool): "true" if user should be Admin, "false" if not
-    
-    Returns:
-        int: status code
-    
-    Raises:
-        None
-    """
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token}  # only bot token is needed, user is chosen by email
-        req = s.put(self.db_adress + "/user/setAdmin", json={"admin": is_admin, "email": str(email)}, headers=headers)
-        return req.status_code
-
-
-
-def set_cron_interval(self, user_id, cron_interval) -
-
-

sets the cron interval of the user

-

Args

-
-
user_id : int
-
id of the user
-
cron_interval : String
-
Update interval in cron format => see https://crontab.guru/ for formatting
-
-

Returns

-
-
int
-
status code
-
-

Raises

-

None

-
- -Expand source code - -
def set_cron_interval(self, user_id, cron_interval):
-    """sets the cron interval of the user
-
-    Args:
-        user_id (int): id of the user
-        cron_interval (String): Update interval in cron format => see https://crontab.guru/ for formatting
-    
-    Returns:
-        int: status code
-    
-    Raises:
-        None
-    """
-    if not croniter.is_valid(cron_interval):  # check if cron_interval is in valid format
-        print("Error: Invalid cron format")
-        return -1  # return error code -1 if invalid cron format
-
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-        req = s.put(self.db_adress + "/user/setCron", json={"cron": str(cron_interval)}, headers=headers)  # put not post (see swagger docs)
-        return req.status_code
-
-
-
-def set_keyword(self, user_id, keyword) -
-
-

sets the keyword of the user

-

Args

-
-
user_id : int
-
id of the user
-
keyword : int
-
keyword of the user
-
-

Returns

-
-
int
-
status code
-
-

Raises

-

None

-
- -Expand source code - -
def set_keyword(self, user_id, keyword):
-    """sets the keyword of the user
-
-    Args:
-        user_id (int): id of the user
-        keyword (int): keyword of the user
-
-    Returns:
-        int: status code
-
-    Raises:
-        None
-    """
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-        req = s.post(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers)
-
-        return req.status_code
-
-
-
-def set_share(self, user_id, isin, comment) -
-
-

sets the share of the user

-

Args

-
-
user_id : int
-
id of the user
-
isin : string
-
identifier of the share (standard is isin)
-
comment : string
-
comment of the share
-
-

Returns

-
-
int
-
status code
-
-

Raises

-

None

-
- -Expand source code - -
def set_share(self, user_id, isin, comment):
-    """sets the share of the user
-
-    Args:
-        user_id (int): id of the user
-        isin (string): identifier of the share (standard is isin)
-        comment (string): comment of the share
-    
-    Returns:
-        int: status code
-
-    Raises:
-        None
-    """
-    with r.Session() as s:
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-        req = s.post(self.db_adress + "/share", json={"comment": comment, "isin": isin},
-                     headers=headers)  # set share by setting comment and isin, comment can be the real name of the share e.g. "Apple Inc."
-        return req.status_code
-
-
-
-def set_transaction(self, user_id, comment, isin, count, price, time) -
-
-

sets the transaction of the user

-

Args

-
-
user_id : int
-
id of the user
-
comment : string
-
comment of the transaction
-
isin : string
-
isin of the transaction
-
count : float
-
count of the transaction
-
price : float
-
price of the transaction
-
time : string
-
time of the transaction formatted like e.g. "2011-10-05T14:48:00.000Z"
-
-

Returns

-
-
int
-
status code
-
-

Raises

-

None

-
- -Expand source code - -
def set_transaction(self, user_id, comment, isin, count, price, time):
-    """sets the transaction of the user
-
-    Args:
-        user_id (int): id of the user
-        comment (string): comment of the transaction
-        isin (string): isin of the transaction
-        count (float): count of the transaction
-        price (float): price of the transaction
-        time (string): time of the transaction formatted like e.g. "2011-10-05T14:48:00.000Z"
-    
-    Returns:
-        int: status code
-    
-    Raises:
-        None
-    """
-    with r.Session() as s:
-        time = time[:-3] + "Z"  # remove last character and add Z to make it a valid date for db
-        headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
-        transaction = {"comment": str(comment), "count": float(count), "isin": str(isin), "price": float(price),
-                       "time": str(time)}  # set transaction as JSON with all the attributes needed according to Swagger docs
-        req = s.post(self.db_adress + "/transaction", json=transaction, headers=headers)
-        return req.status_code
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/documentation/telegram_bot/api_handling/index.html b/documentation/telegram_bot/api_handling/index.html deleted file mode 100644 index a1e025c..0000000 --- a/documentation/telegram_bot/api_handling/index.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - -telegram_bot.api_handling API documentation - - - - - - - - - - - -
-
-
-

Module telegram_bot.api_handling

-
-
-
-
-

Sub-modules

-
-
telegram_bot.api_handling.api_handler
-
-

script for communicating with webservice to get data from database

-
-
-
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/documentation/telegram_bot/bot.html b/documentation/telegram_bot/bot.html deleted file mode 100644 index 2307945..0000000 --- a/documentation/telegram_bot/bot.html +++ /dev/null @@ -1,1810 +0,0 @@ - - - - - - -telegram_bot.bot API documentation - - - - - - - - - - - -
-
-
-

Module telegram_bot.bot

-
-
-

script for telegram bot and its functions

-
- -Expand source code - -
"""
-script for telegram bot and its functions
-"""
-__author__ = "Florian Kellermann, Linus Eickhoff"
-__date__ = "10.05.2022"
-__version__ = "1.2.3"
-__license__ = "None"
-
-# side-dependencies: none
-# Work in Progress
-# text bot at t.me/projektaktienbot
-# API Documentation https://core.telegram.org/bots/api
-# Code examples https://github.com/eternnoir/pyTelegramBotAPI#getting-started
-
-import datetime as dt
-import logging
-import os
-import re
-import sys
-
-import telebot
-from dotenv import load_dotenv
-from telebot import types
-
-import telegram_bot.helper_functions as hf
-import telegram_bot.news.news_fetcher as news
-import telegram_bot.shares.share_fetcher as share_fetcher
-from telegram_bot.api_handling.api_handler import API_Handler
-
-load_dotenv(dotenv_path='.env')  # load environment variables
-
-bot_version = "2.0.1"  # version of bot
-
-# create api handler
-api_handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD")))  # get creds from env vars.
-print("Webserver Token: " + str(api_handler.token))
-
-bot = telebot.TeleBot(os.getenv('BOT_API_KEY'))
-
-
-@bot.message_handler(commands=['start', 'Start'])
-def send_start(message):
-    """ Sending welcome message to new user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/start'
-
-    :raises: none
-
-    :rtype: none
-    """
-    bot.reply_to(message, "Welcome to this share bot project. \
-                 \nType /help to get information on what this bot can do. \
-                 \nAlso see https://gruppe1.testsites.info \
-                 to start configuring your bot")
-
-
-@bot.message_handler(commands=['version', 'Version'])
-def send_version(message):
-    """ Sending programm version
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/version'
-
-    :raises: none
-
-    :rtype:none
-    """
-    bot.reply_to(message, "the current bot version is " + bot_version)
-
-
-@bot.message_handler(commands=['help', 'Help'])  # /help -> sending all functions
-def send_help(message):
-    """ Send all functions
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/help'
-
-    :raises: none
-
-    :rtype: none
-    """
-    bot.reply_to(message,
-                 "/id or /auth get your user id\n/update get updates on your shares.\n/shares get update on interesting shares\n/setAdmin set admin rights of user (ADMIN)\n/users see all users. (ADMIN)\n/me get my user info\n/news get top article for each keyword.\n/allnews get all news (last 7 days)\n/keywords get all your keywords\n/addkeyword add a keyword\n/removekeyword remove a keyword\n/transactions get all transactions\n/newtransaction create new transaction\n/share get price of specific share\n/portfolio see own portfolio\n/removeshare removes share from portfolio\n/interval get update interval\n/setinterval set update interval\n For further details see https://gruppe1.testsites.info")
-
-
-@bot.message_handler(commands=['users', 'Users'])  # /users -> sending all users
-def send_all_users(message):
-    """ Send all users, only possible for admins
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/users'
-
-    :raises: none
-
-    :rtype: none
-    """
-
-    user_id = int(message.from_user.id)
-    user_data = api_handler.get_user(user_id)
-    if (user_data["admin"] == False):  # check if user has admin rights
-        bot.reply_to(message, "You have to be an admin to use this command")
-        return
-
-    user_list = api_handler.get_all_users()
-    user_count = len(user_list)
-    bot.send_message(chat_id=user_id, text="There are " + str(user_count) + " users in the database:")
-
-    for user in user_list:
-        username = user['username']
-        email = user['email']
-        id = user['telegram_user_id']
-        cron = user['cron']
-        admin = user['admin']
-
-        bot.send_message(chat_id=user_id, text=f'Username: {username}\nEmail: {email}\nID: {id}\nCron: {cron}\nAdmin: {admin}')  # format user data into readable message text
-
-
-@bot.message_handler(commands=['setAdmin', 'SetAdmin', 'setadmin', 'Setadmin'])  # set admin rights to user TBD: not working!!
-def set_admin(message):
-    """ Set admin rights to user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/setAdmin'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    user_data = api_handler.get_user(user_id)
-
-    if (user_data["admin"] == False):  # check if user has admin rights
-        bot.reply_to(message, "You have to be an admin to use this command")
-        return
-
-    bot.send_message(chat_id=user_id, text='send email and true if this account should have admin rights, else false\n in format: <email>,<is_admin>')  # request email and admin rights to change to
-    bot.register_next_step_handler(message, set_admin_step)
-
-
-def set_admin_step(message):
-    str_message = str(message.text)
-    args_message = str_message.split(',')  # split message into email and admin rights
-
-    if len(args_message) != 2:  # make sure 2 args (email,is_admin) are given
-
-        bot.reply_to(message, "exactly 2 arguments (<email>,<is_admin>) required, try again")
-        return
-
-    email = args_message[0]
-    is_admin = False  # default: False
-
-    if args_message[1].lower() == "true":  # if user types true, set is_admin to true
-        is_admin = True
-
-    status = api_handler.set_admin(email, is_admin)  # set admin in db
-
-    if (status == 200):
-        bot.reply_to(message, "Admin rights set")
-
-    else:
-        bot.reply_to(message, f"Admin rights could not be set ({status})")
-
-
-@bot.message_handler(commands=['me', 'Me'])  # /me -> sending user info
-def send_user(message):
-    """ Send user data
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/me'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    user_data = api_handler.get_user(user_id)
-    if not user_data or user_data == None:  # true if user is not registered
-        bot.reply_to(message, "This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info")
-        return
-    username = user_data['username']
-    email = user_data['email']
-    user_id = user_data['telegram_user_id']
-    cron = user_data['cron']
-    admin = user_data['admin']
-    bot.reply_to(message, f'Username: {username}\nEmail: {email}\nID: {user_id}\nCron: {cron}\nAdmin: {admin}')  # format user data into readable message text
-
-
-@bot.message_handler(commands=['id', 'auth', 'Id', 'Auth'])  # /auth or /id -> Authentication with user_id over web tool
-def send_id(message):
-    """ Send user id for authentication with browser
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/id' or '/auth'
-
-    :raises: none
-
-    :rtype: none
-    """
-    answer = 'Your ID/Authentication Code is: [' + str(message.from_user.id) + ']. Enter this code in the settings on https://gruppe1.testsites.info to get updates on your shares.'
-    bot.reply_to(message, answer)
-
-
-# function that can be used to ensure that the bot is online and running
-@bot.message_handler(commands=['status', 'Status'])
-def send_status(message):
-    """ Sends status to user
-    :type message: message object bot
-    :param message: message that was reacted to, if no other command handler gets called
-
-    :raises: none
-
-    :rtype: none
-    """
-    bot.reply_to(message, "bot is running")
-
-
-@bot.message_handler(commands=['update', 'Update'])  # /update -> update shares
-def update_for_user(message):
-    p_user_id = int(message.from_user.id)
-    p_my_handler = api_handler
-
-    share_symbols = []
-    share_amounts = []
-
-    my_portfolio = p_my_handler.get_user_portfolio(p_user_id)
-
-    for element in my_portfolio:
-        if element["count"] != '' and element["isin"] != '':
-            print(element["count"], element["isin"])
-            share_symbols.append(element["isin"])
-            share_amounts.append(element["count"])
-
-    my_user = p_my_handler.get_user(p_user_id)
-    send_to_user("Hello %s this is your share update:" % str(my_user["username"]), pUser_id=p_user_id)
-
-    if len(share_symbols) != 0:
-        for i in range(len(share_symbols)):
-            my_price = share_fetcher.get_share_price_no_currency(share_symbols[i])
-            my_update_message = f'{share_fetcher.get_share_information_markdown(share_symbols[i])}\ncount: {share_amounts[i]}\nTotal: {hf.make_markdown_proof(round(float(my_price) * float(share_amounts[i]), 2))} EUR'
-            bot.send_message(chat_id=p_user_id, text=my_update_message, parse_mode="MARKDOWNV2")
-    else:
-        send_to_user("No shares found for your account. Check https://gruppe1.testsites.info to change your settings and add shares.", pUser_id=p_user_id)
-
-
-def send_to_user(pText, pUser_id):
-    """ Send message to user
-    :type pText: string
-    :param pText: Text to send to user
-
-    :type pUser_id: int
-    :param pUser_id: user to send to. per default me (Florian Kellermann)
-
-    :raises: none
-
-    :rtype: none
-    """
-    bot.send_message(chat_id=pUser_id, text=pText)
-
-
-@bot.message_handler(commands=['share', 'Share'])  # /share -> get share price
-def send_share_update(message):
-    """ Send price of a specific share
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/share'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-
-    bot.send_message(chat_id=user_id, text='Send Symbol/ISIN of share or name of company:')
-    bot.register_next_step_handler(message, send_share_price)
-
-
-def send_share_price(message):
-    str_share_price = share_fetcher.get_share_information_markdown(str(message.text))
-    bot.reply_to(message, str_share_price, parse_mode="MARKDOWNV2")
-
-
-@bot.message_handler(commands=['allnews', 'Allnews'])  # /allnews -> get all news
-def send_all_news(message):
-    """ Get news for keywords of user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/allnews'
-
-    :raises: none
-
-    :rtype: none
-    """
-
-    user_id = int(message.from_user.id)
-    keywords = api_handler.get_user_keywords(user_id)  # get keywords of user
-
-    if keywords == None:  # true if user is not registered
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-        return
-
-    if not keywords:  # true if user is registered but does not have any keywords
-        bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /addkeyword')
-        return
-
-    keywords_search = ' OR '.join(keywords)  # concat all keywords with OR -> NewsAPI can understand OR, AND, NOT etc.
-    now = dt.datetime.now().date()  # get current date
-    from_date = now - dt.timedelta(days=7)  # get date 7 days ago -> limit age of news to 7 days old max
-    from_date_formatted = dt.datetime.strftime(from_date, '%Y-%m-%d')
-    news_list = news.get_all_news_by_keyword(keywords_search, from_date_formatted)["articles"]  # array of JSON article objects
-
-    if news_list:  # true if news_list is not empty
-        for article in news_list:
-            formatted_article = news.format_article(article)
-            bot.send_message(chat_id=user_id, text=formatted_article, parse_mode="MARKDOWNV2")  # Markdown allows to write bold text with * etc.
-    else:
-        bot.send_message(chat_id=user_id, text='No news found for your keywords.')
-
-
-@bot.message_handler(commands=['news', 'News'])  # /news -> get news for specific keyword
-def send_news(message):
-    """ Get news for keywords of user
-
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/news'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    keywords = api_handler.get_user_keywords(user_id)  # get keywords of user
-
-    if keywords == None:  # true if user is not registered
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-        return
-
-    if not keywords:  # true if user is registered but does not have any keywords
-        bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /addkeyword')
-        return
-
-    if keywords:
-        for keyword in keywords:
-            top_news = news.get_top_news_by_keyword(keyword)["articles"]
-            if top_news == None:  # true if request to NewsAPI failed
-                bot.send_message(chat_id=user_id, text='News Server did not respond correctly. Try again later.')
-
-            if not top_news:  # true if no news found for keyword (empty list)
-                keyword = hf.make_markdown_proof(keyword)
-                bot.send_message(chat_id=user_id, text=f'No news found for keyword: *{keyword}*', parse_mode="MARKDOWNV2")
-
-            else:
-                keyword = hf.make_markdown_proof(keyword)
-                formatted_article = news.format_article(top_news[0])  # only format and send most popular news
-                bot.send_message(chat_id=user_id, text=f"_keyword: {keyword}_\n\n" + formatted_article, parse_mode="MARKDOWNV2")  # do not use v2 because of bugs related t "." in links
-
-
-@bot.message_handler(commands=['addkeyword', 'Addkeyword'])  # /addkeyword -> add keyword to user
-def add_keyword(message):
-    """ Add keyword to user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/addkeyword'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    bot.send_message(chat_id=user_id, text='Type keyword to add:')
-    bot.register_next_step_handler(message, store_keyword)  # wait for user to send keyword, then call store_keyword function
-
-
-def store_keyword(message):
-    user_id = int(message.from_user.id)
-    keyword = str(message.text).lower()  # lower to ensure Bitcoin and bitcoin is not stored as individual keywords
-    status = api_handler.set_keyword(user_id, keyword)  # set keyword in database
-    if status == 200:  # statuscode 200 means keyword was added successfully without errors
-        bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" added.')  # duplicate keywords are denied by Database, so no need to check for that here
-    else:
-        bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" could not be stored. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info (statuscode {status})')
-
-
-@bot.message_handler(commands=['removekeyword', 'Removekeyword'])  # /removekeyword -> remove keyword from user
-def remove_keyword(message):
-    """ Remove keyword from user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/removekeyword'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    bot.send_message(chat_id=user_id, text='Type keyword to remove:')
-    bot.register_next_step_handler(message, remove_keyword_step)  # wait for user to send keyword to remove, then call remove_keyword_step function
-
-
-def remove_keyword_step(message):
-    user_id = int(message.from_user.id)
-    keyword = str(message.text).lower()
-    status = api_handler.delete_keyword(user_id, keyword)
-    if status == 200:  # statuscode 200 means keyword was removed successfully without errors
-        bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" removed.')  # checking if keyword to remove is in database are handled in database, not here
-    else:
-        bot.send_message(chat_id=user_id, text=f'Failed deleting keyword "{keyword}". (statuscode {status})')
-
-
-@bot.message_handler(commands=['keywords', 'Keywords'])  # /keywords -> get keywords of user
-def send_keywords(message):
-    """ Send keywords of user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/keywords'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    keywords = api_handler.get_user_keywords(user_id)  # get keywords of user
-
-    if keywords == None:  # true if user is not registered
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-        return
-
-    if not keywords:  # true if user is registered but does not have any keywords
-        bot.send_message(chat_id=user_id, text='No keywords set for this account. Add keywords by using /addkeyword')
-        return
-
-    else:  # send keyword list
-        keywords_str = ', '.join(keywords)
-        keywords_str = hf.make_markdown_proof(keywords_str)
-
-        text = f'Your keywords are: _{keywords_str}_'
-        bot.send_message(chat_id=user_id, text=text, parse_mode="MARKDOWNV2")
-
-
-@bot.message_handler(commands=['portfolio', 'Portfolio'])
-def send_portfolio(message):
-    """ Send portfolio of user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/portfolio'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    portfolio = api_handler.get_user_portfolio(user_id)  # get portfolio of user as json
-    if portfolio == None:  # true if user is not registered
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-        return
-    if not portfolio:  # true if user is registered but does not have any stocks in portfolio
-        bot.send_message(chat_id=user_id, text='You do not have any stocks in your portfolio.')
-        return
-    else:  # send portfolio
-        for stock in portfolio:
-            comment = hf.make_markdown_proof(str(stock["comment"]))  # comment may be written name of stock, comment is made by user when adding an stock to portfolio
-            count = hf.make_markdown_proof("{:.2f}".format(float(stock["count"])))  # round count to 2 decimal places
-            isin = hf.make_markdown_proof(str(stock["isin"]))
-            worth = hf.make_markdown_proof("{:.2f}".format(float(stock["current_price"]) * float(stock["count"])))  # round current_price to 2 decimal places
-            bot.send_message(chat_id=user_id, text=f'*{comment}*\n_{isin}_\namount: {count}\nworth: ${worth}', parse_mode="MARKDOWNV2")  # formatted message in markdown
-
-
-@bot.message_handler(commands=['removeshare', 'Removeshare'])
-def remove_share(message):
-    """ Remove share from portfolio
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/removeshare'
-    
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-
-    bot.send_message(chat_id=user_id, text='Type ISIN/Symbol/CompanyName of share to remove (if you are unsure do /shares, find your share and insert the value above amount):')
-    bot.register_next_step_handler(message, remove_share_step)  # wait for user to send ISIN, then call remove_share_step function
-
-
-def remove_share_step(message):
-    user_id = int(message.from_user.id)
-    isin = str(message.text)
-
-    status = api_handler.delete_share(int(user_id), str(isin))  # remove share from portfolio
-
-    if status == 200:  # statuscode 200 means share was removed successfully without errors
-        bot.send_message(chat_id=user_id, text=f'Share "{isin}" removed.')  # checking if share to remove is in database are handled in database, not here
-
-    else:
-        bot.send_message(chat_id=user_id, text=f'Failed deleting share "{isin}". (statuscode {status})\nMake sure that the share is in your portfolio and written exactly like there.')
-
-
-@bot.message_handler(commands=['newtransaction', 'Newtransaction'])  # tbd not working rn may be deleted in future
-def set_new_transaction(message):
-    """ Set new transaction for user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/newtransaction'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    bot.send_message(chat_id=user_id,
-                     text='Type "<name of stock>,<isin/name/symbol>,<amount>,<price_per_stock_usd>" (time of transaction will be set to now, negative amount is selling, positive is buying):')
-    bot.register_next_step_handler(message, set_new_transaction_step)
-
-
-def set_new_transaction_step(message):
-    user_id = int(message.from_user.id)
-
-    if not re.match(r"[A-Za-z0-9 ]+,[A-Za-z0-9]+,(-)?[0-9]+(.[0-9]+)?,[0-9]+(.[0-9]+)?", message.text):
-        bot.send_message(chat_id=user_id, text='Invalid format \n(e.g. Apple,US0378331005,53.2,120.4).\n Try again with /newtransaction.')
-        return
-
-    transaction_data = str(message.text).split(',')
-    desc = str(transaction_data[0])
-    isin = str(transaction_data[1])
-    amount = float(transaction_data[2])
-    price = float(transaction_data[3])
-    time = dt.datetime.now().isoformat()
-    print("\n\n\n\n\n")
-    print(f"{isin},{amount},{price},{time}")
-    status = api_handler.set_transaction(user_id, desc, isin, amount, price, time)
-
-    if status == 200:
-        bot.send_message(chat_id=user_id, text='Transaction succesfully added.')
-    else:
-        bot.send_message(chat_id=user_id, text=f'Failed adding transaction. (statuscode {status})')
-
-
-@bot.message_handler(commands=['interval', 'Interval'])
-def send_interval(message):
-    """ send interval for user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/interval'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    user_data = api_handler.get_user(user_id)  # get cron interval of user (stored in user data)
-    if user_data == None:  # true if user is not registered in DB
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info and set an interval with /setinterval')
-        return
-    else:  # send interval
-        interval = str(user_data['cron'])  # get cron from user data
-        if interval == 'None':  # true if user has no cron set
-            bot.send_message(chat_id=user_id, text='You do not have an interval set. Set one with /setinterval')
-            return
-        formatted_interval = str(interval).replace(' ', '_')  # replace spaces with underscores to add to url of crontab.guru
-        bot.send_message(chat_id=user_id, text=f'Your update interval: {interval} (https://crontab.guru/#{formatted_interval})')
-
-
-@bot.message_handler(commands=['transactions', 'Transactions'])
-def send_transactions(message):
-    """ send transactions for user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/transactions'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    transactions = api_handler.get_user_transactions(user_id)  # get transactions of user
-
-    if transactions == None:  # true if user does not exist
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-        return
-
-    if not transactions:  # true if user has no transactions
-        bot.send_message(chat_id=user_id, text='You do not have any transactions.')
-        return
-
-    else:
-
-        for transaction in transactions:
-            comment = hf.make_markdown_proof(transaction['comment']) or "\(no desc\)"  # if comment is empty, make it "no desc"
-            isin = hf.make_markdown_proof(transaction['isin'])
-            amount = hf.make_markdown_proof(transaction['count'])
-            price = hf.make_markdown_proof(transaction['price'])
-            time = hf.make_markdown_proof(transaction['time'])
-
-            bot.send_message(chat_id=user_id, text=f'_{comment}_\n{isin}\namount: {amount}\nprice: {price}\ntime: {time}', parse_mode="MARKDOWNV2")
-
-
-@bot.message_handler(commands=['shares', 'Shares'])
-def send_shares(message):
-    """ send shares for user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/shares'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    shares = api_handler.get_user_shares(user_id)  # get shares of user
-
-    if shares == None:  # true if user does not exist
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-    elif not shares:  # true if user has no shares
-        bot.send_message(chat_id=user_id, text='You do not have any shares. Add shares on https://gruppe1.testsites.info')
-    else:
-        for element in shares:
-            bot.send_message(chat_id=user_id, text=share_fetcher.get_share_information_markdown(element), parse_mode="MARKDOWNV2")
-
-
-@bot.message_handler(commands=['setinterval', 'Setinterval'])
-def set_new_interval(message):
-    """ Set new interval for user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/setinterval'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    bot.send_message(chat_id=user_id, text='Type interval in cron format:\n(https://crontab.guru/)')
-    bot.register_next_step_handler(message, set_new_interval_step)  # executes function when user sends message
-
-
-def set_new_interval_step(message):
-    user_id = int(message.from_user.id)
-    interval = str(message.text)
-    status = api_handler.set_cron_interval(user_id, interval)  # send cron to db
-
-    if status == 200:
-        bot.send_message(chat_id=user_id, text='Interval succesfully set.')
-        return
-
-    if status == -1:  # only -1 when interval is invalid, not a real statuscode, but used from api_handler.set_cron_interval to tell the crontab has the wrong format
-        bot.send_message(chat_id=user_id, text='Invalid interval format. Try again with\n /setinterval.')
-        return
-    else:
-        bot.send_message(chat_id=user_id, text=f'Failed setting interval. (statuscode {status})')
-
-
-@bot.message_handler(func=lambda message: True)  # Returning that command is unknown for any other statement
-def echo_all(message):
-    """ Tell that command is not known if it is no known command
-    :type message: message object bot
-    :param message: message that was reacted to, if no other command handler gets called
-
-    :raises: none
-
-    :rtype: none
-    """
-    answer = 'Do not know this command or text: ' + message.text
-    bot.reply_to(message, answer)
-
-
-telebot.logger.setLevel(logging.DEBUG)
-
-
-@bot.inline_handler(lambda query: query.query == 'text')  # inline prints for debugging
-def query_text(inline_query):
-    """ Output in the console about current user actions and status of bot
-    :type inline_query: 
-    :param inline_query:
-
-    :raises: none
-
-    :rtype: none
-    """
-    try:
-        r = types.InlineQueryResultArticle('1', 'Result1', types.InputTextMessageContent('hi'))
-        r2 = types.InlineQueryResultArticle('2', 'Result2', types.InputTextMessageContent('hi'))
-        bot.answer_inline_query(inline_query.id, [r, r2])
-    except Exception as e:
-        print(e)
-
-
-def main_loop():
-    """ Start bot
-    :raises: none
-
-    :rtype: none
-    """
-    bot.infinity_polling()
-
-
-if __name__ == '__main__':
-    try:
-        main_loop()
-    except KeyboardInterrupt:
-        print('\nExiting by user request.\n')
-        sys.exit(0)
-
-
-
-
-
-
-
-

Functions

-
-
-def add_keyword(message) -
-
-

Add keyword to user -:type message: message object bot -:param message: message that was reacted to, in this case always '/addkeyword'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['addkeyword', 'Addkeyword'])  # /addkeyword -> add keyword to user
-def add_keyword(message):
-    """ Add keyword to user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/addkeyword'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    bot.send_message(chat_id=user_id, text='Type keyword to add:')
-    bot.register_next_step_handler(message, store_keyword)  # wait for user to send keyword, then call store_keyword function
-
-
-
-def echo_all(message) -
-
-

Tell that command is not known if it is no known command -:type message: message object bot -:param message: message that was reacted to, if no other command handler gets called

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(func=lambda message: True)  # Returning that command is unknown for any other statement
-def echo_all(message):
-    """ Tell that command is not known if it is no known command
-    :type message: message object bot
-    :param message: message that was reacted to, if no other command handler gets called
-
-    :raises: none
-
-    :rtype: none
-    """
-    answer = 'Do not know this command or text: ' + message.text
-    bot.reply_to(message, answer)
-
-
-
-def main_loop() -
-
-

Start bot -:raises: none

-

:rtype: none

-
- -Expand source code - -
def main_loop():
-    """ Start bot
-    :raises: none
-
-    :rtype: none
-    """
-    bot.infinity_polling()
-
-
-
-def query_text(inline_query) -
-
-

Output in the console about current user actions and status of bot -:type inline_query: -:param inline_query:

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.inline_handler(lambda query: query.query == 'text')  # inline prints for debugging
-def query_text(inline_query):
-    """ Output in the console about current user actions and status of bot
-    :type inline_query: 
-    :param inline_query:
-
-    :raises: none
-
-    :rtype: none
-    """
-    try:
-        r = types.InlineQueryResultArticle('1', 'Result1', types.InputTextMessageContent('hi'))
-        r2 = types.InlineQueryResultArticle('2', 'Result2', types.InputTextMessageContent('hi'))
-        bot.answer_inline_query(inline_query.id, [r, r2])
-    except Exception as e:
-        print(e)
-
-
-
-def remove_keyword(message) -
-
-

Remove keyword from user -:type message: message object bot -:param message: message that was reacted to, in this case always '/removekeyword'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['removekeyword', 'Removekeyword'])  # /removekeyword -> remove keyword from user
-def remove_keyword(message):
-    """ Remove keyword from user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/removekeyword'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    bot.send_message(chat_id=user_id, text='Type keyword to remove:')
-    bot.register_next_step_handler(message, remove_keyword_step)  # wait for user to send keyword to remove, then call remove_keyword_step function
-
-
-
-def remove_keyword_step(message) -
-
-
-
- -Expand source code - -
def remove_keyword_step(message):
-    user_id = int(message.from_user.id)
-    keyword = str(message.text).lower()
-    status = api_handler.delete_keyword(user_id, keyword)
-    if status == 200:  # statuscode 200 means keyword was removed successfully without errors
-        bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" removed.')  # checking if keyword to remove is in database are handled in database, not here
-    else:
-        bot.send_message(chat_id=user_id, text=f'Failed deleting keyword "{keyword}". (statuscode {status})')
-
-
-
-def remove_share(message) -
-
-

Remove share from portfolio -:type message: message object bot -:param message: message that was reacted to, in this case always '/removeshare'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['removeshare', 'Removeshare'])
-def remove_share(message):
-    """ Remove share from portfolio
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/removeshare'
-    
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-
-    bot.send_message(chat_id=user_id, text='Type ISIN/Symbol/CompanyName of share to remove (if you are unsure do /shares, find your share and insert the value above amount):')
-    bot.register_next_step_handler(message, remove_share_step)  # wait for user to send ISIN, then call remove_share_step function
-
-
-
-def remove_share_step(message) -
-
-
-
- -Expand source code - -
def remove_share_step(message):
-    user_id = int(message.from_user.id)
-    isin = str(message.text)
-
-    status = api_handler.delete_share(int(user_id), str(isin))  # remove share from portfolio
-
-    if status == 200:  # statuscode 200 means share was removed successfully without errors
-        bot.send_message(chat_id=user_id, text=f'Share "{isin}" removed.')  # checking if share to remove is in database are handled in database, not here
-
-    else:
-        bot.send_message(chat_id=user_id, text=f'Failed deleting share "{isin}". (statuscode {status})\nMake sure that the share is in your portfolio and written exactly like there.')
-
-
-
-def send_all_news(message) -
-
-

Get news for keywords of user -:type message: message object bot -:param message: message that was reacted to, in this case always containing '/allnews'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['allnews', 'Allnews'])  # /allnews -> get all news
-def send_all_news(message):
-    """ Get news for keywords of user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/allnews'
-
-    :raises: none
-
-    :rtype: none
-    """
-
-    user_id = int(message.from_user.id)
-    keywords = api_handler.get_user_keywords(user_id)  # get keywords of user
-
-    if keywords == None:  # true if user is not registered
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-        return
-
-    if not keywords:  # true if user is registered but does not have any keywords
-        bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /addkeyword')
-        return
-
-    keywords_search = ' OR '.join(keywords)  # concat all keywords with OR -> NewsAPI can understand OR, AND, NOT etc.
-    now = dt.datetime.now().date()  # get current date
-    from_date = now - dt.timedelta(days=7)  # get date 7 days ago -> limit age of news to 7 days old max
-    from_date_formatted = dt.datetime.strftime(from_date, '%Y-%m-%d')
-    news_list = news.get_all_news_by_keyword(keywords_search, from_date_formatted)["articles"]  # array of JSON article objects
-
-    if news_list:  # true if news_list is not empty
-        for article in news_list:
-            formatted_article = news.format_article(article)
-            bot.send_message(chat_id=user_id, text=formatted_article, parse_mode="MARKDOWNV2")  # Markdown allows to write bold text with * etc.
-    else:
-        bot.send_message(chat_id=user_id, text='No news found for your keywords.')
-
-
-
-def send_all_users(message) -
-
-

Send all users, only possible for admins -:type message: message object bot -:param message: message that was reacted to, in this case always containing '/users'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['users', 'Users'])  # /users -> sending all users
-def send_all_users(message):
-    """ Send all users, only possible for admins
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/users'
-
-    :raises: none
-
-    :rtype: none
-    """
-
-    user_id = int(message.from_user.id)
-    user_data = api_handler.get_user(user_id)
-    if (user_data["admin"] == False):  # check if user has admin rights
-        bot.reply_to(message, "You have to be an admin to use this command")
-        return
-
-    user_list = api_handler.get_all_users()
-    user_count = len(user_list)
-    bot.send_message(chat_id=user_id, text="There are " + str(user_count) + " users in the database:")
-
-    for user in user_list:
-        username = user['username']
-        email = user['email']
-        id = user['telegram_user_id']
-        cron = user['cron']
-        admin = user['admin']
-
-        bot.send_message(chat_id=user_id, text=f'Username: {username}\nEmail: {email}\nID: {id}\nCron: {cron}\nAdmin: {admin}')  # format user data into readable message text
-
-
-
-def send_help(message) -
-
-

Send all functions -:type message: message object bot -:param message: message that was reacted to, in this case always containing '/help'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['help', 'Help'])  # /help -> sending all functions
-def send_help(message):
-    """ Send all functions
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/help'
-
-    :raises: none
-
-    :rtype: none
-    """
-    bot.reply_to(message,
-                 "/id or /auth get your user id\n/update get updates on your shares.\n/shares get update on interesting shares\n/setAdmin set admin rights of user (ADMIN)\n/users see all users. (ADMIN)\n/me get my user info\n/news get top article for each keyword.\n/allnews get all news (last 7 days)\n/keywords get all your keywords\n/addkeyword add a keyword\n/removekeyword remove a keyword\n/transactions get all transactions\n/newtransaction create new transaction\n/share get price of specific share\n/portfolio see own portfolio\n/removeshare removes share from portfolio\n/interval get update interval\n/setinterval set update interval\n For further details see https://gruppe1.testsites.info")
-
-
-
-def send_id(message) -
-
-

Send user id for authentication with browser -:type message: message object bot -:param message: message that was reacted to, in this case always containing '/id' or '/auth'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['id', 'auth', 'Id', 'Auth'])  # /auth or /id -> Authentication with user_id over web tool
-def send_id(message):
-    """ Send user id for authentication with browser
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/id' or '/auth'
-
-    :raises: none
-
-    :rtype: none
-    """
-    answer = 'Your ID/Authentication Code is: [' + str(message.from_user.id) + ']. Enter this code in the settings on https://gruppe1.testsites.info to get updates on your shares.'
-    bot.reply_to(message, answer)
-
-
-
-def send_interval(message) -
-
-

send interval for user -:type message: message object bot -:param message: message that was reacted to, in this case always '/interval'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['interval', 'Interval'])
-def send_interval(message):
-    """ send interval for user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/interval'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    user_data = api_handler.get_user(user_id)  # get cron interval of user (stored in user data)
-    if user_data == None:  # true if user is not registered in DB
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info and set an interval with /setinterval')
-        return
-    else:  # send interval
-        interval = str(user_data['cron'])  # get cron from user data
-        if interval == 'None':  # true if user has no cron set
-            bot.send_message(chat_id=user_id, text='You do not have an interval set. Set one with /setinterval')
-            return
-        formatted_interval = str(interval).replace(' ', '_')  # replace spaces with underscores to add to url of crontab.guru
-        bot.send_message(chat_id=user_id, text=f'Your update interval: {interval} (https://crontab.guru/#{formatted_interval})')
-
-
-
-def send_keywords(message) -
-
-

Send keywords of user -:type message: message object bot -:param message: message that was reacted to, in this case always '/keywords'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['keywords', 'Keywords'])  # /keywords -> get keywords of user
-def send_keywords(message):
-    """ Send keywords of user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/keywords'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    keywords = api_handler.get_user_keywords(user_id)  # get keywords of user
-
-    if keywords == None:  # true if user is not registered
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-        return
-
-    if not keywords:  # true if user is registered but does not have any keywords
-        bot.send_message(chat_id=user_id, text='No keywords set for this account. Add keywords by using /addkeyword')
-        return
-
-    else:  # send keyword list
-        keywords_str = ', '.join(keywords)
-        keywords_str = hf.make_markdown_proof(keywords_str)
-
-        text = f'Your keywords are: _{keywords_str}_'
-        bot.send_message(chat_id=user_id, text=text, parse_mode="MARKDOWNV2")
-
-
-
-def send_news(message) -
-
-

Get news for keywords of user

-

:type message: message object bot -:param message: message that was reacted to, in this case always containing '/news'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['news', 'News'])  # /news -> get news for specific keyword
-def send_news(message):
-    """ Get news for keywords of user
-
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/news'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    keywords = api_handler.get_user_keywords(user_id)  # get keywords of user
-
-    if keywords == None:  # true if user is not registered
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-        return
-
-    if not keywords:  # true if user is registered but does not have any keywords
-        bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /addkeyword')
-        return
-
-    if keywords:
-        for keyword in keywords:
-            top_news = news.get_top_news_by_keyword(keyword)["articles"]
-            if top_news == None:  # true if request to NewsAPI failed
-                bot.send_message(chat_id=user_id, text='News Server did not respond correctly. Try again later.')
-
-            if not top_news:  # true if no news found for keyword (empty list)
-                keyword = hf.make_markdown_proof(keyword)
-                bot.send_message(chat_id=user_id, text=f'No news found for keyword: *{keyword}*', parse_mode="MARKDOWNV2")
-
-            else:
-                keyword = hf.make_markdown_proof(keyword)
-                formatted_article = news.format_article(top_news[0])  # only format and send most popular news
-                bot.send_message(chat_id=user_id, text=f"_keyword: {keyword}_\n\n" + formatted_article, parse_mode="MARKDOWNV2")  # do not use v2 because of bugs related t "." in links
-
-
-
-def send_portfolio(message) -
-
-

Send portfolio of user -:type message: message object bot -:param message: message that was reacted to, in this case always '/portfolio'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['portfolio', 'Portfolio'])
-def send_portfolio(message):
-    """ Send portfolio of user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/portfolio'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    portfolio = api_handler.get_user_portfolio(user_id)  # get portfolio of user as json
-    if portfolio == None:  # true if user is not registered
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-        return
-    if not portfolio:  # true if user is registered but does not have any stocks in portfolio
-        bot.send_message(chat_id=user_id, text='You do not have any stocks in your portfolio.')
-        return
-    else:  # send portfolio
-        for stock in portfolio:
-            comment = hf.make_markdown_proof(str(stock["comment"]))  # comment may be written name of stock, comment is made by user when adding an stock to portfolio
-            count = hf.make_markdown_proof("{:.2f}".format(float(stock["count"])))  # round count to 2 decimal places
-            isin = hf.make_markdown_proof(str(stock["isin"]))
-            worth = hf.make_markdown_proof("{:.2f}".format(float(stock["current_price"]) * float(stock["count"])))  # round current_price to 2 decimal places
-            bot.send_message(chat_id=user_id, text=f'*{comment}*\n_{isin}_\namount: {count}\nworth: ${worth}', parse_mode="MARKDOWNV2")  # formatted message in markdown
-
-
-
-def send_share_price(message) -
-
-
-
- -Expand source code - -
def send_share_price(message):
-    str_share_price = share_fetcher.get_share_information_markdown(str(message.text))
-    bot.reply_to(message, str_share_price, parse_mode="MARKDOWNV2")
-
-
-
-def send_share_update(message) -
-
-

Send price of a specific share -:type message: message object bot -:param message: message that was reacted to, in this case always containing '/share'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['share', 'Share'])  # /share -> get share price
-def send_share_update(message):
-    """ Send price of a specific share
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/share'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-
-    bot.send_message(chat_id=user_id, text='Send Symbol/ISIN of share or name of company:')
-    bot.register_next_step_handler(message, send_share_price)
-
-
-
-def send_shares(message) -
-
-

send shares for user -:type message: message object bot -:param message: message that was reacted to, in this case always '/shares'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['shares', 'Shares'])
-def send_shares(message):
-    """ send shares for user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/shares'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    shares = api_handler.get_user_shares(user_id)  # get shares of user
-
-    if shares == None:  # true if user does not exist
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-    elif not shares:  # true if user has no shares
-        bot.send_message(chat_id=user_id, text='You do not have any shares. Add shares on https://gruppe1.testsites.info')
-    else:
-        for element in shares:
-            bot.send_message(chat_id=user_id, text=share_fetcher.get_share_information_markdown(element), parse_mode="MARKDOWNV2")
-
-
-
-def send_start(message) -
-
-

Sending welcome message to new user -:type message: message object bot -:param message: message that was reacted to, in this case always containing '/start'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['start', 'Start'])
-def send_start(message):
-    """ Sending welcome message to new user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/start'
-
-    :raises: none
-
-    :rtype: none
-    """
-    bot.reply_to(message, "Welcome to this share bot project. \
-                 \nType /help to get information on what this bot can do. \
-                 \nAlso see https://gruppe1.testsites.info \
-                 to start configuring your bot")
-
-
-
-def send_status(message) -
-
-

Sends status to user -:type message: message object bot -:param message: message that was reacted to, if no other command handler gets called

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['status', 'Status'])
-def send_status(message):
-    """ Sends status to user
-    :type message: message object bot
-    :param message: message that was reacted to, if no other command handler gets called
-
-    :raises: none
-
-    :rtype: none
-    """
-    bot.reply_to(message, "bot is running")
-
-
-
-def send_to_user(pText, pUser_id) -
-
-

Send message to user -:type pText: string -:param pText: Text to send to user

-

:type pUser_id: int -:param pUser_id: user to send to. per default me (Florian Kellermann)

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
def send_to_user(pText, pUser_id):
-    """ Send message to user
-    :type pText: string
-    :param pText: Text to send to user
-
-    :type pUser_id: int
-    :param pUser_id: user to send to. per default me (Florian Kellermann)
-
-    :raises: none
-
-    :rtype: none
-    """
-    bot.send_message(chat_id=pUser_id, text=pText)
-
-
-
-def send_transactions(message) -
-
-

send transactions for user -:type message: message object bot -:param message: message that was reacted to, in this case always '/transactions'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['transactions', 'Transactions'])
-def send_transactions(message):
-    """ send transactions for user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/transactions'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    transactions = api_handler.get_user_transactions(user_id)  # get transactions of user
-
-    if transactions == None:  # true if user does not exist
-        bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
-        return
-
-    if not transactions:  # true if user has no transactions
-        bot.send_message(chat_id=user_id, text='You do not have any transactions.')
-        return
-
-    else:
-
-        for transaction in transactions:
-            comment = hf.make_markdown_proof(transaction['comment']) or "\(no desc\)"  # if comment is empty, make it "no desc"
-            isin = hf.make_markdown_proof(transaction['isin'])
-            amount = hf.make_markdown_proof(transaction['count'])
-            price = hf.make_markdown_proof(transaction['price'])
-            time = hf.make_markdown_proof(transaction['time'])
-
-            bot.send_message(chat_id=user_id, text=f'_{comment}_\n{isin}\namount: {amount}\nprice: {price}\ntime: {time}', parse_mode="MARKDOWNV2")
-
-
-
-def send_user(message) -
-
-

Send user data -:type message: message object bot -:param message: message that was reacted to, in this case always containing '/me'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['me', 'Me'])  # /me -> sending user info
-def send_user(message):
-    """ Send user data
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/me'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    user_data = api_handler.get_user(user_id)
-    if not user_data or user_data == None:  # true if user is not registered
-        bot.reply_to(message, "This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info")
-        return
-    username = user_data['username']
-    email = user_data['email']
-    user_id = user_data['telegram_user_id']
-    cron = user_data['cron']
-    admin = user_data['admin']
-    bot.reply_to(message, f'Username: {username}\nEmail: {email}\nID: {user_id}\nCron: {cron}\nAdmin: {admin}')  # format user data into readable message text
-
-
-
-def send_version(message) -
-
-

Sending programm version -:type message: message object bot -:param message: message that was reacted to, in this case always containing '/version'

-

:raises: none

-

:rtype:none

-
- -Expand source code - -
@bot.message_handler(commands=['version', 'Version'])
-def send_version(message):
-    """ Sending programm version
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/version'
-
-    :raises: none
-
-    :rtype:none
-    """
-    bot.reply_to(message, "the current bot version is " + bot_version)
-
-
-
-def set_admin(message) -
-
-

Set admin rights to user -:type message: message object bot -:param message: message that was reacted to, in this case always containing '/setAdmin'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['setAdmin', 'SetAdmin', 'setadmin', 'Setadmin'])  # set admin rights to user TBD: not working!!
-def set_admin(message):
-    """ Set admin rights to user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always containing '/setAdmin'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    user_data = api_handler.get_user(user_id)
-
-    if (user_data["admin"] == False):  # check if user has admin rights
-        bot.reply_to(message, "You have to be an admin to use this command")
-        return
-
-    bot.send_message(chat_id=user_id, text='send email and true if this account should have admin rights, else false\n in format: <email>,<is_admin>')  # request email and admin rights to change to
-    bot.register_next_step_handler(message, set_admin_step)
-
-
-
-def set_admin_step(message) -
-
-
-
- -Expand source code - -
def set_admin_step(message):
-    str_message = str(message.text)
-    args_message = str_message.split(',')  # split message into email and admin rights
-
-    if len(args_message) != 2:  # make sure 2 args (email,is_admin) are given
-
-        bot.reply_to(message, "exactly 2 arguments (<email>,<is_admin>) required, try again")
-        return
-
-    email = args_message[0]
-    is_admin = False  # default: False
-
-    if args_message[1].lower() == "true":  # if user types true, set is_admin to true
-        is_admin = True
-
-    status = api_handler.set_admin(email, is_admin)  # set admin in db
-
-    if (status == 200):
-        bot.reply_to(message, "Admin rights set")
-
-    else:
-        bot.reply_to(message, f"Admin rights could not be set ({status})")
-
-
-
-def set_new_interval(message) -
-
-

Set new interval for user -:type message: message object bot -:param message: message that was reacted to, in this case always '/setinterval'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['setinterval', 'Setinterval'])
-def set_new_interval(message):
-    """ Set new interval for user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/setinterval'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    bot.send_message(chat_id=user_id, text='Type interval in cron format:\n(https://crontab.guru/)')
-    bot.register_next_step_handler(message, set_new_interval_step)  # executes function when user sends message
-
-
-
-def set_new_interval_step(message) -
-
-
-
- -Expand source code - -
def set_new_interval_step(message):
-    user_id = int(message.from_user.id)
-    interval = str(message.text)
-    status = api_handler.set_cron_interval(user_id, interval)  # send cron to db
-
-    if status == 200:
-        bot.send_message(chat_id=user_id, text='Interval succesfully set.')
-        return
-
-    if status == -1:  # only -1 when interval is invalid, not a real statuscode, but used from api_handler.set_cron_interval to tell the crontab has the wrong format
-        bot.send_message(chat_id=user_id, text='Invalid interval format. Try again with\n /setinterval.')
-        return
-    else:
-        bot.send_message(chat_id=user_id, text=f'Failed setting interval. (statuscode {status})')
-
-
-
-def set_new_transaction(message) -
-
-

Set new transaction for user -:type message: message object bot -:param message: message that was reacted to, in this case always '/newtransaction'

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
@bot.message_handler(commands=['newtransaction', 'Newtransaction'])  # tbd not working rn may be deleted in future
-def set_new_transaction(message):
-    """ Set new transaction for user
-    :type message: message object bot
-    :param message: message that was reacted to, in this case always '/newtransaction'
-
-    :raises: none
-
-    :rtype: none
-    """
-    user_id = int(message.from_user.id)
-    bot.send_message(chat_id=user_id,
-                     text='Type "<name of stock>,<isin/name/symbol>,<amount>,<price_per_stock_usd>" (time of transaction will be set to now, negative amount is selling, positive is buying):')
-    bot.register_next_step_handler(message, set_new_transaction_step)
-
-
-
-def set_new_transaction_step(message) -
-
-
-
- -Expand source code - -
def set_new_transaction_step(message):
-    user_id = int(message.from_user.id)
-
-    if not re.match(r"[A-Za-z0-9 ]+,[A-Za-z0-9]+,(-)?[0-9]+(.[0-9]+)?,[0-9]+(.[0-9]+)?", message.text):
-        bot.send_message(chat_id=user_id, text='Invalid format \n(e.g. Apple,US0378331005,53.2,120.4).\n Try again with /newtransaction.')
-        return
-
-    transaction_data = str(message.text).split(',')
-    desc = str(transaction_data[0])
-    isin = str(transaction_data[1])
-    amount = float(transaction_data[2])
-    price = float(transaction_data[3])
-    time = dt.datetime.now().isoformat()
-    print("\n\n\n\n\n")
-    print(f"{isin},{amount},{price},{time}")
-    status = api_handler.set_transaction(user_id, desc, isin, amount, price, time)
-
-    if status == 200:
-        bot.send_message(chat_id=user_id, text='Transaction succesfully added.')
-    else:
-        bot.send_message(chat_id=user_id, text=f'Failed adding transaction. (statuscode {status})')
-
-
-
-def store_keyword(message) -
-
-
-
- -Expand source code - -
def store_keyword(message):
-    user_id = int(message.from_user.id)
-    keyword = str(message.text).lower()  # lower to ensure Bitcoin and bitcoin is not stored as individual keywords
-    status = api_handler.set_keyword(user_id, keyword)  # set keyword in database
-    if status == 200:  # statuscode 200 means keyword was added successfully without errors
-        bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" added.')  # duplicate keywords are denied by Database, so no need to check for that here
-    else:
-        bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" could not be stored. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info (statuscode {status})')
-
-
-
-def update_for_user(message) -
-
-
-
- -Expand source code - -
@bot.message_handler(commands=['update', 'Update'])  # /update -> update shares
-def update_for_user(message):
-    p_user_id = int(message.from_user.id)
-    p_my_handler = api_handler
-
-    share_symbols = []
-    share_amounts = []
-
-    my_portfolio = p_my_handler.get_user_portfolio(p_user_id)
-
-    for element in my_portfolio:
-        if element["count"] != '' and element["isin"] != '':
-            print(element["count"], element["isin"])
-            share_symbols.append(element["isin"])
-            share_amounts.append(element["count"])
-
-    my_user = p_my_handler.get_user(p_user_id)
-    send_to_user("Hello %s this is your share update:" % str(my_user["username"]), pUser_id=p_user_id)
-
-    if len(share_symbols) != 0:
-        for i in range(len(share_symbols)):
-            my_price = share_fetcher.get_share_price_no_currency(share_symbols[i])
-            my_update_message = f'{share_fetcher.get_share_information_markdown(share_symbols[i])}\ncount: {share_amounts[i]}\nTotal: {hf.make_markdown_proof(round(float(my_price) * float(share_amounts[i]), 2))} EUR'
-            bot.send_message(chat_id=p_user_id, text=my_update_message, parse_mode="MARKDOWNV2")
-    else:
-        send_to_user("No shares found for your account. Check https://gruppe1.testsites.info to change your settings and add shares.", pUser_id=p_user_id)
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/documentation/telegram_bot/bot_updates.html b/documentation/telegram_bot/bot_updates.html deleted file mode 100644 index 6a76576..0000000 --- a/documentation/telegram_bot/bot_updates.html +++ /dev/null @@ -1,507 +0,0 @@ - - - - - - -telegram_bot.bot_updates API documentation - - - - - - - - - - - -
-
-
-

Module telegram_bot.bot_updates

-
-
-

script for regularly sending updates on shares and news based on user interval

-
- -Expand source code - -
"""
-script for regularly sending updates on shares and news based on user interval
-"""
-__author__ = "Florian Kellermann, Linus Eickhoff"
-__date__ = "10.05.2022"
-__version__ = "1.0.2"
-__license__ = "None"
-
-import os
-import sys
-import time
-
-from apscheduler.schedulers.background import BackgroundScheduler # scheduler for cron
-from dotenv import load_dotenv
-
-import telegram_bot.helper_functions as hf
-import telegram_bot.news.news_fetcher as news_fetcher
-import telegram_bot.shares.share_fetcher as share_fetcher
-from telegram_bot.api_handling.api_handler import API_Handler
-from telegram_bot.bot import bot
-
-'''
-* * * * * code
-┬ ┬ ┬ ┬ ┬
-│ │ │ │ │
-│ │ │ │ └──── weekday (0->Monday, 7->Sunday)
-│ │ │ └────── Month (1-12)
-│ │ └──────── Day (1-31)
-│ └────────── Hour (0-23)
-└──────────── Minute (0-59)
-
-example 0 8 * * * -> daily update at 8am
-'''
-user_ids = []
-user_crontab = []
-
-load_dotenv(dotenv_path='.env')
-
-
-def start_updater():
-    """ starting function for regularly sending updates
-    :raises: none
-
-    :rtype: none
-    """
-
-    print("Bot updates started")
-
-    my_handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD")))
-
-    update_crontab(my_handler)
-
-
-def update_crontab(p_my_handler):
-    """ Updating crontab lists every hour
-    :type pCurrent_Time: time when starting crontab update
-    :param pCurrent_Time: datetime
-
-    :raises: none
-
-    :rtype: none
-    """
-
-    global user_crontab
-    global user_ids
-    
-    all_users = p_my_handler.get_all_users() # get all users so crontabs can update for everybody
-    
-    user_ids = []
-    user_crontab = []
-
-    for element in all_users:
-        if element["cron"] != '' and element["telegram_user_id"] != '': # check if both values are existing so I have consistent data
-            try:
-                user_ids.append(int(element["telegram_user_id"]))
-                try:
-                    user_crontab.append(str(element["cron"]))
-                except: 
-                    user_ids.pop() # if something goes wrong with cron I have to delete matching user id 
-            except: continue
-                
-    
-    print(user_ids)
-
-    update_based_on_crontab(user_ids, user_crontab, p_my_handler)
-    
-    update_crontab(p_my_handler) # restart the update after time sleep
-    
-    
-def update_based_on_crontab(p_user_ids, p_user_crontab, p_my_handler):
-    """ Check all the crontab codes and add jobs to start in time
-    :type p_user_ids: array
-    :param p_user_ids: user id array of all users
-
-    :type p_user_crontab: array
-    :param p_user_crontab: crontabs for all users equivalent to the user array
-
-    :type p_my_handler: Api_Handler
-    :param p_my_handler: get database stuff
-
-    :raises: none
-
-    :rtype: none
-    """
-    
-    my_scheduler = BackgroundScheduler() # schedule sends based on cron
-    
-    for i in range(len(p_user_ids)):
-        cron_split = p_user_crontab[i].split(" ") # split it up to use in scheduler
-        print(cron_split[4], cron_split[1], cron_split[0], cron_split[3], cron_split[2])
-        my_scheduler.add_job(update_for_user, 'cron', day_of_week=cron_split[4], hour=cron_split[1], minute=cron_split[0], month=cron_split[3], day=cron_split[2], args=(p_user_ids[i], p_my_handler))
-
-    my_scheduler.start()
-    
-    time.sleep( 600 ) # scheduler runs in background and I wait 10mins
-    my_scheduler.shutdown() # after this the new crontabs will be loaded
-                    
-def update_for_user(p_user_id, p_my_handler):
-    """ Pull shares and send updates for specific user id
-    :type p_user_id: integer
-    :param p_user_id: user id of user that shall receive update
-
-    :type p_my_handler: Api_Handler
-    :param p_my_handler: handle the api and pull from database
-
-    :raises: none
-
-    :rtype: none
-    """
-    share_symbols = []
-    share_amounts = []
-    
-    my_portfolio = p_my_handler.get_user_portfolio(p_user_id) # get all existing shares for user
-    
-    for element in my_portfolio:
-        if element["count"] != '' and element["isin"] != '':
-            print(element["count"], element["isin"])
-            share_symbols.append(element["isin"])
-            share_amounts.append(element["count"])
-
-    my_user = p_my_handler.get_user(p_user_id)
-    send_to_user("Hello %s this is your share update for today:"%str(my_user["username"]), pUser_id=p_user_id)
-    
-    shares = p_my_handler.get_user_shares(p_user_id) # all interest shares
-    
-    if len(share_symbols) != 0: # iterate through all shares
-        for i in range(len(share_symbols)):
-            my_price = share_fetcher.get_share_price_no_currency(share_symbols[i])
-            my_update_message = f'{share_fetcher.get_share_information_markdown(share_symbols[i])}\ncount: {hf.make_markdown_proof(share_amounts[i])}\nTotal: {hf.make_markdown_proof(round(float(my_price) * float(share_amounts[i]), 2))} EUR'
-            bot.send_message(chat_id=p_user_id, text=my_update_message, parse_mode="MARKDOWNV2")
-    else:
-        send_to_user("No shares found for your account. Check https://gruppe1.testsites.info to change your settings and add shares.", pUser_id=p_user_id)
-
-    if len(shares) != 0:  # Send updates on watchlist shares if existing
-        send_to_user("Your watchlist shares:", pUser_id=p_user_id)
-        for element in shares:
-            send_to_user(share_fetcher.get_share_information_markdown(element), pUser_id=p_user_id, md_mode=True)
-
-    keywords = p_my_handler.get_user_keywords(p_user_id)  # get keywords as array
-
-    if (keywords):  # if keywords exist and array is not empty
-        send_to_user("If you haven't read yet: \nHere are some interesting news according to your keywords:", pUser_id=p_user_id)
-        for keyword in keywords:
-            news = news_fetcher.get_top_news_by_keyword(keyword)["articles"]
-            keyword = hf.make_markdown_proof(keyword)
-
-            if not news: # if empty news array
-                send_to_user(f"No news found for keyword _{keyword}_\.", pUser_id=p_user_id, md_mode=True)
-            
-            elif news == None: # if news is none
-                send_to_user(f"Server error for keyword _{keyword}_\.", pUser_id=p_user_id, md_mode=True)
-            else:
-                news_formatted = news_fetcher.format_article(news[0])  # format for message, only use the most popular article
-                send_to_user(f"_keyword: {keyword}_\n\n{news_formatted}", pUser_id=p_user_id, md_mode=True)  # send news with related keyword in Markdown
-
-
-def send_to_user(pText, pUser_id, md_mode=False):
-    """ Send message to user
-    :type pText: string
-    :param pText: Text to send to user
-
-    :type pUser_id: int
-    :param pUser_id: user to send to. per default me (Florian Kellermann)
-
-    :type md_mode: boolean
-    :param md_mode: if true, parse_mode is markdown
-
-    :raises: none
-
-    :rtype: none
-    """
-    if md_mode:
-        bot.send_message(chat_id=pUser_id, text=pText, parse_mode="MARKDOWNV2")
-    else:
-        bot.send_message(chat_id=pUser_id, text=pText)
-
-
-if __name__ == "__main__":
-    try:
-        start_updater()
-        sys.exit(-1)
-    except KeyboardInterrupt:
-        print("Ending")
-        sys.exit(-1)
-
-
-
-
-
-
-
-

Functions

-
-
-def send_to_user(pText, pUser_id, md_mode=False) -
-
-

Send message to user -:type pText: string -:param pText: Text to send to user

-

:type pUser_id: int -:param pUser_id: user to send to. per default me (Florian Kellermann)

-

:type md_mode: boolean -:param md_mode: if true, parse_mode is markdown

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
def send_to_user(pText, pUser_id, md_mode=False):
-    """ Send message to user
-    :type pText: string
-    :param pText: Text to send to user
-
-    :type pUser_id: int
-    :param pUser_id: user to send to. per default me (Florian Kellermann)
-
-    :type md_mode: boolean
-    :param md_mode: if true, parse_mode is markdown
-
-    :raises: none
-
-    :rtype: none
-    """
-    if md_mode:
-        bot.send_message(chat_id=pUser_id, text=pText, parse_mode="MARKDOWNV2")
-    else:
-        bot.send_message(chat_id=pUser_id, text=pText)
-
-
-
-def start_updater() -
-
-

starting function for regularly sending updates -:raises: none

-

:rtype: none

-
- -Expand source code - -
def start_updater():
-    """ starting function for regularly sending updates
-    :raises: none
-
-    :rtype: none
-    """
-
-    print("Bot updates started")
-
-    my_handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD")))
-
-    update_crontab(my_handler)
-
-
-
-def update_based_on_crontab(p_user_ids, p_user_crontab, p_my_handler) -
-
-

Check all the crontab codes and add jobs to start in time -:type p_user_ids: array -:param p_user_ids: user id array of all users

-

:type p_user_crontab: array -:param p_user_crontab: crontabs for all users equivalent to the user array

-

:type p_my_handler: Api_Handler -:param p_my_handler: get database stuff

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
def update_based_on_crontab(p_user_ids, p_user_crontab, p_my_handler):
-    """ Check all the crontab codes and add jobs to start in time
-    :type p_user_ids: array
-    :param p_user_ids: user id array of all users
-
-    :type p_user_crontab: array
-    :param p_user_crontab: crontabs for all users equivalent to the user array
-
-    :type p_my_handler: Api_Handler
-    :param p_my_handler: get database stuff
-
-    :raises: none
-
-    :rtype: none
-    """
-    
-    my_scheduler = BackgroundScheduler() # schedule sends based on cron
-    
-    for i in range(len(p_user_ids)):
-        cron_split = p_user_crontab[i].split(" ") # split it up to use in scheduler
-        print(cron_split[4], cron_split[1], cron_split[0], cron_split[3], cron_split[2])
-        my_scheduler.add_job(update_for_user, 'cron', day_of_week=cron_split[4], hour=cron_split[1], minute=cron_split[0], month=cron_split[3], day=cron_split[2], args=(p_user_ids[i], p_my_handler))
-
-    my_scheduler.start()
-    
-    time.sleep( 600 ) # scheduler runs in background and I wait 10mins
-    my_scheduler.shutdown() # after this the new crontabs will be loaded
-
-
-
-def update_crontab(p_my_handler) -
-
-

Updating crontab lists every hour -:type pCurrent_Time: time when starting crontab update -:param pCurrent_Time: datetime

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
def update_crontab(p_my_handler):
-    """ Updating crontab lists every hour
-    :type pCurrent_Time: time when starting crontab update
-    :param pCurrent_Time: datetime
-
-    :raises: none
-
-    :rtype: none
-    """
-
-    global user_crontab
-    global user_ids
-    
-    all_users = p_my_handler.get_all_users() # get all users so crontabs can update for everybody
-    
-    user_ids = []
-    user_crontab = []
-
-    for element in all_users:
-        if element["cron"] != '' and element["telegram_user_id"] != '': # check if both values are existing so I have consistent data
-            try:
-                user_ids.append(int(element["telegram_user_id"]))
-                try:
-                    user_crontab.append(str(element["cron"]))
-                except: 
-                    user_ids.pop() # if something goes wrong with cron I have to delete matching user id 
-            except: continue
-                
-    
-    print(user_ids)
-
-    update_based_on_crontab(user_ids, user_crontab, p_my_handler)
-    
-    update_crontab(p_my_handler) # restart the update after time sleep
-
-
-
-def update_for_user(p_user_id, p_my_handler) -
-
-

Pull shares and send updates for specific user id -:type p_user_id: integer -:param p_user_id: user id of user that shall receive update

-

:type p_my_handler: Api_Handler -:param p_my_handler: handle the api and pull from database

-

:raises: none

-

:rtype: none

-
- -Expand source code - -
def update_for_user(p_user_id, p_my_handler):
-    """ Pull shares and send updates for specific user id
-    :type p_user_id: integer
-    :param p_user_id: user id of user that shall receive update
-
-    :type p_my_handler: Api_Handler
-    :param p_my_handler: handle the api and pull from database
-
-    :raises: none
-
-    :rtype: none
-    """
-    share_symbols = []
-    share_amounts = []
-    
-    my_portfolio = p_my_handler.get_user_portfolio(p_user_id) # get all existing shares for user
-    
-    for element in my_portfolio:
-        if element["count"] != '' and element["isin"] != '':
-            print(element["count"], element["isin"])
-            share_symbols.append(element["isin"])
-            share_amounts.append(element["count"])
-
-    my_user = p_my_handler.get_user(p_user_id)
-    send_to_user("Hello %s this is your share update for today:"%str(my_user["username"]), pUser_id=p_user_id)
-    
-    shares = p_my_handler.get_user_shares(p_user_id) # all interest shares
-    
-    if len(share_symbols) != 0: # iterate through all shares
-        for i in range(len(share_symbols)):
-            my_price = share_fetcher.get_share_price_no_currency(share_symbols[i])
-            my_update_message = f'{share_fetcher.get_share_information_markdown(share_symbols[i])}\ncount: {hf.make_markdown_proof(share_amounts[i])}\nTotal: {hf.make_markdown_proof(round(float(my_price) * float(share_amounts[i]), 2))} EUR'
-            bot.send_message(chat_id=p_user_id, text=my_update_message, parse_mode="MARKDOWNV2")
-    else:
-        send_to_user("No shares found for your account. Check https://gruppe1.testsites.info to change your settings and add shares.", pUser_id=p_user_id)
-
-    if len(shares) != 0:  # Send updates on watchlist shares if existing
-        send_to_user("Your watchlist shares:", pUser_id=p_user_id)
-        for element in shares:
-            send_to_user(share_fetcher.get_share_information_markdown(element), pUser_id=p_user_id, md_mode=True)
-
-    keywords = p_my_handler.get_user_keywords(p_user_id)  # get keywords as array
-
-    if (keywords):  # if keywords exist and array is not empty
-        send_to_user("If you haven't read yet: \nHere are some interesting news according to your keywords:", pUser_id=p_user_id)
-        for keyword in keywords:
-            news = news_fetcher.get_top_news_by_keyword(keyword)["articles"]
-            keyword = hf.make_markdown_proof(keyword)
-
-            if not news: # if empty news array
-                send_to_user(f"No news found for keyword _{keyword}_\.", pUser_id=p_user_id, md_mode=True)
-            
-            elif news == None: # if news is none
-                send_to_user(f"Server error for keyword _{keyword}_\.", pUser_id=p_user_id, md_mode=True)
-            else:
-                news_formatted = news_fetcher.format_article(news[0])  # format for message, only use the most popular article
-                send_to_user(f"_keyword: {keyword}_\n\n{news_formatted}", pUser_id=p_user_id, md_mode=True)  # send news with related keyword in Markdown
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/documentation/telegram_bot/helper_functions.html b/documentation/telegram_bot/helper_functions.html deleted file mode 100644 index 5f08a9a..0000000 --- a/documentation/telegram_bot/helper_functions.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - -telegram_bot.helper_functions API documentation - - - - - - - - - - - -
-
-
-

Module telegram_bot.helper_functions

-
-
-

script for helper functions for bot related stuff

-
- -Expand source code - -
"""
-script for helper functions for bot related stuff
-"""
-__author__ = "Florian Kellermann, Linus Eickhoff"
-__date__ = "10.05.2022"
-__version__ = "1.0.0"
-__license__ = "None"
-
-
-def contains_markdownv1_symbols(text):
-    """ checks if text contains markdown symbols
-    :type text: string
-
-    :param text: text to check
-
-    :return: true if text contains markdown symbols
-
-    :rtype: bool
-    """
-    if text.find("_") != -1 or text.find("*") != -1 or text.find("`") != -1:  # check if text contains relevant markdown symbols
-        return True
-
-    return False
-
-
-def make_markdown_proof(text):  # used to avoid errors related to markdown parsemode for telegram messaging
-    """ makes text markdown proof
-    :type text: string
-
-    :param text: text to make markdown proof
-
-    :return: markdown proof text
-
-    :rtype: string
-    """
-    text = str(text)
-
-    text = text.replace("_", "\\_")  # replace _ with \_ because \ is used as escape character in markdown, double escape is needed because \ is also a escape character in strings
-    text = text.replace("*", "\\*")
-    text = text.replace("`", "\\`")
-    text = text.replace("[", "\\[")
-    text = text.replace("]", "\\]")
-    text = text.replace("(", "\\(")
-    text = text.replace(")", "\\)")
-    text = text.replace("#", "\\#")
-    text = text.replace("+", "\\+")
-    text = text.replace("-", "\\-")
-    text = text.replace("!", "\\!")
-    text = text.replace(".", "\\.")
-    text = text.replace("?", "\\?")
-    text = text.replace("/", "\\/")
-    text = text.replace("~", "\\~")
-    text = text.replace("|", "\\|")
-    text = text.replace("<", "\\<")
-    text = text.replace(">", "\\>")
-    text = text.replace("&", "\\&")
-    text = text.replace("^", "\\^")
-    text = text.replace("$", "\\$")
-    text = text.replace("%", "\\%")
-    text = text.replace("=", "\\=")
-    text = text.replace("@", "\\@")
-
-    return text
-
-
-if __name__ == '__main__':
-    print("this is a module for helper functions for the bot and should not be run directly")
-    print(make_markdown_proof("_test_"))
-    text = make_markdown_proof("_test_")
-    print(f"{text}")
-
-
-
-
-
-
-
-

Functions

-
-
-def contains_markdownv1_symbols(text) -
-
-

checks if text contains markdown symbols -:type text: string

-

:param text: text to check

-

:return: true if text contains markdown symbols

-

:rtype: bool

-
- -Expand source code - -
def contains_markdownv1_symbols(text):
-    """ checks if text contains markdown symbols
-    :type text: string
-
-    :param text: text to check
-
-    :return: true if text contains markdown symbols
-
-    :rtype: bool
-    """
-    if text.find("_") != -1 or text.find("*") != -1 or text.find("`") != -1:  # check if text contains relevant markdown symbols
-        return True
-
-    return False
-
-
-
-def make_markdown_proof(text) -
-
-

makes text markdown proof -:type text: string

-

:param text: text to make markdown proof

-

:return: markdown proof text

-

:rtype: string

-
- -Expand source code - -
def make_markdown_proof(text):  # used to avoid errors related to markdown parsemode for telegram messaging
-    """ makes text markdown proof
-    :type text: string
-
-    :param text: text to make markdown proof
-
-    :return: markdown proof text
-
-    :rtype: string
-    """
-    text = str(text)
-
-    text = text.replace("_", "\\_")  # replace _ with \_ because \ is used as escape character in markdown, double escape is needed because \ is also a escape character in strings
-    text = text.replace("*", "\\*")
-    text = text.replace("`", "\\`")
-    text = text.replace("[", "\\[")
-    text = text.replace("]", "\\]")
-    text = text.replace("(", "\\(")
-    text = text.replace(")", "\\)")
-    text = text.replace("#", "\\#")
-    text = text.replace("+", "\\+")
-    text = text.replace("-", "\\-")
-    text = text.replace("!", "\\!")
-    text = text.replace(".", "\\.")
-    text = text.replace("?", "\\?")
-    text = text.replace("/", "\\/")
-    text = text.replace("~", "\\~")
-    text = text.replace("|", "\\|")
-    text = text.replace("<", "\\<")
-    text = text.replace(">", "\\>")
-    text = text.replace("&", "\\&")
-    text = text.replace("^", "\\^")
-    text = text.replace("$", "\\$")
-    text = text.replace("%", "\\%")
-    text = text.replace("=", "\\=")
-    text = text.replace("@", "\\@")
-
-    return text
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/documentation/telegram_bot/index.html b/documentation/telegram_bot/index.html deleted file mode 100644 index ed6c2bb..0000000 --- a/documentation/telegram_bot/index.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - -telegram_bot API documentation - - - - - - - - - - - -
- - -
- - - \ No newline at end of file diff --git a/documentation/telegram_bot/news/index.html b/documentation/telegram_bot/news/index.html deleted file mode 100644 index 76fbf5f..0000000 --- a/documentation/telegram_bot/news/index.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - -telegram_bot.news API documentation - - - - - - - - - - - -
- - -
- - - \ No newline at end of file diff --git a/documentation/telegram_bot/news/news_fetcher.html b/documentation/telegram_bot/news/news_fetcher.html deleted file mode 100644 index 9211d75..0000000 --- a/documentation/telegram_bot/news/news_fetcher.html +++ /dev/null @@ -1,264 +0,0 @@ - - - - - - -telegram_bot.news.news_fetcher API documentation - - - - - - - - - - - -
-
-
-

Module telegram_bot.news.news_fetcher

-
-
-

script for news fetching (by keywords)

-
- -Expand source code - -
"""
-script for news fetching (by keywords)
-"""
-__author__ = "Florian Kellermann, Linus Eickhoff"
-__date__ = "26.04.2022"
-__version__ = "1.0.0"
-__license__ = "None"
-
-import os
-import sys
-
-import telegram_bot.helper_functions as hf
-import requests
-from dotenv import load_dotenv
-from newsapi import NewsApiClient
-
-load_dotenv()  # loads environment vars
-
-# Init
-api_key = os.getenv('NEWS_API_KEY')  # get API Key from .env file
-newsapi = NewsApiClient(api_key=api_key)  # news api from https://newsapi.org/
-
-try:
-    # get all available news sources (e.g BBC, New York Times, etc.)
-    source_json = requests.get(f"https://newsapi.org/v2/top-headlines/sources?apiKey={api_key}&language=en").json()
-    sources = source_json["sources"]
-    str_sources = ",".join([source["id"] for source in sources])
-
-except KeyError:
-    print("Error: Could not get sources, may be blocked because of too many requests (free newsapi is limited to 100 reqs per day)")
-    str_sources = str(
-        "Reuters, bbc, cnn, fox-news, google-news, hacker-news, nytimes, the-huffington-post, the-new-york-times, business-insider, bbc-news, cbc-news, ESPN, fox-sports, google-news-uk, independent, the-wall-street-journal, the-washington-times, time, usa-today")
-
-
-def get_all_news_by_keyword(keyword, from_date="2000-01-01"):
-    """get all news to keyword
-    Args:
-        keyword (String): keyword for search
-        from_date (String): min date for search
-
-    Returns:
-        JSON/dict: dict containing articles
-    """
-    top_headlines = newsapi.get_everything(q=keyword, sources=str_sources, language='en', from_param=from_date)  # keywords can be combined with OR (e.g. keyword = "bitcoin OR ethereum")
-    if (top_headlines["status"] == "ok"):
-        return top_headlines
-    else:
-        return None
-
-
-def get_top_news_by_keyword(keyword):
-    """get top news to keyword
-    Args:
-        keyword (String): keyword for search
-
-    Returns:
-        JSON/dict: dict containing articles
-    """
-    top_headlines = newsapi.get_top_headlines(q=keyword, sources=str_sources, language='en')  # get top headlines, measured by popularity from NewsApi
-    if (top_headlines["status"] == "ok"):
-        return top_headlines
-    else:
-        return None
-
-
-def format_article(article):
-    """format article for messaging (using markdown syntax)
-
-    Args:
-        article (dict): article to format for messaging
-
-    Returns:
-        String: formatted article
-    """
-    sourcename = hf.make_markdown_proof(article["source"]["name"])  # make attributes markdownv2 proof
-    headline = hf.make_markdown_proof(article["title"])
-    url = hf.make_markdown_proof(article["url"])
-    formatted_article = f"_{sourcename}_\n*{headline}*\n\n{url}"  # formatting in Markdown syntax
-
-    return formatted_article
-
-
-if __name__ == '__main__':  # only execute if script is called directly -> for simple testing
-
-    print("this is a module and should not be run directly")
-    print("fetching top news by keyword bitcoin...")
-
-    articles = get_all_news_by_keyword("bitcoin")
-    formatted_article = format_article(articles["articles"][0])
-    print(formatted_article)
-    articles = get_top_news_by_keyword("bitcoin")
-    formatted_article = format_article(articles["articles"][0])
-    print(formatted_article)
-    sys.exit(1)
-
-
-
-
-
-
-
-

Functions

-
-
-def format_article(article) -
-
-

format article for messaging (using markdown syntax)

-

Args

-
-
article : dict
-
article to format for messaging
-
-

Returns

-
-
String
-
formatted article
-
-
- -Expand source code - -
def format_article(article):
-    """format article for messaging (using markdown syntax)
-
-    Args:
-        article (dict): article to format for messaging
-
-    Returns:
-        String: formatted article
-    """
-    sourcename = hf.make_markdown_proof(article["source"]["name"])  # make attributes markdownv2 proof
-    headline = hf.make_markdown_proof(article["title"])
-    url = hf.make_markdown_proof(article["url"])
-    formatted_article = f"_{sourcename}_\n*{headline}*\n\n{url}"  # formatting in Markdown syntax
-
-    return formatted_article
-
-
-
-def get_all_news_by_keyword(keyword, from_date='2000-01-01') -
-
-

get all news to keyword

-

Args

-
-
keyword : String
-
keyword for search
-
from_date : String
-
min date for search
-
-

Returns

-

JSON/dict: dict containing articles

-
- -Expand source code - -
def get_all_news_by_keyword(keyword, from_date="2000-01-01"):
-    """get all news to keyword
-    Args:
-        keyword (String): keyword for search
-        from_date (String): min date for search
-
-    Returns:
-        JSON/dict: dict containing articles
-    """
-    top_headlines = newsapi.get_everything(q=keyword, sources=str_sources, language='en', from_param=from_date)  # keywords can be combined with OR (e.g. keyword = "bitcoin OR ethereum")
-    if (top_headlines["status"] == "ok"):
-        return top_headlines
-    else:
-        return None
-
-
-
-def get_top_news_by_keyword(keyword) -
-
-

get top news to keyword

-

Args

-
-
keyword : String
-
keyword for search
-
-

Returns

-

JSON/dict: dict containing articles

-
- -Expand source code - -
def get_top_news_by_keyword(keyword):
-    """get top news to keyword
-    Args:
-        keyword (String): keyword for search
-
-    Returns:
-        JSON/dict: dict containing articles
-    """
-    top_headlines = newsapi.get_top_headlines(q=keyword, sources=str_sources, language='en')  # get top headlines, measured by popularity from NewsApi
-    if (top_headlines["status"] == "ok"):
-        return top_headlines
-    else:
-        return None
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/documentation/telegram_bot/shares/index.html b/documentation/telegram_bot/shares/index.html deleted file mode 100644 index 7707868..0000000 --- a/documentation/telegram_bot/shares/index.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - -telegram_bot.shares API documentation - - - - - - - - - - - -
-
-
-

Namespace telegram_bot.shares

-
-
-
-
-

Sub-modules

-
-
telegram_bot.shares.share_fetcher
-
-

script for share fetching (by symbols (e.g. AAPL, TSLA etc.))

-
-
-
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/documentation/telegram_bot/shares/share_fetcher.html b/documentation/telegram_bot/shares/share_fetcher.html deleted file mode 100644 index bb0e33c..0000000 --- a/documentation/telegram_bot/shares/share_fetcher.html +++ /dev/null @@ -1,384 +0,0 @@ - - - - - - -telegram_bot.shares.share_fetcher API documentation - - - - - - - - - - - -
-
-
-

Module telegram_bot.shares.share_fetcher

-
-
-

script for share fetching (by symbols (e.g. AAPL, TSLA etc.))

-
- -Expand source code - -
"""
-script for share fetching (by symbols (e.g. AAPL, TSLA etc.))
-"""
-__author__ = "Florian Kellermann, Linus Eickhoff"
-__date__ = "10.05.2022"
-__version__ = "1.0.1"
-__license__ = "None"
-
-import telegram_bot.helper_functions as hf
-import investpy
-import pandas
-from currency_converter import CurrencyConverter
-
-
-def get_share_price(str_search_for):
-    """get stock price per share for company name or isin or symbol
-
-    Args:
-        str_search_for (string): search for this string/isin
-
-    Returns: none
-    """
-    try:
-        search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], n_results=1, countries=['germany'])
-
-        currency = str(search_result.retrieve_currency()) # retrieve currency from data
-        # should always be Euro because of countries=['germany']
-        
-        recent_data = pandas.DataFrame(search_result.retrieve_recent_data()) # stock prices of last few days
-        
-        stock_price = recent_data.iloc[-1]["Close"] # retrieve latest stock price
-        
-        stock_price = round(float(stock_price), 2)
-        
-        str_return =str(stock_price) + " " + str(currency) # return + currency 
-    
-        return str_return
-    
-    except RuntimeError: # if no shares are found for germany (e.g. isin: US.....)
-        try:
-            my_Converter = CurrencyConverter() # need a currency converter
-            
-            search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], n_results=1)
-
-            currency = str(search_result.retrieve_currency())
-
-            recent_data = pandas.DataFrame(search_result.retrieve_recent_data())
-
-            stock_price = recent_data.iloc[-1]["Close"]
-            
-            #convert stock price from currency to EUR
-            stock_price = my_Converter.convert(float(stock_price), str(currency), 'EUR') 
-            
-            stock_price = round(float(stock_price), 2)
-
-            str_return = str(stock_price) + " EUR"
-
-            return str_return
-
-        except RuntimeError:
-            return "None"
-
-
-def get_share_price_no_currency(str_search_for):
-    """get stock price per share for company name or isin or symbol no currency
-    Args:
-        str_search_for (string): search for this string/isin
-    Returns: none
-    """
-    try:
-        search_result = investpy.search_quotes(text=str_search_for, products=['stocks'],
-                                               countries=['germany'], n_results=1)
-
-        recent_data = pandas.DataFrame(search_result.retrieve_recent_data())
-
-        stock_price = recent_data.iloc[-1]["Close"]
-
-        stock_price = round(float(stock_price), 2)
-
-        return stock_price
-
-    except RuntimeError:
-        my_Converter = CurrencyConverter()
-
-        search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], n_results=1)
-
-        currency = str(search_result.retrieve_currency())
-
-        recent_data = pandas.DataFrame(search_result.retrieve_recent_data())
-
-        stock_price = recent_data.iloc[-1]["Close"]
-
-        stock_price = my_Converter.convert(float(stock_price), str(currency), 'EUR')
-
-        stock_price = round(float(stock_price), 2)
-
-        str_return = str(stock_price)
-
-        return str_return
-
-
-def get_share_information(str_search_for):
-    search_result = investpy.search_quotes(text=str_search_for, products=['stocks'],
-                                           countries=['germany'], n_results=1)
-
-    str_return = "Company: " + search_result.name + "\nSymbol: " + search_result.symbol + "\nCurrent Price/Share: " + get_share_price(str_search_for)
-
-    return str_return
-
-
-def get_share_information_markdown(str_search_for):
-
-    try:
-        search_result = investpy.search_quotes(text=str_search_for, products=['stocks'],
-                                                countries=['germany'], n_results=1)
-    
-    except RuntimeError as e:
-        return hf.make_markdown_proof(f"no shares found for \"{str_search_for}\"") # if no shares are found, make error message markdown proof and return
-
-    except ConnectionError as e:
-        return hf.make_markdown_proof(f"connection not possible. Try again later.") # if no connection, make error message markdown proof and return
-    
-    str_return = f'*{hf.make_markdown_proof(search_result.name)}*\n_{hf.make_markdown_proof(search_result.symbol)}_\nworth: {hf.make_markdown_proof(get_share_price(str_search_for))}'
-    return str_return
-
-
-def get_share_information_simple(str_search_for):
-    search_result = investpy.search_quotes(text=str_search_for, products=['stocks'],
-                                           countries=['germany'], n_results=1)
-
-    str_return = search_result.name + "\n" + search_result.symbol + "\nworth: " + get_share_price(str_search_for)
-    return str_return
-
-
-if __name__ == "__main__":
-    print("None")
-
-
-
-
-
-
-
-

Functions

-
-
-def get_share_information(str_search_for) -
-
-
-
- -Expand source code - -
def get_share_information(str_search_for):
-    search_result = investpy.search_quotes(text=str_search_for, products=['stocks'],
-                                           countries=['germany'], n_results=1)
-
-    str_return = "Company: " + search_result.name + "\nSymbol: " + search_result.symbol + "\nCurrent Price/Share: " + get_share_price(str_search_for)
-
-    return str_return
-
-
-
-def get_share_information_markdown(str_search_for) -
-
-
-
- -Expand source code - -
def get_share_information_markdown(str_search_for):
-
-    try:
-        search_result = investpy.search_quotes(text=str_search_for, products=['stocks'],
-                                                countries=['germany'], n_results=1)
-    
-    except RuntimeError as e:
-        return hf.make_markdown_proof(f"no shares found for \"{str_search_for}\"") # if no shares are found, make error message markdown proof and return
-
-    except ConnectionError as e:
-        return hf.make_markdown_proof(f"connection not possible. Try again later.") # if no connection, make error message markdown proof and return
-    
-    str_return = f'*{hf.make_markdown_proof(search_result.name)}*\n_{hf.make_markdown_proof(search_result.symbol)}_\nworth: {hf.make_markdown_proof(get_share_price(str_search_for))}'
-    return str_return
-
-
-
-def get_share_information_simple(str_search_for) -
-
-
-
- -Expand source code - -
def get_share_information_simple(str_search_for):
-    search_result = investpy.search_quotes(text=str_search_for, products=['stocks'],
-                                           countries=['germany'], n_results=1)
-
-    str_return = search_result.name + "\n" + search_result.symbol + "\nworth: " + get_share_price(str_search_for)
-    return str_return
-
-
-
-def get_share_price(str_search_for) -
-
-

get stock price per share for company name or isin or symbol

-

Args

-
-
str_search_for : string
-
search for this string/isin
-
-

Returns: none

-
- -Expand source code - -
def get_share_price(str_search_for):
-    """get stock price per share for company name or isin or symbol
-
-    Args:
-        str_search_for (string): search for this string/isin
-
-    Returns: none
-    """
-    try:
-        search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], n_results=1, countries=['germany'])
-
-        currency = str(search_result.retrieve_currency()) # retrieve currency from data
-        # should always be Euro because of countries=['germany']
-        
-        recent_data = pandas.DataFrame(search_result.retrieve_recent_data()) # stock prices of last few days
-        
-        stock_price = recent_data.iloc[-1]["Close"] # retrieve latest stock price
-        
-        stock_price = round(float(stock_price), 2)
-        
-        str_return =str(stock_price) + " " + str(currency) # return + currency 
-    
-        return str_return
-    
-    except RuntimeError: # if no shares are found for germany (e.g. isin: US.....)
-        try:
-            my_Converter = CurrencyConverter() # need a currency converter
-            
-            search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], n_results=1)
-
-            currency = str(search_result.retrieve_currency())
-
-            recent_data = pandas.DataFrame(search_result.retrieve_recent_data())
-
-            stock_price = recent_data.iloc[-1]["Close"]
-            
-            #convert stock price from currency to EUR
-            stock_price = my_Converter.convert(float(stock_price), str(currency), 'EUR') 
-            
-            stock_price = round(float(stock_price), 2)
-
-            str_return = str(stock_price) + " EUR"
-
-            return str_return
-
-        except RuntimeError:
-            return "None"
-
-
-
-def get_share_price_no_currency(str_search_for) -
-
-

get stock price per share for company name or isin or symbol no currency

-

Args

-
-
str_search_for : string
-
search for this string/isin
-
-

Returns: none

-
- -Expand source code - -
def get_share_price_no_currency(str_search_for):
-    """get stock price per share for company name or isin or symbol no currency
-    Args:
-        str_search_for (string): search for this string/isin
-    Returns: none
-    """
-    try:
-        search_result = investpy.search_quotes(text=str_search_for, products=['stocks'],
-                                               countries=['germany'], n_results=1)
-
-        recent_data = pandas.DataFrame(search_result.retrieve_recent_data())
-
-        stock_price = recent_data.iloc[-1]["Close"]
-
-        stock_price = round(float(stock_price), 2)
-
-        return stock_price
-
-    except RuntimeError:
-        my_Converter = CurrencyConverter()
-
-        search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], n_results=1)
-
-        currency = str(search_result.retrieve_currency())
-
-        recent_data = pandas.DataFrame(search_result.retrieve_recent_data())
-
-        stock_price = recent_data.iloc[-1]["Close"]
-
-        stock_price = my_Converter.convert(float(stock_price), str(currency), 'EUR')
-
-        stock_price = round(float(stock_price), 2)
-
-        str_return = str(stock_price)
-
-        return str_return
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 931291d..35388bb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,59 +14,59 @@ "@angular/compiler": "~13.2.0", "@angular/core": "~13.2.0", "@angular/forms": "~13.2.0", - "@angular/material": "^13.3.6", + "@angular/material": "^13.2.6", "@angular/platform-browser": "~13.2.0", "@angular/platform-browser-dynamic": "~13.2.0", "@angular/router": "~13.2.0", "bootstrap": "^5.1.3", "ngx-cron-editor": "^0.7.3", "rxjs": "~7.5.0", - "tslib": "^2.4.0", + "tslib": "^2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { - "@angular-devkit/build-angular": "~13.3.5", + "@angular-devkit/build-angular": "~13.2.5", "@angular-eslint/builder": "13.2.1", "@angular-eslint/eslint-plugin": "13.2.1", "@angular-eslint/eslint-plugin-template": "13.2.1", "@angular-eslint/schematics": "13.2.1", "@angular-eslint/template-parser": "13.2.1", - "@angular/cli": "~13.3.5", + "@angular/cli": "~13.2.5", "@angular/compiler-cli": "~13.2.0", - "@types/jasmine": "~4.0.3", + "@types/jasmine": "~3.10.0", "@types/node": "^12.11.1", - "@typescript-eslint/eslint-plugin": "5.23.0", - "@typescript-eslint/parser": "5.22.0", - "eslint": "^8.15.0", - "jasmine-core": "~4.1.0", - "karma": "~6.3.19", + "@typescript-eslint/eslint-plugin": "5.17.0", + "@typescript-eslint/parser": "5.17.0", + "eslint": "^8.12.0", + "jasmine-core": "~4.0.0", + "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", - "karma-coverage": "~2.2.0", - "karma-jasmine": "~5.0.0", + "karma-coverage": "~2.1.0", + "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.5.2" } }, "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-1.1.1.tgz", + "integrity": "sha512-YVAcA4DKLOj296CF5SrQ8cYiMRiUGc2sqFpLxsDGWE34suHqhGP/5yMsDHKsrh8hs8I5TiRVXNwKPWQpX3iGjw==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/resolve-uri": "^3.0.3", + "sourcemap-codec": "1.4.8" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@angular-devkit/architect": { - "version": "0.1303.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1303.5.tgz", - "integrity": "sha512-ZF5Vul8UqwDSwYPxJ4YvdG7lmciJZ1nncyt9Dbk0swxw4MGdy0ZIf+91o318qUn/5JrttQ7ZCYoCZJCjYOSBtw==", + "version": "0.1302.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1302.6.tgz", + "integrity": "sha512-NztzorUMfwJeRaT7SY00Y8WSqc2lQYuF11yNoyEm7Dae3V7VZ28rW2Z9RwibP27rYQL0RjSMaz2wKITHX2vOAw==", "dev": true, "dependencies": { - "@angular-devkit/core": "13.3.5", + "@angular-devkit/core": "13.2.6", "rxjs": "6.6.7" }, "engines": { @@ -94,15 +94,15 @@ "dev": true }, "node_modules/@angular-devkit/build-angular": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-13.3.5.tgz", - "integrity": "sha512-6ZQ788U0vT7KqMZeOsNQxP01IhOpxlbKonxK2fZNju8e+Ha2K77yV9A9XMbmcUGWRRHCOFvUEaJhvxDFsunESg==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-13.2.6.tgz", + "integrity": "sha512-Y2ojy6xbZ0kwScppcutLHBP8eW0qNOjburTISSBU/L5l/9FOeZ1E7yAreKuVu/qibZiLbSJfAhk+SLwhRHFSSQ==", "dev": true, "dependencies": { - "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1303.5", - "@angular-devkit/build-webpack": "0.1303.5", - "@angular-devkit/core": "13.3.5", + "@ampproject/remapping": "1.1.1", + "@angular-devkit/architect": "0.1302.6", + "@angular-devkit/build-webpack": "0.1302.6", + "@angular-devkit/core": "13.2.6", "@babel/core": "7.16.12", "@babel/generator": "7.16.8", "@babel/helper-annotate-as-pure": "7.16.7", @@ -113,7 +113,7 @@ "@babel/runtime": "7.16.7", "@babel/template": "7.16.7", "@discoveryjs/json-ext": "0.5.6", - "@ngtools/webpack": "13.3.5", + "@ngtools/webpack": "13.2.6", "ansi-colors": "4.1.1", "babel-loader": "8.2.3", "babel-plugin-istanbul": "6.1.1", @@ -135,7 +135,7 @@ "license-webpack-plugin": "4.0.2", "loader-utils": "3.2.0", "mini-css-extract-plugin": "2.5.3", - "minimatch": "3.0.5", + "minimatch": "3.0.4", "open": "8.4.0", "ora": "5.4.1", "parse5-html-rewriting-stream": "6.0.1", @@ -147,7 +147,7 @@ "regenerator-runtime": "0.13.9", "resolve-url-loader": "5.0.0", "rxjs": "6.6.7", - "sass": "1.49.9", + "sass": "1.49.0", "sass-loader": "12.4.0", "semver": "7.3.5", "source-map-loader": "3.0.1", @@ -158,7 +158,7 @@ "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.3.1", - "webpack": "5.70.0", + "webpack": "5.67.0", "webpack-dev-middleware": "5.3.0", "webpack-dev-server": "4.7.3", "webpack-merge": "5.8.0", @@ -173,14 +173,14 @@ "esbuild": "0.14.22" }, "peerDependencies": { - "@angular/compiler-cli": "^13.0.0 || ^13.3.0-rc.0", - "@angular/localize": "^13.0.0 || ^13.3.0-rc.0", - "@angular/service-worker": "^13.0.0 || ^13.3.0-rc.0", + "@angular/compiler-cli": "^13.0.0", + "@angular/localize": "^13.0.0", + "@angular/service-worker": "^13.0.0", "karma": "^6.3.0", "ng-packagr": "^13.0.0", "protractor": "^7.0.0", "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=4.4.3 <4.7" + "typescript": ">=4.4.3 <4.6" }, "peerDependenciesMeta": { "@angular/localize": { @@ -203,18 +203,6 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -240,12 +228,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1303.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1303.5.tgz", - "integrity": "sha512-EI7scRGKPw9Rg4LypUSTf7JM3lE1imTVxY8mY6gqNkRWnvsb5+kptJQ+gK+VZSom/URcPFbN40lJYwgmZBNPeA==", + "version": "0.1302.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1302.6.tgz", + "integrity": "sha512-TYEh2n9tPe932rEIgdiSpojOqtDppW2jzb/empVqCkLF7WUZsXKvTanttZC34L6R2VD6SAGWhb6JDg75ghUVYA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1303.5", + "@angular-devkit/architect": "0.1302.6", "rxjs": "6.6.7" }, "engines": { @@ -277,9 +265,9 @@ "dev": true }, "node_modules/@angular-devkit/core": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.3.5.tgz", - "integrity": "sha512-w7vzK4VoYP9rLgxJ2SwEfrkpKybdD+QgQZlsDBzT0C6Ebp7b4gkNcNVFo8EiZvfDl6Yplw2IAP7g7fs3STn0hQ==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.2.6.tgz", + "integrity": "sha512-8h2mWdBTN/dYwZuzKMg2IODlOWMdbJcpQG4XVrkk9ejCPP+3aX5Aa3glCe/voN6eBNiRfs8YDM0jxmpN2aWVtg==", "dev": true, "dependencies": { "ajv": "8.9.0", @@ -322,12 +310,12 @@ "dev": true }, "node_modules/@angular-devkit/schematics": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.3.5.tgz", - "integrity": "sha512-0N/kL/Vfx0yVAEwa3HYxNx9wYb+G9r1JrLjJQQzDp+z9LtcojNf7j3oey6NXrDUs1WjVZOa/AIdRl3/DuaoG5w==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.2.6.tgz", + "integrity": "sha512-mPgSqdnZRuPSMeUA+T+mwVCrq2yhXpcYm1/Rjbhy09CyHs4wSrFv21WHCrE6shlvXpcmwr0n+I0DIeagAPmjUA==", "dev": true, "dependencies": { - "@angular-devkit/core": "13.3.5", + "@angular-devkit/core": "13.2.6", "jsonc-parser": "3.0.0", "magic-string": "0.25.7", "ora": "5.4.1", @@ -465,9 +453,9 @@ } }, "node_modules/@angular/cdk": { - "version": "13.3.6", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.6.tgz", - "integrity": "sha512-sPwEGCHARxuUMzPLRiiN51GQP0P39ev+eL06NcyYXZ3AxyZIH5N/PWmGKf7EDOXIof4ata13jsIeW38mFe+wvg==", + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.5.tgz", + "integrity": "sha512-fA99fGgybup9ezyB/IzOa9Mk8g8LjejkqikLnC3mAeQ0lROOO7Vf9Rp1v7/ahe2lALTUbA1bzJeXzQYaffkIiA==", "dependencies": { "tslib": "^2.3.0" }, @@ -481,16 +469,16 @@ } }, "node_modules/@angular/cli": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.5.tgz", - "integrity": "sha512-FrPg86cfmm0arWZInt55muCTpcQSNlvoViVrIVkyqSN06GoyCAQ2zn6/OYJnx/XAg/XvXTbygL+58c0WXuOaiA==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.2.6.tgz", + "integrity": "sha512-xIjEaQI5sWemXXc7GXLm4u9UL5sjtrQL/y1PJvvk/Jsa8+kIT+MutOfZfC7zcdAh9fqHd8mokH3guFV8BJdFxA==", "dev": true, "hasInstallScript": true, "dependencies": { - "@angular-devkit/architect": "0.1303.5", - "@angular-devkit/core": "13.3.5", - "@angular-devkit/schematics": "13.3.5", - "@schematics/angular": "13.3.5", + "@angular-devkit/architect": "0.1302.6", + "@angular-devkit/core": "13.2.6", + "@angular-devkit/schematics": "13.2.6", + "@schematics/angular": "13.2.6", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.1", "debug": "4.3.3", @@ -572,6 +560,19 @@ "typescript": ">=4.4.2 <4.6" } }, + "node_modules/@angular/compiler-cli/node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@angular/compiler-cli/node_modules/@babel/core": { "version": "7.17.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.9.tgz", @@ -679,15 +680,15 @@ } }, "node_modules/@angular/material": { - "version": "13.3.6", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-13.3.6.tgz", - "integrity": "sha512-V+3Fs9JK+7KlcdJG/Oa/IQuLHC8WYzuL8ffH1Q06ANSzGxSjsAHs4FZQpKUpVBoL3E+p9Yz+zkKe91k5L62VoQ==", + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-13.3.5.tgz", + "integrity": "sha512-4+FCb6Tbre5SwhZRKfnuh8K+/o+DuCGisJOuk7lxdFKDGDPjsPDWYVrBDal1N70mO09z/ApwNjpsIjuRv79wpg==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^13.0.0 || ^14.0.0-0", - "@angular/cdk": "13.3.6", + "@angular/cdk": "13.3.5", "@angular/common": "^13.0.0 || ^14.0.0-0", "@angular/core": "^13.0.0 || ^14.0.0-0", "@angular/forms": "^13.0.0 || ^14.0.0-0", @@ -2452,19 +2453,19 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", - "integrity": "sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", + "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.2", + "espree": "^9.3.1", "globals": "^13.9.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" }, "engines": { @@ -2494,9 +2495,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.14.0.tgz", - "integrity": "sha512-ERO68sOYwm5UuLvSJTY7w7NP2c8S4UcXs3X1GBX8cwOr+ShOcDBbCY5mH4zxz0jsYCdJ8ve8Mv9n2YGJMB1aeg==", + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", + "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2526,18 +2527,6 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -2649,9 +2638,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.3.5.tgz", - "integrity": "sha512-OaMZR0rO0ljBHamLwzddfZX03ijtpheUpjH5dNzMNyNrrpKgS4/3jTQ1wvs2j3zzKfKjOS12WG0905QFJYWG6g==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.2.6.tgz", + "integrity": "sha512-N8SvRV91+/57TcAfbghc0k0tKCukw/7KqbDaLPAQTGFekJ4xMGT3elMzOyBXTH3Hvp5HL8/hiBt2tG04qiMf+w==", "dev": true, "engines": { "node": "^12.20.0 || ^14.15.0 || >=16.10.0", @@ -2660,7 +2649,7 @@ }, "peerDependencies": { "@angular/compiler-cli": "^13.0.0", - "typescript": ">=4.4.3 <4.7", + "typescript": ">=4.4.3 <4.6", "webpack": "^5.30.0" } }, @@ -3211,13 +3200,13 @@ } }, "node_modules/@schematics/angular": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.3.5.tgz", - "integrity": "sha512-1Ovx0cq72ZaNCyTyRD8ebIwUzpqhEH9ypWF05bfBLq3J0LlZgewIMhPJSxKmwRC3NQB5DZIYEvD0uhzBIuHCCA==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.2.6.tgz", + "integrity": "sha512-8NzHMX9+FSgaB0lJYxlTJv9OcBuolwZJqo9M/yX3RPSqSHghA33jWwgVbV551hBJOpbVEePerG1DQkIC99DXKA==", "dev": true, "dependencies": { - "@angular-devkit/core": "13.3.5", - "@angular-devkit/schematics": "13.3.5", + "@angular-devkit/core": "13.2.6", + "@angular-devkit/schematics": "13.2.6", "jsonc-parser": "3.0.0" }, "engines": { @@ -3623,9 +3612,9 @@ } }, "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", + "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", "dev": true }, "node_modules/@types/express": { @@ -3652,18 +3641,18 @@ } }, "node_modules/@types/http-proxy": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "version": "1.17.8", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz", + "integrity": "sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/jasmine": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", - "integrity": "sha512-Opp1LvvEuZdk8fSSvchK2mZwhVrsNT0JgJE9Di6MjnaIpmEXM8TLCPPrVtNTYh8+5MPdY8j9bAHMu2SSfwpZJg==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.6.tgz", + "integrity": "sha512-twY9adK/vz72oWxCWxzXaxoDtF9TpfEEsxvbc1ibjF3gMD/RThSuSud/GKUTR3aJnfbivAbC/vLqhY+gdWCHfA==", "dev": true }, "node_modules/@types/json-schema": { @@ -3752,14 +3741,14 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz", - "integrity": "sha512-hEcSmG4XodSLiAp1uxv/OQSGsDY6QN3TcRU32gANp+19wGE1QQZLRS8/GV58VRUoXhnkuJ3ZxNQ3T6Z6zM59DA==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.17.0.tgz", + "integrity": "sha512-qVstvQilEd89HJk3qcbKt/zZrfBZ+9h2ynpAGlWjWiizA7m/MtLT9RoX6gjtpE500vfIg8jogAkDzdCxbsFASQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.23.0", - "@typescript-eslint/type-utils": "5.23.0", - "@typescript-eslint/utils": "5.23.0", + "@typescript-eslint/scope-manager": "5.17.0", + "@typescript-eslint/type-utils": "5.17.0", + "@typescript-eslint/utils": "5.17.0", "debug": "^4.3.2", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", @@ -3784,104 +3773,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz", - "integrity": "sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.23.0.tgz", - "integrity": "sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz", - "integrity": "sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.23.0.tgz", - "integrity": "sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.23.0", - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/typescript-estree": "5.23.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz", - "integrity": "sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "eslint-visitor-keys": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/experimental-utils": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.17.0.tgz", @@ -3902,14 +3793,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.22.0.tgz", - "integrity": "sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.17.0.tgz", + "integrity": "sha512-aRzW9Jg5Rlj2t2/crzhA2f23SIYFlF9mchGudyP0uiD6SenIxzKoLjwzHbafgHn39dNV/TV7xwQkLfFTZlJ4ig==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.22.0", - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/typescript-estree": "5.22.0", + "@typescript-eslint/scope-manager": "5.17.0", + "@typescript-eslint/types": "5.17.0", + "@typescript-eslint/typescript-estree": "5.17.0", "debug": "^4.3.2" }, "engines": { @@ -3928,80 +3819,6 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", - "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", - "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz", - "integrity": "sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", - "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.22.0", - "eslint-visitor-keys": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.17.0.tgz", @@ -4020,12 +3837,12 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.23.0.tgz", - "integrity": "sha512-iuI05JsJl/SUnOTXA9f4oI+/4qS/Zcgk+s2ir+lRmXI+80D8GaGwoUqs4p+X+4AxDolPpEpVUdlEH4ADxFy4gw==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.17.0.tgz", + "integrity": "sha512-3hU0RynUIlEuqMJA7dragb0/75gZmwNwFf/QJokWzPehTZousP/MNifVSgjxNcDCkM5HI2K22TjQWUmmHUINSg==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "5.23.0", + "@typescript-eslint/utils": "5.17.0", "debug": "^4.3.2", "tsutils": "^3.21.0" }, @@ -4045,104 +3862,6 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz", - "integrity": "sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.23.0.tgz", - "integrity": "sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz", - "integrity": "sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.23.0.tgz", - "integrity": "sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.23.0", - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/typescript-estree": "5.23.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz", - "integrity": "sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "eslint-visitor-keys": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/types": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.17.0.tgz", @@ -6879,12 +6598,12 @@ } }, "node_modules/eslint": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz", - "integrity": "sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", + "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.2.3", + "@eslint/eslintrc": "^1.2.2", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -6895,7 +6614,7 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", + "espree": "^9.3.1", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -6911,7 +6630,7 @@ "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", @@ -7138,18 +6857,6 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7175,13 +6882,13 @@ } }, "node_modules/espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", + "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", "dev": true, "dependencies": { - "acorn": "^8.7.1", - "acorn-jsx": "^5.3.2", + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -7315,9 +7022,9 @@ } }, "node_modules/express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz", + "integrity": "sha512-EJEXxiTQJS3lIPrU1AE2vRuT7X7E+0KBbpm5GSoK524yl0K8X+er8zS2P14E64eqsVNoWbMCT7MpmQ+ErAhgRg==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -8968,9 +8675,9 @@ } }, "node_modules/jasmine-core": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.1.1.tgz", - "integrity": "sha512-lmUfT5XcK9KKvt3lLYzn93hc4MGzlUBowExFVgzbSW0ZCrdeyS574dfsyfRhxbg81Wj4gk+RxUiTnj7KBfDA1g==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.0.1.tgz", + "integrity": "sha512-w+JDABxQCkxbGGxg+a2hUVZyqUS2JKngvIyLGu/xiw2ZwgsoSB0iiecLQsQORSeaKQ6iGrCyWG86RfNDuoA7Lg==", "dev": true }, "node_modules/jest-worker": { @@ -9165,13 +8872,13 @@ } }, "node_modules/karma-coverage": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.0.tgz", - "integrity": "sha512-gPVdoZBNDZ08UCzdMHHhEImKrw1+PAOQOIiffv1YsvxFhBjqvo/SVXNk4tqn1SYqX0BJZT6S/59zgxiBe+9OuA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.1.1.tgz", + "integrity": "sha512-oxeOSBVK/jdZsiX03LhHQkO4eISSQb5GbHi6Nsw3Mw7G4u6yUgacBAftnO7q+emPBLMsrNbz1pGIrj+Jb3z17A==", "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^4.0.3", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.1", "istanbul-reports": "^3.0.5", @@ -9181,19 +8888,43 @@ "node": ">=10.0.0" } }, - "node_modules/karma-jasmine": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.0.0.tgz", - "integrity": "sha512-dsFkCoTwyoNyQnMgegS72wIA/2xPDJG5yzTry0448U6lAY7P60Wgg4UuLlbdLv8YHbimgNpDXjjmfPdc99EDWQ==", + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "dependencies": { - "jasmine-core": "^4.1.0" + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" }, "engines": { - "node": ">=12" + "node": ">=8" + } + }, + "node_modules/karma-coverage/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/karma-jasmine": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.2.tgz", + "integrity": "sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g==", + "dev": true, + "dependencies": { + "jasmine-core": "^3.6.0" + }, + "engines": { + "node": ">= 10" }, "peerDependencies": { - "karma": "^6.0.0" + "karma": "*" } }, "node_modules/karma-jasmine-html-reporter": { @@ -9207,6 +8938,12 @@ "karma-jasmine": ">=1.1" } }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "3.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", + "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", + "dev": true + }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -12158,9 +11895,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.49.9", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz", - "integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.0.tgz", + "integrity": "sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -12171,7 +11908,7 @@ "sass": "sass.js" }, "engines": { - "node": ">=12.0.0" + "node": ">=8.9.0" } }, "node_modules/sass-loader": { @@ -13498,13 +13235,13 @@ } }, "node_modules/webpack": { - "version": "5.70.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", - "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", + "version": "5.67.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.67.0.tgz", + "integrity": "sha512-LjFbfMh89xBDpUMgA1W9Ur6Rn/gnr2Cq1jjHFPo4v6a79/ypznSYbAyPgGhwsxBtMIaEmDD1oJoA7BEYw/Fbrw==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", + "@types/eslint-scope": "^3.7.0", + "@types/estree": "^0.0.50", "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/wasm-edit": "1.11.1", "@webassemblyjs/wasm-parser": "1.11.1", @@ -13512,7 +13249,7 @@ "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.2", + "enhanced-resolve": "^5.8.3", "es-module-lexer": "^0.9.0", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -13985,22 +13722,22 @@ }, "dependencies": { "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-1.1.1.tgz", + "integrity": "sha512-YVAcA4DKLOj296CF5SrQ8cYiMRiUGc2sqFpLxsDGWE34suHqhGP/5yMsDHKsrh8hs8I5TiRVXNwKPWQpX3iGjw==", "dev": true, "requires": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/resolve-uri": "^3.0.3", + "sourcemap-codec": "1.4.8" } }, "@angular-devkit/architect": { - "version": "0.1303.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1303.5.tgz", - "integrity": "sha512-ZF5Vul8UqwDSwYPxJ4YvdG7lmciJZ1nncyt9Dbk0swxw4MGdy0ZIf+91o318qUn/5JrttQ7ZCYoCZJCjYOSBtw==", + "version": "0.1302.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1302.6.tgz", + "integrity": "sha512-NztzorUMfwJeRaT7SY00Y8WSqc2lQYuF11yNoyEm7Dae3V7VZ28rW2Z9RwibP27rYQL0RjSMaz2wKITHX2vOAw==", "dev": true, "requires": { - "@angular-devkit/core": "13.3.5", + "@angular-devkit/core": "13.2.6", "rxjs": "6.6.7" }, "dependencies": { @@ -14022,15 +13759,15 @@ } }, "@angular-devkit/build-angular": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-13.3.5.tgz", - "integrity": "sha512-6ZQ788U0vT7KqMZeOsNQxP01IhOpxlbKonxK2fZNju8e+Ha2K77yV9A9XMbmcUGWRRHCOFvUEaJhvxDFsunESg==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-13.2.6.tgz", + "integrity": "sha512-Y2ojy6xbZ0kwScppcutLHBP8eW0qNOjburTISSBU/L5l/9FOeZ1E7yAreKuVu/qibZiLbSJfAhk+SLwhRHFSSQ==", "dev": true, "requires": { - "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1303.5", - "@angular-devkit/build-webpack": "0.1303.5", - "@angular-devkit/core": "13.3.5", + "@ampproject/remapping": "1.1.1", + "@angular-devkit/architect": "0.1302.6", + "@angular-devkit/build-webpack": "0.1302.6", + "@angular-devkit/core": "13.2.6", "@babel/core": "7.16.12", "@babel/generator": "7.16.8", "@babel/helper-annotate-as-pure": "7.16.7", @@ -14041,7 +13778,7 @@ "@babel/runtime": "7.16.7", "@babel/template": "7.16.7", "@discoveryjs/json-ext": "0.5.6", - "@ngtools/webpack": "13.3.5", + "@ngtools/webpack": "13.2.6", "ansi-colors": "4.1.1", "babel-loader": "8.2.3", "babel-plugin-istanbul": "6.1.1", @@ -14064,7 +13801,7 @@ "license-webpack-plugin": "4.0.2", "loader-utils": "3.2.0", "mini-css-extract-plugin": "2.5.3", - "minimatch": "3.0.5", + "minimatch": "3.0.4", "open": "8.4.0", "ora": "5.4.1", "parse5-html-rewriting-stream": "6.0.1", @@ -14076,7 +13813,7 @@ "regenerator-runtime": "0.13.9", "resolve-url-loader": "5.0.0", "rxjs": "6.6.7", - "sass": "1.49.9", + "sass": "1.49.0", "sass-loader": "12.4.0", "semver": "7.3.5", "source-map-loader": "3.0.1", @@ -14087,22 +13824,13 @@ "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.3.1", - "webpack": "5.70.0", + "webpack": "5.67.0", "webpack-dev-middleware": "5.3.0", "webpack-dev-server": "4.7.3", "webpack-merge": "5.8.0", "webpack-subresource-integrity": "5.1.0" }, "dependencies": { - "minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -14129,12 +13857,12 @@ } }, "@angular-devkit/build-webpack": { - "version": "0.1303.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1303.5.tgz", - "integrity": "sha512-EI7scRGKPw9Rg4LypUSTf7JM3lE1imTVxY8mY6gqNkRWnvsb5+kptJQ+gK+VZSom/URcPFbN40lJYwgmZBNPeA==", + "version": "0.1302.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1302.6.tgz", + "integrity": "sha512-TYEh2n9tPe932rEIgdiSpojOqtDppW2jzb/empVqCkLF7WUZsXKvTanttZC34L6R2VD6SAGWhb6JDg75ghUVYA==", "dev": true, "requires": { - "@angular-devkit/architect": "0.1303.5", + "@angular-devkit/architect": "0.1302.6", "rxjs": "6.6.7" }, "dependencies": { @@ -14156,9 +13884,9 @@ } }, "@angular-devkit/core": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.3.5.tgz", - "integrity": "sha512-w7vzK4VoYP9rLgxJ2SwEfrkpKybdD+QgQZlsDBzT0C6Ebp7b4gkNcNVFo8EiZvfDl6Yplw2IAP7g7fs3STn0hQ==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.2.6.tgz", + "integrity": "sha512-8h2mWdBTN/dYwZuzKMg2IODlOWMdbJcpQG4XVrkk9ejCPP+3aX5Aa3glCe/voN6eBNiRfs8YDM0jxmpN2aWVtg==", "dev": true, "requires": { "ajv": "8.9.0", @@ -14187,12 +13915,12 @@ } }, "@angular-devkit/schematics": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.3.5.tgz", - "integrity": "sha512-0N/kL/Vfx0yVAEwa3HYxNx9wYb+G9r1JrLjJQQzDp+z9LtcojNf7j3oey6NXrDUs1WjVZOa/AIdRl3/DuaoG5w==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.2.6.tgz", + "integrity": "sha512-mPgSqdnZRuPSMeUA+T+mwVCrq2yhXpcYm1/Rjbhy09CyHs4wSrFv21WHCrE6shlvXpcmwr0n+I0DIeagAPmjUA==", "dev": true, "requires": { - "@angular-devkit/core": "13.3.5", + "@angular-devkit/core": "13.2.6", "jsonc-parser": "3.0.0", "magic-string": "0.25.7", "ora": "5.4.1", @@ -14295,24 +14023,24 @@ } }, "@angular/cdk": { - "version": "13.3.6", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.6.tgz", - "integrity": "sha512-sPwEGCHARxuUMzPLRiiN51GQP0P39ev+eL06NcyYXZ3AxyZIH5N/PWmGKf7EDOXIof4ata13jsIeW38mFe+wvg==", + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.5.tgz", + "integrity": "sha512-fA99fGgybup9ezyB/IzOa9Mk8g8LjejkqikLnC3mAeQ0lROOO7Vf9Rp1v7/ahe2lALTUbA1bzJeXzQYaffkIiA==", "requires": { "parse5": "^5.0.0", "tslib": "^2.3.0" } }, "@angular/cli": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.5.tgz", - "integrity": "sha512-FrPg86cfmm0arWZInt55muCTpcQSNlvoViVrIVkyqSN06GoyCAQ2zn6/OYJnx/XAg/XvXTbygL+58c0WXuOaiA==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.2.6.tgz", + "integrity": "sha512-xIjEaQI5sWemXXc7GXLm4u9UL5sjtrQL/y1PJvvk/Jsa8+kIT+MutOfZfC7zcdAh9fqHd8mokH3guFV8BJdFxA==", "dev": true, "requires": { - "@angular-devkit/architect": "0.1303.5", - "@angular-devkit/core": "13.3.5", - "@angular-devkit/schematics": "13.3.5", - "@schematics/angular": "13.3.5", + "@angular-devkit/architect": "0.1302.6", + "@angular-devkit/core": "13.2.6", + "@angular-devkit/schematics": "13.2.6", + "@schematics/angular": "13.2.6", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.1", "debug": "4.3.3", @@ -14364,6 +14092,16 @@ "yargs": "^17.2.1" }, "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@babel/core": { "version": "7.17.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.9.tgz", @@ -14440,9 +14178,9 @@ } }, "@angular/material": { - "version": "13.3.6", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-13.3.6.tgz", - "integrity": "sha512-V+3Fs9JK+7KlcdJG/Oa/IQuLHC8WYzuL8ffH1Q06ANSzGxSjsAHs4FZQpKUpVBoL3E+p9Yz+zkKe91k5L62VoQ==", + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-13.3.5.tgz", + "integrity": "sha512-4+FCb6Tbre5SwhZRKfnuh8K+/o+DuCGisJOuk7lxdFKDGDPjsPDWYVrBDal1N70mO09z/ApwNjpsIjuRv79wpg==", "requires": { "tslib": "^2.3.0" } @@ -15667,19 +15405,19 @@ "dev": true }, "@eslint/eslintrc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", - "integrity": "sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", + "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.2", + "espree": "^9.3.1", "globals": "^13.9.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" }, "dependencies": { @@ -15702,9 +15440,9 @@ "dev": true }, "globals": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.14.0.tgz", - "integrity": "sha512-ERO68sOYwm5UuLvSJTY7w7NP2c8S4UcXs3X1GBX8cwOr+ShOcDBbCY5mH4zxz0jsYCdJ8ve8Mv9n2YGJMB1aeg==", + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", + "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -15725,15 +15463,6 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -15823,9 +15552,9 @@ } }, "@ngtools/webpack": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.3.5.tgz", - "integrity": "sha512-OaMZR0rO0ljBHamLwzddfZX03ijtpheUpjH5dNzMNyNrrpKgS4/3jTQ1wvs2j3zzKfKjOS12WG0905QFJYWG6g==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.2.6.tgz", + "integrity": "sha512-N8SvRV91+/57TcAfbghc0k0tKCukw/7KqbDaLPAQTGFekJ4xMGT3elMzOyBXTH3Hvp5HL8/hiBt2tG04qiMf+w==", "dev": true, "requires": {} }, @@ -16266,13 +15995,13 @@ "peer": true }, "@schematics/angular": { - "version": "13.3.5", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.3.5.tgz", - "integrity": "sha512-1Ovx0cq72ZaNCyTyRD8ebIwUzpqhEH9ypWF05bfBLq3J0LlZgewIMhPJSxKmwRC3NQB5DZIYEvD0uhzBIuHCCA==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.2.6.tgz", + "integrity": "sha512-8NzHMX9+FSgaB0lJYxlTJv9OcBuolwZJqo9M/yX3RPSqSHghA33jWwgVbV551hBJOpbVEePerG1DQkIC99DXKA==", "dev": true, "requires": { - "@angular-devkit/core": "13.3.5", - "@angular-devkit/schematics": "13.3.5", + "@angular-devkit/core": "13.2.6", + "@angular-devkit/schematics": "13.2.6", "jsonc-parser": "3.0.0" } }, @@ -16520,9 +16249,9 @@ } }, "@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", + "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", "dev": true }, "@types/express": { @@ -16549,18 +16278,18 @@ } }, "@types/http-proxy": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "version": "1.17.8", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz", + "integrity": "sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==", "dev": true, "requires": { "@types/node": "*" } }, "@types/jasmine": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", - "integrity": "sha512-Opp1LvvEuZdk8fSSvchK2mZwhVrsNT0JgJE9Di6MjnaIpmEXM8TLCPPrVtNTYh8+5MPdY8j9bAHMu2SSfwpZJg==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.6.tgz", + "integrity": "sha512-twY9adK/vz72oWxCWxzXaxoDtF9TpfEEsxvbc1ibjF3gMD/RThSuSud/GKUTR3aJnfbivAbC/vLqhY+gdWCHfA==", "dev": true }, "@types/json-schema": { @@ -16649,77 +16378,20 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz", - "integrity": "sha512-hEcSmG4XodSLiAp1uxv/OQSGsDY6QN3TcRU32gANp+19wGE1QQZLRS8/GV58VRUoXhnkuJ3ZxNQ3T6Z6zM59DA==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.17.0.tgz", + "integrity": "sha512-qVstvQilEd89HJk3qcbKt/zZrfBZ+9h2ynpAGlWjWiizA7m/MtLT9RoX6gjtpE500vfIg8jogAkDzdCxbsFASQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.23.0", - "@typescript-eslint/type-utils": "5.23.0", - "@typescript-eslint/utils": "5.23.0", + "@typescript-eslint/scope-manager": "5.17.0", + "@typescript-eslint/type-utils": "5.17.0", + "@typescript-eslint/utils": "5.17.0", "debug": "^4.3.2", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", "regexpp": "^3.2.0", "semver": "^7.3.5", "tsutils": "^3.21.0" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz", - "integrity": "sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0" - } - }, - "@typescript-eslint/types": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.23.0.tgz", - "integrity": "sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz", - "integrity": "sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.23.0.tgz", - "integrity": "sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.23.0", - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/typescript-estree": "5.23.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz", - "integrity": "sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "eslint-visitor-keys": "^3.0.0" - } - } } }, "@typescript-eslint/experimental-utils": { @@ -16732,58 +16404,15 @@ } }, "@typescript-eslint/parser": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.22.0.tgz", - "integrity": "sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.17.0.tgz", + "integrity": "sha512-aRzW9Jg5Rlj2t2/crzhA2f23SIYFlF9mchGudyP0uiD6SenIxzKoLjwzHbafgHn39dNV/TV7xwQkLfFTZlJ4ig==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.22.0", - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/typescript-estree": "5.22.0", + "@typescript-eslint/scope-manager": "5.17.0", + "@typescript-eslint/types": "5.17.0", + "@typescript-eslint/typescript-estree": "5.17.0", "debug": "^4.3.2" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", - "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0" - } - }, - "@typescript-eslint/types": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", - "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz", - "integrity": "sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", - "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.22.0", - "eslint-visitor-keys": "^3.0.0" - } - } } }, "@typescript-eslint/scope-manager": { @@ -16797,71 +16426,14 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.23.0.tgz", - "integrity": "sha512-iuI05JsJl/SUnOTXA9f4oI+/4qS/Zcgk+s2ir+lRmXI+80D8GaGwoUqs4p+X+4AxDolPpEpVUdlEH4ADxFy4gw==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.17.0.tgz", + "integrity": "sha512-3hU0RynUIlEuqMJA7dragb0/75gZmwNwFf/QJokWzPehTZousP/MNifVSgjxNcDCkM5HI2K22TjQWUmmHUINSg==", "dev": true, "requires": { - "@typescript-eslint/utils": "5.23.0", + "@typescript-eslint/utils": "5.17.0", "debug": "^4.3.2", "tsutils": "^3.21.0" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz", - "integrity": "sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0" - } - }, - "@typescript-eslint/types": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.23.0.tgz", - "integrity": "sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz", - "integrity": "sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.23.0.tgz", - "integrity": "sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.23.0", - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/typescript-estree": "5.23.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz", - "integrity": "sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "eslint-visitor-keys": "^3.0.0" - } - } } }, "@typescript-eslint/types": { @@ -18837,12 +18409,12 @@ "dev": true }, "eslint": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz", - "integrity": "sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", + "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.3", + "@eslint/eslintrc": "^1.2.2", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -18853,7 +18425,7 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", + "espree": "^9.3.1", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -18869,7 +18441,7 @@ "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", @@ -18992,15 +18564,6 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -19052,13 +18615,13 @@ "dev": true }, "espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", + "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", "dev": true, "requires": { - "acorn": "^8.7.1", - "acorn-jsx": "^5.3.2", + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", "eslint-visitor-keys": "^3.3.0" } }, @@ -19156,9 +18719,9 @@ } }, "express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz", + "integrity": "sha512-EJEXxiTQJS3lIPrU1AE2vRuT7X7E+0KBbpm5GSoK524yl0K8X+er8zS2P14E64eqsVNoWbMCT7MpmQ+ErAhgRg==", "dev": true, "requires": { "accepts": "~1.3.8", @@ -20404,9 +19967,9 @@ } }, "jasmine-core": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.1.1.tgz", - "integrity": "sha512-lmUfT5XcK9KKvt3lLYzn93hc4MGzlUBowExFVgzbSW0ZCrdeyS574dfsyfRhxbg81Wj4gk+RxUiTnj7KBfDA1g==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.0.1.tgz", + "integrity": "sha512-w+JDABxQCkxbGGxg+a2hUVZyqUS2JKngvIyLGu/xiw2ZwgsoSB0iiecLQsQORSeaKQ6iGrCyWG86RfNDuoA7Lg==", "dev": true }, "jest-worker": { @@ -20602,26 +20165,54 @@ } }, "karma-coverage": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.0.tgz", - "integrity": "sha512-gPVdoZBNDZ08UCzdMHHhEImKrw1+PAOQOIiffv1YsvxFhBjqvo/SVXNk4tqn1SYqX0BJZT6S/59zgxiBe+9OuA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.1.1.tgz", + "integrity": "sha512-oxeOSBVK/jdZsiX03LhHQkO4eISSQb5GbHi6Nsw3Mw7G4u6yUgacBAftnO7q+emPBLMsrNbz1pGIrj+Jb3z17A==", "dev": true, "requires": { "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^4.0.3", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.1", "istanbul-reports": "^3.0.5", "minimatch": "^3.0.4" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "karma-jasmine": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.0.0.tgz", - "integrity": "sha512-dsFkCoTwyoNyQnMgegS72wIA/2xPDJG5yzTry0448U6lAY7P60Wgg4UuLlbdLv8YHbimgNpDXjjmfPdc99EDWQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.2.tgz", + "integrity": "sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g==", "dev": true, "requires": { - "jasmine-core": "^4.1.0" + "jasmine-core": "^3.6.0" + }, + "dependencies": { + "jasmine-core": { + "version": "3.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", + "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", + "dev": true + } } }, "karma-jasmine-html-reporter": { @@ -22757,9 +22348,9 @@ "dev": true }, "sass": { - "version": "1.49.9", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz", - "integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.0.tgz", + "integrity": "sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -23760,13 +23351,13 @@ } }, "webpack": { - "version": "5.70.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", - "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", + "version": "5.67.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.67.0.tgz", + "integrity": "sha512-LjFbfMh89xBDpUMgA1W9Ur6Rn/gnr2Cq1jjHFPo4v6a79/ypznSYbAyPgGhwsxBtMIaEmDD1oJoA7BEYw/Fbrw==", "dev": true, "requires": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", + "@types/eslint-scope": "^3.7.0", + "@types/estree": "^0.0.50", "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/wasm-edit": "1.11.1", "@webassemblyjs/wasm-parser": "1.11.1", @@ -23774,7 +23365,7 @@ "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.2", + "enhanced-resolve": "^5.8.3", "es-module-lexer": "^0.9.0", "eslint-scope": "5.1.1", "events": "^3.2.0", diff --git a/frontend/package.json b/frontend/package.json index bd127e5..1a2e83b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,9 +39,9 @@ "@angular/compiler-cli": "~13.2.0", "@types/jasmine": "~4.0.3", "@types/node": "^12.11.1", - "@typescript-eslint/eslint-plugin": "5.23.0", + "@typescript-eslint/eslint-plugin": "5.22.0", "@typescript-eslint/parser": "5.22.0", - "eslint": "^8.15.0", + "eslint": "^8.12.0", "jasmine-core": "~4.1.0", "karma": "~6.3.19", "karma-chrome-launcher": "~3.1.0", diff --git a/frontend/src/app/Views/dashboard/dashboard.component.html b/frontend/src/app/Views/dashboard/dashboard.component.html index 20376c1..ce7401d 100644 --- a/frontend/src/app/Views/dashboard/dashboard.component.html +++ b/frontend/src/app/Views/dashboard/dashboard.component.html @@ -25,7 +25,7 @@ - ISIN + ISIN/Symbol {{ element.isin }} diff --git a/frontend/src/app/Views/dashboard/dashboard.component.ts b/frontend/src/app/Views/dashboard/dashboard.component.ts index 2cdaaeb..e25b8a6 100644 --- a/frontend/src/app/Views/dashboard/dashboard.component.ts +++ b/frontend/src/app/Views/dashboard/dashboard.component.ts @@ -53,7 +53,7 @@ export class DashboardComponent implements OnInit { var data = JSON.parse(response); this.depotCost = 0; for (let i = 0; i < data.data.length; i++) { - this.depotCost += data.data[i].price; + this.depotCost += data.data[i].price * data.data[i].count; TRANSACTION_DATA.push({ comment: data.data[i].comment, isin: data.data[i].isin, @@ -98,7 +98,7 @@ export class DashboardComponent implements OnInit { var data = JSON.parse(response); this.depotCost = 0; for (let i = 0; i < data.data.length; i++) { - this.depotCost += data.data[i].price; + this.depotCost += data.data[i].price * data.data[i].count; TRANSACTION_DATA.push({ comment: data.data[i].comment, isin: data.data[i].isin, diff --git a/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.html b/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.html index c3e31ee..bb67392 100644 --- a/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.html +++ b/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.html @@ -45,7 +45,7 @@ />
- +
- + ` diff --git a/telegram_bot/api_handling/api_handler.py b/telegram_bot/api_handling/api_handler.py index 9975f6d..35aef19 100644 --- a/telegram_bot/api_handling/api_handler.py +++ b/telegram_bot/api_handling/api_handler.py @@ -2,22 +2,20 @@ script for communicating with webservice to get data from database """ __author__ = "Florian Kellermann, Linus Eickhoff" -__date__ = "10.05.2022" -__version__ = "1.0.2" +__date__ = "26.04.2022" +__version__ = "1.0.1" __license__ = "None" -# side-dependencies: none -# Work in Progress +#side-dependencies: none +#Work in Progress -import os import sys - +import os import requests as r -from croniter import croniter # used for checking cron formatting +from croniter import croniter # used for checking cron formatting from dotenv import load_dotenv -load_dotenv() # loads environment vars - +load_dotenv() # loads environment vars # note: for more information about the api visit swagger documentation on https://gruppe1.testsites.info/api/docs#/ @@ -44,7 +42,8 @@ class API_Handler: set_cron_interval(user_id, interval): sets the cron interval of the user set_admin(email, is_admin): sets the admin status of the user with the given email """ - + + def __init__(self, db_adress, email, password): """initializes the API_Handler class @@ -55,27 +54,22 @@ class API_Handler: """ self.db_adress = db_adress - payload = {'email': email, 'password': password} # credentials for admin account that has all permissions to get and set data (in this case bot account) - with r.Session() as s: # open session - p = s.post(self.db_adress + "/user/login", json=payload) # login to webservice + payload = {'email': email, 'password': password} # credentials for admin account that has all permissions to get and set data (in this case bot account) + with r.Session() as s: # open session + p = s.post(self.db_adress + "/user/login", json=payload) # login to webservice if p.status_code == 200: - self.token = p.json()["data"]['token'] # store token for further authentication of requests + self.token = p.json()["data"]['token'] # store token for further authentication of requests else: print("Error: " + str(p.status_code) + " invalid credentials") self.token = None - def reauthorize(self, email, password): # can be used if token expired + + def reauthorize(self, email, password): # can be used if token expired """set new credentials Args: email (string): email of the user password (string): password of the user - - Returns: - token (string): new token or None if not 200 - - Raises: - None """ payload = {'email': email, 'password': password} with r.Session() as s: @@ -87,7 +81,8 @@ class API_Handler: self.token = None return None - def get_user(self, user_id, max_retries=10): # max retries are used recursively if the request fails + + def get_user(self, user_id, max_retries=10): # max retries are used recursively if the request fails """gets the shares of the user Args: @@ -96,22 +91,20 @@ class API_Handler: Returns: json: json of user infos - - Raises: - None """ if max_retries <= 0: return None with r.Session() as s: - headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} # authorization is bot_token:user_id (user_id is the id of the user you want to get data from) + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} # authorization is bot_token:user_id (user_id is the id of the user you want to get data from) req = s.get(self.db_adress + "/user", headers=headers) - if (req.status_code == 200): + if(req.status_code == 200): return req.json()["data"] - + else: - return self.get_user(user_id, max_retries - 1) # if request fails try again recursively + return self.get_user(user_id, max_retries-1) # if request fails try again recursively + def get_all_users(self, max_retries=10): """gets all users @@ -120,9 +113,6 @@ class API_Handler: Returns: list: list of users - - Raises: - None """ if max_retries <= 0: return None @@ -130,11 +120,12 @@ class API_Handler: with r.Session() as s: headers = {'Authorization': 'Bearer ' + self.token} req = s.get(self.db_adress + "/users", headers=headers) - if (req.status_code == 200): + if(req.status_code == 200): return req.json()["data"] - + else: - return self.get_all_users(max_retries - 1) + return self.get_all_users(max_retries-1) + def get_user_keywords(self, user_id, max_retries=10): """gets the keywords of the user @@ -145,26 +136,25 @@ class API_Handler: Returns: list: list of keywords - - Raises: - None """ if max_retries <= 0: return None - + keywords = [] with r.Session() as s: headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} req = s.get(self.db_adress + "/keywords", headers=headers) - if (req.status_code == 200): + if(req.status_code == 200): keywords_json = req.json()["data"] - for keyword in keywords_json: # keywords_json is a list of dictionaries + for keyword in keywords_json: # keywords_json is a list of dictionaries keywords.append(keyword["keyword"]) - return keywords # will be empty if no keywords are set + return keywords # will be empty if no keywords are set else: - return self.get_user_keywords(user_id, max_retries - 1) + return self.get_user_keywords(user_id, max_retries-1) + + def set_keyword(self, user_id, keyword): """sets the keyword of the user @@ -175,16 +165,14 @@ class API_Handler: Returns: int: status code - - Raises: - None """ with r.Session() as s: headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} req = s.post(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers) - + return req.status_code + def delete_keyword(self, user_id, keyword): """deletes the keyword of the user @@ -194,16 +182,14 @@ class API_Handler: Returns: int: status code - - Raises: - None """ with r.Session() as s: headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} req = s.delete(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers) - + return req.status_code + def get_user_shares(self, user_id, max_retries=10): """gets the shares of the user @@ -213,9 +199,6 @@ class API_Handler: Returns: list: list of shares - - Raises: - None """ if max_retries <= 0: return None @@ -223,55 +206,51 @@ class API_Handler: with r.Session() as s: headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} req = s.get(self.db_adress + "/shares", headers=headers) - if (req.status_code == 200): + if(req.status_code == 200): shares_json = req.json()["data"] shares = [] for share in shares_json: - shares.append(share["isin"]) # we only want the isin of the shares + shares.append(share["isin"]) # we only want the isin of the shares return shares - + else: - return self.get_user_shares(user_id, max_retries - 1) + return self.get_user_shares(user_id, max_retries-1) + def set_share(self, user_id, isin, comment): """sets the share of the user Args: user_id (int): id of the user - isin (string): identifier of the share (standard is isin) + isin (string): isin of the share comment (string): comment of the share Returns: int: status code - - Raises: - None """ with r.Session() as s: headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} - req = s.post(self.db_adress + "/share", json={"comment": comment, "isin": isin}, - headers=headers) # set share by setting comment and isin, comment can be the real name of the share e.g. "Apple Inc." + req = s.post(self.db_adress + "/share", json={"comment": comment, "isin": isin}, headers=headers) # set share by setting comment and isin, comment can be the real name of the share e.g. "Apple Inc." return req.status_code + def delete_share(self, user_id, isin): """deletes the share of the user Args: user_id (int): id of the user - isin (string): identifier of the share (standard is isin) + symbol (string): symbol of the share Returns: int: status code - - Raises: - None """ with r.Session() as s: headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} - req = s.delete(self.db_adress + "/share", json={"isin": str(isin)}, headers=headers) # to delete a share only the isin is needed because it is unique, shares are not transactions! + req = s.delete(self.db_adress + "/share", json={"isin": isin}, headers=headers) # to delete a share only the isin is needed because it is unique, shares are not transactions! return req.status_code + def get_user_transactions(self, user_id, max_retries=10): """gets the transactions of the user @@ -281,9 +260,6 @@ class API_Handler: Returns: dict: dictionary of transactions - - Raises: - None """ if max_retries <= 0: return None @@ -296,7 +272,8 @@ class API_Handler: transactions_dict = req.json()["data"] return transactions_dict else: - return self.get_user_transactions(user_id, max_retries - 1) + return self.get_user_transactions(user_id, max_retries-1) + def set_transaction(self, user_id, comment, isin, count, price, time): """sets the transaction of the user @@ -311,18 +288,15 @@ class API_Handler: Returns: int: status code - - Raises: - None """ with r.Session() as s: - time = time[:-3] + "Z" # remove last character and add Z to make it a valid date for db + time = time[:-3] + "Z" # remove last character and add Z to make it a valid date for db headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} - transaction = {"comment": str(comment), "count": float(count), "isin": str(isin), "price": float(price), - "time": str(time)} # set transaction as JSON with all the attributes needed according to Swagger docs + transaction = {"comment": str(comment), "count": float(count), "isin": str(isin), "price": float(price), "time": str(time)} # set transaction as JSON with all the attributes needed according to Swagger docs req = s.post(self.db_adress + "/transaction", json=transaction, headers=headers) return req.status_code + def get_user_portfolio(self, user_id, max_retries=10): """gets the portfolio of the user @@ -332,22 +306,19 @@ class API_Handler: Returns: dict: dictionary of portfolio - - Raises: - None """ if max_retries <= 0: return None with r.Session() as s: headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} - req = s.get(self.db_adress + "/portfolio", headers=headers) # get portfolio as JSON + req = s.get(self.db_adress + "/portfolio", headers=headers) # get portfolio as JSON if req.status_code == 200: - portfolio_dict = req.json()["data"] # get the data of the JSON + portfolio_dict = req.json()["data"] # get the data of the JSON return portfolio_dict else: - return self.get_user_portfolio(user_id, max_retries - 1) - + return self.get_user_portfolio(user_id, max_retries-1) + def set_cron_interval(self, user_id, cron_interval): """sets the cron interval of the user @@ -357,19 +328,17 @@ class API_Handler: Returns: int: status code - - Raises: - None """ - if not croniter.is_valid(cron_interval): # check if cron_interval is in valid format + if not croniter.is_valid(cron_interval): # check if cron_interval is in valid format print("Error: Invalid cron format") - return -1 # return error code -1 if invalid cron format + return -1 # return error code -1 if invalid cron format with r.Session() as s: headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} - req = s.put(self.db_adress + "/user/setCron", json={"cron": str(cron_interval)}, headers=headers) # put not post (see swagger docs) + req = s.put(self.db_adress + "/user/setCron", json={"cron": str(cron_interval)}, headers=headers) # put not post (see swagger docs) return req.status_code + def set_admin(self, email, is_admin): """sets the admin of the user @@ -379,30 +348,27 @@ class API_Handler: Returns: int: status code - - Raises: - None """ with r.Session() as s: - headers = {'Authorization': 'Bearer ' + self.token} # only bot token is needed, user is chosen by email - req = s.put(self.db_adress + "/user/setAdmin", json={"admin": is_admin, "email": str(email)}, headers=headers) + headers = {'Authorization': 'Bearer ' + self.token} # only bot token is needed, user is chosen by email + req = s.put(self.db_adress + "/user/setAdmin", json={"admin": is_admin,"email": str(email)}, headers=headers) return req.status_code -if __name__ == "__main__": # editable, just for basic on the go testing of new functions +if __name__ == "__main__": # editable, just for basic on the go testing of new functions print("This is a module for the telegram bot. It is not intended to be run directly.") - handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) # get creds from env + handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) # get creds from env print(handler.token) - keywords = handler.get_user_keywords(user_id=1709356058) # user_id here is currently mine (Linus) + keywords = handler.get_user_keywords(user_id = 1709356058) #user_id here is currently mine (Linus) print(keywords) - shares = handler.get_user_portfolio(user_id=1709356058) - print("set cron with status: " + str(handler.set_cron_interval(user_id=1709356058, cron_interval="0 0 * * *"))) - user = handler.get_user(user_id=1709356058) + shares = handler.get_user_portfolio(user_id = 1709356058) + print("set cron with status: "+ str(handler.set_cron_interval(user_id = 1709356058, cron_interval = "0 0 * * *"))) + user = handler.get_user(user_id = 1709356058) print(user) all_users = handler.get_all_users() admin_status = handler.set_admin("test@test.com", "true") print(admin_status) print(all_users) print(shares) - sys.exit(1) + sys.exit(1) \ No newline at end of file diff --git a/telegram_bot/bot.py b/telegram_bot/bot.py index 65a164d..a870d75 100644 --- a/telegram_bot/bot.py +++ b/telegram_bot/bot.py @@ -1,45 +1,49 @@ + """ script for telegram bot and its functions """ __author__ = "Florian Kellermann, Linus Eickhoff" -__date__ = "10.05.2022" -__version__ = "1.2.3" +__date__ = "26.04.2022" +__version__ = "1.2.2" __license__ = "None" # side-dependencies: none # Work in Progress +# Api-Key: 5228016873:AAGFrh0P6brag7oD3gxXjCh5gnLLE8JMvMs /debugAPI Key: 5108535940:AAF5FpPHNV96WxGCDt8aMrGGKke1VILYib4 (https://t.me/mynewdebugbot) # text bot at t.me/projektaktienbot # API Documentation https://core.telegram.org/bots/api # Code examples https://github.com/eternnoir/pyTelegramBotAPI#getting-started -import datetime as dt -import logging import os -import re -import sys import telebot -from dotenv import load_dotenv -from telebot import types +import sys +import logging +import re -import helper_functions as hf import news.news_fetcher as news import shares.share_fetcher as share_fetcher +import datetime as dt + +from telebot import types +from dotenv import load_dotenv + from api_handling.api_handler import API_Handler -load_dotenv(dotenv_path='.env') # load environment variables -bot_version = "3.0.1" # version of bot +load_dotenv(dotenv_path='.env') # load environment variables -# create api handler -api_handler = API_Handler(str(os.getenv("API_URL")), str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) # get creds from env vars. +bot_version = "1.0.1" # version of bot + +#create api handler +api_handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) # get creds from env vars. print("Webserver Token: " + str(api_handler.token)) bot = telebot.TeleBot(os.getenv('BOT_API_KEY')) - -@bot.message_handler(commands=['start', 'Start']) +@bot.message_handler(commands=['start', 'Start']) def send_start(message): + """ Sending welcome message to new user :type message: message object bot :param message: message that was reacted to, in this case always containing '/start' @@ -50,12 +54,13 @@ def send_start(message): """ bot.reply_to(message, "Welcome to this share bot project. \ \nType /help to get information on what this bot can do. \ - \nAlso see " + os.getenv("WEBSITE_URL") + " \ + \nAlso see https://gruppe1.testsites.info \ to start configuring your bot") @bot.message_handler(commands=['version', 'Version']) def send_version(message): + """ Sending programm version :type message: message object bot :param message: message that was reacted to, in this case always containing '/version' @@ -67,8 +72,9 @@ def send_version(message): bot.reply_to(message, "the current bot version is " + bot_version) -@bot.message_handler(commands=['help', 'Help']) # /help -> sending all functions +@bot.message_handler(commands=['help', 'Help']) # /help -> sending all functions def send_help(message): + """ Send all functions :type message: message object bot :param message: message that was reacted to, in this case always containing '/help' @@ -77,12 +83,12 @@ def send_help(message): :rtype: none """ - bot.reply_to(message, - "/id or /auth get your user id\n/shares get update on interesting shares\n/setAdmin set admin rights of user (ADMIN)\n/users see all users. (ADMIN)\n/me get my user info\n/news get top article for each keyword.\n/allnews get all news (last 7 days)\n/keywords get all your keywords\n/addkeyword add a keyword\n/removekeyword remove a keyword\n/transactions get all transactions\n/newtransaction create new transaction\n/share get price of specific share\n/portfolio see own stock portfolio\n/removeshare removes share from portfolio\n/interval get update interval\n/setinterval set update interval\n For further details see " + os.getenv("WEBSITE_URL")) + bot.reply_to(message, "/id or /auth get your user id\n/update get updates on your shares.\n/setAdmin set admin rights of user (ADMIN)\n/users see all users. (ADMIN)\n/me get my user info\n/news get top article for each keyword.\n/allnews get all news (last 7 days)\n/keywords get all your keywords\n/addkeyword add a keyword\n/removekeyword remove a keyword\n/share get price of specific share\n/portfolio see own portfolio\n/newtransaction add new transaction\n/interval get update interval\n/setinterval set update interval\n_For further details see https://gruppe1.testsites.info _", parse_mode='MARKDOWN') -@bot.message_handler(commands=['users', 'Users']) # /users -> sending all users +@bot.message_handler(commands=['users', 'Users']) # /users -> sending all users def send_all_users(message): + """ Send all users, only possible for admins :type message: message object bot :param message: message that was reacted to, in this case always containing '/users' @@ -93,27 +99,29 @@ def send_all_users(message): """ user_id = int(message.from_user.id) - user_data = api_handler.get_user(user_id) - if (user_data["admin"] == False): # check if user has admin rights + user_data = api_handler.get_user(user_id) + if(user_data["admin"] == False): # check if user has admin rights bot.reply_to(message, "You have to be an admin to use this command") return - + user_list = api_handler.get_all_users() user_count = len(user_list) bot.send_message(chat_id=user_id, text="There are " + str(user_count) + " users in the database:") - + for user in user_list: + username = user['username'] email = user['email'] id = user['telegram_user_id'] cron = user['cron'] admin = user['admin'] - bot.send_message(chat_id=user_id, text=f'Username: {username}\nEmail: {email}\nID: {id}\nCron: {cron}\nAdmin: {admin}') # format user data into readable message text + bot.send_message(chat_id=user_id, text=f'Username: {username}\nEmail: {email}\nID: {id}\nCron: {cron}\nAdmin: {admin}') # format user data into readable message text + - -@bot.message_handler(commands=['setAdmin', 'SetAdmin', 'setadmin', 'Setadmin']) # set admin rights to user TBD: not working!! +@bot.message_handler(commands=['setAdmin', 'SetAdmin', 'setadmin', 'Setadmin']) # set admin rights to user TBD: not working!! def set_admin(message): + """ Set admin rights to user :type message: message object bot :param message: message that was reacted to, in this case always containing '/setAdmin' @@ -123,65 +131,65 @@ def set_admin(message): :rtype: none """ user_id = int(message.from_user.id) - user_data = api_handler.get_user(user_id) + user_data = api_handler.get_user(user_id) - if (user_data["admin"] == False): # check if user has admin rights + if(user_data["admin"] == False): # check if user has admin rights bot.reply_to(message, "You have to be an admin to use this command") return - bot.send_message(chat_id=user_id, text='send email and true if this account should have admin rights, else false\n in format: ,') # request email and admin rights to change to + bot.send_message(chat_id=user_id, text='send email and true if this account should have admin rights, else false\n in format: ,') # request email and admin rights to change to bot.register_next_step_handler(message, set_admin_step) - def set_admin_step(message): str_message = str(message.text) - args_message = str_message.split(',') # split message into email and admin rights + args_message = str_message.split(',') # split message into email and admin rights - if len(args_message) != 2: # make sure 2 args (email,is_admin) are given + if len(args_message) != 2: # make sure 2 args (email,is_admin) are given bot.reply_to(message, "exactly 2 arguments (,) required, try again") return email = args_message[0] - is_admin = False # default: False + is_admin = False # default: False - if args_message[1].lower() == "true": # if user types true, set is_admin to true + if args_message[1].lower() == "true": # if user types true, set is_admin to true is_admin = True - status = api_handler.set_admin(email, is_admin) # set admin in db - - if (status == 200): + status = api_handler.set_admin(email, is_admin) # set admin in db + + if(status == 200): bot.reply_to(message, "Admin rights set") - + else: bot.reply_to(message, f"Admin rights could not be set ({status})") -@bot.message_handler(commands=['me', 'Me']) # /me -> sending user info -def send_user(message): - """ Send user data - :type message: message object bot - :param message: message that was reacted to, in this case always containing '/me' - - :raises: none - - :rtype: none - """ - user_id = int(message.from_user.id) - user_data = api_handler.get_user(user_id) - if not user_data or user_data == None: # true if user is not registered - bot.reply_to(message, "This didn\'t work. Make sure to connect your telegram id (/id) on " + os.getenv("WEBSITE_URL")) - return - username = user_data['username'] - email = user_data['email'] - user_id = user_data['telegram_user_id'] - cron = user_data['cron'] - admin = user_data['admin'] - bot.reply_to(message, f'Username: {username}\nEmail: {email}\nID: {user_id}\nCron: {cron}\nAdmin: {admin}') # format user data into readable message text +@bot.message_handler(commands=['me', 'Me']) # /me -> sending user info +def send_user(message): + """ Send user data + :type message: message object bot + :param message: message that was reacted to, in this case always containing '/me' + + :raises: none + + :rtype: none + """ + user_id = int(message.from_user.id) + user_data = api_handler.get_user(user_id) + if not user_data or user_data == None: # true if user is not registered + bot.reply_to(message, "This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info") + return + username = user_data['username'] + email = user_data['email'] + user_id = user_data['telegram_user_id'] + cron = user_data['cron'] + admin = user_data['admin'] + bot.reply_to(message, f'Username: {username}\nEmail: {email}\nID: {user_id}\nCron: {cron}\nAdmin: {admin}') # format user data into readable message text -@bot.message_handler(commands=['id', 'auth', 'Id', 'Auth']) # /auth or /id -> Authentication with user_id over web tool +@bot.message_handler(commands=['id', 'auth', 'Id', 'Auth']) # /auth or /id -> Authentication with user_id over web tool def send_id(message): + """ Send user id for authentication with browser :type message: message object bot :param message: message that was reacted to, in this case always containing '/id' or '/auth' @@ -190,13 +198,14 @@ def send_id(message): :rtype: none """ - answer = 'Your ID/Authentication Code is: [' + str(message.from_user.id) + ']. Enter this code in the settings on ' + os.getenv("WEBSITE_URL") + ' to get updates on your shares.' + answer = 'Your ID/Authentication Code is: [' + str(message.from_user.id) + ']. Enter this code in the settings on https://gruppe1.testsites.info to get updates on your shares.' bot.reply_to(message, answer) -# function that can be used to ensure that the bot is online and running +#function that can be used to ensure that the bot is online and running @bot.message_handler(commands=['status', 'Status']) def send_status(message): + """ Sends status to user :type message: message object bot :param message: message that was reacted to, if no other command handler gets called @@ -208,42 +217,38 @@ def send_status(message): bot.reply_to(message, "bot is running") -@bot.message_handler(commands=['portfolio', 'Portfolio']) # /update -> update shares +@bot.message_handler(commands=['update', 'Update']) # /update -> update shares def update_for_user(message): + p_user_id = int(message.from_user.id) p_my_handler = api_handler - + share_symbols = [] share_amounts = [] - - my_portfolio = p_my_handler.get_user_portfolio(p_user_id) - - if my_portfolio == None: # true if user is not registered - bot.send_message(chat_id=p_user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on ' + os.getenv("WEBSITE_URL")) - return + share_courses = [] - my_user = p_my_handler.get_user(p_user_id) - send_to_user("Hello %s this is your share update:" % str(my_user["username"]), pUser_id=p_user_id) + my_portfolio = p_my_handler.get_user_portfolio(p_user_id) for element in my_portfolio: - if element["count"] != '' and element["isin"] != '': + if element["count"] != '' and element["isin"]!= '': print(element["count"], element["isin"]) share_symbols.append(element["isin"]) share_amounts.append(element["count"]) + share_courses.append(element["current_price"]) + my_user = p_my_handler.get_user(p_user_id) + send_to_user("Hello %s this is your share update:"%str(my_user["username"]), pUser_id=p_user_id) - if len(share_symbols) != 0: for i in range(len(share_symbols)): - my_price = share_fetcher.get_share_price_no_currency(share_symbols[i]) - amounts = hf.make_markdown_proof(share_amounts[i]) - my_update_message = f'{share_fetcher.get_share_information_markdown(share_symbols[i])}\ncount: {amounts}\nTotal: {hf.make_markdown_proof(round(float(my_price) * float(share_amounts[i]), 2))} EUR' - bot.send_message(chat_id=p_user_id, text=my_update_message, parse_mode="MARKDOWNV2") + my_update_message = f'Symbol: {share_symbols[i]}\nCurrent Price per Share: {share_courses[i]}\nAmount owned: {share_amounts[i]}\nTotal Investment: {float(share_courses[i]) * float(share_amounts[i])}' + send_to_user(my_update_message, pUser_id=p_user_id) else: - send_to_user("No shares found for your account. Check " + os.getenv("WEBSITE_URL") + " to change your settings and add shares.", pUser_id=p_user_id) - - + send_to_user("No shares found for your account. Check https://gruppe1.testsites.info to change your settings and add shares.", pUser_id=p_user_id) + + def send_to_user(pText, pUser_id): + """ Send message to user :type pText: string :param pText: Text to send to user @@ -256,10 +261,11 @@ def send_to_user(pText, pUser_id): :rtype: none """ bot.send_message(chat_id=pUser_id, text=pText) - - -@bot.message_handler(commands=['share', 'Share']) # /share -> get share price + + +@bot.message_handler(commands=['share', 'Share']) # /share -> get share price def send_share_update(message): + """ Send price of a specific share :type message: message object bot :param message: message that was reacted to, in this case always containing '/share' @@ -269,18 +275,18 @@ def send_share_update(message): :rtype: none """ user_id = int(message.from_user.id) - + bot.send_message(chat_id=user_id, text='Send Symbol/ISIN of share or name of company:') bot.register_next_step_handler(message, send_share_price) - - + def send_share_price(message): - str_share_price = share_fetcher.get_share_information_markdown(str(message.text)) - bot.reply_to(message, str_share_price, parse_mode="MARKDOWNV2") + str_share_price = share_fetcher.get_share_information(str(message.text)) + bot.reply_to(message, str_share_price) -@bot.message_handler(commands=['allnews', 'Allnews']) # /allnews -> get all news +@bot.message_handler(commands=['allnews', 'Allnews']) # /allnews -> get all news def send_all_news(message): + """ Get news for keywords of user :type message: message object bot :param message: message that was reacted to, in this case always containing '/allnews' @@ -291,31 +297,31 @@ def send_all_news(message): """ user_id = int(message.from_user.id) - keywords = api_handler.get_user_keywords(user_id) # get keywords of user + keywords = api_handler.get_user_keywords(user_id) # get keywords of user - if keywords == None: # true if user is not registered - bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on ' + os.getenv("WEBSITE_URL")) + if keywords == None: # true if user is not registered + bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info') + return + + if not keywords: # true if user is registered but does not have any keywords + bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /news') return - if not keywords: # true if user is registered but does not have any keywords - bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /addkeyword') - return - - keywords_search = ' OR '.join(keywords) # concat all keywords with OR -> NewsAPI can understand OR, AND, NOT etc. - now = dt.datetime.now().date() # get current date - from_date = now - dt.timedelta(days=7) # get date 7 days ago -> limit age of news to 7 days old max + keywords_search = ' OR '.join(keywords) # concat all keywords with OR -> NewsAPI can understand OR, AND, NOT etc. + now = dt.datetime.now().date() # get current date + from_date = now - dt.timedelta(days=7) # get date 7 days ago -> limit age of news to 7 days old max from_date_formatted = dt.datetime.strftime(from_date, '%Y-%m-%d') - news_list = news.get_all_news_by_keyword(keywords_search, from_date_formatted)["articles"] # array of JSON article objects + news_list = news.get_all_news_by_keyword(keywords_search, from_date_formatted)["articles"] # array of JSON article objects - if news_list: # true if news_list is not empty + if news_list: # true if news_list is not empty for article in news_list: formatted_article = news.format_article(article) - bot.send_message(chat_id=user_id, text=formatted_article, parse_mode="MARKDOWNV2") # Markdown allows to write bold text with * etc. + bot.send_message(chat_id=user_id, text=formatted_article, parse_mode="MARKDOWN") # Markdown allows to write bold text with * etc. else: bot.send_message(chat_id=user_id, text='No news found for your keywords.') + - -@bot.message_handler(commands=['news', 'News']) # /news -> get news for specific keyword +@bot.message_handler(commands=['news', 'News']) # /news -> get news for specific keyword def send_news(message): """ Get news for keywords of user @@ -327,33 +333,31 @@ def send_news(message): :rtype: none """ user_id = int(message.from_user.id) - keywords = api_handler.get_user_keywords(user_id) # get keywords of user + keywords = api_handler.get_user_keywords(user_id) # get keywords of user - if keywords == None: # true if user is not registered - bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on ' + os.getenv("WEBSITE_URL")) + if keywords == None: # true if user is not registered + bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info') return - - if not keywords: # true if user is registered but does not have any keywords + + if not keywords: # true if user is registered but does not have any keywords bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /addkeyword') return if keywords: for keyword in keywords: top_news = news.get_top_news_by_keyword(keyword)["articles"] - if top_news == None: # true if request to NewsAPI failed + if top_news == None: # true if request to NewsAPI failed bot.send_message(chat_id=user_id, text='News Server did not respond correctly. Try again later.') - if not top_news: # true if no news found for keyword (empty list) - keyword = hf.make_markdown_proof(keyword) - bot.send_message(chat_id=user_id, text=f'No news found for keyword: *{keyword}*', parse_mode="MARKDOWNV2") + if not top_news: # true if no news found for keyword (empty list) + bot.send_message(chat_id=user_id, text=f'No news found for keyword: *{keyword}*', parse_mode="MARKDOWN") else: - keyword = hf.make_markdown_proof(keyword) - formatted_article = news.format_article(top_news[0]) # only format and send most popular news - bot.send_message(chat_id=user_id, text=f"_keyword: {keyword}_\n\n" + formatted_article, parse_mode="MARKDOWNV2") # do not use v2 because of bugs related t "." in links + formatted_article = news.format_article(top_news[0]) # only format and send most popular news + bot.send_message(chat_id=user_id, text=f"_keyword: {keyword}_\n\n" + formatted_article, parse_mode="MARKDOWN") -@bot.message_handler(commands=['addkeyword', 'Addkeyword']) # /addkeyword -> add keyword to user +@bot.message_handler(commands=['addkeyword', 'Addkeyword']) # /addkeyword -> add keyword to user def add_keyword(message): """ Add keyword to user :type message: message object bot @@ -365,20 +369,19 @@ def add_keyword(message): """ user_id = int(message.from_user.id) bot.send_message(chat_id=user_id, text='Type keyword to add:') - bot.register_next_step_handler(message, store_keyword) # wait for user to send keyword, then call store_keyword function - - + bot.register_next_step_handler(message, store_keyword) # wait for user to send keyword, then call store_keyword function + def store_keyword(message): user_id = int(message.from_user.id) - keyword = str(message.text).lower() # lower to ensure Bitcoin and bitcoin is not stored as individual keywords - status = api_handler.set_keyword(user_id, keyword) # set keyword in database - if status == 200: # statuscode 200 means keyword was added successfully without errors - bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" added.') # duplicate keywords are denied by Database, so no need to check for that here + keyword = str(message.text).lower() # lower to ensure Bitcoin and bitcoin is not stored as individual keywords + status = api_handler.set_keyword(user_id, keyword) # set keyword in database + if status == 200: # statuscode 200 means keyword was added successfully without errors + bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" added.') # duplicate keywords are denied by Database, so no need to check for that here else: bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" could not be stored. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info (statuscode {status})') -@bot.message_handler(commands=['removekeyword', 'Removekeyword']) # /removekeyword -> remove keyword from user +@bot.message_handler(commands=['removekeyword', 'Removekeyword']) # /removekeyword -> remove keyword from user def remove_keyword(message): """ Remove keyword from user :type message: message object bot @@ -390,20 +393,19 @@ def remove_keyword(message): """ user_id = int(message.from_user.id) bot.send_message(chat_id=user_id, text='Type keyword to remove:') - bot.register_next_step_handler(message, remove_keyword_step) # wait for user to send keyword to remove, then call remove_keyword_step function - + bot.register_next_step_handler(message, remove_keyword_step) # wait for user to send keyword to remove, then call remove_keyword_step function def remove_keyword_step(message): user_id = int(message.from_user.id) keyword = str(message.text).lower() status = api_handler.delete_keyword(user_id, keyword) - if status == 200: # statuscode 200 means keyword was removed successfully without errors - bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" removed.') # checking if keyword to remove is in database are handled in database, not here + if status == 200: # statuscode 200 means keyword was removed successfully without errors + bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" removed.') # checking if keyword to remove is in database are handled in database, not here else: bot.send_message(chat_id=user_id, text=f'Failed deleting keyword "{keyword}". (statuscode {status})') -@bot.message_handler(commands=['keywords', 'Keywords']) # /keywords -> get keywords of user +@bot.message_handler(commands=['keywords', 'Keywords']) # /keywords -> get keywords of user def send_keywords(message): """ Send keywords of user :type message: message object bot @@ -414,54 +416,46 @@ def send_keywords(message): :rtype: none """ user_id = int(message.from_user.id) - keywords = api_handler.get_user_keywords(user_id) # get keywords of user - - if keywords == None: # true if user is not registered - bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on ' + os.getenv("WEBSITE_URL")) + keywords = api_handler.get_user_keywords(user_id) # get keywords of user + if keywords == None: # true if user is not registered + bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info') return - - if not keywords: # true if user is registered but does not have any keywords + if not keywords: # true if user is registered but does not have any keywords bot.send_message(chat_id=user_id, text='No keywords set for this account. Add keywords by using /addkeyword') return - - else: # send keyword list + else: # send keyword list keywords_str = ', '.join(keywords) - keywords_str = hf.make_markdown_proof(keywords_str) - - text = f'Your keywords are: _{keywords_str}_' - bot.send_message(chat_id=user_id, text=text, parse_mode="MARKDOWNV2") + bot.send_message(chat_id=user_id, text=f'Your keywords are: _{keywords_str}_', parse_mode="MARKDOWN") -@bot.message_handler(commands=['removeshare', 'Removeshare']) -def remove_share(message): - """ Remove share from portfolio +@bot.message_handler(commands=['portfolio', 'Portfolio']) +def send_portfolio(message): + """ Send portfolio of user :type message: message object bot - :param message: message that was reacted to, in this case always '/removeshare' - + :param message: message that was reacted to, in this case always '/portfolio' + :raises: none :rtype: none """ user_id = int(message.from_user.id) - - bot.send_message(chat_id=user_id, text='Type ISIN/Symbol/CompanyName of share to remove (if you are unsure do /shares, find your share and insert the value above amount):') - bot.register_next_step_handler(message, remove_share_step) # wait for user to send ISIN, then call remove_share_step function + portfolio = api_handler.get_user_portfolio(user_id) # get portfolio of user as json + if portfolio == None: # true if user is not registered + bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info') + return + if not portfolio: # true if user is registered but does not have any stocks in portfolio + bot.send_message(chat_id=user_id, text='You do not have any stocks in your portfolio.') + return + else: # send portfolio + for stock in portfolio: + comment = str(stock["comment"]) # comment may be written name of stock, comment is made by user when adding an stock to portfolio + count = "{:.2f}".format(float(stock["count"])) # round count to 2 decimal places + isin = str(stock["isin"]) + worth = "{:.2f}".format(float(stock["current_price"]) * float(stock["count"])) # round current_price to 2 decimal places + bot.send_message(chat_id=user_id, text=f'*{comment}*\n_{isin}_\namount: {count}\nworth: ${worth}', parse_mode="MARKDOWN") # formatted message in markdown -def remove_share_step(message): - user_id = int(message.from_user.id) - isin = str(message.text) - - status = api_handler.delete_share(int(user_id), str(isin)) # remove share from portfolio - - if status == 200: # statuscode 200 means share was removed successfully without errors - bot.send_message(chat_id=user_id, text=f'Share "{isin}" removed.') # checking if share to remove is in database are handled in database, not here - - else: - bot.send_message(chat_id=user_id, text=f'Failed deleting share "{isin}". (statuscode {status})\nMake sure that the share is in your portfolio and written exactly like there.') - - -@bot.message_handler(commands=['newtransaction', 'Newtransaction']) # tbd not working rn may be deleted in future +@bot.message_handler(commands=['newtransaction', 'Newtransaction']) #tbd not working rn may be deleted in future def set_new_transaction(message): """ Set new transaction for user :type message: message object bot @@ -472,18 +466,16 @@ def set_new_transaction(message): :rtype: none """ user_id = int(message.from_user.id) - bot.send_message(chat_id=user_id, - text='Type ",,," (time of transaction will be set to now, negative amount is selling, positive is buying):') + bot.send_message(chat_id=user_id, text='Type ",,," (time of transaction will be set to now, negative amount is selling, positive is buying):') bot.register_next_step_handler(message, set_new_transaction_step) - def set_new_transaction_step(message): user_id = int(message.from_user.id) - if not re.match(r"[A-Za-z0-9 ]+,[A-Za-z0-9]+,(-)?[0-9]+(.[0-9]+)?,[0-9]+(.[0-9]+)?", message.text): + if not re.match(r"[A-Za-z0-9]+,[A-Za-z0-9]+,(-)?[0-9]+(.[0-9]+)?,[0-9]+(.[0-9]+)?", message.text): bot.send_message(chat_id=user_id, text='Invalid format \n(e.g. Apple,US0378331005,53.2,120.4).\n Try again with /newtransaction.') return - + transaction_data = str(message.text).split(',') desc = str(transaction_data[0]) isin = str(transaction_data[1]) @@ -499,7 +491,7 @@ def set_new_transaction_step(message): else: bot.send_message(chat_id=user_id, text=f'Failed adding transaction. (statuscode {status})') - + @bot.message_handler(commands=['interval', 'Interval']) def send_interval(message): """ send interval for user @@ -511,73 +503,18 @@ def send_interval(message): :rtype: none """ user_id = int(message.from_user.id) - user_data = api_handler.get_user(user_id) # get cron interval of user (stored in user data) - if user_data == None: # true if user is not registered in DB - bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on ' + os.getenv("WEBSITE_URL") + ' and set an interval with /setinterval') + user_data = api_handler.get_user(user_id) # get cron interval of user (stored in user data) + if user_data == None: # true if user is not registered in DB + bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info and set an interval with /setinterval') return - else: # send interval - interval = str(user_data['cron']) # get cron from user data - if interval == 'None': # true if user has no cron set + else: # send interval + interval = str(user_data['cron']) # get cron from user data + if interval == 'None': # true if user has no cron set bot.send_message(chat_id=user_id, text='You do not have an interval set. Set one with /setinterval') return - formatted_interval = str(interval).replace(' ', '_') # replace spaces with underscores to add to url of crontab.guru + formatted_interval = str(interval).replace(' ', '_') # replace spaces with underscores to add to url of crontab.guru bot.send_message(chat_id=user_id, text=f'Your update interval: {interval} (https://crontab.guru/#{formatted_interval})') - - -@bot.message_handler(commands=['transactions', 'Transactions']) -def send_transactions(message): - """ send transactions for user - :type message: message object bot - :param message: message that was reacted to, in this case always '/transactions' - - :raises: none - - :rtype: none - """ - user_id = int(message.from_user.id) - transactions = api_handler.get_user_transactions(user_id) # get transactions of user - - if transactions == None: # true if user does not exist - bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on ' + os.getenv("WEBSITE_URL")) - return - - if not transactions: # true if user has no transactions - bot.send_message(chat_id=user_id, text='You do not have any transactions.') - return - - else: - - for transaction in transactions: - comment = hf.make_markdown_proof(transaction['comment']) or "\(no desc\)" # if comment is empty, make it "no desc" - isin = hf.make_markdown_proof(transaction['isin']) - amount = hf.make_markdown_proof(transaction['count']) - price = hf.make_markdown_proof(transaction['price']) - time = hf.make_markdown_proof(transaction['time']) - - bot.send_message(chat_id=user_id, text=f'_{comment}_\n{isin}\namount: {amount}\nprice: {price}\ntime: {time}', parse_mode="MARKDOWNV2") - - -@bot.message_handler(commands=['shares', 'Shares']) -def send_shares(message): - """ send shares for user - :type message: message object bot - :param message: message that was reacted to, in this case always '/shares' - - :raises: none - - :rtype: none - """ - user_id = int(message.from_user.id) - shares = api_handler.get_user_shares(user_id) # get shares of user - - if shares == None: # true if user does not exist - bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on ' + os.getenv("WEBSITE_URL")) - elif not shares: # true if user has no shares - bot.send_message(chat_id=user_id, text='You do not have any shares. Add shares on ' + os.getenv("WEBSITE_URL")) - else: - for element in shares: - bot.send_message(chat_id=user_id, text=share_fetcher.get_share_information_markdown(element), parse_mode="MARKDOWNV2") - + @bot.message_handler(commands=['setinterval', 'Setinterval']) def set_new_interval(message): @@ -591,27 +528,28 @@ def set_new_interval(message): """ user_id = int(message.from_user.id) bot.send_message(chat_id=user_id, text='Type interval in cron format:\n(https://crontab.guru/)') - bot.register_next_step_handler(message, set_new_interval_step) # executes function when user sends message - + bot.register_next_step_handler(message, set_new_interval_step) # executes function when user sends message def set_new_interval_step(message): + user_id = int(message.from_user.id) interval = str(message.text) - status = api_handler.set_cron_interval(user_id, interval) # send cron to db + status = api_handler.set_cron_interval(user_id, interval) # send cron to db if status == 200: bot.send_message(chat_id=user_id, text='Interval succesfully set.') return - - if status == -1: # only -1 when interval is invalid, not a real statuscode, but used from api_handler.set_cron_interval to tell the crontab has the wrong format + + if status == -1: # only -1 when interval is invalid, not a real statuscode, but used from api_handler.set_cron_interval to tell the crontab has the wrong format bot.send_message(chat_id=user_id, text='Invalid interval format. Try again with\n /setinterval.') return else: bot.send_message(chat_id=user_id, text=f'Failed setting interval. (statuscode {status})') -@bot.message_handler(func=lambda message: True) # Returning that command is unknown for any other statement +@bot.message_handler(func=lambda message: True) # Returning that command is unknown for any other statement def echo_all(message): + """ Tell that command is not known if it is no known command :type message: message object bot :param message: message that was reacted to, if no other command handler gets called @@ -620,15 +558,16 @@ def echo_all(message): :rtype: none """ - answer = 'Do not know this command or text: ' + message.text + answer = 'Do not know this command or text: ' + message.text bot.reply_to(message, answer) telebot.logger.setLevel(logging.DEBUG) -@bot.inline_handler(lambda query: query.query == 'text') # inline prints for debugging +@bot.inline_handler(lambda query: query.query == 'text') # inline prints for debugging def query_text(inline_query): + """ Output in the console about current user actions and status of bot :type inline_query: :param inline_query: @@ -646,6 +585,7 @@ def query_text(inline_query): def main_loop(): + """ Start bot :raises: none @@ -653,10 +593,9 @@ def main_loop(): """ bot.infinity_polling() - if __name__ == '__main__': try: main_loop() except KeyboardInterrupt: print('\nExiting by user request.\n') - sys.exit(0) + sys.exit(0) \ No newline at end of file diff --git a/telegram_bot/bot_updates.py b/telegram_bot/bot_updates.py index 3168bdd..5412190 100644 --- a/telegram_bot/bot_updates.py +++ b/telegram_bot/bot_updates.py @@ -2,22 +2,19 @@ script for regularly sending updates on shares and news based on user interval """ __author__ = "Florian Kellermann, Linus Eickhoff" -__date__ = "10.05.2022" -__version__ = "1.0.2" +__date__ = "26.04.2022" +__version__ = "1.0.2" __license__ = "None" -import os -import sys -import time - -from apscheduler.schedulers.background import BackgroundScheduler # scheduler for cron from dotenv import load_dotenv - -import helper_functions as hf import news.news_fetcher as news_fetcher -import shares.share_fetcher as share_fetcher -from api_handling.api_handler import API_Handler +import time +import os from bot import bot +import sys +from apscheduler.schedulers.background import BackgroundScheduler +from api_handling.api_handler import API_Handler + ''' * * * * * code @@ -36,21 +33,21 @@ user_crontab = [] load_dotenv(dotenv_path='.env') - def start_updater(): """ starting function for regularly sending updates :raises: none :rtype: none """ - + print("Bot updates started") + + my_handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) - my_handler = API_Handler(os.getenv("API_URL"), str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) - + update_crontab(my_handler) - - + + def update_crontab(p_my_handler): """ Updating crontab lists every hour :type pCurrent_Time: time when starting crontab update @@ -60,34 +57,34 @@ def update_crontab(p_my_handler): :rtype: none """ - + global user_crontab global user_ids - - all_users = p_my_handler.get_all_users() # get all users so crontabs can update for everybody - + + all_users = p_my_handler.get_all_users() + user_ids = [] user_crontab = [] - + for element in all_users: - if element["cron"] != '' and element["telegram_user_id"] != '': # check if both values are existing so I have consistent data + if element["cron"] != '' and element["telegram_user_id"] != '': try: user_ids.append(int(element["telegram_user_id"])) try: user_crontab.append(str(element["cron"])) - except: - user_ids.pop() # if something goes wrong with cron I have to delete matching user id - except: - continue - + except: continue + except: continue + + print(user_ids) - + update_based_on_crontab(user_ids, user_crontab, p_my_handler) - - update_crontab(p_my_handler) # restart the update after time sleep - - + + update_crontab(p_my_handler) + + def update_based_on_crontab(p_user_ids, p_user_crontab, p_my_handler): + """ Check all the crontab codes and add jobs to start in time :type p_user_ids: array :param p_user_ids: user id array of all users @@ -102,21 +99,21 @@ def update_based_on_crontab(p_user_ids, p_user_crontab, p_my_handler): :rtype: none """ - - my_scheduler = BackgroundScheduler() # schedule sends based on cron - + + my_scheduler = BackgroundScheduler() + for i in range(len(p_user_ids)): - cron_split = p_user_crontab[i].split(" ") # split it up to use in scheduler + cron_split = p_user_crontab[i].split(" ") print(cron_split[4], cron_split[1], cron_split[0], cron_split[3], cron_split[2]) - my_scheduler.add_job(update_for_user, 'cron', day_of_week=cron_split[4], hour=cron_split[1], minute=cron_split[0], month=cron_split[3], day=cron_split[2], args=(p_user_ids[i], p_my_handler)) - + my_scheduler.add_job(update_for_user, 'cron', day_of_week = cron_split[4] , hour= cron_split[1] , minute = cron_split[0], month= cron_split[3] , day=cron_split[2], args=(p_user_ids[i], p_my_handler )) + my_scheduler.start() - - time.sleep(600) # scheduler runs in background and I wait 10mins - my_scheduler.shutdown() # after this the new crontabs will be loaded - - + + time.sleep( 600 ) + my_scheduler.shutdown() + def update_for_user(p_user_id, p_my_handler): + """ Pull shares and send updates for specific user id :type p_user_id: integer :param p_user_id: user id of user that shall receive update @@ -130,52 +127,47 @@ def update_for_user(p_user_id, p_my_handler): """ share_symbols = [] share_amounts = [] - - my_portfolio = p_my_handler.get_user_portfolio(p_user_id) # get all existing shares for user - + share_courses = [] + + my_portfolio = p_my_handler.get_user_portfolio(p_user_id) + for element in my_portfolio: - if element["count"] != '' and element["isin"] != '': + if element["count"] != '' and element["isin"]!= '': print(element["count"], element["isin"]) share_symbols.append(element["isin"]) share_amounts.append(element["count"]) + share_courses.append(element["current_price"]) my_user = p_my_handler.get_user(p_user_id) - send_to_user("Hello %s this is your share update for today:" % str(my_user["username"]), pUser_id=p_user_id) - - shares = p_my_handler.get_user_shares(p_user_id) # all interest shares - - if len(share_symbols) != 0: # iterate through all shares + send_to_user("Hello %s this is your share update for today:"%str(my_user["username"]), pUser_id=p_user_id) + + if len(share_symbols) != 0: for i in range(len(share_symbols)): - my_price = share_fetcher.get_share_price_no_currency(share_symbols[i]) - my_update_message = f'{share_fetcher.get_share_information_markdown(share_symbols[i])}\ncount: {hf.make_markdown_proof(share_amounts[i])}\nTotal: {hf.make_markdown_proof(round(float(my_price) * float(share_amounts[i]), 2))} EUR' - bot.send_message(chat_id=p_user_id, text=my_update_message, parse_mode="MARKDOWNV2") + my_update_message = f'Symbol: {share_symbols[i]}\nCurrent Price per Share: {share_courses[i]}\nAmount owned: {share_amounts[i]}\nTotal Investment: {float(share_courses[i]) * float(share_amounts[i])}' + send_to_user(my_update_message, pUser_id=p_user_id) else: - send_to_user("No shares found for your account. Check " + os.getenv("WEBSITE_URL") + " to change your settings and add shares.", pUser_id=p_user_id) + send_to_user("No shares found for your account. Check https://gruppe1.testsites.info to change your settings and add shares.", pUser_id=p_user_id) - if len(shares) != 0: # Send updates on watchlist shares if existing - send_to_user("Your watchlist shares:", pUser_id=p_user_id) - for element in shares: - send_to_user(share_fetcher.get_share_information_markdown(element), pUser_id=p_user_id, md_mode=True) + keywords = p_my_handler.get_user_keywords(p_user_id) # get keywords as array - keywords = p_my_handler.get_user_keywords(p_user_id) # get keywords as array - - if (keywords): # if keywords exist and array is not empty + if(keywords): # if keywords exist and array is not empty send_to_user("If you haven't read yet: \nHere are some interesting news according to your keywords:", pUser_id=p_user_id) for keyword in keywords: news = news_fetcher.get_top_news_by_keyword(keyword)["articles"] - keyword = hf.make_markdown_proof(keyword) - if not news: # if empty news array - send_to_user(f"No news found for keyword _{keyword}_\.", pUser_id=p_user_id, md_mode=True) - - elif news == None: # if news is none - send_to_user(f"Server error for keyword _{keyword}_\.", pUser_id=p_user_id, md_mode=True) + if not news: # if empty news array + send_to_user(f"No news found for keyword _{keyword}_.", pUser_id=p_user_id, md_mode=True) + + if news == None: # if news is none + send_to_user(f"Server error for keyword _{keyword}_.", pUser_id=p_user_id, md_mode=True) else: - news_formatted = news_fetcher.format_article(news[0]) # format for message, only use the most popular article - send_to_user(f"_keyword: {keyword}_\n\n{news_formatted}", pUser_id=p_user_id, md_mode=True) # send news with related keyword in Markdown + news_formatted = news_fetcher.format_article(news[0]) # format for message, only use the most popular article + send_to_user(f"_keyword: {keyword}_\n\n{news_formatted}", pUser_id=p_user_id, md_mode=True) # send news with related keyword in Markdown - -def send_to_user(pText, pUser_id, md_mode=False): + + +def send_to_user(pText, pUser_id , md_mode = False): + """ Send message to user :type pText: string :param pText: Text to send to user @@ -191,15 +183,16 @@ def send_to_user(pText, pUser_id, md_mode=False): :rtype: none """ if md_mode: - bot.send_message(chat_id=pUser_id, text=pText, parse_mode="MARKDOWNV2") + bot.send_message(chat_id=pUser_id, text=pText, parse_mode="MARKDOWN") else: bot.send_message(chat_id=pUser_id, text=pText) + if __name__ == "__main__": try: start_updater() sys.exit(-1) except KeyboardInterrupt: print("Ending") - sys.exit(-1) + sys.exit(-1) \ No newline at end of file diff --git a/telegram_bot/helper_functions.py b/telegram_bot/helper_functions.py deleted file mode 100644 index ca6d4d2..0000000 --- a/telegram_bot/helper_functions.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -script for helper functions for bot related stuff -""" -__author__ = "Florian Kellermann, Linus Eickhoff" -__date__ = "10.05.2022" -__version__ = "1.0.0" -__license__ = "None" - - -def contains_markdownv1_symbols(text): - """ checks if text contains markdown symbols - :type text: string - - :param text: text to check - - :return: true if text contains markdown symbols - - :rtype: bool - """ - if text.find("_") != -1 or text.find("*") != -1 or text.find("`") != -1: # check if text contains relevant markdown symbols - return True - - return False - - -def make_markdown_proof(text): # used to avoid errors related to markdown parsemode for telegram messaging - """ makes text markdown proof - :type text: string - - :param text: text to make markdown proof - - :return: markdown proof text - - :rtype: string - """ - text = str(text) - - text = text.replace("_", "\\_") # replace _ with \_ because \ is used as escape character in markdown, double escape is needed because \ is also a escape character in strings - text = text.replace("*", "\\*") - text = text.replace("`", "\\`") - text = text.replace("[", "\\[") - text = text.replace("]", "\\]") - text = text.replace("(", "\\(") - text = text.replace(")", "\\)") - text = text.replace("#", "\\#") - text = text.replace("+", "\\+") - text = text.replace("-", "\\-") - text = text.replace("!", "\\!") - text = text.replace(".", "\\.") - text = text.replace("?", "\\?") - text = text.replace("/", "\\/") - text = text.replace("~", "\\~") - text = text.replace("|", "\\|") - text = text.replace("<", "\\<") - text = text.replace(">", "\\>") - text = text.replace("&", "\\&") - text = text.replace("^", "\\^") - text = text.replace("$", "\\$") - text = text.replace("%", "\\%") - text = text.replace("=", "\\=") - text = text.replace("@", "\\@") - - return text - - -if __name__ == '__main__': - print("this is a module for helper functions for the bot and should not be run directly") - print(make_markdown_proof("_test_")) - text = make_markdown_proof("_test_") - print(f"{text}") diff --git a/telegram_bot/news/article_example.json b/telegram_bot/news/article_example.json index b304752..368fbf4 100644 --- a/telegram_bot/news/article_example.json +++ b/telegram_bot/news/article_example.json @@ -1,32 +1,32 @@ { - "status": "ok", - "totalResults": 1, - "articles": [ - { - "source": { - "id": "the-verge", - "name": "The Verge" - }, - "author": "Justine Calma", - "title": "EU Parliament backs off plans to phase out energy-hungry cryptocurrencies", - "description": "EU Parliament abandoned a measure in its proposed legislative framework for regulating cryptocurrencies that would have amounted to a de facto ban on energy-hungry networks like Bitcoin.", - "url": "https://www.theverge.com/2022/3/14/22977132/bitcoin-european-union-parliament-ban-proof-of-work-cryptocurrencies", - "urlToImage": "https://cdn.vox-cdn.com/thumbor/8bE-uBwwu-eXg-CcB6cOqcAGVDw=/0x286:4000x2380/fit-in/1200x630/cdn.vox-cdn.com/uploads/chorus_asset/file/23315944/834392892.jpg", - "publishedAt": "2022-03-14T23:40:25Z", - "content": "But Bitcoin is still under scrutiny \r\nPower cords for bitcoin mining machines are plugged into electrical outlets at a mining facility operated by Bitmain Technologies Ltd. in Ordos, Inner Mongolia, \u2026 [+5797 chars]" - }, - { - "source": { - "id": "the-verge", - "name": "The Verge" - }, - "author": "Justine Calma", - "title": "EU Parliament backs off plans to phase out energy-hungry cryptocurrencies", - "description": "EU Parliament abandoned a measure in its proposed legislative framework for regulating cryptocurrencies that would have amounted to a de facto ban on energy-hungry networks like Bitcoin.", - "url": "https://www.theverge.com/2022/3/14/22977132/bitcoin-european-union-parliament-ban-proof-of-work-cryptocurrencies", - "urlToImage": "https://cdn.vox-cdn.com/thumbor/8bE-uBwwu-eXg-CcB6cOqcAGVDw=/0x286:4000x2380/fit-in/1200x630/cdn.vox-cdn.com/uploads/chorus_asset/file/23315944/834392892.jpg", - "publishedAt": "2022-03-14T23:40:25Z", - "content": "But Bitcoin is still under scrutiny \r\nPower cords for bitcoin mining machines are plugged into electrical outlets at a mining facility operated by Bitmain Technologies Ltd. in Ordos, Inner Mongolia, \u2026 [+5797 chars]" - } - ] + "status": "ok", + "totalResults": 1, + "articles": [ + { + "source": { + "id": "the-verge", + "name": "The Verge" + }, + "author": "Justine Calma", + "title": "EU Parliament backs off plans to phase out energy-hungry cryptocurrencies", + "description": "EU Parliament abandoned a measure in its proposed legislative framework for regulating cryptocurrencies that would have amounted to a de facto ban on energy-hungry networks like Bitcoin.", + "url": "https://www.theverge.com/2022/3/14/22977132/bitcoin-european-union-parliament-ban-proof-of-work-cryptocurrencies", + "urlToImage": "https://cdn.vox-cdn.com/thumbor/8bE-uBwwu-eXg-CcB6cOqcAGVDw=/0x286:4000x2380/fit-in/1200x630/cdn.vox-cdn.com/uploads/chorus_asset/file/23315944/834392892.jpg", + "publishedAt": "2022-03-14T23:40:25Z", + "content": "But Bitcoin is still under scrutiny \r\nPower cords for bitcoin mining machines are plugged into electrical outlets at a mining facility operated by Bitmain Technologies Ltd. in Ordos, Inner Mongolia, \u2026 [+5797 chars]" + }, + { + "source": { + "id": "the-verge", + "name": "The Verge" + }, + "author": "Justine Calma", + "title": "EU Parliament backs off plans to phase out energy-hungry cryptocurrencies", + "description": "EU Parliament abandoned a measure in its proposed legislative framework for regulating cryptocurrencies that would have amounted to a de facto ban on energy-hungry networks like Bitcoin.", + "url": "https://www.theverge.com/2022/3/14/22977132/bitcoin-european-union-parliament-ban-proof-of-work-cryptocurrencies", + "urlToImage": "https://cdn.vox-cdn.com/thumbor/8bE-uBwwu-eXg-CcB6cOqcAGVDw=/0x286:4000x2380/fit-in/1200x630/cdn.vox-cdn.com/uploads/chorus_asset/file/23315944/834392892.jpg", + "publishedAt": "2022-03-14T23:40:25Z", + "content": "But Bitcoin is still under scrutiny \r\nPower cords for bitcoin mining machines are plugged into electrical outlets at a mining facility operated by Bitmain Technologies Ltd. in Ordos, Inner Mongolia, \u2026 [+5797 chars]" + } + ] } \ No newline at end of file diff --git a/telegram_bot/news/news_fetcher.py b/telegram_bot/news/news_fetcher.py index 6fb21cb..bd083b5 100644 --- a/telegram_bot/news/news_fetcher.py +++ b/telegram_bot/news/news_fetcher.py @@ -3,34 +3,30 @@ script for news fetching (by keywords) """ __author__ = "Florian Kellermann, Linus Eickhoff" __date__ = "26.04.2022" -__version__ = "1.0.0" +__version__ = "1.0.0" __license__ = "None" -import os import sys - -import helper_functions as hf +import os import requests -from dotenv import load_dotenv -from newsapi import NewsApiClient -load_dotenv() # loads environment vars +from newsapi import NewsApiClient +from dotenv import load_dotenv + +load_dotenv() # loads environment vars # Init -api_key = os.getenv('NEWS_API_KEY') # get API Key from .env file -newsapi = NewsApiClient(api_key=api_key) # news api from https://newsapi.org/ - +api_key = os.getenv('NEWS_API_KEY') # get API Key from .env file +newsapi = NewsApiClient(api_key=api_key) # news api from https://newsapi.org/ try: # get all available news sources (e.g BBC, New York Times, etc.) source_json = requests.get(f"https://newsapi.org/v2/top-headlines/sources?apiKey={api_key}&language=en").json() sources = source_json["sources"] str_sources = ",".join([source["id"] for source in sources]) - except KeyError: - print("Error: Could not get sources, may be blocked because of too many requests (free newsapi is limited to 100 reqs per day)") - str_sources = str( - "Reuters, bbc, cnn, fox-news, google-news, hacker-news, nytimes, the-huffington-post, the-new-york-times, business-insider, bbc-news, cbc-news, ESPN, fox-sports, google-news-uk, independent, the-wall-street-journal, the-washington-times, time, usa-today") - + print("Error: Could not get sources") + sys.exit(1) + def get_all_news_by_keyword(keyword, from_date="2000-01-01"): """get all news to keyword @@ -41,8 +37,8 @@ def get_all_news_by_keyword(keyword, from_date="2000-01-01"): Returns: JSON/dict: dict containing articles """ - top_headlines = newsapi.get_everything(q=keyword, sources=str_sources, language='en', from_param=from_date) # keywords can be combined with OR (e.g. keyword = "bitcoin OR ethereum") - if (top_headlines["status"] == "ok"): + top_headlines = newsapi.get_everything(q=keyword, sources=str_sources, language='en', from_param=from_date) # keywords can be combined with OR (e.g. keyword = "bitcoin OR ethereum") + if(top_headlines["status"] == "ok"): return top_headlines else: return None @@ -56,8 +52,8 @@ def get_top_news_by_keyword(keyword): Returns: JSON/dict: dict containing articles """ - top_headlines = newsapi.get_top_headlines(q=keyword, sources=str_sources, language='en') # get top headlines, measured by popularity from NewsApi - if (top_headlines["status"] == "ok"): + top_headlines = newsapi.get_top_headlines(q=keyword, sources=str_sources, language='en') # get top headlines, measured by popularity from NewsApi + if(top_headlines["status"] == "ok"): return top_headlines else: return None @@ -71,24 +67,23 @@ def format_article(article): Returns: String: formatted article - """ - sourcename = hf.make_markdown_proof(article["source"]["name"]) # make attributes markdownv2 proof - headline = hf.make_markdown_proof(article["title"]) - url = hf.make_markdown_proof(article["url"]) - formatted_article = f"_{sourcename}_\n*{headline}*\n\n{url}" # formatting in Markdown syntax + """ + sourcename = article["source"]["name"] + headline = article["title"] + url = article["url"] + formatted_article = f"_{sourcename}_\n*{headline}*\n\n{url}" # formatting in Markdown syntax return formatted_article - -if __name__ == '__main__': # only execute if script is called directly -> for simple testing +if __name__ == '__main__': # only execute if script is called directly -> for simple testing print("this is a module and should not be run directly") print("fetching top news by keyword bitcoin...") - + articles = get_all_news_by_keyword("bitcoin") formatted_article = format_article(articles["articles"][0]) print(formatted_article) articles = get_top_news_by_keyword("bitcoin") formatted_article = format_article(articles["articles"][0]) print(formatted_article) - sys.exit(1) + sys.exit(1) \ No newline at end of file diff --git a/telegram_bot/shares/share_fetcher.py b/telegram_bot/shares/share_fetcher.py index e283444..b424443 100644 --- a/telegram_bot/shares/share_fetcher.py +++ b/telegram_bot/shares/share_fetcher.py @@ -2,16 +2,14 @@ script for share fetching (by symbols (e.g. AAPL, TSLA etc.)) """ __author__ = "Florian Kellermann, Linus Eickhoff" -__date__ = "10.05.2022" -__version__ = "1.0.1" +__date__ = "15.03.2022" +__version__ = "1.0.0" __license__ = "None" -import helper_functions as hf import investpy import pandas from currency_converter import CurrencyConverter - def get_share_price(str_search_for): """get stock price per share for company name or isin or symbol @@ -23,113 +21,92 @@ def get_share_price(str_search_for): try: search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], n_results=1, countries=['germany']) - currency = str(search_result.retrieve_currency()) # retrieve currency from data - # should always be Euro because of countries=['germany'] - - recent_data = pandas.DataFrame(search_result.retrieve_recent_data()) # stock prices of last few days - - stock_price = recent_data.iloc[-1]["Close"] # retrieve latest stock price - + currency = str(search_result.retrieve_currency()) + + recent_data = pandas.DataFrame(search_result.retrieve_recent_data()) + + stock_price = recent_data.iloc[-1]["Close"] + stock_price = round(float(stock_price), 2) - - str_return = str(stock_price) + " " + str(currency) # return + currency - + + str_return =str(stock_price) + " " + str(currency) + return str_return - - except RuntimeError: # if no shares are found for germany (e.g. isin: US.....) + + except RuntimeError: try: - my_Converter = CurrencyConverter() # need a currency converter - + my_Converter = CurrencyConverter() + search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], n_results=1) - + currency = str(search_result.retrieve_currency()) - + recent_data = pandas.DataFrame(search_result.retrieve_recent_data()) - + stock_price = recent_data.iloc[-1]["Close"] - - # convert stock price from currency to EUR + stock_price = my_Converter.convert(float(stock_price), str(currency), 'EUR') - + stock_price = round(float(stock_price), 2) - - str_return = str(stock_price) + " EUR" - + + str_return =str(stock_price) + " EUR" + return str_return except RuntimeError: return "None" - def get_share_price_no_currency(str_search_for): """get stock price per share for company name or isin or symbol no currency + Args: str_search_for (string): search for this string/isin + Returns: none """ try: search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], - countries=['germany'], n_results=1) - + countries=['germany'], n_results=1) + + recent_data = pandas.DataFrame(search_result.retrieve_recent_data()) - + stock_price = recent_data.iloc[-1]["Close"] - + stock_price = round(float(stock_price), 2) - + return stock_price - + except RuntimeError: my_Converter = CurrencyConverter() - + search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], n_results=1) - + currency = str(search_result.retrieve_currency()) - + recent_data = pandas.DataFrame(search_result.retrieve_recent_data()) - + stock_price = recent_data.iloc[-1]["Close"] - + stock_price = my_Converter.convert(float(stock_price), str(currency), 'EUR') - + stock_price = round(float(stock_price), 2) - - str_return = str(stock_price) - + + str_return =str(stock_price) + return str_return def get_share_information(str_search_for): search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], - countries=['germany'], n_results=1) - + countries=['germany'], n_results=1) + str_return = "Company: " + search_result.name + "\nSymbol: " + search_result.symbol + "\nCurrent Price/Share: " + get_share_price(str_search_for) - + return str_return - -def get_share_information_markdown(str_search_for): - try: - search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], - countries=['germany'], n_results=1) - - except RuntimeError as e: - return hf.make_markdown_proof(f"no shares found for \"{str_search_for}\"") # if no shares are found, make error message markdown proof and return - - except ConnectionError as e: - return hf.make_markdown_proof(f"connection not possible. Try again later.") # if no connection, make error message markdown proof and return - - str_return = f'*{hf.make_markdown_proof(search_result.name)}*\n_{hf.make_markdown_proof(search_result.symbol)}_\nworth: {hf.make_markdown_proof(get_share_price(str_search_for))}' - return str_return - - -def get_share_information_simple(str_search_for): - search_result = investpy.search_quotes(text=str_search_for, products=['stocks'], - countries=['germany'], n_results=1) - - str_return = search_result.name + "\n" + search_result.symbol + "\nworth: " + get_share_price(str_search_for) - return str_return - - if __name__ == "__main__": - print("None") + print(get_share_price("US2515661054")) + print(get_share_price("DE0005557508")) + print(get_share_price_no_currency("US2515661054")) + print(get_share_price_no_currency("DE0005557508")) \ No newline at end of file diff --git a/telegram_bot/shares/shares_example.json b/telegram_bot/shares/shares_example.json index 620e7a1..3b3b2fc 100644 --- a/telegram_bot/shares/shares_example.json +++ b/telegram_bot/shares/shares_example.json @@ -1,16 +1,16 @@ { - "user": "FloKell", - "share_count": 2, - "shares": [ - { - "symbol": "APC.DE", - "price_bought": "50.06", - "amount_bought": "5.1" - }, - { - "symbol": "TL0.DE", - "price_bought": "450.06", - "amount_bought": "5.13" - } - ] + "user": "FloKell", + "share_count": 2, + "shares": [ + { + "symbol": "APC.DE", + "price_bought": "50.06", + "amount_bought": "5.1" + }, + { + "symbol": "TL0.DE", + "price_bought": "450.06", + "amount_bought": "5.13" + } + ] } \ No newline at end of file