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 f041a3ff3..4bade9c6a 100644 --- a/octobot/community/authentication.py +++ b/octobot/community/authentication.py @@ -18,8 +18,11 @@ import json import time import typing +import hashlib +import os 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 +183,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 +299,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 +316,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 @@ -505,9 +524,48 @@ 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.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.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("") + # will force reconfiguring the next email + self.save_tradingview_email_confirmed(False) + + 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 ...") if self._fetch_account_task is not None and not self._fetch_account_task.done(): @@ -594,6 +652,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() @@ -603,7 +664,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) @@ -616,7 +679,9 @@ 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: self.logger.exception(err, True, f"Unexpected error when fetching package urls: {err}") finally: @@ -627,56 +692,37 @@ 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): - 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: - return None, None, None + 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, 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"] - return packages, urls, mqtt_id + mqtt_id = extensions_details["mqtt_id"] + tradingview_email = extensions_details["tradingview_email"] + return packages, urls, mqtt_id, tradingview_email - 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 " @@ -697,21 +743,39 @@ 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_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) + 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 [] - 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_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) + + 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) - 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..365a4aae3 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: @@ -93,16 +101,9 @@ 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 self._subscription_attempts = 0 self._connect_task = None self._valid_auth = True @@ -112,6 +113,45 @@ 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: + 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): + """ + 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 + 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: + 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 +222,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 +249,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 +355,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..dce285c13 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 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[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/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/community/supabase_backend/community_supabase_client.py b/octobot/community/supabase_backend/community_supabase_client.py index f1aac67aa..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 @@ -240,6 +203,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 @@ -804,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') 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): diff --git a/octobot/constants.py b/octobot/constants.py index bc0931cb8..7e0108ad3 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 @@ -103,8 +96,11 @@ CONFIG_COMMUNITY = "community" 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" 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) 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" diff --git a/requirements.txt b/requirements.txt index 82ce17488..4e9af4e32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ # Drakkar-Software requirements -OctoBot-Commons==1.9.58 -OctoBot-Trading==2.4.112 +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.20 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 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()