init
This commit is contained in:
121 files changed
+17641
No files matched your search
@@ -0,0 +1,165 @@
|
||||
"""Blocking policy.
|
||||
|
||||
The backend decides *whether* blocking should be on right now and *what* is on
|
||||
the list; the desktop app is the thing that actually enforces it (it is the only
|
||||
piece running on your machine with the privileges to rewrite `/etc/hosts` or
|
||||
kill a process). Keeping the policy here means the web UI can edit the rules,
|
||||
and a second machine running the desktop app obeys the same list.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import BlockEvent
|
||||
from app.services import pomodoro
|
||||
from app.settings_schema import AppSettings
|
||||
from app.timeutil import ensure_utc, in_window, local_date, local_time
|
||||
|
||||
|
||||
def expand_domains(settings: AppSettings) -> list[str]:
|
||||
"""Every hostname the desktop app should point at the redirect IP.
|
||||
|
||||
A rule like `youtube.com` is meant as "that site", so `www.` and the common
|
||||
subdomains come along; anything in `allowed_domains` is removed again, which
|
||||
is how `youtube.com` blocked + `music.youtube.com` allowed works.
|
||||
"""
|
||||
blocker = settings.blocker
|
||||
allowed = {entry.strip().lower().lstrip(".") for entry in blocker.allowed_domains if entry.strip()}
|
||||
|
||||
hosts: list[str] = []
|
||||
for raw in blocker.blocked_domains:
|
||||
domain = raw.strip().lower().lstrip(".")
|
||||
if not domain or domain in allowed:
|
||||
continue
|
||||
for candidate in (domain, f"www.{domain}", f"m.{domain}"):
|
||||
if candidate not in allowed and candidate not in hosts:
|
||||
hosts.append(candidate)
|
||||
return hosts
|
||||
|
||||
|
||||
def schedule_active(settings: AppSettings, moment: datetime | None = None) -> bool:
|
||||
now_time = local_time(settings.timezone, moment)
|
||||
weekday = (moment or datetime.now()).weekday()
|
||||
for window in settings.blocker.schedule:
|
||||
if weekday in window.days and in_window(now_time, window.start, window.end):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def policy(session: Session, settings: AppSettings) -> dict[str, Any]:
|
||||
"""What the desktop app polls: should I block, and what."""
|
||||
blocker = settings.blocker
|
||||
current = pomodoro.current_session(session)
|
||||
active = False
|
||||
reason = "disabled"
|
||||
|
||||
if blocker.enabled:
|
||||
if blocker.mode == "always":
|
||||
active, reason = True, "always on"
|
||||
elif blocker.mode == "scheduled":
|
||||
active = schedule_active(settings)
|
||||
reason = "inside a blocking window" if active else "outside every window"
|
||||
else: # during_work
|
||||
if current is not None and current.kind == "work" and current.status in ("running", "paused"):
|
||||
active, reason = True, "a work interval is running"
|
||||
elif (
|
||||
current is not None
|
||||
and current.kind != "work"
|
||||
and blocker.block_during_breaks
|
||||
and current.status in ("running", "paused")
|
||||
):
|
||||
active, reason = True, "blocking stays on through breaks"
|
||||
else:
|
||||
reason = "no work interval is running"
|
||||
|
||||
return {
|
||||
"active": active,
|
||||
"reason": reason,
|
||||
"mode": blocker.mode,
|
||||
"enabled": blocker.enabled,
|
||||
"strict": blocker.strict,
|
||||
"redirect_ip": blocker.redirect_ip,
|
||||
"domains": expand_domains(settings) if active else [],
|
||||
"apps": [app.strip() for app in blocker.blocked_apps if app.strip()] if active else [],
|
||||
"kill_apps": blocker.kill_blocked_apps,
|
||||
"notify_on_block": blocker.notify_on_block,
|
||||
"poll_seconds": blocker.app_poll_seconds,
|
||||
"session_id": current.id if current else None,
|
||||
"all_domains": expand_domains(settings),
|
||||
"all_apps": [app.strip() for app in blocker.blocked_apps if app.strip()],
|
||||
}
|
||||
|
||||
|
||||
def record_event(
|
||||
session: Session,
|
||||
settings: AppSettings,
|
||||
*,
|
||||
kind: str,
|
||||
target: str,
|
||||
action: str = "blocked",
|
||||
details: str | None = None,
|
||||
) -> BlockEvent:
|
||||
current = pomodoro.current_session(session)
|
||||
event = BlockEvent(
|
||||
kind=kind,
|
||||
target=target.strip().lower(),
|
||||
action=action,
|
||||
details=details,
|
||||
local_date=local_date(settings.timezone),
|
||||
session_id=current.id if current else None,
|
||||
)
|
||||
session.add(event)
|
||||
session.commit()
|
||||
return event
|
||||
|
||||
|
||||
def events_for_day(session: Session, settings: AppSettings, day: date | None = None) -> dict[str, Any]:
|
||||
day = day or local_date(settings.timezone)
|
||||
rows = list(session.scalars(select(BlockEvent).where(BlockEvent.local_date == day)))
|
||||
by_target = dict(
|
||||
session.execute(
|
||||
select(BlockEvent.target, func.count(BlockEvent.id))
|
||||
.where(BlockEvent.local_date == day)
|
||||
.group_by(BlockEvent.target)
|
||||
.order_by(func.count(BlockEvent.id).desc())
|
||||
).all()
|
||||
)
|
||||
return {
|
||||
"date": day.isoformat(),
|
||||
"total": len(rows),
|
||||
"by_target": [{"target": target, "count": count} for target, count in by_target.items()],
|
||||
"events": [
|
||||
{
|
||||
"id": row.id,
|
||||
"kind": row.kind,
|
||||
"target": row.target,
|
||||
"action": row.action,
|
||||
"at": ensure_utc(row.at).isoformat(),
|
||||
"details": row.details,
|
||||
}
|
||||
for row in sorted(rows, key=lambda row: ensure_utc(row.at), reverse=True)[:100]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_DOMAINS = [
|
||||
"youtube.com",
|
||||
"twitter.com",
|
||||
"x.com",
|
||||
"instagram.com",
|
||||
"tiktok.com",
|
||||
"reddit.com",
|
||||
"facebook.com",
|
||||
"vk.com",
|
||||
"twitch.tv",
|
||||
"netflix.com",
|
||||
"9gag.com",
|
||||
"pikabu.ru",
|
||||
]
|
||||
|
||||
DEFAULT_APPS = ["steam", "discord", "telegram-desktop", "spotify"]
|
||||
Reference in new issue
Block a user