diff --git a/constants.py b/constants.py index 3d283bd..4e70f06 100644 --- a/constants.py +++ b/constants.py @@ -256,6 +256,7 @@ class State(Enum): CHANNELS_FETCH = auto() CHANNELS_CLEANUP = auto() CHANNEL_SWITCH = auto() + RESTART = auto() EXIT = auto() diff --git a/gui.py b/gui.py index 654b8f7..8834068 100644 --- a/gui.py +++ b/gui.py @@ -36,7 +36,7 @@ if sys.platform == "darwin": from translate import _ from cache import ImageCache from exceptions import MinerException, ExitRequest -from utils import resource_path, set_root_icon, webopen, Game, _T +from utils import resource_path, set_root_icon, webopen, task_wrapper, Game, _T from constants import ( MAX_INT, SELF_PATH, @@ -61,6 +61,7 @@ if TYPE_CHECKING: from inventory import DropsCampaign, TimedDrop +logger = logging.getLogger("TwitchDrops") TK_PADDING = Union[int, Tuple[int, int], Tuple[int, int, int], Tuple[int, int, int, int]] DIGITS = ceil(log10(WS_TOPICS_LIMIT)) @@ -2096,6 +2097,9 @@ class HelpTab: # use a frame to center the content within the tab center_frame = ttk.Frame(master) center_frame.grid(column=0, row=0) + # use a frame for the bottom row specifically + bottom_frame = ttk.Frame(master) + bottom_frame.grid(column=0, row=1, sticky="nsew") irow = 0 # About about = ttk.LabelFrame(center_frame, padding=(4, 0, 4, 4), text="About") @@ -2160,6 +2164,41 @@ class HelpTab: getstarted, text=_("gui", "help", "getting_started_text"), wraplength=self.WIDTH ).grid(sticky="nsew") + # Invalidate button + invalidate_frame = ttk.Frame(bottom_frame) + bottom_frame.columnconfigure(0, weight=1) # center within the column + invalidate_frame.grid(column=0, row=0, sticky="nse") + ttk.Label( + invalidate_frame, text=_("gui", "help", "invalidate", "text") + ).grid(column=0, row=0) + self._invalidate_button: ttk.Button = ttk.Button( + invalidate_frame, + text=_("gui", "help", "invalidate", "button"), + command=self.invalidate_token, + state="disabled", + ) + self._invalidate_button.grid(column=1, row=0) + + def invalidate_token(self) -> None: + # sync to async bridge + asyncio.create_task(task_wrapper(self._invalidate_token)()) + + async def _invalidate_token(self) -> None: + auth_state = await self._twitch.get_auth() + async with self._twitch.request( + "POST", + "https://id.twitch.tv/oauth2/revoke", + data={ + "client_id": self._twitch._client_type.CLIENT_ID, + "token": auth_state.access_token, + } + ) as response: + if response.status == 200: + auth_state.invalidate(delete_cookies=True) + else: + logger.error(f"Failed to invalidate the auth token: {response.status}") + self._twitch.change_state(State.RESTART) + ########################################## # GUI DEFINITION END / GUI MANAGER START # diff --git a/translate.py b/translate.py index 9f3b56a..8ebe2ec 100644 --- a/translate.py +++ b/translate.py @@ -197,12 +197,18 @@ class GUIHelpLinks(TypedDict): campaigns: str +class GUIHelpInvalidate(TypedDict): + button: str + text: str + + class GUIHelp(TypedDict): links: GUIHelpLinks how_it_works: str how_it_works_text: str getting_started: str getting_started_text: str + invalidate: GUIHelpInvalidate class GUIMessages(TypedDict): @@ -434,6 +440,10 @@ default_translation: Translation = { "the \"Priority mode\", requires you to press on \"Reload\" " "for the changes to take an effect." ), + "invalidate": { + "button": "Invalidate", + "text": "Invalidate the authentication token (log out):", + }, }, }, } diff --git a/twitch.py b/twitch.py index e98406c..bb24894 100644 --- a/twitch.py +++ b/twitch.py @@ -96,6 +96,17 @@ class _AuthState: if hasattr(self, attr): delattr(self, attr) + def invalidate(self, *, delete_cookies: bool = False) -> None: + self._delattrs("access_token", "user_id") + self._logged_in.clear() + self._twitch.gui.help._invalidate_button.config(state="disabled") + if delete_cookies: + session = self._twitch._session + if session is not None: + jar = cast(aiohttp.CookieJar, session.cookie_jar) + jar.clear() + COOKIES_PATH.unlink(missing_ok=True) + def clear(self) -> None: self._delattrs( "user_id", @@ -105,6 +116,7 @@ class _AuthState: "client_version", ) self._logged_in.clear() + self._twitch.gui.help._invalidate_button.config(state="disabled") async def _oauth_login(self) -> str: login_form: LoginForm = self._twitch.gui.login @@ -414,11 +426,9 @@ class _AuthState: # update our cookie and save it jar.update_cookies(cookie, client_info.CLIENT_URL) jar.save(COOKIES_PATH) + self._twitch.gui.help._invalidate_button.config(state="normal") self._logged_in.set() - def invalidate(self): - self._delattrs("access_token") - class Twitch: def __init__(self, settings: Settings): @@ -866,6 +876,8 @@ class Twitch: self.print(_("status", "no_channel")) self.change_state(State.IDLE) del new_watching, selected_channel, watching_channel + elif self._state is State.RESTART: + raise ReloadRequest() elif self._state is State.EXIT: self.gui.tray.change_icon("pickaxe") self.gui.status.update(_("gui", "status", "exiting"))