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()