packages feed

twitchapi-0.0.1: src/Web/TwitchAPI/PubSub.hs

{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}

{- |
Module      :  TwitchAPI.PubSub
Copyright   :  (c) Christina Wuest 2021
License     :  BSD-style

Maintainer  :  tina@wuest.me
Stability   :  experimental
Portability :  non-portable

Messages sent over Twitch's PubSub interface.
-}

module Web.TwitchAPI.PubSub where

import Prelude

import qualified Data.Time             as Time
import qualified Data.Time.RFC3339     as Time ( parseTimeRFC3339 )
import qualified Data.Time.Clock.POSIX as Time ( posixSecondsToUTCTime )

import Data.Aeson ( FromJSON(..), (.:), (.:?), withObject, withText, withEmbeddedJSON
                  , ToJSON(..), (.=), object
                  , Object
                  )

import qualified Data.Aeson as JSON

import Control.Monad ( mzero )
import GHC.Generics  ( Generic )

import qualified Data.Aeson.Types as JSON.Types

data Topic = BitsV1 { channelId :: Integer }
           | BitsV2 { channelId :: Integer }
           | BitsBadge { channelId :: Integer }
           | ChannelPoints { channelId :: Integer }
           | ChannelSubscriptions { channelId :: Integer }
           | ChatModeratorActions { channelId :: Integer, userId :: Integer }
           | Whispers { userId :: Integer }
           deriving ( Eq, Show )

toRequest :: Topic -> String
toRequest BitsV1{..} = ("channel-bits-events-v1." ++) $ show channelId
toRequest BitsV2{..} = ("channel-bits-events-v2." ++) $ show channelId
toRequest BitsBadge{..} = ("channel-bits-badge-unlocks." ++) $ show channelId
toRequest ChannelPoints{..} = ("channel-points-channel-v1." ++) $ show channelId
toRequest ChannelSubscriptions{..} = ("channel-subscribe-events-v1." ++) $ show channelId
toRequest ChatModeratorActions{..} = "chat_moderator_actions." ++ (show userId) ++ "." ++ (show channelId)
toRequest Whispers{..} = ("whispers." ++) $ show userId

scope :: Topic -> String
scope BitsV1{} = "bits:read"
scope BitsV2{} = "bits:read"
scope BitsBadge{} = "bits:read"
scope ChannelPoints{} = "channel:read:redemptions"
scope ChannelSubscriptions{} = "channel:read:subscriptions"
scope ChatModeratorActions{} = "channel:moderate"
scope Whispers{} = "whispers:read"

data RequestType = Listen | Unlisten deriving ( Eq )

instance Show RequestType where
    show Listen   = "LISTEN"
    show Unlisten = "UNLISTEN"

data Request = Request { requestType :: RequestType
                       , nonce :: Maybe String
                       , topics :: [Topic]
                       , authToken :: String
                       } deriving ( Eq, Show )

instance ToJSON Request where
    toJSON Request{..} =
        object [ "type" .= (show requestType)
               , "nonce" .= nonce
               , "data" .= object [ "topics" .= (toRequest <$> topics)
                                  , "auth_token" .= authToken
                                  ]
               ]

data RequestError = BadMessage | BadAuth | ServerFail | BadTopic | None deriving ( Eq, Show )

instance Read RequestError where
    readsPrec _ "ERR_BADMESSAGE" = [(BadMessage, "")]
    readsPrec _ "ERR_BADAUTH"    = [(BadAuth, "")]
    readsPrec _ "ERR_SERVER"     = [(ServerFail, "")]
    readsPrec _ "ERR_BADTOPIC"   = [(BadTopic, "")]
    readsPrec _ _                = [(None, "")]

data Response = Response { nonce :: Maybe String
                         , errorReported :: RequestError
                         } deriving ( Show, Eq )

instance FromJSON Response where
    parseJSON = withObject "Response" $ \o ->
        o .: "type" >>= \(reportedType :: String) ->
        -- This value is required per Twitch or else any response is invalid
        if reportedType == "RESPONSE" then do
            nonce <- o .: "nonce"
            err <- o .: "error"
            let errorReported = read err :: RequestError
            return Response{..}
        else mzero

-- Used for Channel Points rewards
data RewardImages = RewardImages { tiny :: Maybe String
                                 , large :: Maybe String
                                 , huge :: Maybe String
                                 } deriving ( Show, Eq, Generic )

instance FromJSON RewardImages where
    parseJSON = withObject "RewardImages" $ \o -> do
        tiny <- o .: "url_1x"
        large <- o .: "url_2x"
        huge <- o .: "url_4x"
        return RewardImages{..}

data UserInfo = UserInfo { userId :: Integer
                         , userName :: String
                         , displayName :: Maybe String
                         } deriving ( Eq, Show, Generic )

instance FromJSON UserInfo where
    parseJSON = withObject "UserInfo" $ \o -> do
        userId' <- o .: "id"
        let userId = read userId' :: Integer
        userName <- o .: "login"
        displayName <- o .: "display_name"
        return UserInfo{..}

data RewardStatus = Fulfilled | Unfulfilled deriving ( Eq, Show, Generic )

instance Read RewardStatus where
    readsPrec _ "FULFILLED" = [(Fulfilled, "")]
    readsPrec _ _ = [(Unfulfilled, "")]


data BadgeUnlock = BadgeUnlock { newVersion :: Integer
                               , previousVersion :: Integer
                               } deriving ( Eq, Show, Generic )

instance FromJSON BadgeUnlock where
    parseJSON = withObject "BadgeUnlock" $ \o -> do
        newVersion <- o .: "new_version"
        previousVersion <- o .: "previous_version"
        return BadgeUnlock{..}

data SubscriptionTier = Prime | Tier1 | Tier2 | Tier3 deriving ( Eq, Show, Generic )

instance FromJSON SubscriptionTier where
    parseJSON = withText "SubscriptionTier" $ \o ->
        case o of
            "1000" -> return Tier1
            "2000" -> return Tier2
            "3000" -> return Tier3
            "Prime" -> return Prime
            _ -> mzero

data EmoteSpec = EmoteSpec { emoteStart :: Integer
                           , emoteLength :: Integer
                           , emoteId :: Integer
                           } deriving ( Eq, Show, Generic )

instance FromJSON EmoteSpec where
    parseJSON = withObject "EmoteSpec" $ \o -> do
        emoteStart <- o .: "start"
        emoteEnd :: Integer <- o .: "end"
        emoteId' :: Maybe String <- o .:? "id"
        let emoteLength = 1 + emoteEnd - emoteStart
        case emoteId' of
            Just emoteId'' ->
                let emoteId = read emoteId'' :: Integer
                in return EmoteSpec{..}
            Nothing -> do
                altEmoteId :: String <- o .: "emote_id"
                let emoteId = read altEmoteId :: Integer
                return EmoteSpec{..}

data SubscriptionMessage = SubscriptionMessage { messageBody :: String
                                               , emotes :: [EmoteSpec]
                                               } deriving ( Eq, Show, Generic )

instance FromJSON SubscriptionMessage where
    parseJSON = withObject "SubscriptionMessage" $ \o -> do
        messageBody <- o .: "message"
        emotes' <- o .:? "emotes"
        let emotes = case emotes' of
                         Nothing -> []
                         Just xs -> xs
        return SubscriptionMessage{..}

data Message = BitsV2Message { badge :: Maybe BadgeUnlock
                             , bits :: Integer
                             , channelId :: Integer
                             , chatMessage :: Maybe String
                             , context :: String
                             , messageId :: String
                             , messageType :: String
                             , time :: Maybe Time.UTCTime
                             , userTotal :: Integer
                             , userId :: Maybe Integer
                             , userName :: Maybe String
                             , version :: String
                             }
             | BitsV2AnonymousMessage { bits :: Integer
                                      , channelId :: Integer
                                      , chatMessage :: Maybe String
                                      , context :: String
                                      , messageId :: String
                                      , messageType :: String
                                      , time :: Maybe Time.UTCTime
                                      , version :: String
                                      }
             | BitsV1Message { badge :: Maybe BadgeUnlock
                             , bits :: Integer
                             , channelId :: Integer
                             , channelName :: String
                             , chatMessage :: Maybe String
                             , context :: String
                             , messageId :: String
                             , messageType :: String
                             , time :: Maybe Time.UTCTime
                             , userTotal :: Integer
                             , userId :: Maybe Integer
                             , userName :: Maybe String
                             , version :: String
                             }
             | BitsBadgeMessage { userId :: Maybe Integer
                                , userName :: Maybe String
                                , channelId :: Integer
                                , channelName :: String
                                , bitsTier :: Integer
                                , chatMessage :: Maybe String
                                , time :: Maybe Time.UTCTime
                                }
             | ChannelPointsMessage { serverTime :: Maybe Time.UTCTime
                                    , redeemedTime :: Maybe Time.UTCTime
                                    , user :: UserInfo
                                    , rewardId :: String
                                    , channelId :: Integer
                                    , title :: String
                                    , prompt :: Maybe String
                                    , cost :: Integer
                                    , userInput :: Maybe String
                                    , subOnly :: Bool
                                    , image :: Maybe RewardImages
                                    , defaultImage :: RewardImages
                                    , backgroundColor :: String
                                    , enabled :: Bool
                                    , paused :: Bool
                                    , inStock :: Bool
                                    , maxPerStream :: Maybe Integer
                                    , autoFulfilled :: Bool
                                    , status :: RewardStatus
                                    }
             | ChannelSubscriptionMessage { user :: UserInfo
                                          , channelName :: String
                                          , channelId :: Integer
                                          , time :: Maybe Time.UTCTime
                                          , subTier :: SubscriptionTier
                                          , subPlanName :: String
                                          , subMessage :: SubscriptionMessage
                                          }
             | ChannelResubscriptionMessage { user :: UserInfo
                                            , channelName :: String
                                            , channelId :: Integer
                                            , time :: Maybe Time.UTCTime
                                            , subTier :: SubscriptionTier
                                            , subPlanName :: String
                                            , totalMonths :: Integer
                                            , streakMonths :: Maybe Integer
                                            , subMessage :: SubscriptionMessage
                                            }
             | ChannelExtendSubscriptionMessage { user :: UserInfo
                                                , channelName :: String
                                                , channelId :: Integer
                                                , time :: Maybe Time.UTCTime
                                                , subTier :: SubscriptionTier
                                                , subPlanName :: String
                                                , totalMonths :: Integer
                                                , streakMonths :: Maybe Integer
                                                , endMonth :: Integer
                                                , subMessage :: SubscriptionMessage
                                                }
             | ChannelSubscriptionGiftMessage { user :: UserInfo
                                              , channelName :: String
                                              , channelId :: Integer
                                              , time :: Maybe Time.UTCTime
                                              , subTier :: SubscriptionTier
                                              , subPlanName :: String
                                              , recipient :: UserInfo
                                              }
             | ChannelMultiMonthSubscriptionGiftMessage { user :: UserInfo
                                                        , channelName :: String
                                                        , channelId :: Integer
                                                        , time :: Maybe Time.UTCTime
                                                        , subTier :: SubscriptionTier
                                                        , subPlanName :: String
                                                        , recipient :: UserInfo
                                                        , months :: Integer
                                                        }
             | ChannelAnonymousSubscriptionGiftMessage { channelName :: String
                                                       , channelId :: Integer
                                                       , time :: Maybe Time.UTCTime
                                                       , subTier :: SubscriptionTier
                                                       , subPlanName :: String
                                                       , recipient :: UserInfo
                                                       }
             | ChannelAnonymousMultiMonthSubscriptionGiftMessage { channelName :: String
                                                                 , channelId :: Integer
                                                                 , time :: Maybe Time.UTCTime
                                                                 , subTier :: SubscriptionTier
                                                                 , subPlanName :: String
                                                                 , recipient :: UserInfo
                                                                 , months :: Integer
                                                                 }
             | WhisperMessage { messageId :: String
                              , threadId :: String
                              , time :: Maybe Time.UTCTime
                              , messageBody :: String
                              , emotes :: [EmoteSpec]
                              , user :: UserInfo
                              , userColor :: String
                              , recipient :: UserInfo
                              }
             | SuccessMessage { nonce :: Maybe String }
             | ErrorMessage { nonce :: Maybe String
                            , errorString :: String
                            } deriving ( Eq, Show, Generic )

instance JSON.ToJSON EmoteSpec
instance JSON.ToJSON SubscriptionMessage
instance JSON.ToJSON SubscriptionTier
instance JSON.ToJSON RewardStatus
instance JSON.ToJSON RewardImages
instance JSON.ToJSON UserInfo
instance JSON.ToJSON BadgeUnlock
instance JSON.ToJSON Message

type MessageParser = Object -> JSON.Types.Parser Message

parseChannelSubscribeEvent :: MessageParser
parseChannelSubscribeEvent o = do
    uid :: String <- o .: "user_id"
    userName <- o .: "user_name"
    displayName <- o .: "display_name"
    let userId = read uid :: Integer
        user = UserInfo userId userName displayName

    channelName <- o .: "channel_name"
    channel <- o .: "channel_id"
    let channelId = read channel :: Integer

    t :: String <- o .: "time"
    let time = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 t

    subTier <- o .: "sub_plan"
    subPlanName <- o .: "sub_plan_name"
    subMessage <- o .: "sub_message"

    return ChannelSubscriptionMessage{..}

parseChannelResubscribeEvent :: MessageParser
parseChannelResubscribeEvent o = do
    uid :: String <- o .: "user_id"
    userName <- o .: "user_name"
    displayName <- o .: "display_name"
    let userId = read uid :: Integer
        user = UserInfo userId userName displayName

    channelName <- o .: "channel_name"
    channel <- o .: "channel_id"
    let channelId = read channel :: Integer

    t :: String <- o .: "time"
    let time = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 t

    subTier <- o .: "sub_plan"
    subPlanName <- o .: "sub_plan_name"
    totalMonths <- o .: "cumulative_months"
    streakMonths <- o .:? "streak_months"
    subMessage <- o .: "sub_message"

    return ChannelResubscriptionMessage{..}

parseChannelExtendSubEvent :: MessageParser
parseChannelExtendSubEvent o = do
    uid :: String <- o .: "user_id"
    userName <- o .: "user_name"
    displayName <- o .: "display_name"
    let userId = read uid :: Integer
        user = UserInfo userId userName displayName

    channelName <- o .: "channel_name"
    channel <- o .: "channel_id"
    let channelId = read channel :: Integer

    t :: String <- o .: "time"
    let time = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 t

    subTier <- o .: "sub_plan"
    subPlanName <- o .: "sub_plan_name"
    totalMonths <- o .: "cumulative_months"
    endMonth <- o .: "benefit_end_month"
    streakMonths <- o .:? "streak_months"
    subMessage <- o .: "sub_message"

    return ChannelExtendSubscriptionMessage{..}

parseChannelSubGiftEvent :: MessageParser
parseChannelSubGiftEvent o = do
    uid :: String <- o .: "user_id"
    userName <- o .: "user_name"
    displayName <- o .: "display_name"
    let userId = read uid :: Integer
        user = UserInfo userId userName displayName

    rid :: String <- o .: "recipient_id"
    rUserName <- o .: "recipient_user_name"
    rDisplayName <- o .: "recipient_display_name"
    let rUserId = read rid :: Integer
        recipient = UserInfo rUserId rUserName rDisplayName

    channelName <- o .: "channel_name"
    channel <- o .: "channel_id"
    let channelId = read channel :: Integer

    t :: String <- o .: "time"
    let time = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 t

    subTier <- o .: "sub_plan"
    subPlanName <- o .: "sub_plan_name"

    duration :: Maybe Integer <- o .: "multi_month_duration"
    case duration of
        Nothing -> return ChannelSubscriptionGiftMessage{..}
        Just 1 -> return ChannelSubscriptionGiftMessage{..}
        Just months -> return ChannelMultiMonthSubscriptionGiftMessage{..}

parseChannelAnonSubGiftEvent :: MessageParser
parseChannelAnonSubGiftEvent o = do
    rid :: String <- o .: "recipient_id"
    rUserName <- o .: "recipient_user_name"
    rDisplayName <- o .: "recipient_display_name"
    let rUserId = read rid :: Integer
        recipient = UserInfo rUserId rUserName rDisplayName

    channelName <- o .: "channel_name"
    channel <- o .: "channel_id"
    let channelId = read channel :: Integer

    t :: String <- o .: "time"
    let time = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 t

    subTier <- o .: "sub_plan"
    subPlanName <- o .: "sub_plan_name"

    duration :: Maybe Integer <- o .: "multi_month_duration"
    case duration of
        Nothing -> return ChannelAnonymousSubscriptionGiftMessage{..}
        Just 1 -> return ChannelAnonymousSubscriptionGiftMessage{..}
        Just months -> return ChannelAnonymousMultiMonthSubscriptionGiftMessage{..}

parseChannelSubscribeMessage :: MessageParser
parseChannelSubscribeMessage o = do
    context :: String <- o .: "context"
    case context of
      "sub" -> parseChannelSubscribeEvent o
      "resub" -> parseChannelResubscribeEvent o
      "extendsub" -> parseChannelExtendSubEvent o
      "subgift" -> parseChannelSubGiftEvent o
      "resubgift" -> parseChannelSubGiftEvent o
      "anonsubgift" -> parseChannelAnonSubGiftEvent o
      "anonresubgift" -> parseChannelAnonSubGiftEvent o
      _ -> mzero

parseBitsV2Message :: MessageParser
parseBitsV2Message o = do
    dat <- o .: "data"
    anonymous :: Maybe Bool <- o .:? "is_anonymous"
    case anonymous of
      Nothing      -> parseBitsV2 o dat
      (Just False) -> parseBitsV2 o dat
      (Just True)  -> parseBitsV2Anonymous o dat


parseBitsV2 :: Object -> MessageParser
parseBitsV2 o dat = do
    badge <- dat .: "badge_entitlement"
    bits <- dat .: "bits_used"
    chatMessage <- dat .: "chat_message"
    context <- dat .: "context"
    messageId <- o .: "message_id"
    messageType <- o .: "message_type" -- Always bits_event?  No other examples given in API docs
    userTotal <- dat .: "total_bits_used"
    userName <- dat .: "user_name"
    version <- o .: "version"

    channel :: String <- dat .: "channel_id"
    let channelId = read channel :: Integer

    uid :: Maybe String <- dat .: "user_id"
    let userId = fmap (read :: String -> Integer) uid

    t :: String <- dat .: "time"
    let time = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 t

    return BitsV2Message{..}

parseBitsV2Anonymous :: Object -> MessageParser
parseBitsV2Anonymous o dat = do
    bits <- dat .: "bits_used"
    chatMessage <- dat .: "chat_message"
    context <- dat .: "context"
    messageId <- o .: "message_id"
    messageType <- o .: "message_type" -- Always bits_event?  No other examples given in API docs
    version <- o .: "version"

    channel :: String <- dat .: "channel_id"
    let channelId = read channel :: Integer

    t :: String <- dat .: "time"
    let time = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 t

    return BitsV2AnonymousMessage{..}

parseBitsV1Message :: MessageParser
parseBitsV1Message o = do
    dat <- o .: "data"
    badge <- dat .: "badge_entitlement"
    bits <- dat .: "bits_used"
    channelName <- dat .: "channel_name"
    chatMessage <- dat .: "chat_message"
    context <- dat .: "context"
    messageId <- o .: "message_id"
    messageType <- o .: "message_type" -- Always bits_event?  No other examples given in API docs
    userTotal <- dat .: "total_bits_used"
    userName <- dat .: "user_name"
    version <- o .: "version"

    uid :: Maybe String <- dat .: "user_id"
    let userId = fmap (read :: String -> Integer) uid

    channel :: String <- dat .: "channel_id"
    let channelId = read channel :: Integer

    t :: String <- dat .: "time"
    let time = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 t

    return BitsV1Message{..}

parseBitsBadgeMessage :: MessageParser
parseBitsBadgeMessage o = do
    userId <- o .: "user_id"
    userName <- o .: "user_name"
    channelName <- o .: "channel_name"
    bitsTier <- o .: "badge_tier"
    chatMessage <- o .: "chat_message"

    channel :: String <- o .: "channel_id"
    t :: String <- o .: "time"
    let time = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 t
        channelId = read channel :: Integer
    return BitsBadgeMessage{..}

parseRewardMessage :: MessageParser
parseRewardMessage o = do
    dat <- o .: "data"
    redemption <- dat .: "redemption"
    reward <- redemption .: "reward"
    user <- redemption .: "user"
    rewardId <- reward .: "id"
    title <- reward .: "title"
    prompt <- reward .: "prompt"
    cost <- reward .: "cost"
    subOnly <- reward .: "is_sub_only"
    image <- reward .: "image"
    defaultImage <- reward .: "default_image"
    backgroundColor <- reward .: "background_color"
    enabled <- reward .: "is_enabled"
    paused <- reward .: "is_paused"
    inStock <- reward .: "is_in_stock"
    autoFulfilled <- reward .: "should_redemptions_skip_request_queue"

    channel :: String <- redemption .: "channel_id"
    let channelId = read channel :: Integer

    sTime :: String <- dat .: "timestamp"
    rTime :: String <- redemption .: "redeemed_at"
    let serverTime = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 sTime
        redeemedTime = Time.zonedTimeToUTC <$> Time.parseTimeRFC3339 rTime

    promptAnswer <- redemption .:? "user_input"
    promptEnabled :: Bool <- reward .: "is_user_input_required"
    let userInput = if promptEnabled then promptAnswer else Nothing

    maxObject <- reward .: "max_per_stream"
    maxEnabled :: Bool <- maxObject .: "is_enabled"
    maxCount <- maxObject .: "max_per_stream"
    let maxPerStream = if maxEnabled then Just maxCount else Nothing

    status' <- redemption .: "status"
    let status = read status' :: RewardStatus

    return ChannelPointsMessage{..}

parseWhisperMessage :: MessageParser
parseWhisperMessage o = do
    dat <- o .: "data_object"
    messageId <- dat .: "message_id"
    threadId <- dat .: "thread_id"
    messageBody <- dat .: "body"
    tags <- dat .: "tags"
    emotes <- tags .: "emotes"
    userColor <- tags .: "color"

    userId <- dat .: "from_id"
    userName <- tags .: "login"
    displayName <- tags .: "display_name"
    let user = UserInfo userId userName displayName

    recipientData <- dat .: "recipient"
    rUserId <- recipientData .: "id"
    rUserName <- recipientData .: "username"
    rDisplayName <- recipientData .: "display_name"
    let recipient = UserInfo rUserId rUserName rDisplayName

    timestamp :: Integer <- dat .: "sent_ts"
    let time = Just . Time.posixSecondsToUTCTime $ realToFrac timestamp

    return WhisperMessage{..}

parseServerResponse :: MessageParser
parseServerResponse o = do
    errorString <- o .: "error"
    nonce <- o .: "nonce"
    if null errorString
       then return SuccessMessage{..}
       else return ErrorMessage{..}

instance FromJSON Message where
    parseJSON = withObject "Received" $ \o -> do
        o .: "type" >>= \(reportedType :: String) -> case reportedType of
            "RESPONSE" -> parseServerResponse o
            "MESSAGE" -> do
                d <- o .: "data"
                m <- d .: "message"
                m' <- withEmbeddedJSON "Message" parseJSON (JSON.String m)
                m' .:? "type" >>= \(message :: Maybe String) ->
                    case message of
                        Just "reward-redeemed" -> parseRewardMessage m'
                        _ -> d .: "topic" >>= \(topic :: String) -> case takeWhile (/= '.') topic of
                            "channel-bits-badge-unlocks" -> parseBitsBadgeMessage m'
                            "channel-bits-events-v1" -> parseBitsV1Message m'
                            "channel-bits-events-v2" -> parseBitsV2Message m'
                            "channel-subscribe-events-v1" -> parseChannelSubscribeMessage m'
                            "whispers" -> parseWhisperMessage m'
                            _ -> mzero
            _ -> mzero

--data ChatModeratorActionsMessage
--data WhispersMessage