Initial commit
This commit is contained in:
		
						commit
						705c85177a
					
				
							
								
								
									
										0
									
								
								api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										244
									
								
								api/app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								api/app.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,244 @@
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
 | 
			
		||||
from flask import Flask, request
 | 
			
		||||
 | 
			
		||||
from .data import DataFile, DateTimeSpan, MinuteSpan
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DATA = DataFile("data.json")
 | 
			
		||||
 | 
			
		||||
app = Flask(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_action_at(point_in_time, schedules, overrides):
 | 
			
		||||
    for override in overrides:
 | 
			
		||||
        if override.start <= point_in_time < override.end:
 | 
			
		||||
            return override.action
 | 
			
		||||
    for span in schedules[point_in_time.weekday()]:
 | 
			
		||||
        span = DateTimeSpan.from_minutespan(point_in_time, span)
 | 
			
		||||
        if span.start <= point_in_time < span.end:
 | 
			
		||||
            return True
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_next_action_change(point_in_time, action, schedules, overrides):
 | 
			
		||||
    skip_schedule = False
 | 
			
		||||
    if not any(schedules.values()):
 | 
			
		||||
        # No daily schedule is set
 | 
			
		||||
        overrides = [o for o in overrides if o.action]
 | 
			
		||||
        if not overrides:
 | 
			
		||||
            # No allow overrides exist => denied indefinitely
 | 
			
		||||
            return None
 | 
			
		||||
        skip_schedule = True
 | 
			
		||||
    elif all(o == [MinuteSpan(0, 24 * 60)] for o in schedules.values()):
 | 
			
		||||
        # Daily schedule allowed 24/7
 | 
			
		||||
        overrides = [o for o in overrides if not o.action]
 | 
			
		||||
        if not overrides:
 | 
			
		||||
            # No deny overrides exist => allowed indefinitely
 | 
			
		||||
            return None
 | 
			
		||||
        skip_schedule = True
 | 
			
		||||
    if skip_schedule:
 | 
			
		||||
        # Schedule is either empty or allows 24/7, therefore irrelevant
 | 
			
		||||
        # and only overrides are considered.
 | 
			
		||||
        for override_span in overrides:
 | 
			
		||||
            if override_span.start > point_in_time:
 | 
			
		||||
                # Nearest override didn't start yet
 | 
			
		||||
                return override_span.start
 | 
			
		||||
            if override_span.start <= point_in_time < override_span.end:
 | 
			
		||||
                # We're in override
 | 
			
		||||
                return override_span.end
 | 
			
		||||
        # No more overrides => allowed / denied indefinitely
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    while True:
 | 
			
		||||
        new_point_in_time = None
 | 
			
		||||
        for override_span in overrides:
 | 
			
		||||
            if override_span.start <= point_in_time < override_span.end:
 | 
			
		||||
                # We're in override
 | 
			
		||||
                new_point_in_time = override_span.end
 | 
			
		||||
        if new_point_in_time is None:
 | 
			
		||||
            dow = point_in_time.weekday()
 | 
			
		||||
            for span in schedules[dow]:
 | 
			
		||||
                span = DateTimeSpan.from_minutespan(point_in_time, span)
 | 
			
		||||
                if span.end < point_in_time:
 | 
			
		||||
                    # Skip already elapsed spans
 | 
			
		||||
                    continue
 | 
			
		||||
                if span.start <= point_in_time < span.end:
 | 
			
		||||
                    # We're in span allowed by schedule, get the span end
 | 
			
		||||
                    new_point_in_time = span.end
 | 
			
		||||
                    # Check if any upcoming override comes sooner that the span end
 | 
			
		||||
                    for override_span in overrides:
 | 
			
		||||
                        if override_span.start >= point_in_time and override_span.start < span.end:
 | 
			
		||||
                            new_point_in_time = override_span.start
 | 
			
		||||
                            break
 | 
			
		||||
                    break
 | 
			
		||||
        if new_point_in_time is None:
 | 
			
		||||
            # We're in span denied by schedule, get next span start
 | 
			
		||||
            days = 0
 | 
			
		||||
            while True:
 | 
			
		||||
                for span in schedules[(dow + days) % 7]:
 | 
			
		||||
                    span = DateTimeSpan.from_minutespan(point_in_time + timedelta(days=days), span)
 | 
			
		||||
                    if span.start > point_in_time:
 | 
			
		||||
                        new_point_in_time = span.start
 | 
			
		||||
                        # Check if any upcoming override comes sooner that the span start
 | 
			
		||||
                        for override_span in overrides:
 | 
			
		||||
                            if (
 | 
			
		||||
                                override_span.start >= point_in_time
 | 
			
		||||
                                and override_span.start < span.start
 | 
			
		||||
                            ):
 | 
			
		||||
                                new_point_in_time = override_span.start
 | 
			
		||||
                                break
 | 
			
		||||
                        break
 | 
			
		||||
                if new_point_in_time:
 | 
			
		||||
                    break
 | 
			
		||||
                days += 1
 | 
			
		||||
        if action != get_action_at(new_point_in_time, schedules, overrides):
 | 
			
		||||
            return new_point_in_time
 | 
			
		||||
        point_in_time = new_point_in_time
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_status(status, schedules, overrides):
 | 
			
		||||
    now = datetime.now()
 | 
			
		||||
    status.action = get_action_at(now, schedules, overrides)
 | 
			
		||||
    status.until = get_next_action_change(now, status.action, schedules, overrides)
 | 
			
		||||
    status.then_until = (
 | 
			
		||||
        get_next_action_change(status.until, not status.action, schedules, overrides)
 | 
			
		||||
        if status.until
 | 
			
		||||
        else None
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_schedule(schedule, span, allow):
 | 
			
		||||
    if allow:
 | 
			
		||||
        schedule.append(span)
 | 
			
		||||
        schedule.sort()
 | 
			
		||||
        i = len(schedule) - 1
 | 
			
		||||
        while i:
 | 
			
		||||
            if schedule[i - 1].end >= schedule[i].start:
 | 
			
		||||
                # spans overlap -> merge together
 | 
			
		||||
                schedule[i - 1].start = min(schedule[i - 1].start, schedule[i].start)
 | 
			
		||||
                schedule[i - 1].end = max(schedule[i - 1].end, schedule[i].end)
 | 
			
		||||
                del schedule[i]
 | 
			
		||||
            i -= 1
 | 
			
		||||
    else:
 | 
			
		||||
        i = len(schedule) - 1
 | 
			
		||||
        while i >= 0:
 | 
			
		||||
            if span.start <= schedule[i].start and span.end >= schedule[i].end:
 | 
			
		||||
                # deny period larger than span -> remove span
 | 
			
		||||
                del schedule[i]
 | 
			
		||||
            elif span.start > schedule[i].start and span.end < schedule[i].end:
 | 
			
		||||
                # deny period shorter than span -> split span into two
 | 
			
		||||
                schedule.append(MinuteSpan(span.end, schedule[i].end))
 | 
			
		||||
                schedule[i].end = span.start
 | 
			
		||||
            elif span.start <= schedule[i].start < span.end:
 | 
			
		||||
                # span starts within deny period -> move start
 | 
			
		||||
                schedule[i].start = span.end
 | 
			
		||||
            elif span.start < schedule[i].end <= span.end:
 | 
			
		||||
                # span ends within deny period -> move end
 | 
			
		||||
                schedule[i].end = span.start
 | 
			
		||||
            i -= 1
 | 
			
		||||
        schedule.sort()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_overrides(overrides, span):
 | 
			
		||||
    now = datetime.now()
 | 
			
		||||
    if span.end <= now:
 | 
			
		||||
        # The span is in the past, nothing to do
 | 
			
		||||
        return
 | 
			
		||||
    i = len(overrides) - 1
 | 
			
		||||
    while i >= 0:
 | 
			
		||||
        if span.start <= overrides[i].start and span.end >= overrides[i].end:
 | 
			
		||||
            # new override period larger than old override -> remove old override
 | 
			
		||||
            del overrides[i]
 | 
			
		||||
        elif span.start > overrides[i].start and span.end < overrides[i].end:
 | 
			
		||||
            if span.start > now:
 | 
			
		||||
                # new override period shorter than old override -> split old override into two
 | 
			
		||||
                overrides.append(DateTimeSpan(span.end, overrides[i].end, overrides[i].action))
 | 
			
		||||
                overrides[i].end = span.start
 | 
			
		||||
            else:
 | 
			
		||||
                # new override period would split the override, but the first part
 | 
			
		||||
                # has already elapsed -> move old override start
 | 
			
		||||
                overrides[i].start = span.end
 | 
			
		||||
        elif span.start <= overrides[i].start < span.end:
 | 
			
		||||
            # new override starts within old overrides period -> move old override start
 | 
			
		||||
            overrides[i].start = span.end
 | 
			
		||||
        elif span.start < overrides[i].end <= span.end:
 | 
			
		||||
            # new override ends within old overrides period -> move old override end
 | 
			
		||||
            overrides[i].end = span.start
 | 
			
		||||
        i -= 1
 | 
			
		||||
    overrides.append(span)
 | 
			
		||||
    overrides.sort()
 | 
			
		||||
    i = len(overrides) - 1
 | 
			
		||||
    while i:
 | 
			
		||||
        if (
 | 
			
		||||
            overrides[i - 1].end >= overrides[i].start
 | 
			
		||||
            and overrides[i - 1].action == overrides[i].action
 | 
			
		||||
        ):
 | 
			
		||||
            # overrides of the same type overlap -> merge together
 | 
			
		||||
            overrides[i - 1].start = min(overrides[i - 1].start, overrides[i].start)
 | 
			
		||||
            overrides[i - 1].end = max(overrides[i - 1].end, overrides[i].end)
 | 
			
		||||
            del overrides[i]
 | 
			
		||||
        i -= 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.get("/data")
 | 
			
		||||
def get_data():
 | 
			
		||||
    with DATA as data:
 | 
			
		||||
        if data.status.until and datetime.now() >= data.status.until:
 | 
			
		||||
            update_status(data.status, data.schedules, data.overrides)
 | 
			
		||||
            data.save()
 | 
			
		||||
        return data.to_dict()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.post("/override")
 | 
			
		||||
def post_override():
 | 
			
		||||
    span = DateTimeSpan.from_args(**request.json)
 | 
			
		||||
    with DATA as data:
 | 
			
		||||
        update_overrides(data.overrides, span)
 | 
			
		||||
        update_status(data.status, data.schedules, data.overrides)
 | 
			
		||||
        data.save()
 | 
			
		||||
        return data.to_dict()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.post("/delete-override")
 | 
			
		||||
def delete_override():
 | 
			
		||||
    span = DateTimeSpan.from_args(**request.json)
 | 
			
		||||
    with DATA as data:
 | 
			
		||||
        try:
 | 
			
		||||
            data.overrides.remove(span)
 | 
			
		||||
            update_status(data.status, data.schedules, data.overrides)
 | 
			
		||||
            data.save()
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
        return data.to_dict()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.post("/schedule")
 | 
			
		||||
def post_schedule():
 | 
			
		||||
    schedule = request.json
 | 
			
		||||
    span = MinuteSpan.from_args(**schedule)
 | 
			
		||||
    with DATA as data:
 | 
			
		||||
        for day in schedule["days"]:
 | 
			
		||||
            update_schedule(data.schedules[day], span, schedule["action"])
 | 
			
		||||
        update_status(data.status, data.schedules, data.overrides)
 | 
			
		||||
        data.save()
 | 
			
		||||
        return data.to_dict()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.get("/fast-action/<int:minutes>")
 | 
			
		||||
def fast_action(minutes):
 | 
			
		||||
    with DATA as data:
 | 
			
		||||
        now = datetime.now().replace(second=0, microsecond=0)
 | 
			
		||||
        if minutes:
 | 
			
		||||
            start = now
 | 
			
		||||
            action = get_action_at(now, data.schedules, data.overrides)
 | 
			
		||||
            if action:
 | 
			
		||||
                start = get_next_action_change(now, action, data.schedules, data.overrides)
 | 
			
		||||
            end = start + timedelta(minutes=minutes)
 | 
			
		||||
            span = DateTimeSpan(start, end, True)
 | 
			
		||||
        else:
 | 
			
		||||
            span = DateTimeSpan(now, now + timedelta(minutes=5), False)
 | 
			
		||||
        update_overrides(data.overrides, span)
 | 
			
		||||
        update_status(data.status, data.schedules, data.overrides)
 | 
			
		||||
        data.save()
 | 
			
		||||
        return data.to_dict()
 | 
			
		||||
							
								
								
									
										144
									
								
								api/data.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								api/data.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,144 @@
 | 
			
		||||
import fcntl
 | 
			
		||||
import json
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MINS = "minutes"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Span:
 | 
			
		||||
    def __init__(self, start, end):
 | 
			
		||||
        self.start = min(start, end)
 | 
			
		||||
        self.end = max(start, end)
 | 
			
		||||
 | 
			
		||||
    def __eq__(self, other):
 | 
			
		||||
        return (self.start, self.end) == (other.start, other.end)
 | 
			
		||||
 | 
			
		||||
    def __lt__(self, other):
 | 
			
		||||
        return (self.start, self.end) < (other.start, other.end)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MinuteSpan(Span):
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_args(cls, **kwargs):
 | 
			
		||||
        start = kwargs["start"].split(":")
 | 
			
		||||
        start = int(start[0]) * 60 + int(start[1])
 | 
			
		||||
        end = kwargs["end"].split(":")
 | 
			
		||||
        end = int(end[0]) * 60 + int(end[1])
 | 
			
		||||
        if end == 0:
 | 
			
		||||
            # To 00:00 should mean until midnight (24:00)
 | 
			
		||||
            end = 24 * 60
 | 
			
		||||
        return cls(start, end)
 | 
			
		||||
 | 
			
		||||
    def to_dict(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "start": f"{self.start // 60}:{self.start % 60:02}",
 | 
			
		||||
            "end": f"{self.end // 60}:{self.end % 60:02}",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DateTimeSpan(Span):
 | 
			
		||||
    def __init__(self, start, end, action):
 | 
			
		||||
        super().__init__(start, end)
 | 
			
		||||
        self.action = action
 | 
			
		||||
 | 
			
		||||
    def __eq__(self, other):
 | 
			
		||||
        return (self.start, self.end, self.action) == (other.start, other.end, other.action)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_args(cls, **kwargs):
 | 
			
		||||
        return cls(
 | 
			
		||||
            datetime.fromisoformat(kwargs["start"]),
 | 
			
		||||
            datetime.fromisoformat(kwargs["end"]),
 | 
			
		||||
            kwargs["action"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_minutespan(cls, date, minutespan):
 | 
			
		||||
        start = minutespan.start
 | 
			
		||||
        end = minutespan.end
 | 
			
		||||
        add_days = 0
 | 
			
		||||
        if end == 24 * 60:
 | 
			
		||||
            # Ending midnight needs to be set as 0:00 of the next day
 | 
			
		||||
            end = 0
 | 
			
		||||
            add_days = 1
 | 
			
		||||
        return cls(
 | 
			
		||||
            date.replace(hour=start // 60, minute=start % 60, second=0, microsecond=0),
 | 
			
		||||
            date.replace(hour=end // 60, minute=end % 60, second=0, microsecond=0)
 | 
			
		||||
            + timedelta(days=add_days),
 | 
			
		||||
            True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def to_dict(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "start": self.start.isoformat(timespec=MINS),
 | 
			
		||||
            "end": self.end.isoformat(timespec=MINS),
 | 
			
		||||
            "action": self.action,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Status:
 | 
			
		||||
    def __init__(self, action, until=None, then_until=None):
 | 
			
		||||
        self.action = action
 | 
			
		||||
        self.until = until
 | 
			
		||||
        self.then_until = then_until
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_args(cls, **kwargs):
 | 
			
		||||
        until = kwargs.get("until")
 | 
			
		||||
        then_until = kwargs.get("then_until")
 | 
			
		||||
        return cls(
 | 
			
		||||
            kwargs["action"],
 | 
			
		||||
            datetime.fromisoformat(until) if until else None,
 | 
			
		||||
            datetime.fromisoformat(then_until) if then_until else None,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def to_dict(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "action": self.action,
 | 
			
		||||
            "until": self.until.isoformat(timespec=MINS) if self.until else None,
 | 
			
		||||
            "then_until": self.then_until.isoformat(timespec=MINS) if self.then_until else None,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DataFile:
 | 
			
		||||
    def __init__(self, path):
 | 
			
		||||
        self._path = Path(path)
 | 
			
		||||
        self._file = None
 | 
			
		||||
        self.status = None
 | 
			
		||||
        self.schedules = None
 | 
			
		||||
        self.overrides = None
 | 
			
		||||
 | 
			
		||||
    def __enter__(self):
 | 
			
		||||
        self._file = self._path.open("r+", encoding="utf-8")
 | 
			
		||||
        fcntl.flock(self._file, fcntl.LOCK_EX)
 | 
			
		||||
        data = json.load(self._file)
 | 
			
		||||
        self.status = Status.from_args(**data.get("status", {"action": False}))
 | 
			
		||||
        self.schedules = {
 | 
			
		||||
            int(day): [MinuteSpan.from_args(**span) for span in schedule]
 | 
			
		||||
            for day, schedule in data.get("schedules", {day: [] for day in range(7)}).items()
 | 
			
		||||
        }
 | 
			
		||||
        overrides = [DateTimeSpan.from_args(**override) for override in data.get("overrides", [])]
 | 
			
		||||
        # Remove expired overrides
 | 
			
		||||
        now = datetime.now()
 | 
			
		||||
        self.overrides = [o for o in overrides if o.end > now]
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def __exit__(self, exc_type, exc_value, traceback):
 | 
			
		||||
        self._file.close()
 | 
			
		||||
 | 
			
		||||
    def to_dict(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "status": self.status.to_dict(),
 | 
			
		||||
            "schedules": {
 | 
			
		||||
                day: [span.to_dict() for span in schedule]
 | 
			
		||||
                for day, schedule in self.schedules.items()
 | 
			
		||||
            },
 | 
			
		||||
            "overrides": [override.to_dict() for override in self.overrides],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
        self._file.seek(0)
 | 
			
		||||
        self._file.truncate(0)
 | 
			
		||||
        json.dump(self.to_dict(), self._file)
 | 
			
		||||
							
								
								
									
										5
									
								
								client/Installer/Installer.wixproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								client/Installer/Installer.wixproj
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
<Project Sdk="WixToolset.Sdk/4.0.0">
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <ProjectReference Include="..\ParentalControlService\ParentalControlService.csproj" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
							
								
								
									
										41
									
								
								client/Installer/Package.wxs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								client/Installer/Package.wxs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
 | 
			
		||||
  <Package Name="Marek Parental Control" Manufacturer="Disassembler" Version="1.0.0.0" UpgradeCode="feb429a6-684e-4506-816f-006384136754" Compressed="true">
 | 
			
		||||
	  <!-- Allow upgrades and prevent downgrades -->
 | 
			
		||||
	  <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." />
 | 
			
		||||
 | 
			
		||||
	  <!-- Define the directory structure -->
 | 
			
		||||
	  <StandardDirectory Id="ProgramFiles64Folder">
 | 
			
		||||
		  <Directory Id="INSTALLDIR" Name="Marek Parental Control" />
 | 
			
		||||
	  </StandardDirectory>
 | 
			
		||||
 | 
			
		||||
	  <DirectoryRef Id="INSTALLDIR">
 | 
			
		||||
		  <Component Id="ServiceExecutable" Bitness="always64">
 | 
			
		||||
			  <File Id="ParentalControlService.exe"
 | 
			
		||||
					Source="$(var.ParentalControlService.TargetDir)publish\ParentalControlService.exe"
 | 
			
		||||
					KeyPath="true" />
 | 
			
		||||
 | 
			
		||||
			  <RemoveFile Id="ALLFILES" Name="*.*" On="both" />
 | 
			
		||||
 | 
			
		||||
			  <ServiceInstall Id="ServiceInstaller"
 | 
			
		||||
							  Type="ownProcess"
 | 
			
		||||
							  Name="ParentalControlService"
 | 
			
		||||
							  DisplayName="Marek Parental Control"
 | 
			
		||||
							  Description="Marek Parental Control"
 | 
			
		||||
							  Start="auto"
 | 
			
		||||
							  ErrorControl="normal" />
 | 
			
		||||
 | 
			
		||||
			  <ServiceControl Id="StartService"
 | 
			
		||||
							  Start="install"
 | 
			
		||||
							  Stop="both"
 | 
			
		||||
							  Remove="uninstall"
 | 
			
		||||
							  Name="ParentalControlService"
 | 
			
		||||
							  Wait="true" />
 | 
			
		||||
		  </Component>
 | 
			
		||||
	  </DirectoryRef>
 | 
			
		||||
 | 
			
		||||
	  <Feature Id="Service" Title="Marek Parental Control Setup" Level="1">
 | 
			
		||||
		  <ComponentRef Id="ServiceExecutable" />
 | 
			
		||||
	  </Feature>
 | 
			
		||||
 | 
			
		||||
  </Package>
 | 
			
		||||
</Wix>
 | 
			
		||||
							
								
								
									
										61
									
								
								client/ParentalControlService.sln
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								client/ParentalControlService.sln
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
 | 
			
		||||
Microsoft Visual Studio Solution File, Format Version 12.00
 | 
			
		||||
# Visual Studio Version 17
 | 
			
		||||
VisualStudioVersion = 17.7.34024.191
 | 
			
		||||
MinimumVisualStudioVersion = 10.0.40219.1
 | 
			
		||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParentalControlService", "ParentalControlService\ParentalControlService.csproj", "{050D342F-0A53-4A08-B56A-AA63A22A74A9}"
 | 
			
		||||
EndProject
 | 
			
		||||
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Installer", "Installer\Installer.wixproj", "{C4838DE1-3AC8-4931-80C2-340EBA94996F}"
 | 
			
		||||
EndProject
 | 
			
		||||
Global
 | 
			
		||||
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 | 
			
		||||
		Debug|Any CPU = Debug|Any CPU
 | 
			
		||||
		Debug|ARM64 = Debug|ARM64
 | 
			
		||||
		Debug|x64 = Debug|x64
 | 
			
		||||
		Debug|x86 = Debug|x86
 | 
			
		||||
		Release|Any CPU = Release|Any CPU
 | 
			
		||||
		Release|ARM64 = Release|ARM64
 | 
			
		||||
		Release|x64 = Release|x64
 | 
			
		||||
		Release|x86 = Release|x86
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|ARM64.Build.0 = Debug|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x64.ActiveCfg = Debug|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x64.Build.0 = Debug|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x86.ActiveCfg = Debug|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x86.Build.0 = Debug|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|Any CPU.Build.0 = Release|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|ARM64.ActiveCfg = Release|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|ARM64.Build.0 = Release|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x64.ActiveCfg = Release|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x64.Build.0 = Release|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x86.ActiveCfg = Release|Any CPU
 | 
			
		||||
		{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x86.Build.0 = Release|Any CPU
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|Any CPU.ActiveCfg = Debug|x64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|Any CPU.Build.0 = Debug|x64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|ARM64.ActiveCfg = Debug|ARM64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|ARM64.Build.0 = Debug|ARM64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x64.ActiveCfg = Debug|x64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x64.Build.0 = Debug|x64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x86.ActiveCfg = Debug|x86
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x86.Build.0 = Debug|x86
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|Any CPU.ActiveCfg = Release|x64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|Any CPU.Build.0 = Release|x64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|ARM64.ActiveCfg = Release|ARM64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|ARM64.Build.0 = Release|ARM64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x64.ActiveCfg = Release|x64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x64.Build.0 = Release|x64
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x86.ActiveCfg = Release|x86
 | 
			
		||||
		{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x86.Build.0 = Release|x86
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(SolutionProperties) = preSolution
 | 
			
		||||
		HideSolutionNode = FALSE
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(ExtensibilityGlobals) = postSolution
 | 
			
		||||
		SolutionGuid = {E4EF7269-DAC7-4C33-9AF1-FCAEBAE0AD77}
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
EndGlobal
 | 
			
		||||
							
								
								
									
										69
									
								
								client/ParentalControlService/Data.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								client/ParentalControlService/Data.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,69 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace ParentalControlService {
 | 
			
		||||
    public struct Status
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("action")]
 | 
			
		||||
        public bool Action { get; set; }
 | 
			
		||||
        [JsonPropertyName("until")] 
 | 
			
		||||
        public DateTime? Until { get; set; }
 | 
			
		||||
        [JsonPropertyName("then_until")] 
 | 
			
		||||
        public DateTime? ThenUntil { get; set; }
 | 
			
		||||
 | 
			
		||||
        public override bool Equals(object? obj) {
 | 
			
		||||
            return obj is Status other && Equals(other);
 | 
			
		||||
        }
 | 
			
		||||
        public readonly bool Equals(Status other) {
 | 
			
		||||
            return Action == other.Action && Until == other.Until && ThenUntil == other.ThenUntil;
 | 
			
		||||
        }
 | 
			
		||||
        public override readonly int GetHashCode() {
 | 
			
		||||
            return HashCode.Combine(Action, Until, ThenUntil);
 | 
			
		||||
        }
 | 
			
		||||
        public static bool operator ==(Status lhs, Status rhs) {
 | 
			
		||||
            return lhs.Action == rhs.Action && lhs.Until == rhs.Until && lhs.ThenUntil == rhs.ThenUntil;
 | 
			
		||||
        }
 | 
			
		||||
        public static bool operator !=(Status lhs, Status rhs) {
 | 
			
		||||
            return !(lhs == rhs);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public struct Schedule
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("start")] 
 | 
			
		||||
        public string? Start { get; set; }
 | 
			
		||||
        [JsonPropertyName("end")]
 | 
			
		||||
        public string? End { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public struct Override
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("start")] 
 | 
			
		||||
        public DateTime Start { get; set; }
 | 
			
		||||
        [JsonPropertyName("end")] 
 | 
			
		||||
        public DateTime End { get; set; }
 | 
			
		||||
        [JsonPropertyName("action")]
 | 
			
		||||
        public bool Action { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public struct Data
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("status")] 
 | 
			
		||||
        public Status Status { get; set; }
 | 
			
		||||
        [JsonPropertyName("schedules")]
 | 
			
		||||
        public Dictionary<int,List<Schedule>> Schedules { get; set; }
 | 
			
		||||
        [JsonPropertyName("overrides")] 
 | 
			
		||||
        public List<Override> Overrides { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public struct Config
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("url")]
 | 
			
		||||
        public string Url { get; set; }
 | 
			
		||||
        [JsonPropertyName("username")]
 | 
			
		||||
        public string Username { get; set; }
 | 
			
		||||
        [JsonPropertyName("password")]
 | 
			
		||||
        public string Password { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								client/ParentalControlService/ParentalControlService.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								client/ParentalControlService/ParentalControlService.csproj
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>net7.0-windows</TargetFramework>
 | 
			
		||||
    <Nullable>enable</Nullable>
 | 
			
		||||
    <ImplicitUsings>disable</ImplicitUsings>
 | 
			
		||||
    <UserSecretsId>dotnet-ParentalControlService-c499de7d-03ae-4d69-a7cf-e634d0dce29d</UserSecretsId>
 | 
			
		||||
	<OutputType>exe</OutputType>
 | 
			
		||||
	<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
 | 
			
		||||
	<RuntimeIdentifier>win-x64</RuntimeIdentifier>
 | 
			
		||||
	<PlatformTarget>x64</PlatformTarget>
 | 
			
		||||
	<Version>1.1.0</Version>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.1" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
@ -0,0 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <_LastSelectedProfileId>C:\Users\Disassembler\Desktop\client\ParentalControlService\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
							
								
								
									
										134
									
								
								client/ParentalControlService/ParentalControlSvc.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								client/ParentalControlService/ParentalControlSvc.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,134 @@
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using System.Net.Http.Headers;
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using System.Timers;
 | 
			
		||||
using Timer = System.Timers.Timer;
 | 
			
		||||
 | 
			
		||||
namespace ParentalControlService;
 | 
			
		||||
 | 
			
		||||
public class ParentalControlSvc : BackgroundService
 | 
			
		||||
{
 | 
			
		||||
    private readonly string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json"); 
 | 
			
		||||
    private readonly string statusPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "status.json");
 | 
			
		||||
    private readonly ILogger<ParentalControlSvc> _logger;
 | 
			
		||||
    private Config config;
 | 
			
		||||
    private Status status;
 | 
			
		||||
    private bool currentAction;
 | 
			
		||||
    private readonly Timer checkTimer;
 | 
			
		||||
    private readonly HttpClient httpClient;
 | 
			
		||||
 | 
			
		||||
    public ParentalControlSvc(ILogger<ParentalControlSvc> logger)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
 | 
			
		||||
        // Load status
 | 
			
		||||
        try {
 | 
			
		||||
            status = JsonSerializer.Deserialize<Status>(File.ReadAllText(statusPath))!;
 | 
			
		||||
        }
 | 
			
		||||
        catch (FileNotFoundException) {
 | 
			
		||||
            status = new Status() { Action = true, Until = DateTime.MaxValue };
 | 
			
		||||
        }
 | 
			
		||||
        currentAction = status.Action;
 | 
			
		||||
 | 
			
		||||
        // Load config
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            config = JsonSerializer.Deserialize<Config>(File.ReadAllText(configPath))!;
 | 
			
		||||
        }
 | 
			
		||||
        catch (FileNotFoundException)
 | 
			
		||||
        {
 | 
			
		||||
            config = new Config() { Username = "", Password = "" };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create HTTP Client
 | 
			
		||||
        httpClient = new() { BaseAddress = new Uri(config.Url), Timeout = TimeSpan.FromSeconds(5) };
 | 
			
		||||
        string credentials = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{config.Username}:{config.Password}"));
 | 
			
		||||
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
 | 
			
		||||
        httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("ParentalControl", "1.1.0"));
 | 
			
		||||
 | 
			
		||||
        // Initialize Timer
 | 
			
		||||
        checkTimer = new(5000);
 | 
			
		||||
        checkTimer.Elapsed += new ElapsedEventHandler(CheckTimerTick);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            await CheckTimerTick();
 | 
			
		||||
            checkTimer.Start();
 | 
			
		||||
            while (!stoppingToken.IsCancellationRequested) {
 | 
			
		||||
                await Task.Delay(1000, stoppingToken);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (TaskCanceledException) {
 | 
			
		||||
            checkTimer.Stop();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex) {
 | 
			
		||||
            _logger.LogError(ex, "{Message}", ex.Message);
 | 
			
		||||
            Environment.Exit(1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void SaveStatus() {
 | 
			
		||||
        File.WriteAllText(statusPath, JsonSerializer.Serialize(status));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void RunScript(string scriptName) {
 | 
			
		||||
        scriptName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{scriptName}.ps1");
 | 
			
		||||
        if (!File.Exists(scriptName)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        ProcessStartInfo psi = new() {
 | 
			
		||||
            FileName = "powershell.exe",
 | 
			
		||||
            Arguments = $"-ExecutionPolicy Bypass -File \"{scriptName}\"",
 | 
			
		||||
            UseShellExecute = false,
 | 
			
		||||
            WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory,
 | 
			
		||||
        };
 | 
			
		||||
        Process.Start(psi);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task CheckTimerTick() {
 | 
			
		||||
        try {
 | 
			
		||||
            Data data = await httpClient.GetFromJsonAsync<Data>("api/data");
 | 
			
		||||
            if (data.Status != status) {
 | 
			
		||||
                status = data.Status;
 | 
			
		||||
                SaveStatus();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) {
 | 
			
		||||
            _logger.LogError(ex, "{Message}", ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
        if (status.Until != null && DateTime.Now >= status.Until) {
 | 
			
		||||
            // Fetch failed, we need to advance the schedule on our own
 | 
			
		||||
            status.Action = !status.Action;
 | 
			
		||||
            status.Until = status.ThenUntil;
 | 
			
		||||
            status.ThenUntil = null;
 | 
			
		||||
            SaveStatus();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (currentAction == status.Action) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        currentAction = !currentAction;
 | 
			
		||||
        if (currentAction) {
 | 
			
		||||
            _logger.LogInformation("[{time}] Unblocked", DateTime.Now);
 | 
			
		||||
            RunScript("unblock");
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            _logger.LogInformation("[{time}] Blocked", DateTime.Now);
 | 
			
		||||
            RunScript("block");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async void CheckTimerTick(object? sender, ElapsedEventArgs e) {
 | 
			
		||||
        await CheckTimerTick();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								client/ParentalControlService/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								client/ParentalControlService/Program.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Logging.Configuration;
 | 
			
		||||
using Microsoft.Extensions.Logging.EventLog;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using ParentalControlService;
 | 
			
		||||
 | 
			
		||||
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
 | 
			
		||||
builder.Services.AddWindowsService(options => {
 | 
			
		||||
    options.ServiceName = "Marek Parental Control";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
LoggerProviderOptions.RegisterProviderOptions<EventLogSettings, EventLogLoggerProvider>(builder.Services);
 | 
			
		||||
 | 
			
		||||
builder.Services.AddSingleton<ParentalControlSvc>();
 | 
			
		||||
builder.Services.AddHostedService<ParentalControlSvc>();
 | 
			
		||||
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
 | 
			
		||||
 | 
			
		||||
IHost host = builder.Build();
 | 
			
		||||
host.Run();
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!--
 | 
			
		||||
https://go.microsoft.com/fwlink/?LinkID=208121.
 | 
			
		||||
-->
 | 
			
		||||
<Project>
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <Configuration>Release</Configuration>
 | 
			
		||||
    <Platform>Any CPU</Platform>
 | 
			
		||||
    <PublishDir>bin\Release\net7.0-windows\win-x64\publish\</PublishDir>
 | 
			
		||||
    <PublishProtocol>FileSystem</PublishProtocol>
 | 
			
		||||
    <_TargetId>Folder</_TargetId>
 | 
			
		||||
    <TargetFramework>net7.0-windows</TargetFramework>
 | 
			
		||||
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
 | 
			
		||||
    <SelfContained>true</SelfContained>
 | 
			
		||||
    <PublishReadyToRun>true</PublishReadyToRun>
 | 
			
		||||
    <PublishSingleFile>true</PublishSingleFile>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
							
								
								
									
										11
									
								
								client/ParentalControlService/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								client/ParentalControlService/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
  "profiles": {
 | 
			
		||||
    "ParentalControlService": {
 | 
			
		||||
      "commandName": "Project",
 | 
			
		||||
      "dotnetRunMessages": true,
 | 
			
		||||
      "environmentVariables": {
 | 
			
		||||
        "DOTNET_ENVIRONMENT": "Development"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "Logging": {
 | 
			
		||||
    "LogLevel": {
 | 
			
		||||
      "Default": "Information",
 | 
			
		||||
      "Microsoft.Hosting.Lifetime": "Information"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								client/ParentalControlService/appsettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								client/ParentalControlService/appsettings.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "Logging": {
 | 
			
		||||
    "LogLevel": {
 | 
			
		||||
      "Default": "Information",
 | 
			
		||||
      "Microsoft.Hosting.Lifetime": "Information"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								client/block.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								client/block.ps1
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
Start-Sleep 1
 | 
			
		||||
C:\Windows\System32\net.exe user Marek /active:no
 | 
			
		||||
$session_id = ((C:\Windows\System32\quser.exe | Where-Object {$_ -match 'Marek' }) -split ' +')[2]
 | 
			
		||||
If ($session_id) {
 | 
			
		||||
    logoff $session_id
 | 
			
		||||
    Start-Sleep 1
 | 
			
		||||
    shutdown /s /f /t 0
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								client/config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								client/config.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"url": "https://pc.dasm.cz",
 | 
			
		||||
	"username": "parent",
 | 
			
		||||
	"password": "DA2ptE{!my_hSN6="
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								client/unblock.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								client/unblock.ps1
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
C:\Windows\System32\net.exe user Marek /active:yes
 | 
			
		||||
							
								
								
									
										1
									
								
								data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								data.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
{"schedules": {"0": [], "1": [], "2": [], "3": [], "4": [], "5": [], "6": []}, "overrides": []}
 | 
			
		||||
							
								
								
									
										71
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="cs">
 | 
			
		||||
    <head>
 | 
			
		||||
        <title>Parental Control</title>
 | 
			
		||||
        <meta charset="UTF-8">
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
        <link rel="icon" href="data:;base64,iVBORw0KGgo=">
 | 
			
		||||
        <link rel="stylesheet" href="static/style.css" type="text/css" media="all">
 | 
			
		||||
        <script type="text/javascript" src="static/script.js"></script>
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        <div>
 | 
			
		||||
            <h2>Stav</h2>
 | 
			
		||||
            <div id="status"> </div>
 | 
			
		||||
            <input class="fast-action" data-mins="15" type="button" value="+15 min">
 | 
			
		||||
            <input class="fast-action" data-mins="30" type="button" value="+30 min">
 | 
			
		||||
            <input class="fast-action" data-mins="60" type="button" value="+1 hod">
 | 
			
		||||
            <input class="fast-action" data-mins="0" type="button" value="Vypnout hned">
 | 
			
		||||
        </div>
 | 
			
		||||
        <div>
 | 
			
		||||
            <h2>Výjimky</h2>
 | 
			
		||||
            <div id="overrides"></div>
 | 
			
		||||
            <form id="add-override">
 | 
			
		||||
                <label for="override-action">Akce:</label>
 | 
			
		||||
                <select id="override-action">
 | 
			
		||||
                    <option value="allow">Povolit</option>
 | 
			
		||||
                    <option value="deny">Zakázat</option>
 | 
			
		||||
                </select><br>
 | 
			
		||||
                <label for="override-start">Od:</label>
 | 
			
		||||
                <input id="override-start" type="datetime-local"><br>
 | 
			
		||||
                <label for="override-end">Do:</label>
 | 
			
		||||
                <input id="override-end" type="datetime-local"><br>
 | 
			
		||||
                <input type="submit" value="Přidat">
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div>
 | 
			
		||||
            <h2>Plán</h2>
 | 
			
		||||
            <div id="schedules">
 | 
			
		||||
                <span>Pondělí:</span><span id="schedule-0"></span>
 | 
			
		||||
                <span>Úterý:</span><span id="schedule-1"></span>
 | 
			
		||||
                <span>Středa:</span><span id="schedule-2"></span>
 | 
			
		||||
                <span>Čtvrtek:</span><span id="schedule-3"></span>
 | 
			
		||||
                <span>Pátek:</span><span id="schedule-4"></span>
 | 
			
		||||
                <span>Sobota:</span><span id="schedule-5"></span>
 | 
			
		||||
                <span>Neděle:</span><span id="schedule-6"></span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <form id="update-schedule">
 | 
			
		||||
                <label for="schedule-action">Akce:</label>
 | 
			
		||||
                <select id="schedule-action">
 | 
			
		||||
                    <option value="allow">Povolit</option>
 | 
			
		||||
                    <option value="deny">Zakázat</option>
 | 
			
		||||
                </select>
 | 
			
		||||
                <label for="schedule-days">Den:</label>
 | 
			
		||||
                <select id="schedule-days" size="7" multiple>
 | 
			
		||||
                    <option value="0">Pondělí</option>
 | 
			
		||||
                    <option value="1">Úterý</option>
 | 
			
		||||
                    <option value="2">Středa</option>
 | 
			
		||||
                    <option value="3">Čtvrtek</option>
 | 
			
		||||
                    <option value="4">Pátek</option>
 | 
			
		||||
                    <option value="5">Sobota</option>
 | 
			
		||||
                    <option value="6">Neděle</option>
 | 
			
		||||
                </select>
 | 
			
		||||
                <label for="schedule-start">Od:</label>
 | 
			
		||||
                <input id="schedule-start" type="time">
 | 
			
		||||
                <label for="schedule-end">Do:</label>
 | 
			
		||||
                <input id="schedule-end" type="time">
 | 
			
		||||
                <input type="submit" value="Přidat">
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										18
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
[build-system]
 | 
			
		||||
requires = ["setuptools"]
 | 
			
		||||
build-backend = "setuptools.build_meta"
 | 
			
		||||
 | 
			
		||||
[project]
 | 
			
		||||
name = "parental-control"
 | 
			
		||||
version = "0.0.1"
 | 
			
		||||
authors = [
 | 
			
		||||
    {name = "Disassembler", email = "disassembler@dasm.cz"},
 | 
			
		||||
]
 | 
			
		||||
requires-python = ">=3.6"
 | 
			
		||||
license = {text = "MIT"}
 | 
			
		||||
dependencies = [
 | 
			
		||||
    "flask",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[tool.setuptools]
 | 
			
		||||
packages = ["api"]
 | 
			
		||||
							
								
								
									
										122
									
								
								static/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								static/script.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,122 @@
 | 
			
		||||
function humanReadableDate(str) {
 | 
			
		||||
    let date = new Date();
 | 
			
		||||
    let today = date.toISOString().split("T")[0];
 | 
			
		||||
    date.setDate(date.getDate() - 1);
 | 
			
		||||
    let yesterday = date.toISOString().split("T")[0];
 | 
			
		||||
    date.setDate(date.getDate() + 2);
 | 
			
		||||
    let tomorrow = date.toISOString().split("T")[0];
 | 
			
		||||
    return str.replace("T", " ").replaceAll(yesterday, "včera").replaceAll(today, "dnes").replaceAll(tomorrow, "zítra");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function refreshStatus(data) {
 | 
			
		||||
    let status = document.getElementById("status");
 | 
			
		||||
    let action, then_action;
 | 
			
		||||
    if (data["action"]) {
 | 
			
		||||
        action = "Povoleno"
 | 
			
		||||
        then_action = "zakázáno"
 | 
			
		||||
    } else {
 | 
			
		||||
        action = "Zakázáno"
 | 
			
		||||
        then_action = "povoleno"
 | 
			
		||||
    }
 | 
			
		||||
    let text = action;
 | 
			
		||||
    if (data["until"]) {
 | 
			
		||||
        text += " do " + humanReadableDate(data["until"]) + ", pak " + then_action;
 | 
			
		||||
        if (data["then_until"]) {
 | 
			
		||||
            text += " do " + humanReadableDate(data["then_until"]);
 | 
			
		||||
        } else {
 | 
			
		||||
            text += " napořád"
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        text += " napořád"
 | 
			
		||||
    }
 | 
			
		||||
    status.textContent = text;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function refreshOverrides(data) {
 | 
			
		||||
    let overrides = document.getElementById("overrides");
 | 
			
		||||
    overrides.innerHTML = "";
 | 
			
		||||
    for (const record of data) {
 | 
			
		||||
        let overrideText = document.createElement("span");
 | 
			
		||||
        overrideText.textContent = (record["action"] ? "Povoleno" : "Zakázáno") + " od " + humanReadableDate(record["start"]) + " do " + humanReadableDate(record["end"]);
 | 
			
		||||
        let overrideButton = document.createElement("input");
 | 
			
		||||
        overrideButton.type = "button";
 | 
			
		||||
        overrideButton.value = "❌";
 | 
			
		||||
        overrideButton.addEventListener("click", async () => {
 | 
			
		||||
            let response = await fetch("api/delete-override", {
 | 
			
		||||
                method: "POST",
 | 
			
		||||
                headers: {"Content-Type": "application/json"},
 | 
			
		||||
                body: JSON.stringify({
 | 
			
		||||
                    action: record["action"],
 | 
			
		||||
                    start: record["start"],
 | 
			
		||||
                    end: record["end"]
 | 
			
		||||
                }),
 | 
			
		||||
            });
 | 
			
		||||
            await refresh(await response.json());
 | 
			
		||||
        })
 | 
			
		||||
        overrides.appendChild(overrideText);
 | 
			
		||||
        overrides.appendChild(overrideButton);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function refreshSchedules(data) {
 | 
			
		||||
    for (const [day, entries] of Object.entries(data)) {
 | 
			
		||||
        let schedule = "Zakázáno"
 | 
			
		||||
        if (entries.length > 0) {
 | 
			
		||||
            schedule = entries.map((entry) => entry["start"] + " - " + entry["end"]).join(", ");
 | 
			
		||||
        }
 | 
			
		||||
        document.getElementById("schedule-" + day).textContent = schedule;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function refresh(data) {
 | 
			
		||||
    data = data || await (await fetch("api/data")).json();
 | 
			
		||||
    refreshStatus(data["status"]);
 | 
			
		||||
    refreshOverrides(data["overrides"]);
 | 
			
		||||
    refreshSchedules(data["schedules"]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function addOverride(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    let response = await fetch("api/override", {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        headers: {"Content-Type": "application/json"},
 | 
			
		||||
        body: JSON.stringify({
 | 
			
		||||
            action: document.getElementById("override-action").value == "allow",
 | 
			
		||||
            start: document.getElementById("override-start").value,
 | 
			
		||||
            end: document.getElementById("override-end").value,
 | 
			
		||||
        }),
 | 
			
		||||
    });
 | 
			
		||||
    await refresh(await response.json());
 | 
			
		||||
    document.getElementById("add-override").reset();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function updateSchedule(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    let response = await fetch("api/schedule", {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        headers: {"Content-Type": "application/json"},
 | 
			
		||||
        body: JSON.stringify({
 | 
			
		||||
            action: document.getElementById("schedule-action").value == "allow",
 | 
			
		||||
            days: Array.from(document.getElementById("schedule-days").selectedOptions).map(v => parseInt(v.value)),
 | 
			
		||||
            start: document.getElementById("schedule-start").value,
 | 
			
		||||
            end: document.getElementById("schedule-end").value
 | 
			
		||||
        })
 | 
			
		||||
    });
 | 
			
		||||
    await refresh(await response.json());
 | 
			
		||||
    document.getElementById("update-schedule").reset();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function fastAction(e) {
 | 
			
		||||
    let response = await fetch("api/fast-action/" + e.target.dataset.mins);
 | 
			
		||||
    await refresh(await response.json());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
    refresh();
 | 
			
		||||
    document.getElementById("add-override").addEventListener("submit", addOverride);
 | 
			
		||||
    document.getElementById("update-schedule").addEventListener("submit", updateSchedule);
 | 
			
		||||
    Array.from(document.getElementsByClassName("fast-action")).forEach(function(element) {
 | 
			
		||||
        element.addEventListener("click", fastAction);
 | 
			
		||||
    });
 | 
			
		||||
    window.setInterval(refresh, 5000);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										65
									
								
								static/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								static/style.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
			
		||||
body {
 | 
			
		||||
    max-width: 500px;
 | 
			
		||||
    font-family: serif;
 | 
			
		||||
}
 | 
			
		||||
h2 {
 | 
			
		||||
    margin: 1em 0 0.5em;
 | 
			
		||||
}
 | 
			
		||||
form {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    padding: 0 1em 1em 1em;
 | 
			
		||||
    background: #eee;
 | 
			
		||||
    border: 1px solid #ccc;
 | 
			
		||||
    margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
input, select {
 | 
			
		||||
    background: #fff;
 | 
			
		||||
    border: 1px solid #999;
 | 
			
		||||
    padding: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
input[type="button"] {
 | 
			
		||||
    background: #eee;
 | 
			
		||||
}
 | 
			
		||||
input[type="button"]:hover {
 | 
			
		||||
    background: #ddd;
 | 
			
		||||
}
 | 
			
		||||
input[type="submit"] {
 | 
			
		||||
    margin-top: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
input[type="submit"]:hover {
 | 
			
		||||
    background: #f6f6f6;
 | 
			
		||||
}
 | 
			
		||||
label {
 | 
			
		||||
    padding: 0.5em 0.5em 0.5em 0;
 | 
			
		||||
}
 | 
			
		||||
#status {
 | 
			
		||||
    margin-bottom: 1em;
 | 
			
		||||
}
 | 
			
		||||
#overrides {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: 1fr 30px;
 | 
			
		||||
    margin-top: -0.4em;
 | 
			
		||||
}
 | 
			
		||||
#schedules {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: 5em 1fr;
 | 
			
		||||
    margin-top: -0.4em;
 | 
			
		||||
}
 | 
			
		||||
#overrides span, #schedules span {
 | 
			
		||||
    margin: 0.5em 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (min-width: 400px) {
 | 
			
		||||
    form {
 | 
			
		||||
        grid-template-columns: 50px 1fr;
 | 
			
		||||
        grid-gap: 2px;
 | 
			
		||||
        padding: 1em;
 | 
			
		||||
    }
 | 
			
		||||
    form label {
 | 
			
		||||
        grid-column: 1 / 2;
 | 
			
		||||
        text-align: right;
 | 
			
		||||
    }
 | 
			
		||||
    form input, form select {
 | 
			
		||||
        grid-column: 2 / 3;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										0
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										389
									
								
								tests/test_app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										389
									
								
								tests/test_app.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,389 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from parental_control import DATETIME_FORMAT, app
 | 
			
		||||
from parental_control.span import DateTimeSpan, MinuteSpan
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "point_in_time, schedules, overrides, expected_result",
 | 
			
		||||
    [
 | 
			
		||||
        # Point in time within override
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 14:00",
 | 
			
		||||
            {day: [] for day in range(7)},
 | 
			
		||||
            [{"start": "2023-08-24 12:00", "end": "2023-08-24 18:00", "action": True}],
 | 
			
		||||
            True,
 | 
			
		||||
        ),
 | 
			
		||||
        # Point in time within schedule
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 14:00",
 | 
			
		||||
            {3: [{"start": "10:00", "end": "16:00"}]},
 | 
			
		||||
            [],
 | 
			
		||||
            True,
 | 
			
		||||
        ),
 | 
			
		||||
        # Point in time outside schedule and override
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 08:00",
 | 
			
		||||
            {3: [{"start": "10:00", "end": "16:00"}]},
 | 
			
		||||
            [{"start": "2023-08-24 12:00", "end": "2023-08-24 18:00", "action": True}],
 | 
			
		||||
            False,
 | 
			
		||||
        ),
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
def test_get_action_at(point_in_time, schedules, overrides, expected_result):
 | 
			
		||||
    point_in_time = datetime.strptime(point_in_time, DATETIME_FORMAT)
 | 
			
		||||
    schedules = {
 | 
			
		||||
        day: [MinuteSpan.from_args(**span) for span in schedule]
 | 
			
		||||
        for day, schedule in schedules.items()
 | 
			
		||||
    }
 | 
			
		||||
    overrides = [DateTimeSpan.from_args(**o) for o in overrides]
 | 
			
		||||
 | 
			
		||||
    result = app.get_action_at(point_in_time, schedules, overrides)
 | 
			
		||||
 | 
			
		||||
    assert result == expected_result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "point_in_time, action, schedules, overrides, expected_result",
 | 
			
		||||
    [
 | 
			
		||||
        # No schedule, no overrides, next change will never occur
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 08:00",
 | 
			
		||||
            False,
 | 
			
		||||
            {day: [] for day in range(7)},
 | 
			
		||||
            [],
 | 
			
		||||
            None,
 | 
			
		||||
        ),
 | 
			
		||||
        # Schedule 24/7, no overrides, next change will never occur
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 08:00",
 | 
			
		||||
            True,
 | 
			
		||||
            {day: [{"start": "00:00", "end": "24:00"}] for day in range(7)},
 | 
			
		||||
            [],
 | 
			
		||||
            None,
 | 
			
		||||
        ),
 | 
			
		||||
        # No schedule, next change occurs after allow override starts
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 08:00",
 | 
			
		||||
            False,
 | 
			
		||||
            {day: [] for day in range(7)},
 | 
			
		||||
            [{"start": "2023-08-26 10:00", "end": "2023-08-26 14:00", "action": True}],
 | 
			
		||||
            "2023-08-26 10:00",
 | 
			
		||||
        ),
 | 
			
		||||
        # Schedule 24/7, next change occurs after deny override starts
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 08:00",
 | 
			
		||||
            True,
 | 
			
		||||
            {day: [{"start": "00:00", "end": "24:00"}] for day in range(7)},
 | 
			
		||||
            [{"start": "2023-08-26 10:00", "end": "2023-08-26 14:00", "action": False}],
 | 
			
		||||
            "2023-08-26 10:00",
 | 
			
		||||
        ),
 | 
			
		||||
        # No schedule, expired overrides, next change will never occur
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 08:00",
 | 
			
		||||
            False,
 | 
			
		||||
            {day: [] for day in range(7)},
 | 
			
		||||
            [{"start": "2023-08-23 10:00", "end": "2023-08-23 14:00", "action": True}],
 | 
			
		||||
            None,
 | 
			
		||||
        ),
 | 
			
		||||
        # No schedule, next change occurs after allow override expires
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 08:00",
 | 
			
		||||
            True,
 | 
			
		||||
            {day: [] for day in range(7)},
 | 
			
		||||
            [{"start": "2023-08-24 08:00", "end": "2023-08-26 14:00", "action": True}],
 | 
			
		||||
            "2023-08-26 14:00",
 | 
			
		||||
        ),
 | 
			
		||||
        # Next change occurs after allow override expires
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 08:00",
 | 
			
		||||
            True,
 | 
			
		||||
            {day: [{"start": "14:00", "end": "16:00"}] for day in range(7)},
 | 
			
		||||
            [{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
 | 
			
		||||
            "2023-08-24 10:00",
 | 
			
		||||
        ),
 | 
			
		||||
        # Next change occurs after schedule span ends
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 14:00",
 | 
			
		||||
            True,
 | 
			
		||||
            {
 | 
			
		||||
                day: [{"start": "08:00", "end": "10:00"}, {"start": "14:00", "end": "16:00"}]
 | 
			
		||||
                for day in range(7)
 | 
			
		||||
            },
 | 
			
		||||
            [{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
 | 
			
		||||
            "2023-08-24 16:00",
 | 
			
		||||
        ),
 | 
			
		||||
        # Next change occurs after schedule span starts
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 13:00",
 | 
			
		||||
            False,
 | 
			
		||||
            {
 | 
			
		||||
                day: [{"start": "08:00", "end": "10:00"}, {"start": "14:00", "end": "16:00"}]
 | 
			
		||||
                for day in range(7)
 | 
			
		||||
            },
 | 
			
		||||
            [{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
 | 
			
		||||
            "2023-08-24 14:00",
 | 
			
		||||
        ),
 | 
			
		||||
        # Next change occurs after deny override starts
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 11:00",
 | 
			
		||||
            True,
 | 
			
		||||
            {day: [{"start": "10:00", "end": "16:00"}] for day in range(7)},
 | 
			
		||||
            [{"start": "2023-08-24 12:00", "end": "2023-08-24 14:00", "action": False}],
 | 
			
		||||
            "2023-08-24 12:00",
 | 
			
		||||
        ),
 | 
			
		||||
        # Next change occurs after allow override starts
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-24 10:00",
 | 
			
		||||
            False,
 | 
			
		||||
            {day: [{"start": "16:00", "end": "18:00"}] for day in range(7)},
 | 
			
		||||
            [{"start": "2023-08-24 12:00", "end": "2023-08-24 14:00", "action": True}],
 | 
			
		||||
            "2023-08-24 12:00",
 | 
			
		||||
        ),
 | 
			
		||||
        # Next change occurs after schedule span ends after 7 days
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-21 13:00",
 | 
			
		||||
            True,
 | 
			
		||||
            {
 | 
			
		||||
                0: [{"start": "00:00", "end": "04:00"}, {"start": "10:00", "end": "24:00"}],
 | 
			
		||||
                1: [{"start": "00:00", "end": "24:00"}],
 | 
			
		||||
                2: [{"start": "00:00", "end": "24:00"}],
 | 
			
		||||
                3: [{"start": "00:00", "end": "24:00"}],
 | 
			
		||||
                4: [{"start": "00:00", "end": "24:00"}],
 | 
			
		||||
                5: [{"start": "00:00", "end": "24:00"}],
 | 
			
		||||
                6: [{"start": "00:00", "end": "24:00"}],
 | 
			
		||||
            },
 | 
			
		||||
            [{"start": "2023-08-24 12:00", "end": "2023-08-24 14:00", "action": True}],
 | 
			
		||||
            "2023-08-28 04:00",
 | 
			
		||||
        ),
 | 
			
		||||
        # Next change occurs after schedule span starts after 7 days
 | 
			
		||||
        (
 | 
			
		||||
            "2023-08-21 13:00",
 | 
			
		||||
            False,
 | 
			
		||||
            {
 | 
			
		||||
                0: [{"start": "00:00", "end": "04:00"}],
 | 
			
		||||
                1: [],
 | 
			
		||||
                2: [],
 | 
			
		||||
                3: [],
 | 
			
		||||
                4: [],
 | 
			
		||||
                5: [],
 | 
			
		||||
                6: [],
 | 
			
		||||
            },
 | 
			
		||||
            [{"start": "2023-08-31 12:00", "end": "2023-08-31 14:00", "action": True}],
 | 
			
		||||
            "2023-08-28 00:00",
 | 
			
		||||
        ),
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
def test_get_next_action_change(point_in_time, action, schedules, overrides, expected_result):
 | 
			
		||||
    point_in_time = datetime.strptime(point_in_time, DATETIME_FORMAT)
 | 
			
		||||
    schedules = {
 | 
			
		||||
        day: [MinuteSpan.from_args(**span) for span in schedule]
 | 
			
		||||
        for day, schedule in schedules.items()
 | 
			
		||||
    }
 | 
			
		||||
    overrides = [DateTimeSpan.from_args(**o) for o in overrides]
 | 
			
		||||
 | 
			
		||||
    result = app.get_next_action_change(point_in_time, action, schedules, overrides)
 | 
			
		||||
 | 
			
		||||
    if expected_result:
 | 
			
		||||
        expected_result = datetime.strptime(expected_result, DATETIME_FORMAT)
 | 
			
		||||
    assert result == expected_result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "schedules, overrides, expected_result",
 | 
			
		||||
    [
 | 
			
		||||
        (
 | 
			
		||||
            {day: [] for day in range(7)},
 | 
			
		||||
            [],
 | 
			
		||||
            {"action": False, "until": "", "then_until": ""},
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            {day: [{"start": "14:00", "end": "16:00"}] for day in range(7)},
 | 
			
		||||
            [{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
 | 
			
		||||
            {"action": False, "until": "2023-08-24 14:00", "then_until": "2023-08-24 16:00"},
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            {day: [] for day in range(7)},
 | 
			
		||||
            [{"start": "2023-08-24 08:00", "end": "2023-08-24 12:00", "action": True}],
 | 
			
		||||
            {"action": True, "until": "2023-08-24 12:00", "then_until": ""},
 | 
			
		||||
        ),
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
@patch("parental_control.app.datetime", wraps=datetime)
 | 
			
		||||
def test_calculate_status(mock_datetime, schedules, overrides, expected_result):
 | 
			
		||||
    mock_datetime.now.return_value = datetime(2023, 8, 24, 11)
 | 
			
		||||
    schedules = {
 | 
			
		||||
        day: [MinuteSpan.from_args(**span) for span in schedule]
 | 
			
		||||
        for day, schedule in schedules.items()
 | 
			
		||||
    }
 | 
			
		||||
    overrides = [DateTimeSpan.from_args(**o) for o in overrides]
 | 
			
		||||
 | 
			
		||||
    result = app.calculate_status(schedules, overrides)
 | 
			
		||||
 | 
			
		||||
    assert result == expected_result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "schedule, span, action, expected_result",
 | 
			
		||||
    [
 | 
			
		||||
        # Allow span in empty schedule
 | 
			
		||||
        (
 | 
			
		||||
            [],
 | 
			
		||||
            {"start": "09:00", "end": "11:00"},
 | 
			
		||||
            True,
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}],
 | 
			
		||||
        ),
 | 
			
		||||
        # Allow span same as already existing span
 | 
			
		||||
        (
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}],
 | 
			
		||||
            {"start": "09:00", "end": "11:00"},
 | 
			
		||||
            True,
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}],
 | 
			
		||||
        ),
 | 
			
		||||
        # Allow span (starting at 0:00) prepening it before existing span
 | 
			
		||||
        (
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}],
 | 
			
		||||
            {"start": "00:00", "end": "05:00"},
 | 
			
		||||
            True,
 | 
			
		||||
            [{"start": "0:00", "end": "5:00"}, {"start": "9:00", "end": "11:00"}],
 | 
			
		||||
        ),
 | 
			
		||||
        # Allow span (ending at 24:00) appending it after existing span
 | 
			
		||||
        (
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}],
 | 
			
		||||
            {"start": "19:00", "end": "0:00"},
 | 
			
		||||
            True,
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}, {"start": "19:00", "end": "24:00"}],
 | 
			
		||||
        ),
 | 
			
		||||
        # Allow span merging existing spans together
 | 
			
		||||
        (
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}, {"start": "14:00", "end": "16:00"}],
 | 
			
		||||
            {"start": "11:00", "end": "14:00"},
 | 
			
		||||
            True,
 | 
			
		||||
            [{"start": "9:00", "end": "16:00"}],
 | 
			
		||||
        ),
 | 
			
		||||
        # Deny span from empty schedule
 | 
			
		||||
        (
 | 
			
		||||
            [],
 | 
			
		||||
            {"start": "12:00", "end": "14:00"},
 | 
			
		||||
            False,
 | 
			
		||||
            [],
 | 
			
		||||
        ),
 | 
			
		||||
        # Deny span which is already denied
 | 
			
		||||
        (
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
 | 
			
		||||
            {"start": "12:00", "end": "14:00"},
 | 
			
		||||
            False,
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
 | 
			
		||||
        ),
 | 
			
		||||
        # Deny span removing existing allow spans
 | 
			
		||||
        (
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
 | 
			
		||||
            {"start": "0:00", "end": "0:00"},
 | 
			
		||||
            False,
 | 
			
		||||
            [],
 | 
			
		||||
        ),
 | 
			
		||||
        # Deny span within allow span, splitting the allow span
 | 
			
		||||
        (
 | 
			
		||||
            [{"start": "9:00", "end": "16:00"}],
 | 
			
		||||
            {"start": "11:00", "end": "14:00"},
 | 
			
		||||
            False,
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}, {"start": "14:00", "end": "16:00"}],
 | 
			
		||||
        ),
 | 
			
		||||
        # Deny span moving end and start of existing allow spans
 | 
			
		||||
        (
 | 
			
		||||
            [{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
 | 
			
		||||
            {"start": "10:00", "end": "17:00"},
 | 
			
		||||
            False,
 | 
			
		||||
            [{"start": "9:00", "end": "10:00"}, {"start": "17:00", "end": "18:00"}],
 | 
			
		||||
        ),
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
def test_update_schedule(schedule, span, action, expected_result):
 | 
			
		||||
    schedule = [MinuteSpan.from_args(**s) for s in schedule]
 | 
			
		||||
    span = MinuteSpan.from_args(**span)
 | 
			
		||||
 | 
			
		||||
    app.update_schedule(schedule, span, action)
 | 
			
		||||
 | 
			
		||||
    expected_result = [MinuteSpan.from_args(**s) for s in expected_result]
 | 
			
		||||
    assert schedule == expected_result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "overrides, start, end, action, expected_result",
 | 
			
		||||
    [
 | 
			
		||||
        # Add override into empty list of overrides
 | 
			
		||||
        (
 | 
			
		||||
            [],
 | 
			
		||||
            {"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True},
 | 
			
		||||
            [{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
 | 
			
		||||
        ),
 | 
			
		||||
        # Add override into list of overrides
 | 
			
		||||
        (
 | 
			
		||||
            [
 | 
			
		||||
                {"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
 | 
			
		||||
                {"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
 | 
			
		||||
            ],
 | 
			
		||||
            {"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True},
 | 
			
		||||
            [
 | 
			
		||||
                {"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
 | 
			
		||||
                {"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True},
 | 
			
		||||
                {"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        # Add override removing another override spanning over smaller period
 | 
			
		||||
        (
 | 
			
		||||
            [
 | 
			
		||||
                {"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
 | 
			
		||||
                {"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
 | 
			
		||||
            ],
 | 
			
		||||
            {"start": "2023-08-24 15:00", "end": "2023-08-24 22:00", "action": False},
 | 
			
		||||
            [
 | 
			
		||||
                {"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
 | 
			
		||||
                {"start": "2023-08-24 15:00", "end": "2023-08-24 22:00", "action": False},
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        # Add override within existing override span, splitting it
 | 
			
		||||
        (
 | 
			
		||||
            [{"start": "2023-08-24 08:00", "end": "2023-08-24 20:00", "action": False}],
 | 
			
		||||
            {"start": "2023-08-24 16:00", "end": "2023-08-24 18:00", "action": True},
 | 
			
		||||
            [
 | 
			
		||||
                {"start": "2023-08-24 08:00", "end": "2023-08-24 16:00", "action": False},
 | 
			
		||||
                {"start": "2023-08-24 16:00", "end": "2023-08-24 18:00", "action": True},
 | 
			
		||||
                {"start": "2023-08-24 18:00", "end": "2023-08-24 20:00", "action": False},
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        # Add override moving end and start of existing overrides
 | 
			
		||||
        (
 | 
			
		||||
            [
 | 
			
		||||
                {"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
 | 
			
		||||
                {"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
 | 
			
		||||
            ],
 | 
			
		||||
            {"start": "2023-08-24 04:00", "end": "2023-08-24 18:00", "action": False},
 | 
			
		||||
            [
 | 
			
		||||
                {"start": "2023-08-24 00:00", "end": "2023-08-24 04:00", "action": True},
 | 
			
		||||
                {"start": "2023-08-24 04:00", "end": "2023-08-24 18:00", "action": False},
 | 
			
		||||
                {"start": "2023-08-24 18:00", "end": "2023-08-24 20:00", "action": True},
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        # Add override merging existing overrides
 | 
			
		||||
        (
 | 
			
		||||
            [
 | 
			
		||||
                {"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
 | 
			
		||||
                {"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
 | 
			
		||||
            ],
 | 
			
		||||
            {"start": "2023-08-24 04:00", "end": "2023-08-24 18:00", "action": True},
 | 
			
		||||
            [{"start": "2023-08-24 00:00", "end": "2023-08-24 20:00", "action": True}],
 | 
			
		||||
        ),
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
def test_update_overrides(overrides, span, expected_result):
 | 
			
		||||
    overrides = [DateTimeSpan.from_args(**o) for o in overrides]
 | 
			
		||||
    span = DateTimeSpan.from_args(**span)
 | 
			
		||||
 | 
			
		||||
    app.update_overrides(overrides, span)
 | 
			
		||||
 | 
			
		||||
    expected_result = [DateTimeSpan.from_args(**o) for o in expected_result]
 | 
			
		||||
    assert overrides == expected_result
 | 
			
		||||
							
								
								
									
										17
									
								
								tox.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tox.ini
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
[coverage:run]
 | 
			
		||||
branch = True
 | 
			
		||||
 | 
			
		||||
[tox]
 | 
			
		||||
envlist = pylint, pytest, black
 | 
			
		||||
 | 
			
		||||
[testenv:pylint]
 | 
			
		||||
deps = pylint
 | 
			
		||||
commands = pylint parental_control tests {posargs}
 | 
			
		||||
 | 
			
		||||
[testenv:pytest]
 | 
			
		||||
deps = pytest-cov
 | 
			
		||||
commands = pytest tests -vv --cov parental_control --cov-report term --cov-report xml --cov-fail-under 100 {posargs}
 | 
			
		||||
 | 
			
		||||
[testenv:black]
 | 
			
		||||
deps = black
 | 
			
		||||
commands = black -l 100 --check --diff parental_control tests {posargs}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user