apps: update chat bots, readme (#1928)

* apps: update chat bots, readme

* CLI readme

* broadcast bot

* delete messages from non-publishers, better replies, support forwarding low-res images and links

* typo

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* change

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
Evgeny Poberezkin 2023-02-14 07:57:27 +00:00 committed by GitHub
parent 6f68840b3a
commit a0351d6f99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 332 additions and 17 deletions

View file

@ -24,7 +24,7 @@
- 🔐 Double ratchet end-to-end encryption, with additional encryption layer. - 🔐 Double ratchet end-to-end encryption, with additional encryption layer.
- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). - 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**! - 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
- 🖥 Available as a terminal (console) app / CLI on Linux, MacOS, Windows. - 🖥 Available as a terminal (console) [app / CLI](#zap-quick-installation-of-a-terminal-app) on Linux, MacOS, Windows.
**NEW**: Security audit by [Trail of Bits](https://www.trailofbits.com/about), the [new website](https://simplex.chat) and v4.2 released! [See the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md) **NEW**: Security audit by [Trail of Bits](https://www.trailofbits.com/about), the [new website](https://simplex.chat) and v4.2 released! [See the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)

1
apps/ios/README.md Normal file
View file

@ -0,0 +1 @@
# SimpleX Chat iOS app

View file

@ -33,6 +33,9 @@ welcomeGetOpts = do
putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db"
pure opts pure opts
welcomeMessage :: String
welcomeMessage = "Hello! I am a simple squaring bot.\nIf you send me a number, I will calculate its square"
mySquaringBot :: User -> ChatController -> IO () mySquaringBot :: User -> ChatController -> IO ()
mySquaringBot _user cc = do mySquaringBot _user cc = do
initializeBotAddress cc initializeBotAddress cc
@ -41,14 +44,13 @@ mySquaringBot _user cc = do
case resp of case resp of
CRContactConnected _ contact _ -> do CRContactConnected _ contact _ -> do
contactConnected contact contactConnected contact
void . sendMsg contact $ "Hello! I am a simple squaring bot - if you send me a number, I will calculate its square" sendMessage cc contact welcomeMessage
CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) -> do
let msg = T.unpack $ ciContentToText content let msg = T.unpack $ ciContentToText mc
number_ = readMaybe msg :: Maybe Integer number_ = readMaybe msg :: Maybe Integer
void . sendMsg contact $ case number_ of sendMessage cc contact $ case number_ of
Nothing -> "\"" <> msg <> "\" is not a number"
Just n -> msg <> " * " <> msg <> " = " <> show (n * n) Just n -> msg <> " * " <> msg <> " = " <> show (n * n)
_ -> "\"" <> msg <> "\" is not a number"
_ -> pure () _ -> pure ()
where where
sendMsg Contact {contactId} msg = sendChatCmd cc $ "/_send @" <> show contactId <> " text " <> msg
contactConnected Contact {localDisplayName} = putStrLn $ T.unpack localDisplayName <> " connected" contactConnected Contact {localDisplayName} = putStrLn $ T.unpack localDisplayName <> " connected"

View file

@ -0,0 +1,13 @@
# Advanced SimpleX Chat bot example
In most cases [a simple REPL bot](../simplex-bot/) is sufficient, but in cases you want to program more advanced communication scenarios you may take a more complex event-based approach, as in this example.
Event-based approach allows you:
- decide whether to connect to a user or not depending on any factors, e.g. user display name.
- disconnect from users who send too many messages or send messages that bot finds inappropriate.
- react to message deletions and editing.
- process reply messages differently, taking the original message into account.
- process and send images and voice messages.
- create groups of users, e.g. to connect 2 users.
- etc.

View file

@ -14,11 +14,14 @@ main :: IO ()
main = do main = do
opts <- welcomeGetOpts opts <- welcomeGetOpts
simplexChatCore terminalChatConfig opts Nothing $ simplexChatCore terminalChatConfig opts Nothing $
chatBotRepl "Hello! I am a simple squaring bot - if you send me a number, I will calculate its square" $ \msg -> chatBotRepl welcomeMessage $ \_contact msg ->
case readMaybe msg :: Maybe Integer of pure $ case readMaybe msg :: Maybe Integer of
Just n -> msg <> " * " <> msg <> " = " <> show (n * n) Just n -> msg <> " * " <> msg <> " = " <> show (n * n)
_ -> "\"" <> msg <> "\" is not a number" _ -> "\"" <> msg <> "\" is not a number"
welcomeMessage :: String
welcomeMessage = "Hello! I am a simple squaring bot.\nIf you send me a number, I will calculate its square"
welcomeGetOpts :: IO ChatOpts welcomeGetOpts :: IO ChatOpts
welcomeGetOpts = do welcomeGetOpts = do
appDir <- getAppUserDataDirectory "simplex" appDir <- getAppUserDataDirectory "simplex"

View file

@ -0,0 +1,17 @@
# Simple SimpleX Chat bot example
This chat bot is a Haskell implementation of a REPL chat bot.
All you have to do to create your bot based on this example is to provide a welcome message for connecting users and a function of type `Contact -> String -> IO String`. This function should transform the sent message into a reply message, ignoring any system messages related to preferences and user profile changes.
This bot example calculates the square of the number that is sent to it, but you can program it to do other things, simply by changing REPL function:
- a more advanced calculator (e.g., based on [this one](https://github.com/jonathanknowles/haskell-calculator)).
- translation to/from any language.
- lookup of market quotes.
- search of the information.
- AI-powered dialogue the bot can maintain any conversation state based on the contact.
- provide any other online service via chat UI.
- etc.
Please share any bots you create with us, we will add to this page and can host them if you like!

View file

@ -0,0 +1,74 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Monad.Reader
import qualified Data.Text as T
import Options
import Simplex.Chat.Bot
import Simplex.Chat.Controller
import Simplex.Chat.Core
import Simplex.Chat.Messages
import Simplex.Chat.Options
import Simplex.Chat.Protocol (MsgContent (..))
import Simplex.Chat.Terminal (terminalChatConfig)
import Simplex.Chat.Types
import System.Directory (getAppUserDataDirectory)
main :: IO ()
main = do
opts@BroadcastBotOpts {chatOptions} <- welcomeGetOpts
simplexChatCore terminalChatConfig chatOptions Nothing $ broadcastBot opts
welcomeGetOpts :: IO BroadcastBotOpts
welcomeGetOpts = do
appDir <- getAppUserDataDirectory "simplex"
opts@BroadcastBotOpts {chatOptions = ChatOpts {dbFilePrefix}} <- getBroadcastBotOpts appDir "simplex_status_bot"
putStrLn $ "SimpleX Chat Bot v" ++ versionNumber
putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db"
pure opts
broadcastBot :: BroadcastBotOpts -> User -> ChatController -> IO ()
broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _user cc = do
initializeBotAddress cc
race_ (forever $ void getLine) . forever $ do
(_, resp) <- atomically . readTBQueue $ outputQ cc
case resp of
CRContactConnected _ ct _ -> do
contactConnected ct
sendMessage cc ct welcomeMessage
CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc})
| publisher `elem` publishers ->
if allowContent mc
then do
sendChatCmd cc "/contacts" >>= \case
CRContactsList _ cts -> do
let cts' = filter broadcastTo cts
forM_ cts' $ \ct' -> sendComposedMessage cc ct' Nothing mc
sendReply $ "Forwarded to " <> show (length cts') <> " contact(s)"
r -> putStrLn $ "Error getting contacts list: " <> show r
else sendReply "!1 Message is not supported!"
| otherwise -> do
sendReply prohibitedMessage
deleteMessage cc ct $ chatItemId' ci
where
sendReply = sendComposedMessage cc ct (Just $ chatItemId' ci) . textMsgContent
publisher = Publisher {contactId = contactId' ct, localDisplayName = localDisplayName' ct}
allowContent = \case
MCText _ -> True
MCLink {} -> True
MCImage {} -> True
_ -> False
broadcastTo ct'@Contact {activeConn = conn@Connection {connStatus}} =
(connStatus == ConnSndReady || connStatus == ConnReady)
&& not (connDisabled conn)
&& contactId' ct' /= contactId' ct
_ -> pure ()
where
contactConnected ct = putStrLn $ T.unpack (localDisplayName' ct) <> " connected"

View file

@ -0,0 +1,97 @@
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Options where
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Int (Int64)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Options.Applicative
import Simplex.Chat.Controller (updateStr, versionNumber, versionString)
import Simplex.Chat.Options
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util (safeDecodeUtf8)
data Publisher = Publisher
{ contactId :: Int64,
localDisplayName :: Text
}
deriving (Eq)
data BroadcastBotOpts = BroadcastBotOpts
{ chatOptions :: ChatOpts,
publishers :: [Publisher],
welcomeMessage :: String,
prohibitedMessage :: String
}
defaultWelcomeMessage :: [Publisher] -> String
defaultWelcomeMessage ps = "Hello! I am a broadcast bot.\nI broadcast messages to all connected users from " <> publisherNames ps <> "."
defaultProhibitedMessage :: [Publisher] -> String
defaultProhibitedMessage ps = "Sorry, only these users can broadcast messages: " <> publisherNames ps <> ". Your message is deleted."
publisherNames :: [Publisher] -> String
publisherNames = T.unpack . T.intercalate ", " . map (("@" <>) . localDisplayName)
broadcastBotOpts :: FilePath -> FilePath -> Parser BroadcastBotOpts
broadcastBotOpts appDir defaultDbFileName = do
chatOptions <- chatOptsP appDir defaultDbFileName
publishers <-
option
parsePublishers
( long "publishers"
<> metavar "PUBLISHERS"
<> help "Comma-separated list of publishers in the format CONTACT_ID:DISPLAY_NAME whose messages will be broadcasted"
<> value []
)
welcomeMessage_ <-
optional $
strOption
( long "welcome"
<> metavar "WELCOME"
<> help "Welcome message to be sent to all connecting users (default message will list allowed publishers)"
)
prohibitedMessage_ <-
optional $
strOption
( long "prohibited"
<> metavar "PROHIBITED"
<> help "Reply to non-publishers who try to send messages (default reply will list allowed publishers)"
<> showDefault
)
pure
BroadcastBotOpts
{ chatOptions,
publishers,
welcomeMessage = fromMaybe (defaultWelcomeMessage publishers) welcomeMessage_,
prohibitedMessage = fromMaybe (defaultProhibitedMessage publishers) prohibitedMessage_
}
parsePublishers :: ReadM [Publisher]
parsePublishers = eitherReader $ parseAll publishersP . encodeUtf8 . T.pack
publishersP :: A.Parser [Publisher]
publishersP = publisherP `A.sepBy1` A.char ','
where
publisherP = do
contactId <- A.decimal <* A.char ':'
localDisplayName <- safeDecodeUtf8 <$> A.takeTill (A.inClass ", ")
pure Publisher {contactId, localDisplayName}
getBroadcastBotOpts :: FilePath -> FilePath -> IO BroadcastBotOpts
getBroadcastBotOpts appDir defaultDbFileName =
execParser $
info
(helper <*> versionOption <*> broadcastBotOpts appDir defaultDbFileName)
(header versionStr <> fullDesc <> progDesc "Start chat bot with DB_FILE file and use SERVER as SMP server")
where
versionStr = versionString versionNumber
versionOption = infoOption versionAndUpdate (long "version" <> short 'v' <> help "Show version")
versionAndUpdate = versionStr <> "\n" <> updateStr

View file

@ -0,0 +1,9 @@
# SimpleX broadcast bot
The bot allows anybody to connect to it and re-broadcasts all messages received from specified users configured via CLI options (publishers) to all users.
Welcome message and reply to the users who are not set as publishers can also be configured via CLI options.
It's a poor man's feed, until we have a better support for feeds in SimpleX Chat.
We use it to broadcast status notifications for SimpleX Chat servers when we do any maintenance or in case they become unavailable.

View file

@ -0,0 +1,3 @@
# SimpleX Chat CLI app
See [repo REAMDE](../../README.md#zap-quick-installation-of-a-terminal-app) for installation and usage instructions.

View file

@ -88,6 +88,14 @@ executables:
ghc-options: ghc-options:
- -threaded - -threaded
simplex-broadcast-bot:
source-dirs: apps/simplex-broadcast-bot
main: Main.hs
dependencies:
- simplex-chat
ghc-options:
- -threaded
tests: tests:
simplex-chat-test: simplex-chat-test:
source-dirs: tests source-dirs: tests

View file

@ -235,6 +235,54 @@ executable simplex-bot-advanced
if flag(swift) if flag(swift)
cpp-options: -DswiftJSON cpp-options: -DswiftJSON
executable simplex-broadcast-bot
main-is: Main.hs
other-modules:
Options
Paths_simplex_chat
hs-source-dirs:
apps/simplex-broadcast-bot
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends:
aeson ==2.0.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, bytestring ==0.10.*
, composition ==1.0.*
, containers ==0.6.*
, cryptonite >=0.27 && <0.30
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
, email-validate ==2.3.*
, exceptions ==0.10.*
, filepath ==1.4.*
, http-types ==0.12.*
, mtl ==2.2.*
, network >=3.1.2.7 && <3.2
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, record-hasfield ==1.0.*
, simple-logger ==0.1.*
, simplex-chat
, simplexmq >=3.4
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, template-haskell ==2.16.*
, terminal ==0.2.*
, text ==1.2.*
, time ==1.9.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, zip ==1.7.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
executable simplex-chat executable simplex-chat
main-is: Main.hs main-is: Main.hs
other-modules: other-modules:

View file

@ -1,3 +1,4 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
@ -8,17 +9,21 @@ module Simplex.Chat.Bot where
import Control.Concurrent.Async import Control.Concurrent.Async
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad.Reader import Control.Monad.Reader
import qualified Data.Aeson as J
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import qualified Data.Text as T import qualified Data.Text as T
import Simplex.Chat.Controller import Simplex.Chat.Controller
import Simplex.Chat.Core import Simplex.Chat.Core
import Simplex.Chat.Messages import Simplex.Chat.Messages
import Simplex.Chat.Protocol (MsgContent (..))
import Simplex.Chat.Store import Simplex.Chat.Store
import Simplex.Chat.Types (Contact (..), User (..)) import Simplex.Chat.Types (Contact (..), IsContact (..), User (..))
import Simplex.Messaging.Encoding.String (strEncode) import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Util (safeDecodeUtf8)
import System.Exit (exitFailure) import System.Exit (exitFailure)
chatBotRepl :: String -> (String -> String) -> User -> ChatController -> IO () chatBotRepl :: String -> (Contact -> String -> IO String) -> User -> ChatController -> IO ()
chatBotRepl welcome answer _user cc = do chatBotRepl welcome answer _user cc = do
initializeBotAddress cc initializeBotAddress cc
race_ (forever $ void getLine) . forever $ do race_ (forever $ void getLine) . forever $ do
@ -27,9 +32,9 @@ chatBotRepl welcome answer _user cc = do
CRContactConnected _ contact _ -> do CRContactConnected _ contact _ -> do
contactConnected contact contactConnected contact
void $ sendMsg contact welcome void $ sendMsg contact welcome
CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) -> do
let msg = T.unpack $ ciContentToText content let msg = T.unpack $ ciContentToText mc
void . sendMsg contact $ answer msg void $ sendMsg contact =<< answer contact msg
_ -> pure () _ -> pure ()
where where
sendMsg Contact {contactId} msg = sendChatCmd cc $ "/_send @" <> show contactId <> " text " <> msg sendMsg Contact {contactId} msg = sendChatCmd cc $ "/_send @" <> show contactId <> " text " <> msg
@ -49,3 +54,33 @@ initializeBotAddress cc = do
showBotAddress uri = do showBotAddress uri = do
putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri) putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri)
void $ sendChatCmd cc "/auto_accept on" void $ sendChatCmd cc "/auto_accept on"
sendMessage :: ChatController -> Contact -> String -> IO ()
sendMessage cc ct = sendComposedMessage cc ct Nothing . textMsgContent
sendComposedMessage :: ChatController -> Contact -> Maybe ChatItemId -> MsgContent -> IO ()
sendComposedMessage cc ct quotedItemId msgContent = do
let cm = ComposedMessage {filePath = Nothing, quotedItemId, msgContent}
sendChatCmd cc ("/_send @" <> show (contactId' ct) <> " json " <> jsonEncode cm) >>= \case
CRNewChatItem {} -> printLog cc CLLInfo $ "sent message to " <> contactInfo ct
r -> putStrLn $ "unexpected send message response: " <> show r
where
jsonEncode = T.unpack . safeDecodeUtf8 . LB.toStrict . J.encode
deleteMessage :: ChatController -> Contact -> ChatItemId -> IO ()
deleteMessage cc ct chatItemId = do
let cmd = "/_delete item @" <> show (contactId' ct) <> " " <> show chatItemId <> " internal"
sendChatCmd cc cmd >>= \case
CRChatItemDeleted {} -> printLog cc CLLInfo $ "deleted message from " <> contactInfo ct
r -> putStrLn $ "unexpected delete message response: " <> show r
textMsgContent :: String -> MsgContent
textMsgContent = MCText . T.pack
printLog :: ChatController -> ChatLogLevel -> String -> IO ()
printLog cc level s
| logLevel (config cc) <= level = putStrLn s
| otherwise = pure ()
contactInfo :: Contact -> String
contactInfo Contact {contactId, localDisplayName} = T.unpack localDisplayName <> " (" <> show contactId <> ")"

View file

@ -572,6 +572,10 @@ data ComposedMessage = ComposedMessage
} }
deriving (Show, Generic, FromJSON) deriving (Show, Generic, FromJSON)
instance ToJSON ComposedMessage where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
data NtfMsgInfo = NtfMsgInfo {msgTs :: UTCTime, msgFlags :: MsgFlags} data NtfMsgInfo = NtfMsgInfo {msgTs :: UTCTime, msgFlags :: MsgFlags}
deriving (Show, Generic) deriving (Show, Generic)

View file

@ -7,6 +7,7 @@
module Simplex.Chat.Options module Simplex.Chat.Options
( ChatOpts (..), ( ChatOpts (..),
chatOptsP,
getChatOpts, getChatOpts,
smpServersP, smpServersP,
fullNetworkConfig, fullNetworkConfig,
@ -43,8 +44,8 @@ data ChatOpts = ChatOpts
maintenance :: Bool maintenance :: Bool
} }
chatOpts :: FilePath -> FilePath -> Parser ChatOpts chatOptsP :: FilePath -> FilePath -> Parser ChatOpts
chatOpts appDir defaultDbFileName = do chatOptsP appDir defaultDbFileName = do
dbFilePrefix <- dbFilePrefix <-
strOption strOption
( long "database" ( long "database"
@ -229,7 +230,7 @@ getChatOpts :: FilePath -> FilePath -> IO ChatOpts
getChatOpts appDir defaultDbFileName = getChatOpts appDir defaultDbFileName =
execParser $ execParser $
info info
(helper <*> versionOption <*> chatOpts appDir defaultDbFileName) (helper <*> versionOption <*> chatOptsP appDir defaultDbFileName)
(header versionStr <> fullDesc <> progDesc "Start chat with DB_FILE file and use SERVER as SMP server") (header versionStr <> fullDesc <> progDesc "Start chat with DB_FILE file and use SERVER as SMP server")
where where
versionStr = versionString versionNumber versionStr = versionString versionNumber