mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Merge pull request #11539 from Axel-CH/feat/add-paused-state
Feature: add paused state
This commit is contained in:
@@ -1032,6 +1032,7 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"running",
|
||||
"paused",
|
||||
"stopped"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -177,7 +177,7 @@ sudo loginctl enable-linger "$USER"
|
||||
If you run the bot as a service, you can use systemd service manager as a software watchdog monitoring freqtrade bot
|
||||
state and restarting it in the case of failures. If the `internals.sd_notify` parameter is set to true in the
|
||||
configuration or the `--sd-notify` command line option is used, the bot will send keep-alive ping messages to systemd
|
||||
using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running or Stopped)
|
||||
using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running, Paused or Stopped)
|
||||
when it changes.
|
||||
|
||||
The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd
|
||||
|
||||
@@ -266,7 +266,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.<br> *Defaults to `freqtrade`*<br> **Datatype:** String
|
||||
| `external_message_consumer` | Enable [Producer/Consumer mode](producer-consumer.md) for more details. <br> **Datatype:** Dict
|
||||
| | **Other**
|
||||
| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `stopped` or `running`
|
||||
| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `running`, `paused` or `stopped`
|
||||
| `force_entry_enable` | Enables the RPC Commands to force a Trade entry. More information below. <br> **Datatype:** Boolean
|
||||
| `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).<br> *Defaults to `False`*. <br> **Datatype:** Boolean
|
||||
| `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer
|
||||
|
||||
@@ -268,6 +268,9 @@ show_config
|
||||
start
|
||||
Start the bot if it's in the stopped state.
|
||||
|
||||
pause
|
||||
Pause the bot if it's in the running state. If triggered on stopped state will handle open positions.
|
||||
|
||||
stats
|
||||
Return the stats report (durations, sell-reasons).
|
||||
|
||||
@@ -333,6 +336,7 @@ All endpoints in the below table need to be prefixed with the base URL of the AP
|
||||
|-----------|--------|--------------------------|
|
||||
| `/ping` | GET | Simple command testing the API Readiness - requires no authentication.
|
||||
| `/start` | POST | Starts the trader.
|
||||
| `/pause` | POST | Pause the trader. Gracefully handle open trades according to their rules. Do not enter new positions.
|
||||
| `/stop` | POST | Stops the trader.
|
||||
| `/stopbuy` | POST | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `/reload_config` | POST | Reloads the configuration file.
|
||||
|
||||
@@ -188,7 +188,7 @@ You can create your own keyboard in `config.json`:
|
||||
!!! Note "Supported Commands"
|
||||
Only the following commands are allowed. Command arguments are not supported!
|
||||
|
||||
`/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir`
|
||||
`/start`, `/pause`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir`
|
||||
|
||||
## Telegram commands
|
||||
|
||||
@@ -200,6 +200,7 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
|----------|-------------|
|
||||
| **System commands**
|
||||
| `/start` | Starts the trader
|
||||
| `/pause` | Pause the trader. Gracefully handle open trades according to their rules. Do not enter new positions.
|
||||
| `/stop` | Stops the trader
|
||||
| `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `/reload_config` | Reloads the configuration file
|
||||
@@ -250,6 +251,10 @@ Below, example of Telegram message you will receive for each command.
|
||||
|
||||
> **Status:** `running`
|
||||
|
||||
### /pause
|
||||
|
||||
> **Status:** `paused`
|
||||
|
||||
### /stop
|
||||
|
||||
> `Stopping trader ...`
|
||||
|
||||
@@ -689,7 +689,7 @@ CONF_SCHEMA = {
|
||||
"initial_state": {
|
||||
"description": "Initial state of the system.",
|
||||
"type": "string",
|
||||
"enum": ["running", "stopped"],
|
||||
"enum": ["running", "paused", "stopped"],
|
||||
},
|
||||
"force_entry_enable": {
|
||||
"description": "Force enable entry.",
|
||||
|
||||
@@ -7,8 +7,9 @@ class State(Enum):
|
||||
"""
|
||||
|
||||
RUNNING = 1
|
||||
STOPPED = 2
|
||||
RELOAD_CONFIG = 3
|
||||
PAUSED = 2
|
||||
STOPPED = 3
|
||||
RELOAD_CONFIG = 4
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
@@ -300,7 +300,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.process_open_trade_positions()
|
||||
|
||||
# Then looking for entry opportunities
|
||||
if self.get_free_open_trades():
|
||||
if self.state == State.RUNNING and self.get_free_open_trades():
|
||||
self.enter_positions()
|
||||
self._schedule.run_pending()
|
||||
Trade.commit()
|
||||
@@ -781,6 +781,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
|
||||
if stake_amount is not None and stake_amount > 0.0:
|
||||
if self.state == State.PAUSED:
|
||||
logger.debug("Position adjustment aborted because the bot is in PAUSED state")
|
||||
return
|
||||
|
||||
# We should increase our position
|
||||
if self.strategy.max_entry_position_adjustment > -1:
|
||||
count_of_entries = trade.nr_of_successful_entries
|
||||
|
||||
@@ -369,10 +369,11 @@ def stop(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_stop()
|
||||
|
||||
|
||||
@router.post("/pause", response_model=StatusMsg, tags=["botcontrol"])
|
||||
@router.post("/stopentry", response_model=StatusMsg, tags=["botcontrol"])
|
||||
@router.post("/stopbuy", response_model=StatusMsg, tags=["botcontrol"])
|
||||
def stop_buy(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_stopentry()
|
||||
def pause(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_pause()
|
||||
|
||||
|
||||
@router.post("/reload_config", response_model=StatusMsg, tags=["botcontrol"])
|
||||
|
||||
@@ -838,7 +838,7 @@ class RPC:
|
||||
|
||||
def _rpc_stop(self) -> dict[str, str]:
|
||||
"""Handler for stop"""
|
||||
if self._freqtrade.state == State.RUNNING:
|
||||
if self._freqtrade.state != State.STOPPED:
|
||||
self._freqtrade.state = State.STOPPED
|
||||
return {"status": "stopping trader ..."}
|
||||
|
||||
@@ -849,16 +849,25 @@ class RPC:
|
||||
self._freqtrade.state = State.RELOAD_CONFIG
|
||||
return {"status": "Reloading config ..."}
|
||||
|
||||
def _rpc_stopentry(self) -> dict[str, str]:
|
||||
def _rpc_pause(self) -> dict[str, str]:
|
||||
"""
|
||||
Handler to stop buying, but handle open trades gracefully.
|
||||
Handler to pause trading (stop entering new trades), but handle open trades gracefully.
|
||||
"""
|
||||
if self._freqtrade.state == State.RUNNING:
|
||||
# Set 'max_open_trades' to 0
|
||||
self._freqtrade.config["max_open_trades"] = 0
|
||||
self._freqtrade.strategy.max_open_trades = 0
|
||||
self._freqtrade.state = State.PAUSED
|
||||
|
||||
return {"status": "No more entries will occur from now. Run /reload_config to reset."}
|
||||
if self._freqtrade.state == State.STOPPED:
|
||||
self._freqtrade.state = State.PAUSED
|
||||
return {
|
||||
"status": (
|
||||
"starting bot with trader in paused state, no entries will occur. "
|
||||
"Run /start to enable entries."
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "paused, no more entries will occur from now. Run /start to enable entries."
|
||||
}
|
||||
|
||||
def _rpc_reload_trade_from_exchange(self, trade_id: int) -> dict[str, str]:
|
||||
"""
|
||||
@@ -930,7 +939,7 @@ class RPC:
|
||||
Sells the given trade at current price
|
||||
"""
|
||||
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
if self._freqtrade.state == State.STOPPED:
|
||||
raise RPCException("trader is not running")
|
||||
|
||||
with self._freqtrade._exit_lock:
|
||||
@@ -1046,7 +1055,7 @@ class RPC:
|
||||
raise RPCException(f"Failed to enter position for {pair}.")
|
||||
|
||||
def _rpc_cancel_open_order(self, trade_id: int):
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
if self._freqtrade.state == State.STOPPED:
|
||||
raise RPCException("trader is not running")
|
||||
with self._freqtrade._exit_lock:
|
||||
# Query for trade
|
||||
@@ -1214,7 +1223,7 @@ class RPC:
|
||||
|
||||
def _rpc_count(self) -> dict[str, float]:
|
||||
"""Returns the number of trades running"""
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
if self._freqtrade.state == State.STOPPED:
|
||||
raise RPCException("trader is not running")
|
||||
|
||||
trades = Trade.get_open_trades()
|
||||
|
||||
@@ -178,6 +178,7 @@ class Telegram(RPCHandler):
|
||||
# problem in _help()).
|
||||
valid_keys: list[str] = [
|
||||
r"/start$",
|
||||
r"/pause$",
|
||||
r"/stop$",
|
||||
r"/status$",
|
||||
r"/status table$",
|
||||
@@ -293,7 +294,7 @@ class Telegram(RPCHandler):
|
||||
CommandHandler(["unlock", "delete_locks"], self._delete_locks),
|
||||
CommandHandler(["reload_config", "reload_conf"], self._reload_config),
|
||||
CommandHandler(["show_config", "show_conf"], self._show_config),
|
||||
CommandHandler(["stopbuy", "stopentry"], self._stopentry),
|
||||
CommandHandler(["stopbuy", "stopentry", "pause"], self._pause),
|
||||
CommandHandler("whitelist", self._whitelist),
|
||||
CommandHandler("blacklist", self._blacklist),
|
||||
CommandHandler(["blacklist_delete", "bl_delete"], self._blacklist_delete),
|
||||
@@ -1269,15 +1270,15 @@ class Telegram(RPCHandler):
|
||||
await self._send_msg(f"Status: `{msg['status']}`")
|
||||
|
||||
@authorized_only
|
||||
async def _stopentry(self, update: Update, context: CallbackContext) -> None:
|
||||
async def _pause(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /stop_buy.
|
||||
Sets max_open_trades to 0 and gracefully sells all open trades
|
||||
Handler for /stop_buy /stop_entry and /pause.
|
||||
Sets bot state to paused
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc._rpc_stopentry()
|
||||
msg = self._rpc._rpc_pause()
|
||||
await self._send_msg(f"Status: `{msg['status']}`")
|
||||
|
||||
@authorized_only
|
||||
@@ -1829,6 +1830,7 @@ class Telegram(RPCHandler):
|
||||
"_Bot Control_\n"
|
||||
"------------\n"
|
||||
"*/start:* `Starts the trader`\n"
|
||||
"*/pause:* `Pause the new entries for trader, but handles open trades gracefully`\n"
|
||||
"*/stop:* `Stops the trader`\n"
|
||||
"*/stopentry:* `Stops entering, but handles open trades gracefully` \n"
|
||||
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
|
||||
|
||||
@@ -96,7 +96,10 @@ class Worker:
|
||||
logger.info(
|
||||
f"Changing state{f' from {old_state.name}' if old_state else ''} to: {state.name}"
|
||||
)
|
||||
if state == State.RUNNING:
|
||||
if state in (State.RUNNING, State.PAUSED) and old_state not in (
|
||||
State.RUNNING,
|
||||
State.PAUSED,
|
||||
):
|
||||
self.freqtrade.startup()
|
||||
|
||||
if state == State.STOPPED:
|
||||
@@ -112,9 +115,10 @@ class Worker:
|
||||
|
||||
self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs)
|
||||
|
||||
elif state == State.RUNNING:
|
||||
elif state in (State.RUNNING, State.PAUSED):
|
||||
state_str = "RUNNING" if state == State.RUNNING else "PAUSED"
|
||||
# Ping systemd watchdog before throttling
|
||||
self._notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
|
||||
self._notify(f"WATCHDOG=1\nSTATUS=State: {state_str}.")
|
||||
|
||||
# Use an offset of 1s to ensure a new candle has been issued
|
||||
self._throttle(
|
||||
@@ -221,7 +225,7 @@ class Worker:
|
||||
# Load and validate config and create new instance of the bot
|
||||
self._init(True)
|
||||
|
||||
self.freqtrade.notify_status("config reloaded")
|
||||
self.freqtrade.notify_status(f"{State(self.freqtrade.state)} after config reloaded")
|
||||
|
||||
# Tell systemd that we completed reconfiguration
|
||||
self._notify("READY=1")
|
||||
|
||||
@@ -3,6 +3,7 @@ import time
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
import time_machine
|
||||
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
@@ -38,6 +39,25 @@ def test_worker_running(mocker, default_conf, caplog) -> None:
|
||||
assert isinstance(worker.freqtrade.strategy.dp, DataProvider)
|
||||
|
||||
|
||||
def test_worker_paused(mocker, default_conf, caplog) -> None:
|
||||
mock_throttle = MagicMock()
|
||||
mocker.patch("freqtrade.worker.Worker._throttle", mock_throttle)
|
||||
mocker.patch("freqtrade.persistence.Trade.stoploss_reinitialization", MagicMock())
|
||||
|
||||
worker = get_patched_worker(mocker, default_conf)
|
||||
|
||||
worker.freqtrade.state = State.PAUSED
|
||||
state = worker._worker(old_state=State.RUNNING)
|
||||
|
||||
assert state is State.PAUSED
|
||||
assert log_has("Changing state from RUNNING to: PAUSED", caplog)
|
||||
assert mock_throttle.call_count == 1
|
||||
# Check strategy is loaded, and received a dataprovider object
|
||||
assert worker.freqtrade.strategy
|
||||
assert worker.freqtrade.strategy.dp
|
||||
assert isinstance(worker.freqtrade.strategy.dp, DataProvider)
|
||||
|
||||
|
||||
def test_worker_stopped(mocker, default_conf, caplog) -> None:
|
||||
mock_throttle = MagicMock()
|
||||
mocker.patch("freqtrade.worker.Worker._throttle", mock_throttle)
|
||||
@@ -50,6 +70,54 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None:
|
||||
assert mock_throttle.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"old_state,target_state,startup_call,log_fragment",
|
||||
[
|
||||
(State.STOPPED, State.PAUSED, True, "Changing state from STOPPED to: PAUSED"),
|
||||
(State.RUNNING, State.PAUSED, False, "Changing state from RUNNING to: PAUSED"),
|
||||
(State.PAUSED, State.RUNNING, False, "Changing state from PAUSED to: RUNNING"),
|
||||
(State.PAUSED, State.STOPPED, False, "Changing state from PAUSED to: STOPPED"),
|
||||
(State.RELOAD_CONFIG, State.RUNNING, True, "Changing state from RELOAD_CONFIG to: RUNNING"),
|
||||
(
|
||||
State.RELOAD_CONFIG,
|
||||
State.STOPPED,
|
||||
False,
|
||||
"Changing state from RELOAD_CONFIG to: STOPPED",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_worker_lifecycle(
|
||||
mocker,
|
||||
default_conf,
|
||||
caplog,
|
||||
old_state,
|
||||
target_state,
|
||||
startup_call,
|
||||
log_fragment,
|
||||
):
|
||||
mock_throttle = mocker.MagicMock()
|
||||
mocker.patch("freqtrade.worker.Worker._throttle", mock_throttle)
|
||||
mocker.patch("freqtrade.persistence.Trade.stoploss_reinitialization")
|
||||
startup = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.startup")
|
||||
|
||||
worker = get_patched_worker(mocker, default_conf)
|
||||
worker.freqtrade.state = target_state
|
||||
|
||||
new_state = worker._worker(old_state=old_state)
|
||||
|
||||
assert new_state is target_state
|
||||
assert log_has(log_fragment, caplog)
|
||||
assert mock_throttle.call_count == 1
|
||||
assert startup.call_count == (1 if startup_call else 0)
|
||||
|
||||
# For any state where the strategy should be initialized
|
||||
if target_state in (State.RUNNING, State.PAUSED):
|
||||
assert worker.freqtrade.strategy
|
||||
assert isinstance(worker.freqtrade.strategy.dp, DataProvider)
|
||||
else:
|
||||
assert new_state is State.STOPPED
|
||||
|
||||
|
||||
def test_throttle(mocker, default_conf, caplog) -> None:
|
||||
def throttled_func():
|
||||
return 42
|
||||
|
||||
@@ -806,19 +806,19 @@ def test_rpc_stop(mocker, default_conf) -> None:
|
||||
assert freqtradebot.state == State.STOPPED
|
||||
|
||||
|
||||
def test_rpc_stopentry(mocker, default_conf) -> None:
|
||||
def test_rpc_pause(mocker, default_conf) -> None:
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
||||
mocker.patch.multiple(EXMS, fetch_ticker=MagicMock())
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
rpc = RPC(freqtradebot)
|
||||
freqtradebot.state = State.RUNNING
|
||||
freqtradebot.state = State.PAUSED
|
||||
|
||||
assert freqtradebot.config["max_open_trades"] != 0
|
||||
result = rpc._rpc_stopentry()
|
||||
assert {"status": "No more entries will occur from now. Run /reload_config to reset."} == result
|
||||
assert freqtradebot.config["max_open_trades"] == 0
|
||||
result = rpc._rpc_pause()
|
||||
assert {
|
||||
"status": "paused, no more entries will occur from now. Run /start to enable entries."
|
||||
} == result
|
||||
|
||||
|
||||
def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
|
||||
@@ -533,23 +533,26 @@ def test_api_reloadconf(botclient):
|
||||
assert ftbot.state == State.RELOAD_CONFIG
|
||||
|
||||
|
||||
def test_api_stopentry(botclient):
|
||||
def test_api_pause(botclient):
|
||||
ftbot, client = botclient
|
||||
assert ftbot.config["max_open_trades"] != 0
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/stopbuy")
|
||||
rc = client_post(client, f"{BASE_URI}/pause")
|
||||
assert_response(rc)
|
||||
assert rc.json() == {
|
||||
"status": "No more entries will occur from now. Run /reload_config to reset."
|
||||
"status": "paused, no more entries will occur from now. Run /start to enable entries."
|
||||
}
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/pause")
|
||||
assert_response(rc)
|
||||
assert rc.json() == {
|
||||
"status": "paused, no more entries will occur from now. Run /start to enable entries."
|
||||
}
|
||||
assert ftbot.config["max_open_trades"] == 0
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/stopentry")
|
||||
assert_response(rc)
|
||||
assert rc.json() == {
|
||||
"status": "No more entries will occur from now. Run /reload_config to reset."
|
||||
"status": "paused, no more entries will occur from now. Run /start to enable entries."
|
||||
}
|
||||
assert ftbot.config["max_open_trades"] == 0
|
||||
|
||||
|
||||
def test_api_balance(botclient, mocker, rpc_balance, tickers):
|
||||
|
||||
@@ -169,7 +169,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
|
||||
"['stats'], ['daily'], ['weekly'], ['monthly'], "
|
||||
"['count'], ['locks'], ['delete_locks', 'unlock'], "
|
||||
"['reload_conf', 'reload_config'], ['show_conf', 'show_config'], "
|
||||
"['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], "
|
||||
"['pause', 'stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], "
|
||||
"['bl_delete', 'blacklist_delete'], "
|
||||
"['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir'], "
|
||||
"['order'], ['list_custom_data'], ['tg_info']]"
|
||||
@@ -1222,15 +1222,15 @@ async def test_stop_handle_already_stopped(default_conf, update, mocker) -> None
|
||||
assert "already stopped" in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
async def test_stopbuy_handle(default_conf, update, mocker) -> None:
|
||||
async def test_pause_handle(default_conf, update, mocker) -> None:
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
assert freqtradebot.config["max_open_trades"] != 0
|
||||
await telegram._stopentry(update=update, context=MagicMock())
|
||||
assert freqtradebot.config["max_open_trades"] == 0
|
||||
assert freqtradebot.state == State.RUNNING
|
||||
await telegram._pause(update=update, context=MagicMock())
|
||||
assert freqtradebot.state == State.PAUSED
|
||||
assert msg_mock.call_count == 1
|
||||
assert (
|
||||
"No more entries will occur from now. Run /reload_config to reset."
|
||||
"paused, no more entries will occur from now. Run /start to enable entries."
|
||||
in msg_mock.call_args_list[0][0][0]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user