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
3 changes: 3 additions & 0 deletions core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class SettingsFile(TypedDict):
tray_notifications: bool
enable_badges_emotes: bool
available_drops_check: bool
trust_allowed_channels: bool
farm_unlinked: bool
priority_mode: PriorityMode

Expand All @@ -37,6 +38,7 @@ class SettingsFile(TypedDict):
"tray_notifications": True,
"enable_badges_emotes": False,
"available_drops_check": False,
"trust_allowed_channels": False,
"farm_unlinked": False,
"priority_mode": PriorityMode.PRIORITY_ONLY,
}
Expand All @@ -62,6 +64,7 @@ class Settings:
tray_notifications: bool
enable_badges_emotes: bool
available_drops_check: bool
trust_allowed_channels: bool
farm_unlinked: bool
priority_mode: PriorityMode

Expand Down
2 changes: 2 additions & 0 deletions core/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ class GUISettingsAdvanced(TypedDict):
warning_text: str
enable_badges_emotes: str
available_drops_check: str
trust_allowed_channels: str
farm_unlinked: str


Expand Down Expand Up @@ -386,6 +387,7 @@ class Translation(TypedDict):
),
"enable_badges_emotes": "Enable partial support for badges and emotes: ",
"available_drops_check": "Enable extra available drops check: ",
"trust_allowed_channels": "Trust explicitly allowed channels: ",
"farm_unlinked": "Farm unlinked drops: ",
},
"priority_modes": {
Expand Down
18 changes: 18 additions & 0 deletions gui/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,7 @@ class _SettingsVars(TypedDict):
tray_notifications: IntVar
enable_badges_emotes: IntVar
available_drops_check: IntVar
trust_allowed_channels: IntVar
farm_unlinked: IntVar


Expand Down Expand Up @@ -1605,6 +1606,9 @@ def __init__(self, manager: GUIManager, master: ttk.Widget):
"available_drops_check": IntVar(
master, int(self._settings.available_drops_check)
),
"trust_allowed_channels": IntVar(
master, int(self._settings.trust_allowed_channels)
),
"farm_unlinked": IntVar(
master, int(self._settings.farm_unlinked)
),
Expand Down Expand Up @@ -1744,6 +1748,18 @@ def __init__(self, manager: GUIManager, master: ttk.Widget):
bool(self._vars["available_drops_check"].get()),
),
).grid(column=1, row=irow, sticky="w")
ttk.Label(
advanced_center, text=_("gui", "settings", "advanced", "trust_allowed_channels")
).grid(column=0, row=(irow := irow + 1), sticky="e")
ttk.Checkbutton(
advanced_center,
variable=self._vars["trust_allowed_channels"],
command=lambda: setattr(
self._settings,
"trust_allowed_channels",
bool(self._vars["trust_allowed_channels"].get()),
),
).grid(column=1, row=irow, sticky="w")
ttk.Label(
advanced_center, text=_("gui", "settings", "advanced", "farm_unlinked")
).grid(column=0, row=(irow := irow + 1), sticky="e")
Expand Down Expand Up @@ -2838,6 +2854,8 @@ async def main(exit_event: asyncio.Event):
tray_notifications=True,
enable_badges_emotes=False,
available_drops_check=False,
trust_allowed_channels=False,
farm_unlinked=False,
logging_level=LOGGING_LEVELS[0],
priority_mode=PriorityMode.PRIORITY_ONLY,
)
Expand Down
8 changes: 7 additions & 1 deletion models/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,12 +324,18 @@ async def get_spade_url(self) -> URLType:
return URLType(match.group(1))

def _check_drops_enabled(self, available_drops: list[JsonType]) -> bool:
return any(
if any(
(
(campaign := self._twitch._campaigns.get(campaign_data["id"])) is not None
and campaign.can_earn(self, ignore_channel_status=True)
)
for campaign_data in available_drops
):
return True
return self.acl_based and self._twitch.settings.trust_allowed_channels and any(
self in campaign.allowed_channels
and campaign.can_earn(self, ignore_channel_status=True)
for campaign in self._twitch._campaigns.values()
)

def external_update(self, channel_data: JsonType, available_drops: list[JsonType]):
Expand Down
26 changes: 26 additions & 0 deletions tests/test_channel_watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@ async def __aexit__(self, *_args):


class ChannelWatchTest(unittest.IsolatedAsyncioTestCase):
def test_trusted_acl_campaign_is_accepted_when_available_drops_omits_it(self):
channel = object.__new__(Channel)
channel.id = 456
channel.acl_based = True

ewc = SimpleNamespace(
id="ewc",
allowed_channels=[channel],
can_earn=lambda *_args, **_kwargs: False,
)
rainbow_six = SimpleNamespace(
id="rainbow-six",
allowed_channels=[channel],
can_earn=lambda candidate, **kwargs: (
candidate is channel and kwargs["ignore_channel_status"]
),
)
channel._twitch = SimpleNamespace(
_campaigns={ewc.id: ewc, rainbow_six.id: rainbow_six},
settings=SimpleNamespace(trust_allowed_channels=False),
)

self.assertFalse(channel._check_drops_enabled([{"id": ewc.id}]))
channel._twitch.settings.trust_allowed_channels = True
self.assertTrue(channel._check_drops_enabled([{"id": ewc.id}]))

async def test_send_watch_posts_current_payload_to_spade(self):
twitch = SimpleNamespace(_auth_state=SimpleNamespace(user_id="789"))
twitch.request = lambda *args, **kwargs: (
Expand Down
3 changes: 3 additions & 0 deletions tests/test_tui_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ def make_app(self, state=None, *, on_ready=None, **callbacks):
on_set_priority_mode=callbacks.get("on_set_priority_mode", lambda mode: None),
on_set_farm_unlinked=callbacks.get("on_set_farm_unlinked", lambda enabled: None),
on_set_badges_emotes=callbacks.get("on_set_badges_emotes", lambda enabled: None),
on_set_trust_allowed_channels=callbacks.get(
"on_set_trust_allowed_channels", lambda enabled: None
),
on_ready=on_ready,
)

Expand Down
10 changes: 10 additions & 0 deletions tests/test_tui_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def __init__(self):
self.exclude = set()
self.farm_unlinked = False
self.enable_badges_emotes = False
self.trust_allowed_channels = False
self.priority_mode = PriorityMode.PRIORITY_ONLY
self.saved = False

Expand Down Expand Up @@ -148,6 +149,15 @@ def test_badges_command_updates_setting(self):
self.assertTrue(manager._twitch.settings.enable_badges_emotes)
self.assertTrue(manager._twitch.settings.saved)

def test_trust_allowed_command_updates_setting(self):
manager = self.make_manager()

manager._handle_command("/trust-allowed on")

self.assertTrue(manager.state.trust_allowed_channels)
self.assertTrue(manager._twitch.settings.trust_allowed_channels)
self.assertTrue(manager._twitch.settings.saved)

def test_print_logs_without_textual_app(self):
manager = self.make_manager()

Expand Down
15 changes: 15 additions & 0 deletions tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ def __init__(
on_set_priority_mode: abc.Callable[[str], None],
on_set_farm_unlinked: abc.Callable[[bool], None],
on_set_badges_emotes: abc.Callable[[bool], None],
on_set_trust_allowed_channels: abc.Callable[[bool], None],
on_invalidate_auth: abc.Callable[[], None] = lambda: None,
on_ready: abc.Callable[[], None] | None = None,
) -> None:
Expand All @@ -184,6 +185,7 @@ def __init__(
self._on_set_priority_mode = on_set_priority_mode
self._on_set_farm_unlinked = on_set_farm_unlinked
self._on_set_badges_emotes = on_set_badges_emotes
self._on_set_trust_allowed_channels = on_set_trust_allowed_channels
self._on_ready = on_ready or (lambda: None)
self._ready_for_refresh = False
self._syncing_settings = False
Expand Down Expand Up @@ -241,6 +243,11 @@ def compose(self) -> ComposeResult:
id="badges-emotes",
compact=True,
)
yield Checkbox(
"trust allowed channels",
id="trust-allowed-channels",
compact=True,
)
yield Static(
"Only for priority-only mode.",
id="farm-unlinked-note",
Expand Down Expand Up @@ -470,6 +477,10 @@ def _sync_settings_widgets(self) -> None:
if badges_emotes is not None:
badges_emotes.value = self.state.enable_badges_emotes

trust_allowed = self._widget("#trust-allowed-channels", Checkbox)
if trust_allowed is not None:
trust_allowed.value = self.state.trust_allowed_channels

game_select = self._widget("#game-select", Select)
if game_select is not None:
options = [(game, game) for game in self.state.available_games]
Expand Down Expand Up @@ -598,6 +609,10 @@ def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
if event.value != self.state.enable_badges_emotes:
self._on_set_badges_emotes(event.value)
return
if checkbox_id == "trust-allowed-channels":
if event.value != self.state.trust_allowed_channels:
self._on_set_trust_allowed_channels(event.value)
return
filters = self.state.campaign_filters
if checkbox_id == "filter-not-linked":
filters.show_not_linked = event.value
Expand Down
12 changes: 10 additions & 2 deletions tui/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ class PortableCLIManager(TUIManager):
"/badges",
"/badges on",
"/badges off",
"/trust-allowed",
"/trust-allowed on",
"/trust-allowed off",
"/detach",
"/help",
"/help navigation",
Expand Down Expand Up @@ -388,6 +391,8 @@ def _handle_command(self, raw: str) -> None:
self._set_farm_unlinked(rest.lower() in {"1", "on", "true", "yes"})
elif command in {"badges", "badges-emotes"}:
self._set_badges_emotes(rest.lower() in {"1", "on", "true", "yes"})
elif command == "trust-allowed":
self._set_trust_allowed_channels(rest.lower() in {"1", "on", "true", "yes"})
elif command == "detach":
self._detach_tmux()
elif command == "help":
Expand All @@ -408,7 +413,7 @@ def _completion_candidates(self, command: str) -> list[str]:
return list(self.state.exclude)
if command == "/switch":
return [channel.name for channel in self.state.channels.values()]
if command in {"/farm-unlinked", "/badges", "/filter not-linked", "/filter upcoming", "/filter expired", "/filter excluded", "/filter finished"}:
if command in {"/farm-unlinked", "/badges", "/trust-allowed", "/filter not-linked", "/filter upcoming", "/filter expired", "/filter excluded", "/filter finished"}:
return ["on", "off"]
if command == "/mode":
return ["priority-only", "ending-soonest", "low-availability"]
Expand Down Expand Up @@ -537,6 +542,7 @@ def _handle_filter(self, rest: str) -> None:
("/mode <mode>", "Set priority mode: priority-only, ending-soonest, low-availability"),
("/filter <name> <on|off>", "Toggle filters: not-linked, upcoming, expired, excluded, finished"),
("/farm-unlinked on|off", "Enable/disable farming unlinked drops (priority-only mode)"),
("/trust-allowed on|off", "Trust explicitly allowed channels when Twitch omits a campaign"),
],
"system": [
("/open", "Open the Twitch login URL in a browser (when login is pending)"),
Expand Down Expand Up @@ -769,6 +775,7 @@ def _rich_dashboard(self, width: int, available_height: int) -> str:
status_table.add_row("Websockets", Text(f"{spinner} {websockets} connected", style=_C_GREEN if websockets > 0 else _C_YELLOW))
status_table.add_row("Mode", Text(self.state.priority_mode, style=_C_AMBER))
status_table.add_row("Farm unlinked", Text("on" if self.state.farm_unlinked else "off", style=_C_GREEN if self.state.farm_unlinked else _C_DIM))
status_table.add_row("Trust allowed", Text("on" if self.state.trust_allowed_channels else "off", style=_C_GREEN if self.state.trust_allowed_channels else _C_DIM))

status_panel = Panel(status_table, title=f"[bold {_C_CYAN}]Status[/]", border_style=_C_PANEL_BORDER)

Expand Down Expand Up @@ -944,6 +951,7 @@ def _rich_settings(self, width: int) -> str:
table.add_row("Mode", Text(self.state.priority_mode, style=_C_AMBER))
table.add_row("Farm unlinked", Text("on" if self.state.farm_unlinked else "off", style=_C_GREEN if self.state.farm_unlinked else _C_DIM))
table.add_row("Badges/emotes", Text("on" if self.state.enable_badges_emotes else "off", style=_C_GREEN if self.state.enable_badges_emotes else _C_DIM))
table.add_row("Trust allowed", Text("on" if self.state.trust_allowed_channels else "off", style=_C_GREEN if self.state.trust_allowed_channels else _C_DIM))
table.add_row("Available", Text(f"{len(self.state.available_games)} games", style=_C_TEXT))

console.print(table)
Expand Down Expand Up @@ -1012,7 +1020,7 @@ def _rich_settings(self, width: int) -> str:
console.print(Text(" /priority add <game> /priority remove <game>", style=_C_DIM))
console.print(Text(" /priority bump <game> /priority demote <game>", style=_C_DIM))
console.print(Text(" /exclude add <game> /mode <name>", style=_C_DIM))
console.print(Text(" /farm-unlinked on|off /badges on|off", style=_C_DIM))
console.print(Text(" /farm-unlinked on|off /badges on|off /trust-allowed on|off", style=_C_DIM))

return console.file.getvalue() # type: ignore[union-attr]

Expand Down
9 changes: 9 additions & 0 deletions tui/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,13 +387,17 @@ def _update_settings_text(self) -> None:
)
self.state.farm_unlinked = bool(getattr(settings, "farm_unlinked", False))
self.state.enable_badges_emotes = bool(getattr(settings, "enable_badges_emotes", False))
self.state.trust_allowed_channels = bool(
getattr(settings, "trust_allowed_channels", False)
)
self.state.available_games = sorted(game.name for game in self._games)
self.state.settings_text = (
f"Mode: {self.state.priority_mode} | "
f"Priority: {len(self.state.priority)} | "
f"Exclude: {len(self.state.exclude)} | "
f"Farm unlinked: {self.state.farm_unlinked} | "
f"Badges/emotes: {self.state.enable_badges_emotes} | "
f"Trust allowed: {self.state.trust_allowed_channels} | "
f"Games: {len(self.state.available_games)}"
)
self.refresh_settings()
Expand All @@ -420,6 +424,7 @@ def start(self) -> None:
on_set_priority_mode=self._set_priority_mode,
on_set_farm_unlinked=self._set_farm_unlinked,
on_set_badges_emotes=self._set_badges_emotes,
on_set_trust_allowed_channels=self._set_trust_allowed_channels,
on_ready=self._mark_app_ready,
)
self._app_task = asyncio.create_task(self._app.run_async())
Expand Down Expand Up @@ -606,3 +611,7 @@ def _set_farm_unlinked(self, enabled: bool) -> None:
def _set_badges_emotes(self, enabled: bool) -> None:
self._twitch.settings.enable_badges_emotes = enabled
self._save_settings_update(f"Badge/emote drops set to {enabled}.")

def _set_trust_allowed_channels(self, enabled: bool) -> None:
self._twitch.settings.trust_allowed_channels = enabled
self._save_settings_update(f"Trust allowed channels set to {enabled}.")
1 change: 1 addition & 0 deletions tui/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class TUIState:
priority_mode: str = "Priority list only"
farm_unlinked: bool = False
enable_badges_emotes: bool = False
trust_allowed_channels: bool = False
campaign_filters: CampaignFilters = field(default_factory=CampaignFilters)
login: LoginSnapshot = field(default_factory=LoginSnapshot)
current_drop: DropSnapshot = field(default_factory=DropSnapshot)
Expand Down
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "20.0.Opinionated_By_HimanM"
__version__ = "20.1.Opinionated_By_HimanM"
1 change: 1 addition & 0 deletions web/frontend/src/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ function SettingsPanel({ draft, dirty, busy, session, notifications, notificatio
<Toggle label="Farm unlinked drops" description={priorityOnly ? "Farm campaigns that do not require a linked game account." : "Available only when Priority mode is Priority list only."} checked={priorityOnly && Boolean(draft.farm_unlinked)} disabled={!priorityOnly} onChange={(value) => onChange("farm_unlinked", value)} />
<Toggle label="Badge and emote drops" description="Include campaigns whose rewards are badges or emotes." checked={Boolean(draft.enable_badges_emotes)} onChange={(value) => onChange("enable_badges_emotes", value)} />
<Toggle label="Extra availability check" description="Run the additional Twitch availability lookup." checked={Boolean(draft.available_drops_check)} onChange={(value) => onChange("available_drops_check", value)} />
<Toggle label="Trust allowed channels" description="Use a campaign's explicit channel list when Twitch's availability lookup omits it." checked={Boolean(draft.trust_allowed_channels)} onChange={(value) => onChange("trust_allowed_channels", value)} />
</SettingsGroup>
<SettingsGroup title="Connection" icon={<GlobeIcon />}>
<Field label="Proxy URL" description="Optional HTTP or HTTPS proxy. Restart the miner after changing it."><Input value={draft.proxy || ""} placeholder="https://proxy.example:8080" onChange={(event) => onChange("proxy", event.target.value)} /></Field>
Expand Down
1 change: 1 addition & 0 deletions web/frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type Settings = {
farm_unlinked: boolean
enable_badges_emotes: boolean
available_drops_check: boolean
trust_allowed_channels: boolean
proxy: string
language: string
languages: string[]
Expand Down
8 changes: 6 additions & 2 deletions web/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,11 @@ def update_settings(self, payload: dict[str, Any]) -> None:
self._set_badges_emotes(payload["enable_badges_emotes"])
settings = self._twitch.settings
restart_keys = {"proxy", "language", "connection_quality"}
for key in restart_keys | {"available_drops_check"}:
channel_keys = {"available_drops_check", "trust_allowed_channels"}
for key in restart_keys | channel_keys:
if key in payload:
setattr(settings, key, URL(payload[key]) if key == "proxy" else payload[key])
if restart_keys & payload.keys() or "available_drops_check" in payload:
if (restart_keys | channel_keys) & payload.keys():
settings.save()
self._update_settings_text()
self.print("Server settings saved. Restart the miner to apply connection changes.")
Expand Down Expand Up @@ -168,6 +169,9 @@ def snapshot(self) -> dict[str, Any]:
"farm_unlinked": self.state.farm_unlinked,
"enable_badges_emotes": self.state.enable_badges_emotes,
"available_drops_check": bool(self._twitch.settings.available_drops_check),
"trust_allowed_channels": bool(
getattr(self._twitch.settings, "trust_allowed_channels", False)
),
"proxy": str(self._twitch.settings.proxy),
"language": self._twitch.settings.language,
"languages": ["English", *(path.stem for path in sorted(LANG_PATH.glob("*.json")))],
Expand Down
2 changes: 1 addition & 1 deletion web/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ async def update_settings(request: web.Request) -> web.Response:
if body["priority_mode"] not in manager.PRIORITY_MODE_LABELS.values():
raise ValueError("Invalid priority mode.")
clean["priority_mode"] = body["priority_mode"]
for key in ("farm_unlinked", "enable_badges_emotes"):
for key in ("farm_unlinked", "enable_badges_emotes", "trust_allowed_channels"):
if key in body:
if not isinstance(body[key], bool):
raise ValueError(f"{key} must be true or false.")
Expand Down
Loading
Loading