diff --git a/Assistant/Threads/PairListener.hs b/Assistant/Threads/PairListener.hs index 90fce8777e..cd297fad36 100644 --- a/Assistant/Threads/PairListener.hs +++ b/Assistant/Threads/PairListener.hs @@ -103,7 +103,7 @@ pairListenerThread urlrenderer = NamedThread "PairListener" $ do pairReqReceived :: Bool -> UrlRenderer -> PairMsg -> Assistant () pairReqReceived True _ _ = noop -- ignore our own PairReq pairReqReceived False urlrenderer msg = do - url <- liftIO $ renderUrl urlrenderer (FinishPairR msg) [] + url <- liftIO $ renderUrl urlrenderer (FinishLocalPairR msg) [] close <- asIO1 removeAlert void $ addAlert $ pairRequestReceivedAlert repo AlertButton diff --git a/Assistant/Types/Buddies.hs b/Assistant/Types/Buddies.hs index 06ac5526d6..9c070aa6a5 100644 --- a/Assistant/Types/Buddies.hs +++ b/Assistant/Types/Buddies.hs @@ -14,16 +14,37 @@ import Common.Annex import qualified Data.Map as M import Control.Concurrent.STM import Utility.NotificationBroadcaster +import Data.Text as T -{- When XMPP is enabled, this is an XMPP buddy map. - - Otherwise, it's an empty map, for simplicity. -} +{- For simplicity, dummy types are defined even when XMPP is disabled. -} #ifdef WITH_XMPP -import Assistant.XMPP.Buddies +import Network.Protocol.XMPP +import Data.Set as S +import Data.Ord + +newtype Client = Client JID + deriving (Eq, Show) + +instance Ord Client where + compare = comparing show + +data Buddy = Buddy + { buddyPresent :: S.Set Client + , buddyAway :: S.Set Client + , buddyAssistants :: S.Set Client + } #else -type Buddies = M.Map String Buddy data Buddy - deriving (Eq) #endif + deriving (Eq, Show) + +data BuddyID = BuddyID T.Text + deriving (Eq, Ord, Show, Read) + +data BuddyKey = BuddyKey T.Text + deriving (Eq, Ord, Show) + +type Buddies = M.Map BuddyKey Buddy {- A list of buddies, and a way to notify when it changes. -} type BuddyList = (TMVar Buddies, NotificationBroadcaster) @@ -39,6 +60,9 @@ newBuddyList = (,) getBuddyList :: BuddyList -> IO [Buddy] getBuddyList (v, _) = M.elems <$> atomically (readTMVar v) +getBuddyBroadcaster :: BuddyList -> NotificationBroadcaster +getBuddyBroadcaster (_, h) = h + {- Applies a function to modify the buddy list, and if it's changed, - sends notifications to any listeners. -} updateBuddyList :: (Buddies -> Buddies) -> BuddyList -> IO () @@ -50,8 +74,3 @@ updateBuddyList a (v, caster) = do return $ buds /= buds' when changed $ sendNotification caster - -{- Allocates a notification handle for a client to use to listen for - - changes to the buddy list. -} -newBuddyListNotificationHandle :: BuddyList -> IO NotificationHandle -newBuddyListNotificationHandle (_, caster) = newNotificationHandle caster diff --git a/Assistant/WebApp.hs b/Assistant/WebApp.hs index 70cda1046f..10378f60bc 100644 --- a/Assistant/WebApp.hs +++ b/Assistant/WebApp.hs @@ -11,7 +11,6 @@ module Assistant.WebApp where import Assistant.WebApp.Types import Assistant.Common -import Assistant.DaemonStatus import Utility.NotificationBroadcaster import Utility.Yesod import Locations.UserConfig @@ -95,18 +94,15 @@ runAnnex fallback a = ifM (noAnnex <$> getYesod) , liftAssistant $ liftAnnex a ) -waitNotifier :: forall sub. (DaemonStatus -> NotificationBroadcaster) -> NotificationId -> GHandler sub WebApp () -waitNotifier selector nid = do - notifier <- getNotifier selector - liftIO $ waitNotification $ notificationHandleFromId notifier nid +waitNotifier :: forall sub. (Assistant NotificationBroadcaster) -> NotificationId -> GHandler sub WebApp () +waitNotifier getbroadcaster nid = liftAssistant $ do + b <- getbroadcaster + liftIO $ waitNotification $ notificationHandleFromId b nid -newNotifier :: forall sub. (DaemonStatus -> NotificationBroadcaster) -> GHandler sub WebApp NotificationId -newNotifier selector = do - notifier <- getNotifier selector - liftIO $ notificationHandleToId <$> newNotificationHandle notifier - -getNotifier :: forall sub. (DaemonStatus -> NotificationBroadcaster) -> GHandler sub WebApp NotificationBroadcaster -getNotifier selector = selector <$> liftAssistant getDaemonStatus +newNotifier :: forall sub. (Assistant NotificationBroadcaster) -> GHandler sub WebApp NotificationId +newNotifier getbroadcaster = liftAssistant $ do + b <- getbroadcaster + liftIO $ notificationHandleToId <$> newNotificationHandle b {- Adds the auth parameter as a hidden field on a form. Must be put into - every form. -} diff --git a/Assistant/WebApp/Configurators/Pairing.hs b/Assistant/WebApp/Configurators/Pairing.hs index fba59a7b72..2ec5f9891e 100644 --- a/Assistant/WebApp/Configurators/Pairing.hs +++ b/Assistant/WebApp/Configurators/Pairing.hs @@ -14,6 +14,8 @@ import Assistant.Pairing import Assistant.WebApp import Assistant.WebApp.Types import Assistant.WebApp.SideBar +import Assistant.WebApp.Configurators.XMPP +import Assistant.Types.Buddies import Utility.Yesod #ifdef WITH_PAIRING import Assistant.Common @@ -26,6 +28,10 @@ import Utility.Verifiable import Utility.Network import Annex.UUID #endif +#ifdef WITH_XMPP +import Assistant.XMPP.Client +import Network.Protocol.XMPP +#endif import Utility.UserInfo import Yesod @@ -39,22 +45,55 @@ import qualified Control.Exception as E import Control.Concurrent #endif -{- Starts sending out pair requests. -} +{- Starts either kind of pairing. -} getStartPairR :: Handler RepHtml +#ifdef WITH_XMPP +getStartPairR = pairPage $ do + xmppconfigured <- lift $ isJust <$> runAnnex Nothing getXMPPCreds #ifdef WITH_PAIRING -getStartPairR = promptSecret Nothing $ startPairing PairReq noop pairingAlert Nothing + let localsupported = True #else -getStartPairR = noPairing + let localsupported = False +#endif + $(widgetFile "configurators/pairing/start") +#else +#ifdef WITH_PAIRING +getStartPairR = redirect StartLocalPairR +#else +getStartPairR = noPairing "local or jabber" +#endif #endif -{- Runs on the system that responds to a pair request; sets up the ssh +{- Starts pairing with an XMPP buddy. -} +getStartXMPPPairR :: BuddyID -> Handler RepHtml +#ifdef WITH_XMPP +getStartXMPPPairR (BuddyID bid) = case parseJID bid of + Nothing -> error "bad JID" + Just jid -> error "TODO" +#else +getStartXMPPPairR _ = noPairing "XMPP" +#endif + +{- Starts local pairing. -} +getStartLocalPairR :: Handler RepHtml +#ifdef WITH_PAIRING +getStartLocalPairR = promptSecret Nothing $ + startLocalPairing PairReq noop pairingAlert Nothing +#else +getStartLocalPairR = noLocalPairing + +noLocalPairing :: Handler RepHtml +noLocalPairing = noPairing "local" +#endif + +{- Runs on the system that responds to a local pair request; sets up the ssh - authorized key first so that the originating host can immediately sync - with us. -} -getFinishPairR :: PairMsg -> Handler RepHtml +getFinishLocalPairR :: PairMsg -> Handler RepHtml #ifdef WITH_PAIRING -getFinishPairR msg = promptSecret (Just msg) $ \_ secret -> do +getFinishLocalPairR msg = promptSecret (Just msg) $ \_ secret -> do liftIO $ setup - startPairing PairAck cleanup alert uuid "" secret + startLocalPairing PairAck cleanup alert uuid "" secret where alert = pairRequestAcknowledgedAlert (pairRepo msg) . Just setup = setupAuthorizedKeys msg @@ -62,21 +101,21 @@ getFinishPairR msg = promptSecret (Just msg) $ \_ secret -> do remoteSshPubKey $ pairMsgData msg uuid = Just $ pairUUID $ pairMsgData msg #else -getFinishPairR _ = noPairing +getFinishLocalPairR _ = noLocalPairing #endif -getInprogressPairR :: SecretReminder -> Handler RepHtml +getRunningLocalPairR :: SecretReminder -> Handler RepHtml #ifdef WITH_PAIRING -getInprogressPairR s = pairPage $ do +getRunningLocalPairR s = pairPage $ do let secret = fromSecretReminder s - $(widgetFile "configurators/pairing/inprogress") + $(widgetFile "configurators/pairing/local/inprogress") #else -getInprogressPairR _ = noPairing +getRunningLocalPairR _ = noLocalPairing #endif #ifdef WITH_PAIRING -{- Starts pairing, at either the PairReq (initiating host) or +{- Starts local pairing, at either the PairReq (initiating host) or - PairAck (responding host) stage. - - Displays an alert, and starts a thread sending the pairing message, @@ -85,8 +124,8 @@ getInprogressPairR _ = noPairing - - Redirects to the pairing in progress page. -} -startPairing :: PairStage -> IO () -> (AlertButton -> Alert) -> Maybe UUID -> Text -> Secret -> Widget -startPairing stage oncancel alert muuid displaysecret secret = do +startLocalPairing :: PairStage -> IO () -> (AlertButton -> Alert) -> Maybe UUID -> Text -> Secret -> Widget +startLocalPairing stage oncancel alert muuid displaysecret secret = do urlrender <- lift getUrlRender reldir <- fromJust . relDir <$> lift getYesod @@ -106,7 +145,7 @@ startPairing stage oncancel alert muuid displaysecret secret = do startSending pip stage $ sendrequests sender void $ liftIO $ forkIO thread - lift $ redirect $ InprogressPairR $ toSecretReminder displaysecret + lift $ redirect $ RunningLocalPairR $ toSecretReminder displaysecret where {- Sends pairing messages until the thread is killed, - and shows an activity alert while doing it. @@ -164,7 +203,7 @@ promptSecret msg cont = pairPage $ do u <- T.pack <$> liftIO myUserName let sameusername = username == u let authtoken = webAppFormAuthToken - $(widgetFile "configurators/pairing/prompt") + $(widgetFile "configurators/pairing/local/prompt") {- This counts unicode characters as more than one character, - but that's ok; they *do* provide additional entropy. -} @@ -189,8 +228,8 @@ sampleQuote = T.unwords #else -noPairing :: Handler RepHtml -noPairing = pairPage $ +noPairing :: Text -> Handler RepHtml +noPairing pairingtype = pairPage $ $(widgetFile "configurators/pairing/disabled") #endif diff --git a/Assistant/WebApp/Configurators/XMPP.hs b/Assistant/WebApp/Configurators/XMPP.hs index f27937862c..b93867ae5a 100644 --- a/Assistant/WebApp/Configurators/XMPP.hs +++ b/Assistant/WebApp/Configurators/XMPP.hs @@ -12,13 +12,17 @@ module Assistant.WebApp.Configurators.XMPP where import Assistant.WebApp import Assistant.WebApp.Types +import Assistant.WebApp.Notifications import Assistant.WebApp.SideBar +import Assistant.Types.Buddies import Assistant.Alert import Assistant.DaemonStatus import Utility.Yesod -#ifdef WITH_XMPP +import Utility.NotificationBroadcaster import Assistant.Common +#ifdef WITH_XMPP import Assistant.XMPP.Client +import Assistant.XMPP.Buddies import Assistant.Pushes import Utility.SRV #endif @@ -45,7 +49,23 @@ xmppNeeded = whenM (isNothing <$> runAnnex Nothing getXMPPCreds) $ do getXMPPR :: Handler RepHtml #ifdef WITH_XMPP -getXMPPR = xmppPage $ do +getXMPPR = getXMPPR' ConfigR +#else +getXMPPR = xmppPage $ + $(widgetFile "configurators/xmpp/disabled") +#endif + +getXMPPForPairingR :: Handler RepHtml +#ifdef WITH_XMPP +getXMPPForPairingR = getXMPPR' StartPairR +#else +getXMPPForPairingR = xmppPage $ + $(widgetFile "configurators/xmpp/disabled") +#endif + +#ifdef WITH_XMPP +getXMPPR' :: Route WebApp -> Handler RepHtml +getXMPPR' redirto = xmppPage $ do ((result, form), enctype) <- lift $ do oldcreds <- runAnnex Nothing getXMPPCreds runFormGet $ renderBootstrap $ xmppAForm $ @@ -61,12 +81,33 @@ getXMPPR = xmppPage $ do storecreds creds = do void $ runAnnex undefined $ setXMPPCreds creds liftAssistant notifyRestart - redirect ConfigR -#else -getXMPPR = xmppPage $ - $(widgetFile "configurators/xmpp/disabled") + redirect redirto #endif +{- Called by client to get a list of buddies. + - + - Returns a div, which will be inserted into the calling page. + -} +getBuddyListR :: NotificationId -> Handler RepHtml +getBuddyListR nid = do + waitNotifier getBuddyListBroadcaster nid + + page <- widgetToPageContent $ buddyListDisplay + hamletToRepHtml $ [hamlet|^{pageBody page}|] + +buddyListDisplay :: Widget +buddyListDisplay = do + autoUpdate ident NotifierBuddyListR (10 :: Int) (10 :: Int) +#ifdef WITH_XMPP + buddies <- lift $ liftAssistant $ catMaybes . map buddySummary + <$> (getBuddyList <<~ buddyList) +#else + let buddies = [] +#endif + $(widgetFile "configurators/xmpp/buddylist") + where + ident = "buddylist" + #ifdef WITH_XMPP data XMPPForm = XMPPForm diff --git a/Assistant/WebApp/DashBoard.hs b/Assistant/WebApp/DashBoard.hs index f2a74f08ba..44e64613af 100644 --- a/Assistant/WebApp/DashBoard.hs +++ b/Assistant/WebApp/DashBoard.hs @@ -71,7 +71,7 @@ simplifyTransfers (v@(t1, _):r@((t2, _):l)) -} getTransfersR :: NotificationId -> Handler RepHtml getTransfersR nid = do - waitNotifier transferNotifier nid + waitNotifier getTransferBroadcaster nid page <- widgetToPageContent $ transfersDisplay False hamletToRepHtml $ [hamlet|^{pageBody page}|] diff --git a/Assistant/WebApp/Notifications.hs b/Assistant/WebApp/Notifications.hs index 244785a2f3..c841049312 100644 --- a/Assistant/WebApp/Notifications.hs +++ b/Assistant/WebApp/Notifications.hs @@ -12,6 +12,8 @@ module Assistant.WebApp.Notifications where import Assistant.Common import Assistant.WebApp import Assistant.WebApp.Types +import Assistant.DaemonStatus +import Assistant.Types.Buddies import Utility.NotificationBroadcaster import Utility.Yesod @@ -40,9 +42,9 @@ autoUpdate ident geturl ms_delay ms_startdelay = do - of NotificationIds when noscript pages are loaded. This constructs a - notifier url for a given Route and NotificationBroadcaster. -} -notifierUrl :: (NotificationId -> Route WebApp) -> (DaemonStatus -> NotificationBroadcaster) -> Handler RepPlain -notifierUrl route selector = do - (urlbits, _params) <- renderRoute . route <$> newNotifier selector +notifierUrl :: (NotificationId -> Route WebApp) -> Assistant NotificationBroadcaster -> Handler RepPlain +notifierUrl route broadcaster = do + (urlbits, _params) <- renderRoute . route <$> newNotifier broadcaster webapp <- getYesod return $ RepPlain $ toContent $ T.concat [ "/" @@ -52,7 +54,19 @@ notifierUrl route selector = do ] getNotifierTransfersR :: Handler RepPlain -getNotifierTransfersR = notifierUrl TransfersR transferNotifier +getNotifierTransfersR = notifierUrl TransfersR getTransferBroadcaster getNotifierSideBarR :: Handler RepPlain -getNotifierSideBarR = notifierUrl SideBarR alertNotifier +getNotifierSideBarR = notifierUrl SideBarR getAlertBroadcaster + +getNotifierBuddyListR :: Handler RepPlain +getNotifierBuddyListR = notifierUrl BuddyListR getBuddyListBroadcaster + +getTransferBroadcaster :: Assistant NotificationBroadcaster +getTransferBroadcaster = transferNotifier <$> getDaemonStatus + +getAlertBroadcaster :: Assistant NotificationBroadcaster +getAlertBroadcaster = alertNotifier <$> getDaemonStatus + +getBuddyListBroadcaster :: Assistant NotificationBroadcaster +getBuddyListBroadcaster = getBuddyBroadcaster <$> getAssistant buddyList diff --git a/Assistant/WebApp/SideBar.hs b/Assistant/WebApp/SideBar.hs index 18c5180140..c8ccbed5f9 100644 --- a/Assistant/WebApp/SideBar.hs +++ b/Assistant/WebApp/SideBar.hs @@ -59,7 +59,7 @@ sideBarDisplay = do -} getSideBarR :: NotificationId -> Handler RepHtml getSideBarR nid = do - waitNotifier alertNotifier nid + waitNotifier getAlertBroadcaster nid {- This 0.1 second delay avoids very transient notifications from - being displayed and churning the sidebar unnecesarily. diff --git a/Assistant/WebApp/Types.hs b/Assistant/WebApp/Types.hs index ad58d1a066..2c58515e00 100644 --- a/Assistant/WebApp/Types.hs +++ b/Assistant/WebApp/Types.hs @@ -14,6 +14,7 @@ import Assistant.Common import Assistant.Ssh import Assistant.Alert import Assistant.Pairing +import Assistant.Types.Buddies import Utility.NotificationBroadcaster import Utility.WebApp import Logs.Transfer @@ -88,3 +89,7 @@ instance PathPiece SecretReminder where instance PathPiece UUID where toPathPiece = pack . show fromPathPiece = readish . unpack + +instance PathPiece BuddyID where + toPathPiece = pack . show + fromPathPiece = readish . unpack diff --git a/Assistant/WebApp/routes b/Assistant/WebApp/routes index 2e848f44ef..f2cf9687bb 100644 --- a/Assistant/WebApp/routes +++ b/Assistant/WebApp/routes @@ -7,6 +7,7 @@ /config ConfigR GET /config/repository RepositoriesR GET /config/xmpp XMPPR GET +/config/xmpp/for/pairing XMPPForPairingR GET /config/repository/new/first FirstRepositoryR GET /config/repository/new NewRepositoryR GET @@ -26,17 +27,24 @@ /config/repository/add/cloud/S3 AddS3R GET /config/repository/pair/start StartPairR GET -/config/repository/pair/inprogress/#SecretReminder InprogressPairR GET -/config/repository/pair/finish/#PairMsg FinishPairR GET +/config/repository/pair/local/start StartLocalPairR GET +/config/repository/pair/local/running/#SecretReminder RunningLocalPairR GET +/config/repository/pair/local/finish/#PairMsg FinishLocalPairR GET +/config/repository/pair/xmpp/start/#BuddyID StartXMPPPairR GET /config/repository/enable/rsync/#UUID EnableRsyncR GET /config/repository/enable/directory/#UUID EnableDirectoryR GET /config/repository/enable/S3/#UUID EnableS3R GET /transfers/#NotificationId TransfersR GET -/sidebar/#NotificationId SideBarR GET /notifier/transfers NotifierTransfersR GET + +/sidebar/#NotificationId SideBarR GET /notifier/sidebar NotifierSideBarR GET + +/buddylist/#NotificationId BuddyListR GET +/notifier/buddylist NotifierBuddyListR GET + /alert/close/#AlertId CloseAlert GET /alert/click/#AlertId ClickAlert GET /filebrowser FileBrowserR GET POST diff --git a/Assistant/XMPP/Buddies.hs b/Assistant/XMPP/Buddies.hs index de2b570c63..db56235c7a 100644 --- a/Assistant/XMPP/Buddies.hs +++ b/Assistant/XMPP/Buddies.hs @@ -9,32 +9,32 @@ module Assistant.XMPP.Buddies where import Assistant.XMPP import Common.Annex +import Assistant.Types.Buddies import Network.Protocol.XMPP import qualified Data.Map as M import qualified Data.Set as S -import Data.Ord +import Data.Text (Text) +import qualified Data.Text as T -newtype Client = Client JID - deriving (Eq, Show) +genBuddyID :: JID -> BuddyID +genBuddyID j = BuddyID $ formatJID j -instance Ord Client where - compare = comparing show +genKey :: JID -> BuddyKey +genKey j = BuddyKey $ formatJID $ JID (jidNode j) (jidDomain j) Nothing -data Buddy = Buddy - { buddyPresent :: S.Set Client - , buddyAway :: S.Set Client - , buddyAssistants :: S.Set Client - } - deriving (Eq, Show) - -{- Note that the buddy map includes one buddy for the user's own JID, - - so that we can track other git-annex assistant's sharing the same - - account. -} -type Buddies = M.Map String Buddy - -genKey :: JID -> String -genKey j = show $ JID (jidNode j) (jidDomain j) Nothing +{- Summary of info about a buddy. + - + - If the buddy has no clients at all anymore, returns Nothing. -} +buddySummary :: Buddy -> Maybe (Text, Bool, Bool, BuddyID) +buddySummary b = case clients of + ((Client j):_) -> Just (buddyname j, away, canpair, genBuddyID j) + [] -> Nothing + where + buddyname j = maybe (T.pack "") strNode (jidNode j) + away = S.null (buddyPresent b) && S.null (buddyAssistants b) + canpair = not $ S.null (buddyAssistants b) + clients = S.toList $ buddyPresent b `S.union` buddyAway b `S.union` buddyAssistants b {- Updates the buddies with XMPP presence info. -} updateBuddies :: Presence -> Buddies -> Buddies diff --git a/doc/assistant/buddylist.png b/doc/assistant/buddylist.png new file mode 100644 index 0000000000..40b5a92382 Binary files /dev/null and b/doc/assistant/buddylist.png differ diff --git a/templates/configurators/pairing/disabled.hamlet b/templates/configurators/pairing/disabled.hamlet index c946aacc48..c924c4ba2c 100644 --- a/templates/configurators/pairing/disabled.hamlet +++ b/templates/configurators/pairing/disabled.hamlet @@ -1,5 +1,5 @@
- This build of git-annex does not support pairing. Sorry! + This build of git-annex does not support #{pairingtype} pairing. Sorry! diff --git a/templates/configurators/pairing/inprogress.hamlet b/templates/configurators/pairing/local/inprogress.hamlet similarity index 100% rename from templates/configurators/pairing/inprogress.hamlet rename to templates/configurators/pairing/local/inprogress.hamlet diff --git a/templates/configurators/pairing/prompt.hamlet b/templates/configurators/pairing/local/prompt.hamlet similarity index 100% rename from templates/configurators/pairing/prompt.hamlet rename to templates/configurators/pairing/local/prompt.hamlet diff --git a/templates/configurators/pairing/start.hamlet b/templates/configurators/pairing/start.hamlet new file mode 100644 index 0000000000..cae45bcada --- /dev/null +++ b/templates/configurators/pairing/start.hamlet @@ -0,0 +1,25 @@ +
+ Pairing with a another computer combines both git-annex repositories # + into a single shared repository, with changes kept in sync. + $if localsupported +
+ Is the computer you want to pair with located nearby, on the same #
+ local network? (Or sharing a VPN?)
+
+ Or, you can pair with any of your friends using jabber, or with another #
+ computer that shares your jabber account.
+
+ $if xmppconfigured
+ ^{buddyListDisplay}
+ $else
+ First, you need to #
+
+ configure a jabber account
diff --git a/templates/configurators/repositories.hamlet b/templates/configurators/repositories.hamlet
index 1e64ea0321..72e6e248ca 100644
--- a/templates/configurators/repositories.hamlet
+++ b/templates/configurators/repositories.hamlet
@@ -44,12 +44,10 @@
- Pair with a local computer to automatically keep files in sync #
- between computers on your local network.
-
- For easy sharing with family and friends, or between your devices.
+ Connect with another computer used by you, or by a friend,
+ that is also running git-annex.
+ If so, the best choice is local pairing.
+
+
+ Start local pairing
+
- Local computer
+ Pair with another computer
Phone
diff --git a/templates/configurators/xmpp/buddylist.hamlet b/templates/configurators/xmpp/buddylist.hamlet
new file mode 100644
index 0000000000..16591db892
--- /dev/null
+++ b/templates/configurators/xmpp/buddylist.hamlet
@@ -0,0 +1,23 @@
+
+
+ $if null buddies
+
+
+ Nobody is currently available.
+ $else
+ $forall (name, away, canpair, buddyid) <- buddies
+
+
+ #
+ $if away
+
+ #{name}
+ $else
+ #{name}
+
+ $if canpair
+
+ Start pairing
+ $else
+ not using git-annex