Skip to content
Open
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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,42 @@ async def on_message(message):
bot.run()
```

#### Manual login flows

When you rely on the BearWare.dk web login flow, the recommended pattern is:

```python
bot = pytalk.TeamTalkBot()

@bot.event
async def on_ready():
# Call your own helper to fetch a short-lived BearWare session token
await bot.add_server(
pytalk.TeamTalkServerInfo(
{
"host": "example.org",
"tcp_port": 10333,
"udp_port": 10333,
# BearWare.dk requires the e-mail-style login as the username
"username": "login@bearware.dk",
"encrypted": True,
# Store the preference with the server info
"auto_login": False,
}
),
)

@bot.event
async def on_my_connect(server: pytalk.server.Server):
token = pytalk.sdk.ttstr(server.get_properties().properties.szAccessToken)
# Tell your helper to register/log in using the token
server.teamtalk_instance.login()
```

With ``auto_login`` disabled in the server info, the instance still establishes
the TCP/UDP connection and manages its backoff timers, but you stay in control
of *when* ``login()`` runs so the BearWare token exchange can complete first.


## Documentation

Expand Down
13 changes: 11 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,17 @@ Bot
:members:
:exclude-members: event,dispatch

.. automethod:: pytalk.bot.TeamTalkBot.event()
:decorator:
.. automethod:: pytalk.bot.TeamTalkBot.event()
:decorator:

.. tip::

Set ``auto_login=False`` on :class:`pytalk.enums.TeamTalkServerInfo` (or pass
it explicitly to :meth:`pytalk.bot.TeamTalkBot.add_server`) when you need to
defer login. The instance will connect and manage backoff as usual, but the
caller is responsible for invoking
:meth:`pytalk.instance.TeamTalkInstance.login` at the appropriate time (e.g.,
after completing a BearWare.dk token exchange).


Enums
Expand Down
5 changes: 4 additions & 1 deletion pytalk/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ async def add_server(
server: TeamTalkServerInfo | dict[str, Any],
reconnect: bool = True,
backoff_config: dict[str, Any] | None = None,
auto_login: bool | None = None,
) -> None:
"""Add a server to the bot.

Expand All @@ -73,12 +74,14 @@ async def add_server(
Can contain keys: `base`, `exponent`, `max_value`, `max_tries`.
These settings govern the retry behavior for both the initial
connection sequence and for reconnections after a connection loss.
auto_login (Optional[bool]): Whether to automatically log in to the
server. ``None`` (default) defers to ``server.auto_login``.

"""
if isinstance(server, dict):
server = TeamTalkServerInfo.from_dict(server)
_log.debug("Adding server: %s, %s", self, server)
tt = TeamTalkInstance(self, server, reconnect, backoff_config)
tt = TeamTalkInstance(self, server, reconnect, backoff_config, auto_login)
successful_initial_connection = await tt.initial_connect_loop()
if not successful_initial_connection:
_log.error(
Expand Down
12 changes: 11 additions & 1 deletion pytalk/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def __init__(self, data: dict[str, Any]) -> None:
self.nickname = data.get("nickname", data.get("username"))
self.join_channel_id = data.get("join_channel_id", -1)
self.join_channel_password = data.get("join_channel_password", "")
self.auto_login = data.get("auto_login", True)

@classmethod
def from_dict(cls, data: dict[str, Any]) -> Self:
Expand Down Expand Up @@ -53,6 +54,7 @@ def to_dict(self) -> dict[str, Any]:
"nickname": self.nickname if self.nickname else "",
"join_channel_id": self.join_channel_id,
"join_channel_password": self.join_channel_password,
"auto_login": self.auto_login,
}

def __eq__(self, other: object) -> bool:
Expand All @@ -74,6 +76,7 @@ def __eq__(self, other: object) -> bool:
and self.username == other.username
and self.password == other.password
and self.encrypted == other.encrypted
and self.auto_login == other.auto_login
)

def __ne__(self, other: object) -> bool:
Expand All @@ -91,7 +94,14 @@ def __ne__(self, other: object) -> bool:
def __hash__(self) -> int:
"""Return the hash value of the object."""
return hash(
(self.host, self.tcp_port, self.udp_port, self.username, self.encrypted)
(
self.host,
self.tcp_port,
self.udp_port,
self.username,
self.encrypted,
self.auto_login,
)
)


Expand Down
57 changes: 36 additions & 21 deletions pytalk/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(
server_info: TeamTalkServerInfo,
reconnect: bool = True,
backoff_config: dict[str, Any] | None = None,
auto_login: bool | None = None,
) -> None:
"""Initialize a pytalk.TeamTalkInstance instance.

Expand All @@ -91,6 +92,8 @@ def __init__(
These settings govern the retry behavior for both the initial
connection sequence and for reconnections after a connection loss.
Defaults to `None` (using default Backoff settings).
auto_login (Optional[bool]): Whether to automatically log in after
connecting. ``None`` (default) defers to ``server_info.auto_login``.

"""
super().__init__()
Expand All @@ -106,6 +109,7 @@ def __init__(
self._current_input_device_id: int | None = -1
self._audio_sdk_lock = threading.Lock()
self.reconnect_enabled = reconnect
self.auto_login = server_info.auto_login if auto_login is None else auto_login
if backoff_config:
self._backoff = Backoff(**backoff_config)
else:
Expand Down Expand Up @@ -1526,6 +1530,36 @@ async def _process_events(self) -> None: # noqa: C901, PLR0911, PLR0912, PLR091
):
_log.warning("Unhandled event: %s", event)

async def _login_after_connect(self, *, reconnect: bool) -> bool:
"""Handle login steps shared by initial connect and reconnect paths."""
if not self.auto_login:
_log.info(
"Auto-login is disabled. Skipping login after %s.",
"reconnect" if reconnect else "initial connect",
)
self._backoff.reset()
return True

login_args = (True,) if reconnect else ()
logged_in_ok = await self.bot.loop.run_in_executor(
None, self.login, *login_args
)
if logged_in_ok:
_log.info(
"Successfully %slogged in to %s.",
"re" if reconnect else "",
self.server_info.host,
)
self._backoff.reset()
return True

_log.warning(
"Login failed for %s after successful %s.",
self.server_info.host,
"reconnect" if reconnect else "connection",
)
return False

async def initial_connect_loop(self) -> bool:
"""Attempt to establish an initial connection and login to the server.

Expand All @@ -1550,15 +1584,8 @@ async def initial_connect_loop(self) -> bool:
"Successfully connected to %s. Attempting login...",
self.server_info.host,
)
logged_in_ok = await self.bot.loop.run_in_executor(None, self.login)
if logged_in_ok:
_log.info("Successfully logged in to %s.", self.server_info.host)
self._backoff.reset()
if await self._login_after_connect(reconnect=False):
return True
_log.warning(
"Login failed for %s after successful connection.",
self.server_info.host,
)
else:
_log.warning(
"Initial connection attempt failed for %s.",
Expand Down Expand Up @@ -1613,20 +1640,8 @@ async def _reconnect(self) -> None:
"Re-established connection to %s. Attempting login...",
self.server_info.host,
)
logged_in_ok = await self.bot.loop.run_in_executor(
None, self.login, True
)
if logged_in_ok:
_log.info(
"Successfully reconnected and logged in to %s.",
self.server_info.host,
)
self._backoff.reset()
if await self._login_after_connect(reconnect=True):
return
_log.warning(
"Login failed for %s after successful reconnect.",
self.server_info.host,
)
else:
_log.warning(
"Reconnect attempt %s failed for %s.",
Expand Down