about summary refs log tree commit diff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/url-blocker/.envrc (renamed from tools/website-blocker/.envrc)0
-rw-r--r--tools/url-blocker/Main.hs (renamed from tools/website-blocker/Main.hs)95
-rw-r--r--tools/url-blocker/README.md47
-rw-r--r--tools/url-blocker/Spec.hs (renamed from tools/website-blocker/Spec.hs)0
-rw-r--r--tools/url-blocker/default.nix37
-rw-r--r--tools/url-blocker/hosts (renamed from tools/website-blocker/hosts)0
-rw-r--r--tools/url-blocker/rules.json28
-rw-r--r--tools/url-blocker/shell.nix (renamed from tools/website-blocker/shell.nix)0
8 files changed, 182 insertions, 25 deletions
diff --git a/tools/website-blocker/.envrc b/tools/url-blocker/.envrc
index 81755b513b21..81755b513b21 100644
--- a/tools/website-blocker/.envrc
+++ b/tools/url-blocker/.envrc
diff --git a/tools/website-blocker/Main.hs b/tools/url-blocker/Main.hs
index f1a7026714f2..34197981598c 100644
--- a/tools/website-blocker/Main.hs
+++ b/tools/url-blocker/Main.hs
@@ -43,9 +43,9 @@ newtype Hour = Hour { getHour :: Int } deriving (Show, Eq, Generic)
 
 newtype Minute = Minute { getMinute :: Int } deriving (Show, Eq, Generic)
 
-data EtcHostEntry = EtcHostEntry { ip :: IPAddress
-                                 , domains :: [Domain]
-                                 } deriving (Show)
+data EtcHostsEntry = EtcHostsEntry { ip :: IPAddress
+                                   , domains :: [Domain]
+                                   } deriving (Show)
 
 -- | Write these in terms of your system's local time (i.e. `date`).
 data TimeSlot = TimeSlot { beg :: (Hour, Minute)
@@ -103,6 +103,12 @@ instance Aeson.FromJSON Rule where
 -- Functions
 --------------------------------------------------------------------------------
 
+-- | Pipe operator
+(|>) :: a -> (a -> b) -> b
+(|>) a f = f a
+infixl 1 |>
+
+-- | Returns True if the current time falls within any of the `timeslots`.
 isWithinTimeSlot :: LocalTime.LocalTime -> [TimeSlot] -> Bool
 isWithinTimeSlot date timeslots =
   List.any withinTimeSlot timeslots
@@ -115,51 +121,90 @@ isWithinTimeSlot date timeslots =
             LocalTime.localTimeOfDay date
       in (todHour > ah) && (todMin > am) && (todHour < bh) && (todMin < bm)
 
+-- | Returns True if `day` is the same day as today.
 isToday :: LocalTime.LocalTime -> Calendar.DayOfWeek -> Bool
-isToday date day = Calendar.dayOfWeek (LocalTime.localDay date) == day
+isToday date day = today == day
+  where
+    today = Calendar.dayOfWeek (LocalTime.localDay date)
 
-isAllowed :: LocalTime.LocalTime -> [Allowance] -> Bool
-isAllowed _ [] = False
-isAllowed date allowances = do
+-- | Returns True if a list of none of the `allowances` are valid.
+shouldBeBlocked :: LocalTime.LocalTime -> [Allowance] -> Bool
+shouldBeBlocked _ [] = True
+shouldBeBlocked date allowances = do
   case filter (isToday date . day) allowances of
-    [Allowance{timeslots}] ->
-      isWithinTimeSlot date timeslots
-    [] -> False
+    [Allowance{timeslots}] -> not $ isWithinTimeSlot date timeslots
+    [] -> True
     -- Error when more than one rule per day
-    _  -> False
+    _  -> True
 
-serializeEntry :: EtcHostEntry -> Text
-serializeEntry EtcHostEntry{ip, domains} =
+-- | Maps an EtcHostsEntry to the line of text url-blocker will append to /etc/hosts.
+serializeEtcHostEntry :: EtcHostsEntry -> Text
+serializeEtcHostEntry EtcHostsEntry{ip, domains} =
   (getIPAddress ip) <> "\t" <> (Text.unwords $ fmap getDomain domains)
 
-toEtcHostEntry :: LocalTime.LocalTime -> Rule -> Maybe EtcHostEntry
-toEtcHostEntry date Rule{urls, allowed} =
-  if isAllowed date allowed then
-    Nothing
-  else
-    Just $ EtcHostEntry { ip = IPAddress "127.0.0.1"
+-- | Create an EtcHostsEntry mapping the URLs in `rule` to 127.0.0.1 if the
+-- URLs should be blocked.
+maybeBlockURL :: LocalTime.LocalTime -> Rule -> Maybe EtcHostsEntry
+maybeBlockURL date Rule{urls, allowed} =
+  if shouldBeBlocked date allowed then
+    Just $ EtcHostsEntry { ip = IPAddress "127.0.0.1"
                         , domains = fmap (Domain . getURL) urls
                         }
+  else
+    Nothing
 
+-- | Read and parse the rules.json file.
+-- TODO(wpcarro): Properly handle errors for file not found.
+-- TODO(wpcarro): Properly handle errors for parse failures.
+-- TODO(wpcarro): How can we resolve the $HOME directory when this is run as
+-- root?
 getRules :: IO [Rule]
 getRules = do
-  contents <- LazyByteString.readFile "rules.json"
+  contents <- LazyByteString.readFile "/home/wpcarro/.config/url-blocker/rules.json"
   let payload = Aeson.eitherDecode contents
   pure $ Either.fromRight [] payload
 
-header :: Text
-header =
+-- | Informational header added to /etc/hosts before the entries that
+-- url-blocker adds.
+urlBlockerHeader :: Text
+urlBlockerHeader =
   Text.unlines [ "################################################################################"
-               , "# Added by url-blocker"
+               , "# Added by url-blocker."
+               , "#"
+               , "# Warning: url-blocker will remove anything that you add beneath this header."
                , "################################################################################"
                ]
 
+-- | Removes all entries that url-blocker may have added to /etc/hosts.
+removeURLBlockerEntries :: Text -> Text
+removeURLBlockerEntries etcHosts =
+  case Text.breakOn urlBlockerHeader etcHosts of
+    (etcHosts', _) -> etcHosts'
+
+-- | Appends the newly created `entries` to `etcHosts`.
+addURLBlockerEntries :: Text -> Text -> Text
+addURLBlockerEntries entries etcHosts =
+  Text.unlines [ etcHosts
+               , urlBlockerHeader
+               , entries
+               ]
+
+-- | This script reads the current /etc/hosts, removes any entries that
+-- url-blocker may have added in a previous run, and adds new entries to block
+-- URLs according to the rules.json file.
 main :: IO ()
 main = do
   rules <- getRules
   tz <- LocalTime.getCurrentTimeZone
   ct <- Clock.getCurrentTime
   let date = LocalTime.utcToLocalTime tz ct
-      etcHosts = Text.unlines . fmap serializeEntry . Maybe.catMaybes $ fmap (toEtcHostEntry date) rules
+      entries = rules
+                |> fmap (maybeBlockURL date)
+                |> Maybe.catMaybes
+                |> fmap serializeEtcHostEntry
+                |> Text.unlines
   existingEtcHosts <- TextIO.readFile "/etc/hosts"
-  TextIO.putStrLn $ existingEtcHosts <> "\n" <> header <> "\n" <> etcHosts
+  existingEtcHosts
+    |> removeURLBlockerEntries
+    |> addURLBlockerEntries entries
+    |> \x -> writeFile "/etc/hosts" (Text.unpack x)
diff --git a/tools/url-blocker/README.md b/tools/url-blocker/README.md
new file mode 100644
index 000000000000..1b7fea8c15e0
--- /dev/null
+++ b/tools/url-blocker/README.md
@@ -0,0 +1,47 @@
+# url-blocker
+
+`url-blocker` blocks the URLs that you want to block when you want it to block
+them.
+
+Let's say that you don't want to visit Twitter during the work week. Create the
+file `~/.config/url-blocker/rules.json` with the following contents and
+`url-blocker` will take care of the rest.
+
+```json
+# ~/.config/url-blocker/rules.json
+[
+  {
+    "urls": [
+      "twitter.com",
+      "www.twitter.com",
+    ],
+    "allowed": [
+      {
+        "day": "Saturday",
+        "timeslots": [
+          "00:00-11:59"
+        ]
+      },
+      {
+        "day": "Sunday",
+        "timeslots": [
+          "00:00-11:59"
+        ]
+      }
+    ]
+  }
+]
+```
+
+## Installation
+
+```shell
+$ nix-env -iA 'briefcase.tools.url-blocker'
+```
+
+## How does it work?
+
+`systemd` is intended to run `url-blocker` once every minute. `url-blocker` will
+read `/etc/hosts` and map the URLs defined in `rules.json` to `127.0.0.1` when
+you want them blocked. Because `systemd` run once every minute, `/etc/hosts`
+should be current to the minute as well.
diff --git a/tools/website-blocker/Spec.hs b/tools/url-blocker/Spec.hs
index b70d8619cb25..b70d8619cb25 100644
--- a/tools/website-blocker/Spec.hs
+++ b/tools/url-blocker/Spec.hs
diff --git a/tools/url-blocker/default.nix b/tools/url-blocker/default.nix
new file mode 100644
index 000000000000..d6a098851b88
--- /dev/null
+++ b/tools/url-blocker/default.nix
@@ -0,0 +1,37 @@
+{ ... }:
+
+let
+  pkgs = import <unstable> {};
+
+  ghc = pkgs.haskellPackages.ghcWithPackages (hpkgs: [
+    hpkgs.time
+    hpkgs.aeson
+    hpkgs.either
+  ]);
+
+  # This is the systemd service unit
+  service = pkgs.stdenv.mkDerivation {
+    name = "url-blocker";
+    src = ./.;
+    buildInputs = with pkgs; [
+    ];
+    buildPhase = ''
+    ${ghc}/bin/ghc Main.hs
+  '';
+    installPhase = ''
+    mv ./Main $out
+  '';
+  };
+
+  # This is the systemd timer unit.
+  # Run once every minute.
+  # Give root privilege.
+  systemdUnit = {
+    systemd = {
+      timers.simple-timer = {
+        wantedBy = [ "timers.target" ];
+        partOf = [];
+      };
+    };
+  };
+in null
diff --git a/tools/website-blocker/hosts b/tools/url-blocker/hosts
index 0cf45e59584f..0cf45e59584f 100644
--- a/tools/website-blocker/hosts
+++ b/tools/url-blocker/hosts
diff --git a/tools/url-blocker/rules.json b/tools/url-blocker/rules.json
new file mode 100644
index 000000000000..95e4dc9a90c1
--- /dev/null
+++ b/tools/url-blocker/rules.json
@@ -0,0 +1,28 @@
+[
+  {
+    "urls": [
+      "facebook.com",
+      "www.facebook.com",
+      "twitter.com",
+      "www.twitter.com",
+      "youtube.com",
+      "www.youtube.com",
+      "instagram.com",
+      "www.instagram.com"
+    ],
+    "allowed": []
+  },
+  {
+    "urls": [
+      "chat.googleplex.com"
+    ],
+    "allowed": [
+      {
+        "day": "Sunday",
+        "timeslots": [
+          "18:35-18:39"
+        ]
+      }
+    ]
+  }
+]
diff --git a/tools/website-blocker/shell.nix b/tools/url-blocker/shell.nix
index 40f217e3f9a7..40f217e3f9a7 100644
--- a/tools/website-blocker/shell.nix
+++ b/tools/url-blocker/shell.nix