├── Setup.hs ├── static ├── favicon.ico ├── css │ ├── hpaste.png │ └── amelie.css ├── hpaste.el ├── hs │ └── stepeval-prelude.hs └── js │ ├── amelie.js │ └── highlight-haskell.js ├── scripts ├── cron-update ├── start └── server-restart ├── .gitignore ├── src ├── Control │ └── Monad │ │ ├── IO.hs │ │ ├── Catch.hs │ │ └── Env.hs ├── Data │ ├── Monoid │ │ └── Operator.hs │ ├── Maybe │ │ └── Extra.hs │ ├── Text │ │ ├── FromText.hs │ │ └── ToText.hs │ ├── Time │ │ ├── Show.hs │ │ └── Relative.hs │ ├── Either │ │ └── Extra.hs │ └── String │ │ ├── Extra.hs │ │ └── ToString.hs ├── Snap │ └── App.hs ├── Hpaste │ ├── Types │ │ ├── Activity.hs │ │ ├── Page.hs │ │ ├── Stepeval.hs │ │ ├── Cache.hs │ │ ├── Channel.hs │ │ ├── Announcer.hs │ │ ├── Language.hs │ │ ├── Report.hs │ │ ├── Config.hs │ │ ├── Newtypes.hs │ │ └── Paste.hs │ ├── Model │ │ ├── Channel.hs │ │ ├── Language.hs │ │ ├── Activity.hs │ │ ├── Announcer.hs │ │ ├── Report.hs │ │ ├── Spam.hs │ │ ├── Irclogs.hs │ │ └── Paste.hs │ ├── Controller │ │ ├── Script.hs │ │ ├── Diff.hs │ │ ├── Reported.hs │ │ ├── Activity.hs │ │ ├── Browse.hs │ │ ├── Raw.hs │ │ ├── Irclogs.hs │ │ ├── Home.hs │ │ ├── Admin.hs │ │ ├── Rss.hs │ │ ├── New.hs │ │ ├── Report.hs │ │ ├── Cache.hs │ │ └── Paste.hs │ ├── View │ │ ├── New.hs │ │ ├── Edit.hs │ │ ├── Annotate.hs │ │ ├── Thanks.hs │ │ ├── Activity.hs │ │ ├── Hlint.hs │ │ ├── Reported.hs │ │ ├── Irclogs.hs │ │ ├── Diff.hs │ │ ├── Report.hs │ │ ├── Home.hs │ │ ├── Script.hs │ │ ├── Stepeval.hs │ │ ├── Browse.hs │ │ ├── Layout.hs │ │ ├── Steps.hs │ │ ├── Highlight.hs │ │ ├── Html.hs │ │ └── Paste.hs │ ├── Types.hs │ └── Config.hs ├── Text │ └── Blaze │ │ └── Html5 │ │ └── Extra.hs ├── Network │ ├── URI │ │ └── Params.hs │ ├── Email.hs │ └── SendEmail.hs ├── Main.hs └── HJScript │ └── Objects │ └── JQuery │ └── Extra.hs ├── README.md ├── amelie.conf.sample ├── hpaste.conf.sample ├── hpaste.cabal └── sql └── schema.sql /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alpmestan/lpaste/master/static/favicon.ico -------------------------------------------------------------------------------- /static/css/hpaste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alpmestan/lpaste/master/static/css/hpaste.png -------------------------------------------------------------------------------- /scripts/cron-update: -------------------------------------------------------------------------------- 1 | cd /home/chris/hpaste 2 | logger -t hpaste "Clearing cache ..." 3 | rm cache/* 4 | rm log/* -------------------------------------------------------------------------------- /scripts/start: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | cd /home/chris/hpaste/ 3 | mkdir log -p 4 | dist/build/hpaste/hpaste hpaste.conf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | cabal-dev 3 | log 4 | src/TAGS 5 | *.hi 6 | *.o 7 | amelie.conf 8 | irclogs 9 | cache/ 10 | TAGS 11 | .hsenv/ 12 | hpaste.conf -------------------------------------------------------------------------------- /src/Control/Monad/IO.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | 3 | module Control.Monad.IO where 4 | 5 | import Control.Monad.Trans 6 | 7 | io :: MonadIO m => IO a -> m a 8 | io = liftIO 9 | -------------------------------------------------------------------------------- /src/Data/Monoid/Operator.hs: -------------------------------------------------------------------------------- 1 | -- | Convenient operator. 2 | 3 | module Data.Monoid.Operator where 4 | 5 | import Data.Monoid 6 | 7 | (++) :: (Monoid a) => a -> a -> a 8 | (++) = mappend -------------------------------------------------------------------------------- /scripts/server-restart: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | mkdir log -p 4 | echo 'Killing ...' 5 | killall hpaste 6 | sleep 0.2 7 | echo 'Starting ...' 8 | dist/build/hpaste/hpaste hpaste.conf & disown 9 | -------------------------------------------------------------------------------- /src/Control/Monad/Catch.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE PackageImports #-} 2 | 3 | module Control.Monad.Catch (module Control.Monad.CatchIO) where 4 | 5 | import "MonadCatchIO-transformers" Control.Monad.CatchIO 6 | -------------------------------------------------------------------------------- /src/Snap/App.hs: -------------------------------------------------------------------------------- 1 | module Snap.App 2 | (module Snap.Core 3 | ,module Snap.App.Types 4 | ,module Snap.App.Controller 5 | ,module Snap.App.Model) 6 | where 7 | 8 | import Snap.Core 9 | import Snap.App.Types 10 | import Snap.App.Controller 11 | import Snap.App.Model 12 | -------------------------------------------------------------------------------- /src/Hpaste/Types/Activity.hs: -------------------------------------------------------------------------------- 1 | module Hpaste.Types.Activity where 2 | 3 | import Data.Text.Lazy (Text) 4 | import Data.Time (UTCTime) 5 | 6 | data Commit = Commit { 7 | commitTitle :: Text 8 | ,commitContent :: Text 9 | ,commitDate :: UTCTime 10 | ,commitLink :: Text 11 | } deriving Show -------------------------------------------------------------------------------- /src/Data/Maybe/Extra.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Data.Maybe.Extra where 5 | 6 | -- | When a value is Just, do something with it, monadically. 7 | whenJust :: Monad m => Maybe a -> (a -> m c) -> m () 8 | whenJust (Just a) m = m a >> return () 9 | whenJust _ _ = return () 10 | -------------------------------------------------------------------------------- /src/Data/Text/FromText.hs: -------------------------------------------------------------------------------- 1 | module Data.Text.FromText where 2 | 3 | import Data.Text 4 | import qualified Data.Text.Lazy as L 5 | import Data.ByteString 6 | import qualified Data.ByteString.Lazy as L 7 | 8 | class FromText a where 9 | fromText :: Text -> Maybe a 10 | fromLazyText :: L.Text -> Maybe a 11 | fromLazyText = fromText . L.toStrict 12 | -------------------------------------------------------------------------------- /src/Text/Blaze/Html5/Extra.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Text.Blaze.Html5.Extra where 5 | 6 | import Text.Blaze.Html5 as H 7 | import qualified Text.Blaze.Html5.Attributes as A 8 | 9 | -- | A POST form. 10 | postForm :: Html -> Html 11 | postForm = H.form ! A.method "POST" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Database setup 2 | 3 | $ sudo su postgres --command 'createuser hpaste -P' 4 | $ sudo su postgres --command 'createdb hpaste -O hpaste' 5 | $ cat sql/schema.sql | psql -U hpaste -h 127.0.0.1 -d hpaste 6 | 7 | ## Configuration & Running 8 | 9 | $ cp hpaste.conf.sample hpaste.conf 10 | 11 | Edit hpaste.conf. 12 | 13 | $ dist/build/hpaste/hpaste hpaste.conf 14 | 15 | -------------------------------------------------------------------------------- /src/Control/Monad/Env.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | Abstraction of environment functions (could be state, could be 5 | -- reader, whatever). Intended to ease migration from Reader/State. 6 | 7 | module Control.Monad.Env 8 | (env) 9 | where 10 | 11 | import Control.Monad.Reader 12 | 13 | env :: MonadReader env m => (env -> val) -> m val 14 | env = asks 15 | -------------------------------------------------------------------------------- /src/Hpaste/Types/Page.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | The page type. 5 | 6 | module Hpaste.Types.Page 7 | (Page(..)) 8 | where 9 | 10 | import Data.Text (Text) 11 | import Text.Blaze (Markup) 12 | 13 | -- | A page to be rendered in a layout. 14 | data Page = Page { 15 | pageTitle :: Text 16 | , pageBody :: Markup 17 | , pageName :: Text 18 | } 19 | -------------------------------------------------------------------------------- /src/Data/Text/ToText.hs: -------------------------------------------------------------------------------- 1 | module Data.Text.ToText where 2 | 3 | import Data.Text 4 | import qualified Data.Text.Lazy as L 5 | import Data.ByteString 6 | import Data.Text.Encoding 7 | import qualified Data.ByteString.Lazy as L 8 | 9 | class ToText a where 10 | toText :: a -> Text 11 | toLazyText :: a -> L.Text 12 | toLazyText = L.fromStrict . toText 13 | 14 | instance ToText ByteString where 15 | toText = decodeUtf8 16 | -------------------------------------------------------------------------------- /src/Hpaste/Model/Channel.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Channel model. 6 | 7 | module Hpaste.Model.Channel 8 | (getChannels) 9 | where 10 | 11 | import Hpaste.Types 12 | 13 | import Snap.App 14 | 15 | -- | Get the channels. 16 | getChannels :: Model c s [Channel] 17 | getChannels = 18 | queryNoParams ["SELECT *" 19 | ,"FROM channel"] 20 | -------------------------------------------------------------------------------- /src/Data/Time/Show.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Date/time showing functions. 6 | 7 | module Data.Time.Show 8 | (showDateTime) 9 | where 10 | 11 | import Data.Time (FormatTime,formatTime) 12 | import System.Locale (defaultTimeLocale) 13 | 14 | showDateTime :: FormatTime t => t -> String 15 | showDateTime time = formatTime defaultTimeLocale "%F %T %Z" time 16 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Script.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | JavaScript controller. 5 | 6 | module Hpaste.Controller.Script 7 | (handle) 8 | where 9 | 10 | import Hpaste.View.Script (script) 11 | 12 | import Snap.Core (modifyResponse,setContentType) 13 | import Snap.App 14 | 15 | handle :: Controller c s () 16 | handle = do 17 | modifyResponse $ setContentType "text/javascript" 18 | outputText $ script 19 | -------------------------------------------------------------------------------- /src/Hpaste/Types/Stepeval.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-orphans #-} 2 | {-# LANGUAGE RecordWildCards #-} 3 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | 6 | -- | The stepeval types. 7 | 8 | module Hpaste.Types.Stepeval 9 | (StepevalPage(..)) 10 | where 11 | 12 | import Data.Text (Text) 13 | import Language.Haskell.HLint 14 | 15 | data StepevalPage = StepevalPage { 16 | sePaste :: Text 17 | , seHints :: [Suggestion] 18 | } 19 | -------------------------------------------------------------------------------- /src/Data/Either/Extra.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Data.Either.Extra where 5 | 6 | -- | When a value is Right, do something with it, monadically. 7 | whenRight :: Monad m => Either a b -> (b -> m c) -> m () 8 | whenRight (Right x) m = m x >> return () 9 | whenRight _ _ = return () 10 | 11 | -- | When a value is Left, do something with it, monadically. 12 | whenLeft :: Monad m => Either a b -> (a -> m c) -> m () 13 | whenLeft (Left x) m = m x >> return () 14 | whenLeft _ _ = return () 15 | -------------------------------------------------------------------------------- /src/Hpaste/Model/Language.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Language model. 6 | 7 | module Hpaste.Model.Language 8 | (getLanguages) 9 | where 10 | 11 | import Hpaste.Types 12 | 13 | import Snap.App 14 | 15 | -- | Get the languages. 16 | getLanguages :: Model c s [Language] 17 | getLanguages = 18 | queryNoParams ["SELECT id,name,title" 19 | ,"FROM language" 20 | ,"WHERE visible" 21 | ,"ORDER BY ordinal ASC,title ASC"] 22 | -------------------------------------------------------------------------------- /src/Hpaste/Types/Cache.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | 3 | -- | HTML caching types. 4 | 5 | module Hpaste.Types.Cache 6 | (Key(..) 7 | ,Cache(..)) 8 | where 9 | 10 | import Control.Concurrent.MVar (MVar) 11 | import Data.Map (Map) 12 | import Data.Text.Lazy (Text) 13 | import Hpaste.Types.Newtypes 14 | 15 | data Key = 16 | Home 17 | | Paste PasteId 18 | | Revision PasteId 19 | | Activity 20 | deriving (Eq,Ord) 21 | 22 | data Cache = 23 | Cache { 24 | cacheMap :: MVar (Map Key Text) 25 | } 26 | -------------------------------------------------------------------------------- /amelie.conf.sample: -------------------------------------------------------------------------------- 1 | [POSTGRESQL] 2 | host = 127.0.0.1 3 | port = 5432 4 | user = hpaste 5 | pass = hpaste 6 | db = hpaste 7 | 8 | [ANNOUNCE] 9 | user = yourdomain 10 | pass = yourpass 11 | host = 127.0.0.1 12 | port = 6667 13 | 14 | [WEB] 15 | domain = yourdomain.org 16 | cache = /tmp/hpaste-cache 17 | 18 | [DEV] 19 | commits = https://github.com/chrisdone/hpaste/commits/master.atom 20 | repo_url = https://github.com/chrisdone/hpaste 21 | 22 | [IRC] 23 | log_dir = irc_logs 24 | 25 | [ADDRESSES] 26 | admin = email@yourdomain.com 27 | site_addy = noreply@yourdomain.org -------------------------------------------------------------------------------- /src/Hpaste/Controller/Diff.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | 5 | -- | Diff page controller. 6 | 7 | module Hpaste.Controller.Diff 8 | (handle) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.Controller.Paste (withPasteKey) 13 | import Hpaste.View.Diff (page) 14 | 15 | import Snap.App 16 | 17 | -- | Diff one paste with another. 18 | handle :: HPCtrl () 19 | handle = do 20 | withPasteKey "this" $ \this -> 21 | withPasteKey "that" $ \that -> 22 | output $ page this that 23 | -------------------------------------------------------------------------------- /src/Hpaste/Types/Channel.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | 3 | -- | The channel type. 4 | 5 | module Hpaste.Types.Channel 6 | (Channel(..)) 7 | where 8 | 9 | import Hpaste.Types.Newtypes 10 | import Control.Applicative 11 | import Data.Text (Text) 12 | import Database.PostgreSQL.Simple 13 | import Database.PostgreSQL.Simple.FromRow 14 | 15 | data Channel = Channel { 16 | channelId :: ChannelId 17 | ,channelName :: Text 18 | } deriving Show 19 | 20 | instance FromRow Channel where 21 | fromRow = Channel <$> field 22 | <*> field 23 | -------------------------------------------------------------------------------- /src/Data/String/Extra.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TypeSynonymInstances #-} 2 | 3 | -- | Instances that can be converted to a string. 4 | 5 | module Data.String.ToString where 6 | 7 | import Data.ByteString 8 | import qualified Data.ByteString.UTF8 as UTF8 (toString) 9 | 10 | class ToString string where 11 | toString :: string -> String 12 | 13 | instance ToString String where toString = id 14 | 15 | (+++) :: (ToString str1,ToString str2) => str1 -> str2 -> String 16 | str1 +++ str2 = toString str1 ++ toString str2 17 | 18 | instance ToString ByteString where toString = UTF8.toString 19 | -------------------------------------------------------------------------------- /hpaste.conf.sample: -------------------------------------------------------------------------------- 1 | [POSTGRESQL] 2 | host = 127.0.0.1 3 | port = 5432 4 | user = hpaste 5 | pass = hpaste 6 | db = hpaste 7 | 8 | [ANNOUNCE] 9 | user = yourdomain 10 | pass = yourpass 11 | host = 127.0.0.1 12 | port = 6667 13 | 14 | [WEB] 15 | domain = yourdomain.org 16 | cache = /tmp/hpaste-cache 17 | 18 | [DEV] 19 | commits = https://github.com/chrisdone/hpaste/commits/master.atom 20 | repo_url = https://github.com/chrisdone/hpaste 21 | 22 | [IRC] 23 | log_dir = irc_logs 24 | 25 | [ADDRESSES] 26 | admin = email@yourdomain.com 27 | site_addy = noreply@yourdomain.org 28 | 29 | [ADMIN] 30 | key = myadminpass 31 | -------------------------------------------------------------------------------- /src/Hpaste/Types/Announcer.hs: -------------------------------------------------------------------------------- 1 | module Hpaste.Types.Announcer where 2 | 3 | import Control.Concurrent 4 | import Data.Text 5 | 6 | -- | Announcer configuration. 7 | data AnnounceConfig = AnnounceConfig { 8 | announceUser :: String 9 | , announcePass :: String 10 | , announceHost :: String 11 | , announcePort :: Int 12 | } deriving (Show) 13 | 14 | -- | An announcer. 15 | data Announcer = Announcer 16 | { annChan :: Chan Announcement 17 | , annConfig :: AnnounceConfig 18 | } 19 | 20 | -- | An annoucement. 21 | data Announcement = Announcement 22 | { annFrom :: Text 23 | , annContent :: Text 24 | } 25 | -------------------------------------------------------------------------------- /src/Data/String/ToString.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE TypeSynonymInstances #-} 3 | 4 | -- | Instances that can be converted to a string. 5 | 6 | module Data.String.ToString where 7 | 8 | import Data.ByteString 9 | import qualified Data.ByteString.UTF8 as UTF8 (toString) 10 | 11 | class ToString string where 12 | toString :: string -> String 13 | 14 | instance ToString String where toString = id 15 | 16 | (+++) :: (ToString str1,ToString str2) => str1 -> str2 -> String 17 | str1 +++ str2 = toString str1 ++ toString str2 18 | 19 | instance ToString ByteString where toString = UTF8.toString 20 | -------------------------------------------------------------------------------- /src/Hpaste/Types/Language.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | 3 | -- | The language type. 4 | 5 | module Hpaste.Types.Language 6 | (Language(..)) 7 | where 8 | 9 | import Hpaste.Types.Newtypes 10 | import Control.Applicative 11 | import Data.Text (Text) 12 | import Database.PostgreSQL.Simple 13 | import Database.PostgreSQL.Simple.FromRow 14 | 15 | data Language = Language { 16 | languageId :: LanguageId 17 | ,languageName :: Text 18 | ,languageTitle :: Text 19 | } deriving Show 20 | 21 | instance FromRow Language where 22 | fromRow = Language <$> field 23 | <*> field 24 | <*> field 25 | -------------------------------------------------------------------------------- /src/Hpaste/Types/Report.hs: -------------------------------------------------------------------------------- 1 | module Hpaste.Types.Report where 2 | 3 | import Hpaste.Types.Newtypes (PasteId) 4 | 5 | import Control.Applicative 6 | import Data.Text (Text) 7 | import Data.Time (UTCTime,zonedTimeToUTC) 8 | import Database.PostgreSQL.Simple 9 | import Database.PostgreSQL.Simple.FromRow 10 | 11 | data Report = Report { 12 | reportDate :: UTCTime 13 | ,reportPasteId :: PasteId 14 | ,reportComments :: Text 15 | } deriving Show 16 | 17 | instance FromRow Report where 18 | fromRow = Report <$> fmap zonedTimeToUTC field 19 | <*> field 20 | <*> field 21 | -------------------------------------------------------------------------------- /src/Hpaste/View/New.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Create new paste view. 6 | 7 | module Hpaste.View.New 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | import Hpaste.View.Layout 14 | 15 | import Prelude hiding ((++)) 16 | import Text.Blaze.Html5 as H hiding (map) 17 | 18 | -- | Render the create new paste page. 19 | page :: Html -> Html 20 | page form = 21 | layoutPage $ Page { 22 | pageTitle = "Create new paste" 23 | , pageBody = lightSection "Create new paste" form 24 | , pageName = "new" 25 | } 26 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Reported.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | Reported page controller. 5 | 6 | module Hpaste.Controller.Reported 7 | (handle) 8 | where 9 | 10 | import Hpaste.Model.Report (getSomeReports,countReports) 11 | import Hpaste.Controller.Admin (withAuth) 12 | import Hpaste.Types 13 | import Hpaste.View.Reported (page) 14 | 15 | import Text.Blaze.Pagination 16 | import Data.Pagination 17 | import Snap.App 18 | 19 | -- | List the reported pastes. 20 | handle :: HPCtrl () 21 | handle = 22 | withAuth $ \key -> do 23 | pn <- getPagination "reported" 24 | total <- model countReports 25 | reports <- model $ getSomeReports (pnPn pn) 26 | output $ page pn reports key 27 | -------------------------------------------------------------------------------- /src/Hpaste/View/Edit.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Edit paste view. 6 | 7 | module Hpaste.View.Edit 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | import Hpaste.View.Layout 14 | 15 | import Data.Monoid.Operator ((++)) 16 | import Prelude hiding ((++)) 17 | import Text.Blaze.Html5 as H hiding (map) 18 | import Data.Text.Lazy 19 | 20 | -- | Render the create edit paste page. 21 | page :: Paste -> Html -> Html 22 | page Paste{..} form = 23 | layoutPage $ Page { 24 | pageTitle = "Edit: " ++ pasteTitle 25 | , pageBody = lightSection ("Edit: " ++ fromStrict pasteTitle) form 26 | , pageName = "edit" 27 | } 28 | -------------------------------------------------------------------------------- /src/Hpaste/View/Annotate.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Annotate paste view. 6 | 7 | module Hpaste.View.Annotate 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | import Hpaste.View.Layout 14 | 15 | import Data.Monoid.Operator ((++)) 16 | import Prelude hiding ((++)) 17 | import Text.Blaze.Html5 as H hiding (map) 18 | import Data.Text.Lazy 19 | 20 | -- | Render the create annotate paste page. 21 | page :: Paste -> Html -> Html 22 | page Paste{..} form = 23 | layoutPage $ Page { 24 | pageTitle = "Annotate: " ++ pasteTitle 25 | , pageBody = lightSection ("Annotate: " ++ fromStrict pasteTitle) form 26 | , pageName = "annotate" 27 | } 28 | -------------------------------------------------------------------------------- /static/hpaste.el: -------------------------------------------------------------------------------- 1 | (defun lpaste-region () 2 | (interactive) 3 | (format "curl http://lpaste.net/new?%s" 4 | (mapconcat 'identity 5 | (mapcar (lambda (cons) 6 | (concat (url-hexify-string (car cons)) 7 | "=" 8 | (url-hexify-string (cdr cons)))) 9 | `(("title" . "Elis1p") 10 | ("author" . "chrisdone") 11 | ("language" . "haskell") 12 | ("channel" . "") 13 | ("paste" . "Code here'%@!") 14 | ("private" . "private") 15 | ("email" . ""))) 16 | "&"))) 17 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Activity.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | Activity page controller. 5 | 6 | module Hpaste.Controller.Activity 7 | (handle) 8 | where 9 | 10 | import Hpaste.Types 11 | import Hpaste.Controller.Cache (cache) 12 | import Hpaste.Model.Activity (getCommits) 13 | import Hpaste.Types.Cache as Key 14 | import Hpaste.View.Activity (page) 15 | 16 | import Control.Monad.Env (env) 17 | import Snap.App 18 | 19 | -- | Display commit history. 20 | handle :: HPCtrl () 21 | handle = do 22 | html <- cache Key.Activity $ do 23 | uri <- env $ configCommits . controllerStateConfig 24 | repourl <- env $ configRepoURL . controllerStateConfig 25 | commits <- model $ getCommits uri 26 | return $ Just $ page repourl commits 27 | maybe (return ()) outputText html 28 | -------------------------------------------------------------------------------- /src/Hpaste/Types/Config.hs: -------------------------------------------------------------------------------- 1 | -- | Site-wide configuration. 2 | 3 | module Hpaste.Types.Config 4 | (Config(..) 5 | ,AnnounceConfig(..)) 6 | where 7 | 8 | import Database.PostgreSQL.Simple (ConnectInfo) 9 | import Network.Mail.Mime (Address) 10 | import Snap.App.Types 11 | 12 | import Hpaste.Types.Announcer 13 | 14 | -- | Site-wide configuration. 15 | data Config = Config { 16 | configAnnounce :: AnnounceConfig 17 | , configPostgres :: ConnectInfo 18 | , configDomain :: String 19 | , configCommits :: String 20 | , configRepoURL :: String 21 | , configIrcDir :: FilePath 22 | , configAdmin :: Address 23 | , configSiteAddy :: Address 24 | , configCacheDir :: FilePath 25 | , configKey :: String 26 | } 27 | 28 | instance AppConfig Config where 29 | getConfigDomain = configDomain 30 | 31 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Browse.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | Browse page controller. 5 | 6 | module Hpaste.Controller.Browse 7 | (handle) 8 | where 9 | 10 | import Hpaste.Types 11 | import Hpaste.Model.Channel (getChannels) 12 | import Hpaste.Model.Language (getLanguages) 13 | import Hpaste.Model.Paste (getPaginatedPastes,countPublicPastes) 14 | import Hpaste.View.Browse (page) 15 | 16 | import Control.Monad.IO 17 | import Data.Time 18 | import Text.Blaze.Pagination 19 | import Snap.App 20 | 21 | -- | Browse all pastes. 22 | handle :: HPCtrl () 23 | handle = do 24 | pn <- getPagination "pastes" 25 | author <- getStringMaybe "author" 26 | (pn',pastes) <- model $ getPaginatedPastes author (pnPn pn) 27 | chans <- model getChannels 28 | langs <- model getLanguages 29 | now <- io getCurrentTime 30 | output $ page now pn { pnPn = pn' } chans langs pastes author 31 | -------------------------------------------------------------------------------- /src/Hpaste/View/Thanks.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing -fno-warn-unused-do-bind #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Thanks view. 6 | 7 | module Hpaste.View.Thanks 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | import Hpaste.View.Layout 14 | 15 | import Data.String 16 | import Data.Text (Text) 17 | import Prelude hiding ((++)) 18 | import Text.Blaze.Html5 as H hiding (map) 19 | 20 | -- | Render the thanks5 page. 21 | page :: String -> String -> Html 22 | page title msg = 23 | layoutPage $ Page { 24 | pageTitle = fromString title 25 | , pageBody = thanks title msg 26 | , pageName = "thanks" 27 | } 28 | 29 | thanks :: String -> String -> Html 30 | thanks title msg = do 31 | darkSection (fromString title) $ do 32 | p $ toHtml msg 33 | p $ href ("/" :: Text) 34 | ("Go back home" :: Text) 35 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Raw.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | 5 | -- | Raw controller. 6 | 7 | module Hpaste.Controller.Raw 8 | (handle) 9 | where 10 | 11 | import Hpaste.Model.Paste (getPasteById) 12 | import Hpaste.Types 13 | 14 | import Control.Applicative 15 | import Data.ByteString.UTF8 (toString) 16 | import Data.Maybe 17 | import Data.Text.Lazy (fromStrict) 18 | import Prelude hiding ((++)) 19 | import Safe 20 | import Snap.App 21 | 22 | -- | Handle the paste page. 23 | handle :: HPCtrl () 24 | handle = do 25 | pid <- (>>= readMay) . fmap (toString) <$> getParam "id" 26 | case pid of 27 | Nothing -> goHome 28 | Just (pid :: Integer) -> do 29 | modifyResponse $ setContentType "text/plain; charset=UTF-8" 30 | paste <- model $ getPasteById (PasteId pid) 31 | maybe goHome (outputText . fromStrict . pastePaste) paste 32 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Irclogs.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | Irclogs page controller. 5 | 6 | module Hpaste.Controller.Irclogs 7 | (handle) 8 | where 9 | 10 | import Hpaste.Controller 11 | import Hpaste.Model.Irclogs 12 | import Hpaste.Types 13 | import Hpaste.View.Irclogs (page) 14 | 15 | import Data.String.ToString 16 | import Data.String 17 | import Snap.Types 18 | import Safe 19 | 20 | handle :: Controller () 21 | handle = do 22 | channel <- get "channel" 23 | date <- get "date" 24 | time <- get "timestamp" 25 | pasteid <- getMaybe "paste" 26 | logs <- getNarrowedLogs channel date time 27 | output $ page channel date time logs pasteid 28 | 29 | where get key = do 30 | value <- fmap (fmap toString) $ getParam (fromString key) 31 | case value of 32 | Nothing -> error $ "Missing parameter: " ++ key 33 | Just value -> return value 34 | getMaybe key = fmap ((>>= readMay) . fmap toString) $ getParam (fromString key) 35 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Home.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | Home page controller. 5 | 6 | module Hpaste.Controller.Home 7 | (handle) 8 | where 9 | 10 | import Hpaste.Types 11 | import Hpaste.Controller.Cache (cacheIf) 12 | import Hpaste.Controller.Paste (pasteForm) 13 | import Hpaste.Model.Channel (getChannels) 14 | import Hpaste.Model.Language (getLanguages) 15 | import Hpaste.Model.Paste (getLatestPastes) 16 | import Hpaste.Types.Cache as Key 17 | import Hpaste.View.Home (page) 18 | 19 | import Data.Maybe 20 | import Snap.App 21 | 22 | -- | Handle the home page, display a simple list and paste form. 23 | handle :: Bool -> HPCtrl () 24 | handle spam = do 25 | html <- cacheIf (not spam) Key.Home $ do 26 | -- pastes <- model $ getLatestPastes Nothing 27 | chans <- model $ getChannels 28 | langs <- model $ getLanguages 29 | form <- pasteForm chans langs Nothing Nothing Nothing 30 | uri <- getMyURI 31 | return $ Just $ page uri chans langs [] form spam 32 | maybe (return ()) outputText html 33 | -------------------------------------------------------------------------------- /src/Hpaste/Types/Newtypes.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | 4 | -- | Newtypes; foreign keys and such. 5 | 6 | module Hpaste.Types.Newtypes 7 | (PasteId(..) 8 | ,ChannelId(..) 9 | ,LanguageId(..) 10 | ,ReportId(..)) 11 | where 12 | 13 | import Database.PostgreSQL.Simple 14 | import Database.PostgreSQL.Simple.FromField 15 | import Database.PostgreSQL.Simple.ToField 16 | 17 | newtype PasteId = PasteId Integer 18 | deriving (Eq,FromField,ToField,Ord) 19 | 20 | instance Show PasteId where show (PasteId pid) = show pid 21 | 22 | newtype ReportId = ReportId Integer 23 | deriving (Integral,Real,Num,Ord,Eq,Enum,FromField,ToField) 24 | 25 | instance Show ReportId where show (ReportId pid) = show pid 26 | 27 | newtype ChannelId = ChannelId Integer 28 | deriving (Integral,Real,Num,Ord,Eq,Enum,FromField,ToField) 29 | 30 | instance Show ChannelId where show (ChannelId pid) = show pid 31 | 32 | newtype LanguageId = LanguageId Integer 33 | deriving (Integral,Real,Num,Ord,Eq,Enum,FromField,ToField) 34 | 35 | instance Show LanguageId where show (LanguageId pid) = show pid 36 | -------------------------------------------------------------------------------- /src/Hpaste/View/Activity.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing -fno-warn-unused-do-bind #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Activity page view. 6 | 7 | module Hpaste.View.Activity 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | import Hpaste.View.Layout 14 | 15 | import Control.Monad 16 | import Data.Text (Text) 17 | import Prelude hiding ((++)) 18 | import Text.Blaze.Html5 as H hiding (map) 19 | 20 | -- | Render the activity page. 21 | page :: String -> [Commit] -> Html 22 | page repo commits = 23 | layoutPage $ Page { 24 | pageTitle = "Development activity" 25 | , pageBody = activity repo commits 26 | , pageName = "activity" 27 | } 28 | 29 | -- | View the paginated pastes. 30 | activity :: String -> [Commit] -> Html 31 | activity repo commits = do 32 | darkSection "Development activity" $ do 33 | p $ do "Repository: " 34 | href repo repo 35 | forM_ commits $ \Commit{..} -> do 36 | lightSection commitTitle $ do 37 | p $ toHtml $ show commitDate 38 | p $ href commitLink ("Go to diff" :: Text) 39 | -------------------------------------------------------------------------------- /src/Hpaste/View/Hlint.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Show hlint suggestions. 6 | 7 | module Hpaste.View.Hlint 8 | (viewHints 9 | ,viewSuggestions) 10 | where 11 | 12 | import Hpaste.Types 13 | import Hpaste.View.Html 14 | 15 | import Data.List (intersperse) 16 | import Language.Haskell.HLint 17 | import Prelude hiding ((++)) 18 | import Text.Blaze.Html5 as H hiding (map) 19 | 20 | -- | Show hlint hints for a Haskell paste. 21 | viewHints :: [Hint] -> Html 22 | viewHints = mapM_ showHint where 23 | showHint hint = 24 | section $ 25 | pre ! aClass "hint" $ sequence_ $ intersperse br $ map toHtml lns 26 | where section = case hintType hint of 27 | Ignore -> \_ -> return () 28 | Warning -> warnNoTitleSection 29 | Error -> errorNoTitleSection 30 | lns = lines $ clean $ hintContent hint 31 | clean = dropWhile (==':') . dropWhile (/=':') 32 | 33 | viewSuggestions :: [Suggestion] -> Html 34 | viewSuggestions = viewHints . map toHint where 35 | toHint s = Hint (suggestionSeverity s) 36 | (show s) 37 | -------------------------------------------------------------------------------- /src/Network/URI/Params.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NamedFieldPuns #-} 2 | {-# OPTIONS -fno-warn-missing-signatures #-} 3 | module Network.URI.Params (updateUrlParam,updateUrlParams,uriParams,deleteQueryKey) where 4 | 5 | import Control.Arrow 6 | import Network.URI 7 | import Data.List 8 | import Data.Function 9 | import Network.CGI 10 | 11 | updateUrlParam :: String -> String -> URI -> URI 12 | updateUrlParam this value uri@(URI{uriQuery}) = 13 | uri { uriQuery = updated uriQuery } where 14 | updated = editQuery $ ((this,value):) . deleteBy ((==) `on` fst) (this,"") 15 | 16 | deleteQueryKey :: String -> URI -> URI 17 | deleteQueryKey key uri = 18 | uri { uriQuery = editQuery (filter ((/=key).fst)) (uriQuery uri) } 19 | 20 | editQuery :: ([(String,String)] -> [(String,String)]) -> String -> String 21 | editQuery f = ('?':) . formEncodeUrl . f . formDecode . dropWhile (=='?') 22 | 23 | formEncodeUrl = intercalate "&" . map keyval . map (esc *** esc) 24 | where keyval (key,val) = key ++ "=" ++ val 25 | esc = escapeURIString isAllowedInURI 26 | 27 | updateUrlParams :: [(String,String)] -> URI -> URI 28 | updateUrlParams = flip $ foldr $ uncurry updateUrlParam 29 | 30 | uriParams :: URI -> [(String,String)] 31 | uriParams = formDecode . dropWhile (=='?') . uriQuery 32 | -------------------------------------------------------------------------------- /src/Hpaste/Model/Activity.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | Activity model. 5 | 6 | module Hpaste.Model.Activity 7 | (getCommits) 8 | where 9 | 10 | import Hpaste.Types 11 | 12 | import Control.Monad.IO (io) 13 | import Data.Maybe (mapMaybe) 14 | import Data.Text.Lazy (pack) 15 | import Data.Time 16 | import Network.Curl.Download 17 | import Snap.App.Types 18 | import System.Locale 19 | import Text.Feed.Query 20 | 21 | -- | Get commits of this project from a commit feed. 22 | getCommits :: String -> Model c s [Commit] 23 | getCommits uri = io $ do 24 | result <- openAsFeed uri 25 | case result of 26 | Left _ -> return [] 27 | Right feed -> return $ 28 | let items = getFeedItems feed 29 | in mapMaybe makeCommit items 30 | 31 | where makeCommit item = do 32 | title <- getItemTitle item 33 | datestr <- getItemDate item 34 | date <- parseDateString datestr 35 | link <- getItemLink item 36 | return $ Commit { 37 | commitTitle = pack $ title 38 | , commitContent = "" -- Getting content from atom does not work. 39 | , commitDate = date 40 | , commitLink = pack link 41 | } 42 | -- E.g. 2011-06-11T11:15:11-07:00 43 | parseDateString = parseTime defaultTimeLocale "%Y-%M-%dT%T%Z" 44 | -------------------------------------------------------------------------------- /src/Network/Email.hs: -------------------------------------------------------------------------------- 1 | module Network.SendEmail where 2 | 3 | -- | An email to be sent via SMTP. 4 | data Email = 5 | Email { emailSMTPHost :: String 6 | , emailEHLO :: String 7 | , emailFromName :: String 8 | , emailToName :: String 9 | , emailFromEmail :: String 10 | , emailToEmail :: String 11 | , emailSubject :: String 12 | , emailBody :: String 13 | } 14 | 15 | -- | Send an SMTP email. 16 | sendEmail :: (MonadIO m) => Email -> m () 17 | sendEmail Email{..} = 18 | io $ do 19 | addr <- lookupIP emailSMTPHost 20 | case addr of 21 | Just ip -> sendSimpleMessages putStrLn ip emailEHLO [msg] 22 | Nothing -> error "Unable to lookup the SMTP IP." 23 | where msg = SimpleMessage { 24 | from = [NameAddr (Just emailFromName) emailFromEmail] 25 | , to = [NameAddr (Just emailToName) emailToEmail] 26 | , subject = emailSubject 27 | , body = emailBody 28 | } 29 | -- | Look up the IP address for the SMTP server. 30 | lookupIP :: MonadIO m => String -> m (Maybe String) 31 | lookupIP domain = io $ do 32 | let hints = defaultHints { addrFlags = [AI_ADDRCONFIG, AI_CANONNAME] } 33 | addrs <- getAddrInfo (Just hints) (Just domain) (Just "smtp") 34 | return $ listToMaybe $ map (takeWhile (/=':') . show . addrAddress) addrs 35 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Admin.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | 6 | -- | Report controller. 7 | 8 | module Hpaste.Controller.Admin 9 | (withAuth) 10 | where 11 | 12 | import Hpaste.Controller.Cache (resetCache) 13 | import Hpaste.Model.Paste (getPasteById,deletePaste) 14 | import Hpaste.Model.Report 15 | import Hpaste.Types 16 | import Hpaste.Types.Cache as Key 17 | import Hpaste.View.Report 18 | import qualified Hpaste.View.Thanks as Thanks 19 | 20 | import Control.Applicative 21 | import Control.Monad.Reader 22 | import Data.ByteString.UTF8 (toString) 23 | import Data.Maybe 24 | import Data.Monoid.Operator ((++)) 25 | import Data.Text (unpack) 26 | import Prelude hiding ((++)) 27 | import Safe 28 | import Snap.App 29 | import Text.Blaze.Html5 as H hiding (output,map,body) 30 | import Text.Formlet 31 | 32 | -- | Do something with authority. 33 | withAuth :: (String -> HPCtrl ()) -> HPCtrl () 34 | withAuth m = do 35 | key <- fmap (fmap toString) $ getParam "key" 36 | realkey <- asks (configKey . controllerStateConfig) 37 | case key of 38 | Just k | k == realkey -> m k 39 | _ -> goHome 40 | -------------------------------------------------------------------------------- /src/Hpaste/View/Reported.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Reported page view. 6 | 7 | module Hpaste.View.Reported 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | import Hpaste.View.Layout 14 | 15 | import Text.Blaze.Pagination 16 | import Data.Monoid.Operator ((++)) 17 | import Data.Time.Show (showDateTime) 18 | import Prelude hiding ((++)) 19 | import Network.URI 20 | import Text.Blaze.Html5 as H hiding (map) 21 | import Snap.App.Types 22 | 23 | -- | Render the reported page. 24 | page :: PN -> [Report] -> String -> Html 25 | page pn rs key = 26 | layoutPage $ Page { 27 | pageTitle = "Reported pastes" 28 | , pageBody = reported pn rs key 29 | , pageName = "reported" 30 | } 31 | 32 | -- | View the paginated reports. 33 | reported :: PN -> [Report] -> String -> Html 34 | reported pn rs key = do 35 | darkSection "Reported pastes" $ do 36 | pagination pn 37 | table ! aClass "latest-pastes" $ do 38 | tr $ mapM_ (th . toHtml) $ words "Date Paste Delete Comments" 39 | reports rs 40 | pagination pn 41 | 42 | where reports = mapM_ $ \Report{..} -> tr $ do 43 | td $ toHtml $ showDateTime reportDate 44 | td $ href ("/" ++ show reportPasteId ++ "?show_private=true") $ show reportPasteId 45 | td $ href ("/delete?id=" ++ show reportPasteId ++ "&key=" ++ key) ("Delete"::String) 46 | td $ toHtml reportComments 47 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Rss.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE RecordWildCards #-} 3 | 4 | -- | An RSS feed of recent pastes. 5 | 6 | module Hpaste.Controller.Rss where 7 | 8 | import Hpaste.Model.Paste 9 | import Hpaste.Model.Channel 10 | import Hpaste.Types 11 | 12 | import Control.Applicative 13 | import Control.Monad 14 | import Data.List 15 | import Data.Monoid 16 | import Data.String.ToString 17 | import qualified Data.Text as T 18 | import Data.Text.Encoding 19 | import Safe 20 | import Snap.App 21 | import Snap.App.RSS 22 | 23 | handle :: HPCtrl () 24 | handle = do 25 | cid <- getChannelId 26 | case cid of 27 | Nothing -> error "need a channel" 28 | Just cid' -> do 29 | pastes <- model $ getLatestPastes cid 30 | outputRSS "#haskell pastes" 31 | "http://lpaste.net/channel/haskell.rss" 32 | (map (\Paste{..} -> (pasteDate,pasteTitle,pastePaste, T.pack $ 33 | "http://lpaste.net/" ++ show pasteId)) 34 | pastes) 35 | 36 | getChannelId :: HPCtrl (Maybe ChannelId) 37 | getChannelId = do 38 | chname <- fmap (fmap decodeUtf8) $ getParam "channel" 39 | case chname of 40 | Nothing -> return Nothing 41 | Just chname -> do 42 | channels <- model $ getChannels 43 | case find ((==("#" <> chname)) . channelName) channels of 44 | Nothing -> return Nothing 45 | Just Channel{..} -> return (return channelId) 46 | -------------------------------------------------------------------------------- /src/Hpaste/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE TypeSynonymInstances #-} 3 | {-# LANGUAGE MultiParamTypeClasses #-} 4 | {-# OPTIONS -Wall -fno-warn-orphans #-} 5 | 6 | -- | All types. 7 | 8 | module Hpaste.Types 9 | (module Hpaste.Types.Paste 10 | ,module Hpaste.Types.Channel 11 | ,module Hpaste.Types.Language 12 | ,module Hpaste.Types.Page 13 | ,module Hpaste.Types.Newtypes 14 | ,module Hpaste.Types.Config 15 | ,module Hpaste.Types.Activity 16 | ,module Hpaste.Types.Stepeval 17 | ,module Hpaste.Types.Report 18 | ,HPState 19 | ,HPCtrl 20 | ,HPModel) 21 | where 22 | 23 | import Hpaste.Types.Paste 24 | import Hpaste.Types.Channel 25 | import Hpaste.Types.Language 26 | import Hpaste.Types.Page 27 | import Hpaste.Types.Newtypes 28 | import Hpaste.Types.Config 29 | import Hpaste.Types.Activity 30 | import Hpaste.Types.Stepeval 31 | import Hpaste.Types.Report 32 | import Hpaste.Types.Announcer (Announcer) 33 | 34 | import Control.Concurrent (Chan) 35 | import Control.Monad.Env 36 | import Control.Monad.IO 37 | import Control.Monad.Reader 38 | import Data.Text.Lazy (Text) 39 | import Snap.App.Types 40 | 41 | type HPState = Announcer 42 | type HPCtrl = Controller Config HPState 43 | type HPModel = Model Config HPState 44 | 45 | instance AppLiftModel Config HPState where 46 | liftModel action = do 47 | conn <- env controllerStateConn 48 | anns <- env controllerState 49 | conf <- env controllerStateConfig 50 | let state = ModelState conn anns conf 51 | io $ runReaderT (runModel action) state 52 | -------------------------------------------------------------------------------- /src/Network/SendEmail.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE RecordWildCards #-} 2 | 3 | -- | Simple module for sending emails. 4 | 5 | module Network.SendEmail where 6 | 7 | import Control.Monad.IO 8 | import Control.Monad.Trans 9 | import Data.Maybe 10 | import Network.SMTP.Simple 11 | import Network.Socket 12 | 13 | -- | An email to be sent via SMTP. 14 | data Email = 15 | Email { emailSMTPHost :: String 16 | , emailEHLO :: String 17 | , emailFromName :: String 18 | , emailToName :: String 19 | , emailFromEmail :: String 20 | , emailToEmail :: String 21 | , emailSubject :: String 22 | , emailBody :: String 23 | } 24 | 25 | -- | Send an SMTP email. 26 | sendEmail :: (MonadIO m) => Email -> m () 27 | sendEmail Email{..} = 28 | io $ do 29 | addr <- lookupIP emailSMTPHost 30 | case addr of 31 | Just ip -> sendSimpleMessages putStrLn ip emailEHLO [msg] 32 | Nothing -> error "Unable to lookup the SMTP IP." 33 | where msg = SimpleMessage { 34 | from = [NameAddr (Just emailFromName) emailFromEmail] 35 | , to = [NameAddr (Just emailToName) emailToEmail] 36 | , subject = emailSubject 37 | , body = emailBody 38 | } 39 | -- | Look up the IP address for the SMTP server. 40 | lookupIP :: MonadIO m => String -> m (Maybe String) 41 | lookupIP domain = io $ do 42 | let hints = defaultHints { addrFlags = [AI_ADDRCONFIG, AI_CANONNAME] } 43 | addrs <- getAddrInfo (Just hints) (Just domain) (Just "smtp") 44 | return $ listToMaybe $ map (takeWhile (/=':') . show . addrAddress) addrs 45 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/New.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | Create new paste controller. 5 | 6 | module Hpaste.Controller.New 7 | (handle,NewStyle(..)) 8 | where 9 | 10 | import Hpaste.Types 11 | import Hpaste.Controller.Paste (pasteForm,getPasteId) 12 | import Hpaste.Model.Channel (getChannels) 13 | import Hpaste.Model.Language (getLanguages) 14 | import Hpaste.Model.Paste (getPasteById) 15 | import Hpaste.View.Annotate as Annotate (page) 16 | import Hpaste.View.Edit as Edit (page) 17 | import Hpaste.View.New as New (page) 18 | 19 | import Control.Applicative 20 | import Data.Text.Encoding (decodeUtf8) 21 | import Snap.App 22 | 23 | data NewStyle = NewPaste | AnnotatePaste | EditPaste 24 | deriving Eq 25 | 26 | -- | Make a new paste. 27 | handle :: NewStyle -> HPCtrl () 28 | handle style = do 29 | chans <- model $ getChannels 30 | langs <- model $ getLanguages 31 | defChan <- fmap decodeUtf8 <$> getParam "channel" 32 | pid <- if style == NewPaste then return Nothing else getPasteId 33 | case pid of 34 | Just pid -> do 35 | paste <- model $ getPasteById pid 36 | let apaste | style == AnnotatePaste = paste 37 | | otherwise = Nothing 38 | let epaste | style == EditPaste = paste 39 | | otherwise = Nothing 40 | form <- pasteForm chans langs defChan apaste epaste 41 | justOrGoHome paste $ \paste -> do 42 | case style of 43 | AnnotatePaste -> output $ Annotate.page paste form 44 | EditPaste -> output $ Edit.page paste form 45 | _ -> goHome 46 | Nothing -> do 47 | form <- pasteForm chans langs defChan Nothing Nothing 48 | output $ New.page form 49 | -------------------------------------------------------------------------------- /static/hs/stepeval-prelude.hs: -------------------------------------------------------------------------------- 1 | module Prelude where 2 | 3 | -- Arithmetic operations and some other primitives are builtin 4 | -- Type signatures are entirely useless here. 5 | -- Guards on the rhs of function equations are not supported, but in case 6 | -- expressions they are. 7 | 8 | -- combinators 9 | id x = x 10 | 11 | const x _ = x 12 | 13 | f $ x = f x 14 | -- infixr 0 $ 15 | 16 | flip f x y = f y x 17 | 18 | (f . g) x = f (g x) 19 | -- infixr 9 . 20 | 21 | fix f = let x = f x in x 22 | 23 | -- booleans 24 | not True = False 25 | not False = True 26 | 27 | True || _ = True 28 | False || b = b 29 | -- infixr 2 || 30 | 31 | False && _ = False 32 | True && b = b 33 | -- infixr 3 && 34 | 35 | -- tuples 36 | fst (x, _) = x 37 | snd (_, x) = x 38 | 39 | curry f x y = f (x, y) 40 | uncurry f (x, y) = f x y 41 | 42 | -- lists 43 | foldr _ z [] = z 44 | foldr f z (x:xs) = x `f` foldr f z xs 45 | 46 | foldl _ acc [] = acc 47 | foldl f acc (x:xs) = foldl f (f acc x) xs 48 | -- foldl f z xs = foldr (\x r z -> r (f z x)) id xs z 49 | 50 | null [] = True 51 | null _ = False 52 | 53 | map f [] = [] 54 | map f (x:xs) = f x : map f xs 55 | 56 | head (x:_) = x 57 | tail (_:xs) = xs 58 | 59 | [] ++ ys = ys 60 | (x:xs) ++ ys = x : (xs ++ ys) 61 | -- infixr 5 ++ 62 | 63 | take n xs = if n <= 0 then [] 64 | else case xs of 65 | [] -> [] 66 | y:ys -> y : take (n - 1) ys 67 | 68 | repeat x = let xs = x : xs in xs 69 | 70 | drop n xs = if n <= 0 || null xs 71 | then xs 72 | else drop (n - 1) (tail xs) 73 | 74 | length [] = 0 75 | length (x:xs) = 1 + length xs 76 | 77 | scanl f z [] = [z] 78 | scanl f z (x:xs) = z : scanl f (f z x) xs 79 | 80 | reverse = foldl (flip (:)) [] 81 | 82 | zipWith _ [] _ = [] 83 | zipWith _ _ [] = [] 84 | zipWith (*) (x:xs) (y:ys) = x * y : zipWith (*) xs ys 85 | 86 | iterate f x = x : iterate f (f x) 87 | -------------------------------------------------------------------------------- /src/Hpaste/Config.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-missing-signatures -fno-warn-name-shadowing #-} 2 | 3 | -- | Load the configuration file. 4 | 5 | module Hpaste.Config 6 | (getConfig) 7 | where 8 | 9 | import Hpaste.Types.Config 10 | import Hpaste.Model.Announcer 11 | 12 | import Data.ConfigFile 13 | import Database.PostgreSQL.Simple (ConnectInfo(..)) 14 | import qualified Data.Text as T 15 | import Network.Mail.Mime 16 | 17 | getConfig :: FilePath -> IO Config 18 | getConfig conf = do 19 | contents <- readFile conf 20 | let config = do 21 | c <- readstring emptyCP contents 22 | [user,pass,host,port] 23 | <- mapM (get c "ANNOUNCE") 24 | ["user","pass","host","port"] 25 | [pghost,pgport,pguser,pgpass,pgdb] 26 | <- mapM (get c "POSTGRESQL") 27 | ["host","port","user","pass","db"] 28 | [domain,cache] 29 | <- mapM (get c "WEB") 30 | ["domain","cache"] 31 | [commits,url] 32 | <- mapM (get c "DEV") 33 | ["commits","repo_url"] 34 | [ircDir] 35 | <- mapM (get c "IRC") 36 | ["log_dir"] 37 | [admin,siteaddy] 38 | <- mapM (get c "ADDRESSES") 39 | ["admin","site_addy"] 40 | [key] <- mapM (get c "ADMIN") ["key"] 41 | 42 | return Config { 43 | configAnnounce = AnnounceConfig user pass host (read port) 44 | , configPostgres = ConnectInfo pghost (read pgport) pguser pgpass pgdb 45 | , configDomain = domain 46 | , configCommits = commits 47 | , configRepoURL = url 48 | , configIrcDir = ircDir 49 | , configAdmin = Address Nothing (T.pack admin) 50 | , configSiteAddy = Address Nothing (T.pack siteaddy) 51 | , configCacheDir = cache 52 | , configKey = key 53 | } 54 | case config of 55 | Left cperr -> error $ show cperr 56 | Right config -> return config 57 | -------------------------------------------------------------------------------- /src/Data/Time/Relative.hs: -------------------------------------------------------------------------------- 1 | -- | Display times as a relative duration. E.g. x days ago. 2 | 3 | module Data.Time.Relative where 4 | 5 | import Data.List 6 | import Data.Time 7 | import Text.Printf 8 | 9 | -- | Display a time span as one time relative to another. 10 | relativeZoned :: ZonedTime -- ^ The later time span. 11 | -> ZonedTime -- ^ The earlier time span. 12 | -> Bool -- ^ Display 'in/ago'? 13 | -> String -- ^ Example: '3 seconds ago', 'in three days'. 14 | relativeZoned t1 t2 fix = 15 | relative (zonedTimeToUTC t1) (zonedTimeToUTC t2) fix 16 | 17 | -- | Display a time span as one time relative to another. 18 | relative :: UTCTime -- ^ The later time span. 19 | -> UTCTime -- ^ The earlier time span. 20 | -> Bool -- ^ Display 'in/ago'? 21 | -> String -- ^ Example: '3 seconds ago', 'in three days'. 22 | relative t1 t2 fix = maybe "unknown" format $ find (\(s,_,_) -> abs span>=s) $ reverse ranges where 23 | minute = 60; hour = minute * 60; day = hour * 24; 24 | week = day * 7; month = day * 30; year = month * 12 25 | format range = 26 | (if fix && span>0 then "in " else "") 27 | ++ case range of 28 | (_,str,0) -> str 29 | (_,str,base) -> printf str (abs $ round (span / base) :: Integer) 30 | ++ (if fix && span<0 then " ago" else "") 31 | span = t1 `diffUTCTime` t2 32 | ranges = [(0,"%d seconds",1) 33 | ,(minute,"a minute",0) 34 | ,(minute*2,"%d minutes",minute) 35 | ,(minute*30,"half an hour",0) 36 | ,(minute*31,"%d minutes",minute) 37 | ,(hour,"an hour",0) 38 | ,(hour*2,"%d hours",hour) 39 | ,(hour*3,"a few hours",0) 40 | ,(hour*4,"%d hours",hour) 41 | ,(day,"a day",0) 42 | ,(day*2,"%d days",day) 43 | ,(week,"a week",0) 44 | ,(week*2,"%d weeks",week) 45 | ,(month,"a month",0) 46 | ,(month*2,"%d months",month) 47 | ,(year,"a year",0) 48 | ,(year*2,"%d years",year) 49 | ] 50 | -------------------------------------------------------------------------------- /src/Hpaste/View/Irclogs.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing -fno-warn-unused-do-bind #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Irclogs page view. 6 | 7 | module Hpaste.View.Irclogs 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | import Hpaste.View.Layout 14 | 15 | import Control.Monad 16 | import Data.Char 17 | import Data.Maybe 18 | import Data.Monoid.Operator ((++)) 19 | import Data.String 20 | import Data.Text (Text) 21 | import qualified Data.Text as T 22 | import Prelude hiding ((++)) 23 | import Text.Blaze.Extra 24 | import Text.Blaze.Html5 as H hiding (map) 25 | import qualified Text.Blaze.Html5.Attributes as A 26 | 27 | -- | Render the irclogs page. 28 | page :: String -> String -> String -> Either String [Text] -> Maybe Integer -> Html 29 | page channel date time entries pid = 30 | layoutPage $ Page { 31 | pageTitle = "Development irclogs" 32 | , pageBody = irclogs pid channel entries 33 | , pageName = "irclogs" 34 | } 35 | 36 | -- | View the paginated pastes. 37 | irclogs :: Maybe Integer -> String -> Either String [Text] -> Html 38 | irclogs pid channel entries = do 39 | darkSection "IRC logs" $ do 40 | p $ do "Channel: #"; toHtml channel 41 | lightSection (fromString ("#" ++ channel)) $ do 42 | case entries of 43 | Left error -> do "Unable to get logs for this channel and date: " 44 | toHtml error 45 | Right entries -> 46 | ul !. "amelie-irc-entries" $ 47 | forM_ entries $ \entry -> do 48 | let date = toValue $ parseDate entry 49 | url = "http://hpaste.org/" ++ maybe "0" (T.pack . show) pid 50 | currentline | T.isSuffixOf url entry = "current" 51 | | otherwise = "" 52 | li !. (toValue (currentline :: Text)) $ do 53 | a ! A.name date ! A.id date $ return () 54 | toHtml entry 55 | 56 | where parseDate = T.replace ":" "-" . T.takeWhile (not.isSpace) 57 | -------------------------------------------------------------------------------- /src/Hpaste/View/Diff.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing -fno-warn-unused-do-bind #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Diff page view. 6 | 7 | module Hpaste.View.Diff 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | import Hpaste.View.Layout 14 | import Hpaste.View.Paste (pasteLink) 15 | 16 | import Control.Monad 17 | import Data.Algorithm.Diff 18 | import Data.Monoid.Operator ((++)) 19 | import qualified Data.Text as T 20 | import Data.Text.Lazy (pack) 21 | import Prelude hiding ((++)) 22 | import Text.Blaze.Html5 as H hiding (map) 23 | 24 | -- | Render the diff page. 25 | page :: Paste -> Paste -> Html 26 | page this that = 27 | layoutPage $ Page { 28 | pageTitle = "Diff two pastes" 29 | , pageBody = diffBody this that 30 | , pageName = "diff" 31 | } 32 | 33 | -- | View the diff between the two pastes. 34 | diffBody :: Paste -> Paste -> Html 35 | diffBody this that = do 36 | darkSection ("Diff: " ++ pid1 ++ " / " ++ pid2) $ do 37 | pasteMention this pid1 38 | pasteMention that pid2 39 | lightNoTitleSection $ do 40 | viewDiff this that 41 | 42 | where pasteMention paste pid = p $ do 43 | pasteLink paste pid 44 | ": " 45 | toHtml $ pasteTitle paste 46 | pid1 = pack (show (pasteId this)) 47 | pid2 = pack (show (pasteId that)) 48 | 49 | -- | View the diff between the two pastes. 50 | viewDiff :: Paste -> Paste -> Html 51 | viewDiff this that = do 52 | H.table ! aClass "code" $ 53 | td $ 54 | pre $ do 55 | forM_ groups $ \(indicator,lines) -> do 56 | let (ind,prefix) = 57 | case indicator of 58 | B -> ("diff-both"," ") 59 | F -> ("diff-first","- ") 60 | S -> ("diff-second","+ ") 61 | lins = map (prefix++) lines 62 | H.div ! aClass ind $ toHtml $ T.unlines $ lins 63 | 64 | where groups = getGroupedDiff lines1 lines2 65 | lines1 = T.lines (pastePaste this) 66 | lines2 = T.lines (pastePaste that) 67 | -------------------------------------------------------------------------------- /src/Main.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | Main entry point. 5 | 6 | module Main (main) where 7 | 8 | import Hpaste.Config 9 | import Hpaste.Controller.Activity as Activity 10 | import Hpaste.Controller.Browse as Browse 11 | import Hpaste.Controller.Diff as Diff 12 | import Hpaste.Controller.Home as Home 13 | import Hpaste.Controller.New as New 14 | import Hpaste.Controller.Paste as Paste 15 | import Hpaste.Controller.Raw as Raw 16 | import Hpaste.Controller.Report as Report 17 | import Hpaste.Controller.Reported as Reported 18 | import Hpaste.Controller.Rss as Rss 19 | import Hpaste.Controller.Script as Script 20 | import Hpaste.Model.Announcer (newAnnouncer) 21 | import Hpaste.Types 22 | import Hpaste.Types.Announcer 23 | 24 | import Control.Concurrent.Chan (Chan) 25 | import Data.Text.Lazy (Text) 26 | import System.Environment 27 | import Snap.App 28 | import Snap.Http.Server hiding (Config) 29 | import Snap.Util.FileServe 30 | 31 | 32 | -- | Main entry point. 33 | main :: IO () 34 | main = do 35 | cpath:_ <- getArgs 36 | config <- getConfig cpath 37 | announces <- newAnnouncer (configAnnounce config) 38 | pool <- newPool (configPostgres config) 39 | setUnicodeLocale "en_US" 40 | httpServe server (serve config pool announces) 41 | where server = setPort 10000 defaultConfig 42 | 43 | -- | Serve the controllers. 44 | serve :: Config -> Pool -> Announcer -> Snap () 45 | serve config pool ans = route routes where 46 | routes = [("/css/",serveDirectory "static/css") 47 | ,("/js/amelie.hs.js",run Script.handle) 48 | ,("/js/",serveDirectory "static/js") 49 | ,("/hs/",serveDirectory "static/hs") 50 | ,("",run (Home.handle False)) 51 | ,("/spam",run (Home.handle True)) 52 | ,("/:id",run (Paste.handle False)) 53 | ,("/raw/:id",run Raw.handle) 54 | ,("/revision/:id",run (Paste.handle True)) 55 | ,("/report/:id",run Report.handle) 56 | ,("/reported",run Reported.handle) 57 | ,("/new",run (New.handle New.NewPaste)) 58 | ,("/annotate/:id",run (New.handle New.AnnotatePaste)) 59 | ,("/edit/:id",run (New.handle New.EditPaste)) 60 | ,("/new/:channel",run (New.handle New.NewPaste)) 61 | ,("/browse",run Browse.handle) 62 | ,("/activity",run Activity.handle) 63 | ,("/diff/:this/:that",run Diff.handle) 64 | ,("/delete",run Report.handleDelete) 65 | ,("/channel/:channel/rss",run Rss.handle) 66 | ] 67 | run = runHandler ans config pool 68 | -------------------------------------------------------------------------------- /src/Hpaste/Model/Announcer.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ScopedTypeVariables #-} 2 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | 6 | -- | IRC announcer. 7 | 8 | module Hpaste.Model.Announcer 9 | (newAnnouncer 10 | ,announce 11 | ) 12 | where 13 | 14 | import Hpaste.Types.Announcer 15 | import Control.Monad.Fix 16 | import Control.Concurrent 17 | import qualified Control.Exception as E 18 | import Control.Monad 19 | import Control.Monad.Env (env) 20 | import Control.Monad.IO (io) 21 | import qualified Data.ByteString as B 22 | import Data.Monoid.Operator ((++)) 23 | import Data.Char 24 | import Data.Text (Text,pack,unpack) 25 | import qualified Data.Text as T 26 | import Data.Text.Encoding 27 | import Data.Time 28 | import qualified Data.Text.IO as T 29 | import Network 30 | import Prelude hiding ((++)) 31 | import Snap.App.Types 32 | import System.IO 33 | 34 | -- | Start a thread and return a channel to it. 35 | newAnnouncer :: AnnounceConfig -> IO Announcer 36 | newAnnouncer config = do 37 | putStrLn "Connecting..." 38 | ans <- newChan 39 | let self = Announcer { annChan = ans, annConfig = config } 40 | _ <- forkIO $ announcer self (const (return ())) 41 | return self 42 | 43 | -- | Run the announcer bot. 44 | announcer :: Announcer -> (Handle -> IO ()) -> IO () 45 | announcer self@Announcer{annConfig=config,annChan=ans} cont = do 46 | announcements <- getChanContents ans 47 | forM_ announcements $ \ann -> 48 | E.catch (sendIfNickExists config ann) 49 | (\(e::IOError) -> return ()) 50 | 51 | sendIfNickExists AnnounceConfig{..} (Announcement origin line) = do 52 | handle <- connectTo announceHost (PortNumber $ fromIntegral announcePort) 53 | hSetBuffering handle LineBuffering 54 | let send = B.hPutStrLn handle . encodeUtf8 55 | send $ "PASS " ++ pack announcePass 56 | send $ "USER " ++ pack announceUser ++ " * * *" 57 | send $ "NICK " ++ pack announceUser 58 | send $ "WHOIS :" ++ origin 59 | fix $ \loop -> do 60 | incoming <- T.hGetLine handle 61 | case T.takeWhile isDigit (T.drop 1 (T.dropWhile (/=' ') incoming)) of 62 | "311" -> send line 63 | "401" -> return () 64 | _ -> loop 65 | 66 | -- | Announce something to the IRC. 67 | announce :: Announcer -> Text -> Text -> Text -> IO () 68 | announce Announcer{annChan=chan} nick channel line = do 69 | io $ writeChan chan $ Announcement nick ("PRIVMSG " ++ channel ++ " :" ++ line) 70 | -------------------------------------------------------------------------------- /src/Hpaste/View/Report.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing -fno-warn-unused-do-bind #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Report view. 6 | 7 | module Hpaste.View.Report 8 | (page,reportFormlet) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Highlight 13 | import Hpaste.View.Html 14 | import Hpaste.View.Layout 15 | 16 | import Data.Monoid.Operator ((++)) 17 | import Data.Text (Text) 18 | import Prelude hiding ((++)) 19 | import Text.Blaze.Html5 as H hiding (map) 20 | import qualified Text.Blaze.Html5.Attributes as A 21 | import Text.Formlet 22 | 23 | -- | Render the page page. 24 | page :: Html -> Paste -> Html 25 | page form paste = 26 | layoutPage $ Page { 27 | pageTitle = "Report a paste" 28 | , pageBody = do reporting form; viewPaste paste 29 | , pageName = "paste" 30 | } 31 | 32 | reporting :: Html -> Html 33 | reporting form = do 34 | lightSection "Report a paste" $ do 35 | p $ do "Please state any comments regarding the paste:" 36 | H.form ! A.method "post" $ do 37 | form 38 | 39 | -- | View a paste's details and content. 40 | viewPaste :: Paste -> Html 41 | viewPaste Paste{..} = do 42 | pasteDetails pasteTitle 43 | pasteContent pastePaste 44 | 45 | -- | List the details of the page in a dark section. 46 | pasteDetails :: Text -> Html 47 | pasteDetails title = 48 | darkNoTitleSection $ do 49 | pasteNav 50 | h2 $ toHtml title 51 | ul ! aClass "paste-specs" $ do 52 | detail "Language" $ "Haskell" 53 | detail "Raw" $ href ("/stepeval/raw" :: Text) 54 | ("View raw link" :: Text) 55 | clear 56 | 57 | where detail title content = do 58 | li $ do strong (title ++ ":"); content 59 | 60 | -- | Individual paste navigation. 61 | pasteNav :: Html 62 | pasteNav = 63 | H.div ! aClass "paste-nav" $ do 64 | href ("https://github.com/benmachine/stepeval" :: Text) 65 | ("Go to stepeval project" :: Text) 66 | 67 | -- | Show the paste content with highlighting. 68 | pasteContent :: Text -> Html 69 | pasteContent paste = 70 | lightNoTitleSection $ 71 | highlightHaskell paste 72 | 73 | -- | A formlet for report submission / annotating. 74 | reportFormlet :: ReportFormlet -> (Formlet Text,Html) 75 | reportFormlet ReportFormlet{..} = 76 | let frm = form $ do 77 | formletHtml reportSubmit rfParams 78 | submitInput "submit" "Submit" 79 | in (reportSubmit,frm) 80 | 81 | reportSubmit :: Formlet Text 82 | reportSubmit = req (textInput "report" "Comments" Nothing) 83 | -------------------------------------------------------------------------------- /src/Hpaste/Model/Report.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | {-# LANGUAGE ScopedTypeVariables #-} 5 | {-# LANGUAGE ViewPatterns #-} 6 | 7 | -- | Report model. 8 | 9 | module Hpaste.Model.Report 10 | (getSomeReports,createReport,countReports) 11 | where 12 | 13 | import Hpaste.Types 14 | import Hpaste.Controller.Cache 15 | import Hpaste.Types.Cache as Key 16 | 17 | import Control.Monad 18 | 19 | import Control.Monad.Env 20 | import Control.Monad.IO 21 | import Data.Pagination 22 | import Data.Maybe 23 | import Data.Monoid.Operator ((++)) 24 | import qualified Data.Text as T 25 | import qualified Data.Text.Lazy as LT 26 | import Network.Mail.Mime 27 | import Prelude hiding ((++)) 28 | import Snap.App 29 | 30 | -- | Get some paginated reports. 31 | getSomeReports :: Pagination -> Model c s [Report] 32 | getSomeReports Pagination{..} = 33 | queryNoParams ["SELECT created,paste,comments" 34 | ,"FROM report" 35 | ,"ORDER BY id DESC" 36 | ,"OFFSET " ++ show (max 0 (pnCurrentPage - 1) * pnPerPage) 37 | ,"LIMIT " ++ show pnPerPage] 38 | 39 | -- | Count reports. 40 | countReports :: Model c s Integer 41 | countReports = do 42 | rows <- singleNoParams ["SELECT COUNT(*)" 43 | ,"FROM report"] 44 | return $ fromMaybe 0 rows 45 | 46 | -- | Create a new report. 47 | createReport :: ReportSubmit -> Model Config s (Maybe ReportId) 48 | createReport rs@ReportSubmit{..} = do 49 | res <- single ["INSERT INTO report" 50 | ,"(paste,comments)" 51 | ,"VALUES" 52 | ,"(?,?)" 53 | ,"returning id"] 54 | (rsPaste,rsComments) 55 | _ <- exec ["UPDATE paste" 56 | ,"SET public = false" 57 | ,"WHERE id = ?"] 58 | (Only rsPaste) 59 | let reset pid = do 60 | resetCacheModel (Key.Paste pid) 61 | resetCacheModel (Key.Revision pid) 62 | reset rsPaste 63 | sendReport rs 64 | return res 65 | 66 | sendReport :: ReportSubmit -> Model Config s () 67 | sendReport ReportSubmit{..} = do 68 | conf <- env modelStateConfig 69 | m <- io $ simpleMail (configAdmin conf) 70 | (configSiteAddy conf) 71 | (T.pack ("Paste reported: #" ++ show rsPaste)) 72 | (LT.pack body) 73 | (LT.pack body) 74 | [] 75 | io $ renderSendMail m 76 | 77 | where body = 78 | "Paste " ++ show rsPaste ++ "\n\n" ++ 79 | "http://hpaste.org/" ++ show rsPaste ++ "?show_private=true" ++ 80 | "\n\n" ++ 81 | rsComments 82 | -------------------------------------------------------------------------------- /src/Hpaste/View/Home.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Home page view. 6 | 7 | module Hpaste.View.Home 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | import Hpaste.View.Layout 14 | import Hpaste.View.Paste (pasteLink) 15 | 16 | import Control.Monad 17 | import Data.Text (Text) 18 | import Data.Time.Show (showDateTime) 19 | import Prelude hiding ((++)) 20 | import Text.Blaze.Html5 as H hiding (map) 21 | import qualified Data.Text as T 22 | import Text.Blaze.Extra 23 | import Network.URI.Params 24 | import Network.URI 25 | 26 | -- | Render the home page. 27 | page :: URI -> [Channel] -> [Language] -> [Paste] -> Html -> Bool -> Html 28 | page uri chans langs ps form spam = 29 | layoutPage $ Page { 30 | pageTitle = "Recent pastes" 31 | , pageBody = content uri chans langs ps form spam 32 | , pageName = "home" 33 | } 34 | 35 | -- | Render the home page body. 36 | content :: URI -> [Channel] -> [Language] -> [Paste] -> Html -> Bool -> Html 37 | content uri chans langs ps form spam = do 38 | when spam $ p $ strong $ do "Your submission was identified as being probably spam and was ignored. " 39 | "Try reducing links and making your paste look less spammy. " 40 | "If the problem persists, try contacting support and we will adjust the spam filters." 41 | createNew form 42 | -- latest uri chans langs ps 43 | 44 | -- | Create a new paste section. 45 | createNew :: Html -> Html 46 | createNew = lightSection "Create new paste" 47 | 48 | -- | View the latest pastes. 49 | latest :: URI -> [Channel] -> [Language] -> [Paste] -> Html 50 | latest uri channels languages ps = do 51 | darkSection "Latest pastes" $ do 52 | table ! aClass "latest-pastes" $ do 53 | tr $ mapM_ (th . toHtml) $ words "Title Author When Language Channel" 54 | pastes ps 55 | p ! aClass "browse-link" $ browse 56 | 57 | where pastes = mapM_ $ \paste@Paste{..} -> tr $ do 58 | td $ pasteLink paste pasteTitle 59 | td $ do 60 | let author = T.unpack pasteAuthor 61 | if True -- validNick author 62 | then a ! hrefURI (authorUri author) $ toHtml pasteAuthor 63 | else toHtml pasteAuthor 64 | td $ toHtml $ showDateTime $ pasteDate 65 | td $ showLanguage languages pasteLanguage 66 | td $ showChannel Nothing channels pasteChannel 67 | authorUri author = updateUrlParam "author" author 68 | $ updateUrlParam "page" "0" 69 | $ uri { uriPath = "/browse" } 70 | 71 | -- | Browse link. 72 | browse :: Html 73 | browse = href ("/browse" :: Text) ("Browse all pastes" :: Text) 74 | -------------------------------------------------------------------------------- /src/Hpaste/View/Script.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-orphans -fno-warn-name-shadowing -fno-warn-unused-do-bind #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE FlexibleInstances #-} 4 | {-# LANGUAGE TypeSynonymInstances #-} 5 | 6 | -- | Page script. 7 | 8 | module Hpaste.View.Script 9 | (script) 10 | where 11 | 12 | import Data.Text.Lazy (Text,pack) 13 | import HJScript 14 | import HJScript.Objects.JQuery hiding (prepend,append) 15 | import HJScript.Objects.JQuery.Extra 16 | import Prelude hiding ((++),max) 17 | 18 | -- | All scripts on the site. Not much to do. 19 | script :: Text 20 | script = pack $ show $ snd $ evalHJScript $ do 21 | ready $ do 22 | -- resizePage 23 | toggleHints 24 | togglePaste 25 | 26 | -- | Resize the width of the page to match content width. 27 | resizePage :: HJScript () 28 | resizePage = do 29 | max <- varWith (int 0) 30 | each (do max .=. (mathMax 500 31 | (mathMax (getWidth this' + 50) (val max))) 32 | return true) 33 | (j ".amelie-code") 34 | each (do setWidth (mathMax (val max) 500) 35 | (j ".amelie-wrap") 36 | return true) 37 | (j ".amelie-code") 38 | each (do setWidth (mathMax (getWidth this') 500) 39 | (j ".amelie-wrap") 40 | return true) 41 | (j ".amelie-latest-pastes") 42 | 43 | -- | Collapse/expand hints when toggled. 44 | toggleHints :: HJScript () 45 | toggleHints = do 46 | each (do this <- varWith this' 47 | collapse this 48 | css' "cursor" "pointer" (parent this) 49 | toggle (expand this) 50 | (collapse this) 51 | (parent this) 52 | return true) 53 | (j ".amelie-hint") 54 | 55 | where collapse o = do 56 | css "height" "1em" o 57 | css "overflow" "hidden" o 58 | return false 59 | expand o = do 60 | css "height" "auto" o 61 | return false 62 | 63 | -- | Toggle paste details. 64 | togglePaste :: HJScript () 65 | togglePaste = do 66 | each (do btn <- varWith (j "Expand") 67 | this <- varWith this' 68 | prepend (string " - ") this 69 | prepend (val btn) this 70 | details <- varWith (siblings ".amelie-paste-specs" this) 71 | display btn "none" details 72 | toggle (display btn "block" details) 73 | (display btn "none" details) 74 | btn 75 | return true) 76 | (j ".amelie-paste-nav") 77 | 78 | where display btn prop o = do 79 | css "display" prop o 80 | setText (string caption) btn 81 | return false 82 | where caption = if prop == "block" then "Collapse" else "Expand" 83 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Report.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | 6 | -- | Report controller. 7 | 8 | module Hpaste.Controller.Report 9 | (handle 10 | ,handleDelete) 11 | where 12 | 13 | import Hpaste.Controller.Cache (resetCache) 14 | import Hpaste.Controller.Admin (withAuth) 15 | import Hpaste.Model.Paste (getPasteById,deletePaste) 16 | import Hpaste.Model.Report 17 | import Hpaste.Types 18 | import Hpaste.Types.Cache as Key 19 | import Hpaste.View.Report 20 | import qualified Hpaste.View.Thanks as Thanks 21 | 22 | import Control.Applicative 23 | import Control.Monad.Reader 24 | import Data.ByteString.UTF8 (toString) 25 | import Data.String 26 | import Data.Maybe 27 | import Data.Monoid.Operator ((++)) 28 | import Data.Text (unpack) 29 | import Prelude hiding ((++)) 30 | import Safe 31 | import Snap.App 32 | import Text.Blaze.Html5 as H hiding (output,map,body) 33 | import Text.Formlet 34 | 35 | -- | Handle the report/delete page. 36 | handle :: HPCtrl () 37 | handle = do 38 | pid <- (>>= readMay) . fmap (toString) <$> getParam "id" 39 | case pid of 40 | Nothing -> goHome 41 | Just (pid :: Integer) -> do 42 | paste <- model $ getPasteById (PasteId pid) 43 | (frm,val) <- exprForm 44 | case val of 45 | Just comment -> do 46 | _ <- model $ createReport ReportSubmit { rsPaste = PasteId pid 47 | , rsComments = comment } 48 | resetCache Key.Home 49 | output $ Thanks.page "Reported" $ 50 | "Thanks, your comments have " ++ 51 | "been reported to the administrator." 52 | Nothing -> maybe goHome (output . page frm) paste 53 | 54 | -- | Report form. 55 | exprForm :: HPCtrl (Html,Maybe String) 56 | exprForm = do 57 | params <- getParams 58 | submitted <- isJust <$> getParam "submit" 59 | let formlet = ReportFormlet { 60 | rfSubmitted = submitted 61 | , rfParams = params 62 | } 63 | (getValue,_) = reportFormlet formlet 64 | value = formletValue getValue params 65 | (_,html) = reportFormlet formlet 66 | val = either (const Nothing) Just $ value 67 | return (html,fmap unpack val) 68 | 69 | handleDelete :: HPCtrl () 70 | handleDelete = 71 | withAuth $ \_ -> do 72 | pid <- (>>= readMay) . fmap (toString) <$> getParam "id" 73 | case pid of 74 | Nothing -> goReport 75 | Just (pid :: Integer) -> do 76 | model $ deletePaste pid 77 | goReport 78 | 79 | -- | Go back to the reported page. 80 | goReport :: HPCtrl () 81 | goReport = withAuth $ \key -> redirect (fromString ("/reported?key=" ++ key)) 82 | -------------------------------------------------------------------------------- /src/Hpaste/Controller/Cache.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | 3 | -- | HTML caching. 4 | 5 | module Hpaste.Controller.Cache 6 | (newCache 7 | ,cache 8 | ,cacheIf 9 | ,resetCache 10 | ,resetCacheModel) 11 | where 12 | 13 | 14 | import Hpaste.Types.Cache 15 | import Hpaste.Types.Config 16 | 17 | import Control.Concurrent 18 | import Control.Monad 19 | import Control.Monad.IO (io) 20 | import Control.Monad.Reader (asks) 21 | import qualified Data.Map as M 22 | import Data.Text.Lazy (Text) 23 | import qualified Data.Text.Lazy.IO as T 24 | import Snap.App.Types 25 | import System.Directory 26 | import Text.Blaze.Html5 (Html) 27 | import Text.Blaze.Renderer.Text (renderHtml) 28 | 29 | -- | Create a new cache. 30 | newCache :: IO Cache 31 | newCache = do 32 | var <- newMVar M.empty 33 | return $ Cache var 34 | 35 | -- | Cache conditionally. 36 | cacheIf :: Bool -> Key -> Controller Config s (Maybe Html) -> Controller Config s (Maybe Text) 37 | cacheIf pred key generate = 38 | if pred 39 | then cache key generate 40 | else fmap (fmap renderHtml) generate 41 | 42 | -- | Generate and save into the cache, or retrieve existing from the 43 | -- | cache. 44 | cache :: Key -> Controller Config s (Maybe Html) -> Controller Config s (Maybe Text) 45 | cache key generate = do 46 | tmpdir <- asks (configCacheDir . controllerStateConfig) 47 | let cachePath = tmpdir ++ "/" ++ keyToString key 48 | exists <- io $ doesFileExist cachePath 49 | if exists 50 | then do text <- io $ T.readFile cachePath 51 | return (Just text) 52 | else do text <- fmap (fmap renderHtml) generate 53 | case text of 54 | Just text' -> do io $ T.writeFile cachePath text' 55 | return text 56 | Nothing -> return text 57 | 58 | -- | Reset an item in the cache. 59 | resetCache :: Key -> Controller Config s () 60 | resetCache key = do 61 | tmpdir <- asks (configCacheDir . controllerStateConfig) 62 | io $ do 63 | let cachePath = tmpdir ++ "/" ++ keyToString key 64 | exists <- io $ doesFileExist cachePath 65 | when exists $ removeFile cachePath 66 | 67 | -- | Reset an item in the cache. 68 | resetCacheModel :: Key -> Model Config s () 69 | resetCacheModel key = do 70 | tmpdir <- asks (configCacheDir . modelStateConfig) 71 | io $ do 72 | let cachePath = tmpdir ++ "/" ++ keyToString key 73 | exists <- io $ doesFileExist cachePath 74 | when exists $ removeFile cachePath 75 | 76 | keyToString :: Key -> String 77 | keyToString Home = "home.html" 78 | keyToString Activity = "activity.html" 79 | keyToString (Paste i) = "paste-" ++ show i ++ ".html" 80 | keyToString (Revision i) = "revision-" ++ show i ++ ".html" 81 | -------------------------------------------------------------------------------- /src/Hpaste/View/Stepeval.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing -fno-warn-unused-do-bind #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Stepeval explanation view. 6 | 7 | module Hpaste.View.Stepeval 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Highlight 13 | import Hpaste.View.Hlint 14 | import Hpaste.View.Html 15 | import Hpaste.View.Layout 16 | 17 | import Data.Monoid.Operator ((++)) 18 | import Data.Text (Text) 19 | import Language.Haskell.HLint 20 | import Prelude hiding ((++)) 21 | import Text.Blaze.Html5 as H hiding (map) 22 | 23 | -- | Render the page page. 24 | page :: StepevalPage -> Html 25 | page StepevalPage{..} = 26 | layoutPage $ Page { 27 | pageTitle = "Stepeval support" 28 | , pageBody = do explanation 29 | viewPaste sePaste seHints 30 | , pageName = "paste" 31 | } 32 | 33 | explanation :: Html 34 | explanation = do 35 | lightSection "Stepeval" $ do 36 | p $ do "A program/library for evaluating " 37 | "a Haskell expression step-by-step. This web site uses it " 38 | "for stepping through provided expressions." 39 | p $ href ("https://github.com/benmachine/stepeval" :: Text) 40 | ("Repository for Stepeval" :: Text) 41 | p $ do "Stepeval comes with a simple Prelude of pure functions " 42 | "(see below) that can be used when stepping through " 43 | "expressions. This may be expanded upon in the future." 44 | p $ do "This web site will automatically include declarations " 45 | "from the paste as the expression to be evaluted." 46 | 47 | -- | View a paste's details and content. 48 | viewPaste :: Text -> [Suggestion] -> Html 49 | viewPaste paste hints = do 50 | pasteDetails "Stepeval Prelude" 51 | pasteContent paste 52 | viewSuggestions hints 53 | 54 | -- | List the details of the page in a dark section. 55 | pasteDetails :: Text -> Html 56 | pasteDetails title = 57 | darkNoTitleSection $ do 58 | pasteNav 59 | h2 $ toHtml title 60 | ul ! aClass "paste-specs" $ do 61 | detail "Language" $ "Haskell" 62 | detail "Raw" $ href ("/stepeval/raw" :: Text) 63 | ("View raw link" :: Text) 64 | clear 65 | 66 | where detail title content = do 67 | li $ do strong (title ++ ":"); content 68 | 69 | -- | Individual paste navigation. 70 | pasteNav :: Html 71 | pasteNav = 72 | H.div ! aClass "paste-nav" $ do 73 | href ("https://github.com/benmachine/stepeval" :: Text) 74 | ("Go to stepeval project" :: Text) 75 | 76 | -- | Show the paste content with highlighting. 77 | pasteContent :: Text -> Html 78 | pasteContent paste = 79 | lightNoTitleSection $ 80 | highlightHaskell paste -------------------------------------------------------------------------------- /src/Hpaste/View/Browse.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Browse page view. 6 | 7 | module Hpaste.View.Browse 8 | (page) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | import Hpaste.View.Layout 14 | import Hpaste.View.Paste (pasteLink) 15 | 16 | import Control.Monad 17 | import Data.Maybe 18 | import Data.Monoid.Operator 19 | import Data.Pagination 20 | import qualified Data.Text as T 21 | import qualified Data.Text.Lazy as LT 22 | import Data.Time 23 | import Data.Time.Relative 24 | import Network.URI 25 | import Network.URI.Params 26 | import Prelude hiding ((++)) 27 | import Snap.App.Types 28 | import System.Locale 29 | import Text.Blaze.Extra 30 | import Text.Blaze.Html5 as H hiding (map) 31 | import qualified Text.Blaze.Html5.Attributes as A 32 | import Text.Blaze.Pagination 33 | 34 | -- | Render the browse page. 35 | page :: UTCTime -> PN -> [Channel] -> [Language] -> [Paste] -> Maybe String -> Html 36 | page now pn chans langs ps mauthor = 37 | layoutPage $ Page { 38 | pageTitle = "Browse pastes" 39 | , pageBody = browse now pn chans langs ps mauthor 40 | , pageName = "browse" 41 | } 42 | 43 | -- | View the paginated pastes. 44 | browse :: UTCTime -> PN -> [Channel] -> [Language] -> [Paste] -> Maybe String -> Html 45 | browse now pn channels languages ps mauthor = do 46 | darkSection title $ do 47 | pagination pn 48 | table ! aClass "latest-pastes" $ do 49 | tr $ mapM_ (th . (toHtml :: String -> Html)) $ 50 | ["Title"] ++ ["Author"|isNothing mauthor] ++ ["When","Language","Channel"] 51 | pastes ps 52 | pagination pn { pnPn = (pnPn pn) { pnShowDesc = False } } 53 | 54 | where pastes = mapM_ $ \paste@Paste{..} -> tr $ do 55 | td $ pasteLink paste pasteTitle 56 | unless (isJust mauthor) $ 57 | td $ do 58 | let author = T.unpack pasteAuthor 59 | if True -- validNick author 60 | then a ! hrefURI (authorUri author) $ toHtml pasteAuthor 61 | else toHtml pasteAuthor 62 | td $ ago pasteDate now 63 | td $ showLanguage languages pasteLanguage 64 | td $ showChannel Nothing channels pasteChannel 65 | authorUri author = updateUrlParam "author" author 66 | $ updateUrlParam "pastes_page" "0" 67 | $ pnURI pn 68 | title = LT.pack $ case mauthor of 69 | Just author -> "Pastes by " ++ author 70 | Nothing -> "Latest pastes" 71 | 72 | epoch = formatTime defaultTimeLocale "%s" 73 | 74 | ago t1 t2 = H.span !. "relative-time" 75 | ! dataAttribute "epoch" (toValue (epoch t1)) 76 | ! A.title (toValue (show t1)) $ 77 | toHtml (relative t1 t2 True) 78 | -------------------------------------------------------------------------------- /src/Hpaste/View/Layout.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-unused-do-bind #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Page layout. 6 | 7 | module Hpaste.View.Layout 8 | (layoutPage) 9 | where 10 | 11 | import Hpaste.Types 12 | import Hpaste.View.Html 13 | 14 | import Data.Monoid.Operator ((++)) 15 | import Prelude hiding ((++)) 16 | import Text.Blaze.Html5 as H hiding (map,nav) 17 | import qualified Text.Blaze.Html5.Attributes as A 18 | 19 | -- | Render the page in a layout. 20 | layoutPage :: Page -> Markup 21 | layoutPage Page{..} = do 22 | docTypeHtml $ do 23 | H.head $ do 24 | meta ! A.httpEquiv "Content-Type" ! A.content "text/html; charset=UTF-8" 25 | link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/css/amelie.css" 26 | js "jquery.js" 27 | js "amelie.js" 28 | js "highlight.pack.js" 29 | title $ toMarkup $ pageTitle ++ " :: lpaste — Lambda pastebin" 30 | script $ 31 | "hljs.tabReplace = ' ';hljs.initHighlightingOnLoad();" 32 | body ! A.id (toValue pageName) $ do 33 | wrap $ do 34 | nav 35 | logo 36 | pageBody 37 | foot 38 | preEscapedString "" 47 | 48 | where js s = script ! A.type_ "text/javascript" 49 | ! A.src ("/js/" ++ s) $ 50 | return () 51 | 52 | -- | Show the lpaste logo. 53 | logo :: Markup 54 | logo = return () 55 | 56 | -- | Layout wrapper. 57 | wrap :: Markup -> Markup 58 | wrap x = H.div ! aClass "wrap" $ x 59 | 60 | -- | Navigation. 61 | nav :: Markup 62 | nav = do 63 | H.div ! aClass "nav" $ do 64 | a ! aClass "logo" ! A.href "/" $ "λ" 65 | a ! A.href "/browse" $ "Browse" 66 | a ! A.href "mailto:chrisdone@gmail.com" $ "Contact" 67 | -- " | " 68 | -- a ! A.href "/activity" $ "Changes" 69 | 70 | -- | Page footer. 71 | foot :: Markup 72 | foot = H.div ! aClass "footer" $ p $ 73 | lnk "http://github.com/chrisdone/lpaste" "Web site source code on Github" 74 | // 75 | lnk "http://book.realworldhaskell.org/" "Real World Haskell" 76 | // 77 | lnk "http://haskell.org/" "Haskell.org" 78 | // 79 | lnk "http://planet.haskell.org/" "Planet Haskell" 80 | 81 | where lnk url t = href (url :: String) (t :: String) 82 | left // right = do _ <- left; (" / " :: Markup); right 83 | -------------------------------------------------------------------------------- /src/Hpaste/View/Steps.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | -- | Paste steps view. 6 | 7 | module Hpaste.View.Steps 8 | (page 9 | ,exprFormlet) 10 | where 11 | 12 | import Hpaste.Types 13 | import Hpaste.View.Highlight 14 | import Hpaste.View.Hlint (viewHints) 15 | import Hpaste.View.Html 16 | import Hpaste.View.Layout 17 | import Hpaste.View.Paste (pasteLink) 18 | 19 | import Control.Monad 20 | import Data.Monoid.Operator ((++)) 21 | import Data.Text (Text) 22 | import qualified Data.Text as T 23 | import Data.Text.Lazy (fromStrict) 24 | import Prelude hiding ((++),div) 25 | import Text.Blaze.Html5 as H hiding (map) 26 | import qualified Text.Blaze.Html5.Attributes as A 27 | import Text.Formlet 28 | 29 | -- | Render the steps page. 30 | page :: StepsPage -> Html 31 | page StepsPage{spPaste=p@Paste{..},..} = 32 | layoutPage $ Page { 33 | pageTitle = pasteTitle 34 | , pageBody = viewPaste spForm p spHints spSteps 35 | , pageName = "steps" 36 | } 37 | 38 | -- | View a paste's details and content. 39 | viewPaste :: Html -> Paste -> [Hint] -> [Text] -> Html 40 | viewPaste form paste@Paste{..} hints steps = do 41 | case pasteParent of 42 | Nothing -> return () 43 | Just{} -> let an = "a" ++ show (fromIntegral pasteId :: Integer) 44 | in a ! A.name (toValue an) $ return () 45 | pasteDetails paste 46 | pasteContent paste 47 | stepsForm form 48 | viewSteps steps 49 | viewHints hints 50 | 51 | stepsForm :: Html -> Html 52 | stepsForm form = 53 | lightNoTitleSection $ 54 | div ! aClass "steps-expr" $ 55 | form 56 | 57 | -- | A formlet for expr submission / annotating. 58 | exprFormlet :: ExprFormlet -> (Formlet Text,Html) 59 | exprFormlet ExprFormlet{..} = 60 | let frm = form $ do 61 | formletHtml exprSubmit efParams 62 | submitInput "submit" "Submit" 63 | in (exprSubmit,frm) 64 | 65 | exprSubmit :: Formlet Text 66 | exprSubmit = req (textInput "expr" "Expression" Nothing) 67 | 68 | viewSteps :: [Text] -> Html 69 | viewSteps steps = 70 | lightSection "Steps (displaying 50 max.)" $ 71 | div ! aClass "steps" $ do 72 | highlightHaskell $ T.intercalate "\n\n" steps 73 | 74 | -- | List the details of the page in a dark section. 75 | pasteDetails :: Paste -> Html 76 | pasteDetails paste@Paste{..} = 77 | darkNoTitleSection $ do 78 | pasteNav 79 | h2 $ toHtml $ fromStrict pasteTitle 80 | ul ! aClass "paste-specs" $ do 81 | detail "Paste" $ pasteLink paste $ "#" ++ show pasteId 82 | detail "Author" $ pasteAuthor 83 | clear 84 | 85 | where detail title content = do 86 | li $ do strong (title ++ ":"); toHtml content 87 | 88 | -- | Individual paste navigation. 89 | pasteNav :: Html 90 | pasteNav = 91 | H.div ! aClass "paste-nav" $ do 92 | href ("/stepeval" :: Text) 93 | ("About evaluation step support" :: Text) 94 | 95 | -- | Show the paste content with highlighting. 96 | pasteContent :: Paste -> Html 97 | pasteContent paste = 98 | lightNoTitleSection $ highlightHaskell (pastePaste paste) 99 | -------------------------------------------------------------------------------- /src/Hpaste/View/Highlight.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS -Wall -fno-warn-name-shadowing #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE ViewPatterns #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE NamedFieldPuns #-} 6 | 7 | -- | Code highlighting. 8 | 9 | module Hpaste.View.Highlight 10 | (highlightPaste 11 | ,highlightHaskell) 12 | where 13 | 14 | import Hpaste.Types 15 | import Hpaste.View.Html 16 | 17 | import Data.Monoid 18 | import Control.Monad 19 | import Data.List (find) 20 | import Data.Monoid.Operator 21 | import Data.Text (Text,unpack,pack) 22 | import qualified Data.Text as T 23 | import Language.Haskell.HsColour.CSS (hscolour) 24 | import Prelude hiding ((++)) 25 | import Text.Blaze.Html5 as H hiding (map) 26 | import qualified Text.Blaze.Html5.Attributes as A 27 | 28 | -- | Syntax highlight the paste. 29 | highlightPaste :: [Language] -> Paste -> Html 30 | highlightPaste langs Paste{..} = 31 | H.table ! aClass "code" $ do 32 | td ! aClass "line-nums" $ do 33 | pre $ 34 | forM_ [1..length (T.lines pastePaste)] $ \i -> do 35 | let name = "line" ++ pack (show i) 36 | href ("#" ++ name) (toHtml i) ! A.id (toValue name) ! A.name (toValue name) 37 | "\n" 38 | td $ 39 | case lang of 40 | Just (Language{languageName}) 41 | | languageName == "literatehaskell" -> 42 | birdStyle pastePaste 43 | | elem languageName ["haskell","agda","idris","elm"] -> 44 | preEscapedString $ hscolour False (unpack pastePaste) 45 | Just (Language{..}) -> 46 | pre $ code ! A.class_ (toValue $ "language-" ++ lang) $ 47 | toHtml pastePaste 48 | where lang | languageName == "elisp" = "lisp" 49 | | otherwise = languageName 50 | _ -> 51 | pre $ toHtml pastePaste 52 | 53 | where lang = find ((==pasteLanguage) . Just . languageId) langs 54 | 55 | highlightHaskell :: Text -> Html 56 | highlightHaskell paste = 57 | H.table ! aClass "code" $ 58 | td $ preEscapedString $ hscolour False (unpack paste) 59 | 60 | birdStyle :: Text -> Html 61 | birdStyle = collect mempty (Right []) . map T.unpack . T.lines where 62 | collect doc acc (('>':(dropSpace -> hsline)):xs) = 63 | case acc of 64 | Right hslines -> collect doc (Right (hslines ++ hsline ++ "\n")) xs 65 | Left text -> collect (doc <> plaintext text) (Right (hsline ++ "\n")) xs 66 | collect doc acc (textline:xs) = 67 | case acc of 68 | Right hslines -> collect (doc <> highlight hslines) (Left (textline ++ "\n")) xs 69 | Left text -> collect doc (Left (text ++ textline ++ "\n")) xs 70 | collect doc acc [] = 71 | case acc of 72 | Right hslines -> doc <> highlight hslines 73 | Left text -> doc <> plaintext text 74 | highlight = preEscapedString . beaks . hscolour False 75 | plaintext = pre . toHtml 76 | dropSpace (' ':xs) = xs 77 | dropSpace xs = xs 78 | beaks x = "
" ++ unlines (map beakize (lines x)) ++ "" where 79 | beakize ('<':'p':'r':'e':'>':code) = "
> " ++ code
80 | beakize "" = ""
81 | beakize code = "> " ++ code
82 |
--------------------------------------------------------------------------------
/src/Hpaste/Model/Spam.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE RecordWildCards #-}
2 | {-# LANGUAGE OverloadedStrings #-}
3 | {-# LANGUAGE ScopedTypeVariables #-}
4 |
5 | -- | Spam detection.
6 |
7 | module Hpaste.Model.Spam where
8 |
9 | import Hpaste.Types
10 | import Data.Monoid
11 | import Data.Text (Text)
12 | import Control.Monad.IO
13 | import Control.Monad.Env
14 | import Control.Monad
15 | import qualified Data.Text.Lazy as LT
16 | import qualified Data.Text as T
17 | import System.Process hiding (env)
18 | import Snap.App
19 | import Network.Mail.Mime
20 |
21 | -- | Get a spam rating for the given potential paste.
22 | spamRating :: PasteSubmit -> Model Config s Integer
23 | spamRating ps = do
24 | score <- if definitelySpam ps
25 | then return 100
26 | else fmap (weighted ps) (io (getRating mail))
27 | when (score > spamMaxLevel) $ reportBadScore ps score
28 | return score
29 |
30 | where mail = unlines ["from: noreply@hpaste.org"
31 | ,"subject: " ++ T.unpack (pasteSubmitTitle ps)
32 | ,""
33 | ,T.unpack (pasteSubmitPaste ps)]
34 |
35 | reportBadScore PasteSubmit{..} score = do
36 | conf <- env modelStateConfig
37 | m <- io $ simpleMail (configAdmin conf)
38 | (configSiteAddy conf)
39 | ("Paste marked as spam: " <> pasteSubmitTitle)
40 | body
41 | body
42 | []
43 | io $ renderSendMail m
44 |
45 | where body = LT.pack $
46 | "Paste '" ++ T.unpack pasteSubmitTitle ++ "' by " ++ T.unpack pasteSubmitAuthor ++ " " ++
47 | "has rating " ++ show score ++ " with content: " ++
48 | T.unpack pasteSubmitPaste
49 |
50 | -- | Get the rating from spam assassin.
51 | getRating :: String -> IO Integer
52 | getRating mail = do
53 | (_,err,_) <- readProcessWithExitCode "spamc" ["-c"] mail
54 | return $ case reads err of
55 | [(n :: Double,_)] -> round (n*10)
56 | _ -> 50
57 |
58 | -- | Mark something as definitely spam.
59 | definitelySpam :: PasteSubmit -> Bool
60 | definitelySpam ps =
61 | T.isInfixOf "http://" (pasteSubmitTitle ps) ||
62 | T.isInfixOf "http://" (pasteSubmitAuthor ps) ||
63 | T.isInfixOf "stooorage" (allText ps) ||
64 | T.isInfixOf "http://fur.ly" (allText ps) ||
65 | T.isInfixOf "anekahosting.com" (allText ps) ||
66 | justUrl
67 | where justUrl =
68 | (T.isPrefixOf "http://" paste ||
69 | T.isPrefixOf "https://" paste) &&
70 | lineCount == 1
71 | lineCount = length (filter (not . T.null)
72 | (map T.strip
73 | (T.lines paste)))
74 | paste = T.strip (pasteSubmitPaste ps)
75 |
76 | -- | Multiple the rating by weights specific to hpaste.
77 | weighted :: PasteSubmit -> Integer -> Integer
78 | weighted ps n = foldr ($) n weights where
79 | weights = [if T.isInfixOf "http://" text || T.isInfixOf "https://" text
80 | then (+ (20 * fromIntegral (T.count "http://" text + T.count "https://" text))) else id
81 | ,if pasteSubmitAuthor ps == "Anonymous Coward" || pasteSubmitAuthor ps == "Anonymous"
82 | then (+20) else id
83 | ]
84 | text = allText ps
85 |
86 | -- | Get the text of the paste.
87 | allText :: PasteSubmit -> Text
88 | allText PasteSubmit{..} = T.toLower $ pasteSubmitTitle <> " " <> pasteSubmitPaste
89 |
90 | -- | Maximum level, anything equal or above this is treated as definitely spam, ignored.
91 | spamMaxLevel = 100
92 |
93 | -- | Minimum level, anything equal or above this is treated as possibly spam, accepted but not listed.
94 | spamMinLevel = 60
95 |
--------------------------------------------------------------------------------
/src/HJScript/Objects/JQuery/Extra.hs:
--------------------------------------------------------------------------------
1 | {-# OPTIONS -Wall -fno-warn-orphans -fno-warn-name-shadowing -fno-warn-unused-do-bind #-}
2 | {-# LANGUAGE OverloadedStrings #-}
3 | {-# LANGUAGE FlexibleInstances #-}
4 | {-# LANGUAGE TypeSynonymInstances #-}
5 |
6 | module HJScript.Objects.JQuery.Extra where
7 |
8 | import HJScript
9 | import HJScript.Objects.JQuery
10 | import qualified HJScript.Objects.Math as Math
11 | import Prelude hiding ((++),max)
12 |
13 | -- | jQuery selector.
14 | j :: String -> JObject JQuery
15 | j = selectExpr . string
16 |
17 | -- | Set the width of a DOM element.
18 | setWidth :: IsJQuery o => Exp Int -> o -> HJScript ()
19 | setWidth w o = do
20 | runExp $ methodCall "width" w o
21 |
22 | -- | Set the text of a DOM element.
23 | setText :: IsJQuery o => Exp String -> o -> HJScript ()
24 | setText w o = do
25 | runExp $ methodCall "text" w o
26 |
27 | -- | Append an element to another.
28 | append :: IsJQuery o => Exp a -> o -> HJScript ()
29 | append w o = do
30 | runExp $ methodCall "append" w o
31 |
32 | -- | Prepend an element before another.
33 | prepend :: IsJQuery o => Exp a -> o -> HJScript ()
34 | prepend w o = do
35 | runExp $ methodCall "prepend" w o
36 |
37 | -- | Add a class to an object.
38 | addClass :: IsJQuery o => String -> o -> HJScript ()
39 | addClass w o = do
40 | runExp $ methodCall "addClass" (string w) o
41 |
42 | -- | Does an object have a class?
43 | hasClass :: IsJQuery o => String -> o -> JBool
44 | hasClass w o = do
45 | methodCall "hasClass" (string w) o
46 |
47 | -- | Remove a class from an object.
48 | removeClass :: IsJQuery o => String -> o -> HJScript ()
49 | removeClass w o = do
50 | runExp $ methodCall "removeClass" (string w) o
51 |
52 | -- | Set the width of a DOM element.
53 | css' :: IsJQuery o => String -> String -> o -> HJScript ()
54 | css' key value o = do
55 | runExp $ methodCall "css" (string key,string value) o
56 |
57 | -- | Set the width of a DOM element.
58 | css :: IsJQuery o => String -> String -> o -> HJScript ()
59 | css key value o = do
60 | runExp $ methodCall "css" (string key,string value) o
61 |
62 | -- | Get the width of a DOM element.
63 | getWidth :: IsJQuery o => o -> Exp Int
64 | getWidth o = do
65 | methodCall "width" () o
66 |
67 | -- | Get siblings of an elements.
68 | siblings :: IsJQuery o => String -> o -> JObject JQuery
69 | siblings q o = do
70 | methodCall "siblings" (string q) o
71 |
72 | -- | When toggling by clicking, run these events on this object.
73 | toggle :: IsJQuery o => HJScript JBool -> HJScript JBool -> o -> HJScript ()
74 | toggle on off query = do
75 | fnon <- function $ \() -> on
76 | fnoff <- function $ \() -> off
77 | runExp $ methodCall "toggle" (fnon,fnoff) query
78 |
79 | -- | When toggling by hover, run these events on this object.
80 | hover :: IsJQuery o => HJScript JBool -> HJScript JBool -> o -> HJScript ()
81 | hover on off query = do
82 | fnon <- function $ \() -> on
83 | fnoff <- function $ \() -> off
84 | runExp $ methodCall "hover" (fnon,fnoff) query
85 |
86 | -- | For each object in a jQuery selection.
87 | each :: IsJQuery o => HJScript JBool -> o -> HJScript ()
88 | each script query = do
89 | fn <- function $ \() -> script
90 | runExp $ methodCall "each" fn query
91 |
92 | -- | The jQuery 'this' object.
93 | this' :: JObject JQuery
94 | this' = selectExpr (this :: JObject JQuery)
95 |
96 | -- | Parent of a jQuery object.
97 | parent :: IsJQuery o => o -> JObject JQuery
98 | parent o = callMethod "parent" () o
99 |
100 | -- | Max.
101 | mathMax :: Exp a -> Exp a -> Exp a
102 | mathMax a b = callMethod "max" (a,b) Math.Math
103 |
104 | -- | Simple instance so we can use number literals and simple
105 | -- | arithmetic.
106 | instance Num (Exp Int) where
107 | a + b = a .+. b
108 | a * b = a .*. b
109 | abs = undefined
110 | signum = undefined
111 | fromInteger = int . fromIntegral
112 | instance Eq (Exp Int)
113 |
114 | class (IsDeref o) => IsJQuery o
115 | instance IsJQuery (JObject JQuery)
116 | instance IsClass (Var JQuery)
117 | instance IsJQuery (Var JQuery)
118 |
--------------------------------------------------------------------------------
/hpaste.cabal:
--------------------------------------------------------------------------------
1 | Name: hpaste
2 | Version: 1.2.0
3 | stability: Stable
4 | Synopsis: Haskell paste web site.
5 | Description: Haskell paste web site. Includes: syntax highlighting for
6 | various languages, HLint suggestions for Haskell, annotations,
7 | revisions, diffs between revisions, announcement to IRC channels,
8 | browsing of author's pastes, spam reporting/deletion,
9 | SpamAssassin-based spam protection, secret/private pastes.
10 | Homepage: http://hpaste.org/
11 | License: GPL
12 | Author: Chris Done block contents.
501 | var pre = block.parentNode;
502 | var container = document.createElement('div');
503 | container.innerHTML = '' + result.value + '
';
504 | block = container.firstChild.firstChild;
505 | container.firstChild.className = pre.className;
506 | pre.parentNode.replaceChild(container.firstChild, pre);
507 | } else {
508 | block.innerHTML = result.value;
509 | }
510 | block.className = class_name;
511 | block.result = {
512 | language: language,
513 | kw: result.keyword_count,
514 | re: result.relevance
515 | };
516 | if (result.second_best) {
517 | block.second_best = {
518 | language: result.second_best.language,
519 | kw: result.second_best.keyword_count,
520 | re: result.second_best.relevance
521 | };
522 | }
523 | }
524 |
525 | /*
526 | Applies highlighting to all ..
blocks on a page.
527 | */
528 | function initHighlighting() {
529 | if (initHighlighting.called)
530 | return;
531 | initHighlighting.called = true;
532 | var pres = document.getElementsByTagName('pre');
533 | for (var i = 0; i < pres.length; i++) {
534 | var code = findCode(pres[i]);
535 | if (code)
536 | highlightBlock(code, hljs.tabReplace);
537 | }
538 | }
539 |
540 | /*
541 | Attaches highlighting to the page load event.
542 | */
543 | function initHighlightingOnLoad() {
544 | if (window.addEventListener) {
545 | window.addEventListener('DOMContentLoaded', initHighlighting, false);
546 | window.addEventListener('load', initHighlighting, false);
547 | } else if (window.attachEvent)
548 | window.attachEvent('onload', initHighlighting);
549 | else
550 | window.onload = initHighlighting;
551 | }
552 |
553 | var languages = {}; // a shortcut to avoid writing "this." everywhere
554 |
555 | /* Interface definition */
556 |
557 | this.LANGUAGES = languages;
558 | this.highlight = highlight;
559 | this.highlightAuto = highlightAuto;
560 | this.fixMarkup = fixMarkup;
561 | this.highlightBlock = highlightBlock;
562 | this.initHighlighting = initHighlighting;
563 | this.initHighlightingOnLoad = initHighlightingOnLoad;
564 |
565 | // Common regexps
566 | this.IDENT_RE = '[a-zA-Z][a-zA-Z0-9_]*';
567 | this.UNDERSCORE_IDENT_RE = '[a-zA-Z_][a-zA-Z0-9_]*';
568 | this.NUMBER_RE = '\\b\\d+(\\.\\d+)?';
569 | this.C_NUMBER_RE = '\\b(0x[A-Za-z0-9]+|\\d+(\\.\\d+)?)';
570 | this.RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|\\.|-|-=|/|/=|:|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~';
571 |
572 | // Common modes
573 | this.BACKSLASH_ESCAPE = {
574 | begin: '\\\\.', relevance: 0
575 | };
576 | this.APOS_STRING_MODE = {
577 | className: 'string',
578 | begin: '\'', end: '\'',
579 | illegal: '\\n',
580 | contains: [this.BACKSLASH_ESCAPE],
581 | relevance: 0
582 | };
583 | this.QUOTE_STRING_MODE = {
584 | className: 'string',
585 | begin: '"', end: '"',
586 | illegal: '\\n',
587 | contains: [this.BACKSLASH_ESCAPE],
588 | relevance: 0
589 | };
590 | this.C_LINE_COMMENT_MODE = {
591 | className: 'comment',
592 | begin: '//', end: '$'
593 | };
594 | this.C_BLOCK_COMMENT_MODE = {
595 | className: 'comment',
596 | begin: '/\\*', end: '\\*/'
597 | };
598 | this.HASH_COMMENT_MODE = {
599 | className: 'comment',
600 | begin: '#', end: '$'
601 | };
602 | this.NUMBER_MODE = {
603 | className: 'number',
604 | begin: this.NUMBER_RE,
605 | relevance: 0
606 | };
607 | this.C_NUMBER_MODE = {
608 | className: 'number',
609 | begin: this.C_NUMBER_RE,
610 | relevance: 0
611 | };
612 |
613 | // Utility functions
614 | this.inherit = function(parent, obj) {
615 | var result = {}
616 | for (var key in parent)
617 | result[key] = parent[key];
618 | if (obj)
619 | for (var key in obj)
620 | result[key] = obj[key];
621 | return result;
622 | }
623 | }();
624 |
625 | /*
626 | Language: Haskell
627 | Author: Jeremy Hull
628 | */
629 |
630 | hljs.LANGUAGES.haskell = function(){
631 | var LABEL = {
632 | className: 'label',
633 | begin: '\\b[A-Z][\\w\']*',
634 | relevance: 0
635 | };
636 | var CONTAINER = {
637 | className: 'container',
638 | begin: '\\(', end: '\\)',
639 | contains: [
640 | {className: 'label', begin: '\\b[A-Z][\\w\\(\\)\\.\']*'},
641 | {className: 'title', begin: '[_a-z][\\w\']*'}
642 | ]
643 | };
644 |
645 | return {
646 | defaultMode: {
647 | keywords: {
648 | 'keyword': {
649 | 'let': 1, 'in': 1, 'if': 1, 'then': 1, 'else': 1, 'case': 1, 'of': 1,
650 | 'where': 1, 'do': 1, 'module': 1, 'import': 1, 'hiding': 1,
651 | 'qualified': 1, 'type': 1, 'data': 1, 'newtype': 1, 'deriving': 1,
652 | 'class': 1, 'instance': 1, 'null': 1, 'not': 1, 'as': 1
653 | }
654 | },
655 | contains: [
656 | {
657 | className: 'comment',
658 | begin: '--', end: '$'
659 | },
660 | {
661 | className: 'comment',
662 | begin: '{-', end: '-}'
663 | },
664 | {
665 | className: 'string',
666 | begin: '\\s+\'', end: '\'',
667 | contains: [hljs.BACKSLASH_ESCAPE],
668 | relevance: 0
669 | },
670 | hljs.QUOTE_STRING_MODE,
671 | {
672 | className: 'import',
673 | begin: '\\bimport', end: '$',
674 | keywords: {'import': 1, 'qualified': 1, 'as': 1, 'hiding': 1},
675 | contains: [CONTAINER]
676 | },
677 | {
678 | className: 'module',
679 | begin: '\\bmodule', end: 'where',
680 | keywords: {'module': 1, 'where': 1},
681 | contains: [CONTAINER]
682 | },
683 | {
684 | className: 'class',
685 | begin: '\\b(class|instance|data|(new)?type)', end: '(where|$)',
686 | keywords: {'class': 1, 'where': 1, 'instance': 1,'data': 1,'type': 1,'newtype': 1, 'deriving': 1},
687 | contains: [LABEL]
688 | },
689 | hljs.C_NUMBER_MODE,
690 | {
691 | className: 'shebang',
692 | begin: '#!\\/usr\\/bin\\/env\ runhaskell', end: '$'
693 | },
694 | LABEL,
695 | {
696 | className: 'title', begin: '^[_a-z][\\w\']*'
697 | }
698 | ]
699 | }
700 | };
701 | }();
702 |
--------------------------------------------------------------------------------