From 467bb9682d7082e15c08ea0696c10545b83ec73e Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 30 Sep 2024 21:28:33 +0200 Subject: [PATCH 01/15] [Community] fix non auth private data fetch error --- octobot/community/authentication.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/octobot/community/authentication.py b/octobot/community/authentication.py index f041a3ff3..ab0a6426c 100644 --- a/octobot/community/authentication.py +++ b/octobot/community/authentication.py @@ -594,6 +594,9 @@ async def fetch_bot_tentacles_data_based_config( async def fetch_private_data(self, reset=False): try: + if not self.is_logged_in(): + self.logger.info(f"Can't fetch private data: no authenticated user") + return mqtt_uuid = None try: mqtt_uuid = self.get_saved_mqtt_device_uuid() From b355288da79257ba6cc8583d0aaafbf1b63e7de5 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 2 Oct 2024 18:26:21 +0200 Subject: [PATCH 02/15] [Requirements] bump trading --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 82ce17488..faf22e345 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Drakkar-Software requirements OctoBot-Commons==1.9.58 -OctoBot-Trading==2.4.112 +OctoBot-Trading==2.4.113 OctoBot-Evaluators==1.9.7 OctoBot-Tentacles-Manager==2.9.16 OctoBot-Services==1.6.17 From 96d86c73e8da062fe6579b540f836dc5d4777dfb Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 2 Oct 2024 20:25:18 +0200 Subject: [PATCH 03/15] [Requirements] bump --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index faf22e345..2c7755149 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ # Drakkar-Software requirements OctoBot-Commons==1.9.58 -OctoBot-Trading==2.4.113 +OctoBot-Trading==2.4.114 OctoBot-Evaluators==1.9.7 OctoBot-Tentacles-Manager==2.9.16 OctoBot-Services==1.6.17 OctoBot-Backtesting==1.9.7 Async-Channel==2.2.1 -trading-backend==1.2.28 +trading-backend==1.2.29 ## Others colorlog==6.8.0 From 47c67e92d47025e2e15549a6522820b1114ee589 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 1 Oct 2024 12:12:34 +0200 Subject: [PATCH 04/15] [Community] use supabase func to fetch extension --- octobot/community/authentication.py | 48 ++++++------------- .../community_supabase_client.py | 25 ++++++++++ 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/octobot/community/authentication.py b/octobot/community/authentication.py index ab0a6426c..47432837c 100644 --- a/octobot/community/authentication.py +++ b/octobot/community/authentication.py @@ -631,55 +631,35 @@ async def fetch_private_data(self, reset=False): await self._refresh_products() async def _fetch_package_urls(self, mqtt_uuid: typing.Optional[str]) -> (list[str], str): - self.logger.debug(f"Fetching package") - resp = await self.supabase_client.http_get( - constants.COMMUNITY_EXTENSIONS_CHECK_ENDPOINT, - headers={ - "Content-Type": "application/json", - "X-Auth-Token": constants.COMMUNITY_EXTENSIONS_CHECK_ENDPOINT_KEY - }, - params={"mqtt_id": mqtt_uuid} if mqtt_uuid else {}, - timeout=constants.COMMUNITY_FETCH_TIMEOUT - ) - self.logger.debug("Fetched package") - resp.raise_for_status() - json_resp = json.loads(resp.json().get("message", {})) - if not json_resp: + self.logger.debug(f"Fetching extension package details") + extensions_details = await self.supabase_client.fetch_extensions(mqtt_uuid) + self.logger.debug("Fetched extension package details") + if not extensions_details: return None, None, None packages = [ package - for package in json_resp["paid_package_slugs"] + for package in extensions_details["paid_package_slugs"] if package ] urls = [ url - for url in json_resp["package_urls"] + for url in extensions_details["package_urls"] if url ] - mqtt_id = json_resp["mqtt_id"] + mqtt_id = extensions_details["mqtt_id"] return packages, urls, mqtt_id - async def fetch_checkout_url(self, payment_method, redirect_url): + async def fetch_checkout_url(self, payment_method: str, redirect_url: str): try: + if not self.is_logged_in(): + self.logger.info(f"Can't fetch checkout url: no authenticated user") + return None self.logger.debug(f"Fetching {payment_method} checkout url") - resp = await self.supabase_client.http_post( - constants.COMMUNITY_EXTENSIONS_CHECK_ENDPOINT, - json={ - "payment_method": payment_method, - "success_url": redirect_url, - }, - headers={ - "Content-Type": "application/json", - "X-Auth-Token": constants.COMMUNITY_EXTENSIONS_CHECK_ENDPOINT_KEY - }, - timeout=constants.COMMUNITY_FETCH_TIMEOUT - ) - resp.raise_for_status() - json_resp = json.loads(resp.json().get("message", {})) - if not json_resp: + url_details = await self.supabase_client.fetch_checkout_url(payment_method, redirect_url) + if not url_details: # valid error code but no content: user already has this product return None - url = json_resp["checkout_url"] + url = url_details["checkout_url"] self.logger.info( f"Here is your {constants.OCTOBOT_EXTENSION_PACKAGE_1_NAME} checkout url {url} " f"paste it into a web browser to proceed to payment if your browser did to automatically " diff --git a/octobot/community/supabase_backend/community_supabase_client.py b/octobot/community/supabase_backend/community_supabase_client.py index f1aac67aa..ab977f6e7 100644 --- a/octobot/community/supabase_backend/community_supabase_client.py +++ b/octobot/community/supabase_backend/community_supabase_client.py @@ -240,6 +240,31 @@ async def get_otp_with_auth_key(self, user_email: str, auth_key: str) -> str: except Exception: raise authentication.AuthenticationError(f"Invalid auth key authentication details") + async def fetch_extensions(self, mqtt_uuid: typing.Optional[str]) -> dict: + resp = await self.functions.invoke( + "os-paid-package-api", + { + "body": { + "action": "get_extension_details", + "mqtt_id": mqtt_uuid + }, + } + ) + return json.loads(json.loads(resp)["message"]) + + async def fetch_checkout_url(self, payment_method: str, redirect_url: str) -> dict: + resp = await self.functions.invoke( + "os-paid-package-api", + { + "body": { + "action": "get_checkout_url", + "payment_method": payment_method, + "success_url": redirect_url, + }, + } + ) + return json.loads(json.loads(resp)["message"]) + async def fetch_bot(self, bot_id) -> dict: try: # https://postgrest.org/en/stable/references/api/resource_embedding.html#hint-disambiguation From 8af1a51299ef8101431ca68bec4d2646b9daff6a Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 1 Oct 2024 12:15:44 +0200 Subject: [PATCH 05/15] [Community] remove authenticated http calls --- .../community_supabase_client.py | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/octobot/community/supabase_backend/community_supabase_client.py b/octobot/community/supabase_backend/community_supabase_client.py index ab977f6e7..53b53081f 100644 --- a/octobot/community/supabase_backend/community_supabase_client.py +++ b/octobot/community/supabase_backend/community_supabase_client.py @@ -14,12 +14,10 @@ # You should have received a copy of the GNU General Public # License along with OctoBot. If not, see . import asyncio -import base64 import datetime import time import typing import logging -import httpx import uuid import json import contextlib @@ -66,41 +64,6 @@ def error_describer(): raise errors.SessionTokenExpiredError(err) from err raise - -def _httpx_retrier(f): - async def httpx_retrier_wrapper(*args, **kwargs): - resp = None - for i in range(0, HTTP_RETRY_COUNT): - error = None - try: - resp: httpx.Response = await f(*args, **kwargs) - if resp.status_code in (502, 503, 520): - # waking up or SLA issue, retry - error = f"{resp.status_code} error {resp.reason_phrase}" - commons_logging.get_logger(__name__).debug( - f"{f.__name__}(args={args[1:]}) failed with {error} after {i+1} attempts, retrying." - ) - else: - if i > 0: - commons_logging.get_logger(__name__).debug( - f"{f.__name__}(args={args[1:]}) succeeded after {i+1} attempts" - ) - return resp - except httpx.ReadTimeout as err: - error = f"{err} ({err.__class__.__name__})" - # retry - commons_logging.get_logger(__name__).debug( - f"Error on {f.__name__}(args={args[1:]}) " - f"request, retrying now. Attempt {i+1} / {HTTP_RETRY_COUNT} ({error})." - ) - # no more attempts - if resp: - resp.raise_for_status() - return resp - else: - raise errors.RequestError(f"Failed to execute {f.__name__}(args={args[1:]} kwargs={kwargs})") - return httpx_retrier_wrapper - class CommunitySupabaseClient(supabase_client.AuthenticatedAsyncSupabaseClient): """ Octobot Community layer added to supabase_client.AuthenticatedSupabaseClient @@ -829,28 +792,6 @@ def _get_auth_key(self): return session.access_token return self.supabase_key - @_httpx_retrier - async def http_get(self, url: str, *args, params=None, headers=None, **kwargs) -> httpx.Response: - """ - Perform http get using the current supabase auth token - """ - params = params or {} - params["access_token"] = params.get("access_token", base64.b64encode(self._get_auth_key().encode()).decode()) - return await self.postgrest.session.get(url, *args, params=params, headers=headers, **kwargs) - - @_httpx_retrier - async def http_post( - self, url: str, *args, json=None, params=None, headers=None, **kwargs - ) -> httpx.Response: - """ - Perform http get using the current supabase auth token - """ - json_body = json or {} - json_body["access_token"] = json_body.get("access_token", self._get_auth_key()) - return await self.postgrest.session.post( - url, *args, json=json_body, params=params, headers=headers, **kwargs - ) - @staticmethod def get_formatted_time(timestamp: float) -> str: return datetime.datetime.utcfromtimestamp(timestamp).isoformat('T') From 60e305b954ce6eb3a3b73330dc0a457800eb2fc6 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Thu, 3 Oct 2024 18:32:31 +0200 Subject: [PATCH 06/15] [Community] init TV email config --- octobot/community/authentication.py | 48 +++++++++++++--- octobot/community/feeds/abstract_feed.py | 4 +- .../community/feeds/community_mqtt_feed.py | 55 ++++++++++++++++++- .../feeds/community_supabase_feed.py | 4 +- octobot/community/feeds/community_ws_feed.py | 5 +- .../models/community_user_account.py | 2 + octobot/constants.py | 1 + octobot/enums.py | 4 ++ 8 files changed, 107 insertions(+), 16 deletions(-) diff --git a/octobot/community/authentication.py b/octobot/community/authentication.py index 47432837c..cd0c5554e 100644 --- a/octobot/community/authentication.py +++ b/octobot/community/authentication.py @@ -20,6 +20,7 @@ import typing import octobot.constants as constants +import octobot.enums as enums import octobot.community.errors as errors import octobot.community.identifiers_provider as identifiers_provider import octobot.community.models.community_supports as community_supports @@ -180,6 +181,11 @@ def get_user_id(self): raise authentication.AuthenticationRequired() return self.user_account.get_user_id() + def get_last_email_address_confirm_code_email_content(self) -> typing.Optional[str]: + if not self.user_account.has_user_data(): + raise authentication.AuthenticationRequired() + return self.user_account.last_email_address_confirm_code_email_content + async def get_deployment_url(self): deployment_url_data = await self.supabase_client.fetch_deployment_url( self.user_account.get_selected_bot_deployment_id() @@ -291,12 +297,15 @@ async def _create_community_feed_if_necessary(self) -> bool: return True return False - async def _ensure_init_community_feed(self): + async def _ensure_init_community_feed( + self, + stop_on_cfg_action: typing.Optional[enums.CommunityConfigurationActions]=None + ): await self._create_community_feed_if_necessary() if not self._community_feed.is_connected() and self._community_feed.can_connect(): if self.initialized_event is not None and not self.initialized_event.is_set(): await asyncio.wait_for(self.initialized_event.wait(), self.LOGIN_TIMEOUT) - await self._community_feed.start() + await self._community_feed.start(stop_on_cfg_action) async def register_feed_callback(self, channel_type: commons_enums.CommunityChannelTypes, callback, identifier=None): try: @@ -305,6 +314,14 @@ async def register_feed_callback(self, channel_type: commons_enums.CommunityChan except errors.BotError as e: self.logger.error(f"Impossible to connect to community signals: {e}") + async def trigger_wait_for_email_address_confirm_code_email(self): + if not self.get_owned_packages(): + raise errors.ExtensionRequiredError( + f"The {constants.OCTOBOT_EXTENSION_PACKAGE_1_NAME} is required to use TradingView email alerts" + ) + await self._ensure_init_community_feed(enums.CommunityConfigurationActions.EMAIL_CONFIRM_CODE) + + async def send(self, message, channel_type, identifier=None): """ Sends a message @@ -506,6 +523,8 @@ def remove_login_detail(self): self.user_account.flush() self._reset_login_token() self._save_bot_id("") + self.save_tradingview_email("") + self.save_mqtt_device_uuid("") self.logger.debug("Removed community login data") async def stop(self): @@ -606,7 +625,9 @@ async def fetch_private_data(self, reset=False): self.logger.info("Community extension check is disabled") elif reset or (not self.user_account.community_package_urls or not mqtt_uuid): self.successfully_fetched_tentacles_package_urls = False - packages, package_urls, fetched_mqtt_uuid = await self._fetch_package_urls(mqtt_uuid) + packages, package_urls, fetched_mqtt_uuid, tradingview_email = ( + await self._fetch_extensions_details(mqtt_uuid) + ) self.successfully_fetched_tentacles_package_urls = True self.user_account.owned_packages = packages self.save_installed_package_urls(package_urls) @@ -620,6 +641,8 @@ async def fetch_private_data(self, reset=False): self.user_account.has_pending_packages_to_install = True if fetched_mqtt_uuid and fetched_mqtt_uuid != mqtt_uuid: self.save_mqtt_device_uuid(fetched_mqtt_uuid) + if tradingview_email and tradingview_email != self.get_saved_tradingview_email(): + self.save_tradingview_email(tradingview_email) except Exception as err: self.logger.exception(err, True, f"Unexpected error when fetching package urls: {err}") finally: @@ -630,12 +653,12 @@ async def fetch_private_data(self, reset=False): # fetch indexes as well await self._refresh_products() - async def _fetch_package_urls(self, mqtt_uuid: typing.Optional[str]) -> (list[str], str): + async def _fetch_extensions_details(self, mqtt_uuid: typing.Optional[str]) -> (list[str], list[str], str, str): self.logger.debug(f"Fetching extension package details") extensions_details = await self.supabase_client.fetch_extensions(mqtt_uuid) self.logger.debug("Fetched extension package details") if not extensions_details: - return None, None, None + return None, None, None, None packages = [ package for package in extensions_details["paid_package_slugs"] @@ -647,7 +670,8 @@ async def _fetch_package_urls(self, mqtt_uuid: typing.Optional[str]) -> (list[st if url ] mqtt_id = extensions_details["mqtt_id"] - return packages, urls, mqtt_id + tradingview_email = extensions_details["tradingview_email"] + return packages, urls, mqtt_id, tradingview_email async def fetch_checkout_url(self, payment_method: str, redirect_url: str): try: @@ -680,21 +704,27 @@ def _reset_login_token(self): def save_installed_package_urls(self, package_urls: list[str]): self._save_value_in_config(constants.CONFIG_COMMUNITY_PACKAGE_URLS, package_urls) - def save_mqtt_device_uuid(self, mqtt_uuid): + def save_tradingview_email(self, tradingview_email: str): + self._save_value_in_config(constants.CONFIG_COMMUNITY_TRADINGVIEW_EMAIL, tradingview_email) + + def save_mqtt_device_uuid(self, mqtt_uuid: str): self._save_value_in_config(constants.CONFIG_COMMUNITY_MQTT_UUID, mqtt_uuid) def get_saved_package_urls(self) -> list[str]: return self._get_value_in_config(constants.CONFIG_COMMUNITY_PACKAGE_URLS) or [] - def get_saved_mqtt_device_uuid(self): + def get_saved_mqtt_device_uuid(self) -> str: if mqtt_uuid := self._get_value_in_config(constants.CONFIG_COMMUNITY_MQTT_UUID): return mqtt_uuid raise errors.NoBotDeviceError("No MQTT device ID has been set") + def get_saved_tradingview_email(self) -> str: + return self._get_value_in_config(constants.CONFIG_COMMUNITY_TRADINGVIEW_EMAIL) + def _save_bot_id(self, bot_id): self._save_value_in_config(constants.CONFIG_COMMUNITY_BOT_ID, bot_id) - def _get_saved_bot_id(self): + def _get_saved_bot_id(self) -> str: return constants.COMMUNITY_BOT_ID or self._get_value_in_config(constants.CONFIG_COMMUNITY_BOT_ID) def _save_value_in_config(self, key, value): diff --git a/octobot/community/feeds/abstract_feed.py b/octobot/community/feeds/abstract_feed.py index 338d270be..e5f3c3351 100644 --- a/octobot/community/feeds/abstract_feed.py +++ b/octobot/community/feeds/abstract_feed.py @@ -14,7 +14,9 @@ # You should have received a copy of the GNU General Public # License along with OctoBot. If not, see . import time +import typing +import octobot.enums as enums import octobot_commons.logging as bot_logging @@ -35,7 +37,7 @@ def __init__(self, feed_url, authenticator): def has_registered_feed(self) -> bool: return bool(self.feed_callbacks) - async def start(self): + async def start(self, stop_on_cfg_action: typing.Optional[enums.CommunityConfigurationActions]): raise NotImplementedError("start is not implemented") async def stop(self): diff --git a/octobot/community/feeds/community_mqtt_feed.py b/octobot/community/feeds/community_mqtt_feed.py index 38aeba242..44cc927ae 100644 --- a/octobot/community/feeds/community_mqtt_feed.py +++ b/octobot/community/feeds/community_mqtt_feed.py @@ -26,6 +26,7 @@ import octobot.community.errors as errors import octobot.community.feeds.abstract_feed as abstract_feed import octobot.constants as constants +import octobot.enums as enums def _disable_gmqtt_info_loggers(): @@ -69,12 +70,19 @@ def __init__(self, feed_url, authenticator): self._connected_at_least_once = False self._processed_messages = [] - async def start(self): + self._default_callbacks_by_subscription_topic = self._build_default_callbacks_by_subscription_topic() + self._stop_on_cfg_action: typing.Optional[enums.CommunityConfigurationActions] = None + + async def start(self, stop_on_cfg_action: typing.Optional[enums.CommunityConfigurationActions]): + if self.is_connected(): + self.logger.info("Already connected") + return self.should_stop = False try: await self._connect() if self.is_connected(): self.logger.info("Successful connection request to mqtt device") + self._stop_on_cfg_action = stop_on_cfg_action else: self.logger.info("Failed to connect to mqtt device") except asyncio.TimeoutError as err: @@ -103,6 +111,7 @@ async def restart(self): def _reset(self): self._connected_at_least_once = False + self._stop_on_cfg_action = None self._subscription_attempts = 0 self._connect_task = None self._valid_auth = True @@ -112,6 +121,41 @@ async def _stop_mqtt_client(self): if self.is_connected(): await self._mqtt_client.disconnect() + def _get_default_subscription_topics(self) -> set: + """ + topics that are always to be subscribed + """ + return set(self._default_callbacks_by_subscription_topic) + + def _build_default_callbacks_by_subscription_topic(self) -> dict: + return { + self._build_topic( + commons_enums.CommunityChannelTypes.CONFIGURATION, + self.authenticator.get_saved_mqtt_device_uuid() + ): [self._config_feed_callback, ] + } + + async def _config_feed_callback(self, data: dict): + """ + format: + { + "u": "ABCCD "v": "1.0.0", +-D11 ...", + "s": '{"action": "email_confirm_code", "code_email": "hello 123-1232"}', + } + """ + parsed_message = data[commons_enums.CommunityFeedAttrs.VALUE.value] + action = parsed_message["action"] + if action == enums.CommunityConfigurationActions.EMAIL_CONFIRM_CODE.value: + email_body = parsed_message["code_email"] + self.logger.info(f"Received email address confirm code:\n{email_body}") + self.authenticator.user_account.last_email_address_confirm_code_email_content = email_body + else: + self.logger.error(f"Unknown cfg message action: {action=}") + if action and self._stop_on_cfg_action and self._stop_on_cfg_action.value == action: + self.logger.info(f"Stopping after expected {action} configuration action.") + await self.stop() + def is_connected(self): return self._mqtt_client is not None and self._mqtt_client.is_connected and not self._disconnected @@ -182,9 +226,12 @@ async def send(self, message, channel_type, identifier, **kwargs): raise NotImplementedError("Sending is not implemented") def _get_callbacks(self, topic): - for callback in self.feed_callbacks.get(topic, ()): + for callback in self._get_feed_callbacks(topic): yield callback + def _get_feed_callbacks(self, topic) -> list: + return self._default_callbacks_by_subscription_topic.get(topic, []) + self.feed_callbacks.get(topic, []) + def _get_channel_type(self, message): return commons_enums.CommunityChannelTypes(message[commons_enums.CommunityFeedAttrs.CHANNEL_TYPE.value]) @@ -206,7 +253,7 @@ def _on_connect(self, client, flags, rc, properties): # There are no subscription when we just connected self.subscribed = False # Auto subscribe to known topics (mainly used in case of reconnection) - self._subscribe(self._subscription_topics) + self._subscribe(self._subscription_topics.union(self._get_default_subscription_topics())) def _try_reconnect_if_necessary(self, client): if self._reconnect_task is None or self._reconnect_task.done(): @@ -312,6 +359,8 @@ def _get_username(client: gmqtt.Client) -> str: async def _connect(self): device_uuid = self.authenticator.get_saved_mqtt_device_uuid() + # ensure _default_callbacks_by_subscription_topic is up to date + self._default_callbacks_by_subscription_topic = self._build_default_callbacks_by_subscription_topic() if device_uuid is None: self._valid_auth = False raise errors.BotError("mqtt device uuid is None, impossible to connect client") diff --git a/octobot/community/feeds/community_supabase_feed.py b/octobot/community/feeds/community_supabase_feed.py index aaeda887c..f30969006 100644 --- a/octobot/community/feeds/community_supabase_feed.py +++ b/octobot/community/feeds/community_supabase_feed.py @@ -16,6 +16,7 @@ import uuid import json import realtime +import typing import packaging.version as packaging_version import octobot_commons.enums as commons_enums @@ -24,6 +25,7 @@ import octobot.community.supabase_backend.enums as enums import octobot.community.feeds.abstract_feed as abstract_feed import octobot.constants as constants +import octobot.enums as enums class CommunitySupabaseFeed(abstract_feed.AbstractFeed): @@ -76,7 +78,7 @@ async def _process_message(self, table: str, message: dict): except Exception as err: self.logger.exception(err, True, f"Unexpected error when processing message: {err}") - async def start(self): + async def start(self, stop_on_cfg_action: typing.Optional[enums.CommunityConfigurationActions]): # handled in supabase client directly, just ensure no subscriptions are pending for table in self.feed_callbacks: await self._subscribe_to_table_if_necessary(table) diff --git a/octobot/community/feeds/community_ws_feed.py b/octobot/community/feeds/community_ws_feed.py index f33db7b0a..1598ffc72 100644 --- a/octobot/community/feeds/community_ws_feed.py +++ b/octobot/community/feeds/community_ws_feed.py @@ -15,7 +15,7 @@ # License along with OctoBot. If not, see . import random import time - +import typing import websockets import asyncio import enum @@ -26,6 +26,7 @@ import octobot_commons.enums as commons_enums import octobot_commons.authentication as authentication import octobot.constants as constants +import octobot.enums as enums import octobot.community.feeds.abstract_feed as abstract_feed import octobot.community.identifiers_provider as identifiers_provider @@ -55,7 +56,7 @@ def __init__(self, feed_url, authenticator): self._reconnect_attempts = 0 self._last_ping_time = None - async def start(self): + async def start(self, stop_on_cfg_action: typing.Optional[enums.CommunityConfigurationActions]): await self._ensure_connection() if self.consumer_task is None or self.consumer_task.done(): self.consumer_task = asyncio.create_task(self.start_consumer()) diff --git a/octobot/community/models/community_user_account.py b/octobot/community/models/community_user_account.py index 9f3421036..95581a318 100644 --- a/octobot/community/models/community_user_account.py +++ b/octobot/community/models/community_user_account.py @@ -36,6 +36,8 @@ def __init__(self): self.owned_packages: list[str] = [] self.has_pending_packages_to_install = False + self.last_email_address_confirm_code_email_content: typing.Optional[str] = None + self._profile_raw_data = None self._selected_bot_raw_data = None self._all_user_bots_raw_data = [] diff --git a/octobot/constants.py b/octobot/constants.py index bc0931cb8..896e811c7 100644 --- a/octobot/constants.py +++ b/octobot/constants.py @@ -103,6 +103,7 @@ CONFIG_COMMUNITY = "community" CONFIG_COMMUNITY_BOT_ID = "bot_id" CONFIG_COMMUNITY_MQTT_UUID = "mqtt_uuid" +CONFIG_COMMUNITY_TRADINGVIEW_EMAIL = "tradingview_email" CONFIG_COMMUNITY_PACKAGE_URLS = "package_urls" CONFIG_COMMUNITY_ENVIRONMENT = "environment" USE_BETA_EARLY_ACCESS = os_util.parse_boolean_environment_var("USE_BETA_EARLY_ACCESS", "false") diff --git a/octobot/enums.py b/octobot/enums.py index bcbef1b4c..cbc70881d 100644 --- a/octobot/enums.py +++ b/octobot/enums.py @@ -27,6 +27,10 @@ class CommunityEnvironments(enum.Enum): Production = "Production" +class CommunityConfigurationActions(enum.Enum): + EMAIL_CONFIRM_CODE = "email_confirm_code" + + class OptimizerModes(enum.Enum): NORMAL = "normal" GENETIC = "genetic" From 6841e3efdb2a24a8a892c6975556a9da4f89c0ce Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Thu, 3 Oct 2024 18:36:05 +0200 Subject: [PATCH 07/15] [Requirements] bump --- octobot/community/feeds/community_mqtt_feed.py | 2 +- requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/octobot/community/feeds/community_mqtt_feed.py b/octobot/community/feeds/community_mqtt_feed.py index 44cc927ae..87ff63fbf 100644 --- a/octobot/community/feeds/community_mqtt_feed.py +++ b/octobot/community/feeds/community_mqtt_feed.py @@ -141,7 +141,7 @@ async def _config_feed_callback(self, data: dict): { "u": "ABCCD "v": "1.0.0", -D11 ...", - "s": '{"action": "email_confirm_code", "code_email": "hello 123-1232"}', + "s": {"action": "email_confirm_code", "code_email": "hello 123-1232"}, } """ parsed_message = data[commons_enums.CommunityFeedAttrs.VALUE.value] diff --git a/requirements.txt b/requirements.txt index 2c7755149..e9fd751ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ # Drakkar-Software requirements -OctoBot-Commons==1.9.58 +OctoBot-Commons==1.9.59 OctoBot-Trading==2.4.114 OctoBot-Evaluators==1.9.7 OctoBot-Tentacles-Manager==2.9.16 -OctoBot-Services==1.6.17 +OctoBot-Services==1.6.18 OctoBot-Backtesting==1.9.7 Async-Channel==2.2.1 trading-backend==1.2.29 From 6f7bb8dffe5d4b13bf155f49ceca568681901ad7 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Fri, 4 Oct 2024 10:27:42 +0200 Subject: [PATCH 08/15] [Community] update tests --- .../community/feeds/community_mqtt_feed.py | 23 ++++++++----------- .../feeds/community_supabase_feed.py | 4 ++-- .../community/test_community_mqtt_feed.py | 4 ++-- .../community/test_community_supabase_feed.py | 8 +++---- .../community/test_community_ws_feed.py | 4 ++-- 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/octobot/community/feeds/community_mqtt_feed.py b/octobot/community/feeds/community_mqtt_feed.py index 87ff63fbf..950ad208f 100644 --- a/octobot/community/feeds/community_mqtt_feed.py +++ b/octobot/community/feeds/community_mqtt_feed.py @@ -101,14 +101,6 @@ async def stop(self): self._reset() self.logger.debug("Stopped") - async def restart(self): - try: - if not self.should_stop: - await self.stop() - await self.start() - except Exception as err: - self.logger.exception(err, True, f"{err}") - def _reset(self): self._connected_at_least_once = False self._stop_on_cfg_action = None @@ -128,12 +120,15 @@ def _get_default_subscription_topics(self) -> set: return set(self._default_callbacks_by_subscription_topic) def _build_default_callbacks_by_subscription_topic(self) -> dict: - return { - self._build_topic( - commons_enums.CommunityChannelTypes.CONFIGURATION, - self.authenticator.get_saved_mqtt_device_uuid() - ): [self._config_feed_callback, ] - } + try: + return { + self._build_topic( + commons_enums.CommunityChannelTypes.CONFIGURATION, + self.authenticator.get_saved_mqtt_device_uuid() + ): [self._config_feed_callback, ] + } + except errors.NoBotDeviceError: + return {} async def _config_feed_callback(self, data: dict): """ diff --git a/octobot/community/feeds/community_supabase_feed.py b/octobot/community/feeds/community_supabase_feed.py index f30969006..dce285c13 100644 --- a/octobot/community/feeds/community_supabase_feed.py +++ b/octobot/community/feeds/community_supabase_feed.py @@ -25,7 +25,7 @@ import octobot.community.supabase_backend.enums as enums import octobot.community.feeds.abstract_feed as abstract_feed import octobot.constants as constants -import octobot.enums as enums +import octobot.enums class CommunitySupabaseFeed(abstract_feed.AbstractFeed): @@ -78,7 +78,7 @@ async def _process_message(self, table: str, message: dict): except Exception as err: self.logger.exception(err, True, f"Unexpected error when processing message: {err}") - async def start(self, stop_on_cfg_action: typing.Optional[enums.CommunityConfigurationActions]): + async def start(self, stop_on_cfg_action: typing.Optional[octobot.enums.CommunityConfigurationActions]): # handled in supabase client directly, just ensure no subscriptions are pending for table in self.feed_callbacks: await self._subscribe_to_table_if_necessary(table) diff --git a/tests/unit_tests/community/test_community_mqtt_feed.py b/tests/unit_tests/community/test_community_mqtt_feed.py index 9dbeff309..52e3be0c5 100644 --- a/tests/unit_tests/community/test_community_mqtt_feed.py +++ b/tests/unit_tests/community/test_community_mqtt_feed.py @@ -62,8 +62,8 @@ async def connected_community_feed(authenticator): mock.patch.object(gmqtt.Client, "connect", mock.AsyncMock()) as _connect_mock: await feed.register_feed_callback(commons_enums.CommunityChannelTypes.SIGNAL, mock.AsyncMock()) _subscribe_mock.assert_called_once_with((f"{commons_enums.CommunityChannelTypes.SIGNAL.value}/None", )) - await feed.start() - get_selected_bot_device_uuid_mock.assert_called_once() + await feed.start(None) + assert get_selected_bot_device_uuid_mock.call_count == 2 _connect_mock.assert_called_once_with(FEED_URL, feed.mqtt_broker_port, version=feed.MQTT_VERSION) yield feed finally: diff --git a/tests/unit_tests/community/test_community_supabase_feed.py b/tests/unit_tests/community/test_community_supabase_feed.py index 82016393f..2ff742019 100644 --- a/tests/unit_tests/community/test_community_supabase_feed.py +++ b/tests/unit_tests/community/test_community_supabase_feed.py @@ -88,13 +88,13 @@ async def test_start_and_connect(authenticated_feed): # without feed_callbacks with mock.patch.object(authenticated_feed.authenticator.supabase_client, "get_subscribed_channel_tables", mock.Mock(return_value=[])) as get_subscribed_channel_tables_mock: - await authenticated_feed.start() + await authenticated_feed.start(None) is_logged_in_mock.assert_not_called() _subscribe_to_table_mock.assert_not_called() get_subscribed_channel_tables_mock.assert_not_called() # with feed_callbacks authenticated_feed.feed_callbacks = {"signals": None, "plopplop":None} - await authenticated_feed.start() + await authenticated_feed.start(None) assert is_logged_in_mock.call_count == 2 # no sub channel: call _subscribe_to_table assert _subscribe_to_table_mock.call_count == 2 @@ -106,7 +106,7 @@ async def test_start_and_connect(authenticated_feed): with mock.patch.object(authenticated_feed.authenticator.supabase_client, "get_subscribed_channel_tables", mock.Mock(return_value=["signals"])) as get_subscribed_channel_tables_mock: # sub channel on signals: call _subscribe_to_table just for plopplop - await authenticated_feed.start() + await authenticated_feed.start(None) is_logged_in_mock.assert_called_once() # no sub channel: call _subscribe_to_table _subscribe_to_table_mock.assert_called_with("plopplop") @@ -120,7 +120,7 @@ async def test_start_and_connect(authenticated_feed): with mock.patch.object(authenticated_feed.authenticator.supabase_client, "get_subscribed_channel_tables", mock.Mock(return_value=["plop"])), \ mock.patch.object(authenticated_feed.authenticator, "is_logged_in", mock.Mock(return_value=False)): - await authenticated_feed.start() + await authenticated_feed.start(None) async def test_connection(authenticated_feed): with mock.patch.object(authenticated_feed.authenticator, "is_logged_in", mock.Mock(return_value=True)) \ diff --git a/tests/unit_tests/community/test_community_ws_feed.py b/tests/unit_tests/community/test_community_ws_feed.py index ac296238d..0894d3c34 100644 --- a/tests/unit_tests/community/test_community_ws_feed.py +++ b/tests/unit_tests/community/test_community_ws_feed.py @@ -127,7 +127,7 @@ async def connected_community_feed(authenticator): as _fetch_stream_identifier_mock: await feed.register_feed_callback(commons_enums.CommunityChannelTypes.SIGNAL, mock.AsyncMock()) _fetch_stream_identifier_mock.assert_called_once_with(None) - await feed.start() + await feed.start(None) yield feed finally: if feed is not None: @@ -217,7 +217,7 @@ async def test_reconnect(authenticator): client = community.CommunityWSFeed(f"ws://{HOST}:{PORT}", authenticator) client.RECONNECT_DELAY = 0 await client.register_feed_callback(commons_enums.CommunityChannelTypes.SIGNAL, client_handler) - await client.start() + await client.start(None) # 1. ensure client is both receiving and sending messages client_handler.assert_not_called() From de2c9b625d7b3a03a10ccdea62afcd712b0ade05 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Fri, 4 Oct 2024 23:54:09 +0200 Subject: [PATCH 09/15] [Requirements] bump --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e9fd751ac..6ef7de2f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ OctoBot-Commons==1.9.59 OctoBot-Trading==2.4.114 OctoBot-Evaluators==1.9.7 OctoBot-Tentacles-Manager==2.9.16 -OctoBot-Services==1.6.18 +OctoBot-Services==1.6.19 OctoBot-Backtesting==1.9.7 Async-Channel==2.2.1 trading-backend==1.2.29 From 113d3f16f2d4901c233cc07ebfd00a4a210dbdd3 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 7 Oct 2024 13:47:21 +0200 Subject: [PATCH 10/15] [Community] add clear_bot_scoped_config --- octobot/community/authentication.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/octobot/community/authentication.py b/octobot/community/authentication.py index cd0c5554e..36a3868c7 100644 --- a/octobot/community/authentication.py +++ b/octobot/community/authentication.py @@ -522,11 +522,24 @@ async def has_login_info(self): def remove_login_detail(self): self.user_account.flush() self._reset_login_token() + # force user to (re)select a bot self._save_bot_id("") + # mqtt feed can't connect as long as the user is not authenticated: don't display unusable email address self.save_tradingview_email("") - self.save_mqtt_device_uuid("") self.logger.debug("Removed community login data") + def clear_bot_scoped_config(self): + """ + Clears all bot local data including mqtt id, which will trigger a new mqtt device creation. + Warning: should only be called in rare cases, mostly to avoid multi connection on the same mqtt + device + """ + self._save_bot_id("") + self.save_tradingview_email("") + # also reset mqtt id to force a new mqtt id creation + self._save_mqtt_device_uuid("") + + async def stop(self): self.logger.debug("Stopping ...") if self._fetch_account_task is not None and not self._fetch_account_task.done(): @@ -640,7 +653,7 @@ async def fetch_private_data(self, reset=False): self.logger.info(f"New tentacles are available for installation") self.user_account.has_pending_packages_to_install = True if fetched_mqtt_uuid and fetched_mqtt_uuid != mqtt_uuid: - self.save_mqtt_device_uuid(fetched_mqtt_uuid) + self._save_mqtt_device_uuid(fetched_mqtt_uuid) if tradingview_email and tradingview_email != self.get_saved_tradingview_email(): self.save_tradingview_email(tradingview_email) except Exception as err: @@ -707,7 +720,7 @@ def save_installed_package_urls(self, package_urls: list[str]): def save_tradingview_email(self, tradingview_email: str): self._save_value_in_config(constants.CONFIG_COMMUNITY_TRADINGVIEW_EMAIL, tradingview_email) - def save_mqtt_device_uuid(self, mqtt_uuid: str): + def _save_mqtt_device_uuid(self, mqtt_uuid: str): self._save_value_in_config(constants.CONFIG_COMMUNITY_MQTT_UUID, mqtt_uuid) def get_saved_package_urls(self) -> list[str]: From 383a7a4eda1d8ea39ef31bbcd64e57040f85ac83 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 7 Oct 2024 14:34:54 +0200 Subject: [PATCH 11/15] [Community] add clear_local_data_if_necessary --- octobot/cli.py | 1 + octobot/community/authentication.py | 32 ++++++++++++++++++++++++++++- octobot/constants.py | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/octobot/cli.py b/octobot/cli.py index 8bf7f162f..48f9fdad5 100644 --- a/octobot/cli.py +++ b/octobot/cli.py @@ -194,6 +194,7 @@ async def _get_authenticated_community_if_possible(config, logger): # switch environments if necessary octobot_community.IdentifiersProvider.use_environment_from_config(config) community_auth = octobot_community.CommunityAuthentication.create(config) + community_auth.clear_local_data_if_necessary() try: if not community_auth.is_initialized(): if constants.IS_CLOUD_ENV: diff --git a/octobot/community/authentication.py b/octobot/community/authentication.py index 36a3868c7..416bab4d4 100644 --- a/octobot/community/authentication.py +++ b/octobot/community/authentication.py @@ -18,6 +18,8 @@ import json import time import typing +import hashlib +import os import octobot.constants as constants import octobot.enums as enums @@ -528,17 +530,39 @@ def remove_login_detail(self): self.save_tradingview_email("") self.logger.debug("Removed community login data") - def clear_bot_scoped_config(self): + def _clear_bot_scoped_config(self): """ Clears all bot local data including mqtt id, which will trigger a new mqtt device creation. Warning: should only be called in rare cases, mostly to avoid multi connection on the same mqtt device """ + self.logger.info( + "Clearing bot local scoped config data. Your TradingView alert email address " + "and webhook url will be different on this bot." + ) self._save_bot_id("") self.save_tradingview_email("") # also reset mqtt id to force a new mqtt id creation self._save_mqtt_device_uuid("") + def clear_local_data_if_necessary(self): + if constants.IS_CLOUD_ENV: + # disabled on cloud environments + return + previous_local_identifier = self._get_saved_bot_scoped_data_identifier() + current_local_identifier = self._get_bot_scoped_data_identifier() + if not previous_local_identifier: + self._save_bot_scoped_data_identifier(current_local_identifier) + # nothing to clear + return + if current_local_identifier != previous_local_identifier: + self._clear_bot_scoped_config() + self._save_bot_scoped_data_identifier(current_local_identifier) + + def _get_bot_scoped_data_identifier(self) -> str: + # identifier is based on the path to the local bot to ensure the same data are not re-used + # when copy/pasting a bot config to another bot + return hashlib.sha256(os.getcwd().encode()).hexdigest() async def stop(self): self.logger.debug("Stopping ...") @@ -723,6 +747,9 @@ def save_tradingview_email(self, tradingview_email: str): def _save_mqtt_device_uuid(self, mqtt_uuid: str): self._save_value_in_config(constants.CONFIG_COMMUNITY_MQTT_UUID, mqtt_uuid) + def _save_bot_scoped_data_identifier(self, identifier: str): + self._save_value_in_config(constants.CONFIG_COMMUNITY_LOCAL_DATA_IDENTIFIER, identifier) + def get_saved_package_urls(self) -> list[str]: return self._get_value_in_config(constants.CONFIG_COMMUNITY_PACKAGE_URLS) or [] @@ -731,6 +758,9 @@ def get_saved_mqtt_device_uuid(self) -> str: return mqtt_uuid raise errors.NoBotDeviceError("No MQTT device ID has been set") + def _get_saved_bot_scoped_data_identifier(self) -> str: + return self._get_value_in_config(constants.CONFIG_COMMUNITY_LOCAL_DATA_IDENTIFIER) + def get_saved_tradingview_email(self) -> str: return self._get_value_in_config(constants.CONFIG_COMMUNITY_TRADINGVIEW_EMAIL) diff --git a/octobot/constants.py b/octobot/constants.py index 896e811c7..f7c666285 100644 --- a/octobot/constants.py +++ b/octobot/constants.py @@ -106,6 +106,7 @@ CONFIG_COMMUNITY_TRADINGVIEW_EMAIL = "tradingview_email" CONFIG_COMMUNITY_PACKAGE_URLS = "package_urls" CONFIG_COMMUNITY_ENVIRONMENT = "environment" +CONFIG_COMMUNITY_LOCAL_DATA_IDENTIFIER = "local_data_identifier" USE_BETA_EARLY_ACCESS = os_util.parse_boolean_environment_var("USE_BETA_EARLY_ACCESS", "false") USER_ACCOUNT_EMAIL = os.getenv("USER_ACCOUNT_EMAIL", "") USER_PASSWORD_TOKEN = os.getenv("USER_PASSWORD_TOKEN", None) From 30c4e2c7354aa5fde6d61bad5be5737d57c95767 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 7 Oct 2024 14:49:21 +0200 Subject: [PATCH 12/15] [Community] add deployment stopped at --- octobot/community/supabase_backend/enums.py | 1 + 1 file changed, 1 insertion(+) diff --git a/octobot/community/supabase_backend/enums.py b/octobot/community/supabase_backend/enums.py index d4d667f64..6b49cbdc0 100644 --- a/octobot/community/supabase_backend/enums.py +++ b/octobot/community/supabase_backend/enums.py @@ -55,6 +55,7 @@ class BotDeploymentKeys(enum.Enum): ERROR_STATUS = "error_status" ACTIVITIES = "activities" EXPIRATION_TIME = "expiration_time" + STOPPED_AT = "stopped_at" class BotDeploymentActivitiesKeys(enum.Enum): From 3004fdc707f3a1de2b69c8c6cefe38e91941f0ab Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 7 Oct 2024 16:09:47 +0200 Subject: [PATCH 13/15] [Constant] remove unused variables --- octobot/constants.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/octobot/constants.py b/octobot/constants.py index f7c666285..35fa68ca7 100644 --- a/octobot/constants.py +++ b/octobot/constants.py @@ -66,13 +66,6 @@ COMMUNITY_TRADINGVIEW_WEBHOOK_BASE_URL = os.getenv( "COMMUNITY_TRADINGVIEW_WEBHOOK_BASE_URL", "https://webhook.octobot.cloud/tradingview" ) -COMMUNITY_EXTENSIONS_IDENTIFIER = "scaleway" -COMMUNITY_EXTENSIONS_CHECK_ENDPOINT = os.getenv( - "COMMUNITY_EXTENSIONS_CHECK_ENDPOINT", "https://premium.octobot.cloud" -) -COMMUNITY_EXTENSIONS_CHECK_ENDPOINT_KEY = os.getenv( - "COMMUNITY_EXTENSIONS_CHECK_ENDPOINT_KEY", "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBsaWNhdGlvbl9jbGFpbSI6W3sibmFtZXNwYWNlX2lkIjoiIiwiYXBwbGljYXRpb25faWQiOiI5YTEzMTg2Mi1iMmY2LTRlOWUtYjU1OC0yOTU4MWFjYjM0ZjUifV0sInZlcnNpb24iOjIsImF1ZCI6ImZ1bmN0aW9ucyIsImp0aSI6IjJkMTM5OWQxLTRkNjMtNGVmNi1hNTI3LWNhMDQxMDdiNmUwYSIsImlhdCI6MTcyMDEyNjc1OSwiaXNzIjoiU0NBTEVXQVkiLCJuYmYiOjE3MjAxMjY3NTksInN1YiI6InRva2VuIn0.S2StO0Jey_BGotVdIYOa1hUNyF1m-BTLr-5oy24tiIXoh6nysMn_wBx0EzTDjQ_rG9yyUWbEYENjVlUzRJukiUf-5jjmIY0sgp6gYwtn6tu5Va1HyLOHpTNLYmSFcj7S-DcJXfd0uIGJcNRSAvYftnt-SVqjray0g5SfQEoB6UDSQolfECs4Avj7O0_Wtny1LHoIX_BEqlGWetODNklNNrBJuFUtSxoGfGarVGejOyvCdk10tFXpGJQr9dKPhnNSChs6N3qk4ApH5ET6JjOUENVF6x-KZ8Ed82KFU0gdGXICMVIiCUJz-b-QU88-HG6QG2-fD8dtvRUSCt_PsZPPZ_7IDWWuA-LEdNlKCyatVz0Yx3mCDusHN7Tt3ae-dJg9wpC4VCxqy8-MHOg9uf9GREkkc8Al-Nfn04tLWrl-OY_lrJ_jJ5_6N_XTwzNGmEdN3EVAeedwfyfpiuiXJMy84WQpfmJWn1zKEUrsBmx8xrTPz1pmZBB6uRKcdjUNWV2MpiAgxFxQI8Mo_zUJvagydfylcijjen8wP1ML0y8ywF8KSUmNprBv2SUwY8AXywtP5qIusnUEv-WxtoFdOU7Rgu3bCsdlktVEo2n2S-j6R9bki43gIgAmyxCveE-lwcNYoc_MahHMrjRW2uoO5deDo_yq90OJmnvnl35cLgVbkoA" -) DISABLE_COMMUNITY_EXTENSIONS_CHECK = os_util.parse_boolean_environment_var("DISABLE_COMMUNITY_EXTENSIONS_CHECK", "false") COMMUNITY_EXTENSIONS_PACKAGES_IDENTIFIER = ".cloud" COMMUNITY_FETCH_TIMEOUT = 30 From fc687b2dbff6a468be2f8d0b93908deed71c4b86 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 7 Oct 2024 17:58:00 +0200 Subject: [PATCH 14/15] [Community] add tradingview_email_confirmed accessor --- octobot/community/authentication.py | 8 ++++++++ octobot/community/feeds/community_mqtt_feed.py | 1 + octobot/constants.py | 1 + 3 files changed, 10 insertions(+) diff --git a/octobot/community/authentication.py b/octobot/community/authentication.py index 416bab4d4..4bade9c6a 100644 --- a/octobot/community/authentication.py +++ b/octobot/community/authentication.py @@ -544,6 +544,8 @@ def _clear_bot_scoped_config(self): self.save_tradingview_email("") # also reset mqtt id to force a new mqtt id creation self._save_mqtt_device_uuid("") + # will force reconfiguring the next email + self.save_tradingview_email_confirmed(False) def clear_local_data_if_necessary(self): if constants.IS_CLOUD_ENV: @@ -744,6 +746,9 @@ def save_installed_package_urls(self, package_urls: list[str]): def save_tradingview_email(self, tradingview_email: str): self._save_value_in_config(constants.CONFIG_COMMUNITY_TRADINGVIEW_EMAIL, tradingview_email) + def save_tradingview_email_confirmed(self, confirmed: bool): + self._save_value_in_config(constants.CONFIG_COMMUNITY_TRADINGVIEW_EMAIL_CONFIRMED, confirmed) + def _save_mqtt_device_uuid(self, mqtt_uuid: str): self._save_value_in_config(constants.CONFIG_COMMUNITY_MQTT_UUID, mqtt_uuid) @@ -764,6 +769,9 @@ def _get_saved_bot_scoped_data_identifier(self) -> str: def get_saved_tradingview_email(self) -> str: return self._get_value_in_config(constants.CONFIG_COMMUNITY_TRADINGVIEW_EMAIL) + def is_tradingview_email_confirmed(self) -> bool: + return self._get_value_in_config(constants.CONFIG_COMMUNITY_TRADINGVIEW_EMAIL_CONFIRMED) is True + def _save_bot_id(self, bot_id): self._save_value_in_config(constants.CONFIG_COMMUNITY_BOT_ID, bot_id) diff --git a/octobot/community/feeds/community_mqtt_feed.py b/octobot/community/feeds/community_mqtt_feed.py index 950ad208f..365a4aae3 100644 --- a/octobot/community/feeds/community_mqtt_feed.py +++ b/octobot/community/feeds/community_mqtt_feed.py @@ -145,6 +145,7 @@ async def _config_feed_callback(self, data: dict): email_body = parsed_message["code_email"] self.logger.info(f"Received email address confirm code:\n{email_body}") self.authenticator.user_account.last_email_address_confirm_code_email_content = email_body + self.authenticator.save_tradingview_email_confirmed(True) else: self.logger.error(f"Unknown cfg message action: {action=}") if action and self._stop_on_cfg_action and self._stop_on_cfg_action.value == action: diff --git a/octobot/constants.py b/octobot/constants.py index 35fa68ca7..7e0108ad3 100644 --- a/octobot/constants.py +++ b/octobot/constants.py @@ -97,6 +97,7 @@ CONFIG_COMMUNITY_BOT_ID = "bot_id" CONFIG_COMMUNITY_MQTT_UUID = "mqtt_uuid" CONFIG_COMMUNITY_TRADINGVIEW_EMAIL = "tradingview_email" +CONFIG_COMMUNITY_TRADINGVIEW_EMAIL_CONFIRMED = "tradingview_email_confirmed" CONFIG_COMMUNITY_PACKAGE_URLS = "package_urls" CONFIG_COMMUNITY_ENVIRONMENT = "environment" CONFIG_COMMUNITY_LOCAL_DATA_IDENTIFIER = "local_data_identifier" From 9ac8f331a46a82daf3260fd6a39933669884a9b9 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 7 Oct 2024 17:58:29 +0200 Subject: [PATCH 15/15] [Requirements] bump --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6ef7de2f0..4e9af4e32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ OctoBot-Commons==1.9.59 OctoBot-Trading==2.4.114 OctoBot-Evaluators==1.9.7 OctoBot-Tentacles-Manager==2.9.16 -OctoBot-Services==1.6.19 +OctoBot-Services==1.6.20 OctoBot-Backtesting==1.9.7 Async-Channel==2.2.1 trading-backend==1.2.29