{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-| Module : Network.WebexTeams Copyright : (c) Naoto Shimazaki 2017,2018 License : MIT (see the file LICENSE) Maintainer : https://github.com/nshimaza Stability : experimental This module provides types and functions for accessing Cisco Webex Teams REST API. The module is designed to improve type safety over the API. Each entity is separately typed. JSON messages contained in REST responses are decoded into appropriate type of Haskell record. JSON messages sent in REST requests are encoded only from correct type of record. Some Webex Teams REST API return list of objects. Those APIs require HTTP Link Header based pagination. Haskell functions for those APIs automatically request subsequent pages as needed. = Examples @ -- Sending a message to a room. let auth = Authorization "your authorization token" roomId = RoomId "Room ID your message to be sent" messageText = MessageText "your message" message = CreateMessage (Just roomId) Nothing Nothing (Just messageText) Nothing Nothing createEntity auth def createMessage >>= print . getResponseBody -- Obtaining detail of a user. let personId = PersonId "your person ID" getDetail auth def personId >>= print . getResponseBody -- Obtaining membership of a room as stream of object representing each membership relation. let filter = MembershipFilter yourRoomId Nothing Nothing runConduit $ streamListWithFilter auth def filter .| takeC 200 .| mapM_C print -- Create a room. let createRoom = CreateRoom "Title of the new room" Nothing createEntity auth def createRoom >>= print . getResponseBody -- Delete a room. deleteRoom auth def roomId >>= print . getResponseBody @ = List and steaming The 'WebexTeams' module doesn't provide streaming API for REST response returning list of entities. It is because the author of the package wants to keep it streaming library agnostic. Instead, it provides 'ListReader' IO action to read list responses with automatic pagenation. Streaming APIs can be found in separate packages like webex-teams-pipes or webex-teams-conduit. = Support for Lens This package provides many of records representing objects communicated via Webex Teams REST API. Those records are designed to allow create lenses by Control.Lens.TH.makeFields. Following example creates overloaded accessors for 'Person', 'Room' and 'Team'. @ makeFields ''Person makeFields ''Room makeFields ''Team @ You can access 'personId', 'roomId' and 'teamId' via overloaded accessor function 'id' like this. @ let yourPersonId = yourPerson ^. id yourRoomId = yourRoom ^. id yourTeamId = yourTeam ^. id @ This package does not provide pre-generated lenses for you because not everyone need it but you can make it by yourself so easily as described. -} module Network.WebexTeams ( -- * Types -- ** Class and Type Families WebexTeamsFilter , WebexTeamsListItem , ToResponse -- ** Common Types , Authorization (..) , CiscoSparkRequest (..) , WebexTeamsRequest (..) , Timestamp (..) , ErrorCode (..) , ErrorTitle (..) , Errors (..) -- ** People related types , Person (..) , PersonId (..) , Email (..) , DisplayName (..) , NickName (..) , FirstName (..) , LastName (..) , AvatarUrl (..) , Timezone (..) , PersonStatus (..) , PersonType (..) , PersonList (..) , PersonFilter (..) , CreatePerson (..) , UpdatePerson (..) -- ** Room related types , Room (..) , RoomId (..) , RoomTitle (..) , RoomType (..) , SipAddr (..) , RoomList (..) , RoomFilter (..) , RoomFilterSortBy (..) , CreateRoom (..) , UpdateRoom (..) -- ** Membership related types , Membership (..) , MembershipId (..) , MembershipList (..) , MembershipFilter (..) , CreateMembership (..) , UpdateMembership (..) -- ** Message related types , Message (..) , MessageId (..) , MessageText (..) , MessageHtml (..) , MessageMarkdown (..) , FileUrl (..) , MessageList (..) , MessageFilter (..) , MentionedPeople (..) , CreateMessage (..) -- ** Team related types , TeamName (..) , TeamId (..) , Team (..) , TeamList (..) , CreateTeam (..) , UpdateTeam (..) -- ** Team Membership related types , TeamMembership (..) , TeamMembershipId (..) , TeamMembershipList (..) , TeamMembershipFilter (..) , CreateTeamMembership (..) , UpdateTeamMembership (..) -- ** Organization related types , Organization (..) , OrganizationId (..) , OrganizationDisplayName (..) , OrganizationList (..) -- ** License related types , License (..) , LicenseId (..) , LicenseName (..) , LicenseUnit (..) , LicenseList (..) , LicenseFilter (..) -- ** Role related types , Role (..) , RoleId (..) , RoleName (..) , RoleList (..) -- * Functions -- ** Getting detail of an entity , getDetail , getDetailEither -- ** Streaming response of List API with auto pagenation , ListReader , getListWithFilter , getTeamList , getOrganizationList , getRoleList , streamEntityWithFilter , streamTeamList , streamOrganizationList , streamRoleList -- ** Creating an entity , createEntity , createEntityEither -- ** Updating an entity , updateEntity , updateEntityEither -- ** Creating default filter spec from mandatory field , defaultMessageFilter , defaultTeamMembershipFilter -- ** Deleting an entity , deleteRoom , deleteMembership , deleteMessage , deleteTeam , deleteTeamMembership ) where import Conduit (ConduitT, yieldMany) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Aeson (FromJSON, ToJSON) import Data.ByteString (ByteString) import Data.ByteString.Char8 as C8 (unpack) import Data.Default (Default (def)) import Data.IORef (IORef, newIORef, readIORef, writeIORef) import Data.Maybe (catMaybes, maybeToList) import Data.Monoid ((<>)) import Data.Text (Text) import Data.Text.Encoding (encodeUtf8) import Network.HTTP.Simple import Network.URI (URIAuth (..)) import Network.WebexTeams.Internal import Network.WebexTeams.Types -- | Authorization string against Webex Teams API to be contained in HTTP Authorization header of every request. newtype Authorization = Authorization ByteString deriving (Eq, Show) -- | Wrapping 'Request' in order to provide easy default value specifically for Webex Teams public API. data WebexTeamsRequest = WebexTeamsRequest { webexTeamsRequestRequest :: Request -- ^ Holds pre-set 'Request' for REST API. , webexTeamsRequestScheme :: String -- ^ Should be "https:" in production. , webexTeamsRequestAuthority :: URIAuth -- ^ Authority part of request URI. } deriving (Show) -- | Type synonym for backward compatibility. type CiscoSparkRequest = WebexTeamsRequest -- | Common part of 'Request' against Webex Teams API. webexTeamsBaseRequest :: Request webexTeamsBaseRequest = addRequestHeader "Content-Type" "application/json; charset=utf-8" $ setRequestPort 443 $ setRequestHost "api.ciscospark.com" $ setRequestSecure True $ defaultRequest -- | Default parameters for HTTP request to Webex Teams REST API. instance Default WebexTeamsRequest where def = WebexTeamsRequest webexTeamsBaseRequest "https:" $ URIAuth "" "api.ciscospark.com" "" -- | Add given Authorization into request header. addAuthorizationHeader :: Authorization -> Request -> Request addAuthorizationHeader (Authorization auth) = addRequestHeader "Authorization" ("Bearer " <> auth) -- | Building common part of 'Request' for List APIs. makeCommonListReq :: WebexTeamsRequest -- ^ Common request components -> ByteString -- ^ API category part of REST URL path -> WebexTeamsRequest makeCommonListReq base@WebexTeamsRequest { webexTeamsRequestRequest = req } path = base { webexTeamsRequestRequest = setRequestPath ("/v1/" <> path) $ setRequestMethod "GET" req } {-| Common worker function for List APIs. It accesses List API with given 'Request', unwrap result into list of items, stream them to Conduit pipe and finally it automatically accesses next page designated via HTTP Link header if available. -} streamList :: (MonadIO m, WebexTeamsListItem i) => Authorization -> WebexTeamsRequest -> ConduitT () i m () streamList auth (WebexTeamsRequest req scheme uriAuth) = do res <- httpJSON $ addAuthorizationHeader auth req yieldMany . unwrap $ getResponseBody res streamListLoop auth res scheme uriAuth -- | Processing pagination by HTTP Link header. streamListLoop :: (MonadIO m, FromJSON a, WebexTeamsListItem i) => Authorization -> Response a -> String -> URIAuth -> ConduitT () i m () streamListLoop auth res scheme uriAuth = case getNextUrl res >>= validateUrl scheme uriAuth >>= (\url -> parseRequest $ "GET " <> C8.unpack url) of Nothing -> pure () Just nextReq -> do nextRes <- httpJSON $ addAuthorizationHeader auth nextReq yieldMany . unwrap $ getResponseBody nextRes streamListLoop auth nextRes scheme uriAuth -- | Get list of entities with query parameter and stream it into Conduit pipe. It automatically performs pagination. {-# DEPRECATED streamEntityWithFilter "Use getListWithFilter or streamListWithFilter of webex-teams-conduit" #-} streamEntityWithFilter :: (MonadIO m, WebexTeamsFilter filter, WebexTeamsListItem (ToResponse filter)) => Authorization -> WebexTeamsRequest -> filter -> ConduitT () (ToResponse filter) m () streamEntityWithFilter auth base param = streamList auth $ setQeuryString $ makeCommonListReq base (apiPath param) where setQeuryString comm@WebexTeamsRequest { webexTeamsRequestRequest = req } = comm { webexTeamsRequestRequest = setRequestQueryString (toFilterList param) req } -- | List of 'Team' and stream it into Conduit pipe. It automatically performs pagination. {-# DEPRECATED streamTeamList "Use getTeamList or streamTeamList of webex-teams-conduit" #-} streamTeamList :: MonadIO m => Authorization -> WebexTeamsRequest -> ConduitT () Team m () streamTeamList auth base = streamList auth $ makeCommonListReq base teamsPath -- | Filter list of 'Organization' and stream it into Conduit pipe. It automatically performs pagination. {-# DEPRECATED streamOrganizationList "Use getOrganizationList or streamOrganizationList of webex-teams-conduit" #-} streamOrganizationList :: MonadIO m => Authorization -> WebexTeamsRequest -> ConduitT () Organization m () streamOrganizationList auth base = streamList auth $ makeCommonListReq base organizationsPath -- | List of 'Role' and stream it into Conduit pipe. It automatically performs pagination. {-# DEPRECATED streamRoleList "Use getRoleList or streamRoleList of webex-teams-conduit" #-} streamRoleList :: MonadIO m => Authorization -> WebexTeamsRequest -> ConduitT () Role m () streamRoleList auth base = streamList auth $ makeCommonListReq base rolesPath {-| 'ListReader' is IO action returned by functions for list API ('getListWithFilter', 'getTeamList' etc). It is containing URL inside to be accessed. When you call the IO action, it accesses to Webex Teams REST API, parse next page URL if available, then return new IO action. The new IO action contains list of responded items and new URL for next page so you can call the new IO action to get the next page. Following example demonstrates how you can get all items into single list. @ readAllList :: ListReader i -> IO [i] readAllList reader = go [] where go xs = reader >>= \chunk -> case chunk of [] -> pure xs ys -> go (xs <> ys) @ Note that this example is only for explaining how 'ListReader' works. Practically you should not do the above because it eagerly creates entire list. You should use streaming APIs instead. Streaming APIs are available via webex-teams-conduit and webex-teams-pipes package. -} type ListReader a = IO [a] {-| Returns common worker function 'ListReader' for List APIs. ListReader accesses List API with given 'Request' then return responded list of items. ListReader also keeps next URL if response is pagenated and next page is available. Next call of ListReader causes another List API access for the next page. ListReader returns [] when there is no more page. -} getList :: (MonadIO m, WebexTeamsListItem i) => Authorization -> WebexTeamsRequest -> m (ListReader i) getList auth wxReq = liftIO $ listReader <$> newIORef (Just wxReq) where listReader :: WebexTeamsListItem i => IORef (Maybe WebexTeamsRequest) -> ListReader i listReader wxReqRef = do maybeReq <- readIORef wxReqRef case maybeReq of Nothing -> pure [] Just (WebexTeamsRequest req scheme uriAuth) -> do res <- httpJSON $ addAuthorizationHeader auth req writeIORef wxReqRef $ do maybeUrl <- getNextUrl res maybeValidUrl <-validateUrl scheme uriAuth maybeUrl maybeNextReq <- parseRequest $ "GET " <> C8.unpack maybeValidUrl pure (WebexTeamsRequest maybeNextReq scheme uriAuth) rr <- readIORef wxReqRef pure . unwrap $ getResponseBody res -- | Get list with query parameter. getListWithFilter :: (MonadIO m, WebexTeamsFilter filter, WebexTeamsListItem (ToResponse filter)) => Authorization -> WebexTeamsRequest -> filter -> m (ListReader (ToResponse filter)) getListWithFilter auth base param = getList auth $ setQeuryString $ makeCommonListReq base (apiPath param) where setQeuryString comm@WebexTeamsRequest { webexTeamsRequestRequest = req } = comm { webexTeamsRequestRequest = setRequestQueryString (toFilterList param) req } -- | Return 'ListReader' for 'Team'. getTeamList :: MonadIO m => Authorization -> WebexTeamsRequest -> m (ListReader Team) getTeamList auth base = getList auth $ makeCommonListReq base teamsPath -- | Return 'ListReader' for 'Team'. getOrganizationList :: MonadIO m => Authorization -> WebexTeamsRequest -> m (ListReader Organization) getOrganizationList auth base = getList auth $ makeCommonListReq base organizationsPath -- | Return 'ListReader' for 'Team'. getRoleList :: MonadIO m => Authorization -> WebexTeamsRequest -> m (ListReader Role) getRoleList auth base = getList auth $ makeCommonListReq base rolesPath makeCommonDetailReq :: WebexTeamsRequest -- ^ Common request components. -> Authorization -- ^ Authorization string against Webex Teams API. -> ByteString -- ^ API category part of REST URL path. -> Text -- ^ Identifier string part of REST URL path. -> Request makeCommonDetailReq (WebexTeamsRequest base _ _) auth path idStr = setRequestPath ("/v1/" <> path <> "/" <> encodeUtf8 idStr) $ setRequestMethod "GET" $ addAuthorizationHeader auth $ base {-| Get details of a Webex Teams entity. Obtaining detail of an entity identified by key. The key can be a value in one of following types: 'PersonId', 'RoomId', 'MembershipId', 'MessageId', 'TeamId', 'TeamMembershipId', 'OrganizationId', 'LicenseId', 'RoleId'. API is automatically selected by type of the key. A JSONException runtime exception will be thrown on an JSON parse errors. -} getDetail :: (MonadIO m, WebexTeamsDetail key) => Authorization -- ^ Authorization string against Webex Teams API. -> WebexTeamsRequest -- ^ Predefined part of 'Request' commonly used for Webex Teams API. -> key -- ^ One of PersonId, RoomId, MembershipId, MessageId, TeamId, TeamMembershipId, -- OrganizationId, LicenseId and RoleId. -> m (Response (ToResponse key)) getDetail auth base entityId = httpJSON $ makeCommonDetailReq base auth (apiPath entityId) (toIdStr entityId) -- | Get details of a Webex Teams entity. A Left value will be returned on an JSON parse errors. getDetailEither :: (MonadIO m, WebexTeamsDetail key) => Authorization -> WebexTeamsRequest -> key -> m (Response (Either JSONException (ToResponse key))) getDetailEither auth base entityId = httpJSONEither $ makeCommonDetailReq base auth (apiPath entityId) (toIdStr entityId) makeCommonCreateReq :: ToJSON a => WebexTeamsRequest -> Authorization -> ByteString -> a -> Request makeCommonCreateReq (WebexTeamsRequest base _ _) auth path body = setRequestBodyJSON body $ setRequestPath ("/v1/" <> path) $ setRequestMethod "POST" $ addAuthorizationHeader auth $ base {-| Create a Webex Teams entity with given parameters. Creating a new entity of Webex Teams such as space, team, membership or message. REST API path is automatically selected by type of createParams. A JSONException runtime exception will be thrown on an JSON parse errors. -} createEntity :: (MonadIO m, WebexTeamsCreate createParams) => Authorization -- ^ Authorization string against Webex Teams API. -> WebexTeamsRequest -- ^ Predefined part of 'Request' commonly used for Webex Teams API. -> createParams -- ^ One of 'CreatePerson', 'CreateRoom', 'CreateMembership', 'CreateMessage', -- 'CreateTeam' and 'CreateTeamMembership'. -> m (Response (ToResponse createParams)) createEntity auth base param = httpJSON $ makeCommonCreateReq base auth (apiPath param) param -- | Create a Webex Teams entity with given parameters. A Left value will be returned on an JSON parse errors. createEntityEither :: (MonadIO m, WebexTeamsCreate createParams) => Authorization -> WebexTeamsRequest -> createParams -> m (Response (Either JSONException (ToResponse createParams))) createEntityEither auth base param = httpJSONEither $ makeCommonCreateReq base auth (apiPath param) param makeCommonUpdateReq :: ToJSON a => WebexTeamsRequest -> Authorization -> ByteString -> a -> Request makeCommonUpdateReq (WebexTeamsRequest base _ _) auth path body = setRequestBodyJSON body $ setRequestPath ("/v1/" <> path) $ setRequestMethod "PUT" $ addAuthorizationHeader auth $ base {-| Update a Webex Teams entity with given parameters. Creating a new entity of Webex Teams such as space, team, or membership. REST API path is automatically selected by type of updateParams. A JSONException runtime exception will be thrown on an JSON parse errors. -} updateEntity :: (MonadIO m, WebexTeamsUpdate updateParams) => Authorization -- ^ Authorization string against Webex Teams API. -> WebexTeamsRequest -- ^ Predefined part of 'Request' commonly used for Webex Teams API. -> updateParams -- ^ One of 'UpdatePerson', 'UpdateRoom', 'UpdateMembership', -- 'UpdateTeam' and 'UpdateTeamMembership'. -> m (Response (ToResponse updateParams)) updateEntity auth base param = httpJSON $ makeCommonUpdateReq base auth (apiPath param) param -- | Update a Webex Teams entity with given parameters. A Left value will be returned on an JSON parse errors. updateEntityEither :: (MonadIO m, WebexTeamsUpdate updateParams) => Authorization -> WebexTeamsRequest -> updateParams -> m (Response (Either JSONException (ToResponse updateParams))) updateEntityEither auth base param = httpJSONEither $ makeCommonUpdateReq base auth (apiPath param) param makeCommonDeleteReq :: Authorization -- ^ Authorization string against Webex Teams API. -> Request -- ^ Common request components. -> ByteString -- ^ API category part of REST URL path. -> Text -- ^ Identifier string part of REST URL path. -> Request makeCommonDeleteReq auth base path idStr = setRequestPath ("/v1/" <> path <> "/" <> encodeUtf8 idStr) $ setRequestMethod "DELETE" $ addAuthorizationHeader auth $ base -- | Polymorphic version of delete. Intentionally not exposed to outside of the module. deleteEntity :: (MonadIO m, WebexTeamsDetail key) => Authorization -- ^ Authorization string against Webex Teams API. -> WebexTeamsRequest -- ^ Predefined part of 'Request' commonly used for Webex Teams API. -> key -- ^ One of PersonId, RoomId, MembershipId, MessageId, TeamId, TeamMembershipId. -> m (Response ()) deleteEntity auth (WebexTeamsRequest base _ _) entityId = httpNoBody $ makeCommonDeleteReq auth base (apiPath entityId) (toIdStr entityId) -- | Deletes a room, by ID. deleteRoom :: MonadIO m => Authorization -- ^ Authorization string against Webex Teams API. -> WebexTeamsRequest -- ^ Predefined part of 'Request' commonly used for Webex Teams API. -> RoomId -- ^ Identifier of a space to be deleted. -> m (Response ()) deleteRoom = deleteEntity -- | Deletes a membership, by ID. deleteMembership :: MonadIO m => Authorization -- ^ Authorization string against Webex Teams API. -> WebexTeamsRequest -- ^ Predefined part of 'Request' commonly used for Webex Teams API. -> MembershipId -- ^ Identifier of a space to be deleted. -> m (Response ()) deleteMembership = deleteEntity -- | Deletes a message, by ID. deleteMessage :: MonadIO m => Authorization -- ^ Authorization string against Webex Teams API. -> WebexTeamsRequest -- ^ Predefined part of 'Request' commonly used for Webex Teams API. -> MessageId -- ^ Identifier of a space to be deleted. -> m (Response ()) deleteMessage = deleteEntity -- | Deletes a team, by ID. deleteTeam :: MonadIO m => Authorization -- ^ Authorization string against Webex Teams API. -> WebexTeamsRequest -- ^ Predefined part of 'Request' commonly used for Webex Teams API. -> TeamId -- ^ Identifier of a space to be deleted. -> m (Response ()) deleteTeam = deleteEntity -- | Deletes a teamMembership, by ID. deleteTeamMembership :: MonadIO m => Authorization -- ^ Authorization string against Webex Teams API. -> WebexTeamsRequest -- ^ Predefined part of 'Request' commonly used for Webex Teams API. -> TeamMembershipId -- ^ Identifier of a space to be deleted. -> m (Response ()) deleteTeamMembership = deleteEntity