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
16 changed files with 332 additions and 17 deletions

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"
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 cc = do
initializeBotAddress cc
@@ -41,14 +44,13 @@ mySquaringBot _user cc = do
case resp of
CRContactConnected _ contact _ -> do
contactConnected contact
void . sendMsg contact $ "Hello! I am a simple squaring bot - if you send me a number, I will calculate its square"
CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do
let msg = T.unpack $ ciContentToText content
sendMessage cc contact welcomeMessage
CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) -> do
let msg = T.unpack $ ciContentToText mc
number_ = readMaybe msg :: Maybe Integer
void . sendMsg contact $ case number_ of
Nothing -> "\"" <> msg <> "\" is not a number"
sendMessage cc contact $ case number_ of
Just n -> msg <> " * " <> msg <> " = " <> show (n * n)
_ -> "\"" <> msg <> "\" is not a number"
_ -> pure ()
where
sendMsg Contact {contactId} msg = sendChatCmd cc $ "/_send @" <> show contactId <> " text " <> msg
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
opts <- welcomeGetOpts
simplexChatCore terminalChatConfig opts Nothing $
chatBotRepl "Hello! I am a simple squaring bot - if you send me a number, I will calculate its square" $ \msg ->
case readMaybe msg :: Maybe Integer of
chatBotRepl welcomeMessage $ \_contact msg ->
pure $ case readMaybe msg :: Maybe Integer of
Just n -> msg <> " * " <> msg <> " = " <> show (n * n)
_ -> "\"" <> 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 = do
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.