espial-0.0.40: src/Handler/User.hs
-- {-# OPTIONS_GHC -fno-warn-unused-matches #-}
module Handler.User where
import Data.Aeson.Encoding (encodingToLazyByteString)
import Data.Map qualified as Map
import Data.Text qualified as T
import Data.Time (diffUTCTime)
import Handler.Common
import Import
import Network.Wai.Internal qualified as W
import Text.Blaze.Html5 qualified as H
import Yesod.RssFeed
getUserR :: UserNameP -> Handler Html
getUserR uname =
_getUser uname SharedAll FilterAll (TagsP [])
getUserSharedR :: UserNameP -> SharedP -> Handler Html
getUserSharedR uname sharedp =
_getUser uname sharedp FilterAll (TagsP [])
getUserFilterR :: UserNameP -> FilterP -> Handler Html
getUserFilterR uname filterp =
_getUser uname SharedAll filterp (TagsP [])
getUserTagsR :: UserNameP -> TagsP -> Handler Html
getUserTagsR uname = _getUser uname SharedAll FilterAll
_getUser :: UserNameP -> SharedP -> FilterP -> TagsP -> Handler Html
_getUser unamep@(UserNameP uname) sharedp' filterp' (TagsP pathtags) = do
app <- getYesod
muser <- fmap entityVal <$> maybeAuth
let lang = fromMaybe (appLanguageDefault (appSettings app)) (muser >>= userLanguage)
t = \key -> appTranslate app lang (I18nKey key)
frontendBundleName = appFrontendBundleName app
(limit', page') <- lookupPagingParams Nothing
let limit = maybe 120 (min 160 . fromIntegral) limit'
page = maybe 1 fromIntegral page'
isowner = Just uname == fmap userName muser
sharedp = if isowner then sharedp' else SharedPublic
filterp = case filterp' of
FilterSingle _ -> filterp'
_ -> if isowner then filterp' else FilterAll
isAll = filterp == FilterAll && sharedp == SharedAll && null pathtags
queryp = "query" :: Text
beforep = pagingCursorBeforeParam
afterp = pagingCursorAfterParam
mquery <- lookupGetParam queryp
mcursor <-
parsePagingCursorParams
(fmap PagingCursorBefore . parsePagingCursorTime)
(fmap PagingCursorAfter . parsePagingCursorTime)
<$> lookupGetParam beforep
<*> lookupGetParam afterp
(suggestTags, suggestTagsUseReturnKey, publicTagCloud, bcount, btmarks, hasEarlier, hasLater) <- runDB $ do
Entity userId user <- getBy404 (UniqueUserName uname)
when
(not isowner && userPrivacyLock user)
(redirect (AuthR LoginR))
(bcount, btmarks, hasEarlier, hasLater) <-
bookmarksTagsQuery userId isowner sharedp filterp pathtags mquery mcursor limit page
pure (userSuggestTags user, userSuggestTagsUseReturnKey user, userPublicTagCloud user, bcount, btmarks, hasEarlier, hasLater)
when (bcount == 0) (case filterp of FilterSingle _ -> notFound; _ -> pure ())
mroute <- getCurrentRoute
tagCloudMode <- getTagCloudMode isowner publicTagCloud pathtags
render <- getUrlRender
let archiveBackendEnabled = isJust (appArchiver app)
mfirstBookmark = headMay (map fst btmarks)
mlastBookmark = lastMay (map fst btmarks)
mqueryEarlierp =
fmap
(beforep,)
(formatEntityPagingCursorTimeBm <$> mlastBookmark)
mqueryLaterp =
fmap
(afterp,)
(formatEntityPagingCursorTimeBm <$> mfirstBookmark)
mqueryp = fmap (queryp,) mquery
renderEl = "bookmarks" :: Text
tagCloudRenderEl = "tagCloud" :: Text
showTagCloud = isowner || publicTagCloud
tagCloudUrl :: Text
tagCloudUrl
| isowner = render UserTagCloudR
| publicTagCloud = render (UserPublicTagCloudR unamep)
| otherwise = ""
defaultLayout do
let pager = $(widgetFile "pager")
search = $(widgetFile "search")
rssLink (UserFeedR unamep) "feed"
$(widgetFile "user")
toWidgetBody
[julius|
app.dat.bmarks = #{ toRawJs $ toBookmarkFormListForViewer isowner btmarks } || [];
app.dat.bcount = #{ toJSON bcount };
app.dat.isowner = #{ isowner };
app.dat.suggestTags = #{ suggestTags };
app.dat.suggestTagsUseReturnKey = #{ suggestTagsUseReturnKey };
app.dat.archiveBackendEnabled = #{ archiveBackendEnabled };
app.dat.filter = #{ toJSON filterp } || {};
app.dat.sharedp = #{ toJSON sharedp };
app.dat.tags = #{ toJSON pathtags };
app.dat.query = #{ toJSON mquery };
app.userR = "@{UserR unamep}";
app.tagCloudMode = #{ toJSON $ tagCloudMode } || {};
app.tagCloudR = #{toJSON tagCloudUrl};
|]
toWidget
[hamlet|
<script type="module">
import { renderBookmarks, renderTagCloud, renderBulkEdit } from '@{StaticR (StaticRoute ["js", frontendBundleName] [])}'
setTimeout(() => {
renderBookmarks('##{renderEl}')(app.dat.bmarks)();
}, 0);
setTimeout(() => {
renderTagCloud('##{tagCloudRenderEl}')(app.tagCloudMode)();
}, 0);
$if isowner
setTimeout(() => {
renderBulkEdit('#bulkEditRenderEl')(app.dat.bcount)();
}, 0);
|]
where
toRawJs = rawJS . decodeUtf8 . encodingToLazyByteString . toEncoding
-- Form
postUserTagCloudR :: Handler ()
postUserTagCloudR = do
userId <- requireAuthId
mode <- requireCheckJsonBody
_updateTagCloudMode mode
tc <- runDB $ case mode of
TagCloudModeTop _ -> tagCountTop userId True
TagCloudModeTopLowerBound _ n -> tagCountLowerBound userId n True
TagCloudModeRelated _ tags -> tagCountRelated userId tags True
TagCloudModeRelatedLowerBound _ tags lb -> tagCountRelatedLowerBound userId tags lb True
TagCloudModeNone -> pure []
sendStatusJSON ok200 (Map.fromList tc :: Map.Map Text Int)
postUserPublicTagCloudR :: UserNameP -> Handler ()
postUserPublicTagCloudR (UserNameP uname) = do
Entity userId user <- runDB $ getBy404 (UniqueUserName uname)
unless (userPublicTagCloud user) notFound
when (userPrivacyLock user) notFound
mode <- requireCheckJsonBody
_updateTagCloudMode mode
app' <- getYesod
let cacheRef = appPublicTagCloudCache app'
ttl = fromIntegral (appPublicTagCloudCacheDurationSeconds (appSettings app'))
now <- liftIO getCurrentTime
cacheMap <- liftIO $ readIORef cacheRef
case Map.lookup (uname, mode) cacheMap of
Just (storedAt, tc)
| diffUTCTime now storedAt < ttl ->
sendStatusJSON ok200 tc
_ -> do
tcList <- runDB $ case mode of
TagCloudModeTop _ -> tagCountTop userId False
TagCloudModeTopLowerBound _ n -> tagCountLowerBound userId n False
TagCloudModeRelated _ tags -> tagCountRelated userId tags False
TagCloudModeRelatedLowerBound _ tags lb -> tagCountRelatedLowerBound userId tags lb False
TagCloudModeNone -> pure []
let tc = Map.fromList tcList
liftIO $ atomicModifyIORef' cacheRef $ \m ->
let pruned = Map.filter (\(t, _) -> diffUTCTime now t < ttl) m
evicted = if Map.size pruned >= 1000 then Map.deleteMin pruned else pruned
in (Map.insert (uname, mode) (now, tc) evicted, ())
sendStatusJSON ok200 tc
postUserTagCloudModeR :: Handler ()
postUserTagCloudModeR = do
void requireAuthId
mode <- requireCheckJsonBody
_updateTagCloudMode mode
_updateTagCloudMode :: TagCloudMode -> Handler ()
_updateTagCloudMode mode =
case mode of
TagCloudModeTop _ -> setTagCloudMode mode
TagCloudModeTopLowerBound _ _ -> setTagCloudMode mode
TagCloudModeRelated _ _ -> pure ()
TagCloudModeRelatedLowerBound _ _ _ -> pure ()
TagCloudModeNone -> notFound
bookmarkToRssEntry :: (Entity Bookmark, Maybe Text) -> FeedEntry Text
bookmarkToRssEntry (Entity _entryId entry, tags) =
FeedEntry
{ feedEntryLink = bookmarkHref entry,
feedEntryUpdated = bookmarkTime entry,
feedEntryTitle = bookmarkDescription entry,
feedEntryContent = toHtml (bookmarkExtended entry),
feedEntryCategories = map (EntryCategory Nothing Nothing) (maybe [] words tags),
feedEntryEnclosure = Nothing
}
getUserFeedR :: UserNameP -> Handler RepRss
getUserFeedR unamep = do
_getUserFeed unamep SharedAll FilterAll (TagsP [])
getUserFeedSharedR :: UserNameP -> SharedP -> Handler RepRss
getUserFeedSharedR uname sharedp =
_getUserFeed uname sharedp FilterAll (TagsP [])
getUserFeedFilterR :: UserNameP -> FilterP -> Handler RepRss
getUserFeedFilterR uname filterp =
_getUserFeed uname SharedAll filterp (TagsP [])
getUserFeedTagsR :: UserNameP -> TagsP -> Handler RepRss
getUserFeedTagsR uname = _getUserFeed uname SharedAll FilterAll
_getUserFeed :: UserNameP -> SharedP -> FilterP -> TagsP -> Handler RepRss
_getUserFeed unamep@(UserNameP uname) sharedp' filterp' (TagsP pathtags) = do
mauthuname <- maybeAuthUsername
(limit', page') <- lookupPagingParams Nothing
let limit = maybe 120 (min 160 . fromIntegral) limit'
page = maybe 1 fromIntegral page'
isowner = Just uname == mauthuname
sharedp = if isowner then sharedp' else SharedPublic
filterp = case filterp' of
FilterSingle _ -> filterp'
_ -> if isowner then filterp' else FilterAll
-- isAll = filterp == FilterAll && sharedp == SharedAll && null pathtags
queryp = "query" :: Text
beforep = pagingCursorBeforeParam
afterp = pagingCursorAfterParam
mquery <- lookupGetParam queryp
mcursor <-
parsePagingCursorParams
(fmap PagingCursorBefore . parsePagingCursorTime)
(fmap PagingCursorAfter . parsePagingCursorTime)
<$> lookupGetParam beforep
<*> lookupGetParam afterp
(_, btmarks, _, _) <- runDB $ do
Entity userId user <- getBy404 (UniqueUserName uname)
when
(not isowner && userPrivacyLock user)
(redirect (AuthR LoginR))
bookmarksTagsQuery userId isowner sharedp filterp pathtags mquery mcursor limit page
let (descr :: Html) = toHtml $ H.text ("Bookmarks saved by " <> uname)
entries = map bookmarkToRssEntry btmarks
updated <- case maximumMay (map feedEntryUpdated entries) of
Nothing -> liftIO getCurrentTime
Just m -> pure m
(feedLinkSelf, feedLinkHome) <- getFeedLinkSelf
rssFeedText
$ Feed
{ feedTitle = "espial " <> uname,
feedLinkSelf = feedLinkSelf,
feedLinkHome = feedLinkHome,
feedAuthor = uname,
feedDescription = descr,
feedLanguage = "en",
feedUpdated = updated,
feedLogo = Nothing,
feedEntries = entries
}
where
getFeedLinkSelf = do
request <- getRequest
render <- getUrlRender
let rawRequest = reqWaiRequest request
feedLinkSelf = render HomeR <> (T.drop 1 (decodeUtf8 (W.rawPathInfo rawRequest <> W.rawQueryString rawRequest)))
feedLinkHome = render (UserR unamep)
pure (feedLinkSelf, feedLinkHome)