1 -- This file is part of Intricacy
2 -- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of version 3 of the GNU General Public License as
6 -- published by the Free Software Foundation, or any later version.
8 -- You should have received a copy of the GNU General Public License
9 -- along with this program. If not, see http://www.gnu.org/licenses/.
11 {-# LANGUAGE ScopedTypeVariables #-}
17 import Control
.Applicative
18 import Control
.Concurrent
(forkIO
, threadDelay
)
19 import Control
.Exception
.Base
(evaluate
)
21 import Control
.Monad
.Catch
22 import Control
.Monad
.IO.Class
23 import Control
.Monad
.Trans
24 import Control
.Monad
.Trans
.Except
25 import Control
.Monad
.Trans
.Maybe
26 import Control
.Monad
.Trans
.Reader
27 import Control
.Monad
.Trans
.State
29 import Data
.Bifunctor
(bimap
)
30 import qualified Data
.Binary
as B
31 import qualified Data
.ByteString
.Char8
as CS
32 import qualified Data
.ByteString
.Lazy
as BL
33 import Data
.Foldable
(for_
)
34 import Data
.Function
(on
)
37 import qualified Data
.Text
as TS
38 import qualified Data
.Text
.Lazy
as TL
39 import qualified Data
.Text
.Short
as TSh
40 import Data
.Time
.Clock
43 import qualified Pipes
.Prelude
as P
44 import System
.Directory
(renameFile)
45 import System
.FilePath
47 import System
.IO.Error
50 import Data
.Time
.Format
51 import Data
.Time
.LocalTime
52 import Text
.Feed
.Constructor
53 import Text
.Feed
.Export
(xmlFeed
)
54 import Text
.Feed
.Import
(parseFeedFromFile
)
55 import qualified Text
.XML
as XML
57 import qualified Crypto
.Argon2
as A2
58 import Crypto
.Hash
.Algorithms
(SHA256
(..))
59 import Crypto
.PubKey
.RSA
(generate
, generateBlinder
)
60 import Crypto
.PubKey
.RSA
.OAEP
(decrypt
, defaultOAEPParams
)
61 import Crypto
.PubKey
.RSA
.Types
(private_n
)
63 import Network
.Mail
.Mime
(plainPart
)
64 import qualified Network
.Mail
.SMTP
as SMTP
65 import qualified Text
.Email
.Validate
67 import System
.Console
.GetOpt
68 import System
.Environment
81 defaultPort
= 27001 -- 27001 == ('i'<<8) + 'y'
83 data Opt
= RequestDelay
Int | Daemon | LogFile
FilePath | Port
Int | DBDir
FilePath | ServerLockSize
Int | FeedPath
FilePath | Help | Version
84 deriving (Eq
, Ord
, Show)
86 [ Option
['p
'] ["port"] (ReqArg
(Port
. read) "PORT") $ "TCP port to listen on (default: " ++ show defaultPort
++ ")"
87 , Option
['P
'] ["delay"] (ReqArg
(RequestDelay
. read) "MICROSECS") "delay before sending response (for testing) (default: 0)"
88 -- , Option ['d'] ["daemon"] (NoArg Daemon) "Run as daemon"
89 , Option
['l
'] ["logfile"] (ReqArg LogFile
"PATH") "Log to file"
90 , Option
['d
'] ["dir"] (ReqArg DBDir
"PATH") "directory for server database [default: intricacydb]"
91 , Option
['s
'] ["locksize"] (ReqArg
(ServerLockSize
. read) "SIZE") "size of locks (only takes effect when creating a new database) [default: 8]"
92 , Option
['f
'] ["feed"] (ReqArg FeedPath
"PATH") "write news feed to this path"
93 , Option
['h
'] ["help"] (NoArg Help
) "show usage information"
94 , Option
['v
'] ["version"] (NoArg Version
) "show version information"
98 usage
= usageInfo header options
99 where header
= "Usage: intricacy-server [OPTION...]"
101 parseArgs
:: [String] -> IO ([Opt
],[String])
103 case getOpt Permute options argv
of
104 (o
,n
,[]) -> return (o
,n
)
105 (_
,_
,errs
) -> ioError (userError (concat errs
++ usageInfo header options
))
106 where header
= "Usage: intricacy-server [OPTION...]"
110 (opts
,_
) <- parseArgs argv
111 {- FIXME: doesn't work
112 if Daemon `elem` opts
113 then void $ forkIO $ main' opts
116 when (Help `
elem` opts
) $ putStr usage
>> exitSuccess
117 when (Version `
elem` opts
) $ putStrLn version
>> exitSuccess
118 let delay
= fromMaybe 0 $ listToMaybe [ d | RequestDelay d
<- opts
]
119 port
= fromMaybe defaultPort
$ listToMaybe [ p | Port p
<- opts
]
120 dbpath
= fromMaybe "intricacydb" $ listToMaybe [ p | DBDir p
<- opts
]
121 mfeedPath
= listToMaybe [ p | FeedPath p
<- opts
]
122 locksize
= min maxlocksize
$ fromMaybe 8 $ listToMaybe [ s | ServerLockSize s
<- opts
]
123 withDB dbpath
$ setDefaultServerInfo locksize
>> setKeyPair
124 writeFile (lockFilePath dbpath
) ""
125 logh
<- case listToMaybe [ f | LogFile f
<- opts
] of
126 Nothing
-> return stdout
127 Just path
-> openFile path AppendMode
128 streamServer serverSpec
{address
= IPv4
"" port
, threading
=Threaded
} $ handler dbpath delay logh mfeedPath
131 setDefaultServerInfo locksize
= do
132 alreadySet
<- recordExists RecServerInfo
133 unless alreadySet
$ putRecord RecServerInfo
(RCServerInfo
$ defaultServerInfo locksize
)
137 alreadySet
<- recordExists RecPublicKey
138 unless alreadySet
$ do
139 (publicKey
, secretKey
) <- liftIO
$ generate
256 65537
140 putRecord RecPublicKey
$ RCPublicKey publicKey
141 putRecord RecSecretKey
$ RCSecretKey secretKey
143 -- Note: switching to cryptonite's argon2 implementation would not be
144 -- straightforwardsly backwards-compatible, the output format is different.
145 argon2
:: String -> ExceptT
String IO String
146 argon2 s
= either (throwE
. show) return $
147 TSh
.unpack
<$> A2
.hashEncoded hashOptions
(CS
.pack s
) (CS
.pack salt
)
149 salt
= "intricacy salt"
150 -- |default argon2 hash options
151 hashOptions
= A2
.HashOptions
152 { A2
.hashIterations
= 3
153 , A2
.hashMemory
= 2 ^
12 -- 4 MiB
154 , A2
.hashParallelism
= 1
155 , A2
.hashVariant
= A2
.Argon2i
156 , A2
.hashVersion
= A2
.Argon2Version13
157 , A2
.hashLength
= 2 ^
5 -- 32 bytes
161 -- | We lock the whole database during each request, using haskell's native
162 -- file locking, meaning that we have at any time one writer *xor* any number
164 withDBLock
:: MonadIO m
=> [Char] -> IOMode -> m b
-> m b
165 withDBLock dbpath lockMode m
= do
166 h
<- liftIO
$ getDBLock lockMode
172 catchIO
(openFile (lockFilePath dbpath
) lockMode
) (\_
-> threadDelay
(50*10^
3) >> getDBLock lockMode
)
174 lockFilePath dbpath
= dbpath
++ [pathSeparator
] ++ "lockfile"
176 logit h s
= hPutStrLn h s
>> hFlush h
178 handler
:: FilePath -> Int -> Handle -> Maybe FilePath -> Handle -> Address
-> IO ()
179 handler dbpath delay logh mfeedPath hdl addr
= handle
((\e
-> return ()) :: SomeException
-> IO ()) $
181 where handler
' hdl addr
= do
182 response
<- handle
(\e
-> return $ ServerError
$ show (e
::SomeException
)) $ do
183 request
<- B
.decode
<$> BL
.hGetContents hdl
184 let hostname
= case addr
of
189 hashedHostname
= take 8 $ hash hostname
190 now
<- liftIO getCurrentTime
191 logit logh
$ show now
++ ": " ++ hashedHostname
++ " >>> " ++ showRequest request
192 response
<- handleRequest dbpath mfeedPath request
193 when (delay
> 0) $ threadDelay delay
194 now
' <- liftIO getCurrentTime
195 logit logh
$ show now
' ++ ": " ++ hashedHostname
++ " <<< " ++ showResponse response
197 BL
.hPut hdl
$ B
.encode response
199 showRequest
:: ClientRequest
-> String
200 showRequest
(ClientRequest ver mauth act
) = show ver
++ " "
201 ++ maybe "" (\(Auth name _
) -> "Auth:" ++ name
) mauth
++ " "
203 showAction
:: Action
-> String
204 showAction
(SetLock lock idx soln
) = "SetLock " ++ show idx
++ " lock:"
205 ++ (if not $ validLock
$ reframe lock
then " [INVALID LOCK] " else "\n" ++ unlines (lockToAscii lock
))
207 showAction
(DeclareSolution soln ls target idx
) = "DeclareSolution [SOLN] "
208 ++ unwords [show ls
,show target
,show idx
]
209 showAction act
= show act
210 showResponse
:: ServerResponse
-> String
211 showResponse
(ServedLock lock
) = "ServedLock lock:\n" ++ unlines (lockToAscii lock
)
212 showResponse
(ServedSolution soln
) = "ServedSolution [SOLN]"
213 showResponse resp
= show resp
215 handleRequest
:: FilePath -> Maybe FilePath -> ClientRequest
-> IO ServerResponse
216 handleRequest dbpath mfeedPath req
@(ClientRequest pv auth action
) = do
217 let lockMode
= case action
of
218 Authenticate
-> ReadMode
219 GetServerInfo
-> ReadMode
220 GetPublicKey
-> ReadMode
221 GetLock _
-> ReadMode
222 GetUserInfo _ _
-> ReadMode
223 GetRetired _
-> ReadMode
224 GetSolution _
-> ReadMode
225 GetRandomNames _
-> ReadMode
228 -- Check solutions prior to write-locking database.
229 -- Slightly awkward, because we have to drop the read lock before
230 -- acquiring the write lock, so need to check preconditions again once we
231 -- have the write lock.
232 withDBLock dbpath ReadMode
(runExceptT
$ checkRequest Nothing
) >>=
233 either (return . ServerError
) (\mCheckedLock
->
234 withDBLock dbpath lockMode
$
235 runExceptT
(checkRequest mCheckedLock
>> handleRequest
') >>=
236 either (return . ServerError
) return)
238 checkRequest mCheckedLock
= do
239 when (pv
/= protocolVersion
) $ throwE
"Bad protocol version"
241 DeclareSolution soln ls target idx
-> do
242 info
<- getUserInfoOfAuth auth
244 tinfo
<- getALock target
245 when (ls
/= lockSpec tinfo
) $ throwE
"Lock no longer in use!"
246 when (public tinfo
) $ throwE
"Lock solution already public knowledge!"
247 let name
= codename info
248 let behind
= ActiveLock name idx
249 when (name `
elem`
map noteAuthor
(lockSolutions tinfo
)) $
250 throwE
"Note already taken on that lock!"
251 when (name
== lockOwner target
) $
252 throwE
"That's your lock!"
253 behindLock
<- getALock behind
254 when (public behindLock
) $ throwE
"Your lock is cracked!"
256 Nothing
-> unless (checkSolution lock soln
) $ throwE
"Bad solution"
257 Just lock
' -> unless (lock
== lock
') $ throwE
"Lock changed!"
259 SetLock lock
@(frame
,_
) idx soln
-> do
260 ServerInfo serverSize _
<- getServerInfo
261 when (frame
/= BasicFrame serverSize
) $ throwE
$
262 "Server only accepts size "++show serverSize
++" locks."
263 unless (validLock
$ reframe lock
) $ throwE
"Invalid lock!"
264 when (checkSolved
$ reframe lock
) $ throwE
"Lock not locked!"
265 RCLockHashes hashes
<- getRecordErrored RecLockHashes
266 `catchE`
const (return (RCLockHashes
[]))
267 let hashed
= hash
$ show lock
268 when (hashed `
elem` hashes
) $ throwE
"Lock has already been used"
270 Nothing
-> unless (checkSolution lock soln
) $ throwE
"Bad solution"
271 Just lock
' -> unless (lock
== lock
') $ throwE
"Lock changed!"
276 UndefinedAction
-> throwE
"Request not recognised by this server"
279 return $ ServerMessage
$ "Welcome, " ++ authUser
(fromJust auth
)
282 doNews
$ "New user " ++ authUser
(fromJust auth
) ++ " registered."
284 ResetPassword passwd
-> resetPassword auth passwd
>> return ServerAck
285 SetEmail address
-> setEmail auth address
>> return ServerAck
286 GetServerInfo
-> ServedServerInfo
<$> getServerInfo
287 GetPublicKey
-> ServedPublicKey
<$> getPublicKey
288 GetLock ls
-> ServedLock
<$> getLock ls
289 GetRetired name
-> ServedRetired
<$> getRetired name
290 GetUserInfo name mversion
-> (do
291 RCUserInfo
(curV
,info
) <- getRecordErrored
$ RecUserInfo name
292 (fromJust<$>)$ runMaybeT
$ msum [ do
293 v
<- MaybeT
$ return mversion
294 msum [ guard (v
>= curV
) >> return ServerFresh
296 guard (v
>= curV
- 10)
297 RCUserInfoDeltas deltas
<- lift
$ getRecordErrored
$ RecUserInfoLog name
298 return $ ServedUserInfoDeltas
$ take (curV
-v
) deltas
300 , return $ ServedUserInfo
(curV
,info
)
302 ) `catchE`
\_
-> return ServerCodenameFree
303 GetSolution note
-> do
304 uinfo
<- getUserInfoOfAuth auth
305 let uname
= codename uinfo
306 onLinfo
<- getALock
$ noteOn note
307 behindMLinfo
<- maybe (return Nothing
) ((Just
<$>).getALock
) $ noteBehind note
308 if uname
== lockOwner
(noteOn note
)
309 || uname
== noteAuthor note
310 then ServedSolution
<$> getSolution note
311 else if case behindMLinfo
of
313 Just behindInfo
-> public behindInfo || uname `
elem` accessedBy behindInfo
314 || note `
elem` notesRead uinfo
315 then if public onLinfo || uname `
elem` accessedBy onLinfo
316 then ServedSolution
<$> getSolution note
317 else throwE
"You can't wholly decipher this note - you would need more notes on the same lock."
318 else throwE
"This note is secured behind a lock you have not opened."
319 DeclareSolution soln ls target idx
-> do
320 info
<- getUserInfoOfAuth auth
321 let name
= codename info
322 let behind
= ActiveLock name idx
323 let note
= NoteInfo name
(Just behind
) target
324 erroredDB
$ putRecord
(RecNote note
) (RCSolution soln
)
325 execStateT
(declareNote note behind
) [] >>= applyDeltasToRecords
326 doNews
$ name
++ " declares solution to "
327 ++ alockStr target
++ ", securing their note behind "
328 ++ alockStr behind
++ "."
329 mailDeclaration target behind
331 SetLock lock
@(frame
,_
) idx soln
-> do
332 info
<- getUserInfoOfAuth auth
333 let name
= codename info
334 let al
= ActiveLock name idx
335 RCLockHashes hashes
<- getRecordErrored RecLockHashes
336 `catchE`
const (return (RCLockHashes
[]))
337 let hashed
= hash
$ show lock
338 erroredDB
$ putRecord RecLockHashes
$ RCLockHashes
$ hashed
:hashes
340 ls
<- erroredDB
$ newLockRecord lock
341 let oldLockInfo
= userLocks info
! idx
343 when (isJust oldLockInfo
) $
344 lift
(getALock al
) >>= retireLock
345 addDelta name
$ PutLock ls idx
346 ) [] >>= applyDeltasToRecords
348 for_ oldLockInfo
$ \oldui
-> do
349 lss
<- getRetired name
350 erroredDB
$ putRecord
(RecRetiredLocks name
) $ RCLockSpecs
$ lockSpec oldui
:lss
351 doNews
$ "New lock " ++ alockStr al
++ "."
353 GetRandomNames n
-> do
354 names
<- erroredDB listUsers
355 gen
<- erroredIO
getStdGen
357 namesArray
= listArray (0,l
-1) names
359 uinfo
<- getUserInfo name
360 return $ all (maybe True public
. (userLocks uinfo
!)) [0..2]
363 shuffled
<- P
.toListM
$
364 mapM_ Pipes
.yield
(nub $ randomRs (0,l
-1) gen
)
365 >-> P
.take l
-- give up once we've permuted all of [0..l-1]
366 >-> P
.map (namesArray
!)
367 >-> P
.filterM ((not <$>) . negligible
) -- throw away negligibles
368 >-> P
.take n
-- try to take as many as we were asked for
370 return $ ServedRandomNames shuffled
371 _
-> throwE
"BUG: bad request"
372 erroredIO
:: IO a
-> ExceptT
String IO a
374 ret
<- liftIO
$ catchIO
(Right
<$> c
) (return.Left
)
376 Left e
-> throwE
$ "Server IO error: " ++ show e
378 erroredDB
:: DBM a
-> ExceptT
String IO a
379 erroredDB
= erroredIO
. withDB dbpath
380 getRecordErrored
:: Record
-> ExceptT
String IO RecordContents
381 getRecordErrored rec
= do
382 mrc
<- lift
$ withDB dbpath
$ getRecord rec
385 Nothing
-> throwE
$ "Bad record on server! Record was: " ++ show rec
387 RCLock lock
<- getRecordErrored
$ RecLock ls
389 getSolution note
= do
390 RCSolution soln
<- getRecordErrored
$ RecNote note
393 RCServerInfo sinfo
<- getRecordErrored RecServerInfo
396 RCPublicKey publicKey
<- getRecordErrored RecPublicKey
399 RCLockSpecs lss
<- fromMaybe (RCLockSpecs
[]) <$> erroredDB
(getRecord
$ RecRetiredLocks name
)
401 getALock
(ActiveLock name idx
) = do
402 info
<- getUserInfo name
403 checkValidLockIndex idx
404 case ((! idx
).userLocks
) info
of
405 Nothing
-> throwE
"Lock not set"
406 Just lockinfo
-> return lockinfo
407 checkValidLockIndex idx
=
408 unless (0<=idx
&& idx
< maxLocks
) $ throwE
"Bad lock index"
409 getUserInfo name
= do
410 RCUserInfo
(version
,info
) <- getRecordErrored
$ RecUserInfo name
412 getUserInfoOfAuth auth
= do
414 let Just
(Auth name _
) = auth
417 decryptPassword
:: String -> ExceptT
String IO String
418 decryptPassword pw
= do
419 RCSecretKey secretKey
<- getRecordErrored RecSecretKey
420 blinder
<- liftIO
. generateBlinder
$ private_n secretKey
421 ExceptT
. return . bimap
422 (\err
-> show err
++ "; try deleting ~/.intricacy/cache ?")
424 decrypt
(Just blinder
) (defaultOAEPParams SHA256
) secretKey
. CS
.pack
$ pw
425 -- XXX: <=intricacy-0.6.2 sends the hashed password unencrypted,
426 -- but we don't support that anymore
427 convertLegacyPW
:: Codename
-> IO ()
428 convertLegacyPW name
= void
. runExceptT
$ do
429 RCPasswordLegacy legacyPw
<- getRecordErrored
(RecPasswordLegacy name
)
430 pwA2
<- argon2 legacyPw
431 erroredDB
$ putRecord
(RecPasswordArgon2 name
) (RCPasswordArgon2 pwA2
)
432 erroredDB
$ delRecord
(RecPasswordLegacy name
)
433 checkAuth
:: Maybe Auth
-> ExceptT
String IO ()
434 checkAuth Nothing
= throwE
"Authentication required"
435 checkAuth
(Just
(Auth name pw
)) = do
436 exists
<- checkCodeName name
437 unless exists
$ throwE
"No such user"
438 liftIO
$ convertLegacyPW name
439 pw
' <- decryptPassword pw
440 RCPasswordArgon2 correctPwA2
<- getRecordErrored
(RecPasswordArgon2 name
)
442 when (pwA2
/= correctPwA2
) $ throwE
"Wrong password"
443 newUser
:: Maybe Auth
-> ExceptT
String IO ()
444 newUser Nothing
= throwE
"Require authentication"
445 newUser
(Just
(Auth name pw
)) = do
446 exists
<- checkCodeName name
447 when exists
$ throwE
"Codename taken"
448 pw
' <- decryptPassword pw
>>= argon2
449 erroredDB
$ putRecord
(RecPasswordArgon2 name
) (RCPasswordArgon2 pw
')
450 erroredDB
$ putRecord
(RecUserInfo name
) (RCUserInfo
(1,initUserInfo name
))
451 erroredDB
$ putRecord
(RecUserInfoLog name
) (RCUserInfoDeltas
[])
452 resetPassword Nothing _
= throwE
"Authentication required"
453 resetPassword auth
@(Just
(Auth name _
)) newpw
= do
455 newpw
' <- decryptPassword newpw
>>= argon2
456 erroredDB
$ putRecord
(RecPasswordArgon2 name
) (RCPasswordArgon2 newpw
')
457 setEmail Nothing _
= throwE
"Authentication required"
458 setEmail auth
@(Just
(Auth name _
)) addressStr
= do
460 serverAddr
<- erroredDB
$ getRecord RecServerEmail
461 when (isNothing serverAddr
) $ throwE
"This server is not configured to support email notifications."
462 let addr
= CS
.pack addressStr
463 unless (CS
.null addr || Text
.Email
.Validate
.isValid addr
) $ throwE
"Invalid email address"
464 erroredDB
$ putRecord
(RecEmail name
) (RCEmail addr
)
465 checkCodeName
:: Codename
-> ExceptT
String IO Bool
466 checkCodeName name
= do
467 unless (validCodeName name
) $ throwE
"Invalid codename"
468 liftIO
$ withDB dbpath
$ do
469 ok
<- recordExists
$ RecPasswordArgon2 name
470 oklegacy
<- recordExists
$ RecPasswordLegacy name
471 return $ ok || oklegacy
472 --- | TODO: journalling so we can survive death during database writes?
473 applyDeltasToRecords
:: [(Codename
, UserInfoDelta
)] -> ExceptT
String IO ()
474 applyDeltasToRecords nds
= sequence_ $ [applyDeltasToRecord name deltas
475 |
group <- groupBy ((==) `on`
fst) nds
476 , let name
= fst $ head group
477 , let deltas
= map snd group ]
478 applyDeltasToRecord name deltas
= do
479 erroredDB
$ modifyRecord
(RecUserInfoLog name
) $
480 \(RCUserInfoDeltas deltas
') -> RCUserInfoDeltas
$ deltas
++ deltas
'
481 erroredDB
$ modifyRecord
(RecUserInfo name
) $
482 \(RCUserInfo
(v
,info
)) -> RCUserInfo
483 (v
+length deltas
, applyDeltas info deltas
)
484 declareNote note
@(NoteInfo _ _ target
) behind
@(ActiveLock name idx
) = do
485 accessLock name target
=<< getCurrALock target
486 addDelta
(lockOwner target
) $ LockDelta
(lockIndex target
) $ AddSolution note
487 addDelta name
$ LockDelta idx
$ AddSecured note
488 accessed
<- accessedBy
<$> getCurrALock behind
489 mapM_ (addReadNote note
) (name
:accessed
)
490 addReadNote note
@(NoteInfo _ _ target
) name
= do
491 info
<- getCurrUserInfo name
492 tlock
<- getCurrALock target
493 unless (note `
elem` notesRead info
) $ do
494 addDelta name
$ AddRead note
495 checkSuffReadNotes target name
496 accessLock name target
@(ActiveLock tname ti
) tlock
= do
497 addDelta tname
$ LockDelta ti
$ AddAccessed name
498 mapM_ (`addReadNote` name
) $ notesSecured tlock
499 publiciseLock al
@(ActiveLock name idx
) lock
= do
500 addDelta name
$ LockDelta idx SetPublic
503 mapM_ scrapNote
$ lockSolutions lock
504 mapM_ publiciseNote
$ notesSecured lock
505 scrapNote note
@(NoteInfo _
(Just al
@(ActiveLock name idx
)) _
) = do
506 addDelta name
$ LockDelta idx
(DelSecured note
)
508 scrapNote _
= return ()
509 unreadNote note
@(NoteInfo name
(Just al
) _
) = do
510 lock
<- getCurrALock al
511 mapM_ (\name
' -> addDelta name
' (DelRead note
)) $ name
:accessedBy lock
512 publiciseNote note
@(NoteInfo _ _ al
@(ActiveLock name idx
)) = do
514 addDelta name
$ LockDelta idx
$ SetPubNote note
515 publified
<- checkSuffPubNotes al
516 unless publified
$ do
517 lock
<- getCurrALock al
518 accessorsOfNotesOnLock
<- (++ map noteAuthor
(lockSolutions lock
)).concat
520 [ accessedBy
<$> getCurrALock behind | NoteInfo _
(Just behind
) _
<- lockSolutions lock
]
521 forM_ accessorsOfNotesOnLock
$ checkSuffReadNotes al
522 checkSuffReadNotes target name
= do
523 info
<- getCurrUserInfo name
524 tlock
<- getCurrALock target
525 unless (name `
elem` accessedBy tlock || public tlock || name
== lockOwner target
) $ do
526 let countRead
= fromIntegral $ length $
527 filter (\n -> isNothing (noteBehind n
) || n `
elem` notesRead info
) $ lockSolutions tlock
528 when (countRead
== notesNeeded
) $
529 accessLock name target tlock
530 checkSuffPubNotes al
@(ActiveLock name idx
) = do
531 lock
<- getCurrALock al
532 let countPub
= fromIntegral $ length $
533 filter (isNothing.noteBehind
) $ lockSolutions lock
534 if countPub
== notesNeeded
535 then publiciseLock al lock
>> return True
537 -- | XXX we apply deltas right-to-left, so in the order of adding
538 addDelta name delta
= modify
((name
,delta
):)
539 getCurrUserInfo name
= do
540 info
<- lift
$ getUserInfo name
541 applyDeltas info
. map snd . filter ((==name
).fst) <$> get
542 getCurrALock al
@(ActiveLock name idx
) =
543 fromJust.(! idx
).userLocks
<$> getCurrUserInfo name
544 doNews
:: String -> ExceptT
String IO ()
545 doNews news
= case mfeedPath
of
547 Just feedPath
-> lift
$ void
$ forkIO
$ do
548 let baseFeed
= withFeedTitle
(TS
.pack
"Intricacy updates") $ newFeed
$ RSSKind Nothing
549 feed
<- fromMaybe baseFeed
<$> parseFeedFromFile feedPath
550 time
<- formatTime
defaultTimeLocale rfc822DateFormat
<$> getZonedTime
551 let newsText
= TS
.pack news
552 timeText
= TS
.pack time
553 item = withItemTitle newsText
$ withItemDescription newsText
$
554 withItemPubDate timeText
$ newItem
$ RSSKind Nothing
555 -- TODO: purge old entries
556 let Right element
= XML
.fromXMLElement
$ xmlFeed
$ withFeedLastUpdate timeText
$ addItem
item feed
557 document
= XML
.Document
(XML
.Prologue
[] Nothing
[]) element
[]
558 writeFile feedPath
$ TL
.unpack
$ XML
.renderText XML
.def document
559 mailDeclaration target
@(ActiveLock name _
) behind
@(ActiveLock solverName _
) = runMaybeT
$ do
560 let makeAddr
:: CS
.ByteString
-> SMTP
.Address
561 makeAddr bs
= SMTP
.Address Nothing
$ TS
.pack
$ CS
.unpack bs
562 RCEmail serverAddr
<- MaybeT
$ erroredDB
$ getRecord RecServerEmail
563 RCEmail playerAddr
<- MaybeT
$ erroredDB
$ getRecord
$ RecEmail name
564 guard $ not $ CS
.null playerAddr
565 lift
.lift
$ SMTP
.sendMail
"localhost" $ SMTP
.simpleMail
(makeAddr serverAddr
)
566 [makeAddr playerAddr
] [] []
567 (TS
.pack
$ "[Intricacy] " ++ alockStr target
++" solved by " ++ solverName
)
568 [plainPart
$ TL
.pack
$ "A solution to your lock " ++ alockStr target
++ " has been declared by " ++ solverName
++
569 " and secured behind " ++ alockStr behind
++ "." ++
570 "\n\n-----\n\nYou received this email from the game Intricacy" ++
571 "\n\thttp://sdf.org/~mbays/intricacy ." ++
572 "\nYou can disable notifications in-game by pressing 'R' on your home" ++
573 "\nscreen and setting an empty address." ++
574 "\nAlternatively, just reply to this email with the phrase \"stop bugging me\"." ]