diff options
author | Jacques Comeaux <jacquesrcomeaux@gmail.com> | 2022-05-27 21:07:43 -0500 |
---|---|---|
committer | Jacques Comeaux <jacquesrcomeaux@gmail.com> | 2022-05-27 21:07:43 -0500 |
commit | 3d3163a1028e99e5073fd39adc9aee5d76a5e5a2 (patch) | |
tree | d1cff9ed00418183d3ed8de412d0fdccd7666eaf |
Initial Commit
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | Setup.hs | 2 | ||||
-rw-r--r-- | app/Main.hs | 73 | ||||
-rw-r--r-- | package.yaml | 84 | ||||
-rw-r--r-- | src/Data/Message.hs | 78 | ||||
-rw-r--r-- | src/Data/Tomato.hs | 70 | ||||
-rw-r--r-- | src/Tomato/.Post.hs.swp | bin | 0 -> 12288 bytes | |||
-rw-r--r-- | src/Tomato/Post.hs | 65 | ||||
-rw-r--r-- | src/Tomato/Validate.hs | 16 | ||||
-rw-r--r-- | stack.yaml | 67 | ||||
-rw-r--r-- | stack.yaml.lock | 13 | ||||
-rw-r--r-- | tomatobot.cabal | 163 |
13 files changed, 634 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c368d45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.stack-work/ +*~
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..16c61ea --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# tomatobot diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/app/Main.hs b/app/Main.hs new file mode 100644 index 0000000..bdbef74 --- /dev/null +++ b/app/Main.hs @@ -0,0 +1,73 @@ +module Main + ( main + ) where + +import RIO + +import Network.HTTP.Req + ( jsonResponse + , req + , (=:) + , NoReqBody (..) + , (/:) + , GET (..) + , https + , responseBody + , Req + , defaultHttpConfig + , runReq + ) + +import Data.Tomato (Tomato) +import Tomato.Post (postTomato) + +import qualified Data.Aeson.Types as Ae + + +data App = App + { appLogFunc :: !LogFunc + , appName :: !Utf8Builder + , appToken :: !Text + } + +instance HasLogFunc App where + logFuncL = lens appLogFunc (\x y -> x { appLogFunc = y }) + +runApp :: RIO App a -> IO a +runApp inner = do + logOptions' <- logOptionsHandle stderr False + -- let logOptions = setLogUseTime True $ setLogUseLoc True logOptions' + let logOptions = logOptions' + withLogFunc logOptions $ \logFunc -> do + let app = App + { appLogFunc = logFunc + , appName = "Tomato Bot" + , appToken = "placeholder" + } + runRIO app inner + +randomTomato :: Req Tomato +randomTomato = do + let url = https "api.unsplash.com" /: "photos" /: "random" + js <- req GET url NoReqBody jsonResponse $ + "query" =: ("tomato" :: Text) <> + "client_id" =: ("FbzqI-oR7277JwL1ZGsyUw7yG1F5U0U3WhQ3kOW71Do" :: Text) + tomato <- case Ae.fromJSON (responseBody js) of + Ae.Success r -> pure r + Ae.Error _s -> error "deal with this later" + return tomato + +tomatoBot :: RIO App () +tomatoBot = do + name <- view $ to appName + logInfo $ "Hello, " <> name + logInfo $ "Fetching tomato" + -- tomato <- runReq defaultHttpConfig randomTomato + -- logInfo $ displayShow tomato + -- url <- runReq defaultHttpConfig postTomato + runReq defaultHttpConfig postTomato + -- logInfo $ displayShow url + logInfo $ "All done" + +main :: IO () +main = runApp tomatoBot diff --git a/package.yaml b/package.yaml new file mode 100644 index 0000000..efb09cf --- /dev/null +++ b/package.yaml @@ -0,0 +1,84 @@ +name: tomatobot +version: 0.1.0.0 +github: "githubuser/tomatobot" +license: BSD3 +author: "Author name here" +maintainer: "example@example.com" +copyright: "2022 Author name here" + +extra-source-files: +- README.md +- ChangeLog.md + +# Metadata used when publishing your package +# synopsis: Short description of your package +# category: Web + +# To avoid duplicated efforts in documentation and dealing with the +# complications of embedding Haddock markup inside cabal files, it is +# common to point users to the README.md file. +description: Please see the README on GitHub at <https://github.com/githubuser/tomatobot#readme> + +dependencies: +- aeson +- base >= 4.7 && < 5 +- modern-uri +- req +- rio + +library: + source-dirs: src + +executables: + tomatobot-exe: + main: Main.hs + source-dirs: app + ghc-options: + - -threaded + - -rtsopts + - -with-rtsopts=-N + dependencies: + - tomatobot + +ghc-options: +- -Wall +- -Wcompat +- -Wincomplete-record-updates +- -Wredundant-constraints +- -Wmissing-local-signatures +- -Wmissing-export-lists +- -Wpartial-fields +- -Wmonomorphism-restriction +- -Widentities +- -Wno-unticked-promoted-constructors +- -fprint-expanded-synonyms + +default-extensions: +- ApplicativeDo +- BangPatterns +- ConstraintKinds +- DataKinds +- EmptyCase +- ExistentialQuantification +- FlexibleContexts +- FlexibleInstances +- GADTs +- GeneralizedNewtypeDeriving +- InstanceSigs +- KindSignatures +- LambdaCase +- MultiParamTypeClasses +- NoImplicitPrelude +- NoStarIsType +- OverloadedStrings +- PatternSynonyms +- PolyKinds +- RankNTypes +- ScopedTypeVariables +- StandaloneDeriving +- StandaloneKindSignatures +- TupleSections +- TypeApplications +- TypeFamilies +- TypeFamilyDependencies +- TypeOperators diff --git a/src/Data/Message.hs b/src/Data/Message.hs new file mode 100644 index 0000000..dfbc20b --- /dev/null +++ b/src/Data/Message.hs @@ -0,0 +1,78 @@ +module Data.Message + ( InMessage (..) + , GMIUrl (..) + , OutMessage (..) + ) where + +import RIO + +import Data.Aeson ((.:), (.=)) +import Network.HTTP.Req + ( Url + , Scheme (Https) + , useHttpsURI + , renderUrl + ) + +import qualified Data.Aeson as Ae +import qualified Data.Aeson.Types as Ae +import qualified RIO.Vector as V +import qualified Text.URI as URI + +data InMessage = InMessage + { hasAttach :: !Bool + , groupId :: !Word + , isUser :: !Bool + , text :: !Text + } deriving Show + +instance Ae.FromJSON InMessage where + parseJSON = Ae.withObject "InMessage" $ \o -> InMessage + <$> (o .: "attachments" >>= Ae.withArray "attachments" (pure . not . null)) + <*> o .: "group-id" + <*> ((==) ("user" :: Text) <$> o .: "sender_type") + <*> o .: "text" + + +newtype GMIUrl = GMIUrl + { unGMIUrl :: Url Https + } deriving Show + +instance Ae.FromJSON GMIUrl where + parseJSON = Ae.withObject "payload" $ \o -> do + p <- o .: "payload" + u <- p .: "url" + GMIUrl <$> toUrl u + where + toUrl :: Text -> Ae.Parser (Url Https) + toUrl t = maybe mzero pure $ fmap fst $ useHttpsURI =<< URI.mkURI t + +data OutMessage = OutMessage + { image :: !GMIUrl + } deriving Show + +instance Ae.ToJSON OutMessage where + toJSON om = Ae.object + [ "attachments" .= + ( Ae.Array $ V.singleton attach ) + -- , "bot_id" .= botId + ] + where + attach :: Ae.Value + attach = Ae.object + [ "type" .= ("image" :: Text) + , "url" .= url + ] + url :: Text + url = renderUrl $ unGMIUrl $ image om + + -- { + -- "bot_id" : "j5abcdefg", + -- "text" : "Hello world", + -- "attachments" : [ + -- { + -- "type" : "image", + -- "url" : "https://i.groupme.com/somethingsomething.large" + -- } + -- ] +-- } diff --git a/src/Data/Tomato.hs b/src/Data/Tomato.hs new file mode 100644 index 0000000..fc9b8b5 --- /dev/null +++ b/src/Data/Tomato.hs @@ -0,0 +1,70 @@ +module Data.Tomato + ( Links (..) + , Tomato (..) + , Urls (..) + ) where + +import RIO + +import Data.Aeson ((.:)) +import Network.HTTP.Req (Url, Scheme (Https), useHttpsURI) + +import qualified Data.Aeson as Ae +import qualified Data.Aeson.Types as Ae +import qualified Text.URI as URI + + +data Tomato = Tomato + { id :: !Text + , width :: !Word + , height :: !Word + , color :: !Text + , blur_hash :: !Text + , description :: !Text + , urls :: !Urls + , links :: !Links + } deriving Show + +instance Ae.FromJSON Tomato where + parseJSON = Ae.withObject "Tomato" $ \o -> Tomato + <$> o .: "id" + <*> o .: "width" + <*> o .: "height" + <*> o .: "color" + <*> o .: "blur_hash" + <*> o .: "description" + <*> o .: "urls" + <*> o .: "links" + +data Links = Links + { self :: !(Url Https) + , html :: !(Url Https) + , download :: !(Url Https) + , download_location :: !(Url Https) + } deriving Show + +instance Ae.FromJSON Links where + parseJSON = Ae.withObject "Links" $ \o -> Links + <$> (o .: "self" >>= toUrl) + <*> (o .: "html" >>= toUrl) + <*> (o .: "download" >>= toUrl) + <*> (o .: "download_location" >>= toUrl) + +data Urls = Urls + { raw :: !(Url Https) + , full :: !(Url Https) + , regular :: !(Url Https) + , small :: !(Url Https) + , thumb :: !(Url Https) + } deriving Show + +instance Ae.FromJSON Urls where + parseJSON = Ae.withObject "Urls" $ \o -> Urls + <$> (o .: "raw" >>= toUrl) + <*> (o .: "full" >>= toUrl) + <*> (o .: "regular" >>= toUrl) + <*> (o .: "small" >>= toUrl) + <*> (o .: "thumb" >>= toUrl) + +toUrl :: Text -> Ae.Parser (Url Https) +toUrl t = maybe mzero pure $ fmap fst $ useHttpsURI =<< URI.mkURI t diff --git a/src/Tomato/.Post.hs.swp b/src/Tomato/.Post.hs.swp Binary files differnew file mode 100644 index 0000000..f823f90 --- /dev/null +++ b/src/Tomato/.Post.hs.swp diff --git a/src/Tomato/Post.hs b/src/Tomato/Post.hs new file mode 100644 index 0000000..1e97782 --- /dev/null +++ b/src/Tomato/Post.hs @@ -0,0 +1,65 @@ +module Tomato.Post + ( postTomato + , uploadTomato + , postMessage + ) where + +import RIO + +import Network.HTTP.Req + ( jsonResponse + , ignoreResponse + , req + , ReqBodyFile (..) + , ReqBodyJson (..) + , (/:) + , POST (..) + , Req + , https + , header + , responseBody + ) + +import Data.Message (GMIUrl (..), OutMessage (..)) + +import qualified Data.Aeson as Ae +import qualified Data.Aeson.KeyMap as Ae + +-- TODO global config reader monad +tomatoFile :: FilePath +tomatoFile = "tomato.png" + +accessToken :: ByteString +accessToken = "nj8X8tB3TC0MoZdMLhIUA4G89r9IlQfVej97Mhg3" + +botId :: Text +botId = "713bcb4a4604006c944804552c" + +-- | Post the current tomato +postTomato :: Req () +postTomato = do + url <- uploadTomato + postMessage $ OutMessage url + +-- | Upload the current tomato to the GroupMe image server +uploadTomato :: Req GMIUrl +uploadTomato = do + let url = https "image.groupme.com" /: "pictures" + js <- req POST url (ReqBodyFile tomatoFile) jsonResponse $ + header "X-Access-Token" accessToken <> + header "Content-Type" "image/png" + gmURL <- case Ae.fromJSON (responseBody js) of + Ae.Success r -> pure r + Ae.Error _s -> error "deal with this later" -- TODO error monad + return gmURL + +-- | Post a message to the group with image as attachment +postMessage :: OutMessage -> Req () +postMessage outMes = do + let url = https "api.groupme.com" /: "v3" /: "bots" /: "post" + outMes' = case Ae.toJSON outMes of + Ae.Object o -> Ae.Object $ Ae.insert "bot_id" (Ae.toJSON botId) o + _ -> error "exceptional" + req POST url (ReqBodyJson outMes') ignoreResponse $ + header "X-Access-Token" accessToken + return () diff --git a/src/Tomato/Validate.hs b/src/Tomato/Validate.hs new file mode 100644 index 0000000..a3dd328 --- /dev/null +++ b/src/Tomato/Validate.hs @@ -0,0 +1,16 @@ +module Tomato.Validate + ( isTomato + ) where + +import RIO + +import Data.Message (InMessage (..)) + + +isTomato :: InMessage -> Bool +isTomato m = + not (hasAttach m) && + groupId m == 87220147 && + isUser m && + text m == "tomato" + diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..0214217 --- /dev/null +++ b/stack.yaml @@ -0,0 +1,67 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# +# The location of a snapshot can be provided as a file or url. Stack assumes +# a snapshot provided as a file might change, whereas a url resource does not. +# +# resolver: ./custom-snapshot.yaml +# resolver: https://example.com/snapshots/2018-01-01.yaml +resolver: + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/8.yaml + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# subdirs: +# - auto-update +# - wai +packages: +- . +# Dependency packages to be pulled from upstream that are not in the resolver. +# These entries can reference officially published versions as well as +# forks / in-progress versions pinned to a git hash. For example: +# +# extra-deps: +# - acme-missiles-0.3 +# - git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# +# extra-deps: [] + +# Override default flag values for local packages and extra-deps +# flags: {} + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=2.7" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor diff --git a/stack.yaml.lock b/stack.yaml.lock new file mode 100644 index 0000000..ecc702e --- /dev/null +++ b/stack.yaml.lock @@ -0,0 +1,13 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/lock_files + +packages: [] +snapshots: +- completed: + size: 618506 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/8.yaml + sha256: f1c4aca9b9b81afbb9db55571acb0690cdc01ac97a178234de281f9dc075e95e + original: + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/8.yaml diff --git a/tomatobot.cabal b/tomatobot.cabal new file mode 100644 index 0000000..95e382a --- /dev/null +++ b/tomatobot.cabal @@ -0,0 +1,163 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.34.4. +-- +-- see: https://github.com/sol/hpack + +name: tomatobot +version: 0.1.0.0 +description: Please see the README on GitHub at <https://github.com/githubuser/tomatobot#readme> +homepage: https://github.com/githubuser/tomatobot#readme +bug-reports: https://github.com/githubuser/tomatobot/issues +author: Author name here +maintainer: example@example.com +copyright: 2022 Author name here +license: BSD3 +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + ChangeLog.md + +source-repository head + type: git + location: https://github.com/githubuser/tomatobot + +library + exposed-modules: + Data.Message + Data.Tomato + Tomato.Post + Tomato.Validate + other-modules: + Paths_tomatobot + hs-source-dirs: + src + default-extensions: + ApplicativeDo + BangPatterns + ConstraintKinds + DataKinds + EmptyCase + ExistentialQuantification + FlexibleContexts + FlexibleInstances + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + NoImplicitPrelude + NoStarIsType + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + ScopedTypeVariables + StandaloneDeriving + StandaloneKindSignatures + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wredundant-constraints -Wmissing-local-signatures -Wmissing-export-lists -Wpartial-fields -Wmonomorphism-restriction -Widentities -Wno-unticked-promoted-constructors -fprint-expanded-synonyms + build-depends: + aeson + , base >=4.7 && <5 + , modern-uri + , req + , rio + default-language: Haskell2010 + +executable tomatobot-exe + main-is: Main.hs + other-modules: + Paths_tomatobot + hs-source-dirs: + app + default-extensions: + ApplicativeDo + BangPatterns + ConstraintKinds + DataKinds + EmptyCase + ExistentialQuantification + FlexibleContexts + FlexibleInstances + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + NoImplicitPrelude + NoStarIsType + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + ScopedTypeVariables + StandaloneDeriving + StandaloneKindSignatures + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wredundant-constraints -Wmissing-local-signatures -Wmissing-export-lists -Wpartial-fields -Wmonomorphism-restriction -Widentities -Wno-unticked-promoted-constructors -fprint-expanded-synonyms -threaded -rtsopts -with-rtsopts=-N + build-depends: + aeson + , base >=4.7 && <5 + , modern-uri + , req + , rio + , tomatobot + default-language: Haskell2010 + +test-suite tomatobot-test + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: + Paths_tomatobot + hs-source-dirs: + test + default-extensions: + ApplicativeDo + BangPatterns + ConstraintKinds + DataKinds + EmptyCase + ExistentialQuantification + FlexibleContexts + FlexibleInstances + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + NoImplicitPrelude + NoStarIsType + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + ScopedTypeVariables + StandaloneDeriving + StandaloneKindSignatures + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wredundant-constraints -Wmissing-local-signatures -Wmissing-export-lists -Wpartial-fields -Wmonomorphism-restriction -Widentities -Wno-unticked-promoted-constructors -fprint-expanded-synonyms -threaded -rtsopts -with-rtsopts=-N + build-depends: + aeson + , base >=4.7 && <5 + , modern-uri + , req + , rio + , tomatobot + default-language: Haskell2010 |