mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 04:39:53 +00:00
directory: better search, allow both simplex:/ and simplex.chat links in description (#3546)
* directory: new commands * better search * search test * return group links in simplex.chat domain, allow both simplex:/ and simplex.chat links in group description
This commit is contained in:
parent
6fa0001ea7
commit
f0338a03d1
6 changed files with 250 additions and 33 deletions
|
@ -21,14 +21,18 @@ where
|
||||||
import Control.Applicative ((<|>))
|
import Control.Applicative ((<|>))
|
||||||
import Data.Attoparsec.Text (Parser)
|
import Data.Attoparsec.Text (Parser)
|
||||||
import qualified Data.Attoparsec.Text as A
|
import qualified Data.Attoparsec.Text as A
|
||||||
|
import Data.Functor (($>))
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import Data.Text.Encoding (encodeUtf8)
|
||||||
import Directory.Store
|
import Directory.Store
|
||||||
import Simplex.Chat.Controller
|
import Simplex.Chat.Controller
|
||||||
import Simplex.Chat.Messages
|
import Simplex.Chat.Messages
|
||||||
import Simplex.Chat.Messages.CIContent
|
import Simplex.Chat.Messages.CIContent
|
||||||
import Simplex.Chat.Protocol (MsgContent (..))
|
import Simplex.Chat.Protocol (MsgContent (..))
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
|
import Simplex.Messaging.Encoding.String
|
||||||
|
import Simplex.Messaging.Util ((<$?>))
|
||||||
import Data.Char (isSpace)
|
import Data.Char (isSpace)
|
||||||
import Data.Either (fromRight)
|
import Data.Either (fromRight)
|
||||||
|
|
||||||
|
@ -83,6 +87,10 @@ deriving instance Show (SDirectoryRole r)
|
||||||
|
|
||||||
data DirectoryCmdTag (r :: DirectoryRole) where
|
data DirectoryCmdTag (r :: DirectoryRole) where
|
||||||
DCHelp_ :: DirectoryCmdTag 'DRUser
|
DCHelp_ :: DirectoryCmdTag 'DRUser
|
||||||
|
DCSearchNext_ :: DirectoryCmdTag 'DRUser
|
||||||
|
DCAllGroups_ :: DirectoryCmdTag 'DRUser
|
||||||
|
DCRecentGroups_ :: DirectoryCmdTag 'DRUser
|
||||||
|
DCSubmitGroup_ :: DirectoryCmdTag 'DRUser
|
||||||
DCConfirmDuplicateGroup_ :: DirectoryCmdTag 'DRUser
|
DCConfirmDuplicateGroup_ :: DirectoryCmdTag 'DRUser
|
||||||
DCListUserGroups_ :: DirectoryCmdTag 'DRUser
|
DCListUserGroups_ :: DirectoryCmdTag 'DRUser
|
||||||
DCDeleteGroup_ :: DirectoryCmdTag 'DRUser
|
DCDeleteGroup_ :: DirectoryCmdTag 'DRUser
|
||||||
|
@ -100,6 +108,10 @@ data ADirectoryCmdTag = forall r. ADCT (SDirectoryRole r) (DirectoryCmdTag r)
|
||||||
data DirectoryCmd (r :: DirectoryRole) where
|
data DirectoryCmd (r :: DirectoryRole) where
|
||||||
DCHelp :: DirectoryCmd 'DRUser
|
DCHelp :: DirectoryCmd 'DRUser
|
||||||
DCSearchGroup :: Text -> DirectoryCmd 'DRUser
|
DCSearchGroup :: Text -> DirectoryCmd 'DRUser
|
||||||
|
DCSearchNext :: DirectoryCmd 'DRUser
|
||||||
|
DCAllGroups :: DirectoryCmd 'DRUser
|
||||||
|
DCRecentGroups :: DirectoryCmd 'DRUser
|
||||||
|
DCSubmitGroup :: ConnReqContact -> DirectoryCmd 'DRUser
|
||||||
DCConfirmDuplicateGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser
|
DCConfirmDuplicateGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser
|
||||||
DCListUserGroups :: DirectoryCmd 'DRUser
|
DCListUserGroups :: DirectoryCmd 'DRUser
|
||||||
DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser
|
DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser
|
||||||
|
@ -120,7 +132,9 @@ deriving instance Show ADirectoryCmd
|
||||||
|
|
||||||
directoryCmdP :: Parser ADirectoryCmd
|
directoryCmdP :: Parser ADirectoryCmd
|
||||||
directoryCmdP =
|
directoryCmdP =
|
||||||
(A.char '/' *> cmdStrP) <|> (ADC SDRUser . DCSearchGroup <$> A.takeText)
|
(A.char '/' *> cmdStrP)
|
||||||
|
<|> (A.char '.' $> ADC SDRUser DCSearchNext)
|
||||||
|
<|> (ADC SDRUser . DCSearchGroup <$> A.takeText)
|
||||||
where
|
where
|
||||||
cmdStrP =
|
cmdStrP =
|
||||||
(tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t)))
|
(tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t)))
|
||||||
|
@ -128,6 +142,10 @@ directoryCmdP =
|
||||||
tagP = A.takeTill (== ' ') >>= \case
|
tagP = A.takeTill (== ' ') >>= \case
|
||||||
"help" -> u DCHelp_
|
"help" -> u DCHelp_
|
||||||
"h" -> u DCHelp_
|
"h" -> u DCHelp_
|
||||||
|
"next" -> u DCSearchNext_
|
||||||
|
"all" -> u DCAllGroups_
|
||||||
|
"new" -> u DCRecentGroups_
|
||||||
|
"submit" -> u DCSubmitGroup_
|
||||||
"confirm" -> u DCConfirmDuplicateGroup_
|
"confirm" -> u DCConfirmDuplicateGroup_
|
||||||
"list" -> u DCListUserGroups_
|
"list" -> u DCListUserGroups_
|
||||||
"ls" -> u DCListUserGroups_
|
"ls" -> u DCListUserGroups_
|
||||||
|
@ -146,6 +164,10 @@ directoryCmdP =
|
||||||
cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r)
|
cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r)
|
||||||
cmdP = \case
|
cmdP = \case
|
||||||
DCHelp_ -> pure DCHelp
|
DCHelp_ -> pure DCHelp
|
||||||
|
DCSearchNext_ -> pure DCSearchNext
|
||||||
|
DCAllGroups_ -> pure DCAllGroups
|
||||||
|
DCRecentGroups_ -> pure DCRecentGroups
|
||||||
|
DCSubmitGroup_ -> fmap DCSubmitGroup . strDecode . encodeUtf8 <$?> (A.takeWhile1 isSpace *> A.takeText)
|
||||||
DCConfirmDuplicateGroup_ -> gc DCConfirmDuplicateGroup
|
DCConfirmDuplicateGroup_ -> gc DCConfirmDuplicateGroup
|
||||||
DCListUserGroups_ -> pure DCListUserGroups
|
DCListUserGroups_ -> pure DCListUserGroups
|
||||||
DCDeleteGroup_ -> gc DCDeleteGroup
|
DCDeleteGroup_ -> gc DCDeleteGroup
|
||||||
|
|
|
@ -21,6 +21,7 @@ data DirectoryOpts = DirectoryOpts
|
||||||
superUsers :: [KnownContact],
|
superUsers :: [KnownContact],
|
||||||
directoryLog :: Maybe FilePath,
|
directoryLog :: Maybe FilePath,
|
||||||
serviceName :: String,
|
serviceName :: String,
|
||||||
|
searchResults :: Int,
|
||||||
testing :: Bool
|
testing :: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ directoryOpts appDir defaultDbFileName = do
|
||||||
superUsers,
|
superUsers,
|
||||||
directoryLog,
|
directoryLog,
|
||||||
serviceName,
|
serviceName,
|
||||||
|
searchResults = 10,
|
||||||
testing = False
|
testing = False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
apps/simplex-directory-service/src/Directory/Search.hs
Normal file
32
apps/simplex-directory-service/src/Directory/Search.hs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
|
||||||
|
module Directory.Search where
|
||||||
|
|
||||||
|
import Data.List (sortOn)
|
||||||
|
import Data.Ord (Down (..))
|
||||||
|
import Data.Set (Set)
|
||||||
|
import qualified Data.Set as S
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Time.Clock (UTCTime)
|
||||||
|
import Simplex.Chat.Types
|
||||||
|
|
||||||
|
data SearchRequest = SearchRequest
|
||||||
|
{ searchType :: SearchType,
|
||||||
|
searchTime :: UTCTime,
|
||||||
|
sentGroups :: Set GroupId
|
||||||
|
}
|
||||||
|
|
||||||
|
data SearchType = STAll | STRecent | STSearch Text
|
||||||
|
|
||||||
|
takeTop :: Int -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)]
|
||||||
|
takeTop n = take n . sortOn (Down . currentMembers . snd)
|
||||||
|
|
||||||
|
takeRecent :: Int -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)]
|
||||||
|
takeRecent n = take n . sortOn (Down . (\GroupInfo {createdAt} -> createdAt) . fst)
|
||||||
|
|
||||||
|
groupIds :: [(GroupInfo, GroupSummary)] -> Set GroupId
|
||||||
|
groupIds = S.fromList . map (\(GroupInfo {groupId}, _) -> groupId)
|
||||||
|
|
||||||
|
filterNotSent :: Set GroupId -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)]
|
||||||
|
filterNotSent sentGroups = filter (\(GroupInfo {groupId}, _) -> groupId `S.notMember` sentGroups)
|
|
@ -17,16 +17,16 @@ import Control.Concurrent.Async
|
||||||
import Control.Concurrent.STM
|
import Control.Concurrent.STM
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import qualified Data.ByteString.Char8 as B
|
import qualified Data.ByteString.Char8 as B
|
||||||
import Data.List (sortOn)
|
|
||||||
import Data.Maybe (fromMaybe, maybeToList)
|
import Data.Maybe (fromMaybe, maybeToList)
|
||||||
import Data.Ord (Down(..))
|
import Data.Set (Set)
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Time.Clock (getCurrentTime)
|
import Data.Time.Clock (diffUTCTime, getCurrentTime)
|
||||||
import Data.Time.LocalTime (getCurrentTimeZone)
|
import Data.Time.LocalTime (getCurrentTimeZone)
|
||||||
import Directory.Events
|
import Directory.Events
|
||||||
import Directory.Options
|
import Directory.Options
|
||||||
|
import Directory.Search
|
||||||
import Directory.Store
|
import Directory.Store
|
||||||
import Simplex.Chat.Bot
|
import Simplex.Chat.Bot
|
||||||
import Simplex.Chat.Bot.KnownContacts
|
import Simplex.Chat.Bot.KnownContacts
|
||||||
|
@ -36,8 +36,10 @@ import Simplex.Chat.Messages
|
||||||
import Simplex.Chat.Options
|
import Simplex.Chat.Options
|
||||||
import Simplex.Chat.Protocol (MsgContent (..))
|
import Simplex.Chat.Protocol (MsgContent (..))
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
import Simplex.Chat.View (serializeChatResponse)
|
import Simplex.Chat.View (serializeChatResponse, simplexChatContact)
|
||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
|
import Simplex.Messaging.TMap (TMap)
|
||||||
|
import qualified Simplex.Messaging.TMap as TM
|
||||||
import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>))
|
import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>))
|
||||||
import System.Directory (getAppUserDataDirectory)
|
import System.Directory (getAppUserDataDirectory)
|
||||||
|
|
||||||
|
@ -55,6 +57,15 @@ data GroupRolesStatus
|
||||||
| GRSBadRoles
|
| GRSBadRoles
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
|
data ServiceState = ServiceState
|
||||||
|
{ searchRequests :: TMap ContactId SearchRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
newServiceState :: IO ServiceState
|
||||||
|
newServiceState = do
|
||||||
|
searchRequests <- atomically TM.empty
|
||||||
|
pure ServiceState {searchRequests}
|
||||||
|
|
||||||
welcomeGetOpts :: IO DirectoryOpts
|
welcomeGetOpts :: IO DirectoryOpts
|
||||||
welcomeGetOpts = do
|
welcomeGetOpts = do
|
||||||
appDir <- getAppUserDataDirectory "simplex"
|
appDir <- getAppUserDataDirectory "simplex"
|
||||||
|
@ -65,8 +76,9 @@ welcomeGetOpts = do
|
||||||
pure opts
|
pure opts
|
||||||
|
|
||||||
directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO ()
|
directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO ()
|
||||||
directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {userId} cc = do
|
directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testing} user@User {userId} cc = do
|
||||||
initializeBotAddress' (not testing) cc
|
initializeBotAddress' (not testing) cc
|
||||||
|
env <- newServiceState
|
||||||
race_ (forever $ void getLine) . forever $ do
|
race_ (forever $ void getLine) . forever $ do
|
||||||
(_, _, resp) <- atomically . readTBQueue $ outputQ cc
|
(_, _, resp) <- atomically . readTBQueue $ outputQ cc
|
||||||
forM_ (crDirectoryEvent resp) $ \case
|
forM_ (crDirectoryEvent resp) $ \case
|
||||||
|
@ -84,7 +96,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {
|
||||||
DEItemEditIgnored _ct -> pure ()
|
DEItemEditIgnored _ct -> pure ()
|
||||||
DEItemDeleteIgnored _ct -> pure ()
|
DEItemDeleteIgnored _ct -> pure ()
|
||||||
DEContactCommand ct ciId aCmd -> case aCmd of
|
DEContactCommand ct ciId aCmd -> case aCmd of
|
||||||
ADC SDRUser cmd -> deUserCommand ct ciId cmd
|
ADC SDRUser cmd -> deUserCommand env ct ciId cmd
|
||||||
ADC SDRSuperUser cmd -> deSuperUserCommand ct ciId cmd
|
ADC SDRSuperUser cmd -> deSuperUserCommand ct ciId cmd
|
||||||
where
|
where
|
||||||
withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId
|
withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId
|
||||||
|
@ -105,8 +117,11 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {
|
||||||
T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name."
|
T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name."
|
||||||
|
|
||||||
getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)])
|
getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)])
|
||||||
getGroups search =
|
getGroups = getGroups_ . Just
|
||||||
sendChatCmd cc (APIListGroups userId Nothing $ Just $ T.unpack search) >>= \case
|
|
||||||
|
getGroups_ :: Maybe Text -> IO (Maybe [(GroupInfo, GroupSummary)])
|
||||||
|
getGroups_ search_ =
|
||||||
|
sendChatCmd cc (APIListGroups userId Nothing $ T.unpack <$> search_) >>= \case
|
||||||
CRGroupsList {groups} -> pure $ Just groups
|
CRGroupsList {groups} -> pure $ Just groups
|
||||||
_ -> pure Nothing
|
_ -> pure Nothing
|
||||||
|
|
||||||
|
@ -140,7 +155,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {
|
||||||
sendMessage cc ct $
|
sendMessage cc ct $
|
||||||
"Welcome to " <> serviceName <> " service!\n\
|
"Welcome to " <> serviceName <> " service!\n\
|
||||||
\Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\
|
\Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\
|
||||||
\For example, send _privacy_ to find groups about privacy.\n\n\
|
\For example, send _privacy_ to find groups about privacy.\n\
|
||||||
|
\Or send */all* or */new* to list groups.\n\n\
|
||||||
\Content and privacy policy: https://simplex.chat/docs/directory.html"
|
\Content and privacy policy: https://simplex.chat/docs/directory.html"
|
||||||
|
|
||||||
deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO ()
|
deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO ()
|
||||||
|
@ -201,7 +217,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {
|
||||||
"Created the public link to join the group via this directory service that is always online.\n\n\
|
"Created the public link to join the group via this directory service that is always online.\n\n\
|
||||||
\Please add it to the group welcome message.\n\
|
\Please add it to the group welcome message.\n\
|
||||||
\For example, add:"
|
\For example, add:"
|
||||||
notifyOwner gr $ "Link to join the group " <> T.unpack displayName <> ": " <> B.unpack (strEncode connReqContact)
|
notifyOwner gr $ "Link to join the group " <> T.unpack displayName <> ": " <> B.unpack (strEncode $ simplexChatContact connReqContact)
|
||||||
CRChatCmdError _ (ChatError e) -> case e of
|
CRChatCmdError _ (ChatError e) -> case e of
|
||||||
CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin."
|
CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin."
|
||||||
CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group."
|
CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group."
|
||||||
|
@ -276,9 +292,10 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {
|
||||||
where
|
where
|
||||||
profileUpdate = \case
|
profileUpdate = \case
|
||||||
CRGroupLink {connReqContact} ->
|
CRGroupLink {connReqContact} ->
|
||||||
let groupLink = safeDecodeUtf8 $ strEncode connReqContact
|
let groupLink1 = safeDecodeUtf8 $ strEncode connReqContact
|
||||||
hadLinkBefore = groupLink `isInfix` description p
|
groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact
|
||||||
hasLinkNow = groupLink `isInfix` description p'
|
hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p
|
||||||
|
hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p'
|
||||||
in if
|
in if
|
||||||
| hadLinkBefore && hasLinkNow -> GPHasServiceLink
|
| hadLinkBefore && hasLinkNow -> GPHasServiceLink
|
||||||
| hadLinkBefore -> GPServiceLinkRemoved
|
| hadLinkBefore -> GPServiceLinkRemoved
|
||||||
|
@ -379,8 +396,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {
|
||||||
notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory."
|
notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory."
|
||||||
notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)."
|
notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)."
|
||||||
|
|
||||||
deUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO ()
|
deUserCommand :: ServiceState -> Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO ()
|
||||||
deUserCommand ct ciId = \case
|
deUserCommand env@ServiceState {searchRequests} ct ciId = \case
|
||||||
DCHelp ->
|
DCHelp ->
|
||||||
sendMessage cc ct $
|
sendMessage cc ct $
|
||||||
"You must be the owner to add the group to the directory:\n\
|
"You must be the owner to add the group to the directory:\n\
|
||||||
|
@ -389,20 +406,25 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {
|
||||||
\3. You will then need to add this link to the group welcome message.\n\
|
\3. You will then need to add this link to the group welcome message.\n\
|
||||||
\4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\
|
\4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\
|
||||||
\Start from inviting the bot to your group as admin - it will guide you through the process"
|
\Start from inviting the bot to your group as admin - it will guide you through the process"
|
||||||
DCSearchGroup s ->
|
DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s
|
||||||
getGroups s >>= \case
|
DCSearchNext ->
|
||||||
Just groups ->
|
atomically (TM.lookup (contactId' ct) searchRequests) >>= \case
|
||||||
atomically (filterListedGroups st groups) >>= \case
|
Just search@SearchRequest {searchType, searchTime} -> do
|
||||||
[] -> sendReply "No groups found"
|
currentTime <- getCurrentTime
|
||||||
gs -> do
|
if diffUTCTime currentTime searchTime > 300 -- 5 minutes
|
||||||
sendReply $ "Found " <> show (length gs) <> " group(s)" <> if length gs > 10 then ", sending 10." else ""
|
then do
|
||||||
void . forkIO $ forM_ (take 10 $ sortOn (Down . currentMembers . snd) gs) $
|
atomically $ TM.delete (contactId' ct) searchRequests
|
||||||
\(GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do
|
showAllGroups
|
||||||
let membersStr = "_" <> tshow currentMembers <> " members_"
|
else case searchType of
|
||||||
text = groupInfoText p <> "\n" <> membersStr
|
STSearch s -> withFoundListedGroups (Just s) $ sendNextSearchResults takeTop search
|
||||||
msg = maybe (MCText text) (\image -> MCImage {text, image}) image_
|
STAll -> withFoundListedGroups Nothing $ sendNextSearchResults takeTop search
|
||||||
sendComposedMessage cc ct Nothing msg
|
STRecent -> withFoundListedGroups Nothing $ sendNextSearchResults takeRecent search
|
||||||
Nothing -> sendReply "Error: getGroups. Please notify the developers."
|
Nothing -> showAllGroups
|
||||||
|
where
|
||||||
|
showAllGroups = deUserCommand env ct ciId DCAllGroups
|
||||||
|
DCAllGroups -> withFoundListedGroups Nothing $ sendAllGroups takeTop "top" STAll
|
||||||
|
DCRecentGroups -> withFoundListedGroups Nothing $ sendAllGroups takeRecent "the most recent" STRecent
|
||||||
|
DCSubmitGroup _link -> pure ()
|
||||||
DCConfirmDuplicateGroup ugrId gName ->
|
DCConfirmDuplicateGroup ugrId gName ->
|
||||||
atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case
|
atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case
|
||||||
Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found"
|
Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found"
|
||||||
|
@ -429,6 +451,54 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {
|
||||||
DCCommandError tag -> sendReply $ "Command error: " <> show tag
|
DCCommandError tag -> sendReply $ "Command error: " <> show tag
|
||||||
where
|
where
|
||||||
sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent
|
sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent
|
||||||
|
withFoundListedGroups s_ action =
|
||||||
|
getGroups_ s_ >>= \case
|
||||||
|
Just groups -> atomically (filterListedGroups st groups) >>= action
|
||||||
|
Nothing -> sendReply "Error: getGroups. Please notify the developers."
|
||||||
|
sendSearchResults s = \case
|
||||||
|
[] -> sendReply "No groups found"
|
||||||
|
gs -> do
|
||||||
|
let gs' = takeTop searchResults gs
|
||||||
|
moreGroups = length gs - length gs'
|
||||||
|
more = if moreGroups > 0 then ", sending top " <> show (length gs') else ""
|
||||||
|
sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "."
|
||||||
|
updateSearchRequest (STSearch s) $ groupIds gs'
|
||||||
|
sendFoundGroups gs' moreGroups
|
||||||
|
sendAllGroups takeFirst sortName searchType = \case
|
||||||
|
[] -> sendReply "No groups listed"
|
||||||
|
gs -> do
|
||||||
|
let gs' = takeFirst searchResults gs
|
||||||
|
moreGroups = length gs - length gs'
|
||||||
|
more = if moreGroups > 0 then ", sending " <> sortName <> " " <> show (length gs') else ""
|
||||||
|
sendReply $ show (length gs) <> " group(s) listed" <> more <> "."
|
||||||
|
updateSearchRequest searchType $ groupIds gs'
|
||||||
|
sendFoundGroups gs' moreGroups
|
||||||
|
sendNextSearchResults takeFirst SearchRequest {searchType, sentGroups} = \case
|
||||||
|
[] -> do
|
||||||
|
sendReply "Sorry, no more groups"
|
||||||
|
atomically $ TM.delete (contactId' ct) searchRequests
|
||||||
|
gs -> do
|
||||||
|
let gs' = takeFirst searchResults $ filterNotSent sentGroups gs
|
||||||
|
sentGroups' = sentGroups <> groupIds gs'
|
||||||
|
moreGroups = length gs - S.size sentGroups'
|
||||||
|
sendReply $ "Sending " <> show (length gs') <> " more group(s)."
|
||||||
|
updateSearchRequest searchType sentGroups'
|
||||||
|
sendFoundGroups gs' moreGroups
|
||||||
|
updateSearchRequest :: SearchType -> Set GroupId -> IO ()
|
||||||
|
updateSearchRequest searchType sentGroups = do
|
||||||
|
searchTime <- getCurrentTime
|
||||||
|
let search = SearchRequest {searchType, searchTime, sentGroups}
|
||||||
|
atomically $ TM.insert (contactId' ct) search searchRequests
|
||||||
|
sendFoundGroups gs moreGroups =
|
||||||
|
void . forkIO $ do
|
||||||
|
forM_ gs $
|
||||||
|
\(GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do
|
||||||
|
let membersStr = "_" <> tshow currentMembers <> " members_"
|
||||||
|
text = groupInfoText p <> "\n" <> membersStr
|
||||||
|
msg = maybe (MCText text) (\image -> MCImage {text, image}) image_
|
||||||
|
sendComposedMessage cc ct Nothing msg
|
||||||
|
when (moreGroups > 0) $
|
||||||
|
sendComposedMessage cc ct Nothing $ MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)."
|
||||||
|
|
||||||
deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO ()
|
deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO ()
|
||||||
deSuperUserCommand ct ciId cmd
|
deSuperUserCommand ct ciId cmd
|
||||||
|
|
|
@ -467,6 +467,7 @@ executable simplex-directory-service
|
||||||
other-modules:
|
other-modules:
|
||||||
Directory.Events
|
Directory.Events
|
||||||
Directory.Options
|
Directory.Options
|
||||||
|
Directory.Search
|
||||||
Directory.Service
|
Directory.Service
|
||||||
Directory.Store
|
Directory.Store
|
||||||
Paths_simplex_chat
|
Paths_simplex_chat
|
||||||
|
@ -553,6 +554,7 @@ test-suite simplex-chat-test
|
||||||
Broadcast.Options
|
Broadcast.Options
|
||||||
Directory.Events
|
Directory.Events
|
||||||
Directory.Options
|
Directory.Options
|
||||||
|
Directory.Search
|
||||||
Directory.Service
|
Directory.Service
|
||||||
Directory.Store
|
Directory.Store
|
||||||
Paths_simplex_chat
|
Paths_simplex_chat
|
||||||
|
|
|
@ -30,6 +30,7 @@ directoryServiceTests = do
|
||||||
it "should suspend and resume group" testSuspendResume
|
it "should suspend and resume group" testSuspendResume
|
||||||
it "should join found group via link" testJoinGroup
|
it "should join found group via link" testJoinGroup
|
||||||
it "should support group names with spaces" testGroupNameWithSpaces
|
it "should support group names with spaces" testGroupNameWithSpaces
|
||||||
|
it "should return more groups in search, all and recent groups" testSearchGroups
|
||||||
describe "de-listing the group" $ do
|
describe "de-listing the group" $ do
|
||||||
it "should de-list if owner leaves the group" testDelistedOwnerLeaves
|
it "should de-list if owner leaves the group" testDelistedOwnerLeaves
|
||||||
it "should de-list if owner is removed from the group" testDelistedOwnerRemoved
|
it "should de-list if owner is removed from the group" testDelistedOwnerRemoved
|
||||||
|
@ -67,6 +68,7 @@ mkDirectoryOpts tmp superUsers =
|
||||||
superUsers,
|
superUsers,
|
||||||
directoryLog = Just $ tmp </> "directory_service.log",
|
directoryLog = Just $ tmp </> "directory_service.log",
|
||||||
serviceName = "SimpleX-Directory",
|
serviceName = "SimpleX-Directory",
|
||||||
|
searchResults = 3,
|
||||||
testing = True
|
testing = True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +160,7 @@ testDirectoryService tmp =
|
||||||
search u s welcome = do
|
search u s welcome = do
|
||||||
u #> ("@SimpleX-Directory " <> s)
|
u #> ("@SimpleX-Directory " <> s)
|
||||||
u <# ("SimpleX-Directory> > " <> s)
|
u <# ("SimpleX-Directory> > " <> s)
|
||||||
u <## " Found 1 group(s)"
|
u <## " Found 1 group(s)."
|
||||||
u <# "SimpleX-Directory> PSA (Privacy, Security & Anonymity)"
|
u <# "SimpleX-Directory> PSA (Privacy, Security & Anonymity)"
|
||||||
u <## "Welcome message:"
|
u <## "Welcome message:"
|
||||||
u <## welcome
|
u <## welcome
|
||||||
|
@ -206,7 +208,7 @@ testJoinGroup tmp =
|
||||||
cath `connectVia` dsLink
|
cath `connectVia` dsLink
|
||||||
cath #> "@SimpleX-Directory privacy"
|
cath #> "@SimpleX-Directory privacy"
|
||||||
cath <# "SimpleX-Directory> > privacy"
|
cath <# "SimpleX-Directory> > privacy"
|
||||||
cath <## " Found 1 group(s)"
|
cath <## " Found 1 group(s)."
|
||||||
cath <# "SimpleX-Directory> privacy (Privacy)"
|
cath <# "SimpleX-Directory> privacy (Privacy)"
|
||||||
cath <## "Welcome message:"
|
cath <## "Welcome message:"
|
||||||
welcomeMsg <- getTermLine cath
|
welcomeMsg <- getTermLine cath
|
||||||
|
@ -263,6 +265,92 @@ testGroupNameWithSpaces tmp =
|
||||||
bob <# "SimpleX-Directory> The group ID 1 (Privacy & Security) is listed in the directory again!"
|
bob <# "SimpleX-Directory> The group ID 1 (Privacy & Security) is listed in the directory again!"
|
||||||
groupFound bob "Privacy & Security"
|
groupFound bob "Privacy & Security"
|
||||||
|
|
||||||
|
testSearchGroups :: HasCallStack => FilePath -> IO ()
|
||||||
|
testSearchGroups tmp =
|
||||||
|
withDirectoryService tmp $ \superUser dsLink ->
|
||||||
|
withNewTestChat tmp "bob" bobProfile $ \bob -> do
|
||||||
|
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||||
|
bob `connectVia` dsLink
|
||||||
|
cath `connectVia` dsLink
|
||||||
|
forM_ [1..8 :: Int] $ \i -> registerGroupId superUser bob (groups !! (i - 1)) "" i i
|
||||||
|
connectUsers bob cath
|
||||||
|
fullAddMember "MyGroup" "" bob cath GRMember
|
||||||
|
joinGroup "MyGroup" cath bob
|
||||||
|
cath <## "#MyGroup: member SimpleX-Directory_1 is connected"
|
||||||
|
cath <## "contact and member are merged: SimpleX-Directory, #MyGroup SimpleX-Directory_1"
|
||||||
|
cath <## "use @SimpleX-Directory <message> to send messages"
|
||||||
|
cath #> "@SimpleX-Directory MyGroup"
|
||||||
|
cath <# "SimpleX-Directory> > MyGroup"
|
||||||
|
cath <## " Found 7 group(s), sending top 3."
|
||||||
|
receivedGroup cath 0 3
|
||||||
|
receivedGroup cath 1 2
|
||||||
|
receivedGroup cath 2 2
|
||||||
|
cath <# "SimpleX-Directory> Send /next or just . for 4 more result(s)."
|
||||||
|
cath #> "@SimpleX-Directory /next"
|
||||||
|
cath <# "SimpleX-Directory> > /next"
|
||||||
|
cath <## " Sending 3 more group(s)."
|
||||||
|
receivedGroup cath 3 2
|
||||||
|
receivedGroup cath 4 2
|
||||||
|
receivedGroup cath 5 2
|
||||||
|
cath <# "SimpleX-Directory> Send /next or just . for 1 more result(s)."
|
||||||
|
-- search of another user does not affect the search of the first user
|
||||||
|
groupFound bob "Another"
|
||||||
|
cath #> "@SimpleX-Directory ."
|
||||||
|
cath <# "SimpleX-Directory> > ."
|
||||||
|
cath <## " Sending 1 more group(s)."
|
||||||
|
receivedGroup cath 6 2
|
||||||
|
cath #> "@SimpleX-Directory /all"
|
||||||
|
cath <# "SimpleX-Directory> > /all"
|
||||||
|
cath <## " 8 group(s) listed, sending top 3."
|
||||||
|
receivedGroup cath 0 3
|
||||||
|
receivedGroup cath 1 2
|
||||||
|
receivedGroup cath 2 2
|
||||||
|
cath <# "SimpleX-Directory> Send /next or just . for 5 more result(s)."
|
||||||
|
cath #> "@SimpleX-Directory /new"
|
||||||
|
cath <# "SimpleX-Directory> > /new"
|
||||||
|
cath <## " 8 group(s) listed, sending the most recent 3."
|
||||||
|
receivedGroup cath 7 2
|
||||||
|
receivedGroup cath 6 2
|
||||||
|
receivedGroup cath 5 2
|
||||||
|
cath <# "SimpleX-Directory> Send /next or just . for 5 more result(s)."
|
||||||
|
cath #> "@SimpleX-Directory term3"
|
||||||
|
cath <# "SimpleX-Directory> > term3"
|
||||||
|
cath <## " Found 3 group(s)."
|
||||||
|
receivedGroup cath 4 2
|
||||||
|
receivedGroup cath 5 2
|
||||||
|
receivedGroup cath 6 2
|
||||||
|
cath #> "@SimpleX-Directory term1"
|
||||||
|
cath <# "SimpleX-Directory> > term1"
|
||||||
|
cath <## " Found 6 group(s), sending top 3."
|
||||||
|
receivedGroup cath 1 2
|
||||||
|
receivedGroup cath 2 2
|
||||||
|
receivedGroup cath 3 2
|
||||||
|
cath <# "SimpleX-Directory> Send /next or just . for 3 more result(s)."
|
||||||
|
cath #> "@SimpleX-Directory ."
|
||||||
|
cath <# "SimpleX-Directory> > ."
|
||||||
|
cath <## " Sending 3 more group(s)."
|
||||||
|
receivedGroup cath 4 2
|
||||||
|
receivedGroup cath 5 2
|
||||||
|
receivedGroup cath 6 2
|
||||||
|
where
|
||||||
|
groups :: [String]
|
||||||
|
groups =
|
||||||
|
[ "MyGroup",
|
||||||
|
"MyGroup term1 1",
|
||||||
|
"MyGroup term1 2",
|
||||||
|
"MyGroup term1 term2",
|
||||||
|
"MyGroup term1 term2 term3",
|
||||||
|
"MyGroup term1 term2 term3 term4",
|
||||||
|
"MyGroup term1 term2 term3 term4 term5",
|
||||||
|
"Another"
|
||||||
|
]
|
||||||
|
receivedGroup :: TestCC -> Int -> Int -> IO ()
|
||||||
|
receivedGroup u ix count = do
|
||||||
|
u <#. ("SimpleX-Directory> " <> groups !! ix)
|
||||||
|
u <## "Welcome message:"
|
||||||
|
u <##. "Link to join the group "
|
||||||
|
u <## (show count <> " members")
|
||||||
|
|
||||||
testDelistedOwnerLeaves :: HasCallStack => FilePath -> IO ()
|
testDelistedOwnerLeaves :: HasCallStack => FilePath -> IO ()
|
||||||
testDelistedOwnerLeaves tmp =
|
testDelistedOwnerLeaves tmp =
|
||||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||||
|
@ -930,6 +1018,7 @@ u `connectVia` dsLink = do
|
||||||
u <## "Send a search string to find groups or /help to learn how to add groups to directory."
|
u <## "Send a search string to find groups or /help to learn how to add groups to directory."
|
||||||
u <## ""
|
u <## ""
|
||||||
u <## "For example, send privacy to find groups about privacy."
|
u <## "For example, send privacy to find groups about privacy."
|
||||||
|
u <## "Or send /all or /new to list groups."
|
||||||
u <## ""
|
u <## ""
|
||||||
u <## "Content and privacy policy: https://simplex.chat/docs/directory.html"
|
u <## "Content and privacy policy: https://simplex.chat/docs/directory.html"
|
||||||
|
|
||||||
|
@ -967,7 +1056,7 @@ groupFoundN :: Int -> TestCC -> String -> IO ()
|
||||||
groupFoundN count u name = do
|
groupFoundN count u name = do
|
||||||
u #> ("@SimpleX-Directory " <> name)
|
u #> ("@SimpleX-Directory " <> name)
|
||||||
u <# ("SimpleX-Directory> > " <> name)
|
u <# ("SimpleX-Directory> > " <> name)
|
||||||
u <## " Found 1 group(s)"
|
u <## " Found 1 group(s)."
|
||||||
u <#. ("SimpleX-Directory> " <> name)
|
u <#. ("SimpleX-Directory> " <> name)
|
||||||
u <## "Welcome message:"
|
u <## "Welcome message:"
|
||||||
u <##. "Link to join the group "
|
u <##. "Link to join the group "
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue