never executed always true always false
1 module PureClaw.Channels.Signal
2 ( -- * Signal channel
3 SignalChannel (..)
4 , SignalConfig (..)
5 , mkSignalChannel
6 -- * Message parsing
7 , parseSignalEnvelope
8 , SignalEnvelope (..)
9 , SignalDataMessage (..)
10 ) where
11
12 import Control.Concurrent.STM
13 import Data.Aeson
14 import Data.Text (Text)
15 import Data.Text qualified as T
16
17 import PureClaw.Channels.Class
18 import PureClaw.Core.Errors
19 import PureClaw.Core.Types
20 import PureClaw.Handles.Channel
21 import PureClaw.Handles.Log
22
23 -- | Configuration for Signal channel.
24 newtype SignalConfig = SignalConfig
25 { _sc_account :: Text
26 }
27 deriving stock (Show, Eq)
28
29 -- | A Signal channel backed by a message queue. Messages from signal-cli
30 -- are pushed into the queue and the agent loop pulls them out via 'receive'.
31 data SignalChannel = SignalChannel
32 { _sch_config :: SignalConfig
33 , _sch_inbox :: TQueue SignalEnvelope
34 , _sch_log :: LogHandle
35 }
36
37 -- | Create a Signal channel with an empty inbox.
38 mkSignalChannel :: SignalConfig -> LogHandle -> IO SignalChannel
39 mkSignalChannel config lh = do
40 inbox <- newTQueueIO
41 pure SignalChannel
42 { _sch_config = config
43 , _sch_inbox = inbox
44 , _sch_log = lh
45 }
46
47 instance Channel SignalChannel where
48 toHandle sc = ChannelHandle
49 { _ch_receive = receiveEnvelope sc
50 , _ch_send = sendSignalMessage sc
51 , _ch_sendError = sendSignalError sc
52 , _ch_sendChunk = \_ -> pure () -- Signal doesn't support streaming
53 , _ch_readSecret = ioError (userError "Vault management requires the CLI interface")
54 }
55
56 -- | Block until a Signal envelope arrives in the queue.
57 receiveEnvelope :: SignalChannel -> IO IncomingMessage
58 receiveEnvelope sc = do
59 envelope <- atomically $ readTQueue (_sch_inbox sc)
60 let sender = _se_source envelope
61 content = maybe "" _sdm_message (_se_dataMessage envelope)
62 pure IncomingMessage
63 { _im_userId = UserId sender
64 , _im_content = content
65 }
66
67 -- | Send a message via Signal. In a full implementation this would
68 -- invoke signal-cli. For now, logs the outgoing message.
69 sendSignalMessage :: SignalChannel -> OutgoingMessage -> IO ()
70 sendSignalMessage sc msg =
71 _lh_logInfo (_sch_log sc) $ "Signal send: " <> _om_content msg
72
73 -- | Send an error via Signal.
74 sendSignalError :: SignalChannel -> PublicError -> IO ()
75 sendSignalError sc err =
76 _lh_logWarn (_sch_log sc) $ "Signal error: " <> T.pack (show err)
77
78 -- | A Signal envelope (simplified JSON-RPC format from signal-cli).
79 data SignalEnvelope = SignalEnvelope
80 { _se_source :: Text
81 , _se_timestamp :: Int
82 , _se_dataMessage :: Maybe SignalDataMessage
83 }
84 deriving stock (Show, Eq)
85
86 -- | A Signal data message.
87 data SignalDataMessage = SignalDataMessage
88 { _sdm_message :: Text
89 , _sdm_timestamp :: Int
90 }
91 deriving stock (Show, Eq)
92
93 instance FromJSON SignalEnvelope where
94 parseJSON = withObject "SignalEnvelope" $ \o ->
95 SignalEnvelope <$> o .: "source" <*> o .: "timestamp" <*> o .:? "dataMessage"
96
97 instance FromJSON SignalDataMessage where
98 parseJSON = withObject "SignalDataMessage" $ \o ->
99 SignalDataMessage <$> o .: "message" <*> o .: "timestamp"
100
101 -- | Parse a JSON value as a Signal envelope.
102 parseSignalEnvelope :: Value -> Either String SignalEnvelope
103 parseSignalEnvelope v = case fromJSON v of
104 Error err -> Left err
105 Success e -> Right e