Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions 2fa_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
"no",
"off",
}
DISMISS_SMALL_GATEWAY_DIALOGS = os.environ.get(
"IBKR_DISMISS_SMALL_GATEWAY_DIALOGS",
"yes",
Comment on lines +40 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Forward the new dismissal toggle into the container

In the normal deployment path the bot runs inside the compose service, but docker-compose.yml uses an explicit environment: list with no env_file, and the workflow env writer only emits the existing 2FA variables. As a result, setting IBKR_DISMISS_SMALL_GATEWAY_DIALOGS=no in the host .env or repo variables never reaches 2fa_bot.py, so operators cannot disable this new broad IBKR Gateway dialog dismissal without editing the compose file itself.

Useful? React with 👍 / 👎.

).strip().lower() not in {
"0",
"false",
"no",
"off",
}

# Window titles to search for 2FA prompts. Live IBKR accounts can show mobile
# push / IB Key wording instead of the shorter TOTP-oriented prompts.
Expand Down Expand Up @@ -71,10 +80,14 @@
)
DISMISSIBLE_DIALOG_SEARCH_PATTERNS = [
"Login Messages",
"IBKR Gateway",
]
DISMISSIBLE_DIALOG_TITLE_KEYWORDS = (
"login messages",
)
SMALL_GATEWAY_DIALOG_TITLE = "ibkr gateway"
SMALL_GATEWAY_DIALOG_MAX_WIDTH = 650
SMALL_GATEWAY_DIALOG_MAX_HEIGHT = 220
# Current IBKR Gateway TOTP prompts place the code field in the upper half of
# the compact dialog. Keep the click centered on the text field instead of the
# button/link area below it.
Expand Down Expand Up @@ -205,9 +218,21 @@ def is_auth_candidate(title):
return any(keyword in normalized_title for keyword in AUTH_TITLE_KEYWORDS)


def is_dismissible_dialog_candidate(title):
def is_small_gateway_dialog(title, width, height):
if not DISMISS_SMALL_GATEWAY_DIALOGS:
return False
if title.lower() != SMALL_GATEWAY_DIALOG_TITLE:
return False
if not width or not height:
return False
return width <= SMALL_GATEWAY_DIALOG_MAX_WIDTH and height <= SMALL_GATEWAY_DIALOG_MAX_HEIGHT


def is_dismissible_dialog_candidate(title, width=None, height=None):
normalized_title = title.lower()
return any(keyword in normalized_title for keyword in DISMISSIBLE_DIALOG_TITLE_KEYWORDS)
if any(keyword in normalized_title for keyword in DISMISSIBLE_DIALOG_TITLE_KEYWORDS):
return True
return is_small_gateway_dialog(title, width, height)


def find_windows_by_patterns(patterns):
Expand Down Expand Up @@ -243,9 +268,9 @@ def find_dismissible_dialogs():
candidates = []
for window_id in reversed(find_windows_by_patterns(DISMISSIBLE_DIALOG_SEARCH_PATTERNS)):
title = get_window_title(window_id)
if not is_dismissible_dialog_candidate(title):
continue
width, height = get_window_geometry(window_id)
if not is_dismissible_dialog_candidate(title, width, height):
continue
candidates.append(WindowCandidate(window_id, title, width, height))
return candidates

Expand Down
22 changes: 22 additions & 0 deletions tests/test_docker_compose_ports.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,26 @@ grep -Fq ' - ACCEPT_API_FROM_IP=${ACCEPT_API_FROM_IP:?Set ACCEPT_API_FROM_I

grep -Fq 'INPUT_CLICK_POSITION = (0.50, 0.40)' "$repo_dir/2fa_bot.py"
grep -Fq 'MIN_TOTP_SECONDS_REMAINING = 15' "$repo_dir/2fa_bot.py"
grep -Fq 'SMALL_GATEWAY_DIALOG_MAX_WIDTH = 650' "$repo_dir/2fa_bot.py"
grep -Fq 'SMALL_GATEWAY_DIALOG_MAX_HEIGHT = 220' "$repo_dir/2fa_bot.py"
grep -Fq 'def is_small_gateway_dialog(title, width, height):' "$repo_dir/2fa_bot.py"
grep -Fq 'is_dismissible_dialog_candidate(title, width, height)' "$repo_dir/2fa_bot.py"
grep -Fq 'def type_totp_into_active_window(code):' "$repo_dir/2fa_bot.py"

REPO_DIR="$repo_dir" python3 <<'PY'
import importlib.util
import os
import sys
import types

sys.modules["pyotp"] = types.SimpleNamespace()
module_path = os.path.join(os.environ["REPO_DIR"], "2fa_bot.py")
spec = importlib.util.spec_from_file_location("ibkr_2fa_bot_test", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

assert module.is_dismissible_dialog_candidate("Login Messages")
assert module.is_dismissible_dialog_candidate("IBKR Gateway", 509, 131)
assert not module.is_dismissible_dialog_candidate("IBKR Gateway", 700, 550)
assert not module.is_dismissible_dialog_candidate("IBKR Gateway", 790, 610)
PY