From bf99a78d44fa0a45d29e76e5afceebbb53553224 Mon Sep 17 00:00:00 2001 From: Kevin Pauer Date: Tue, 10 May 2022 21:41:59 +0200 Subject: [PATCH 1/7] Fix last few bugs --- frontend/src/app/Views/dashboard/dashboard.component.html | 8 ++++---- frontend/src/app/Views/dashboard/dashboard.component.ts | 5 ++--- .../dashboard/user-dialog/user-dialog.component.html | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/Views/dashboard/dashboard.component.html b/frontend/src/app/Views/dashboard/dashboard.component.html index db59f2b..4c3fb73 100644 --- a/frontend/src/app/Views/dashboard/dashboard.component.html +++ b/frontend/src/app/Views/dashboard/dashboard.component.html @@ -33,7 +33,7 @@ Price - {{ element.current_price }}$ + {{ element.current_price }}€ @@ -84,18 +84,18 @@
savings{{ depotCurrentValue.toFixed(2) }}$ + >{{ depotCurrentValue.toFixed(2) }}€
paid{{ depotCost.toFixed(2) }}$ + >{{ depotCost.toFixed(2) }}€
account_balance{{ profit.toFixed(2) }}${{ profit.toFixed(2) }}€
diff --git a/frontend/src/app/Views/dashboard/dashboard.component.ts b/frontend/src/app/Views/dashboard/dashboard.component.ts index 23d514d..449e21e 100644 --- a/frontend/src/app/Views/dashboard/dashboard.component.ts +++ b/frontend/src/app/Views/dashboard/dashboard.component.ts @@ -63,8 +63,6 @@ export class DashboardComponent implements OnInit { }); } this.dataSourceTransactions = TRANSACTION_DATA; - //TODO move to helper service - this.profit = this.depotCurrentValue - this.depotCost; }); } @@ -78,7 +76,8 @@ export class DashboardComponent implements OnInit { console.log(data); this.depotCurrentValue = 0; for (let i = 0; i < data.data.length; i++) { - this.depotCurrentValue = data.data[i].current_price; + this.depotCurrentValue += + data.data[i].current_price * data.data[i].count; STOCK_DATA.push({ count: data.data[i].count, comment: data.data[i].comment, 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 460d431..c3e31ee 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 @@ -20,7 +20,7 @@ />
- +
@@ -61,7 +61,7 @@
- + Date: Thu, 12 May 2022 09:09:05 +0200 Subject: [PATCH 2/7] Reformatting --- telegram_bot/bot_updates.py | 67 ++++++++++++++-------------- telegram_bot/shares/share_fetcher.py | 45 +++++++++---------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/telegram_bot/bot_updates.py b/telegram_bot/bot_updates.py index 8f391ef..95396f1 100644 --- a/telegram_bot/bot_updates.py +++ b/telegram_bot/bot_updates.py @@ -10,7 +10,7 @@ import os import sys import time -from apscheduler.schedulers.background import BackgroundScheduler # scheduler for cron +from apscheduler.schedulers.background import BackgroundScheduler # scheduler for cron from dotenv import load_dotenv import helper_functions as hf @@ -63,30 +63,30 @@ def update_crontab(p_my_handler): 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() # 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 + 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 - - + 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 - - + + 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 @@ -102,19 +102,20 @@ 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() # 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 + 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 - + + 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 @@ -129,9 +130,9 @@ 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 - + + 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"]) @@ -139,11 +140,11 @@ def update_for_user(p_user_id, p_my_handler): 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 + 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' @@ -164,10 +165,10 @@ def update_for_user(p_user_id, p_my_handler): news = news_fetcher.get_top_news_by_keyword(keyword)["articles"] keyword = hf.make_markdown_proof(keyword) - if not news: # if empty news array + 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 + + 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 diff --git a/telegram_bot/shares/share_fetcher.py b/telegram_bot/shares/share_fetcher.py index 66cf0a4..e283444 100644 --- a/telegram_bot/shares/share_fetcher.py +++ b/telegram_bot/shares/share_fetcher.py @@ -23,23 +23,23 @@ 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 + 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 - + + 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 - + + 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.....) + + except RuntimeError: # if no shares are found for germany (e.g. isin: US.....) try: - my_Converter = CurrencyConverter() # need a currency converter - + 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()) @@ -47,10 +47,10 @@ def get_share_price(str_search_for): 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') - + + # 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" @@ -109,17 +109,16 @@ def get_share_information(str_search_for): 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) - + 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 + 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 - + 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 -- 2.45.2 From 5d66376c7734717de8323c4abf44f292d9de949a Mon Sep 17 00:00:00 2001 From: Florian Kaiser <44125287+H4CK3R-01@users.noreply.github.com> Date: Thu, 12 May 2022 09:17:09 +0200 Subject: [PATCH 3/7] Update bot-settings.component.ts --- frontend/src/app/Views/bot-settings/bot-settings.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/Views/bot-settings/bot-settings.component.ts b/frontend/src/app/Views/bot-settings/bot-settings.component.ts index aa96424..6f752ec 100644 --- a/frontend/src/app/Views/bot-settings/bot-settings.component.ts +++ b/frontend/src/app/Views/bot-settings/bot-settings.component.ts @@ -130,6 +130,6 @@ export class BotSettingsComponent implements OnInit { use24HourTime: true, hideSeconds: true, - cronFlavor: 'quartz', //standard or quartz + cronFlavor: 'standard', //standard or quartz }; } -- 2.45.2 From f54850963c02e54418c23d221e3a860ab213049d Mon Sep 17 00:00:00 2001 From: Linus E <75929322+Rripped@users.noreply.github.com> Date: Thu, 12 May 2022 09:39:57 +0200 Subject: [PATCH 4/7] small comment fix --- telegram_bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram_bot/bot.py b/telegram_bot/bot.py index 0763021..3ba5689 100644 --- a/telegram_bot/bot.py +++ b/telegram_bot/bot.py @@ -464,7 +464,7 @@ def remove_share(message): """ 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 /portfolio, find your share and insert the value above amount):') + 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 -- 2.45.2 From 5f183de1747ceb46e84ec25f3d9fa0069b84dff6 Mon Sep 17 00:00:00 2001 From: kevinpauer Date: Thu, 12 May 2022 10:14:10 +0200 Subject: [PATCH 5/7] Move documentation --- .../frontend}/components/AppComponent.html | 0 .../frontend}/components/BotSettingsComponent.html | 0 .../components/ConfirmationDialogComponent.html | 0 .../frontend}/components/DashboardComponent.html | 0 .../frontend}/components/HeaderComponent.html | 0 .../frontend}/components/HelpDialogComponent.html | 0 .../frontend}/components/LoginComponent.html | 0 .../frontend}/components/ProfileComponent.html | 0 .../frontend}/components/RegisterComponent.html | 0 .../frontend}/components/UserDialogComponent.html | 0 .../frontend}/coverage.html | 0 .../frontend}/dependencies.html | 0 .../frontend}/fonts/FontAwesome.otf | Bin .../frontend}/fonts/fontawesome-webfont.eot | Bin .../frontend}/fonts/fontawesome-webfont.svg | 0 .../frontend}/fonts/fontawesome-webfont.ttf | Bin .../frontend}/fonts/fontawesome-webfont.woff | Bin .../frontend}/fonts/fontawesome-webfont.woff2 | Bin .../frontend}/fonts/ionicons.eot | Bin .../frontend}/fonts/ionicons.svg | 0 .../frontend}/fonts/ionicons.ttf | Bin .../frontend}/fonts/ionicons.woff | Bin .../frontend}/fonts/ionicons.woff2 | Bin .../frontend}/fonts/roboto-v15-latin-300.eot | Bin .../frontend}/fonts/roboto-v15-latin-300.svg | 0 .../frontend}/fonts/roboto-v15-latin-300.ttf | Bin .../frontend}/fonts/roboto-v15-latin-300.woff | Bin .../frontend}/fonts/roboto-v15-latin-300.woff2 | Bin .../frontend}/fonts/roboto-v15-latin-700.eot | Bin .../frontend}/fonts/roboto-v15-latin-700.svg | 0 .../frontend}/fonts/roboto-v15-latin-700.ttf | Bin .../frontend}/fonts/roboto-v15-latin-700.woff | Bin .../frontend}/fonts/roboto-v15-latin-700.woff2 | Bin .../frontend}/fonts/roboto-v15-latin-italic.eot | Bin .../frontend}/fonts/roboto-v15-latin-italic.svg | 0 .../frontend}/fonts/roboto-v15-latin-italic.ttf | Bin .../frontend}/fonts/roboto-v15-latin-italic.woff | Bin .../frontend}/fonts/roboto-v15-latin-italic.woff2 | Bin .../frontend}/fonts/roboto-v15-latin-regular.eot | Bin .../frontend}/fonts/roboto-v15-latin-regular.svg | 0 .../frontend}/fonts/roboto-v15-latin-regular.ttf | Bin .../frontend}/fonts/roboto-v15-latin-regular.woff | Bin .../frontend}/fonts/roboto-v15-latin-regular.woff2 | Bin .../frontend}/graph/dependencies.svg | 0 .../images/compodoc-vectorise-inverted.png | Bin .../images/compodoc-vectorise-inverted.svg | 0 .../frontend}/images/compodoc-vectorise.png | Bin .../frontend}/images/compodoc-vectorise.svg | 0 .../frontend}/images/compodoc.png | Bin .../frontend}/images/compodoc.svg | 0 .../images/coverage-badge-documentation.svg | 0 .../frontend}/images/favicon.ico | Bin .../frontend}/index.html | 0 .../frontend}/injectables/AuthInterceptor.html | 0 .../frontend}/injectables/AuthService.html | 0 .../frontend}/injectables/BotService.html | 0 .../frontend}/injectables/DataService.html | 0 .../frontend}/injectables/HelperService.html | 0 .../frontend}/injectables/ProfileService.html | 0 .../frontend}/injectables/TokenStorageService.html | 0 .../frontend}/interceptors/AuthInterceptor.html | 0 .../frontend}/interfaces/Fruit.html | 0 .../frontend}/interfaces/Keyword.html | 0 .../frontend}/interfaces/PeriodicElement.html | 0 .../frontend}/interfaces/Share.html | 0 .../frontend}/interfaces/Stock.html | 0 .../frontend}/interfaces/TransactionData.html | 0 .../frontend}/js/compodoc.js | 0 .../frontend}/js/lazy-load-graphs.js | 0 .../frontend}/js/libs/EventDispatcher.js | 0 .../frontend}/js/libs/bootstrap-native.js | 0 .../frontend}/js/libs/clipboard.min.js | 0 .../js/libs/custom-elements-es5-adapter.js | 0 .../frontend}/js/libs/custom-elements.min.js | 0 .../frontend}/js/libs/d3.v3.min.js | 0 .../frontend}/js/libs/deep-iterator.js | 0 .../frontend}/js/libs/es6-shim.min.js | 0 .../frontend}/js/libs/highlight.pack.js | 0 .../js/libs/highlightjs-line-numbers.min.js | 0 .../frontend}/js/libs/htmlparser.js | 0 .../frontend}/js/libs/innersvg.js | 0 .../frontend}/js/libs/lit-html.js | 0 .../frontend}/js/libs/prism.js | 0 .../frontend}/js/libs/promise.min.js | 0 .../frontend}/js/libs/svg-pan-zoom.min.js | 0 .../frontend}/js/libs/tablesort.min.js | 0 .../frontend}/js/libs/tablesort.number.min.js | 0 .../frontend}/js/libs/vis.min.js | 0 .../frontend}/js/libs/zepto.min.js | 0 .../frontend}/js/menu-wc.js | 0 .../frontend}/js/menu-wc_es5.js | 0 .../frontend}/js/menu.js | 0 .../frontend}/js/routes.js | 0 .../frontend}/js/routes/routes_index.js | 0 .../frontend}/js/search/lunr.min.js | 0 .../frontend}/js/search/search-engine.js | 0 .../frontend}/js/search/search-lunr.js | 0 .../frontend}/js/search/search.js | 0 .../frontend}/js/search/search_index.js | 0 .../frontend}/js/sourceCode.js | 0 .../frontend}/js/svg-pan-zoom.controls.js | 0 .../frontend}/js/tabs.js | 0 .../frontend}/js/tree.js | 0 .../frontend}/miscellaneous.html | 0 .../frontend}/miscellaneous/variables.html | 0 .../frontend}/modules.html | 0 .../frontend}/modules/AppModule.html | 0 .../frontend}/modules/AppModule/dependencies.svg | 0 .../frontend}/modules/AppRoutingModule.html | 0 .../frontend}/overview.html | 0 .../frontend}/properties.html | 0 .../frontend}/routes.html | 0 .../frontend}/styles/bootstrap-card.css | 0 .../frontend}/styles/bootstrap.min.css | 0 .../frontend}/styles/compodoc.css | 0 .../frontend}/styles/dark.css | 0 .../frontend}/styles/font-awesome.min.css | 0 .../frontend}/styles/ionicons.min.css | 0 .../frontend}/styles/laravel.css | 0 .../frontend}/styles/material.css | 0 .../frontend}/styles/monokai-sublime.css | 0 .../frontend}/styles/original.css | 0 .../frontend}/styles/postmark.css | 0 .../frontend}/styles/prism.css | 0 .../frontend}/styles/readthedocs.css | 0 .../frontend}/styles/reset.css | 0 .../frontend}/styles/stripe.css | 0 .../frontend}/styles/style.css | 0 .../frontend}/styles/tablesort.css | 0 .../frontend}/styles/vagrant.css | 0 130 files changed, 0 insertions(+), 0 deletions(-) rename {frontend/documentation => documentation/frontend}/components/AppComponent.html (100%) rename {frontend/documentation => documentation/frontend}/components/BotSettingsComponent.html (100%) rename {frontend/documentation => documentation/frontend}/components/ConfirmationDialogComponent.html (100%) rename {frontend/documentation => documentation/frontend}/components/DashboardComponent.html (100%) rename {frontend/documentation => documentation/frontend}/components/HeaderComponent.html (100%) rename {frontend/documentation => documentation/frontend}/components/HelpDialogComponent.html (100%) rename {frontend/documentation => documentation/frontend}/components/LoginComponent.html (100%) rename {frontend/documentation => documentation/frontend}/components/ProfileComponent.html (100%) rename {frontend/documentation => documentation/frontend}/components/RegisterComponent.html (100%) rename {frontend/documentation => documentation/frontend}/components/UserDialogComponent.html (100%) rename {frontend/documentation => documentation/frontend}/coverage.html (100%) rename {frontend/documentation => documentation/frontend}/dependencies.html (100%) rename {frontend/documentation => documentation/frontend}/fonts/FontAwesome.otf (100%) rename {frontend/documentation => documentation/frontend}/fonts/fontawesome-webfont.eot (100%) rename {frontend/documentation => documentation/frontend}/fonts/fontawesome-webfont.svg (100%) rename {frontend/documentation => documentation/frontend}/fonts/fontawesome-webfont.ttf (100%) rename {frontend/documentation => documentation/frontend}/fonts/fontawesome-webfont.woff (100%) rename {frontend/documentation => documentation/frontend}/fonts/fontawesome-webfont.woff2 (100%) rename {frontend/documentation => documentation/frontend}/fonts/ionicons.eot (100%) rename {frontend/documentation => documentation/frontend}/fonts/ionicons.svg (100%) rename {frontend/documentation => documentation/frontend}/fonts/ionicons.ttf (100%) rename {frontend/documentation => documentation/frontend}/fonts/ionicons.woff (100%) rename {frontend/documentation => documentation/frontend}/fonts/ionicons.woff2 (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-300.eot (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-300.svg (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-300.ttf (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-300.woff (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-300.woff2 (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-700.eot (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-700.svg (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-700.ttf (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-700.woff (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-700.woff2 (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-italic.eot (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-italic.svg (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-italic.ttf (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-italic.woff (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-italic.woff2 (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-regular.eot (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-regular.svg (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-regular.ttf (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-regular.woff (100%) rename {frontend/documentation => documentation/frontend}/fonts/roboto-v15-latin-regular.woff2 (100%) rename {frontend/documentation => documentation/frontend}/graph/dependencies.svg (100%) rename {frontend/documentation => documentation/frontend}/images/compodoc-vectorise-inverted.png (100%) rename {frontend/documentation => documentation/frontend}/images/compodoc-vectorise-inverted.svg (100%) rename {frontend/documentation => documentation/frontend}/images/compodoc-vectorise.png (100%) rename {frontend/documentation => documentation/frontend}/images/compodoc-vectorise.svg (100%) rename {frontend/documentation => documentation/frontend}/images/compodoc.png (100%) rename {frontend/documentation => documentation/frontend}/images/compodoc.svg (100%) rename {frontend/documentation => documentation/frontend}/images/coverage-badge-documentation.svg (100%) rename {frontend/documentation => documentation/frontend}/images/favicon.ico (100%) rename {frontend/documentation => documentation/frontend}/index.html (100%) rename {frontend/documentation => documentation/frontend}/injectables/AuthInterceptor.html (100%) rename {frontend/documentation => documentation/frontend}/injectables/AuthService.html (100%) rename {frontend/documentation => documentation/frontend}/injectables/BotService.html (100%) rename {frontend/documentation => documentation/frontend}/injectables/DataService.html (100%) rename {frontend/documentation => documentation/frontend}/injectables/HelperService.html (100%) rename {frontend/documentation => documentation/frontend}/injectables/ProfileService.html (100%) rename {frontend/documentation => documentation/frontend}/injectables/TokenStorageService.html (100%) rename {frontend/documentation => documentation/frontend}/interceptors/AuthInterceptor.html (100%) rename {frontend/documentation => documentation/frontend}/interfaces/Fruit.html (100%) rename {frontend/documentation => documentation/frontend}/interfaces/Keyword.html (100%) rename {frontend/documentation => documentation/frontend}/interfaces/PeriodicElement.html (100%) rename {frontend/documentation => documentation/frontend}/interfaces/Share.html (100%) rename {frontend/documentation => documentation/frontend}/interfaces/Stock.html (100%) rename {frontend/documentation => documentation/frontend}/interfaces/TransactionData.html (100%) rename {frontend/documentation => documentation/frontend}/js/compodoc.js (100%) rename {frontend/documentation => documentation/frontend}/js/lazy-load-graphs.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/EventDispatcher.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/bootstrap-native.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/clipboard.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/custom-elements-es5-adapter.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/custom-elements.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/d3.v3.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/deep-iterator.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/es6-shim.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/highlight.pack.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/highlightjs-line-numbers.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/htmlparser.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/innersvg.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/lit-html.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/prism.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/promise.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/svg-pan-zoom.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/tablesort.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/tablesort.number.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/vis.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/libs/zepto.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/menu-wc.js (100%) rename {frontend/documentation => documentation/frontend}/js/menu-wc_es5.js (100%) rename {frontend/documentation => documentation/frontend}/js/menu.js (100%) rename {frontend/documentation => documentation/frontend}/js/routes.js (100%) rename {frontend/documentation => documentation/frontend}/js/routes/routes_index.js (100%) rename {frontend/documentation => documentation/frontend}/js/search/lunr.min.js (100%) rename {frontend/documentation => documentation/frontend}/js/search/search-engine.js (100%) rename {frontend/documentation => documentation/frontend}/js/search/search-lunr.js (100%) rename {frontend/documentation => documentation/frontend}/js/search/search.js (100%) rename {frontend/documentation => documentation/frontend}/js/search/search_index.js (100%) rename {frontend/documentation => documentation/frontend}/js/sourceCode.js (100%) rename {frontend/documentation => documentation/frontend}/js/svg-pan-zoom.controls.js (100%) rename {frontend/documentation => documentation/frontend}/js/tabs.js (100%) rename {frontend/documentation => documentation/frontend}/js/tree.js (100%) rename {frontend/documentation => documentation/frontend}/miscellaneous.html (100%) rename {frontend/documentation => documentation/frontend}/miscellaneous/variables.html (100%) rename {frontend/documentation => documentation/frontend}/modules.html (100%) rename {frontend/documentation => documentation/frontend}/modules/AppModule.html (100%) rename {frontend/documentation => documentation/frontend}/modules/AppModule/dependencies.svg (100%) rename {frontend/documentation => documentation/frontend}/modules/AppRoutingModule.html (100%) rename {frontend/documentation => documentation/frontend}/overview.html (100%) rename {frontend/documentation => documentation/frontend}/properties.html (100%) rename {frontend/documentation => documentation/frontend}/routes.html (100%) rename {frontend/documentation => documentation/frontend}/styles/bootstrap-card.css (100%) rename {frontend/documentation => documentation/frontend}/styles/bootstrap.min.css (100%) rename {frontend/documentation => documentation/frontend}/styles/compodoc.css (100%) rename {frontend/documentation => documentation/frontend}/styles/dark.css (100%) rename {frontend/documentation => documentation/frontend}/styles/font-awesome.min.css (100%) rename {frontend/documentation => documentation/frontend}/styles/ionicons.min.css (100%) rename {frontend/documentation => documentation/frontend}/styles/laravel.css (100%) rename {frontend/documentation => documentation/frontend}/styles/material.css (100%) rename {frontend/documentation => documentation/frontend}/styles/monokai-sublime.css (100%) rename {frontend/documentation => documentation/frontend}/styles/original.css (100%) rename {frontend/documentation => documentation/frontend}/styles/postmark.css (100%) rename {frontend/documentation => documentation/frontend}/styles/prism.css (100%) rename {frontend/documentation => documentation/frontend}/styles/readthedocs.css (100%) rename {frontend/documentation => documentation/frontend}/styles/reset.css (100%) rename {frontend/documentation => documentation/frontend}/styles/stripe.css (100%) rename {frontend/documentation => documentation/frontend}/styles/style.css (100%) rename {frontend/documentation => documentation/frontend}/styles/tablesort.css (100%) rename {frontend/documentation => documentation/frontend}/styles/vagrant.css (100%) diff --git a/frontend/documentation/components/AppComponent.html b/documentation/frontend/components/AppComponent.html similarity index 100% rename from frontend/documentation/components/AppComponent.html rename to documentation/frontend/components/AppComponent.html diff --git a/frontend/documentation/components/BotSettingsComponent.html b/documentation/frontend/components/BotSettingsComponent.html similarity index 100% rename from frontend/documentation/components/BotSettingsComponent.html rename to documentation/frontend/components/BotSettingsComponent.html diff --git a/frontend/documentation/components/ConfirmationDialogComponent.html b/documentation/frontend/components/ConfirmationDialogComponent.html similarity index 100% rename from frontend/documentation/components/ConfirmationDialogComponent.html rename to documentation/frontend/components/ConfirmationDialogComponent.html diff --git a/frontend/documentation/components/DashboardComponent.html b/documentation/frontend/components/DashboardComponent.html similarity index 100% rename from frontend/documentation/components/DashboardComponent.html rename to documentation/frontend/components/DashboardComponent.html diff --git a/frontend/documentation/components/HeaderComponent.html b/documentation/frontend/components/HeaderComponent.html similarity index 100% rename from frontend/documentation/components/HeaderComponent.html rename to documentation/frontend/components/HeaderComponent.html diff --git a/frontend/documentation/components/HelpDialogComponent.html b/documentation/frontend/components/HelpDialogComponent.html similarity index 100% rename from frontend/documentation/components/HelpDialogComponent.html rename to documentation/frontend/components/HelpDialogComponent.html diff --git a/frontend/documentation/components/LoginComponent.html b/documentation/frontend/components/LoginComponent.html similarity index 100% rename from frontend/documentation/components/LoginComponent.html rename to documentation/frontend/components/LoginComponent.html diff --git a/frontend/documentation/components/ProfileComponent.html b/documentation/frontend/components/ProfileComponent.html similarity index 100% rename from frontend/documentation/components/ProfileComponent.html rename to documentation/frontend/components/ProfileComponent.html diff --git a/frontend/documentation/components/RegisterComponent.html b/documentation/frontend/components/RegisterComponent.html similarity index 100% rename from frontend/documentation/components/RegisterComponent.html rename to documentation/frontend/components/RegisterComponent.html diff --git a/frontend/documentation/components/UserDialogComponent.html b/documentation/frontend/components/UserDialogComponent.html similarity index 100% rename from frontend/documentation/components/UserDialogComponent.html rename to documentation/frontend/components/UserDialogComponent.html diff --git a/frontend/documentation/coverage.html b/documentation/frontend/coverage.html similarity index 100% rename from frontend/documentation/coverage.html rename to documentation/frontend/coverage.html diff --git a/frontend/documentation/dependencies.html b/documentation/frontend/dependencies.html similarity index 100% rename from frontend/documentation/dependencies.html rename to documentation/frontend/dependencies.html diff --git a/frontend/documentation/fonts/FontAwesome.otf b/documentation/frontend/fonts/FontAwesome.otf similarity index 100% rename from frontend/documentation/fonts/FontAwesome.otf rename to documentation/frontend/fonts/FontAwesome.otf diff --git a/frontend/documentation/fonts/fontawesome-webfont.eot b/documentation/frontend/fonts/fontawesome-webfont.eot similarity index 100% rename from frontend/documentation/fonts/fontawesome-webfont.eot rename to documentation/frontend/fonts/fontawesome-webfont.eot diff --git a/frontend/documentation/fonts/fontawesome-webfont.svg b/documentation/frontend/fonts/fontawesome-webfont.svg similarity index 100% rename from frontend/documentation/fonts/fontawesome-webfont.svg rename to documentation/frontend/fonts/fontawesome-webfont.svg diff --git a/frontend/documentation/fonts/fontawesome-webfont.ttf b/documentation/frontend/fonts/fontawesome-webfont.ttf similarity index 100% rename from frontend/documentation/fonts/fontawesome-webfont.ttf rename to documentation/frontend/fonts/fontawesome-webfont.ttf diff --git a/frontend/documentation/fonts/fontawesome-webfont.woff b/documentation/frontend/fonts/fontawesome-webfont.woff similarity index 100% rename from frontend/documentation/fonts/fontawesome-webfont.woff rename to documentation/frontend/fonts/fontawesome-webfont.woff diff --git a/frontend/documentation/fonts/fontawesome-webfont.woff2 b/documentation/frontend/fonts/fontawesome-webfont.woff2 similarity index 100% rename from frontend/documentation/fonts/fontawesome-webfont.woff2 rename to documentation/frontend/fonts/fontawesome-webfont.woff2 diff --git a/frontend/documentation/fonts/ionicons.eot b/documentation/frontend/fonts/ionicons.eot similarity index 100% rename from frontend/documentation/fonts/ionicons.eot rename to documentation/frontend/fonts/ionicons.eot diff --git a/frontend/documentation/fonts/ionicons.svg b/documentation/frontend/fonts/ionicons.svg similarity index 100% rename from frontend/documentation/fonts/ionicons.svg rename to documentation/frontend/fonts/ionicons.svg diff --git a/frontend/documentation/fonts/ionicons.ttf b/documentation/frontend/fonts/ionicons.ttf similarity index 100% rename from frontend/documentation/fonts/ionicons.ttf rename to documentation/frontend/fonts/ionicons.ttf diff --git a/frontend/documentation/fonts/ionicons.woff b/documentation/frontend/fonts/ionicons.woff similarity index 100% rename from frontend/documentation/fonts/ionicons.woff rename to documentation/frontend/fonts/ionicons.woff diff --git a/frontend/documentation/fonts/ionicons.woff2 b/documentation/frontend/fonts/ionicons.woff2 similarity index 100% rename from frontend/documentation/fonts/ionicons.woff2 rename to documentation/frontend/fonts/ionicons.woff2 diff --git a/frontend/documentation/fonts/roboto-v15-latin-300.eot b/documentation/frontend/fonts/roboto-v15-latin-300.eot similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-300.eot rename to documentation/frontend/fonts/roboto-v15-latin-300.eot diff --git a/frontend/documentation/fonts/roboto-v15-latin-300.svg b/documentation/frontend/fonts/roboto-v15-latin-300.svg similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-300.svg rename to documentation/frontend/fonts/roboto-v15-latin-300.svg diff --git a/frontend/documentation/fonts/roboto-v15-latin-300.ttf b/documentation/frontend/fonts/roboto-v15-latin-300.ttf similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-300.ttf rename to documentation/frontend/fonts/roboto-v15-latin-300.ttf diff --git a/frontend/documentation/fonts/roboto-v15-latin-300.woff b/documentation/frontend/fonts/roboto-v15-latin-300.woff similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-300.woff rename to documentation/frontend/fonts/roboto-v15-latin-300.woff diff --git a/frontend/documentation/fonts/roboto-v15-latin-300.woff2 b/documentation/frontend/fonts/roboto-v15-latin-300.woff2 similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-300.woff2 rename to documentation/frontend/fonts/roboto-v15-latin-300.woff2 diff --git a/frontend/documentation/fonts/roboto-v15-latin-700.eot b/documentation/frontend/fonts/roboto-v15-latin-700.eot similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-700.eot rename to documentation/frontend/fonts/roboto-v15-latin-700.eot diff --git a/frontend/documentation/fonts/roboto-v15-latin-700.svg b/documentation/frontend/fonts/roboto-v15-latin-700.svg similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-700.svg rename to documentation/frontend/fonts/roboto-v15-latin-700.svg diff --git a/frontend/documentation/fonts/roboto-v15-latin-700.ttf b/documentation/frontend/fonts/roboto-v15-latin-700.ttf similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-700.ttf rename to documentation/frontend/fonts/roboto-v15-latin-700.ttf diff --git a/frontend/documentation/fonts/roboto-v15-latin-700.woff b/documentation/frontend/fonts/roboto-v15-latin-700.woff similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-700.woff rename to documentation/frontend/fonts/roboto-v15-latin-700.woff diff --git a/frontend/documentation/fonts/roboto-v15-latin-700.woff2 b/documentation/frontend/fonts/roboto-v15-latin-700.woff2 similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-700.woff2 rename to documentation/frontend/fonts/roboto-v15-latin-700.woff2 diff --git a/frontend/documentation/fonts/roboto-v15-latin-italic.eot b/documentation/frontend/fonts/roboto-v15-latin-italic.eot similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-italic.eot rename to documentation/frontend/fonts/roboto-v15-latin-italic.eot diff --git a/frontend/documentation/fonts/roboto-v15-latin-italic.svg b/documentation/frontend/fonts/roboto-v15-latin-italic.svg similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-italic.svg rename to documentation/frontend/fonts/roboto-v15-latin-italic.svg diff --git a/frontend/documentation/fonts/roboto-v15-latin-italic.ttf b/documentation/frontend/fonts/roboto-v15-latin-italic.ttf similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-italic.ttf rename to documentation/frontend/fonts/roboto-v15-latin-italic.ttf diff --git a/frontend/documentation/fonts/roboto-v15-latin-italic.woff b/documentation/frontend/fonts/roboto-v15-latin-italic.woff similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-italic.woff rename to documentation/frontend/fonts/roboto-v15-latin-italic.woff diff --git a/frontend/documentation/fonts/roboto-v15-latin-italic.woff2 b/documentation/frontend/fonts/roboto-v15-latin-italic.woff2 similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-italic.woff2 rename to documentation/frontend/fonts/roboto-v15-latin-italic.woff2 diff --git a/frontend/documentation/fonts/roboto-v15-latin-regular.eot b/documentation/frontend/fonts/roboto-v15-latin-regular.eot similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-regular.eot rename to documentation/frontend/fonts/roboto-v15-latin-regular.eot diff --git a/frontend/documentation/fonts/roboto-v15-latin-regular.svg b/documentation/frontend/fonts/roboto-v15-latin-regular.svg similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-regular.svg rename to documentation/frontend/fonts/roboto-v15-latin-regular.svg diff --git a/frontend/documentation/fonts/roboto-v15-latin-regular.ttf b/documentation/frontend/fonts/roboto-v15-latin-regular.ttf similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-regular.ttf rename to documentation/frontend/fonts/roboto-v15-latin-regular.ttf diff --git a/frontend/documentation/fonts/roboto-v15-latin-regular.woff b/documentation/frontend/fonts/roboto-v15-latin-regular.woff similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-regular.woff rename to documentation/frontend/fonts/roboto-v15-latin-regular.woff diff --git a/frontend/documentation/fonts/roboto-v15-latin-regular.woff2 b/documentation/frontend/fonts/roboto-v15-latin-regular.woff2 similarity index 100% rename from frontend/documentation/fonts/roboto-v15-latin-regular.woff2 rename to documentation/frontend/fonts/roboto-v15-latin-regular.woff2 diff --git a/frontend/documentation/graph/dependencies.svg b/documentation/frontend/graph/dependencies.svg similarity index 100% rename from frontend/documentation/graph/dependencies.svg rename to documentation/frontend/graph/dependencies.svg diff --git a/frontend/documentation/images/compodoc-vectorise-inverted.png b/documentation/frontend/images/compodoc-vectorise-inverted.png similarity index 100% rename from frontend/documentation/images/compodoc-vectorise-inverted.png rename to documentation/frontend/images/compodoc-vectorise-inverted.png diff --git a/frontend/documentation/images/compodoc-vectorise-inverted.svg b/documentation/frontend/images/compodoc-vectorise-inverted.svg similarity index 100% rename from frontend/documentation/images/compodoc-vectorise-inverted.svg rename to documentation/frontend/images/compodoc-vectorise-inverted.svg diff --git a/frontend/documentation/images/compodoc-vectorise.png b/documentation/frontend/images/compodoc-vectorise.png similarity index 100% rename from frontend/documentation/images/compodoc-vectorise.png rename to documentation/frontend/images/compodoc-vectorise.png diff --git a/frontend/documentation/images/compodoc-vectorise.svg b/documentation/frontend/images/compodoc-vectorise.svg similarity index 100% rename from frontend/documentation/images/compodoc-vectorise.svg rename to documentation/frontend/images/compodoc-vectorise.svg diff --git a/frontend/documentation/images/compodoc.png b/documentation/frontend/images/compodoc.png similarity index 100% rename from frontend/documentation/images/compodoc.png rename to documentation/frontend/images/compodoc.png diff --git a/frontend/documentation/images/compodoc.svg b/documentation/frontend/images/compodoc.svg similarity index 100% rename from frontend/documentation/images/compodoc.svg rename to documentation/frontend/images/compodoc.svg diff --git a/frontend/documentation/images/coverage-badge-documentation.svg b/documentation/frontend/images/coverage-badge-documentation.svg similarity index 100% rename from frontend/documentation/images/coverage-badge-documentation.svg rename to documentation/frontend/images/coverage-badge-documentation.svg diff --git a/frontend/documentation/images/favicon.ico b/documentation/frontend/images/favicon.ico similarity index 100% rename from frontend/documentation/images/favicon.ico rename to documentation/frontend/images/favicon.ico diff --git a/frontend/documentation/index.html b/documentation/frontend/index.html similarity index 100% rename from frontend/documentation/index.html rename to documentation/frontend/index.html diff --git a/frontend/documentation/injectables/AuthInterceptor.html b/documentation/frontend/injectables/AuthInterceptor.html similarity index 100% rename from frontend/documentation/injectables/AuthInterceptor.html rename to documentation/frontend/injectables/AuthInterceptor.html diff --git a/frontend/documentation/injectables/AuthService.html b/documentation/frontend/injectables/AuthService.html similarity index 100% rename from frontend/documentation/injectables/AuthService.html rename to documentation/frontend/injectables/AuthService.html diff --git a/frontend/documentation/injectables/BotService.html b/documentation/frontend/injectables/BotService.html similarity index 100% rename from frontend/documentation/injectables/BotService.html rename to documentation/frontend/injectables/BotService.html diff --git a/frontend/documentation/injectables/DataService.html b/documentation/frontend/injectables/DataService.html similarity index 100% rename from frontend/documentation/injectables/DataService.html rename to documentation/frontend/injectables/DataService.html diff --git a/frontend/documentation/injectables/HelperService.html b/documentation/frontend/injectables/HelperService.html similarity index 100% rename from frontend/documentation/injectables/HelperService.html rename to documentation/frontend/injectables/HelperService.html diff --git a/frontend/documentation/injectables/ProfileService.html b/documentation/frontend/injectables/ProfileService.html similarity index 100% rename from frontend/documentation/injectables/ProfileService.html rename to documentation/frontend/injectables/ProfileService.html diff --git a/frontend/documentation/injectables/TokenStorageService.html b/documentation/frontend/injectables/TokenStorageService.html similarity index 100% rename from frontend/documentation/injectables/TokenStorageService.html rename to documentation/frontend/injectables/TokenStorageService.html diff --git a/frontend/documentation/interceptors/AuthInterceptor.html b/documentation/frontend/interceptors/AuthInterceptor.html similarity index 100% rename from frontend/documentation/interceptors/AuthInterceptor.html rename to documentation/frontend/interceptors/AuthInterceptor.html diff --git a/frontend/documentation/interfaces/Fruit.html b/documentation/frontend/interfaces/Fruit.html similarity index 100% rename from frontend/documentation/interfaces/Fruit.html rename to documentation/frontend/interfaces/Fruit.html diff --git a/frontend/documentation/interfaces/Keyword.html b/documentation/frontend/interfaces/Keyword.html similarity index 100% rename from frontend/documentation/interfaces/Keyword.html rename to documentation/frontend/interfaces/Keyword.html diff --git a/frontend/documentation/interfaces/PeriodicElement.html b/documentation/frontend/interfaces/PeriodicElement.html similarity index 100% rename from frontend/documentation/interfaces/PeriodicElement.html rename to documentation/frontend/interfaces/PeriodicElement.html diff --git a/frontend/documentation/interfaces/Share.html b/documentation/frontend/interfaces/Share.html similarity index 100% rename from frontend/documentation/interfaces/Share.html rename to documentation/frontend/interfaces/Share.html diff --git a/frontend/documentation/interfaces/Stock.html b/documentation/frontend/interfaces/Stock.html similarity index 100% rename from frontend/documentation/interfaces/Stock.html rename to documentation/frontend/interfaces/Stock.html diff --git a/frontend/documentation/interfaces/TransactionData.html b/documentation/frontend/interfaces/TransactionData.html similarity index 100% rename from frontend/documentation/interfaces/TransactionData.html rename to documentation/frontend/interfaces/TransactionData.html diff --git a/frontend/documentation/js/compodoc.js b/documentation/frontend/js/compodoc.js similarity index 100% rename from frontend/documentation/js/compodoc.js rename to documentation/frontend/js/compodoc.js diff --git a/frontend/documentation/js/lazy-load-graphs.js b/documentation/frontend/js/lazy-load-graphs.js similarity index 100% rename from frontend/documentation/js/lazy-load-graphs.js rename to documentation/frontend/js/lazy-load-graphs.js diff --git a/frontend/documentation/js/libs/EventDispatcher.js b/documentation/frontend/js/libs/EventDispatcher.js similarity index 100% rename from frontend/documentation/js/libs/EventDispatcher.js rename to documentation/frontend/js/libs/EventDispatcher.js diff --git a/frontend/documentation/js/libs/bootstrap-native.js b/documentation/frontend/js/libs/bootstrap-native.js similarity index 100% rename from frontend/documentation/js/libs/bootstrap-native.js rename to documentation/frontend/js/libs/bootstrap-native.js diff --git a/frontend/documentation/js/libs/clipboard.min.js b/documentation/frontend/js/libs/clipboard.min.js similarity index 100% rename from frontend/documentation/js/libs/clipboard.min.js rename to documentation/frontend/js/libs/clipboard.min.js diff --git a/frontend/documentation/js/libs/custom-elements-es5-adapter.js b/documentation/frontend/js/libs/custom-elements-es5-adapter.js similarity index 100% rename from frontend/documentation/js/libs/custom-elements-es5-adapter.js rename to documentation/frontend/js/libs/custom-elements-es5-adapter.js diff --git a/frontend/documentation/js/libs/custom-elements.min.js b/documentation/frontend/js/libs/custom-elements.min.js similarity index 100% rename from frontend/documentation/js/libs/custom-elements.min.js rename to documentation/frontend/js/libs/custom-elements.min.js diff --git a/frontend/documentation/js/libs/d3.v3.min.js b/documentation/frontend/js/libs/d3.v3.min.js similarity index 100% rename from frontend/documentation/js/libs/d3.v3.min.js rename to documentation/frontend/js/libs/d3.v3.min.js diff --git a/frontend/documentation/js/libs/deep-iterator.js b/documentation/frontend/js/libs/deep-iterator.js similarity index 100% rename from frontend/documentation/js/libs/deep-iterator.js rename to documentation/frontend/js/libs/deep-iterator.js diff --git a/frontend/documentation/js/libs/es6-shim.min.js b/documentation/frontend/js/libs/es6-shim.min.js similarity index 100% rename from frontend/documentation/js/libs/es6-shim.min.js rename to documentation/frontend/js/libs/es6-shim.min.js diff --git a/frontend/documentation/js/libs/highlight.pack.js b/documentation/frontend/js/libs/highlight.pack.js similarity index 100% rename from frontend/documentation/js/libs/highlight.pack.js rename to documentation/frontend/js/libs/highlight.pack.js diff --git a/frontend/documentation/js/libs/highlightjs-line-numbers.min.js b/documentation/frontend/js/libs/highlightjs-line-numbers.min.js similarity index 100% rename from frontend/documentation/js/libs/highlightjs-line-numbers.min.js rename to documentation/frontend/js/libs/highlightjs-line-numbers.min.js diff --git a/frontend/documentation/js/libs/htmlparser.js b/documentation/frontend/js/libs/htmlparser.js similarity index 100% rename from frontend/documentation/js/libs/htmlparser.js rename to documentation/frontend/js/libs/htmlparser.js diff --git a/frontend/documentation/js/libs/innersvg.js b/documentation/frontend/js/libs/innersvg.js similarity index 100% rename from frontend/documentation/js/libs/innersvg.js rename to documentation/frontend/js/libs/innersvg.js diff --git a/frontend/documentation/js/libs/lit-html.js b/documentation/frontend/js/libs/lit-html.js similarity index 100% rename from frontend/documentation/js/libs/lit-html.js rename to documentation/frontend/js/libs/lit-html.js diff --git a/frontend/documentation/js/libs/prism.js b/documentation/frontend/js/libs/prism.js similarity index 100% rename from frontend/documentation/js/libs/prism.js rename to documentation/frontend/js/libs/prism.js diff --git a/frontend/documentation/js/libs/promise.min.js b/documentation/frontend/js/libs/promise.min.js similarity index 100% rename from frontend/documentation/js/libs/promise.min.js rename to documentation/frontend/js/libs/promise.min.js diff --git a/frontend/documentation/js/libs/svg-pan-zoom.min.js b/documentation/frontend/js/libs/svg-pan-zoom.min.js similarity index 100% rename from frontend/documentation/js/libs/svg-pan-zoom.min.js rename to documentation/frontend/js/libs/svg-pan-zoom.min.js diff --git a/frontend/documentation/js/libs/tablesort.min.js b/documentation/frontend/js/libs/tablesort.min.js similarity index 100% rename from frontend/documentation/js/libs/tablesort.min.js rename to documentation/frontend/js/libs/tablesort.min.js diff --git a/frontend/documentation/js/libs/tablesort.number.min.js b/documentation/frontend/js/libs/tablesort.number.min.js similarity index 100% rename from frontend/documentation/js/libs/tablesort.number.min.js rename to documentation/frontend/js/libs/tablesort.number.min.js diff --git a/frontend/documentation/js/libs/vis.min.js b/documentation/frontend/js/libs/vis.min.js similarity index 100% rename from frontend/documentation/js/libs/vis.min.js rename to documentation/frontend/js/libs/vis.min.js diff --git a/frontend/documentation/js/libs/zepto.min.js b/documentation/frontend/js/libs/zepto.min.js similarity index 100% rename from frontend/documentation/js/libs/zepto.min.js rename to documentation/frontend/js/libs/zepto.min.js diff --git a/frontend/documentation/js/menu-wc.js b/documentation/frontend/js/menu-wc.js similarity index 100% rename from frontend/documentation/js/menu-wc.js rename to documentation/frontend/js/menu-wc.js diff --git a/frontend/documentation/js/menu-wc_es5.js b/documentation/frontend/js/menu-wc_es5.js similarity index 100% rename from frontend/documentation/js/menu-wc_es5.js rename to documentation/frontend/js/menu-wc_es5.js diff --git a/frontend/documentation/js/menu.js b/documentation/frontend/js/menu.js similarity index 100% rename from frontend/documentation/js/menu.js rename to documentation/frontend/js/menu.js diff --git a/frontend/documentation/js/routes.js b/documentation/frontend/js/routes.js similarity index 100% rename from frontend/documentation/js/routes.js rename to documentation/frontend/js/routes.js diff --git a/frontend/documentation/js/routes/routes_index.js b/documentation/frontend/js/routes/routes_index.js similarity index 100% rename from frontend/documentation/js/routes/routes_index.js rename to documentation/frontend/js/routes/routes_index.js diff --git a/frontend/documentation/js/search/lunr.min.js b/documentation/frontend/js/search/lunr.min.js similarity index 100% rename from frontend/documentation/js/search/lunr.min.js rename to documentation/frontend/js/search/lunr.min.js diff --git a/frontend/documentation/js/search/search-engine.js b/documentation/frontend/js/search/search-engine.js similarity index 100% rename from frontend/documentation/js/search/search-engine.js rename to documentation/frontend/js/search/search-engine.js diff --git a/frontend/documentation/js/search/search-lunr.js b/documentation/frontend/js/search/search-lunr.js similarity index 100% rename from frontend/documentation/js/search/search-lunr.js rename to documentation/frontend/js/search/search-lunr.js diff --git a/frontend/documentation/js/search/search.js b/documentation/frontend/js/search/search.js similarity index 100% rename from frontend/documentation/js/search/search.js rename to documentation/frontend/js/search/search.js diff --git a/frontend/documentation/js/search/search_index.js b/documentation/frontend/js/search/search_index.js similarity index 100% rename from frontend/documentation/js/search/search_index.js rename to documentation/frontend/js/search/search_index.js diff --git a/frontend/documentation/js/sourceCode.js b/documentation/frontend/js/sourceCode.js similarity index 100% rename from frontend/documentation/js/sourceCode.js rename to documentation/frontend/js/sourceCode.js diff --git a/frontend/documentation/js/svg-pan-zoom.controls.js b/documentation/frontend/js/svg-pan-zoom.controls.js similarity index 100% rename from frontend/documentation/js/svg-pan-zoom.controls.js rename to documentation/frontend/js/svg-pan-zoom.controls.js diff --git a/frontend/documentation/js/tabs.js b/documentation/frontend/js/tabs.js similarity index 100% rename from frontend/documentation/js/tabs.js rename to documentation/frontend/js/tabs.js diff --git a/frontend/documentation/js/tree.js b/documentation/frontend/js/tree.js similarity index 100% rename from frontend/documentation/js/tree.js rename to documentation/frontend/js/tree.js diff --git a/frontend/documentation/miscellaneous.html b/documentation/frontend/miscellaneous.html similarity index 100% rename from frontend/documentation/miscellaneous.html rename to documentation/frontend/miscellaneous.html diff --git a/frontend/documentation/miscellaneous/variables.html b/documentation/frontend/miscellaneous/variables.html similarity index 100% rename from frontend/documentation/miscellaneous/variables.html rename to documentation/frontend/miscellaneous/variables.html diff --git a/frontend/documentation/modules.html b/documentation/frontend/modules.html similarity index 100% rename from frontend/documentation/modules.html rename to documentation/frontend/modules.html diff --git a/frontend/documentation/modules/AppModule.html b/documentation/frontend/modules/AppModule.html similarity index 100% rename from frontend/documentation/modules/AppModule.html rename to documentation/frontend/modules/AppModule.html diff --git a/frontend/documentation/modules/AppModule/dependencies.svg b/documentation/frontend/modules/AppModule/dependencies.svg similarity index 100% rename from frontend/documentation/modules/AppModule/dependencies.svg rename to documentation/frontend/modules/AppModule/dependencies.svg diff --git a/frontend/documentation/modules/AppRoutingModule.html b/documentation/frontend/modules/AppRoutingModule.html similarity index 100% rename from frontend/documentation/modules/AppRoutingModule.html rename to documentation/frontend/modules/AppRoutingModule.html diff --git a/frontend/documentation/overview.html b/documentation/frontend/overview.html similarity index 100% rename from frontend/documentation/overview.html rename to documentation/frontend/overview.html diff --git a/frontend/documentation/properties.html b/documentation/frontend/properties.html similarity index 100% rename from frontend/documentation/properties.html rename to documentation/frontend/properties.html diff --git a/frontend/documentation/routes.html b/documentation/frontend/routes.html similarity index 100% rename from frontend/documentation/routes.html rename to documentation/frontend/routes.html diff --git a/frontend/documentation/styles/bootstrap-card.css b/documentation/frontend/styles/bootstrap-card.css similarity index 100% rename from frontend/documentation/styles/bootstrap-card.css rename to documentation/frontend/styles/bootstrap-card.css diff --git a/frontend/documentation/styles/bootstrap.min.css b/documentation/frontend/styles/bootstrap.min.css similarity index 100% rename from frontend/documentation/styles/bootstrap.min.css rename to documentation/frontend/styles/bootstrap.min.css diff --git a/frontend/documentation/styles/compodoc.css b/documentation/frontend/styles/compodoc.css similarity index 100% rename from frontend/documentation/styles/compodoc.css rename to documentation/frontend/styles/compodoc.css diff --git a/frontend/documentation/styles/dark.css b/documentation/frontend/styles/dark.css similarity index 100% rename from frontend/documentation/styles/dark.css rename to documentation/frontend/styles/dark.css diff --git a/frontend/documentation/styles/font-awesome.min.css b/documentation/frontend/styles/font-awesome.min.css similarity index 100% rename from frontend/documentation/styles/font-awesome.min.css rename to documentation/frontend/styles/font-awesome.min.css diff --git a/frontend/documentation/styles/ionicons.min.css b/documentation/frontend/styles/ionicons.min.css similarity index 100% rename from frontend/documentation/styles/ionicons.min.css rename to documentation/frontend/styles/ionicons.min.css diff --git a/frontend/documentation/styles/laravel.css b/documentation/frontend/styles/laravel.css similarity index 100% rename from frontend/documentation/styles/laravel.css rename to documentation/frontend/styles/laravel.css diff --git a/frontend/documentation/styles/material.css b/documentation/frontend/styles/material.css similarity index 100% rename from frontend/documentation/styles/material.css rename to documentation/frontend/styles/material.css diff --git a/frontend/documentation/styles/monokai-sublime.css b/documentation/frontend/styles/monokai-sublime.css similarity index 100% rename from frontend/documentation/styles/monokai-sublime.css rename to documentation/frontend/styles/monokai-sublime.css diff --git a/frontend/documentation/styles/original.css b/documentation/frontend/styles/original.css similarity index 100% rename from frontend/documentation/styles/original.css rename to documentation/frontend/styles/original.css diff --git a/frontend/documentation/styles/postmark.css b/documentation/frontend/styles/postmark.css similarity index 100% rename from frontend/documentation/styles/postmark.css rename to documentation/frontend/styles/postmark.css diff --git a/frontend/documentation/styles/prism.css b/documentation/frontend/styles/prism.css similarity index 100% rename from frontend/documentation/styles/prism.css rename to documentation/frontend/styles/prism.css diff --git a/frontend/documentation/styles/readthedocs.css b/documentation/frontend/styles/readthedocs.css similarity index 100% rename from frontend/documentation/styles/readthedocs.css rename to documentation/frontend/styles/readthedocs.css diff --git a/frontend/documentation/styles/reset.css b/documentation/frontend/styles/reset.css similarity index 100% rename from frontend/documentation/styles/reset.css rename to documentation/frontend/styles/reset.css diff --git a/frontend/documentation/styles/stripe.css b/documentation/frontend/styles/stripe.css similarity index 100% rename from frontend/documentation/styles/stripe.css rename to documentation/frontend/styles/stripe.css diff --git a/frontend/documentation/styles/style.css b/documentation/frontend/styles/style.css similarity index 100% rename from frontend/documentation/styles/style.css rename to documentation/frontend/styles/style.css diff --git a/frontend/documentation/styles/tablesort.css b/documentation/frontend/styles/tablesort.css similarity index 100% rename from frontend/documentation/styles/tablesort.css rename to documentation/frontend/styles/tablesort.css diff --git a/frontend/documentation/styles/vagrant.css b/documentation/frontend/styles/vagrant.css similarity index 100% rename from frontend/documentation/styles/vagrant.css rename to documentation/frontend/styles/vagrant.css -- 2.45.2 From 321b2130544e98e97842d6b4e67fadedf92f784b Mon Sep 17 00:00:00 2001 From: Linus E <75929322+Rripped@users.noreply.github.com> Date: Thu, 12 May 2022 13:30:56 +0200 Subject: [PATCH 6/7] added documentation --- .../api_handling/api_handler.html | 1571 ++++++++++++++ .../telegram_bot/api_handling/index.html | 65 + documentation/telegram_bot/bot.html | 1810 +++++++++++++++++ documentation/telegram_bot/bot_updates.html | 507 +++++ .../telegram_bot/helper_functions.html | 220 ++ documentation/telegram_bot/index.html | 85 + documentation/telegram_bot/news/index.html | 65 + .../telegram_bot/news/news_fetcher.html | 264 +++ documentation/telegram_bot/shares/index.html | 65 + .../telegram_bot/shares/share_fetcher.html | 384 ++++ 10 files changed, 5036 insertions(+) create mode 100644 documentation/telegram_bot/api_handling/api_handler.html create mode 100644 documentation/telegram_bot/api_handling/index.html create mode 100644 documentation/telegram_bot/bot.html create mode 100644 documentation/telegram_bot/bot_updates.html create mode 100644 documentation/telegram_bot/helper_functions.html create mode 100644 documentation/telegram_bot/index.html create mode 100644 documentation/telegram_bot/news/index.html create mode 100644 documentation/telegram_bot/news/news_fetcher.html create mode 100644 documentation/telegram_bot/shares/index.html create mode 100644 documentation/telegram_bot/shares/share_fetcher.html diff --git a/documentation/telegram_bot/api_handling/api_handler.html b/documentation/telegram_bot/api_handling/api_handler.html new file mode 100644 index 0000000..84fbb60 --- /dev/null +++ b/documentation/telegram_bot/api_handling/api_handler.html @@ -0,0 +1,1571 @@ + + + + + + +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 new file mode 100644 index 0000000..a1e025c --- /dev/null +++ b/documentation/telegram_bot/api_handling/index.html @@ -0,0 +1,65 @@ + + + + + + +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 new file mode 100644 index 0000000..2307945 --- /dev/null +++ b/documentation/telegram_bot/bot.html @@ -0,0 +1,1810 @@ + + + + + + +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 new file mode 100644 index 0000000..6a76576 --- /dev/null +++ b/documentation/telegram_bot/bot_updates.html @@ -0,0 +1,507 @@ + + + + + + +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 new file mode 100644 index 0000000..5f08a9a --- /dev/null +++ b/documentation/telegram_bot/helper_functions.html @@ -0,0 +1,220 @@ + + + + + + +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 new file mode 100644 index 0000000..ed6c2bb --- /dev/null +++ b/documentation/telegram_bot/index.html @@ -0,0 +1,85 @@ + + + + + + +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 new file mode 100644 index 0000000..76fbf5f --- /dev/null +++ b/documentation/telegram_bot/news/index.html @@ -0,0 +1,65 @@ + + + + + + +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 new file mode 100644 index 0000000..9211d75 --- /dev/null +++ b/documentation/telegram_bot/news/news_fetcher.html @@ -0,0 +1,264 @@ + + + + + + +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 new file mode 100644 index 0000000..7707868 --- /dev/null +++ b/documentation/telegram_bot/shares/index.html @@ -0,0 +1,65 @@ + + + + + + +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 new file mode 100644 index 0000000..bb0e33c --- /dev/null +++ b/documentation/telegram_bot/shares/share_fetcher.html @@ -0,0 +1,384 @@ + + + + + + +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 -- 2.45.2 From f81d99e1a9561b775b1edafb3e454bfb671676e3 Mon Sep 17 00:00:00 2001 From: Florian Kellermann Date: Thu, 12 May 2022 15:46:29 +0200 Subject: [PATCH 7/7] /update shall also be markdown proof --- telegram_bot/bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telegram_bot/bot.py b/telegram_bot/bot.py index 3ba5689..9fecd70 100644 --- a/telegram_bot/bot.py +++ b/telegram_bot/bot.py @@ -230,7 +230,8 @@ def update_for_user(message): 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' + 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") 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) -- 2.45.2