File: UploadPaste.hs

package info (click to toggle)
haskell-wreq 0.5.4.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 380 kB
  • sloc: haskell: 2,992; makefile: 25
file content (169 lines) | stat: -rw-r--r-- 6,839 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
-- upload a paste to lpaste.net
--
-- This example is pretty beefy, as it does double duty.
--
-- Perhaps the majority of it shows off some complex uses of the
-- optparse-applicative package.
--
-- The POST portion is in the function named upload below.  It uploads
-- an application/x-www-urlencoded form that creates a paste on the
-- Haskell community pastebin at <http://lpaste.net/>.

{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
{-# OPTIONS_GHC -Wall #-}


import Control.Applicative
import Control.Lens
import Data.Char (toLower)
import Data.Maybe (listToMaybe)
import Data.Monoid (mempty, (<>))
import Network.Wreq (FormParam((:=)), post, responseBody)
import Network.Wreq.Types (FormValue(..))
import Options.Applicative as Opts
import Options.Applicative.Types (readerAsk)
import System.FilePath (takeExtension, takeFileName)
import Text.HTML.TagSoup
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as L

-- A post to lpaste.net can either be private or public (visible in an
-- index).
data Visibility = Private | Public
                deriving (Show)

-- Wreq supports uploading an application/x-www-urlencoded form (see
-- uses of the := operator in the upload function below), so we tell
-- it how to render a value of a custom datatype.
instance FormValue Visibility where
  renderFormValue = renderFormValue . show

-- The languages that lpaste.net supports. It just so happens that if
-- we convert one of these constructor names to a lower-case string,
-- it exactly matches what lpaste.net needs in its upload form.
data Language =
  Haskell | Agda | Assembly | Bash | C | Coq | Cpp | Cs | Diff | Elm | ELisp |
  Erlang | Go | Idris | Java | JavaScript | LiterateHaskell | Lisp |
  Lua | OCaml | ObjectiveC | Perl | Prolog | Python | Ruby | SQL | Scala |
  Scheme | Smalltalk | TeX
  deriving (Eq, Show)

instance FormValue Language where
  renderFormValue = renderFormValue . fmap toLower . show

-- An association between filename suffixes and our Language type.
languages :: [([String], Language)]
languages = [
  ([".hs"], Haskell), ([".agda"], Agda), ([".el"], ELisp), ([".ocaml"], OCaml),
  ([".cl"], Lisp), ([".erl"], Erlang), ([".lhs"], LiterateHaskell),
  ([".scala"], Scala), ([".go"], Go), ([".py"], Python), ([".rb"], Ruby),
  ([".elm"], Elm), ([".idris"], Idris), ([".prl"], Prolog), ([".scm"], Scheme),
  ([".coq"], Coq), ([".s", ".asm"], Assembly), ([".sh"], Bash),
  ([".c", ".h"], C), ([".cs"], Cs), ([".tex"], TeX), ([".lua"], Lua),
  ([".cxx", ".cpp", ".cc", ".hxx", ".hpp", ".hh"], Cpp), ([".pl", ".pm"], Perl),
  ([".diff", ".patch"], Diff), ([".java"], Java), ([".js"], JavaScript),
  ([".m"], ObjectiveC), ([".smalltalk"], Smalltalk), ([".sql"], SQL)
  ]

-- An IRC channel to which an announcement of a paste can be posted.
-- We wrap this in a newtype so we can control how it is rendered in a
-- form.
newtype Channel = Channel { fromChannel :: String }
                deriving (Eq, Show)

-- If a user forgot to supply a leading '#' for a channel name, we add
-- it here.
instance FormValue Channel where
  renderFormValue = renderFormValue . checkHash . fromChannel
    where checkHash cs@('#':_) = cs
          checkHash cs@(_:_)   = '#' : cs
          checkHash cs         = cs

-- This type plays two roles.  It describes the command line options
-- we accept, and also the contents of the form we'll upload to create
-- a new paste.
--
-- We've parameterised the type so that the payload field can either
-- be a filename or the actual contents of the file.
data Paste a = Paste {
    _private :: Visibility
  , _title :: Maybe String
  , _author :: Maybe String
  , _channel :: Maybe Channel
  , _language :: Maybe Language
  , _payload :: a
  , _email :: () -- used by lpaste.net for spam protection
  } deriving (Show)

makeLenses ''Paste

-- Try to match a user-supplied name to a Language type, looking at
-- both full names and filename extensions.
readLanguage :: ReadM Language
readLanguage = do
  l <- readerAsk
  let ll = toLower <$> l
      ms = [lang | (suffixes, lang) <- languages,
            ll == (toLower <$> show lang) || ll `elem` (tail <$> suffixes)]
  case ms of
    [m] -> return m
    _   -> fail $ "unsupported language " ++ show l

-- Figure out the language to specify for a file, either explicitly as
-- specified by the user, or implicitly from the filename extension.
guessLanguage :: FilePath -> Paste a -> Maybe Language
guessLanguage filename p =
    (p ^. language) <|>
    listToMaybe [lang | (suffixes, lang) <- languages, sfx `elem` suffixes]
  where sfx = toLower <$> takeExtension filename

upload :: Paste FilePath -> IO ()
upload p0 = do
  let path = p0 ^. payload
  body <- B.readFile path
  -- Transform command line options into form contents.
  let p = p0 & payload .~ body
             & title .~ (p0 ^. title <|> Just (takeFileName path))
             & language .~ guessLanguage path p0
  -- The := operator defines a key/value pair for a form.
  resp <- post "http://lpaste.net/new" [
            "private" := p ^. private
          , "title" := p ^. title
          , "author" := p ^. author
          , "channel" := p ^. channel
          , "language" := p ^. language
          , "paste" := p ^. payload
          , "email" := p ^. email
          ]
  -- Since lpaste.net doesn't provide an API and just spits HTML back
  -- at us, we use tagsoup to look through the tags for the permalink
  -- of the paste we just uploaded.
  let findURI (TagOpen "strong" [] : TagText "Paste:" : TagClose "strong" :
               TagOpen "a" [("href",uri)] : _) = Just uri
      findURI (_:xs) = findURI xs
      findURI _      = Nothing
  case findURI (parseTagsOptions parseOptionsFast (resp ^. responseBody)) of
    Just uri -> L.putStrLn $ "http://lpaste.net" <> uri
    Nothing  -> putStrLn "no uri in response!?"

main :: IO ()
main = upload =<< execParser opts
  where opts = info (helper <*> optionParser) mempty
        optionParser = Paste <$>
          (flag Private Public $ long "public" <>
                                 help "display in index of pastes") <*>
          (optional . strOption $
           long "title" <> short 't' <> metavar "TITLE" <>
           help "title to use for paste") <*>
          (optional . strOption $
           long "author" <> short 'a' <> metavar "AUTHOR" <>
           help "author to display for paste") <*>
          (optional . fmap Channel . strOption $
           long "channel" <> short 'c' <> metavar "CHANNEL" <>
           help "name of IRC channel to announce") <*>
          (optional . option readLanguage $
           long "language" <> short 'l' <> metavar "LANG" <>
           help "language to use") <*>
          (Opts.argument str $ metavar "PATH" <>
                               help "file to upload") <*>
          (pure ())