1
0
Fork 0

Formatting

This commit is contained in:
Malte 2023-01-16 02:18:18 +01:00
parent bd3186e144
commit 0108f1ee7f
24 changed files with 626 additions and 537 deletions

5
.hlint.yaml Normal file
View file

@ -0,0 +1,5 @@
- arguments:
- -XRecursiveDo
- -XQuasiQuotes
- ignore:
name: Eta reduce

View file

@ -50,13 +50,13 @@ serveWebsocket config backendRequestQueue params =
mayCreds = liftA2 (,) mayUsername mayPassword mayCreds = liftA2 (,) mayUsername mayPassword
action (Just (username, password)) action (Just (username, password))
| Just userConfig <- lookup username (users config) | Just userConfig <- lookup username (users config)
, PasswordCheckSuccess <- checkPassword (mkPassword password) (passwordHash userConfig) = , PasswordCheckSuccess <- checkPassword (mkPassword password) (passwordHash userConfig) =
acceptSocket backendRequestQueue username userConfig acceptSocket backendRequestQueue username userConfig
action (Just (username, _)) action (Just (username, _))
| Just _ <- lookup username (users config) = | Just _ <- lookup username (users config) =
\connection -> do \connection -> do
say [i|Rejecting Websocket request for #{username :: Text}, wrong password.|] say [i|Rejecting Websocket request for #{username :: Text}, wrong password.|]
rejectRequest connection "No valid 'username' and 'password' provided." rejectRequest connection "No valid 'username' and 'password' provided."
action _ = \connection -> do action _ = \connection -> do
say [i|Rejecting Websocket request #{show mayUsername :: Text}. No matching user found.|] say [i|Rejecting Websocket request #{show mayUsername :: Text}. No matching user found.|]
rejectRequest connection "No valid 'username' and 'password' provided." rejectRequest connection "No valid 'username' and 'password' provided."

View file

@ -1,147 +1,165 @@
{ obelisk ? import ./.obelisk/impl { {
system = builtins.currentSystem; obelisk ?
config.android_sdk.accept_license = true; import ./.obelisk/impl {
} system = builtins.currentSystem;
config.android_sdk.accept_license = true;
},
}: }:
with obelisk; with obelisk;
project ./. ( project ./. (
{ pkgs, ... }: {pkgs, ...}: let
let inherit
inherit (pkgs.haskell.lib) (pkgs.haskell.lib)
markUnbroken dontCheck addBuildDepend doJailbreak markUnbroken
overrideCabal dontCheck
; addBuildDepend
in doJailbreak
{ overrideCabal
android = { ;
applicationId = "de.maralorn.kassandra"; in {
displayName = "Kassandra"; android = {
releaseKey = null; applicationId = "de.maralorn.kassandra";
isRelease = true; displayName = "Kassandra";
version = { releaseKey = null;
code = import ./code.nix; isRelease = true;
name = "0.1.0"; version = {
code = import ./code.nix;
name = "0.1.0";
};
}; };
}; overrides = self: super: {
overrides = self: super: { kassandra = overrideCabal super.kassandra {doHaddock = false;};
kassandra = overrideCabal super.kassandra { doHaddock = false; }; backend = addBuildDepend super.backend pkgs.taskwarrior;
backend = addBuildDepend super.backend pkgs.taskwarrior; clay = markUnbroken (dontCheck super.clay);
clay = markUnbroken (dontCheck super.clay); haskeline = dontCheck (self.callHackage "haskeline" "0.8.0.1" {});
haskeline = dontCheck (self.callHackage "haskeline" "0.8.0.1" { }); repline = doJailbreak (self.callHackage "repline" "0.4.0.0" {});
repline = doJailbreak (self.callHackage "repline" "0.4.0.0" { }); dhall = dontCheck (self.callHackage "dhall" "1.35.0" {});
dhall = dontCheck (self.callHackage "dhall" "1.35.0" { }); relude = dontCheck super.relude;
relude = dontCheck super.relude; stm-containers = markUnbroken super.stm-containers;
stm-containers = markUnbroken super.stm-containers; stm-hamt = markUnbroken (doJailbreak super.stm-hamt);
stm-hamt = markUnbroken (doJailbreak super.stm-hamt); streamly-bytestring =
streamly-bytestring = self.callHackageDirect self.callHackageDirect
{ {
pkg = "streamly-bytestring"; pkg = "streamly-bytestring";
ver = "0.1.2"; ver = "0.1.2";
sha256 = "08xhp8zgf5n1j4v1br1dz9ih8j05vk92swp3nz9in5xajllkc7qv"; sha256 = "08xhp8zgf5n1j4v1br1dz9ih8j05vk92swp3nz9in5xajllkc7qv";
} }
{ }; {};
streamly = self.callHackageDirect streamly =
{ self.callHackageDirect
pkg = "streamly"; {
ver = "0.7.0"; pkg = "streamly";
sha256 = "0hr2cz14w6nnbvhnq1fvr8v4rzyqcj3b9khf2rszyji00fmp27l1"; ver = "0.7.0";
} sha256 = "0hr2cz14w6nnbvhnq1fvr8v4rzyqcj3b9khf2rszyji00fmp27l1";
{ }; }
nonempty-vector = self.callHackageDirect {};
{ nonempty-vector =
pkg = "nonempty-vector"; self.callHackageDirect
ver = "0.1.0.0"; {
sha256 = "06abdmdy9z0w6ishiibir3qfjpqxmb4mrkhgyc4j58hd14s8rj0x"; pkg = "nonempty-vector";
} ver = "0.1.0.0";
{ }; sha256 = "06abdmdy9z0w6ishiibir3qfjpqxmb4mrkhgyc4j58hd14s8rj0x";
nonempty-containers = self.callHackageDirect }
{ {};
pkg = "nonempty-containers"; nonempty-containers =
ver = "0.3.4.1"; self.callHackageDirect
sha256 = "0nbnr0az201lv09dwcxcppkfc9b05kyw4la990z5asn9737pvpr2"; {
} pkg = "nonempty-containers";
{ }; ver = "0.3.4.1";
iCalendar = overrideCabal (doJailbreak (markUnbroken super.iCalendar)) { sha256 = "0nbnr0az201lv09dwcxcppkfc9b05kyw4la990z5asn9737pvpr2";
preConfigure = ''substituteInPlace iCalendar.cabal --replace "network >=2.6 && <2.7" "network -any"''; }
{};
iCalendar = overrideCabal (doJailbreak (markUnbroken super.iCalendar)) {
preConfigure = ''substituteInPlace iCalendar.cabal --replace "network >=2.6 && <2.7" "network -any"'';
};
prettyprinter =
self.callHackageDirect
{
pkg = "prettyprinter";
ver = "1.5.1";
sha256 = "0wx01rvgwnnmg10sh9x2whif5z12058w5djh7m5swz94wvkg5cg3";
}
{};
cborg-json =
self.callHackageDirect
{
pkg = "cborg-json";
ver = "0.2.2.0";
sha256 = "1s7pv3jz8s1qb0ydcc5nra9f63jp4ay4d0vncv919bakf8snj4vw";
}
{};
generic-random =
self.callHackageDirect
{
pkg = "generic-random";
ver = "1.3.0.0";
sha256 = "0m7lb40wgmyszv8l6qmarkfgs8r0idgl9agwsi72236hpvp353ad";
}
{};
atomic-write =
self.callHackageDirect
{
pkg = "atomic-write";
ver = "0.2.0.7";
sha256 = "1r9ckwljdbw3mi8rmzmsnh89z8nhw2qnds9n271gkjgavb6hxxf3";
}
{};
taskwarrior =
self.callHackageDirect
{
pkg = "taskwarrior";
ver = "0.5.0.0";
sha256 = "sha256-elDUtz0NSG4WHxkyCQ1CunYXWIVRj6EqkKSchPy+c3E=";
}
{};
base64 =
self.callHackageDirect
{
pkg = "base64";
ver = "0.4.1";
sha256 = "1pz9s8bmnkrrr3v5mhkwv8vaf251vmxs87zzc5nsjsa027j9lr22";
}
{};
password =
self.callHackageDirect
{
pkg = "password";
ver = "2.0.1.0";
sha256 = "1q99v7w6bdfpnw245aa3zaj3x7mhl9i2y7f2rzlc30g066p9jhaz";
}
{};
indexed-profunctors =
self.callHackageDirect
{
pkg = "indexed-profunctors";
ver = "0.1";
sha256 = "0vpgbymfhnvip90jwvyniqi34lhz5n3ni1f21g81n5rap0q140za";
}
{};
generic-lens-core =
self.callHackageDirect
{
pkg = "generic-lens-core";
ver = "2.0.0.0";
sha256 = "07parw0frqxxkjbbas9m9xb3pmpqrx9wz63m35wa6xqng9vlcscm";
}
{};
generic-optics =
self.callHackageDirect
{
pkg = "generic-optics";
ver = "2.0.0.0";
sha256 = "0xy5k5b35w1i1zxy0dv5fk1b3zrd3hx3v5kh593k2la7ri880wmq";
}
{};
optics-core = self.callHackage "optics-core" "0.3.0.1" {};
optics-th = self.callHackage "optics-th" "0.3.0.2" {};
optics-extra = self.callHackage "optics-extra" "0.3" {};
optics = self.callHackage "optics" "0.3" {};
}; };
prettyprinter = self.callHackageDirect packages = {
{ kassandra = ./kassandra;
pkg = "prettyprinter"; standalone = ./standalone;
ver = "1.5.1"; };
sha256 = "0wx01rvgwnnmg10sh9x2whif5z12058w5djh7m5swz94wvkg5cg3"; }
} )
{ };
cborg-json = self.callHackageDirect
{
pkg = "cborg-json";
ver = "0.2.2.0";
sha256 = "1s7pv3jz8s1qb0ydcc5nra9f63jp4ay4d0vncv919bakf8snj4vw";
}
{ };
generic-random = self.callHackageDirect
{
pkg = "generic-random";
ver = "1.3.0.0";
sha256 = "0m7lb40wgmyszv8l6qmarkfgs8r0idgl9agwsi72236hpvp353ad";
}
{ };
atomic-write = self.callHackageDirect
{
pkg = "atomic-write";
ver = "0.2.0.7";
sha256 = "1r9ckwljdbw3mi8rmzmsnh89z8nhw2qnds9n271gkjgavb6hxxf3";
}
{ };
taskwarrior = self.callHackageDirect
{
pkg = "taskwarrior";
ver = "0.5.0.0";
sha256 = "sha256-elDUtz0NSG4WHxkyCQ1CunYXWIVRj6EqkKSchPy+c3E=";
}
{ };
base64 = self.callHackageDirect
{
pkg = "base64";
ver = "0.4.1";
sha256 = "1pz9s8bmnkrrr3v5mhkwv8vaf251vmxs87zzc5nsjsa027j9lr22";
}
{ };
password = self.callHackageDirect
{
pkg = "password";
ver = "2.0.1.0";
sha256 = "1q99v7w6bdfpnw245aa3zaj3x7mhl9i2y7f2rzlc30g066p9jhaz";
}
{ };
indexed-profunctors = self.callHackageDirect
{
pkg = "indexed-profunctors";
ver = "0.1";
sha256 = "0vpgbymfhnvip90jwvyniqi34lhz5n3ni1f21g81n5rap0q140za";
}
{ };
generic-lens-core = self.callHackageDirect
{
pkg = "generic-lens-core";
ver = "2.0.0.0";
sha256 = "07parw0frqxxkjbbas9m9xb3pmpqrx9wz63m35wa6xqng9vlcscm";
}
{ };
generic-optics = self.callHackageDirect
{
pkg = "generic-optics";
ver = "2.0.0.0";
sha256 = "0xy5k5b35w1i1zxy0dv5fk1b3zrd3hx3v5kh593k2la7ri880wmq";
}
{ };
optics-core = self.callHackage "optics-core" "0.3.0.1" { };
optics-th = self.callHackage "optics-th" "0.3.0.2" { };
optics-extra = self.callHackage "optics-extra" "0.3" { };
optics = self.callHackage "optics" "0.3" { };
};
packages = {
kassandra = ./kassandra;
standalone = ./standalone;
};
}
)

View file

@ -28,8 +28,9 @@ frontend =
} }
frontendBody :: WidgetJSM t m => m () frontendBody :: WidgetJSM t m => m ()
frontendBody = D.dyn_ . fmap (maybe pass mainWidget) frontendBody =
=<< remoteBackendWidget (wrap D.never) Nothing D.dyn_ . fmap (maybe pass mainWidget)
=<< remoteBackendWidget (wrap D.never) Nothing
css = cssAsText (static @"MaterialIcons-Regular-Outlined.otf") css = cssAsText (static @"MaterialIcons-Regular-Outlined.otf")

View file

@ -12,7 +12,7 @@ import Control.Category
import qualified Control.Category import qualified Control.Category
import Data.Text (Text) import Data.Text (Text)
--import Data.Functor.Identity -- import Data.Functor.Identity
import Obelisk.Route import Obelisk.Route
import Obelisk.Route.TH import Obelisk.Route.TH
@ -41,7 +41,7 @@ fullRouteEncoder =
(FullRoute_Backend BackendRouteMissing :/ ()) (FullRoute_Backend BackendRouteMissing :/ ())
( \case ( \case
BackendRouteMissing -> PathSegment "missing" $ unitEncoder mempty BackendRouteMissing -> PathSegment "missing" $ unitEncoder mempty
BackendRouteSocket -> PathSegment "socket" $ Control.Category.id BackendRouteSocket -> PathSegment "socket" Control.Category.id
) )
( \case ( \case
FrontendRouteMain -> PathEnd $ unitEncoder mempty FrontendRouteMain -> PathEnd $ unitEncoder mempty

View file

@ -23,7 +23,7 @@ module Kassandra.Config (
TaskwarriorOption (..), TaskwarriorOption (..),
) where ) where
import Data.Default.Class ( Default(..) ) import Data.Default.Class (Default (..))
import Data.Password.Argon2 ( import Data.Password.Argon2 (
Argon2, Argon2,
PasswordHash, PasswordHash,
@ -142,22 +142,22 @@ data PasswordConfig = Prompt | Password {plaintext :: Text} | PasswordCommand {c
data LocalBackend data LocalBackend
= TaskwarriorBackend = TaskwarriorBackend
{ -- | Set config file { taskRcPath :: Maybe Text
taskRcPath :: Maybe Text -- ^ Set config file
, -- | Set task data directory , taskDataPath :: Maybe Text
taskDataPath :: Maybe Text -- ^ Set task data directory
, -- | Override config variables , taskConfig :: Seq TaskwarriorOption
taskConfig :: Seq TaskwarriorOption -- ^ Override config variables
, -- | Path to taskwarrior binary. Nothing => Lookup "task" from PATH , taskBin :: Maybe Text
taskBin :: Maybe Text -- ^ Path to taskwarrior binary. Nothing => Lookup "task" from PATH
, -- | Use the first free port from the given range for the taskwarrior hook listener. , hookListenPort :: PortConfig
hookListenPort :: PortConfig -- ^ Use the first free port from the given range for the taskwarrior hook listener.
, -- | Created hooks are called ".on-add.<suffix>.<port>" and ".on-remove.<suffix>.<port>" , hookSuffix :: Text
hookSuffix :: Text -- ^ Created hooks are called ".on-add.<suffix>.<port>" and ".on-remove.<suffix>.<port>"
, -- | Ensure existence of taskwarrior hook on every start , createHooksOnStart :: Bool
createHooksOnStart :: Bool -- ^ Ensure existence of taskwarrior hook on every start
, -- | Remove hook on exit. , removeHooksOnExit :: Bool
removeHooksOnExit :: Bool -- ^ Remove hook on exit.
} }
| GitBackend | GitBackend
{ directoryPath :: Text { directoryPath :: Text

View file

@ -31,10 +31,11 @@ css fontPath = do
fontFace $ do fontFace $ do
fontFamily [fontName] [] fontFamily [fontName] []
fontFaceSrc [FontFaceSrcUrl fontSrc (Just OpenType)] fontFaceSrc [FontFaceSrcUrl fontSrc (Just OpenType)]
let --darkBlue = rgb 0 0 33 let
lightBlue = rgb 200 200 255 -- darkBlue = rgb 0 0 33
noMargin = margin (px 0) (px 0) (px 0) (px 0) lightBlue = rgb 200 200 255
noPadding = padding (px 0) (px 0) (px 0) (px 0) noMargin = margin (px 0) (px 0) (px 0) (px 0)
noPadding = padding (px 0) (px 0) (px 0) (px 0)
star ? do star ? do
fontFamily ["B612"] [] fontFamily ["B612"] []
noMargin noMargin
@ -91,13 +92,13 @@ css fontPath = do
color white color white
".button" ? buttonCss ".button" ? buttonCss
".selector" ? buttonCss ".selector" ? buttonCss
--".tag" ? ".icon" ? do -- ".tag" ? ".icon" ? do
--position absolute -- position absolute
--borderRadius tagRadius tagRadius tagRadius tagRadius -- borderRadius tagRadius tagRadius tagRadius tagRadius
--background lightBlue -- background lightBlue
--marginLeft (em (-1.1)) -- marginLeft (em (-1.1))
--marginTop (em 0.70) -- marginTop (em 0.70)
--fontSize (em 0.85) -- fontSize (em 0.85)
".material-icons" ? do ".material-icons" ? do
fontFamily [fontName] [] fontFamily [fontName] []
fontWeight normal fontWeight normal
@ -141,9 +142,9 @@ css fontPath = do
".children" ? do ".children" ? do
padding (px 0) (px 0) (px 0) leftBarWidth padding (px 0) (px 0) (px 0) leftBarWidth
background black background black
--".slimButton" ? do -- ".slimButton" ? do
--marginRight (px (-5)) -- marginRight (px (-5))
--marginLeft (px (-5)) -- marginLeft (px (-5))
let blockSize = do let blockSize = do
width (em 1) width (em 1)
height (em 1) height (em 1)
@ -162,5 +163,6 @@ css fontPath = do
".grey" & color (grayish 160) ".grey" & color (grayish 160)
".show" & color black ".show" & color black
".showable" & display none ".showable" & display none
active & i ? do active
background black & i ? do
background black

View file

@ -21,6 +21,9 @@ import Reflex as R
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
import qualified Debug.Trace as Trace import qualified Debug.Trace as Trace
import Relude.Extra.Bifunctor
import Relude.Extra.Enum
import Say
import System.Console.ANSI ( import System.Console.ANSI (
Color (..), Color (..),
ColorIntensity (Vivid), ColorIntensity (Vivid),
@ -28,9 +31,6 @@ import System.Console.ANSI (
SGR (..), SGR (..),
setSGRCode, setSGRCode,
) )
import Relude.Extra.Bifunctor
import Relude.Extra.Enum
import Say
data Severity = Debug | Info | Warning | Error deriving stock (Show, Read, Eq, Ord) data Severity = Debug | Info | Warning | Error deriving stock (Show, Read, Eq, Ord)
@ -149,7 +149,8 @@ showSeverity = \case
where where
color :: Color -> Text -> Text color :: Color -> Text -> Text
color c txt = color c txt =
toText (setSGRCode [SetColor Foreground Vivid c]) <> txt toText (setSGRCode [SetColor Foreground Vivid c])
<> txt
<> toText <> toText
(setSGRCode [Reset]) (setSGRCode [Reset])

View file

@ -9,7 +9,7 @@ import qualified Data.Sequence as Seq
import qualified Data.Sequence.NonEmpty as NESeq import qualified Data.Sequence.NonEmpty as NESeq
import qualified Data.Set as Set import qualified Data.Set as Set
import Kassandra.AgendaWidget (agendaWidget) import Kassandra.AgendaWidget (agendaWidget)
import Kassandra.BaseWidgets (button, br) import Kassandra.BaseWidgets (br, button)
import Kassandra.Calendar (CalendarEvent) import Kassandra.Calendar (CalendarEvent)
import Kassandra.Config (DefinitionElement, Widget (DefinitionElementWidget, SearchWidget)) import Kassandra.Config (DefinitionElement, Widget (DefinitionElementWidget, SearchWidget))
import Kassandra.Debug ( import Kassandra.Debug (
@ -183,17 +183,20 @@ nextWidget = do
<$> getTasks <$> getTasks
D.dynText $ D.dynText $
(\x y -> if length x + length y > 0 then [i|There are #{length x} tasks in the inbox and #{length y} tasks unsorted.|] else "Nothing to do.") (\x y -> if length x + length y > 0 then [i|There are #{length x} tasks in the inbox and #{length y} tasks unsorted.|] else "Nothing to do.")
<$> inboxTasks <*> unsortedTasks <$> inboxTasks
<*> unsortedTasks
inboxTaskDyn <- R.maybeDyn $ Seq.lookup 0 <$> inboxTasks inboxTaskDyn <- R.maybeDyn $ Seq.lookup 0 <$> inboxTasks
let decorateSortTask x = D.el "p" (D.text "Sort this task into the task tree:") *> taskTreeWidget x let decorateSortTask x = D.el "p" (D.text "Sort this task into the task tree:") *> taskTreeWidget x
decorateInboxTask x = D.el "p" $ do decorateInboxTask x =
D.text "Process this task from the inbox:" *> br D.el "p" $
D.text "1. Does it need to be done?" *> br do
D.text "2. Can you do it in under 2 minutes?" *> br D.text "Process this task from the inbox:" *> br
D.text "3. Should someone else do this?" *> br D.text "1. Does it need to be done?" *> br
D.text "4. Should you split this task into sub tasks?" *> br D.text "2. Can you do it in under 2 minutes?" *> br
D.text "5. On which tag list does it belong or when do you want to do it?" *> br D.text "3. Should someone else do this?" *> br
*> taskTreeWidget x D.text "4. Should you split this task into sub tasks?" *> br
D.text "5. On which tag list does it belong or when do you want to do it?" *> br
*> taskTreeWidget x
sortTask = do sortTask = do
taskDyn <- R.maybeDyn $ viaNonEmpty head <$> unsortedTasks taskDyn <- R.maybeDyn $ viaNonEmpty head <$> unsortedTasks
D.dyn_ (maybe pass decorateSortTask <$> taskDyn) D.dyn_ (maybe pass decorateSortTask <$> taskDyn)

View file

@ -10,7 +10,8 @@ module Kassandra.ReflexUtil (
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Patch.Map as Patch import qualified Data.Patch.Map as Patch
--import qualified Data.Patch.MapWithMove as Patch
-- import qualified Data.Patch.MapWithMove as Patch
import qualified Data.Sequence as Seq import qualified Data.Sequence as Seq
import qualified Reflex as R import qualified Reflex as R
import qualified Reflex.Dom as D import qualified Reflex.Dom as D
@ -29,27 +30,28 @@ smartSimpleList ::
m () m ()
smartSimpleList widget listElements = do smartSimpleList widget listElements = do
void $ R.simpleList (toList <$> listElements) \vDyn -> do void $ R.simpleList (toList <$> listElements) \vDyn -> do
u <- R.holdUniqDyn vDyn u <- R.holdUniqDyn vDyn
D.dyn_ . fmap widget $ u D.dyn_ . fmap widget $ u
--postBuild <- R.getPostBuild
--keyMap <- R.holdUniqDyn $ Seq.foldMapWithIndex (curry one) <$> listElements
--let keyMapChange =
--R.attachWith
--((Newtype.under @(Map Int (Patch.NodeInfo Int v)) fixPatchMap .) . Patch.patchThatChangesMap)
--(R.current keyMap)
--(R.updated keyMap)
--initialKeyMap = Patch.patchMapWithMoveInsertAll <$> R.tag (R.current keyMap) postBuild
--keyMapEvents = keyMapChange <> initialKeyMap
--void $ R.mapMapWithAdjustWithMove (const widget) mempty keyMapEvents
-- | A workaround for a bug in patchThatChangesMap in patch 0.0.3.2. -- postBuild <- R.getPostBuild
--fixPatchMap :: Map Int (Patch.NodeInfo Int v) -> Map Int (Patch.NodeInfo Int v) -- keyMap <- R.holdUniqDyn $ Seq.foldMapWithIndex (curry one) <$> listElements
--fixPatchMap inputMap = appEndo setMoves . fmap (Patch.nodeInfoSetTo Nothing) $ inputMap -- let keyMapChange =
-- where -- R.attachWith
-- setMoves = Map.foldMapWithKey f inputMap -- ((Newtype.under @(Map Int (Patch.NodeInfo Int v)) fixPatchMap .) . Patch.patchThatChangesMap)
-- f to' (Patch.NodeInfo (Patch.From_Move from) _) = Endo $ Map.adjust (Patch.nodeInfoSetTo (Just to')) from -- (R.current keyMap)
-- f _ _ = mempty -- (R.updated keyMap)
-- initialKeyMap = Patch.patchMapWithMoveInsertAll <$> R.tag (R.current keyMap) postBuild
-- keyMapEvents = keyMapChange <> initialKeyMap
-- void $ R.mapMapWithAdjustWithMove (const widget) mempty keyMapEvents
{- | A workaround for a bug in patchThatChangesMap in patch 0.0.3.2.
fixPatchMap :: Map Int (Patch.NodeInfo Int v) -> Map Int (Patch.NodeInfo Int v)
fixPatchMap inputMap = appEndo setMoves . fmap (Patch.nodeInfoSetTo Nothing) $ inputMap
where
setMoves = Map.foldMapWithKey f inputMap
f to' (Patch.NodeInfo (Patch.From_Move from) _) = Endo $ Map.adjust (Patch.nodeInfoSetTo (Just to')) from
f _ _ = mempty
-}
listWithGaps :: listWithGaps ::
(R.Adjustable t m, R.PostBuild t m, R.MonadHold t m, MonadFix m, Ord v, D.NotReady t m) => (R.Adjustable t m, R.PostBuild t m, R.MonadHold t m, MonadFix m, Ord v, D.NotReady t m) =>
(v -> m ()) -> (v -> m ()) ->
@ -62,9 +64,9 @@ listWithGaps widget gapWidget listD = do
gapWidget lastElementD gapWidget lastElementD
where where
elementWidget currentElement = do elementWidget currentElement = do
elementPair <- R.holdUniqDyn $ (,Just currentElement) . Map.lookup currentElement <$> prevElementsD elementPair <- R.holdUniqDyn $ (,Just currentElement) . Map.lookup currentElement <$> prevElementsD
gapWidget elementPair gapWidget elementPair
widget currentElement widget currentElement
prevElementsD = (\xs -> Map.unions . fmap one $ Seq.zip (Seq.drop 1 xs) xs) <$> listD prevElementsD = (\xs -> Map.unions . fmap one $ Seq.zip (Seq.drop 1 xs) xs) <$> listD
keyDynamic :: keyDynamic ::

View file

@ -51,7 +51,7 @@ remoteBackendWidget closeEvent mayBackend = D.divClass "remoteBackend" $ do
backendDyn <- maybe inputBackend getPassword mayBackend backendDyn <- maybe inputBackend getPassword mayBackend
responseEvent <- responseEvent <-
D.dyn D.dyn
(withBackend (closeEvent <> wrap (() <$ R.updated backendDyn)) <$> backendDyn) (withBackend (closeEvent <> wrap (void $ R.updated backendDyn)) <$> backendDyn)
D.holdDyn Nothing responseEvent D.holdDyn Nothing responseEvent
where where
getPassword :: RemoteBackend PasswordConfig -> m (R.Dynamic t (Maybe (RemoteBackend Text))) getPassword :: RemoteBackend PasswordConfig -> m (R.Dynamic t (Maybe (RemoteBackend Text)))
@ -122,9 +122,9 @@ remoteBackendWidget closeEvent mayBackend = D.divClass "remoteBackend" $ do
D.inputElement $ D.inputElement $
D.def D.def
& lensVL D.inputElementConfig_initialValue & lensVL D.inputElementConfig_initialValue
.~ defaultValue .~ defaultValue
& lensVL (D.inputElementConfig_elementConfig . D.elementConfig_initialAttributes) & lensVL (D.inputElementConfig_elementConfig . D.elementConfig_initialAttributes)
.~ if hidden then "type" D.=: "password" else mempty .~ if hidden then "type" D.=: "password" else mempty
inputValue = R.current . D._inputElement_value inputValue = R.current . D._inputElement_value
data WebSocketState = WebSocketError Text | Connecting deriving stock (Show) data WebSocketState = WebSocketError Text | Connecting deriving stock (Show)

View file

@ -7,12 +7,12 @@ module Kassandra.Sorting (
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import Data.Scientific (toRealFloat) import Data.Scientific (toRealFloat)
import qualified Data.Sequence as Seq
import Data.Set (member) import Data.Set (member)
import Kassandra.Types (TaskInfos) import Kassandra.Types (TaskInfos)
import qualified Reflex as R import qualified Reflex as R
import Relude.Extra.Foldable1 (maximum1) import Relude.Extra.Foldable1 (maximum1)
import qualified Taskwarrior.Task as Task import qualified Taskwarrior.Task as Task
import qualified Data.Sequence as Seq
data SortMode = SortModePartof UUID | SortModeTag Task.Tag data SortMode = SortModePartof UUID | SortModeTag Task.Tag
deriving stock (Show, Eq, Ord, Generic) deriving stock (Show, Eq, Ord, Generic)
@ -80,9 +80,9 @@ sortingChanges mode list =
| otherwise = assureSort minTouchedDist sortedList | otherwise = assureSort minTouchedDist sortedList
getWrite (task, sortState) getWrite (task, sortState)
| has #_WillWrite sortState || not (taskInList mode task) = | has #_WillWrite sortState || not (taskInList mode task) =
Just . setSortOrder mode (newValue sortState) . insertInList mode $ task Just . setSortOrder mode (newValue sortState) . insertInList mode $ task
| otherwise = | otherwise =
Nothing Nothing
in mapMaybe getWrite finalList in mapMaybe getWrite finalList
applyUntil :: (a -> a) -> (a -> Bool) -> a -> a applyUntil :: (a -> a) -> (a -> Bool) -> a -> a
@ -92,7 +92,7 @@ applyUntil f condition x
minOrder, maxOrder, minDist, minTouchedDist :: Double minOrder, maxOrder, minDist, minTouchedDist :: Double
minOrder = -1 minOrder = -1
maxOrder = - minOrder maxOrder = -minOrder
minDist = 10 ** (-6) minDist = 10 ** (-6)
minTouchedDist = 10 ** (-3) minTouchedDist = 10 ** (-3)
@ -111,9 +111,9 @@ unSetWorstUnsorted unSet delta (IsNonEmpty (x :<|| xs))
Seq.breakl Seq.breakl
((worst ==) . snd) ((worst ==) . snd)
(toSeq badnesses) = (toSeq badnesses) =
fine <> (unSet a <| alsoFine) fine <> (unSet a <| alsoFine)
| otherwise = | otherwise =
error "Assumed wrong invariant in unSetWorstUnsorted" -- The list of badnesses has to contain its maximum error "Assumed wrong invariant in unSetWorstUnsorted" -- The list of badnesses has to contain its maximum
where where
badnesses = go mempty (x :<|| xs) <&> \(a, _, badness) -> (a, badness) badnesses = go mempty (x :<|| xs) <&> \(a, _, badness) -> (a, badness)
worst = maximum1 $ snd <$> badnesses worst = maximum1 $ snd <$> badnesses
@ -146,16 +146,16 @@ addSortState f = go (minOrder, 0)
go (iprev, dprev) list go (iprev, dprev) list
| IsEmpty <- list = mempty | IsEmpty <- list = mempty
| IsNonEmpty (x :<|| xs) <- list | IsNonEmpty (x :<|| xs) <- list
, Just int <- f x = , Just int <- f x =
(x, HasSortPos int) <| go (int, 0) xs (x, HasSortPos int) <| go (int, 0) xs
| IsNonEmpty (x :<|| xs) <- list | IsNonEmpty (x :<|| xs) <- list
, next@(IsNonEmpty ((_, sortStateNext -> (inext, dnext)) :<|| _)) <- , next@(IsNonEmpty ((_, sortStateNext -> (inext, dnext)) :<|| _)) <-
go go
(iprev, dprev + 1) (iprev, dprev + 1)
xs = xs =
(x, WillWrite iprev (dprev + 1) inext (dnext + 1)) <| next (x, WillWrite iprev (dprev + 1) inext (dnext + 1)) <| next
| IsNonEmpty (x :<|| _) <- list = | IsNonEmpty (x :<|| _) <- list =
one (x, WillWrite iprev (dprev + 1) maxOrder 1) one (x, WillWrite iprev (dprev + 1) maxOrder 1)
insertBefore :: Seq Task -> Seq Task -> Maybe UUID -> Seq Task insertBefore :: Seq Task -> Seq Task -> Maybe UUID -> Seq Task
insertBefore list toInsert = \case insertBefore list toInsert = \case

View file

@ -10,8 +10,9 @@ import qualified Data.Sequence.NonEmpty as NESeq
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Text as Text import qualified Data.Text as Text
import Kassandra.BaseWidgets ( import Kassandra.BaseWidgets (
br,
button, button,
icon, br icon,
) )
import Kassandra.Config (DefinitionElement) import Kassandra.Config (DefinitionElement)
import Kassandra.Debug ( import Kassandra.Debug (
@ -138,7 +139,8 @@ dependenciesWidget = do
revDepends <- filter stillTodo <<$>> lookupTasksM (taskInfos ^. #revDepends) revDepends <- filter stillTodo <<$>> lookupTasksM (taskInfos ^. #revDepends)
depends <- filter stillTodo <<$>> (lookupTasksM . toList) (taskInfos ^. #depends) depends <- filter stillTodo <<$>> (lookupTasksM . toList) (taskInfos ^. #depends)
D.dyn_ $ D.dyn_ $
whenNotNull <$> depends whenNotNull
<$> depends
<*> pure <*> pure
( \ds -> do ( \ds -> do
br br
@ -150,7 +152,8 @@ dependenciesWidget = do
br br
) )
D.dyn_ $ D.dyn_ $
whenJust . nonEmptySeq <$> revDepends whenJust . nonEmptySeq
<$> revDepends
<*> pure <*> pure
( \rds -> do ( \rds -> do
br br
@ -186,7 +189,8 @@ dropChildWidget = do
childrenD <- getChildren childrenD <- getChildren
showIcon <- fmap not <$> getIsExpanded (taskInfos ^. #uuid) showIcon <- fmap not <$> getIsExpanded (taskInfos ^. #uuid)
D.dyn_ $ D.dyn_ $
when <$> showIcon when
<$> showIcon
<*> pure <*> pure
( childDropArea ( childDropArea
( SortPosition ( SortPosition
@ -204,11 +208,11 @@ dropChildWidget = do
(icon "dropHere plusOne" "block") (icon "dropHere plusOne" "block")
$ fmap $ fmap
( \dependencies -> ( \dependencies ->
one $ one
#depends $ #depends
%~ Set.union (Set.fromList $ toList $ (^. #uuid) <$> dependencies) %~ Set.union (Set.fromList $ toList $ (^. #uuid) <$> dependencies)
$ taskInfos $ taskInfos
^. #task ^. #task
) )
taskDropArea taskDropArea
(taskInfos ^. #uuid % to (R.constDyn . one)) (taskInfos ^. #uuid % to (R.constDyn . one))
@ -245,7 +249,8 @@ childrenWidget taskInfosD = do
showOptional :: Bool -> m () showOptional :: Bool -> m ()
showOptional x = when x $ do showOptional x = when x $ do
children <- children <-
R.holdUniqDyn . fmap (filter stillTodo) =<< lookupTasksDynM R.holdUniqDyn . fmap (filter stillTodo)
=<< lookupTasksDynM
=<< R.holdUniqDyn =<< R.holdUniqDyn
(taskInfosD ^. mapping #children) (taskInfosD ^. mapping #children)
let sortModeD = SortModePartof <$> taskInfosD ^. mapping #uuid let sortModeD = SortModePartof <$> taskInfosD ^. mapping #uuid
@ -256,7 +261,6 @@ childrenWidget taskInfosD = do
D.divClass "children" $ D.divClass "children" $
taskList (sortModeD ^. #current) sortedList blacklist taskWidget taskList (sortModeD ^. #current) sortedList blacklist taskWidget
taskList :: taskList ::
StandardWidget t m r e => StandardWidget t m r e =>
R.Behavior t SortMode -> R.Behavior t SortMode ->
@ -368,7 +372,8 @@ statusWidget = do
(el, ()) <- D.elAttr' "div" ("class" =: "checkbox") $ do (el, ()) <- D.elAttr' "div" ("class" =: "checkbox") $ do
D.elClass D.elClass
"i" "i"
( "material-icons " <> showClass ( "material-icons "
<> showClass
<> if isJust handlerMay <> if isJust handlerMay
then " hideable" then " hideable"
else "" else ""

View file

@ -53,11 +53,12 @@ module Prelude (
pattern IsNonEmpty, pattern IsNonEmpty,
pattern (:<||), pattern (:<||),
pattern (:||>), pattern (:||>),
(|>),(<|), (|>),
(<|),
toSeq, toSeq,
mapMaybe, mapMaybe,
filter, filter,
partitionEithersNESeq partitionEithersNESeq,
) where ) where
import Control.Concurrent.Async ( import Control.Concurrent.Async (
@ -81,12 +82,11 @@ import Data.Generics.Product.Typed (HasType (typed))
import Data.Generics.Sum.Constructors (AsConstructor' (_Ctor')) import Data.Generics.Sum.Constructors (AsConstructor' (_Ctor'))
import Data.Generics.Sum.Typed (AsType (_Typed)) import Data.Generics.Sum.Typed (AsType (_Typed))
import Data.List.Extra (firstJust) import Data.List.Extra (firstJust)
import Data.Sequence.NonEmpty hiding (filter, (|>), (<|)) import Data.Sequence ((<|), (|>))
import Data.Sequence ((|>),(<|)) import Data.Sequence.NonEmpty hiding (filter, (<|), (|>))
import Data.String.Interpolate (i) import Data.String.Interpolate (i)
import Data.Text.Optics hiding (text) import Data.Text.Optics hiding (text)
import Data.These (partitionEithersNE, These(..)) import Data.These (These (..), partitionEithersNE)
import Data.Witherable ( mapMaybe, (<$?>), (<&?>), filter )
import Data.Time ( import Data.Time (
UTCTime, UTCTime,
addUTCTime, addUTCTime,
@ -105,14 +105,15 @@ import Data.Time.LocalTime (
zonedTimeToLocalTime, zonedTimeToLocalTime,
) )
import Data.UUID (UUID) import Data.UUID (UUID)
import Data.Witherable (filter, mapMaybe, (<$?>), (<&?>))
import Language.Haskell.TH.Syntax ( import Language.Haskell.TH.Syntax (
Dec, Dec,
Name, Name,
Q, Q,
) )
import Optics hiding ((|>), (<|)) import Optics hiding ((<|), (|>))
import Optics.TH import Optics.TH
import Relude hiding (uncons, mapMaybe, filter) import Relude hiding (filter, mapMaybe, uncons)
import Relude.Extra.Foldable1 import Relude.Extra.Foldable1
import Taskwarrior.Status (Status) import Taskwarrior.Status (Status)
import Taskwarrior.Task (Task) import Taskwarrior.Task (Task)
@ -122,7 +123,7 @@ instance One (NESeq a) where
one = singleton one = singleton
instance Foldable1 NESeq where instance Foldable1 NESeq where
foldMap1 f = foldMapWithIndex (const f) foldMap1 f = foldMapWithIndex (const f)
-- (lensField .~ noPrefixNamer $ fieldLabelsRules) == noPrefixFieldLabels but only in optics-th 0.2 -- (lensField .~ noPrefixNamer $ fieldLabelsRules) == noPrefixFieldLabels but only in optics-th 0.2
makeLabels :: Name -> Q [Dec] makeLabels :: Name -> Q [Dec]

View file

@ -1 +1 @@
(import ./. { }).shells.ghc (import ./. {}).shells.ghc

View file

@ -1,18 +1,16 @@
{ pkgs ? import (import nix/sources.nix).nixpkgs { } }: {pkgs ? import (import nix/sources.nix).nixpkgs {}}: let
let
haskellPackages = pkgs.haskellPackages.extend ( haskellPackages = pkgs.haskellPackages.extend (
self: super: { self: super: {
kassandra = self.callCabal2nix "kassandra" ./kassandra { }; kassandra = self.callCabal2nix "kassandra" ./kassandra {};
standalone = self.callCabal2nix "standalone" ./standalone { }; standalone = self.callCabal2nix "standalone" ./standalone {};
} }
); );
reflex-platform = import ./. { }; reflex-platform = import ./. {};
in in {
{
lib = haskellPackages.kassandra; lib = haskellPackages.kassandra;
app = haskellPackages.standalone; app = haskellPackages.standalone;
server = reflex-platform.exe; server = reflex-platform.exe;
android = pkgs.runCommand "kassandra-android-apk" { } '' android = pkgs.runCommand "kassandra-android-apk" {} ''
mkdir -p $out mkdir -p $out
cp ${reflex-platform.android.frontend}/android-app-release-unsigned.apk $out/de.maralorn.kassandra_${import ./code.nix}.apk cp ${reflex-platform.android.frontend}/android-app-release-unsigned.apk $out/de.maralorn.kassandra_${import ./code.nix}.apk
''; '';

View file

@ -73,9 +73,9 @@ import Kassandra.Debug (Severity (..), log)
import qualified Streamly.Data.Fold as FL import qualified Streamly.Data.Fold as FL
import Streamly.External.ByteString (fromArray, toArray) import Streamly.External.ByteString (fromArray, toArray)
import qualified Streamly.FileSystem.Handle as FS import qualified Streamly.FileSystem.Handle as FS
import Streamly.Internal.Data.Array.Stream.Foreign (splitOn)
import qualified Streamly.Internal.FileSystem.File as FSFile import qualified Streamly.Internal.FileSystem.File as FSFile
import Streamly.Memory.Array as Mem (fromList) import Streamly.Memory.Array as Mem (fromList)
import Streamly.Internal.Data.Array.Stream.Foreign (splitOn)
dirName :: FilePath dirName :: FilePath
dirName = "/home/maralorn/.calendars/" dirName = "/home/maralorn/.calendars/"
@ -121,12 +121,12 @@ saveCache cache = do
writeJSONStream :: (IsStream t, MonadIO (t IO), ToJSON k, ToJSON v) => STM.Map k v -> FilePath -> t IO () writeJSONStream :: (IsStream t, MonadIO (t IO), ToJSON k, ToJSON v) => STM.Map k v -> FilePath -> t IO ()
writeJSONStream stmMap fileName = writeJSONStream stmMap fileName =
FSFile.withFile fileName WriteMode \handle -> FSFile.withFile fileName WriteMode \handle ->
liftIO $ liftIO
S.fold (FS.writeChunks handle) $ S.fold (FS.writeChunks handle)
. asyncly . asyncly
. S.intersperse (Mem.fromList [10]) . S.intersperse (Mem.fromList [10])
. fmap (toArray . toStrict . encode) . fmap (toArray . toStrict . encode)
$ streamSTMMap stmMap $ streamSTMMap stmMap
streamSTMMap :: forall t k v. (MonadIO (t IO), IsStream t) => STM.Map k v -> t IO (k, v) streamSTMMap :: forall t k v. (MonadIO (t IO), IsStream t) => STM.Map k v -> t IO (k, v)
streamSTMMap = join . atomically . UnfoldlM.foldlM' (\x y -> pure $ S.cons y x) S.nil . STM.unfoldlM streamSTMMap = join . atomically . UnfoldlM.foldlM' (\x y -> pure $ S.cons y x) S.nil . STM.unfoldlM
@ -145,7 +145,8 @@ data Cache = Cache
{ icsCache :: ICSCache { icsCache :: ICSCache
, tzCache :: TZCache , tzCache :: TZCache
, uidCache :: UIDCache , uidCache :: UIDCache
} deriving (Generic) }
deriving (Generic)
newCache :: IO Cache newCache :: IO Cache
newCache = atomically $ Cache <$> STM.new <*> STM.new <*> STM.new newCache = atomically $ Cache <$> STM.new <*> STM.new <*> STM.new
@ -284,11 +285,11 @@ translateEvent cache calendarName vEvent =
-- Also we are ignoring the timezone delivered with this calendar and taking our own -- Also we are ignoring the timezone delivered with this calendar and taking our own
getTimes getTimes
| Just (DTStartDateTime start _) <- veDTStart vEvent | Just (DTStartDateTime start _) <- veDTStart vEvent
, Just (Left (DTEndDateTime end _)) <- veDTEndDuration vEvent = , Just (Left (DTEndDateTime end _)) <- veDTEndDuration vEvent =
S.yieldM . liftIO $ SimpleEvent <$> datetimeToTZTime cache start <*> datetimeToTZTime cache end S.yieldM . liftIO $ SimpleEvent <$> datetimeToTZTime cache start <*> datetimeToTZTime cache end
| Just (dateValue . dtStartDateValue -> start) <- veDTStart vEvent | Just (dateValue . dtStartDateValue -> start) <- veDTStart vEvent
, Just (Left (dateValue . dtEndDateValue -> end)) <- veDTEndDuration vEvent = , Just (Left (dateValue . dtEndDateValue -> end)) <- veDTEndDuration vEvent =
S.yield $ AllDayEvent start (addDays (-1) end) S.yield $ AllDayEvent start (addDays (-1) end)
| otherwise = S.nil | otherwise = S.nil
datetimeToTZTime :: Cache -> DateTime -> IO TZTime datetimeToTZTime :: Cache -> DateTime -> IO TZTime

View file

@ -6,14 +6,15 @@ module Kassandra.Standalone.Config (
StandaloneAccount (LocalAccount, RemoteAccount), StandaloneAccount (LocalAccount, RemoteAccount),
BackendConfig (..), BackendConfig (..),
backends, backends,
dhallTypes dhallTypes,
) where ) where
import Dhall (FromDhall) import Dhall (FromDhall)
import Kassandra.Config ( import Kassandra.Config (
Dict,
AccountConfig, AccountConfig,
DefinitionElement, DefinitionElement,
Dict,
ListItem,
ListQuery, ListQuery,
LocalBackend, LocalBackend,
NamedBackend, NamedBackend,
@ -24,7 +25,7 @@ import Kassandra.Config (
TaskwarriorOption, TaskwarriorOption,
TreeOption, TreeOption,
UserConfig, UserConfig,
Widget, ListItem Widget,
) )
import Kassandra.Config.Dhall ( import Kassandra.Config.Dhall (
DhallLoadConfig (..), DhallLoadConfig (..),
@ -42,7 +43,7 @@ data StandaloneAccount = RemoteAccount {backend :: Maybe (RemoteBackend Password
deriving stock (Show, Eq, Ord, Generic) deriving stock (Show, Eq, Ord, Generic)
deriving anyclass (FromDhall) deriving anyclass (FromDhall)
data BackendConfig = BackendConfig newtype BackendConfig = BackendConfig
{ users :: Dict AccountConfig { users :: Dict AccountConfig
} }
deriving (Show, Eq, Generic, FromDhall) deriving (Show, Eq, Generic, FromDhall)

View file

@ -1,41 +1,44 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BlockArguments #-} {-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}
import Prelude ( ) import Control.Error (
import Relude throwE,
import qualified Notmuch tryJust,
import Say tryRight,
import Data.String.Interpolate withExceptT,
import qualified Data.MIME as MIME )
import Data.MIME.Charset import Control.Lens hiding (argument)
import Control.Lens hiding ( argument ) import Control.Monad.Catch (
import Control.Error ( withExceptT MonadCatch,
, throwE handleIOError,
, tryJust, tryRight )
)
import qualified Data.Text as T
import Control.Monad.Catch ( MonadCatch
, handleIOError
)
import Data.Time
import Relude.Extra.Group
import qualified Data.Map as Map
import qualified Options.Applicative as O
import Text.Atom.Feed.Export ( textFeed )
import Text.Atom.Feed
import Text.HTML.TagSoup
import Data.Either.Extra (mapLeft) import Data.Either.Extra (mapLeft)
import Data.MIME qualified as MIME
import Data.MIME.Charset
import Data.Map qualified as Map
import Data.String.Interpolate
import Data.Text qualified as T
import Data.Time
import Notmuch qualified
import Options.Applicative qualified as O
import Relude
import Relude.Extra.Group
import Say
import Text.Atom.Feed
import Text.Atom.Feed.Export (textFeed)
import Text.HTML.TagSoup
import Prelude ()
data Options = Options data Options = Options
{ dbPath :: String { dbPath :: String
@ -43,166 +46,184 @@ data Options = Options
} }
data Thread = Thread data Thread = Thread
{ subject :: Text { subject :: Text
, threadid :: ByteString , threadid :: ByteString
, authors :: [Text] , authors :: [Text]
, date :: UTCTime , date :: UTCTime
, totalCount :: Int , totalCount :: Int
, messages :: [Message] , messages :: [Message]
} }
type Error = Text type Error = Text
data Body = HTMLBody Text | TextBody Text data Body = HTMLBody Text | TextBody Text
data Message = Message data Message = Message
{ date :: UTCTime { date :: UTCTime
, headers :: [(Text, Text)] , headers :: [(Text, Text)]
, body :: Body , body :: Body
} }
main :: IO () main :: IO ()
main = do main = do
Options { dbPath, folder } <- O.execParser $ O.info Options{dbPath, folder} <-
( Options O.execParser $
<$> O.argument O.info
O.str ( Options
( O.metavar "DBPATH" <$> O.argument
<> O.help "The full path to the notmuch database" O.str
) ( O.metavar "DBPATH"
<*> O.argument <> O.help "The full path to the notmuch database"
O.str )
(O.metavar "FOLDER" <> O.help "The maildir to scan for messages.") <*> O.argument
<**> O.helper O.str
) (O.metavar "FOLDER" <> O.help "The maildir to scan for messages.")
O.fullDesc <**> O.helper
)
O.fullDesc
res <- runExceptT do res <- runExceptT do
(thrds, msgs) <- withExceptT (thrds, msgs) <- withExceptT
(\(er :: Notmuch.Status) -> ( \(er :: Notmuch.Status) ->
[i|Failed to read notmuch data.\ndb path: #{dbPath}\nquery: Folder #{folder}\nerror: #{er}|] [i|Failed to read notmuch data.\ndb path: #{dbPath}\nquery: Folder #{folder}\nerror: #{er}|]
) )
do do
db <- Notmuch.databaseOpenReadOnly dbPath db <- Notmuch.databaseOpenReadOnly dbPath
q <- Notmuch.query db (Notmuch.Folder folder) q <- Notmuch.query db (Notmuch.Folder folder)
(,) <$> Notmuch.threads q <*> Notmuch.messages q (,) <$> Notmuch.threads q <*> Notmuch.messages q
msgsByThread <- forM msgs \msg -> Notmuch.threadId msg <&> (, Right msg) msgsByThread <- forM msgs \msg -> Notmuch.threadId msg <&> (,Right msg)
thrdsByThread <- forM thrds \thrd -> Notmuch.threadId thrd <&> (, Left thrd) thrdsByThread <- forM thrds \thrd -> Notmuch.threadId thrd <&> (,Left thrd)
result <- result <-
mapM (runExceptT . processThread) . Map.toList $ fmap snd <$> groupBy mapM (runExceptT . processThread) . Map.toList $
fst fmap snd
(msgsByThread <> thrdsByThread) <$> groupBy
fst
(msgsByThread <> thrdsByThread)
now <- lift getCurrentTime now <- lift getCurrentTime
let entries = let entries =
threadToEntry <$> sortOn (date :: Thread -> UTCTime) (rights result) threadToEntry <$> sortOn (date :: Thread -> UTCTime) (rights result)
feed = nullFeed [i|read-later-e-mails-#{timestamp now}|] feed =
(TextString "Readlater-E-Mail") nullFeed
(timestamp now) [i|read-later-e-mails-#{timestamp now}|]
(TextString "Readlater-E-Mail")
(timestamp now)
errors = lefts result errors = lefts result
feedText <- tryJust [i|Failed to generate feed.|] . textFeed $ feed feedText <-
{ feedEntries = (if null errors then id else (errorsToEntry now errors :)) tryJust [i|Failed to generate feed.|] . textFeed $
entries feed
} { feedEntries =
(if null errors then id else (errorsToEntry now errors :))
entries
}
say $ toStrict feedText say $ toStrict feedText
either either
(\(er :: Text) -> ( \(er :: Text) ->
sayErr [i|mail2feed failed to export mails to rss.\n#{er}|] sayErr [i|mail2feed failed to export mails to rss.\n#{er}|]
) )
(const pass) (const pass)
res res
threadToEntry :: Thread -> Entry threadToEntry :: Thread -> Entry
threadToEntry Thread { subject, messages, threadid, totalCount, date, authors } threadToEntry Thread{subject, messages, threadid, totalCount, date, authors} =
= (nullEntry threadUrl threadTitle (timestamp date)) (nullEntry threadUrl threadTitle (timestamp date))
{ entryContent = Just . HTMLContent $ content { entryContent = Just . HTMLContent $ content
, entryAuthors = (\x -> nullPerson { personName = x }) <$> authors , entryAuthors = (\x -> nullPerson{personName = x}) <$> authors
} }
where where
threadUrl = [i|thread-#{threadid}-#{timestamp date}|] threadUrl = [i|thread-#{threadid}-#{timestamp date}|]
threadTitle = TextString [i|#{subject} (#{length messages}/#{totalCount})|] threadTitle = TextString [i|#{subject} (#{length messages}/#{totalCount})|]
content = T.intercalate [i|<br>\n<hr>\n|] (messageToHtml <$> messages) content = T.intercalate [i|<br>\n<hr>\n|] (messageToHtml <$> messages)
errorsToEntry :: UTCTime -> [Error] -> Entry errorsToEntry :: UTCTime -> [Error] -> Entry
errorsToEntry now er = (nullEntry [i|mailerrors - #{timestamp now}|] errorsToEntry now er =
(TextString [i|Mail processing Errors|]) ( nullEntry
(timestamp now) [i|mailerrors - #{timestamp now}|]
) (TextString [i|Mail processing Errors|])
{ entryContent = Just (timestamp now)
. HTMLContent )
. T.intercalate "<br>\n" { entryContent =
. T.splitOn "\n" Just
. T.intercalate "\n" . HTMLContent
$ er . T.intercalate "<br>\n"
} . T.splitOn "\n"
. T.intercalate "\n"
$ er
}
timestamp :: UTCTime -> Text timestamp :: UTCTime -> Text
timestamp = toText . formatTime defaultTimeLocale "%Y-%m-%d %H:%M" timestamp = toText . formatTime defaultTimeLocale "%Y-%m-%d %H:%M"
processThread processThread ::
:: (MonadIO m, MonadCatch m) (MonadIO m, MonadCatch m) =>
=> ( Notmuch.ThreadId ( Notmuch.ThreadId
, NonEmpty (Either (Notmuch.Thread a) (Notmuch.Message n a)) , NonEmpty (Either (Notmuch.Thread a) (Notmuch.Message n a))
) ) ->
-> ExceptT Error m Thread ExceptT Error m Thread
processThread (threadid, toList -> thrdAndMsgs) = processThread (threadid, toList -> thrdAndMsgs) =
handleIOError (\er -> throwE [i|IOError: #{er}|]) $ do handleIOError (\er -> throwE [i|IOError: #{er}|]) $ do
thread <- thread <-
tryJust [i|No Thread object found for Threadid #{threadid}|] tryJust [i|No Thread object found for Threadid #{threadid}|]
. viaNonEmpty head . viaNonEmpty head
. lefts . lefts
$ thrdAndMsgs $ thrdAndMsgs
let msgs = rights thrdAndMsgs let msgs = rights thrdAndMsgs
results <- mapM processMessage msgs results <- mapM processMessage msgs
let messages = sortOn (date :: Message -> UTCTime) results let messages = sortOn (date :: Message -> UTCTime) results
subject <- decodeUtf8 <$> Notmuch.threadSubject thread subject <- decodeUtf8 <$> Notmuch.threadSubject thread
totalCount <- Notmuch.threadTotalMessages thread totalCount <- Notmuch.threadTotalMessages thread
authors <- (^. Notmuch.matchedAuthors) <$> Notmuch.threadAuthors thread authors <- (^. Notmuch.matchedAuthors) <$> Notmuch.threadAuthors thread
date <- Notmuch.threadNewestDate thread date <- Notmuch.threadNewestDate thread
pure (Thread { subject, threadid, messages, totalCount, authors, date }) pure (Thread{subject, threadid, messages, totalCount, authors, date})
messageToHtml :: Message -> Text messageToHtml :: Message -> Text
messageToHtml Message { headers, body } = messageToHtml Message{headers, body} =
T.intercalate "<br>\n" T.intercalate "<br>\n" $
$ ((\(name, content) -> [i|<b>#{name}:</b> #{content}|]) <$> headers) ((\(name, content) -> [i|<b>#{name}:</b> #{content}|]) <$> headers)
<> one (bodyToHtml body) <> one (bodyToHtml body)
bodyToHtml :: Body -> Text bodyToHtml :: Body -> Text
bodyToHtml (HTMLBody x) = fromMaybe x onlyBody bodyToHtml (HTMLBody x) = fromMaybe x onlyBody
where onlyBody = renderTags . takeWhile (not . isTagCloseName "body") <$> (viaNonEmpty tail . dropWhile (not . isTagOpenName "body") . parseTags $ x) where
onlyBody = renderTags . takeWhile (not . isTagCloseName "body") <$> (viaNonEmpty tail . dropWhile (not . isTagOpenName "body") . parseTags $ x)
bodyToHtml (TextBody x) = T.intercalate "<br>\n" . T.splitOn "\n" $ x bodyToHtml (TextBody x) = T.intercalate "<br>\n" . T.splitOn "\n" $ x
processMessage :: (MonadIO m, MonadCatch m) => Notmuch.Message n a -> m Message processMessage :: (MonadIO m, MonadCatch m) => Notmuch.Message n a -> m Message
processMessage msg = do processMessage msg = do
fileName <- Notmuch.messageFilename msg fileName <- Notmuch.messageFilename msg
date <- Notmuch.messageDate msg date <- Notmuch.messageDate msg
subject <- tryHdr "subject" msg subject <- tryHdr "subject" msg
fromField <- tryHdr "from" msg fromField <- tryHdr "from" msg
toField <- tryHdr "to" msg toField <- tryHdr "to" msg
cc <- tryHdr "cc" msg cc <- tryHdr "cc" msg
unsub <- tryHdr "list-unsubscribe" msg unsub <- tryHdr "list-unsubscribe" msg
let hdrs = mapMaybe let hdrs =
(\(x, a) -> (x, ) <$> a) mapMaybe
[ ("Subject", subject) (\(x, a) -> (x,) <$> a)
, ("From" , fromField) [ ("Subject", subject)
, ("To" , toField) , ("From", fromField)
, ("Cc" , cc) , ("To", toField)
, ("Date" , Just (timestamp date)) , ("Cc", cc)
, ("Unsubscribe" , unsub) , ("Date", Just (timestamp date))
] , ("Unsubscribe", unsub)
]
msgEither <- runExceptT $ withExceptT msgEither <- runExceptT $ withExceptT
(\er -> [i|Failed to read msg\nFilename:#{fileName}\nerror: #{er}|]) (\er -> [i|Failed to read msg\nFilename:#{fileName}\nerror: #{er}|])
do do
msgContent <- handleIOError (\er -> throwE [i|IOError: #{er}|]) msgContent <-
$ readFileBS fileName handleIOError (\er -> throwE [i|IOError: #{er}|]) $
parseResult <- hoistEither . first toText $ MIME.parse readFileBS fileName
(MIME.message MIME.mime) parseResult <-
msgContent hoistEither . first toText $
textPart <- tryJust [i|No text or html part in message|] $ firstOf MIME.parse
(MIME.entities . filtered isHtml <> MIME.entities . filtered isTextPlain (MIME.message MIME.mime)
) msgContent
parseResult textPart <-
tryJust [i|No text or html part in message|] $
firstOf
( MIME.entities . filtered isHtml <> MIME.entities . filtered isTextPlain
)
parseResult
(if isHtml textPart then HTMLBody else TextBody) (if isHtml textPart then HTMLBody else TextBody)
<$> tryRight (mapLeft ("Could not decode message "<> ) $ decode textPart) <$> tryRight (mapLeft ("Could not decode message " <>) $ decode textPart)
pure $ Message { date, headers = hdrs, body = either TextBody id msgEither } pure $ Message{date, headers = hdrs, body = either TextBody id msgEither}
tryHdr :: MonadIO m => ByteString -> Notmuch.Message n a -> m (Maybe Text) tryHdr :: MonadIO m => ByteString -> Notmuch.Message n a -> m (Maybe Text)
tryHdr h msg = tryHdr h msg =

View file

@ -1,29 +1,41 @@
{-# LANGUAGE ViewPatterns, ScopedTypeVariables, NamedFieldPuns, OverloadedStrings, NoImplicitPrelude, ExtendedDefaultRules, QuasiQuotes, MultiWayIf #-} {-# LANGUAGE ExtendedDefaultRules #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Main where module Main where
import qualified Data.List.Extra as L import qualified Data.List.Extra as L
import Data.List.NonEmpty ( groupBy import Data.List.NonEmpty (
, zip groupBy,
) zip,
import Data.String.Interpolate ( i ) )
import Data.Text ( intercalate import Data.String.Interpolate (i)
, replace import Data.Text (
) intercalate,
import qualified Data.Text as Text replace,
import qualified Data.Time.Calendar as T )
import qualified Data.Time.Clock as T import qualified Data.Text as Text
import qualified Data.Time.Format as T import qualified Data.Time.Calendar as T
import Relude hiding ( intercalate import qualified Data.Time.Clock as T
, zip import qualified Data.Time.Format as T
) import Relude hiding (
import System.Environment () intercalate,
import System.FilePattern.Directory ( getDirectoryFiles ) zip,
import Text.Atom.Feed )
import Text.Atom.Feed.Export ( textFeed ) import System.Environment ()
import qualified Text.Megaparsec as MP import System.FilePattern.Directory (getDirectoryFiles)
import qualified Text.Megaparsec.Char as MP import Text.Atom.Feed
import qualified Text.Megaparsec.Char as MPC import Text.Atom.Feed.Export (textFeed)
import qualified Text.Megaparsec.Char.Lexer as MP import qualified Text.Megaparsec as MP
import qualified Text.Megaparsec.Char as MP
import qualified Text.Megaparsec.Char as MPC
import qualified Text.Megaparsec.Char.Lexer as MP
-- TODO: use Text instead of linked lists of chars -- TODO: use Text instead of linked lists of chars
type WeechatLog = [WeechatLine] type WeechatLog = [WeechatLine]
@ -31,14 +43,15 @@ data WeechatLine = WeechatLine
{ wlDate :: Text { wlDate :: Text
, wlTime :: Text , wlTime :: Text
, wlNick :: Text , wlNick :: Text
, wlMsg :: Text , wlMsg :: Text
} }
deriving (Show, Eq, Ord) deriving (Show, Eq, Ord)
-- TODO: specific handling of join/part/network messages -- TODO: specific handling of join/part/network messages
data LogFile = LogFile data LogFile = LogFile
{ path :: Text { path :: Text
, server :: Text , server :: Text
, channel :: Text , channel :: Text
} }
deriving (Show, Eq, Ord, Read) deriving (Show, Eq, Ord, Read)
@ -88,8 +101,8 @@ ircParser :: Text -> Parser LogFile
ircParser p = do ircParser p = do
void $ MP.count 4 MP.digitChar void $ MP.count 4 MP.digitChar
void dirSep void dirSep
prefix <- symbol "irc:" :: Parser Text prefix <- symbol "irc:" :: Parser Text
server <- folder server <- folder
channel <- folder channel <- folder
void parseDate void parseDate
void $ symbol ".weechatlog" void $ symbol ".weechatlog"
@ -101,23 +114,26 @@ logFolder = "/home/maralorn/logs/"
main :: IO () main :: IO ()
main = do main = do
now <- T.getCurrentTime now <- T.getCurrentTime
let getFiles t p = L.groupSortOn (\x -> (channel x, server x)) let getFiles t p =
. mapMaybe ((\x -> MP.parseMaybe (p x) x) . toText) L.groupSortOn (\x -> (channel x, server x))
. mapMaybe ((\x -> MP.parseMaybe (p x) x) . toText)
<$> getDirectoryFiles <$> getDirectoryFiles
(toString logFolder) (toString logFolder)
( T.formatTime T.defaultTimeLocale t ( T.formatTime T.defaultTimeLocale t
<$> [yesterday now, today now] <$> [yesterday now, today now]
) )
matrixFiles <- getFiles "%Y/matrix:*/*.!*/%Y-%m-%d-*.weechatlog" matrixParser matrixFiles <- getFiles "%Y/matrix:*/*.!*/%Y-%m-%d-*.weechatlog" matrixParser
ircFiles <- getFiles "%Y/irc:*/#*/%Y-%m-%d.weechatlog" ircParser ircFiles <- getFiles "%Y/irc:*/#*/%Y-%m-%d.weechatlog" ircParser
logs <- mapM readLogFiles $ mapMaybe nonEmpty $ matrixFiles <> ircFiles logs <- mapM readLogFiles $ mapMaybe nonEmpty $ matrixFiles <> ircFiles
let entries = logs & mapMaybe (logToFeedEntry now) let entries = logs & mapMaybe (logToFeedEntry now)
feed = nullFeed [i|weechat-logs-#{timestamp now}|] feed =
(TextString "Weechat Logs") nullFeed
(timestamp now) [i|weechat-logs-#{timestamp now}|]
(TextString "Weechat Logs")
(timestamp now)
[pathToWrite] <- getArgs [pathToWrite] <- getArgs
whenJust (textFeed feed { feedEntries = entries }) whenJust (textFeed feed{feedEntries = entries}) $
$ \file -> writeFileLText pathToWrite file \file -> writeFileLText pathToWrite file
today :: T.UTCTime -> T.Day today :: T.UTCTime -> T.Day
today = T.utctDay today = T.utctDay
@ -129,14 +145,17 @@ timestamp = toText . T.formatTime T.defaultTimeLocale "%Y-%m-%d %H:%M"
logToFeedEntry :: T.UTCTime -> Log -> Maybe Entry logToFeedEntry :: T.UTCTime -> Log -> Maybe Entry
logToFeedEntry now = logToFeedEntry now =
\Log { logchannel, logserver, messages = filter msgFilter -> messages } -> \Log{logchannel, logserver, messages = filter msgFilter -> messages} ->
if not (null messages) if not (null messages)
then Just (nullEntry [i|#{logserver}-#{logchannel}-#{timestamp now}|] then
(TextString [i|#{logchannel} - (#{logserver})|]) Just
(timestamp now) ( nullEntry
) [i|#{logserver}-#{logchannel}-#{timestamp now}|]
{ entryContent = Just $ HTMLContent $ printHTML messages (TextString [i|#{logchannel} - (#{logserver})|])
} (timestamp now)
)
{ entryContent = Just $ HTMLContent $ printHTML messages
}
else Nothing else Nothing
where where
cutoff = cutoff =
@ -145,8 +164,8 @@ logToFeedEntry now =
data Log = Log data Log = Log
{ logchannel :: Text { logchannel :: Text
, logserver :: Text , logserver :: Text
, messages :: [WeechatLine] , messages :: [WeechatLine]
} }
deriving (Show, Eq, Ord) deriving (Show, Eq, Ord)
@ -155,16 +174,16 @@ readLogFiles files =
readLogFile (head files) readLogFile (head files)
<$> mapM (readFileText . toString . (logFolder <>) . path) files <$> mapM (readFileText . toString . (logFolder <>) . path) files
readLogFile :: LogFile -> NonEmpty Text -> Log readLogFile :: LogFile -> NonEmpty Text -> Log
readLogFile LogFile { channel, server } contents = Log readLogFile LogFile{channel, server} contents =
{ logchannel = channel Log
, logserver = server { logchannel = channel
, messages = L.sortOn (\x -> (wlDate x, wlTime x)) , logserver = server
. concat , messages =
$ parseWeechatLog L.sortOn (\x -> (wlDate x, wlTime x))
<$> contents . concatMap parseWeechatLog
} $ contents
}
parseWeechatLine :: Parser WeechatLine parseWeechatLine :: Parser WeechatLine
parseWeechatLine = do parseWeechatLine = do
@ -179,34 +198,38 @@ parseWeechatLog :: Text -> [WeechatLine]
parseWeechatLog = filter actualMessage . mapMaybe parseLine . lines parseWeechatLog = filter actualMessage . mapMaybe parseLine . lines
where where
actualMessage = not . (`elem` ["-->", "<--", "--"]) . wlNick actualMessage = not . (`elem` ["-->", "<--", "--"]) . wlNick
parseLine = MP.parseMaybe parseWeechatLine parseLine = MP.parseMaybe parseWeechatLine
printHTML :: [WeechatLine] -> Text printHTML :: [WeechatLine] -> Text
printHTML log = intercalate "\n" $ map printDay days printHTML log = intercalate "\n" $ map printDay days
where where
days = groupBy ((==) `on` wlDate) log days = groupBy ((==) `on` wlDate) log
printDay ls = printDay ls =
intercalate "\n" $ ["<h3>" <> wlDate (head ls) <> "</h3>"] <> toList intercalate "\n" $
(printRow <$> zip (WeechatLine "" "" "" "" :| toList ls) ls) ["<h3>" <> wlDate (head ls) <> "</h3>"]
<> toList
(printRow <$> zip (WeechatLine "" "" "" "" :| toList ls) ls)
printRow :: (WeechatLine, WeechatLine) -> Text printRow :: (WeechatLine, WeechatLine) -> Text
printRow (prevRow, curRow) = printRow (prevRow, curRow) =
"<i>" <> time <> "</i> <b>" <> printNick <> "</b> " <> message <> "<br>" "<i>" <> time <> "</i> <b>" <> printNick <> "</b> " <> message <> "<br>"
where where
prevTime = Text.take 5 $ wlTime prevRow prevTime = Text.take 5 $ wlTime prevRow
curTime = Text.take 5 $ wlTime curRow curTime = Text.take 5 $ wlTime curRow
prevNick = wlNick prevRow prevNick = wlNick prevRow
curNick = wlNick curRow curNick = wlNick curRow
time | prevTime == curTime = "" time
| otherwise = curTime | prevTime == curTime = ""
nick | specialNick curNick = curNick | otherwise = curTime
| prevNick == curNick = "" nick
| otherwise = curNick | specialNick curNick = curNick
| prevNick == curNick = ""
| otherwise = curNick
printNick = Text.dropWhile (`elem` ['&', '@']) nick printNick = Text.dropWhile (`elem` ['&', '@']) nick
msg = wlMsg curRow msg = wlMsg curRow
message message
| not (Text.null msg) && Text.head msg == '>' | not (Text.null msg) && Text.head msg == '>' =
= "|<i style='color: grey'>" <> escape (Text.tail msg) <> "</i>" "|<i style='color: grey'>" <> escape (Text.tail msg) <> "</i>"
| otherwise | otherwise =
= escape msg escape msg
specialNick = (`elem` ["-->", "<--", "--", "*"]) specialNick = (`elem` ["-->", "<--", "--", "*"])
escape = replace "<" "&lt;" . replace ">" "&gt;" escape = replace "<" "&lt;" . replace ">" "&gt;"

View file

@ -1,3 +1,5 @@
{ pkgs ? import (import nix/sources.nix).nixpkgs {} }: {pkgs ? import (import nix/sources.nix).nixpkgs {}}:
with pkgs; with haskell.lib; with haskellPackages; with pkgs;
callCabal2nix "logfeed" ./. { purebred-email = doJailbreak (unmarkBroken (dontCheck purebred-email)); } with haskell.lib;
with haskellPackages;
callCabal2nix "logfeed" ./. {purebred-email = doJailbreak (unmarkBroken (dontCheck purebred-email));}

View file

@ -1,12 +1,11 @@
{ pkgs ? import (import nix/sources.nix).nixpkgs {} }: {pkgs ? import (import nix/sources.nix).nixpkgs {}}: let
let
inherit (pkgs) haskellPackages; inherit (pkgs) haskellPackages;
in in
haskellPackages.shellFor { haskellPackages.shellFor {
withHoogle = true; withHoogle = true;
packages = p: [ (import ./. { inherit pkgs; }) ]; packages = p: [(import ./. {inherit pkgs;})];
buildInputs = builtins.attrValues { buildInputs = builtins.attrValues {
inherit (haskellPackages) hlint cabal-install notmuch hsemail; inherit (haskellPackages) hlint cabal-install notmuch hsemail;
inherit (pkgs) coreutils zlib; inherit (pkgs) coreutils zlib;
}; };
} }

View file

@ -269,8 +269,12 @@ getMissingAuthorSubscriptions pr_key author = do
author_subs <- SQL.select $ do author_subs <- SQL.select $ do
author_sub <- SQL.from $ SQL.table @AuthorSubscription author_sub <- SQL.from $ SQL.table @AuthorSubscription
SQL.where_ SQL.where_
( author_sub ^. AuthorSubscriptionGithubLogin ==. SQL.val author ( author_sub
SQL.&&. author_sub ^. AuthorSubscriptionUser `notIn` SQL.subSelectList users_subscribed_to_this_pr ^. AuthorSubscriptionGithubLogin
==. SQL.val author
SQL.&&. author_sub
^. AuthorSubscriptionUser
`notIn` SQL.subSelectList users_subscribed_to_this_pr
) )
pure author_sub pure author_sub
pure $ fmap (authorSubscriptionUser . Persist.entityVal) author_subs pure $ fmap (authorSubscriptionUser . Persist.entityVal) author_subs
@ -461,10 +465,12 @@ deleteUnusedQueries :: App ()
deleteUnusedQueries = SQL.delete do deleteUnusedQueries = SQL.delete do
query <- SQL.from $ SQL.table @Query query <- SQL.from $ SQL.table @Query
SQL.where_ $ SQL.where_ $
(query ^. QueryUser) `notIn` SQL.subList_select do (query ^. QueryUser)
sub <- SQL.from $ SQL.table @Subscription `notIn` SQL.subList_select do
pure (sub ^. SubscriptionUser) sub <- SQL.from $ SQL.table @Subscription
&&. (query ^. QueryUser) `notIn` SQL.subList_select do pure (sub ^. SubscriptionUser)
&&. (query ^. QueryUser)
`notIn` SQL.subList_select do
sub <- SQL.from $ SQL.table @AuthorSubscription sub <- SQL.from $ SQL.table @AuthorSubscription
pure (sub ^. AuthorSubscriptionUser) pure (sub ^. AuthorSubscriptionUser)
@ -565,8 +571,8 @@ setQueries commands = do
Just query Just query
| queryRoom query == coerce (roomId command) -> pass | queryRoom query == coerce (roomId command) -> pass
| otherwise -> do | otherwise -> do
set_room set_room
sendMessageToUser (author command) $ m "Because you sent your most recent message to this room, I will use this room for direct messages to you from now on." sendMessageToUser (author command) $ m "Because you sent your most recent message to this room, I will use this room for direct messages to you from now on."
_ -> do _ -> do
putTextLn $ "Setting Query for user " <> author command <> " to " <> coerce (roomId command) putTextLn $ "Setting Query for user " <> author command <> " to " <> coerce (roomId command)
set_room set_room
@ -595,32 +601,32 @@ resultHandler syncResult@Matrix.SyncResult{Matrix.srNextBatch, Matrix.srRooms} =
(cmd,) <$> catchAll case cmd of (cmd,) <$> catchAll case cmd of
MkCommand{command, author, args} MkCommand{command, author, args}
| Text.isPrefixOf command "subscribe" | Text.isPrefixOf command "subscribe"
, split_args <- Text.words args , split_args <- Text.words args
, fromMaybe False (viaNonEmpty (flip Text.isPrefixOf "user" . head) split_args) -> do , fromMaybe False (viaNonEmpty (flip Text.isPrefixOf "user" . head) split_args) -> do
case maybeAt 1 split_args of case maybeAt 1 split_args of
Nothing -> pure $ m "Please tell me a user to subscribe to." Nothing -> pure $ m "Please tell me a user to subscribe to."
Just user -> do Just user -> do
notSubbed <- hasAuthorSub author user notSubbed <- hasAuthorSub author user
if notSubbed if notSubbed
then do then do
Persist.insert_ $ AuthorSubscription author user Persist.insert_ $ AuthorSubscription author user
pure $ m $ "I will now track for you all pull requests by " <> user pure $ m $ "I will now track for you all pull requests by " <> user
else pure $ m $ "Okay, but you were already subscribed to pull requests by user " <> user else pure $ m $ "Okay, but you were already subscribed to pull requests by user " <> user
MkCommand{command, author, args} MkCommand{command, author, args}
| Text.isPrefixOf command "unsubscribe" | Text.isPrefixOf command "unsubscribe"
, split_args <- Text.words args , split_args <- Text.words args
, fromMaybe False (viaNonEmpty (flip Text.isPrefixOf "user" . head) split_args) -> do , fromMaybe False (viaNonEmpty (flip Text.isPrefixOf "user" . head) split_args) -> do
case maybeAt 1 split_args of case maybeAt 1 split_args of
Nothing -> pure $ m "Please tell me a user to unsubscribe from." Nothing -> pure $ m "Please tell me a user to unsubscribe from."
Just user -> do Just user -> do
notSubbed <- hasAuthorSub author user notSubbed <- hasAuthorSub author user
if notSubbed if notSubbed
then pure $ m $ "I havent been tracking pull requests by " <> user <> " for you." then pure $ m $ "I havent been tracking pull requests by " <> user <> " for you."
else do else do
SQL.delete $ do SQL.delete $ do
author_sub <- SQL.from $ SQL.table @AuthorSubscription author_sub <- SQL.from $ SQL.table @AuthorSubscription
SQL.where_ (author_sub ^. AuthorSubscriptionUser ==. SQL.val author &&. author_sub ^. AuthorSubscriptionGithubLogin ==. SQL.val user) SQL.where_ (author_sub ^. AuthorSubscriptionUser ==. SQL.val author &&. author_sub ^. AuthorSubscriptionGithubLogin ==. SQL.val user)
pure $ m $ "I will not subscribe you automatically to new pull requests by user " <> user <> " anymore." pure $ m $ "I will not subscribe you automatically to new pull requests by user " <> user <> " anymore."
MkCommand{command, author, args} | Text.isPrefixOf command "subscribe" -> MkCommand{command, author, args} | Text.isPrefixOf command "subscribe" ->
case parsePRNumber args of case parsePRNumber args of
Nothing -> pure $ m $ "I could not parse \"" <> args <> "\" as a pull request number. Have you maybe mistyped it?" Nothing -> pure $ m $ "I could not parse \"" <> args <> "\" as a pull request number. Have you maybe mistyped it?"
@ -644,7 +650,7 @@ resultHandler syncResult@Matrix.SyncResult{Matrix.srNextBatch, Matrix.srRooms} =
case pr_msg_may of case pr_msg_may of
Just prMsg Just prMsg
| notSubbed -> | notSubbed ->
pure $ m "Well, you were not subscribed to pull request " <> prMsg pure $ m "Well, you were not subscribed to pull request " <> prMsg
Just prMsg -> do Just prMsg -> do
Persist.delete $ SubscriptionKey author pr_key Persist.delete $ SubscriptionKey author pr_key
pure $ m "Okay, I will not send you updates about pull request " <> prMsg pure $ m "Okay, I will not send you updates about pull request " <> prMsg

View file

@ -14,7 +14,7 @@ import Data.GraphQL.Bootstrap
import NixpkgsBot.GraphQL.Scalars import NixpkgsBot.GraphQL.Scalars
{----------------------------------------------------------------------------- {-----------------------------------------------------------------------------
* MergingPullRequest \* MergingPullRequest
-- result :: Object MergingPullRequestSchema; throws a GraphQL exception on errors -- result :: Object MergingPullRequestSchema; throws a GraphQL exception on errors
result <- runQuery MergingPullRequestQuery result <- runQuery MergingPullRequestQuery
@ -113,7 +113,7 @@ instance GraphQLQuery MergingPullRequestQuery where
] ]
{----------------------------------------------------------------------------- {-----------------------------------------------------------------------------
* PullRequest \* PullRequest
-- result :: Object PullRequestSchema; throws a GraphQL exception on errors -- result :: Object PullRequestSchema; throws a GraphQL exception on errors
result <- runQuery PullRequestQuery result <- runQuery PullRequestQuery