mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-27 08:35:58 +00:00
docs: bots API (#6091)
* docs: bot API commands * generate API commands doc * generate commands docs with parameters and responses * add API types * more types * document all types (with some deviations from JSON encodings) * rename types * interface objects * separator * command syntax * more syntax * API events * event types * fix all type definitions * pre-process types outside of rendering * pre-process event types * overview * pre-process commands * param syntax WIP * syntax for types in command parameters * API error response and chat event * remove unsupported/deprecated command parameters * reorder * syntax for choice * show command errors * event descriptions * python syntax for commands and types (#6099) * python syntax for commands and types * python snippets: convert numbers to string * fixes * update readme, enable all tests * fix operators test * update plans --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
@@ -114,9 +114,8 @@ Read about the app features and settings in the new [User guide](./docs/guide/RE
|
||||
|
||||
We would love to have you join the development! You can help us with:
|
||||
|
||||
- [share the color theme](./docs/THEMES.md) you use in Android app!
|
||||
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
||||
- contributing to SimpleX Chat knowledge-base.
|
||||
- [develop a chat bot](#develop-a-chat-bot) for SimpleX Chat!
|
||||
- writing a tutorial or recipes about hosting servers, chat bots, etc.
|
||||
- developing features - please connect to us via chat so we can help you get started.
|
||||
|
||||
## Help translating SimpleX Chat
|
||||
@@ -194,6 +193,7 @@ SimpleX Chat founder
|
||||
- [SimpleX Platform design](#simplex-platform-design)
|
||||
- [Privacy and security: technical details and limitations](#privacy-and-security-technical-details-and-limitations)
|
||||
- [For developers](#for-developers)
|
||||
- [Develop a chat bot](#develop-a-chat-bot)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Disclaimers, Security contact, License](#disclaimers)
|
||||
|
||||
@@ -325,15 +325,23 @@ We plan to add:
|
||||
|
||||
You can:
|
||||
|
||||
- [create chat bots and services](#develop-a-chat-bot).
|
||||
- run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution.
|
||||
- use SimpleX Chat library to integrate chat functionality into your mobile apps.
|
||||
- create chat bots and services in Haskell - see [simple](./apps/simplex-bot/) and more [advanced chat bot example](./apps/simplex-bot-advanced/).
|
||||
- create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
|
||||
- run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution.
|
||||
|
||||
If you are considering developing with SimpleX platform please get in touch for any advice and support.
|
||||
|
||||
Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) group to ask any questions and share your success stories.
|
||||
|
||||
## Develop a chat bot
|
||||
|
||||
You can create a chat bot or any chat-based service in any language running SimpleX Chat terminal CLI as a local WebSocket server.
|
||||
|
||||
See [our new bot API reference](./bots/README.md). Most of it is automatically generated from core library types, so it stays up to date.
|
||||
|
||||
Also see [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
|
||||
|
||||
## Roadmap
|
||||
|
||||
- ✅ Easy to deploy SimpleX server with in-memory message storage, without any dependencies.
|
||||
|
||||
@@ -921,8 +921,6 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
// sending file responses
|
||||
case sndFileCancelled(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
|
||||
case sndStandaloneFileCreated(user: UserRef, fileTransferMeta: FileTransferMeta) // returned by _upload
|
||||
case sndFileStartXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) // not used
|
||||
case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
|
||||
// call invitations
|
||||
case callInvitations(callInvitations: [RcvCallInvitation])
|
||||
// notifications
|
||||
@@ -969,8 +967,6 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
case .rcvFileCancelled: "rcvFileCancelled"
|
||||
case .sndFileCancelled: "sndFileCancelled"
|
||||
case .sndStandaloneFileCreated: "sndStandaloneFileCreated"
|
||||
case .sndFileStartXFTP: "sndFileStartXFTP"
|
||||
case .sndFileCancelledXFTP: "sndFileCancelledXFTP"
|
||||
case .callInvitations: "callInvitations"
|
||||
case .ntfTokenStatus: "ntfTokenStatus"
|
||||
case .ntfToken: "ntfToken"
|
||||
@@ -1015,8 +1011,6 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
|
||||
case .sndStandaloneFileCreated: return noDetails
|
||||
case let .sndFileStartXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .callInvitations(invs): return String(describing: invs)
|
||||
case let .ntfTokenStatus(status): return String(describing: status)
|
||||
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)"
|
||||
|
||||
@@ -2539,7 +2539,7 @@ func processReceivedMsg(_ res: ChatEvent) async {
|
||||
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
// TODO when aChatItem added
|
||||
// case let .rcvFileAcceptedSndCancelled(user, aChatItem, _): // usually rcvFileAcceptedSndCancelled is a response, but it's also an event for XFTP files auto-accepted from NSE
|
||||
// case let .rcvFileAcceptedSndCancelled(user, aChatItem, _): // usually rcvFileAcceptedSndCancelled is a response, but it's also an event for legacy files auto-accepted from NSE.
|
||||
// await chatItemSimpleUpdate(user, aChatItem)
|
||||
// Task { cleanupFile(aChatItem) }
|
||||
case let .rcvFileStart(user, aChatItem):
|
||||
|
||||
-6
@@ -6144,12 +6144,10 @@ sealed class CR {
|
||||
@Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: UserRef, val chatItem_: AChatItem?, val sndFileTransfer: SndFileTransfer): CR()
|
||||
@Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR()
|
||||
@Serializable @SerialName("sndStandaloneFileCreated") class SndStandaloneFileCreated(val user: UserRef, val fileTransferMeta: FileTransferMeta): CR() // returned by _upload
|
||||
@Serializable @SerialName("sndFileStartXFTP") class SndFileStartXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() // not used
|
||||
@Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR()
|
||||
@Serializable @SerialName("sndFileRedirectStartXFTP") class SndFileRedirectStartXFTP(val user: UserRef, val fileTransferMeta: FileTransferMeta, val redirectMeta: FileTransferMeta): CR()
|
||||
@Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR()
|
||||
@Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List<String>): CR()
|
||||
@Serializable @SerialName("sndFileCancelledXFTP") class SndFileCancelledXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
|
||||
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR()
|
||||
@Serializable @SerialName("sndFileWarning") class SndFileWarning(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR()
|
||||
// call events
|
||||
@@ -6319,7 +6317,6 @@ sealed class CR {
|
||||
is RcvStandaloneFileComplete -> "rcvStandaloneFileComplete"
|
||||
is RcvFileCancelled -> "rcvFileCancelled"
|
||||
is SndStandaloneFileCreated -> "sndStandaloneFileCreated"
|
||||
is SndFileStartXFTP -> "sndFileStartXFTP"
|
||||
is RcvFileSndCancelled -> "rcvFileSndCancelled"
|
||||
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
|
||||
is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP"
|
||||
@@ -6332,7 +6329,6 @@ sealed class CR {
|
||||
is SndFileProgressXFTP -> "sndFileProgressXFTP"
|
||||
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
|
||||
is SndStandaloneFileComplete -> "sndStandaloneFileComplete"
|
||||
is SndFileCancelledXFTP -> "sndFileCancelledXFTP"
|
||||
is SndFileError -> "sndFileError"
|
||||
is SndFileWarning -> "sndFileWarning"
|
||||
is CallInvitations -> "callInvitations"
|
||||
@@ -6502,7 +6498,6 @@ sealed class CR {
|
||||
is RcvFileWarning -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer")
|
||||
is SndFileCancelled -> json.encodeToString(chatItem_)
|
||||
is SndStandaloneFileCreated -> noDetails()
|
||||
is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndFileComplete -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem_))
|
||||
is SndFileStart -> withUser(user, json.encodeToString(chatItem))
|
||||
@@ -6510,7 +6505,6 @@ sealed class CR {
|
||||
is SndFileRedirectStartXFTP -> withUser(user, json.encodeToString(redirectMeta))
|
||||
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString())
|
||||
is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_))
|
||||
is SndFileError -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}")
|
||||
is SndFileWarning -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}")
|
||||
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
|
||||
|
||||
@@ -19,14 +19,14 @@ data SearchRequest = SearchRequest
|
||||
|
||||
data SearchType = STAll | STRecent | STSearch Text
|
||||
|
||||
takeTop :: Int -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)]
|
||||
takeTop n = take n . sortOn (Down . currentMembers . snd)
|
||||
takeTop :: Int -> [GroupInfoSummary] -> [GroupInfoSummary]
|
||||
takeTop n = take n . sortOn (\(GIS _ GroupSummary {currentMembers}) -> Down currentMembers)
|
||||
|
||||
takeRecent :: Int -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)]
|
||||
takeRecent n = take n . sortOn (Down . (\GroupInfo {createdAt} -> createdAt) . fst)
|
||||
takeRecent :: Int -> [GroupInfoSummary] -> [GroupInfoSummary]
|
||||
takeRecent n = take n . sortOn (\(GIS GroupInfo {createdAt} _) -> Down createdAt)
|
||||
|
||||
groupIds :: [(GroupInfo, GroupSummary)] -> Set GroupId
|
||||
groupIds = S.fromList . map (\(GroupInfo {groupId}, _) -> groupId)
|
||||
groupIds :: [GroupInfoSummary] -> Set GroupId
|
||||
groupIds = S.fromList . map (\(GIS GroupInfo {groupId} _) -> groupId)
|
||||
|
||||
filterNotSent :: Set GroupId -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)]
|
||||
filterNotSent sentGroups = filter (\(GroupInfo {groupId}, _) -> groupId `S.notMember` sentGroups)
|
||||
filterNotSent :: Set GroupId -> [GroupInfoSummary] -> [GroupInfoSummary]
|
||||
filterNotSent sentGroups = filter (\(GIS GroupInfo {groupId} _) -> groupId `S.notMember` sentGroups)
|
||||
|
||||
@@ -248,10 +248,10 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
groupAlreadyListed GroupInfo {groupProfile = p} =
|
||||
"The group " <> groupNameDescr p <> " is already listed in the directory, please choose another name."
|
||||
|
||||
getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)])
|
||||
getGroups :: Text -> IO (Maybe [GroupInfoSummary])
|
||||
getGroups = getGroups_ . Just
|
||||
|
||||
getGroups_ :: Maybe Text -> IO (Maybe [(GroupInfo, GroupSummary)])
|
||||
getGroups_ :: Maybe Text -> IO (Maybe [GroupInfoSummary])
|
||||
getGroups_ search_ =
|
||||
sendChatCmd cc (APIListGroups userId Nothing $ T.unpack <$> search_) >>= \case
|
||||
Right CRGroupsList {groups} -> pure $ Just groups
|
||||
@@ -261,7 +261,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
getDuplicateGroup GroupInfo {groupId, groupProfile = GroupProfile {displayName, fullName}} =
|
||||
getGroups fullName >>= mapM duplicateGroup
|
||||
where
|
||||
sameGroupNotRemoved (g@GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}}, _) =
|
||||
sameGroupNotRemoved (GIS g@GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}} _) =
|
||||
gId /= groupId && n == displayName && fn == fullName && not (memberRemoved $ membership g)
|
||||
duplicateGroup [] = pure DGUnique
|
||||
duplicateGroup groups = do
|
||||
@@ -270,13 +270,13 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
then pure DGUnique
|
||||
else do
|
||||
(lgs, rgs) <- atomically $ (,) <$> readTVar (listedGroups st) <*> readTVar (reservedGroups st)
|
||||
let reserved = any (\(GroupInfo {groupId = gId}, _) -> gId `S.member` lgs || gId `S.member` rgs) gs
|
||||
let reserved = any (\(GIS GroupInfo {groupId = gId} _) -> gId `S.member` lgs || gId `S.member` rgs) gs
|
||||
if reserved
|
||||
then pure DGReserved
|
||||
else do
|
||||
removed <- foldM (\r -> fmap (r &&) . isGroupRemoved) True gs
|
||||
pure $ if removed then DGUnique else DGRegistered
|
||||
isGroupRemoved (GroupInfo {groupId = gId}, _) =
|
||||
isGroupRemoved (GIS GroupInfo {groupId = gId} _) =
|
||||
getGroupReg st gId >>= \case
|
||||
Just GroupReg {groupRegStatus} -> groupRemoved <$> readTVarIO groupRegStatus
|
||||
Nothing -> pure True
|
||||
@@ -819,7 +819,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
where
|
||||
msgs = replyMsg :| map foundGroup gs <> [moreMsg | moreGroups > 0]
|
||||
replyMsg = (Just ciId, MCText reply)
|
||||
foundGroup (GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) =
|
||||
foundGroup (GIS GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}} GroupSummary {currentMembers}) =
|
||||
let membersStr = "_" <> tshow currentMembers <> " members_"
|
||||
showId = if isAdmin then tshow groupId <> ". " else ""
|
||||
text = showId <> groupInfoText p <> "\n" <> membersStr
|
||||
|
||||
@@ -242,10 +242,10 @@ getUserGroupReg st ctId ugrId = find (\r -> ctId == dbContactId r && ugrId == us
|
||||
getUserGroupRegs :: DirectoryStore -> ContactId -> IO [GroupReg]
|
||||
getUserGroupRegs st ctId = filter ((ctId ==) . dbContactId) <$> readTVarIO (groupRegs st)
|
||||
|
||||
filterListedGroups :: DirectoryStore -> [(GroupInfo, GroupSummary)] -> IO [(GroupInfo, GroupSummary)]
|
||||
filterListedGroups :: DirectoryStore -> [GroupInfoSummary] -> IO [GroupInfoSummary]
|
||||
filterListedGroups st gs = do
|
||||
lgs <- readTVarIO $ listedGroups st
|
||||
pure $ filter (\(GroupInfo {groupId}, _) -> groupId `S.member` lgs) gs
|
||||
pure $ filter (\(GIS GroupInfo {groupId} _) -> groupId `S.member` lgs) gs
|
||||
|
||||
listGroup :: DirectoryStore -> GroupId -> STM ()
|
||||
listGroup st gId = do
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
# SimpleX Chat bot API
|
||||
|
||||
- [Why create a bot](#why-create-a-bot)
|
||||
- [How to create a bot](#how-to-create-a-bot)
|
||||
- [Sending commands](#sending-commands)
|
||||
- [Processing events](#processing-events)
|
||||
- [Security considerations](#security-considerations)
|
||||
- [Useful bots](#useful-bots)
|
||||
- [API types reference](./api/README.md) (another page)
|
||||
|
||||
|
||||
## Why create a bot
|
||||
|
||||
You can implement SimpleX Chat for these and many other scenarios:
|
||||
- customer support - both as a single- and a multi-agent support chat (using SimpleX Chat [business address]() feature),
|
||||
- information search and retrieval bots, with or without LLM integration,
|
||||
- broadcast bot, when messages from your trusted users are forwarded to all connected contacts - e.g., see our SimpleX Status bot in the app ([source code](../apps/simplex-broadcast-bot/)),
|
||||
- feedback bot, when messages from connected contacts are forwarded to a preset list of your trusted users,
|
||||
- P2P trading bots, connecting buyers and sellers,
|
||||
- etc.
|
||||
|
||||
We will share all useful bots you create in the bottom of this page - please submit a PR to add it.
|
||||
|
||||
|
||||
## How to create a bot
|
||||
|
||||
[SimpleX Chat CLI](../docs/CLI.md) can be run as a local WebSockets server on any port:
|
||||
|
||||
```bash
|
||||
simplex-chat -p 5225
|
||||
```
|
||||
|
||||
To see all supported parameters:
|
||||
|
||||
```bash
|
||||
simplex-chat -h
|
||||
```
|
||||
|
||||
Your bot must run as a standalone process connecting to CLI via WebSockets on the chosen port. See [Security considerations](#security-considerations) about connecting your bot process to CLI.
|
||||
|
||||
All communication between your bot process and CLI happens via JSON-encoded WebSocket text messages.
|
||||
|
||||
To connect to other SimpleX Chat users and to send messages the bot must send commands to CLI. The command WebSocket message contains correlation ID and commands as strings.
|
||||
|
||||
CLI will respond to command messages with command processing results. The response WebSocket message contains the same correlation ID as was sent in the command and JSON-encoded response record.
|
||||
|
||||
See [Sending commands](#sending-commands) about message formats and types for commands and responses.
|
||||
|
||||
CLI will also send chat events to your bot process. These events represent information about connecting SimpleX Chat users, received messages, etc.
|
||||
|
||||
See [Processing events](#processing-events) about event message format and types.
|
||||
|
||||
In most cases, the bot needs to have a pre-configured user profile and SimpleX address, configured to automatically accept incoming contact requests from all users. It is simpler to do it manually via desktop client and then use this chat database with your bot. But it can also be done programmatically when bot starts.
|
||||
|
||||
In the simplest case, your bot must process [NewChatItems](./api/EVENTS.md#newchatitems) event to receive messages from connected users and use [APISendMessages](./api/COMMANDS.md#apisendmessages) command to respond to them.
|
||||
|
||||
|
||||
## Sending commands
|
||||
|
||||
CLI WebSockets API allows to:
|
||||
|
||||
- send and receive messages and files.
|
||||
- create and change user profile - you also can do it manually, via SimpleX Chat desktop app or CLI.
|
||||
- create and accept invitations or connect with the contacts.
|
||||
- create and manage long-term user address, accepting connection requests automatically or via code.
|
||||
- create, join and manage group.
|
||||
|
||||
Each command your bot sends to CLI should have this JSON format:
|
||||
|
||||
```json
|
||||
{
|
||||
"corrId": "<any unique string>",
|
||||
"cmd": "<command string>"
|
||||
}
|
||||
```
|
||||
|
||||
You can use sequential numbers, UUIDs or some other unique strings in `corrId` field.
|
||||
|
||||
Command strings are the same commands you can can see in `Settings / Developer tools / Chat Console` of mobile and desktop apps. You can test these commands via SimpleX Chat CLI.
|
||||
|
||||
When command is processed, CLI will send a response as a WebSockets message in this format:
|
||||
|
||||
```json
|
||||
{
|
||||
"corrId": "<corrId sent with a command>",
|
||||
"resp": {
|
||||
"type": "<response record tag>",
|
||||
"other response fields": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`corrId` will be the same as you used in commands. Your bot must maintain the map of pending commands responses, and can implement an internal callback or async API for convenience. See our [TypeScript bot library](../packages/simplex-chat-client/typescript/README.md) for an example. TypeScript library sends commands sequentially, via a queue, but your bot can send commands concurrently.
|
||||
|
||||
`resp` field is a command-specific response in JSON format. All command responses form a discriminated union with `type` field as a tag.
|
||||
|
||||
See [API Commands and Responses](./api/COMMANDS.md) reference about specific command strings and JSON types for command responses. As CLI has the same API as used by mobile and desktop apps, it supports other commands not included in the reference.
|
||||
|
||||
*Please note*: CLI uses network connection for most API commands. Command network usage is included in the reference:
|
||||
- "no" - command doesn't use network,
|
||||
- "interactive" - all or some network requests will complete before command response is sent to the bot,
|
||||
- "background" - command response will be sent to the bot before scheduled network requests are sent.
|
||||
|
||||
## Processing events
|
||||
|
||||
Chat event is a WebSocket message in this format:
|
||||
|
||||
```json
|
||||
{
|
||||
"resp": {
|
||||
"type": "<event record tag>",
|
||||
"other event fields": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
While it uses the same `resp` property as responses for backward compatibility, the event type is a different discriminated union. Some record types are used both as a command response and an event. The most important example is `NewChatItems` that can be sent both as a response to [APISendMessages](./api/COMMANDS.md#apisendmessages) with correlation ID (when message is scheduled for delivery) and as [events](./api/EVENTS.md#newchatitems) when messages are received.
|
||||
|
||||
See [API Events](./api/EVENTS.md) reference about specific JSON types for chat events. CLI can send other events not included in the reference.
|
||||
|
||||
*Please note*: Your bot must allow and ignore all events it does not process, it should not fail when it encounters undocumented event types. Your bot JSON parser must allow additional properties in all types, and must allow and ignore records with unknown union tags and unknown enum strings.
|
||||
|
||||
|
||||
## Security considerations
|
||||
|
||||
WebSockets API of SimpleX Chat CLI does not support any authentication. CLI binds only to localhost to prevent accidental access from public network, in case you did not close this port in firewall. The messages in WebSocket API are not encrypted in any way, and must not be sent via public networks.
|
||||
|
||||
It is usually simpler to run your bot process on the same machine where you run SimpleX Chat CLI, and to close CLI port in firewall. That makes connection between your bot and CLI secure. It also simplifies sending and receiving files via bot, as they are stored on the file system accessible to SimpleX Chat CLI.
|
||||
|
||||
If you have to run your bot on another machine, you need to secure access to bot CLI via any web proxy that supports WebSockets, e.g. Caddy or Nginx. You must configure TLS termination in the proxy and connect CLI process from bot via a secure TLS connection. If you connect to bot via a public network, you also must configure HTTP basic auth to prevent unauthorized access. You can validate TLS security of your proxy via a free test at [SSLLabs.com](https://www.ssllabs.com/ssltest/). You can also configure firewall on the machine where you run SimpleX CLI to only allow connections from the IP address of your bot.
|
||||
|
||||
|
||||
## Useful bots
|
||||
|
||||
- [Broadcast bot](../apps/simplex-broadcast-bot/) (Haskell) - we use it to send [status and release updates](https://status.simplex.chat/status/public).
|
||||
- [Moderation bot](https://github.com/NCalex42/simplex-bot) (Java)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,725 @@
|
||||
# API Events
|
||||
|
||||
This file is generated automatically.
|
||||
|
||||
[Contact connection events](#contact-connection-events)
|
||||
- Main event
|
||||
- [ContactConnected](#contactconnected)
|
||||
- Other events
|
||||
- [ContactUpdated](#contactupdated)
|
||||
- [ContactDeletedByContact](#contactdeletedbycontact)
|
||||
- [ReceivedContactRequest](#receivedcontactrequest)
|
||||
- [NewMemberContactReceivedInv](#newmembercontactreceivedinv)
|
||||
- [ContactSndReady](#contactsndready)
|
||||
|
||||
[Message events](#message-events)
|
||||
- Main event
|
||||
- [NewChatItems](#newchatitems)
|
||||
- Other events
|
||||
- [ChatItemReaction](#chatitemreaction)
|
||||
- [ChatItemsDeleted](#chatitemsdeleted)
|
||||
- [ChatItemUpdated](#chatitemupdated)
|
||||
- [GroupChatItemsDeleted](#groupchatitemsdeleted)
|
||||
- [ChatItemsStatusesUpdated](#chatitemsstatusesupdated)
|
||||
|
||||
[Group events](#group-events)
|
||||
- Main events
|
||||
- [ReceivedGroupInvitation](#receivedgroupinvitation)
|
||||
- [UserJoinedGroup](#userjoinedgroup)
|
||||
- [GroupUpdated](#groupupdated)
|
||||
- [JoinedGroupMember](#joinedgroupmember)
|
||||
- [MemberRole](#memberrole)
|
||||
- [DeletedMember](#deletedmember)
|
||||
- [LeftMember](#leftmember)
|
||||
- [DeletedMemberUser](#deletedmemberuser)
|
||||
- [GroupDeleted](#groupdeleted)
|
||||
- Other events
|
||||
- [ConnectedToGroupMember](#connectedtogroupmember)
|
||||
- [MemberAcceptedByOther](#memberacceptedbyother)
|
||||
- [MemberBlockedForAll](#memberblockedforall)
|
||||
- [GroupMemberUpdated](#groupmemberupdated)
|
||||
|
||||
[File events](#file-events)
|
||||
- Main events
|
||||
- [RcvFileDescrReady](#rcvfiledescrready)
|
||||
- [RcvFileComplete](#rcvfilecomplete)
|
||||
- [SndFileCompleteXFTP](#sndfilecompletexftp)
|
||||
- Other events
|
||||
- [RcvFileStart](#rcvfilestart)
|
||||
- [RcvFileSndCancelled](#rcvfilesndcancelled)
|
||||
- [RcvFileAccepted](#rcvfileaccepted)
|
||||
- [RcvFileError](#rcvfileerror)
|
||||
- [RcvFileWarning](#rcvfilewarning)
|
||||
- [SndFileError](#sndfileerror)
|
||||
- [SndFileWarning](#sndfilewarning)
|
||||
|
||||
[Connection progress events](#connection-progress-events)
|
||||
- [AcceptingContactRequest](#acceptingcontactrequest)
|
||||
- [AcceptingBusinessRequest](#acceptingbusinessrequest)
|
||||
- [ContactConnecting](#contactconnecting)
|
||||
- [BusinessLinkConnecting](#businesslinkconnecting)
|
||||
- [JoinedGroupMemberConnecting](#joinedgroupmemberconnecting)
|
||||
- [SentGroupInvitation](#sentgroupinvitation)
|
||||
- [GroupLinkConnecting](#grouplinkconnecting)
|
||||
|
||||
[Error events](#error-events)
|
||||
- [MessageError](#messageerror)
|
||||
- [ChatError](#chaterror)
|
||||
- [ChatErrors](#chaterrors)
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Contact connection events
|
||||
|
||||
Bots must use these events to process connecting users.
|
||||
|
||||
Most bots enable auto-accept and don't need to accept connections via commands.
|
||||
|
||||
You may create bot SimpleX address manually via CLI or desktop app or from bot code with these commands:
|
||||
- [APIShowMyAddress](./COMMANDS.md#apishowmyaddress) to check if address exists,
|
||||
- [APICreateMyAddress](./COMMANDS.md#apicreatemyaddress) to create address,
|
||||
- [APISetAddressSettings](./COMMANDS.md#apisetaddresssettings) to enable auto-access.
|
||||
|
||||
|
||||
### ContactConnected
|
||||
|
||||
This event is sent after a user connects via bot SimpleX address (not a business address).
|
||||
|
||||
**Record type**:
|
||||
- type: "contactConnected"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- contact: [Contact](./TYPES.md#contact)
|
||||
- userCustomProfile: [Profile](./TYPES.md#profile)?
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ContactUpdated
|
||||
|
||||
Contact profile of another user is updated.
|
||||
|
||||
**Record type**:
|
||||
- type: "contactUpdated"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- fromContact: [Contact](./TYPES.md#contact)
|
||||
- toContact: [Contact](./TYPES.md#contact)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ContactDeletedByContact
|
||||
|
||||
Bot user's connection with another contact is deleted (conversation is kept).
|
||||
|
||||
**Record type**:
|
||||
- type: "contactDeletedByContact"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- contact: [Contact](./TYPES.md#contact)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ReceivedContactRequest
|
||||
|
||||
Contact request received.
|
||||
|
||||
This event is only sent when auto-accept is disabled.
|
||||
|
||||
The request needs to be accepted using [APIAcceptContact](./COMMANDS.md#apiacceptcontact) command
|
||||
|
||||
**Record type**:
|
||||
- type: "receivedContactRequest"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- contactRequest: [UserContactRequest](./TYPES.md#usercontactrequest)
|
||||
- chat_: [AChat](./TYPES.md#achat)?
|
||||
|
||||
---
|
||||
|
||||
|
||||
### NewMemberContactReceivedInv
|
||||
|
||||
Received invitation to connect directly with a group member.
|
||||
|
||||
This event only needs to be processed to associate contact with group, the connection will proceed automatically.
|
||||
|
||||
**Record type**:
|
||||
- type: "newMemberContactReceivedInv"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- contact: [Contact](./TYPES.md#contact)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ContactSndReady
|
||||
|
||||
Connecting via 1-time invitation or after accepting contact request.
|
||||
|
||||
After this event bot can send messages to this contact.
|
||||
|
||||
**Record type**:
|
||||
- type: "contactSndReady"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- contact: [Contact](./TYPES.md#contact)
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Message events
|
||||
|
||||
Bots must use these events to process received messages.
|
||||
|
||||
|
||||
### NewChatItems
|
||||
|
||||
Received message(s).
|
||||
|
||||
**Record type**:
|
||||
- type: "newChatItems"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItems: [[AChatItem](./TYPES.md#achatitem)]
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ChatItemReaction
|
||||
|
||||
Received message reaction.
|
||||
|
||||
**Record type**:
|
||||
- type: "chatItemReaction"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- added: bool
|
||||
- reaction: [ACIReaction](./TYPES.md#acireaction)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ChatItemsDeleted
|
||||
|
||||
Message was deleted by another user.
|
||||
|
||||
**Record type**:
|
||||
- type: "chatItemsDeleted"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItemDeletions: [[ChatItemDeletion](./TYPES.md#chatitemdeletion)]
|
||||
- byUser: bool
|
||||
- timed: bool
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ChatItemUpdated
|
||||
|
||||
Message was updated by another user.
|
||||
|
||||
**Record type**:
|
||||
- type: "chatItemUpdated"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem: [AChatItem](./TYPES.md#achatitem)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### GroupChatItemsDeleted
|
||||
|
||||
Group messages are deleted or moderated.
|
||||
|
||||
**Record type**:
|
||||
- type: "groupChatItemsDeleted"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- chatItemIDs: [int64]
|
||||
- byUser: bool
|
||||
- member_: [GroupMember](./TYPES.md#groupmember)?
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ChatItemsStatusesUpdated
|
||||
|
||||
Message delivery status updates.
|
||||
|
||||
**Record type**:
|
||||
- type: "chatItemsStatusesUpdated"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItems: [[AChatItem](./TYPES.md#achatitem)]
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Group events
|
||||
|
||||
Bots may use these events to manage users' groups and business address groups.
|
||||
|
||||
*Please note*: programming groups is more complex than programming direct connections
|
||||
|
||||
|
||||
### ReceivedGroupInvitation
|
||||
|
||||
Received group invitation.
|
||||
|
||||
**Record type**:
|
||||
- type: "receivedGroupInvitation"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- contact: [Contact](./TYPES.md#contact)
|
||||
- fromMemberRole: [GroupMemberRole](./TYPES.md#groupmemberrole)
|
||||
- memberRole: [GroupMemberRole](./TYPES.md#groupmemberrole)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### UserJoinedGroup
|
||||
|
||||
Bot user joined group. Received when connection via group link completes.
|
||||
|
||||
**Record type**:
|
||||
- type: "userJoinedGroup"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- hostMember: [GroupMember](./TYPES.md#groupmember)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### GroupUpdated
|
||||
|
||||
Group profile or preferences updated.
|
||||
|
||||
**Record type**:
|
||||
- type: "groupUpdated"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- fromGroup: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- toGroup: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- member_: [GroupMember](./TYPES.md#groupmember)?
|
||||
|
||||
---
|
||||
|
||||
|
||||
### JoinedGroupMember
|
||||
|
||||
Another member joined group.
|
||||
|
||||
**Record type**:
|
||||
- type: "joinedGroupMember"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### MemberRole
|
||||
|
||||
Member (or bot user's) group role changed.
|
||||
|
||||
**Record type**:
|
||||
- type: "memberRole"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- byMember: [GroupMember](./TYPES.md#groupmember)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
- fromRole: [GroupMemberRole](./TYPES.md#groupmemberrole)
|
||||
- toRole: [GroupMemberRole](./TYPES.md#groupmemberrole)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### DeletedMember
|
||||
|
||||
Another member is removed from the group.
|
||||
|
||||
**Record type**:
|
||||
- type: "deletedMember"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- byMember: [GroupMember](./TYPES.md#groupmember)
|
||||
- deletedMember: [GroupMember](./TYPES.md#groupmember)
|
||||
- withMessages: bool
|
||||
|
||||
---
|
||||
|
||||
|
||||
### LeftMember
|
||||
|
||||
Another member left the group.
|
||||
|
||||
**Record type**:
|
||||
- type: "leftMember"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### DeletedMemberUser
|
||||
|
||||
Bot user was removed from the group.
|
||||
|
||||
**Record type**:
|
||||
- type: "deletedMemberUser"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
- withMessages: bool
|
||||
|
||||
---
|
||||
|
||||
|
||||
### GroupDeleted
|
||||
|
||||
Group was deleted by the owner (not bot user).
|
||||
|
||||
**Record type**:
|
||||
- type: "groupDeleted"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ConnectedToGroupMember
|
||||
|
||||
Connected to another group member.
|
||||
|
||||
**Record type**:
|
||||
- type: "connectedToGroupMember"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
- memberContact: [Contact](./TYPES.md#contact)?
|
||||
|
||||
---
|
||||
|
||||
|
||||
### MemberAcceptedByOther
|
||||
|
||||
Another group owner, admin or moderator accepted member to the group after review ("knocking").
|
||||
|
||||
**Record type**:
|
||||
- type: "memberAcceptedByOther"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- acceptingMember: [GroupMember](./TYPES.md#groupmember)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### MemberBlockedForAll
|
||||
|
||||
Another member blocked for all members.
|
||||
|
||||
**Record type**:
|
||||
- type: "memberBlockedForAll"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- byMember: [GroupMember](./TYPES.md#groupmember)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
- blocked: bool
|
||||
|
||||
---
|
||||
|
||||
|
||||
### GroupMemberUpdated
|
||||
|
||||
Another group member profile updated.
|
||||
|
||||
**Record type**:
|
||||
- type: "groupMemberUpdated"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- fromMember: [GroupMember](./TYPES.md#groupmember)
|
||||
- toMember: [GroupMember](./TYPES.md#groupmember)
|
||||
|
||||
---
|
||||
|
||||
|
||||
## File events
|
||||
|
||||
Bots that send or receive files may process these events to track delivery status and to process completion.
|
||||
|
||||
Bots that need to receive or moderate files (e.g., based on name, size or extension), can use relevant commands (e.g., [ReceiveFile](./COMMANDS.md#receivefile) or [APIDeleteMemberChatItem](./COMMANDS.md#apideletememberchatitem)) when processing [NewChatItems](#newchatitems) event.
|
||||
|
||||
Bots that need to send files should use [APISendMessages](./COMMANDS.md#apisendmessages) command.
|
||||
|
||||
|
||||
### RcvFileDescrReady
|
||||
|
||||
File is ready to be received.
|
||||
|
||||
This event is useful for processing sender file servers and monitoring file reception progress.
|
||||
|
||||
[ReceiveFile](./COMMANDS.md#receivefile) command can be used before this event.
|
||||
|
||||
**Record type**:
|
||||
- type: "rcvFileDescrReady"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem: [AChatItem](./TYPES.md#achatitem)
|
||||
- rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer)
|
||||
- rcvFileDescr: [RcvFileDescr](./TYPES.md#rcvfiledescr)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### RcvFileComplete
|
||||
|
||||
File reception is competed.
|
||||
|
||||
**Record type**:
|
||||
- type: "rcvFileComplete"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem: [AChatItem](./TYPES.md#achatitem)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### SndFileCompleteXFTP
|
||||
|
||||
File upload is competed.
|
||||
|
||||
**Record type**:
|
||||
- type: "sndFileCompleteXFTP"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem: [AChatItem](./TYPES.md#achatitem)
|
||||
- fileTransferMeta: [FileTransferMeta](./TYPES.md#filetransfermeta)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### RcvFileStart
|
||||
|
||||
File reception started. This event will be sent after [CEvtRcvFileDescrReady](#rcvfiledescrready) event.
|
||||
|
||||
**Record type**:
|
||||
- type: "rcvFileStart"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem: [AChatItem](./TYPES.md#achatitem)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### RcvFileSndCancelled
|
||||
|
||||
File was cancelled by the sender. This event may be sent instead of [CEvtRcvFileDescrReady](#rcvfiledescrready) event.
|
||||
|
||||
**Record type**:
|
||||
- type: "rcvFileSndCancelled"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem: [AChatItem](./TYPES.md#achatitem)
|
||||
- rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### RcvFileAccepted
|
||||
|
||||
This event will be sent when file is automatically accepted because of CLI option.
|
||||
|
||||
**Record type**:
|
||||
- type: "rcvFileAccepted"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem: [AChatItem](./TYPES.md#achatitem)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### RcvFileError
|
||||
|
||||
Error receiving file.
|
||||
|
||||
**Record type**:
|
||||
- type: "rcvFileError"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem_: [AChatItem](./TYPES.md#achatitem)?
|
||||
- agentError: [AgentErrorType](./TYPES.md#agenterrortype)
|
||||
- rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### RcvFileWarning
|
||||
|
||||
Warning when receiving file. It can happen when CLI settings do not allow to connect to file server(s).
|
||||
|
||||
**Record type**:
|
||||
- type: "rcvFileWarning"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem_: [AChatItem](./TYPES.md#achatitem)?
|
||||
- agentError: [AgentErrorType](./TYPES.md#agenterrortype)
|
||||
- rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### SndFileError
|
||||
|
||||
Error sending file.
|
||||
|
||||
**Record type**:
|
||||
- type: "sndFileError"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem_: [AChatItem](./TYPES.md#achatitem)?
|
||||
- fileTransferMeta: [FileTransferMeta](./TYPES.md#filetransfermeta)
|
||||
- errorMessage: string
|
||||
|
||||
---
|
||||
|
||||
|
||||
### SndFileWarning
|
||||
|
||||
Warning when sending file.
|
||||
|
||||
**Record type**:
|
||||
- type: "sndFileWarning"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatItem_: [AChatItem](./TYPES.md#achatitem)?
|
||||
- fileTransferMeta: [FileTransferMeta](./TYPES.md#filetransfermeta)
|
||||
- errorMessage: string
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Connection progress events
|
||||
|
||||
Bots may use these events to track progress of connections for monitoring or debugging.
|
||||
|
||||
|
||||
### AcceptingContactRequest
|
||||
|
||||
Automatically accepting contact request via bot's SimpleX address with auto-accept enabled.
|
||||
|
||||
**Record type**:
|
||||
- type: "acceptingContactRequest"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- contact: [Contact](./TYPES.md#contact)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### AcceptingBusinessRequest
|
||||
|
||||
Automatically accepting contact request via bot's business address.
|
||||
|
||||
**Record type**:
|
||||
- type: "acceptingBusinessRequest"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ContactConnecting
|
||||
|
||||
Contact confirmed connection.
|
||||
|
||||
Sent when contact started connecting via bot's 1-time invitation link or when bot connects to another SimpleX address.
|
||||
|
||||
**Record type**:
|
||||
- type: "contactConnecting"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- contact: [Contact](./TYPES.md#contact)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### BusinessLinkConnecting
|
||||
|
||||
Contact confirmed connection.
|
||||
|
||||
Sent when bot connects to another business address.
|
||||
|
||||
**Record type**:
|
||||
- type: "businessLinkConnecting"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- hostMember: [GroupMember](./TYPES.md#groupmember)
|
||||
- fromContact: [Contact](./TYPES.md#contact)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### JoinedGroupMemberConnecting
|
||||
|
||||
Group member is announced to the group and will be connecting to bot.
|
||||
|
||||
**Record type**:
|
||||
- type: "joinedGroupMemberConnecting"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- hostMember: [GroupMember](./TYPES.md#groupmember)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### SentGroupInvitation
|
||||
|
||||
Sent when another user joins group via bot's link.
|
||||
|
||||
**Record type**:
|
||||
- type: "sentGroupInvitation"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- contact: [Contact](./TYPES.md#contact)
|
||||
- member: [GroupMember](./TYPES.md#groupmember)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### GroupLinkConnecting
|
||||
|
||||
Sent when bot joins group via another user link.
|
||||
|
||||
**Record type**:
|
||||
- type: "groupLinkConnecting"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
|
||||
- hostMember: [GroupMember](./TYPES.md#groupmember)
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Error events
|
||||
|
||||
Bots may log these events for debugging. There will be many error events - this does NOT indicate a malfunction - e.g., they may happen because of bad network connectivity, or because messages may be delivered to deleted chats for a short period of time (they will be ignored).
|
||||
|
||||
|
||||
### MessageError
|
||||
|
||||
Message error.
|
||||
|
||||
**Record type**:
|
||||
- type: "messageError"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- severity: string
|
||||
- errorMessage: string
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ChatError
|
||||
|
||||
Chat error.
|
||||
|
||||
**Record type**:
|
||||
- type: "chatError"
|
||||
- chatError: [ChatError](./TYPES.md#chaterror)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ChatErrors
|
||||
|
||||
Chat errors.
|
||||
|
||||
**Record type**:
|
||||
- type: "chatErrors"
|
||||
- chatErrors: [[ChatError](./TYPES.md#chaterror)]
|
||||
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
# SimpleX Chat API types reference
|
||||
|
||||
- [API Commands and Responses](./COMMANDS.md)
|
||||
- [API Events](./EVENTS.md)
|
||||
- [API Types](./TYPES.md)
|
||||
+3768
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,462 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module API.Docs.Commands where
|
||||
|
||||
import API.Docs.Responses
|
||||
import API.Docs.Syntax.Types
|
||||
import API.Docs.Types
|
||||
import API.TypeInfo
|
||||
import Data.String
|
||||
import Data.List (find)
|
||||
import Data.Text (Text)
|
||||
import GHC.Generics
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Messaging.Parsers (dropPrefix, fstToLower)
|
||||
|
||||
chatCommandsDocs :: [CCCategory]
|
||||
chatCommandsDocs = map toCategory chatCommandsDocsData
|
||||
where
|
||||
toCategory (categoryName, categoryDescr, commandsData) =
|
||||
CCCategory {categoryName, categoryDescr, commands = map toCmd commandsData}
|
||||
toCmd (consName, hideParams, commandDescr, respNames, errors, network, syntax) = case find ((consName ==) . consName') chatCommandsTypeInfo of
|
||||
Just RecordTypeInfo {fieldInfos} ->
|
||||
let fields = filter ((`notElem` hideParams) . fieldName') $ map (toAPIField consName) fieldInfos
|
||||
commandType = ATUnionMember (fstToLower consName) fields
|
||||
findResp name = case find ((name ==) . consName') chatResponsesDocs of
|
||||
Just resp -> resp
|
||||
Nothing -> error $ "Missing response doc for " <> name
|
||||
responses = map findResp respNames
|
||||
errors' = map (\(TD err descr) -> TD (dropPrefix "CE" err) descr) errors
|
||||
in CCDoc {consName, commandType, commandDescr, responses, errors = errors', network, syntax}
|
||||
Nothing -> error $ "Missing command type info for " <> consName
|
||||
|
||||
deriving instance Generic ChatCommand
|
||||
|
||||
chatCommandsTypeInfo :: [RecordTypeInfo]
|
||||
chatCommandsTypeInfo = recordTypesInfo @ChatCommand
|
||||
|
||||
data CCCategory = CCCategory
|
||||
{ categoryName :: String,
|
||||
categoryDescr :: String,
|
||||
commands :: [CCDoc]
|
||||
}
|
||||
|
||||
data CCDoc = CCDoc
|
||||
{ consName :: ConsName,
|
||||
commandType :: ATUnionMember,
|
||||
commandDescr :: Text,
|
||||
responses :: [CRDoc],
|
||||
errors :: [ErrorTypeDoc],
|
||||
network :: Maybe UsesNetwork,
|
||||
syntax :: Expr
|
||||
}
|
||||
|
||||
instance ConstructorName CCDoc where consName' CCDoc {consName} = consName
|
||||
|
||||
data ErrorTypeDoc = TD
|
||||
{ consName :: ConsName,
|
||||
description :: String
|
||||
}
|
||||
|
||||
data UsesNetwork = UNBackground | UNInteractive
|
||||
|
||||
instance IsString ErrorTypeDoc where fromString s = TD s ""
|
||||
|
||||
|
||||
-- category name, category description, commands
|
||||
-- inner: constructor, description, responses, errors (ChatErrorType constructors), network usage, syntax
|
||||
chatCommandsDocsData :: [(String, String, [(ConsName, [String], Text, [ConsName], [ErrorTypeDoc], Maybe UsesNetwork, Expr)])]
|
||||
chatCommandsDocsData =
|
||||
[ ( "Address commands",
|
||||
"Bots can use these commands to automatically check and create address when initialized",
|
||||
[ ("APICreateMyAddress", [], "Create bot address.", ["CRUserContactLinkCreated"], [], Just UNInteractive, "/_address " <> Param "userId"),
|
||||
("APIDeleteMyAddress", [], "Delete bot address.", ["CRUserContactLinkDeleted"], [], Just UNBackground, "/_delete_address " <> Param "userId"),
|
||||
("APIShowMyAddress", [], "Get bot address and settings.", ["CRUserContactLink"], [], Nothing, "/_show_address " <> Param "userId"),
|
||||
("APISetProfileAddress", [], "Add address to bot profile.", ["CRUserProfileUpdated"], [], Just UNInteractive, "/_profile_address " <> Param "userId" <> " " <> OnOff "enable"),
|
||||
("APISetAddressSettings", [], "Set bot address settings.", ["CRUserContactLinkUpdated"], [], Just UNInteractive, "/_address_settings " <> Param "userId" <> " " <> Json "settings")
|
||||
]
|
||||
),
|
||||
( "Message commands",
|
||||
"Commands to send, update, delete, moderate messages and set message reactions",
|
||||
[ ("APISendMessages", [], "Send messages.", ["CRNewChatItems"], [], Just UNBackground, "/_send " <> Param "sendRef" <> OnOffParam "live" "liveMessage" (Just False) <> Optional "" (" ttl=" <> Param "$0") "ttl" <> " json " <> Json "composedMessages"),
|
||||
( "APIUpdateChatItem",
|
||||
[],
|
||||
"Update message.",
|
||||
["CRChatItemUpdated", "CRChatItemNotChanged", "CRChatCmdError"],
|
||||
[TD "CEInvalidChatItemUpdate" "Not user's message or cannot be edited"],
|
||||
Just UNBackground,
|
||||
"/_update item " <> Param "chatRef" <> " " <> Param "chatItemId" <> OnOffParam "live" "liveMessage" (Just False) <> " json " <> Json "updatedMessage"
|
||||
),
|
||||
("APIDeleteChatItem", [], "Delete message.", ["CRChatItemsDeleted"], [], Just UNBackground, "/_delete item " <> Param "chatRef" <> " " <> Join ',' "chatItemIds" <> " " <> Param "deleteMode"),
|
||||
("APIDeleteMemberChatItem", [], "Moderate message. Requires Moderator role (and higher than message author's).", ["CRChatItemsDeleted"], [], Just UNBackground, "/_delete member item #" <> Param "groupId" <> " " <> Join ',' "chatItemIds"),
|
||||
("APIChatItemReaction", [], "Add/remove message reaction.", ["CRChatItemReaction"], [], Just UNBackground, "/_reaction " <> Param "chatRef" <> " " <> Param "chatItemId" <> " " <> OnOff "add" <> " " <> Json "reaction")
|
||||
]
|
||||
),
|
||||
( "File commands",
|
||||
"Commands to receive and to cancel files. Files are sent as part of the message, there are no separate commands to send files.",
|
||||
[ ("ReceiveFile", [], "Receive file.", ["CRRcvFileAccepted", "CRRcvFileAcceptedSndCancelled"], [], Nothing, "/freceive " <> Param "fileId" <> OnOffParam "approved_relays" "userApprovedRelays" (Just False) <> OnOffParam "encrypt" "storeEncrypted" Nothing <> OnOffParam "inline" "fileInline" Nothing <> Optional "" (" " <> Param "$0") "filePath"),
|
||||
("CancelFile", [], "Cancel file.", ["CRSndFileCancelled", "CRRcvFileCancelled", "CRChatCmdError"], [TD "CEFileCancel" "Cannot cancel file"], Just UNBackground, "/fcancel " <> Param "fileId")
|
||||
]
|
||||
),
|
||||
( "Group commands",
|
||||
"Commands to manage and moderate groups. These commands can be used with business chats as well - they are groups. E.g., a common scenario would be to add human agents to business chat with the customer who connected via business address.",
|
||||
[ ("APIAddMember", [], "Add contact to group. Requires bot to have Admin role.", ["CRSentGroupInvitation"], [], Just UNInteractive, "/_add #" <> Param "groupId" <> " " <> Param "contactId" <> " " <> Param "memberRole"),
|
||||
("APIJoinGroup", ["enableNtfs"], "Join group.", ["CRUserAcceptedGroupSent"], [], Just UNInteractive, "/_join #" <> Param "groupId"),
|
||||
("APIAcceptMember", [], "Accept group member. Requires Admin role.", ["CRMemberAccepted", "CRChatCmdError"], [TD "CEGroupMemberNotActive" "Member is not connected yet"], Just UNBackground, "/_accept member #" <> Param "groupId" <> " " <> Param "groupMemberId" <> " " <> Param "memberRole"),
|
||||
("APIMembersRole", [], "Set members role. Requires Admin role.", ["CRMembersRoleUser"], [], Just UNBackground, "/_member role #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> " " <> Param "memberRole"),
|
||||
("APIBlockMembersForAll", [], "Block members. Requires Moderator role.", ["CRMembersBlockedForAllUser"], [], Just UNBackground, "/_block #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> OnOffParam "blocked" "blocked" Nothing),
|
||||
("APIRemoveMembers", [], "Remove members. Requires Admin role.", ["CRUserDeletedMembers", "CRChatCmdError"], ["CEGroupMemberNotFound"], Just UNBackground, "/_remove #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> OnOffParam "messages" "withMessages" (Just False)),
|
||||
("APILeaveGroup", [], "Leave group.", ["CRLeftMemberUser"], [], Just UNBackground, "/_leave #" <> Param "groupId")
|
||||
-- ("APIListMembers", [], "Get group members.", ["CRGroupMembers"], [], Nothing, ""),
|
||||
-- ("APINewGroup", [], "Create group.", ["CRGroupCreated"], [], Nothing, ""),
|
||||
-- ("APIUpdateGroupProfile", [], "Update group profile.", ["CRGroupUpdated"], [], Just UNBackground, [])
|
||||
]
|
||||
),
|
||||
( "Group link commands",
|
||||
"These commands can be used by bots that manage multiple public groups",
|
||||
[ ("APICreateGroupLink", [], "Create group link.", ["CRGroupLinkCreated"], [], Just UNInteractive, "/_create link #" <> Param "groupId" <> " " <> Param "memberRole"),
|
||||
("APIGroupLinkMemberRole", [], "Set member role for group link.", ["CRGroupLink"], [], Nothing, "/_set link role #" <> Param "groupId" <> " " <> Param "memberRole"),
|
||||
("APIDeleteGroupLink", [], "Delete group link.", ["CRGroupLinkDeleted"], [], Just UNBackground, "/_delete link #" <> Param "groupId"),
|
||||
("APIGetGroupLink", [], "Get group link.", ["CRGroupLink"], [], Nothing, "/_get link #" <> Param "groupId")
|
||||
]
|
||||
),
|
||||
( "Connection commands",
|
||||
"These commands may be used to create connections. Most bots do not need to use them - bot users will connect via bot address with auto-accept enabled.",
|
||||
[ ("APIAddContact", [], "Create 1-time invitation link.", ["CRInvitation"], [], Just UNInteractive, "/_connect " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False)),
|
||||
("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link.", ["CRConnectionPlan"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectionLink"),
|
||||
("APIConnect", [], "Connect via SimpleX link. The link can be 1-time invitation link, contact address or group link", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation"], [], Just UNInteractive, "/_connect " <> Param "userId" <> " " <> Param "connLink_"),
|
||||
("APIAcceptContact", ["incognito"], "Accept contact request.", ["CRAcceptingContactRequest"], [], Just UNInteractive, "/_accept " <> Param "contactReqId"),
|
||||
("APIRejectContact", [], "Reject contact request. The user who sent the request is **not notified**.", ["CRContactRequestRejected"], [], Nothing, "/_reject " <> Param "contactReqId")
|
||||
]
|
||||
),
|
||||
( "Chat commands",
|
||||
"Commands to list and delete coversations.",
|
||||
[ ("APIListContacts", [], "Get contacts.", ["CRContactsList"], [], Nothing, "/_contacts " <> Param "userId"),
|
||||
("APIListGroups", [], "Get groups.", ["CRGroupsList"], [], Nothing, "/_groups " <> Param "userId" <> Optional "" (" @" <> Param "$0") "contactId_" <> Optional "" (" " <> Param "$0") "search"),
|
||||
("APIDeleteChat", [], "Delete chat.", ["CRContactDeleted", "CRContactConnectionDeleted", "CRGroupDeletedUser"], [], Just UNBackground, "/_delete " <> Param "chatRef" <> " " <> Param "chatDeleteMode")
|
||||
-- ("APIChatItemsRead", [], "Mark items as read.", ["CRItemsReadForChat"], [], Nothing, ""),
|
||||
-- ("APIChatRead", [], "Mark chat as read.", ["CRCmdOk"], [], Nothing, ""),
|
||||
-- ("APIChatUnread", [], "Mark chat as unread.", ["CRCmdOk"], [], Nothing, ""),
|
||||
-- ("APIClearChat", [], "Clear chat.", ["CRChatCleared"], [], Nothing, ""),
|
||||
-- ("APIGetChat", [], "Get chat.", ["CRApiChat"], [], Nothing, ""),
|
||||
-- ("APIGetChatItemInfo", [], "Get message information.", ["CRChatItemInfo"], [], Nothing, ""),
|
||||
-- ("APIGetChatItems", [], "Get the most recent messages from all chats.", ["CRChatItems"], [], Nothing, ""),
|
||||
-- ("APIGetChats", [], "Get chats.", ["CRApiChats"], [], Nothing, ""),
|
||||
-- ("APISetChatSettings", [], "Set chat settings.", ["CRCmdOk"], [], Nothing, ""),
|
||||
-- ("APISetChatTTL", [], "Set TTL for chat messages.", ["CRCmdOk"], [], Nothing, ""),
|
||||
-- ("APISetConnectionAlias", [], "Set connection alias.", ["CRConnectionAliasUpdated"], [], Nothing, ""),
|
||||
-- ("APISetContactAlias", [], "Set contact alias.", ["CRContactAliasUpdated"], [], Nothing, ""),
|
||||
-- ("APISetContactPrefs", [], "Set contact preferences.", ["CRContactPrefsUpdated"], [], Just UNBackground, ""),
|
||||
-- ("APISetGroupAlias", [], "Set group alias.", ["CRGroupAliasUpdated"], [], Nothing, ""),
|
||||
-- ("APISyncContactRatchet", [], "Synchronize encryption with contact.", ["CRContactRatchetSyncStarted"], [], Just UNBackground, ""),
|
||||
-- ("APISyncGroupMemberRatchet", [], "Synchronize encryption with member.", ["CRGroupMemberRatchetSyncStarted"], [], Just UNBackground, ""),
|
||||
]
|
||||
),
|
||||
( "User profile commands",
|
||||
"Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents).",
|
||||
[ ("ShowActiveUser", [], "Get active user profile", ["CRActiveUser"], [], Nothing, "/user"),
|
||||
( "CreateActiveUser",
|
||||
[],
|
||||
"Create new user profile",
|
||||
["CRActiveUser"],
|
||||
[TD "CEUserExists" "User or contact with this name already exists", TD "CEInvalidDisplayName" "Invalid user display name"],
|
||||
Nothing,
|
||||
"/_create user " <> Json "newUser"
|
||||
),
|
||||
("ListUsers", [], "Get all user profiles", ["CRUsersList"], [], Nothing, "/users"),
|
||||
("APISetActiveUser", [], "Set active user profile", ["CRActiveUser"], ["CEChatNotStarted"], Nothing, "/_user " <> Param "userId" <> Optional "" (" " <> Json "$0") "viewPwd"),
|
||||
("APIDeleteUser", [], "Delete user profile.", ["CRCmdOk"], [], Just UNBackground, "/_delete user " <> Param "userId" <> OnOffParam "del_smp" "delSMPQueues" Nothing <> Optional "" (" " <> Json "$0") "viewPwd"),
|
||||
("APIUpdateProfile", [], "Update user profile.", ["CRUserProfileUpdated"], [], Just UNBackground, "/_profile " <> Param "userId" <> " " <> Json "profile")
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
cliCommands :: [ConsName]
|
||||
cliCommands =
|
||||
[ "AbortSwitchContact",
|
||||
"AbortSwitchGroupMember",
|
||||
"AcceptContact",
|
||||
"AcceptMember",
|
||||
"AddContact",
|
||||
"AddMember",
|
||||
"BlockForAll",
|
||||
"ChatHelp",
|
||||
"ClearContact",
|
||||
"ClearGroup",
|
||||
"ClearNoteFolder",
|
||||
"Connect",
|
||||
"ConnectSimplex",
|
||||
"ContactInfo",
|
||||
"ContactQueueInfo",
|
||||
"CreateGroupLink",
|
||||
"CreateMyAddress",
|
||||
"DeleteContact",
|
||||
"DeleteGroup",
|
||||
"DeleteGroupLink",
|
||||
"DeleteMemberMessage",
|
||||
"DeleteMessage",
|
||||
"DeleteMyAddress",
|
||||
"DeleteUser",
|
||||
"EditMessage",
|
||||
"EnableContact",
|
||||
"EnableGroupMember",
|
||||
"FileStatus",
|
||||
"ForwardFile",
|
||||
"ForwardGroupMessage",
|
||||
"ForwardImage",
|
||||
"ForwardLocalMessage",
|
||||
"ForwardMessage",
|
||||
"GetChatTTL",
|
||||
"GetContactCode",
|
||||
"GetGroupMemberCode",
|
||||
"GroupLinkMemberRole",
|
||||
"GroupMemberInfo",
|
||||
"GroupMemberQueueInfo",
|
||||
"HideUser",
|
||||
"JoinGroup",
|
||||
"LastChatItemId",
|
||||
"LastChats",
|
||||
"LastMessages",
|
||||
"LeaveGroup",
|
||||
"ListContacts",
|
||||
"ListGroups",
|
||||
"ListMembers",
|
||||
"ListMemberSupportChats",
|
||||
"MemberRole",
|
||||
"MuteUser",
|
||||
"NewGroup",
|
||||
"QuitChat",
|
||||
"ReactToMessage",
|
||||
"RejectContact",
|
||||
"RemoveMembers",
|
||||
"ReportMessage",
|
||||
"SendCallInvitation",
|
||||
"SendFile",
|
||||
"SendFileDescription",
|
||||
"SendGroupMessageQuote",
|
||||
"SendImage",
|
||||
"SendLiveMessage",
|
||||
"SendMemberContactMessage",
|
||||
"SendMessage",
|
||||
"SendMessageBroadcast",
|
||||
"SendMessageQuote",
|
||||
"SetActiveUser",
|
||||
"SetAddressSettings",
|
||||
"SetChatTTL",
|
||||
"SetContactFeature",
|
||||
"SetContactTimedMessages",
|
||||
"SetGroupFeature",
|
||||
"SetGroupFeatureRole",
|
||||
"SetGroupMemberAdmissionReview",
|
||||
"SetGroupTimedMessages",
|
||||
"SetLocalDeviceName",
|
||||
"SetProfileAddress",
|
||||
"SetSendReceipts",
|
||||
"SetShowMemberMessages",
|
||||
"SetShowMessages",
|
||||
"SetUserContactReceipts",
|
||||
"SetUserFeature",
|
||||
"SetUserGroupReceipts",
|
||||
"SetUserTimedMessages",
|
||||
"ShowChatItem",
|
||||
"ShowChatItemInfo",
|
||||
"ShowGroupDescription",
|
||||
"ShowGroupInfo",
|
||||
"ShowGroupLink",
|
||||
"ShowGroupProfile",
|
||||
"ShowLiveItems",
|
||||
"ShowMyAddress",
|
||||
"ShowProfile",
|
||||
"ShowProfileImage",
|
||||
"ShowVersion",
|
||||
"SwitchContact",
|
||||
"SwitchGroupMember",
|
||||
"SyncContactRatchet",
|
||||
"SyncGroupMemberRatchet",
|
||||
"UnhideUser",
|
||||
"UnmuteUser",
|
||||
"UpdateGroupDescription",
|
||||
"UpdateGroupNames",
|
||||
"UpdateLiveMessage",
|
||||
"UpdateProfile",
|
||||
"UpdateProfileImage",
|
||||
"UserRead",
|
||||
"VerifyContact",
|
||||
"VerifyGroupMember",
|
||||
"Welcome"
|
||||
]
|
||||
|
||||
undocumentedCommands :: [ConsName]
|
||||
undocumentedCommands =
|
||||
[ "APIAbortSwitchContact",
|
||||
"APIAbortSwitchGroupMember",
|
||||
"APIAcceptConditions",
|
||||
"APIActivateChat",
|
||||
"APIAddGroupShortLink",
|
||||
"APIAddMyAddressShortLink",
|
||||
"APIArchiveReceivedReports",
|
||||
"APICallStatus",
|
||||
"APIChangeConnectionUser",
|
||||
"APIChangePreparedContactUser",
|
||||
"APIChangePreparedGroupUser",
|
||||
"APIChatItemsRead",
|
||||
"APIChatRead",
|
||||
"APIChatUnread",
|
||||
"APICheckToken",
|
||||
"APIClearChat",
|
||||
"APIConnectContactViaAddress",
|
||||
"APIConnectPreparedContact",
|
||||
"APIConnectPreparedGroup",
|
||||
"APIContactInfo",
|
||||
"APIContactQueueInfo",
|
||||
"APICreateChatItems",
|
||||
"APICreateChatTag",
|
||||
"APICreateMemberContact",
|
||||
"APIDeleteChatTag",
|
||||
"APIDeleteMemberSupportChat",
|
||||
"APIDeleteReceivedReports",
|
||||
"APIDeleteStorage",
|
||||
"APIDeleteToken",
|
||||
"APIDownloadStandaloneFile",
|
||||
"APIEnableContact",
|
||||
"APIEnableGroupMember",
|
||||
"APIEndCall",
|
||||
"APIExportArchive",
|
||||
"APIForwardChatItems",
|
||||
"APIGetAppSettings",
|
||||
"APIGetCallInvitations",
|
||||
"APIGetChat",
|
||||
"APIGetChatItemInfo",
|
||||
"APIGetChatItems",
|
||||
"APIGetChatItemTTL",
|
||||
"APIGetChats",
|
||||
"APIGetChatTags",
|
||||
"APIGetConnNtfMessages",
|
||||
"APIGetContactCode",
|
||||
"APIGetGroupMemberCode",
|
||||
"APIGetNetworkConfig",
|
||||
"APIGetNetworkStatuses",
|
||||
"APIGetNtfConns",
|
||||
"APIGetNtfToken",
|
||||
"APIGetReactionMembers",
|
||||
"APIGetServerOperators",
|
||||
"APIGetUsageConditions",
|
||||
"APIGetUserServers",
|
||||
"APIGroupInfo",
|
||||
"APIGroupMemberInfo",
|
||||
"APIGroupMemberQueueInfo",
|
||||
"APIHideUser",
|
||||
"APIImportArchive",
|
||||
"APIListMembers",
|
||||
"APIMuteUser",
|
||||
"APINewGroup",
|
||||
"APIPlanForwardChatItems",
|
||||
"APIPrepareContact",
|
||||
"APIPrepareGroup",
|
||||
"APIRegisterToken",
|
||||
"APIRejectCall",
|
||||
"APIReorderChatTags",
|
||||
"APIReportMessage",
|
||||
"APISaveAppSettings",
|
||||
"APISendCallAnswer",
|
||||
"APISendCallExtraInfo",
|
||||
"APISendCallInvitation",
|
||||
"APISendCallOffer",
|
||||
"APISendMemberContactInvitation",
|
||||
"APISetAppFilePaths",
|
||||
"APISetChatItemTTL",
|
||||
"APISetChatSettings",
|
||||
"APISetChatTags",
|
||||
"APISetChatTTL",
|
||||
"APISetChatUIThemes",
|
||||
"APISetConditionsNotified",
|
||||
"APISetConnectionAlias",
|
||||
"APISetConnectionIncognito",
|
||||
"APISetContactAlias",
|
||||
"APISetContactPrefs",
|
||||
"APISetEncryptLocalFiles",
|
||||
"APISetGroupAlias",
|
||||
"APISetMemberSettings",
|
||||
"APISetNetworkConfig",
|
||||
"APISetNetworkInfo",
|
||||
"APISetServerOperators",
|
||||
"APISetUserContactReceipts",
|
||||
"APISetUserGroupReceipts",
|
||||
"APISetUserServers",
|
||||
"APISetUserUIThemes",
|
||||
"APIStandaloneFileInfo",
|
||||
"APIStopChat",
|
||||
"APIStorageEncryption",
|
||||
"APISuspendChat",
|
||||
"APISwitchContact",
|
||||
"APISwitchGroupMember",
|
||||
"APISyncContactRatchet",
|
||||
"APISyncGroupMemberRatchet",
|
||||
"APITestProtoServer",
|
||||
"APIUnhideUser",
|
||||
"APIUnmuteUser",
|
||||
"APIUpdateChatTag",
|
||||
"APIUpdateGroupProfile",
|
||||
"APIUploadStandaloneFile",
|
||||
"APIUserRead",
|
||||
"APIValidateServers",
|
||||
"APIVerifyContact",
|
||||
"APIVerifyGroupMember",
|
||||
"APIVerifyToken",
|
||||
"CheckChatRunning",
|
||||
"ConfirmRemoteCtrl",
|
||||
"ConnectRemoteCtrl",
|
||||
"CustomChatCommand",
|
||||
"DebugEvent",
|
||||
"DebugLocks",
|
||||
"DeleteRemoteCtrl",
|
||||
"DeleteRemoteHost",
|
||||
"ExecAgentStoreSQL",
|
||||
"ExecChatStoreSQL",
|
||||
"ExportArchive",
|
||||
"FindKnownRemoteCtrl",
|
||||
"GetAgentQueuesInfo",
|
||||
"GetAgentServersSummary",
|
||||
"GetAgentSubs",
|
||||
"GetAgentSubsDetails",
|
||||
"GetAgentSubsTotal",
|
||||
"GetAgentWorkers",
|
||||
"GetAgentWorkersDetails",
|
||||
"GetChatItemTTL",
|
||||
"GetRemoteFile",
|
||||
"GetUserProtoServers",
|
||||
"ListRemoteCtrls",
|
||||
"ListRemoteHosts",
|
||||
"ReconnectAllServers",
|
||||
"ReconnectServer",
|
||||
"ResetAgentServersStats",
|
||||
"ResubscribeAllConnections",
|
||||
"SetAllContactReceipts",
|
||||
"SetChatItemTTL",
|
||||
"SetContactMergeEnabled",
|
||||
"SetFilesFolder",
|
||||
"SetFileToReceive",
|
||||
"SetNetworkConfig",
|
||||
"SetRemoteHostsFolder",
|
||||
"SetServerOperators",
|
||||
"SetTempFolder",
|
||||
"SetUserProtoServers",
|
||||
"SlowSQLQueries",
|
||||
"StartChat",
|
||||
"StartRemoteHost",
|
||||
"StopRemoteCtrl",
|
||||
"StopRemoteHost",
|
||||
"StoreRemoteFile",
|
||||
"SwitchRemoteHost",
|
||||
"TestProtoServer",
|
||||
"TestStorageEncryption",
|
||||
"VerifyRemoteCtrlSession"
|
||||
]
|
||||
@@ -0,0 +1,216 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module API.Docs.Events where
|
||||
|
||||
import API.Docs.Types
|
||||
import API.TypeInfo
|
||||
import Data.List (find)
|
||||
import GHC.Generics
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Messaging.Parsers (dropPrefix)
|
||||
|
||||
data CECategory = CECategory
|
||||
{ categoryName :: String,
|
||||
categoryDescr :: String,
|
||||
mainEvents :: [CEDoc],
|
||||
otherEvents :: [CEDoc]
|
||||
}
|
||||
|
||||
data CEDoc = CEDoc
|
||||
{ consName :: ConsName,
|
||||
eventType :: ATUnionMember,
|
||||
eventDescr :: String
|
||||
}
|
||||
|
||||
instance ConstructorName CEDoc where consName' CEDoc {consName} = consName
|
||||
|
||||
chatEventsDocs :: [CECategory]
|
||||
chatEventsDocs = map toCategory chatEventsDocsData
|
||||
where
|
||||
toCategory (categoryName, categoryDescr, mainEvts, otherEvts) =
|
||||
CECategory {categoryName, categoryDescr, mainEvents = map toEvt mainEvts, otherEvents = map toEvt otherEvts}
|
||||
toEvt (consName, eventDescr)
|
||||
| consName == "CEvtChatError" =
|
||||
let field = toAPIField consName $ FieldInfo "chatError" (ti "ChatError")
|
||||
eventType = ATUnionMember (dropPrefix "CEvt" consName) [field]
|
||||
in CEDoc {consName, eventType, eventDescr}
|
||||
| otherwise = case find ((consName ==) . consName') chatEventsTypeInfo of
|
||||
Just RecordTypeInfo {fieldInfos} ->
|
||||
let fields = map (toAPIField consName) fieldInfos
|
||||
eventType = ATUnionMember (dropPrefix "CEvt" consName) fields
|
||||
in CEDoc {consName, eventType, eventDescr}
|
||||
Nothing -> error $ "Missing event type info for " <> consName
|
||||
|
||||
deriving instance Generic ChatEvent
|
||||
|
||||
chatEventsTypeInfo :: [RecordTypeInfo]
|
||||
chatEventsTypeInfo = recordTypesInfo @ChatEvent
|
||||
|
||||
chatEventsDocsData :: [(String, String, [(ConsName, String)], [(ConsName, String)])]
|
||||
chatEventsDocsData =
|
||||
[ ( "Contact connection events", -- which event should be processed by a bot that has business address. Maybe needs a separate category.
|
||||
"Bots must use these events to process connecting users.\n\n\
|
||||
\Most bots enable auto-accept and don't need to accept connections via commands.\n\n\
|
||||
\You may create bot SimpleX address manually via CLI or desktop app or from bot code with these commands:\n\
|
||||
\- [APIShowMyAddress](./COMMANDS.md#apishowmyaddress) to check if address exists,\n\
|
||||
\- [APICreateMyAddress](./COMMANDS.md#apicreatemyaddress) to create address,\n\
|
||||
\- [APISetAddressSettings](./COMMANDS.md#apisetaddresssettings) to enable auto-access.",
|
||||
[ ( "CEvtContactConnected", "This event is sent after a user connects via bot SimpleX address (not a business address).")
|
||||
],
|
||||
[
|
||||
("CEvtContactUpdated", "Contact profile of another user is updated."),
|
||||
("CEvtContactDeletedByContact", "Bot user's connection with another contact is deleted (conversation is kept)."),
|
||||
("CEvtReceivedContactRequest", "Contact request received.\n\nThis event is only sent when auto-accept is disabled.\n\nThe request needs to be accepted using [APIAcceptContact](./COMMANDS.md#apiacceptcontact) command"),
|
||||
("CEvtNewMemberContactReceivedInv", "Received invitation to connect directly with a group member.\n\nThis event only needs to be processed to associate contact with group, the connection will proceed automatically."),
|
||||
("CEvtContactSndReady", "Connecting via 1-time invitation or after accepting contact request.\n\nAfter this event bot can send messages to this contact.") -- JOINED
|
||||
]
|
||||
),
|
||||
( "Message events",
|
||||
"Bots must use these events to process received messages.",
|
||||
[ ("CEvtNewChatItems", "Received message(s).")
|
||||
],
|
||||
[ ("CEvtChatItemReaction", "Received message reaction."),
|
||||
("CEvtChatItemsDeleted", "Message was deleted by another user."),
|
||||
("CEvtChatItemUpdated", "Message was updated by another user."),
|
||||
("CEvtGroupChatItemsDeleted", "Group messages are deleted or moderated."),
|
||||
("CEvtChatItemsStatusesUpdated", "Message delivery status updates.")
|
||||
]
|
||||
),
|
||||
( "Group events",
|
||||
"Bots may use these events to manage users' groups and business address groups.\n\n\
|
||||
\*Please note*: programming groups is more complex than programming direct connections",
|
||||
[ ("CEvtReceivedGroupInvitation", ""),
|
||||
("CEvtUserJoinedGroup", "Bot user joined group. Received when connection via group link completes."),
|
||||
("CEvtGroupUpdated", "Group profile or preferences updated."),
|
||||
("CEvtJoinedGroupMember", "Another member joined group."),
|
||||
("CEvtMemberRole", "Member (or bot user's) group role changed."),
|
||||
("CEvtDeletedMember", "Another member is removed from the group."),
|
||||
("CEvtLeftMember", "Another member left the group."),
|
||||
("CEvtDeletedMemberUser", "Bot user was removed from the group."),
|
||||
("CEvtGroupDeleted", "Group was deleted by the owner (not bot user).")
|
||||
],
|
||||
[ ("CEvtConnectedToGroupMember", "Connected to another group member."),
|
||||
("CEvtMemberAcceptedByOther", "Another group owner, admin or moderator accepted member to the group after review (\"knocking\")."),
|
||||
("CEvtMemberBlockedForAll", "Another member blocked for all members."),
|
||||
("CEvtGroupMemberUpdated", "Another group member profile updated.")
|
||||
]
|
||||
),
|
||||
( "File events",
|
||||
"Bots that send or receive files may process these events to track delivery status and to process completion.\n\n\
|
||||
\Bots that need to receive or moderate files (e.g., based on name, size or extension), \
|
||||
\can use relevant commands (e.g., [ReceiveFile](./COMMANDS.md#receivefile) or \
|
||||
\[APIDeleteMemberChatItem](./COMMANDS.md#apideletememberchatitem)) \
|
||||
\when processing [NewChatItems](#newchatitems) event.\n\n\
|
||||
\Bots that need to send files should use [APISendMessages](./COMMANDS.md#apisendmessages) command.",
|
||||
[ ( "CEvtRcvFileDescrReady",
|
||||
"File is ready to be received.\n\n\
|
||||
\This event is useful for processing sender file servers and monitoring file reception progress.\n\n\
|
||||
\[ReceiveFile](./COMMANDS.md#receivefile) command can be used before this event."
|
||||
),
|
||||
("CEvtRcvFileComplete", "File reception is competed."),
|
||||
("CEvtSndFileCompleteXFTP", "File upload is competed.")
|
||||
],
|
||||
[ ("CEvtRcvFileStart", "File reception started. This event will be sent after [CEvtRcvFileDescrReady](#rcvfiledescrready) event."),
|
||||
("CEvtRcvFileSndCancelled", "File was cancelled by the sender. This event may be sent instead of [CEvtRcvFileDescrReady](#rcvfiledescrready) event."),
|
||||
("CEvtRcvFileAccepted", "This event will be sent when file is automatically accepted because of CLI option."),
|
||||
("CEvtRcvFileError", "Error receiving file."),
|
||||
("CEvtRcvFileWarning", "Warning when receiving file. It can happen when CLI settings do not allow to connect to file server(s)."),
|
||||
("CEvtSndFileError", "Error sending file."),
|
||||
("CEvtSndFileWarning", "Warning when sending file.")
|
||||
]
|
||||
),
|
||||
( "Connection progress events",
|
||||
"Bots may use these events to track progress of connections for monitoring or debugging.",
|
||||
[ ("CEvtAcceptingContactRequest", "Automatically accepting contact request via bot's SimpleX address with auto-accept enabled."),
|
||||
("CEvtAcceptingBusinessRequest", "Automatically accepting contact request via bot's business address."),
|
||||
("CEvtContactConnecting", "Contact confirmed connection.\n\nSent when contact started connecting via bot's 1-time invitation link or when bot connects to another SimpleX address."), -- CONF
|
||||
("CEvtBusinessLinkConnecting", "Contact confirmed connection.\n\nSent when bot connects to another business address."), -- CONF
|
||||
("CEvtJoinedGroupMemberConnecting", "Group member is announced to the group and will be connecting to bot."), -- MSG
|
||||
("CEvtSentGroupInvitation", "Sent when another user joins group via bot's link."), -- INV
|
||||
("CEvtGroupLinkConnecting", "Sent when bot joins group via another user link.") -- CONF
|
||||
],
|
||||
[]
|
||||
),
|
||||
( "Error events",
|
||||
"Bots may log these events for debugging. \
|
||||
\There will be many error events - this does NOT indicate a malfunction - \
|
||||
\e.g., they may happen because of bad network connectivity, \
|
||||
\or because messages may be delivered to deleted chats for a short period of time \
|
||||
\(they will be ignored).",
|
||||
[ ("CEvtMessageError", ""),
|
||||
("CEvtChatError", ""), -- only used in WebSockets API, Haskell code uses Either, with error in Left
|
||||
("CEvtChatErrors", "")
|
||||
],
|
||||
[]
|
||||
)
|
||||
]
|
||||
|
||||
undocumentedEvents :: [ConsName]
|
||||
undocumentedEvents =
|
||||
[ "CEvtAcceptingGroupJoinRequestMember",
|
||||
"CEvtAgentConnsDeleted",
|
||||
"CEvtAgentRcvQueuesDeleted",
|
||||
"CEvtAgentUserDeleted",
|
||||
"CEvtBusinessRequestAlreadyAccepted",
|
||||
"CEvtCallAnswer",
|
||||
"CEvtCallEnded",
|
||||
"CEvtCallExtraInfo",
|
||||
"CEvtCallInvitation",
|
||||
"CEvtCallOffer",
|
||||
"CEvtChatInfoUpdated",
|
||||
"CEvtChatItemDeletedNotFound",
|
||||
"CEvtChatItemNotChanged",
|
||||
"CEvtChatSuspended",
|
||||
"CEvtConnectionDisabled",
|
||||
"CEvtConnectionInactive",
|
||||
"CEvtContactAndMemberAssociated",
|
||||
"CEvtContactAnotherClient",
|
||||
"CEvtContactDisabled",
|
||||
"CEvtContactPQEnabled",
|
||||
"CEvtContactRatchetSync",
|
||||
"CEvtContactRequestAlreadyAccepted",
|
||||
"CEvtContactsDisconnected",
|
||||
"CEvtContactsMerged",
|
||||
"CEvtContactsSubscribed",
|
||||
"CEvtContactSubError",
|
||||
"CEvtContactSubSummary",
|
||||
"CEvtContactSwitch",
|
||||
"CEvtCustomChatEvent",
|
||||
"CEvtGroupMemberRatchetSync",
|
||||
"CEvtGroupMemberSwitch",
|
||||
"CEvtHostConnected",
|
||||
"CEvtHostDisconnected",
|
||||
"CEvtNetworkStatus",
|
||||
"CEvtNetworkStatuses",
|
||||
"CEvtNewRemoteHost",
|
||||
"CEvtNoMemberContactCreating",
|
||||
"CEvtNtfMessage",
|
||||
"CEvtRcvFileAcceptedSndCancelled", -- only sent with legacy SMP files when they are cancelled
|
||||
"CEvtRcvFileProgressXFTP",
|
||||
"CEvtRcvStandaloneFileComplete",
|
||||
"CEvtRemoteCtrlFound",
|
||||
"CEvtRemoteCtrlSessionCode",
|
||||
"CEvtRemoteCtrlStopped",
|
||||
"CEvtRemoteHostConnected",
|
||||
"CEvtRemoteHostSessionCode",
|
||||
"CEvtRemoteHostStopped",
|
||||
"CEvtSndFileComplete", -- legacy SMP files
|
||||
"CEvtSndFileProgressXFTP",
|
||||
"CEvtSndFileRcvCancelled", -- legacy SMP files
|
||||
"CEvtSndFileRedirectStartXFTP",
|
||||
"CEvtSndFileStart", -- legacy SMP files
|
||||
"CEvtSndStandaloneFileComplete",
|
||||
"CEvtSubscriptionEnd",
|
||||
"CEvtTerminalEvent",
|
||||
"CEvtTimedAction",
|
||||
"CEvtUnknownMemberAnnounced",
|
||||
"CEvtUnknownMemberBlocked",
|
||||
"CEvtUnknownMemberCreated",
|
||||
"CEvtUserAcceptedGroupSent", -- repeat group invitation after it was accepted by the user
|
||||
"CEvtUserContactSubSummary"
|
||||
]
|
||||
@@ -0,0 +1,164 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module API.Docs.Generate where
|
||||
|
||||
import API.Docs.Commands
|
||||
import API.Docs.Events
|
||||
import API.Docs.Responses
|
||||
import API.Docs.Syntax
|
||||
import API.Docs.Syntax.Types
|
||||
import API.Docs.Types
|
||||
import API.TypeInfo
|
||||
import Data.Char (isSpace, isUpper, toLower, toUpper)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
|
||||
commandsDocFile :: FilePath
|
||||
commandsDocFile = "./bots/api/COMMANDS.md"
|
||||
|
||||
eventsDocFile :: FilePath
|
||||
eventsDocFile = "./bots/api/EVENTS.md"
|
||||
|
||||
typesDocFile :: FilePath
|
||||
typesDocFile = "./bots/api/TYPES.md"
|
||||
|
||||
commandsDocText :: Text
|
||||
commandsDocText =
|
||||
("# API Commands and Responses\n\n" <> autoGenerated <> "\n")
|
||||
<> foldMap commandCatTOC chatCommandsDocs
|
||||
<> "\n---\n"
|
||||
<> foldMap commandCatText chatCommandsDocs
|
||||
where
|
||||
commandCatTOC CCCategory {categoryName, commands} =
|
||||
(T.pack $ "\n" <> withLink "" categoryName <> "\n")
|
||||
<> foldMap commandTOC commands
|
||||
where
|
||||
commandTOC CCDoc {commandType = ATUnionMember tag _} = T.pack $ "- " <> withLink "" (fstToUpper tag) <> "\n"
|
||||
commandCatText CCCategory {categoryName, categoryDescr, commands} =
|
||||
(T.pack $ "\n\n## " <> categoryName <> "\n\n" <> categoryDescr <> "\n")
|
||||
<> foldMap commandDocText commands
|
||||
where
|
||||
commandDocText CCDoc {commandType = ATUnionMember tag params, commandDescr, network, syntax, responses, errors} =
|
||||
("\n\n### " <> T.pack (fstToUpper tag) <> "\n\n" <> commandDescr <> "\n\n*Network usage*: " <> networkUsage <> ".\n")
|
||||
<> (if null params then "" else paramsText)
|
||||
<> (if syntax == "" then "" else syntaxText (tag, params) syntax)
|
||||
<> (if length responses > 1 then "\n**Responses**:\n" else "\n**Response**:\n")
|
||||
<> foldMap responseText responses
|
||||
<> (if null errors then "" else "\n**Errors**:\n")
|
||||
<> foldMap errorText errors
|
||||
<> "\n---\n"
|
||||
where
|
||||
networkUsage = case network of
|
||||
Nothing -> "no"
|
||||
Just UNInteractive -> "interactive"
|
||||
Just UNBackground -> "background"
|
||||
paramsText = "\n**Parameters**:\n" <> fieldsText "./TYPES.md" params
|
||||
responseText CRDoc {responseType = ATUnionMember tag fields, responseDescr} =
|
||||
(T.pack $ "\n" <> fstToUpper tag <> ": " <> respDescr <> ".\n")
|
||||
<> ("- type: \"" <> T.pack tag <> "\"\n")
|
||||
<> fieldsText "./TYPES.md" fields
|
||||
where
|
||||
respDescr = if null responseDescr then camelToSpace tag else responseDescr
|
||||
errorText (TD err descr) =
|
||||
let descr' = if null descr then camelToSpace err else descr
|
||||
in T.pack $ "- " <> fstToUpper err <> ": " <> descr' <> ".\n"
|
||||
|
||||
syntaxText :: TypeAndFields -> Expr -> Text
|
||||
syntaxText r syntax =
|
||||
"\n**Syntax**:\n"
|
||||
<> "\n```\n" <> docSyntaxText r syntax <> "\n```\n"
|
||||
<> (if isConst syntax then "" else "\n```javascript\n" <> jsSyntaxText r syntax <> " // JavaScript\n```\n")
|
||||
<> (if isConst syntax then "" else "\n```python\n" <> pySyntaxText r syntax <> " # Python\n```\n")
|
||||
|
||||
camelToSpace :: String -> String
|
||||
camelToSpace [] = []
|
||||
camelToSpace (x : xs) = toUpper x : go xs
|
||||
where
|
||||
go [] = []
|
||||
go (y : ys)
|
||||
| isUpper y = ' ' : toLower y : go ys
|
||||
| otherwise = y : go ys
|
||||
|
||||
eventsDocText :: Text
|
||||
eventsDocText =
|
||||
("# API Events\n\n" <> autoGenerated <> "\n")
|
||||
<> foldMap eventCatTOC chatEventsDocs
|
||||
<> "\n---\n"
|
||||
<> foldMap eventCatText chatEventsDocs
|
||||
where
|
||||
eventCatTOC CECategory {categoryName, mainEvents, otherEvents} =
|
||||
(T.pack $ "\n" <> withLink "" categoryName <> "\n")
|
||||
<> (if hasOthers then "- Main event" <> plural mainEvents <> "\n" else "")
|
||||
<> foldMap eventTOC mainEvents
|
||||
<> (if hasOthers then "- Other event" <> plural otherEvents <> "\n" <> foldMap eventTOC otherEvents else "")
|
||||
where
|
||||
eventTOC CEDoc {eventType = ATUnionMember tag _} = T.pack $ indent <> "- " <> withLink "" (fstToUpper tag) <> "\n"
|
||||
hasOthers = not $ null otherEvents
|
||||
indent = if hasOthers then " " else ""
|
||||
eventCatText CECategory {categoryName, categoryDescr, mainEvents, otherEvents} =
|
||||
(T.pack $ "\n\n## " <> categoryName <> "\n\n" <> categoryDescr <> "\n")
|
||||
<> foldMap eventDocText (mainEvents ++ otherEvents)
|
||||
where
|
||||
eventDocText CEDoc {eventType = ATUnionMember tag fields, eventDescr} =
|
||||
(T.pack $ "\n\n### " <> fstToUpper tag <> "\n\n" <> evtDescr)
|
||||
<> "\n\n**Record type**:\n"
|
||||
<> ("- type: \"" <> T.pack tag <> "\"\n")
|
||||
<> fieldsText "./TYPES.md" fields
|
||||
<> "\n---\n"
|
||||
where
|
||||
evtDescr = if null eventDescr then camelToSpace tag <> "." else eventDescr
|
||||
plural evts = if length evts == 1 then "" else "s"
|
||||
|
||||
typesDocText :: Text
|
||||
typesDocText =
|
||||
("# API Types\n\n" <> autoGenerated <> "\n")
|
||||
<> (foldMap (\t -> T.pack $ "\n- " <> withLink "" (docTypeName t)) chatTypesDocs <> "\n")
|
||||
<> foldMap typeDocText chatTypesDocs
|
||||
where
|
||||
typeDocText CTDoc {typeDef = td@APITypeDef {typeName' = name, typeDef}, typeDescr, typeSyntax} =
|
||||
("\n\n---\n\n## " <> T.pack name <> "\n")
|
||||
<> (if T.null typeDescr then "" else "\n" <> typeDescr <> "\n")
|
||||
<> typeDefText typeDef
|
||||
<> (if typeSyntax == "" then "" else syntaxText (name, self : typeFields) typeSyntax)
|
||||
where
|
||||
self = APIRecordField "self" (ATDef td)
|
||||
typeFields = case typeDef of
|
||||
ATDRecord fs -> L.toList fs
|
||||
ATDUnion ms -> APIRecordField "type" tagType : concatMap (\(ATUnionMember _ fs) -> fs) ms
|
||||
where
|
||||
tagType = ATDef $ APITypeDef (name <> ".type") $ ATDEnum tags
|
||||
tags = L.map (\(ATUnionMember tag _) -> tag) ms
|
||||
ATDEnum _ -> []
|
||||
typeDefText = \case
|
||||
ATDRecord fields -> "\n**Record type**:\n" <> fieldsText "" (L.toList fields)
|
||||
ATDEnum cs -> "\n**Enum type**:\n" <> foldMap (\m -> "- \"" <> T.pack m <> "\"\n") cs
|
||||
ATDUnion cs -> "\n**Discriminated union type**:\n" <> foldMap constrText cs
|
||||
where
|
||||
constrText (ATUnionMember tag fields) =
|
||||
("\n" <> T.pack (fstToUpper tag) <> ":\n")
|
||||
<> ("- type: \"" <> T.pack tag <> "\"\n")
|
||||
<> fieldsText "" fields
|
||||
|
||||
fieldsText :: FilePath -> [APIRecordField] -> Text
|
||||
fieldsText docPath = foldMap $ T.pack . fieldText
|
||||
where
|
||||
fieldText (APIRecordField name t) = "- " <> name <> ": " <> typeText t <> "\n"
|
||||
typeText = \case
|
||||
ATPrim (PT t) -> t
|
||||
ATDef (APITypeDef t _) -> withLink docPath t
|
||||
ATRef t -> withLink docPath t
|
||||
ATOptional t -> typeText t <> "?"
|
||||
ATArray {elemType} -> "[" <> typeText elemType <> "]"
|
||||
ATMap (PT t) valueType -> "{" <> t <> " : " <> typeText valueType <> "}"
|
||||
|
||||
autoGenerated :: Text
|
||||
autoGenerated = "This file is generated automatically."
|
||||
|
||||
withLink :: FilePath -> String -> String
|
||||
withLink docPath s = "[" <> s <> "](" <> docPath <> "#" <> headerAnchor s <> ")"
|
||||
where
|
||||
headerAnchor = map $ \c -> if isSpace c then '-' else toLower c
|
||||
@@ -0,0 +1,206 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module API.Docs.Responses where
|
||||
|
||||
import API.Docs.Types
|
||||
import API.TypeInfo
|
||||
import Data.List (find)
|
||||
import GHC.Generics
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Messaging.Parsers (dropPrefix)
|
||||
|
||||
data CRDoc = CRDoc
|
||||
{ consName :: ConsName,
|
||||
responseType :: ATUnionMember,
|
||||
responseDescr :: String
|
||||
}
|
||||
|
||||
instance ConstructorName CRDoc where consName' CRDoc {consName} = consName
|
||||
|
||||
chatResponsesDocs :: [CRDoc]
|
||||
chatResponsesDocs = map toResp chatResponsesDocsData
|
||||
where
|
||||
toResp (consName, responseDescr)
|
||||
| consName == "CRChatCmdError" =
|
||||
let field = toAPIField consName $ FieldInfo "chatError" (ti "ChatError")
|
||||
responseType = ATUnionMember (dropPrefix "CR" consName) [field]
|
||||
in CRDoc {consName, responseType, responseDescr}
|
||||
| otherwise = case find ((consName ==) . consName') chatResponsesTypeInfo of
|
||||
Just RecordTypeInfo {fieldInfos} ->
|
||||
let fields = map (toAPIField consName) fieldInfos
|
||||
responseType = ATUnionMember (dropPrefix "CR" consName) fields
|
||||
in CRDoc {consName, responseType, responseDescr}
|
||||
Nothing -> error $ "Missing response type info for " <> consName
|
||||
|
||||
deriving instance Generic ChatResponse
|
||||
|
||||
chatResponsesTypeInfo :: [RecordTypeInfo]
|
||||
chatResponsesTypeInfo = recordTypesInfo @ChatResponse
|
||||
|
||||
chatResponsesDocsData :: [(ConsName, String)]
|
||||
chatResponsesDocsData =
|
||||
[ ("CRAcceptingContactRequest", "Contact request accepted"),
|
||||
("CRActiveUser", "Active user profile"),
|
||||
("CRChatItemNotChanged", "Message not changed"),
|
||||
("CRChatItemReaction", "Message reaction"),
|
||||
("CRChatItemUpdated", "Message updated"),
|
||||
("CRChatItemsDeleted", "Messages deleted"),
|
||||
("CRCmdOk", "Ok"),
|
||||
("CRChatCmdError", "Command error"), -- only used in WebSockets API, Haskell code uses Either, with error in Left
|
||||
("CRConnectionPlan", "Connection link information"),
|
||||
("CRContactAlreadyExists", ""),
|
||||
("CRContactConnectionDeleted", "Connection deleted"),
|
||||
("CRContactDeleted", ""),
|
||||
("CRContactRequestRejected", ""),
|
||||
("CRContactsList", "Contacts"),
|
||||
("CRGroupDeletedUser", "User deleted group"),
|
||||
("CRGroupLink", ""),
|
||||
("CRGroupLinkCreated", ""),
|
||||
("CRGroupLinkDeleted", ""),
|
||||
("CRGroupsList", "Groups"),
|
||||
("CRInvitation", "One-time invitation"),
|
||||
("CRLeftMemberUser", "User left group"),
|
||||
("CRMemberAccepted", "Member accepted to group"),
|
||||
("CRMembersBlockedForAllUser", "Members blocked for all by admin"),
|
||||
("CRMembersRoleUser", "Members role changed by user"),
|
||||
("CRNewChatItems", "New messages"),
|
||||
("CRRcvFileAccepted", "File accepted to be received"),
|
||||
("CRRcvFileAcceptedSndCancelled", "File accepted, but no longer sent"),
|
||||
("CRRcvFileCancelled", "Cancelled receiving file"),
|
||||
("CRSentConfirmation", "Confirmation sent to one-time invitation"),
|
||||
("CRSentGroupInvitation", "Group invitation sent"),
|
||||
("CRSentInvitation", "Invitation sent to contact address"),
|
||||
("CRSndFileCancelled", "Cancelled sending file"),
|
||||
("CRUserAcceptedGroupSent", "User accepted group invitation"),
|
||||
("CRUserContactLink", "User contact address"),
|
||||
("CRUserContactLinkCreated", "User contact address created"),
|
||||
("CRUserContactLinkDeleted", "User contact address deleted"),
|
||||
("CRUserContactLinkUpdated", "User contact address updated"),
|
||||
("CRUserDeletedMembers", "Members deleted"),
|
||||
("CRUserProfileUpdated", "User profile updated"),
|
||||
("CRUsersList", "Users")
|
||||
-- ("CRApiChat", "Chat and messages"),
|
||||
-- ("CRApiChats", "Chats with the most recent messages"),
|
||||
-- ("CRChatCleared", ""),
|
||||
-- ("CRChatItemInfo", "Message information"),
|
||||
-- ("CRChatItems", "The most recent messages"),
|
||||
-- ("CRConnectionAliasUpdated", ""),
|
||||
-- ("CRContactAliasUpdated", ""),
|
||||
-- ("CRContactPrefsUpdated", "Contact preferences updated"),
|
||||
-- ("CRContactRatchetSyncStarted", "Contact encryption synchronization started"),
|
||||
-- ("CRGroupAliasUpdated", ""),
|
||||
-- ("CRGroupCreated", ""),
|
||||
-- ("CRGroupMemberRatchetSyncStarted", "Member encryption synchronization started"),
|
||||
-- ("CRGroupMembers", ""),
|
||||
-- ("CRGroupUpdated", ""),
|
||||
-- ("CRItemsReadForChat", "Messages marked as read"),
|
||||
-- ("CRReactionMembers", "Members who set reaction on the message"),
|
||||
]
|
||||
|
||||
undocumentedResponses :: [ConsName]
|
||||
undocumentedResponses =
|
||||
[ "CRAgentQueuesInfo",
|
||||
"CRAgentServersSummary",
|
||||
"CRAgentSubs",
|
||||
"CRAgentSubsDetails",
|
||||
"CRAgentSubsTotal",
|
||||
"CRAgentWorkersDetails",
|
||||
"CRAgentWorkersSummary",
|
||||
"CRApiChat",
|
||||
"CRApiChats",
|
||||
"CRAppSettings",
|
||||
"CRArchiveExported",
|
||||
"CRArchiveImported",
|
||||
"CRBroadcastSent",
|
||||
"CRCallInvitations",
|
||||
"CRChatCleared",
|
||||
"CRChatHelp",
|
||||
"CRChatItemId",
|
||||
"CRChatItemInfo",
|
||||
"CRChatItems",
|
||||
"CRChatItemTTL",
|
||||
"CRChatRunning",
|
||||
"CRChats",
|
||||
"CRChatStarted",
|
||||
"CRChatStopped",
|
||||
"CRChatTags",
|
||||
"CRConnectionAliasUpdated",
|
||||
"CRConnectionIncognitoUpdated",
|
||||
"CRConnectionUserChanged",
|
||||
"CRConnectionVerified",
|
||||
"CRConnNtfMessages",
|
||||
"CRContactAliasUpdated",
|
||||
"CRContactCode",
|
||||
"CRContactInfo",
|
||||
"CRContactPrefsUpdated",
|
||||
"CRContactRatchetSyncStarted",
|
||||
"CRContactSwitchAborted",
|
||||
"CRContactSwitchStarted",
|
||||
"CRContactUserChanged",
|
||||
"CRCurrentRemoteHost",
|
||||
"CRCustomChatResponse",
|
||||
"CRDebugLocks",
|
||||
"CRFileTransferStatus",
|
||||
"CRFileTransferStatusXFTP",
|
||||
"CRForwardPlan",
|
||||
"CRGroupAliasUpdated",
|
||||
"CRGroupChatItemsDeleted",
|
||||
"CRGroupCreated",
|
||||
"CRGroupDescription",
|
||||
"CRGroupInfo",
|
||||
"CRGroupMemberCode",
|
||||
"CRGroupMemberInfo",
|
||||
"CRGroupMemberRatchetSyncStarted",
|
||||
"CRGroupMembers",
|
||||
"CRGroupMemberSwitchAborted",
|
||||
"CRGroupMemberSwitchStarted",
|
||||
"CRGroupProfile",
|
||||
"CRGroupUpdated",
|
||||
"CRGroupUserChanged",
|
||||
"CRItemsReadForChat",
|
||||
"CRJoinedGroupMember",
|
||||
"CRMemberSupportChatDeleted",
|
||||
"CRMemberSupportChats",
|
||||
"CRNetworkConfig",
|
||||
"CRNetworkStatuses",
|
||||
"CRNewMemberContact",
|
||||
"CRNewMemberContactSentInv",
|
||||
"CRNewPreparedChat",
|
||||
"CRNtfConns",
|
||||
"CRNtfToken",
|
||||
"CRNtfTokenStatus",
|
||||
"CRQueueInfo",
|
||||
"CRRcvStandaloneFileCreated",
|
||||
"CRReactionMembers",
|
||||
"CRRemoteCtrlConnected",
|
||||
"CRRemoteCtrlConnecting",
|
||||
"CRRemoteCtrlList",
|
||||
"CRRemoteFileStored",
|
||||
"CRRemoteHostList",
|
||||
"CRRemoteHostStarted",
|
||||
"CRSentInvitationToContact",
|
||||
"CRServerOperatorConditions",
|
||||
"CRServerTestResult",
|
||||
"CRSlowSQLQueries",
|
||||
"CRSndStandaloneFileCreated",
|
||||
"CRSQLResult",
|
||||
"CRStandaloneFileInfo",
|
||||
"CRStartedConnectionToContact",
|
||||
"CRStartedConnectionToGroup",
|
||||
"CRTagsUpdated",
|
||||
"CRUsageConditions",
|
||||
"CRUserPrivacy",
|
||||
"CRUserProfile",
|
||||
"CRUserProfileImage",
|
||||
"CRUserProfileNoChange",
|
||||
"CRUserServers",
|
||||
"CRUserServersValidation",
|
||||
"CRVersionInfo",
|
||||
"CRWelcome"
|
||||
]
|
||||
@@ -0,0 +1,220 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module API.Docs.Syntax where
|
||||
|
||||
import API.Docs.Syntax.Types
|
||||
import API.Docs.Types
|
||||
import API.TypeInfo
|
||||
import Data.List (find, intercalate)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
|
||||
docSyntaxText :: TypeAndFields -> Expr -> Text
|
||||
docSyntaxText r@(tag, _) = T.pack . go Nothing
|
||||
where
|
||||
go param = \case
|
||||
Concat exs -> concatMap (go param) exs
|
||||
Const s -> s
|
||||
Param p ->
|
||||
withParamType r param p $ \case
|
||||
ATDef td -> strSyntax td
|
||||
ATOptional (ATDef td) -> strSyntax td
|
||||
_ -> defSyntax Nothing
|
||||
where
|
||||
strSyntax (APITypeDef typeName t)
|
||||
| typeHasSyntax typeName = "<str(" <> paramName param p <> ")>"
|
||||
| otherwise = defSyntax (Just t)
|
||||
defSyntax = \case
|
||||
Just (ATDEnum ms) -> intercalate "|" $ L.toList ms
|
||||
_ -> "<" <> paramName param p <> ">"
|
||||
Optional exN exJ p ->
|
||||
withParamType r param p $ \case
|
||||
ATOptional {}
|
||||
| exN == "" -> "[" <> go (Just p) exJ <> "]"
|
||||
| otherwise -> go param exN <> "|" <> go (Just p) exJ
|
||||
_ -> paramError r param p "is not optional"
|
||||
Choice p opts else' ->
|
||||
withParamType r param p $ \case
|
||||
ATDef td -> choiceSyntax td
|
||||
ATOptional (ATDef td) -> choiceSyntax td
|
||||
_ -> paramError r param p "is not union type"
|
||||
where
|
||||
choiceSyntax = \case
|
||||
APITypeDef _ (ATDUnion _) -> choices
|
||||
APITypeDef _ (ATDEnum _) -> choices
|
||||
_ -> paramError r param p "is not union or enum type"
|
||||
choices = (if null optsSyntax then "" else optsSyntax <> "|") <> go param else'
|
||||
where
|
||||
optsSyntax = intercalate "|" (mapMaybe ((\s -> if null s then Nothing else Just s) . go param . snd) (L.toList opts))
|
||||
Join c p ->
|
||||
withParamType r param p $ \case
|
||||
ATArray {} -> let n = paramName param p in "<" <> n <> "[0]>[" <> [c] <> "<" <> n <> "[1]>...]"
|
||||
_ -> paramError r param p "is not array"
|
||||
Json p ->
|
||||
withParamType r param p $ \_ -> "<json(" <> paramName param p <> ")>"
|
||||
OnOff p -> withBoolParam r param p "on|off"
|
||||
OnOffParam name p def_
|
||||
| null name -> error $ fstToUpper tag <> ": on/off parameter " <> paramName param p <> " has empty name"
|
||||
| otherwise -> case def_ of
|
||||
Just def -> withOptBoolParam r param p $ \_ -> "[ " <> name <> "=" <> onOff <> "]"
|
||||
where
|
||||
onOff = if def then "off" else "on"
|
||||
Nothing -> withOptBoolParam r param p $ \optional -> if optional then "[" <> res <> "]" else res
|
||||
where
|
||||
res = " " <> name <> "=on|off"
|
||||
|
||||
typeHasSyntax :: String -> Bool
|
||||
typeHasSyntax typeName = case find ((typeName ==) . docTypeName) chatTypesDocs of
|
||||
Just CTDoc {typeSyntax} -> typeSyntax /= ""
|
||||
_ -> False
|
||||
|
||||
paramError :: TypeAndFields -> Maybe ExprParam -> ExprParam -> String -> String
|
||||
paramError (tag, _) param p err = error $ fstToUpper tag <> ": " <> paramName param p <> " " <> err
|
||||
|
||||
withParamType :: TypeAndFields -> Maybe ExprParam -> ExprParam -> (APIType -> String) -> String
|
||||
withParamType r@(_, params) param p f = case find ((paramName param p ==) . fieldName') params of
|
||||
Just APIRecordField {typeInfo} -> f typeInfo
|
||||
Nothing -> paramError r param p "is unknown"
|
||||
|
||||
withBoolParam :: TypeAndFields -> Maybe ExprParam -> ExprParam -> String -> String
|
||||
withBoolParam r param p s =
|
||||
withParamType r param p $ \case
|
||||
ATPrim (PT TBool) -> s
|
||||
_ -> paramError r param p "is not boolean"
|
||||
|
||||
withOptBoolParam :: TypeAndFields -> Maybe ExprParam -> ExprParam -> (Bool -> String) -> String
|
||||
withOptBoolParam r param p f =
|
||||
withParamType r param p $ \case
|
||||
ATPrim (PT TBool) -> f False
|
||||
(ATOptional (ATPrim (PT TBool))) -> f True
|
||||
_ -> paramError r param p "is not [optional] boolean"
|
||||
|
||||
jsSyntaxText :: TypeAndFields -> Expr -> Text
|
||||
jsSyntaxText r = T.replace "' + '" "" . T.pack . go Nothing True
|
||||
where
|
||||
go param top = \case
|
||||
Concat exs -> intercalate " + " $ map (go param False) $ L.toList exs
|
||||
Const s -> "'" <> escapeChar '\'' s <> "'"
|
||||
Param p ->
|
||||
withParamType r param p $ \case
|
||||
ATDef td -> toStringSyntax td
|
||||
ATOptional (ATDef td) -> toStringSyntax td
|
||||
_ -> paramName param p
|
||||
where
|
||||
toStringSyntax (APITypeDef typeName _)
|
||||
| typeHasSyntax typeName = paramName param p <> ".toString()"
|
||||
| otherwise = paramName param p
|
||||
Optional exN exJ p -> open <> n <> " ? " <> go (Just p) False exJ <> " : " <> nothing <> close
|
||||
where
|
||||
n = paramName param p
|
||||
nothing = if exN == "" then "''" else go param False exN
|
||||
Choice p opts else' ->
|
||||
withParamType r param p $ \case
|
||||
ATDef td -> choiceSyntax td
|
||||
ATOptional (ATDef td) -> choiceSyntax td
|
||||
_ -> paramError r param p "is not union type"
|
||||
where
|
||||
choiceSyntax = \case
|
||||
APITypeDef _ (ATDUnion _) -> choices "type"
|
||||
APITypeDef _ (ATDEnum _) -> choices "self"
|
||||
_ -> paramError r param p "is not union type"
|
||||
choices var = open <> optsSyntax <> " : " <> go param top else' <> close
|
||||
where
|
||||
optsSyntax = intercalate " : " $ map (\(tag, ex) -> var <> " == '" <> tag <> "' ? " <> go param top ex) $ L.toList opts
|
||||
Join c p -> paramName param p <> ".join('" <> [c] <> "')"
|
||||
Json p -> "JSON.stringify(" <> paramName param p <> ")"
|
||||
OnOff p -> open <> paramName param p <> " ? 'on' : 'off'" <> close
|
||||
OnOffParam name p def_ -> case def_ of
|
||||
Nothing ->
|
||||
withOptBoolParam r param p $ \optional ->
|
||||
if optional
|
||||
then "(typeof " <> n <> " == 'boolean' ? " <> res <> " : '')"
|
||||
else res
|
||||
where
|
||||
n = paramName param p
|
||||
res = "' " <> name <> "=' + (" <> n <> " ? 'on' : 'off')"
|
||||
Just def
|
||||
| def -> open <> "!" <> n <> " ? ' " <> name <> "=off' : ''" <> close
|
||||
| otherwise -> open <> n <> " ? ' " <> name <> "=on' : ''" <> close
|
||||
where
|
||||
n = paramName param p
|
||||
where
|
||||
open = if top then "" else "("
|
||||
close = if top then "" else ")"
|
||||
|
||||
escapeChar :: Char -> String -> String
|
||||
escapeChar c s
|
||||
| c `elem` s = concatMap (\c' -> if c' == c then ['\\', c] else [c]) s
|
||||
| otherwise = s
|
||||
|
||||
pySyntaxText :: TypeAndFields -> Expr -> Text
|
||||
pySyntaxText r = T.pack . go Nothing True
|
||||
where
|
||||
go param top = \case
|
||||
Concat exs -> intercalate " + " $ map (go param False) $ L.toList exs
|
||||
Const s -> "'" <> escapeChar '\'' s <> "'"
|
||||
Param p ->
|
||||
withParamType r param p $ \case
|
||||
ATPrim (PT TString) -> paramName param p
|
||||
ATOptional (ATPrim (PT TString)) -> paramName param p
|
||||
_ -> "str(" <> paramName param p <> ")"
|
||||
Optional exN exJ p -> open <> "(" <> go (Just p) False exJ <> ") if " <> n <> " is not None else " <> nothing <> close
|
||||
where
|
||||
n = paramName param p
|
||||
nothing = if exN == "" then "''" else go param False exN
|
||||
Choice p opts else' ->
|
||||
withParamType r param p $ \case
|
||||
ATDef td -> choiceSyntax td
|
||||
ATOptional (ATDef td) -> choiceSyntax td
|
||||
_ -> paramError r param p "is not union type"
|
||||
where
|
||||
choiceSyntax = \case
|
||||
APITypeDef _ (ATDUnion _) -> choices "type"
|
||||
APITypeDef _ (ATDEnum _) -> choices "self"
|
||||
_ -> paramError r param p "is not union type"
|
||||
choices var = open <> optsSyntax <> " else " <> go param top else' <> close
|
||||
where
|
||||
optsSyntax = intercalate " else " $ map (\(tag, ex) -> go param top ex <> " if " <> var' <> " == '" <> tag <> "'") $ L.toList opts
|
||||
var' =
|
||||
withParamType r param var $ \case
|
||||
ATPrim (PT TString) -> var
|
||||
ATOptional (ATPrim (PT TString)) -> var
|
||||
_ -> "str(" <> var <> ")"
|
||||
Join c p ->
|
||||
withParamType r param p $ \case
|
||||
ATArray {elemType = ATPrim (PT TString)} -> "'" <> [c] <> "'.join(" <> paramName param p <> ")"
|
||||
_ -> "'" <> [c] <> "'.join(map(str, " <> paramName param p <> "))"
|
||||
Json p -> "json.dumps(" <> paramName param p <> ")"
|
||||
OnOff p -> open <> "'on' if " <> paramName param p <> " else 'off'" <> close
|
||||
OnOffParam name p def_ -> case def_ of
|
||||
Nothing ->
|
||||
withOptBoolParam r param p $ \optional ->
|
||||
if optional
|
||||
then "((" <> res <> ") if " <> n <> " is not None else '')"
|
||||
else res
|
||||
where
|
||||
n = paramName param p
|
||||
res = "' " <> name <> "=' + ('on' if " <> n <> " else 'off')"
|
||||
Just def
|
||||
| def -> open <> "' " <> name <> "=off' if not " <> n <> " else ''" <> close
|
||||
| otherwise -> open <> "' " <> name <> "=on' if " <> n <> " else ''" <> close
|
||||
where
|
||||
n = paramName param p
|
||||
where
|
||||
open = if top then "" else "("
|
||||
close = if top then "" else ")"
|
||||
|
||||
paramName :: Maybe ExprParam -> ExprParam -> String
|
||||
paramName param_ p = case param_ of
|
||||
Just param | p == "$0" -> param
|
||||
_ -> p
|
||||
@@ -0,0 +1,30 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module API.Docs.Syntax.Types where
|
||||
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Semigroup
|
||||
import Data.String
|
||||
|
||||
type ExprParam = String -- param name
|
||||
|
||||
data Expr
|
||||
= Concat (NonEmpty Expr)
|
||||
| Const String
|
||||
| Param ExprParam
|
||||
| Optional Expr Expr ExprParam -- Nothing expr, Just expr (using [$0] as ExprParam), optional param
|
||||
| Choice ExprParam (NonEmpty (String, Expr)) Expr -- union type param, choices for "type" tags, else
|
||||
| Join Char ExprParam
|
||||
| Json ExprParam
|
||||
| OnOff ExprParam -- does not include leading space
|
||||
| OnOffParam String ExprParam (Maybe Bool) -- name, param, default. Includes leading space in all cases. Name must not be empty
|
||||
deriving (Eq, Show)
|
||||
|
||||
isConst :: Expr -> Bool
|
||||
isConst = \case
|
||||
Const _ -> True
|
||||
_ -> False
|
||||
|
||||
instance IsString Expr where fromString = Const
|
||||
|
||||
instance Semigroup Expr where sconcat = Concat
|
||||
@@ -0,0 +1,557 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module API.Docs.Types where
|
||||
|
||||
import API.Docs.Syntax.Types
|
||||
import API.TypeInfo
|
||||
import Data.Bifunctor (second)
|
||||
import Data.Char (isUpper, toLower, toUpper)
|
||||
import Data.List (find, mapAccumL, sortOn)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Set as S
|
||||
import Data.Text (Text)
|
||||
import GHC.Generics
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Chat.Messages
|
||||
import Simplex.Chat.Messages.CIContent
|
||||
import Simplex.Chat.Messages.CIContent.Events
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Store.Groups
|
||||
import Simplex.Chat.Store.Profiles
|
||||
import Simplex.Chat.Store.Shared
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.Types.UITheme
|
||||
import Simplex.FileTransfer.Transport
|
||||
import Simplex.FileTransfer.Types hiding (RcvFileStatus) -- the type with the same name is used in simplex-chat.
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Client
|
||||
import Simplex.Messaging.Crypto.File
|
||||
import Simplex.Messaging.Parsers (dropPrefix, fstToLower)
|
||||
import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), ProxyError (..))
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.RemoteControl.Types
|
||||
import System.Console.ANSI.Types (Color (..))
|
||||
|
||||
data CTDoc = CTDoc
|
||||
{ typeDef :: APITypeDef,
|
||||
typeSyntax :: Expr, -- syntax for types used in commands
|
||||
typeDescr :: Text
|
||||
}
|
||||
|
||||
docTypeName :: CTDoc -> String
|
||||
docTypeName CTDoc {typeDef = APITypeDef name _} = name
|
||||
|
||||
toAPIField :: ConsName -> FieldInfo -> APIRecordField
|
||||
toAPIField typeName = snd . toAPIField_ typeName (S.empty, chatTypeDefs)
|
||||
|
||||
chatTypeDefs :: M.Map String APITypeDef
|
||||
chatTypeDefs = M.fromList $ map (\CTDoc {typeDef = td@(APITypeDef name _)} -> (name, td)) chatTypesDocs
|
||||
|
||||
chatTypesDocs :: [CTDoc]
|
||||
chatTypesDocs = sortOn docTypeName $! snd $! mapAccumL toCTDoc (S.empty, M.empty) chatTypesDocsData
|
||||
where
|
||||
toCTDoc !tds sumTypeInfo@(STI typeName _, _, _, _, typeSyntax, typeDescr) =
|
||||
let (tds', td_) = toTypeDef tds sumTypeInfo
|
||||
in case td_ of
|
||||
Just typeDef -> (tds', CTDoc {typeDef, typeSyntax, typeDescr})
|
||||
Nothing -> error $ "Recursive type: " <> typeName
|
||||
|
||||
toTypeDef :: (S.Set String, M.Map String APITypeDef) -> (SumTypeInfo, SumTypeJsonEncoding, String, [ConsName], Expr, Text) -> ((S.Set String, M.Map String APITypeDef), Maybe APITypeDef)
|
||||
toTypeDef acc@(!visited, !typeDefs) (STI typeName allConstrs, jsonEncoding, consPrefix, hideConstrs, _, _) =
|
||||
let constrs = filter ((`notElem` hideConstrs) . consName') allConstrs
|
||||
in case M.lookup typeName typeDefs of
|
||||
Just td -> (acc, Just td)
|
||||
Nothing
|
||||
| S.member typeName visited -> (acc, Nothing)
|
||||
| otherwise -> case jsonEncoding of
|
||||
STRecord -> case constrs of
|
||||
[RecordTypeInfo {fieldInfos}] ->
|
||||
let fields = fromMaybe (error $ "Record type without fields: " <> typeName) $ L.nonEmpty fieldInfos
|
||||
((visited', typeDefs'), fields') = mapAccumL (toAPIField_ typeName) (S.insert typeName visited, typeDefs) fields
|
||||
td = APITypeDef typeName $ ATDRecord fields'
|
||||
in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td)
|
||||
_ -> error $ "Record type with " <> show (length constrs) <> " constructors: " <> typeName
|
||||
STUnion -> if length constrs > 1 then toUnionType constrs else unionError constrs
|
||||
STUnion1 -> if length constrs == 1 then toUnionType constrs else unionError constrs
|
||||
STEnum -> if length constrs > 1 then toEnumType constrs else enumError constrs
|
||||
STEnum1 -> if length constrs == 1 then toEnumType constrs else enumError constrs
|
||||
STEnum' f
|
||||
| length constrs <= 1 -> enumError constrs
|
||||
| null consPrefix -> toEnumType_ f constrs
|
||||
| otherwise -> error $ "Enum type with custom encoding and prefix: " <> typeName
|
||||
where
|
||||
toUnionType constrs =
|
||||
let ((visited', typeDefs'), members) = mapAccumL toUnionMember (S.insert typeName visited, typeDefs) $ fromMaybe (unionError constrs) $ L.nonEmpty constrs
|
||||
td = APITypeDef typeName $ ATDUnion members
|
||||
in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td)
|
||||
toUnionMember tds RecordTypeInfo {consName, fieldInfos} =
|
||||
let memberTag = normalizeConsName consPrefix consName
|
||||
in second (ATUnionMember memberTag) $ mapAccumL (toAPIField_ typeName) tds fieldInfos
|
||||
unionError constrs = error $ "Union type with " <> show (length constrs) <> " constructor(s): " <> typeName
|
||||
toEnumType = toEnumType_ $ normalizeConsName consPrefix
|
||||
toEnumType_ f constrs =
|
||||
let members = L.map toEnumMember $ fromMaybe (enumError constrs) $ L.nonEmpty constrs
|
||||
td = APITypeDef typeName $ ATDEnum members
|
||||
in ((S.insert typeName visited, M.insert typeName td typeDefs), Just td)
|
||||
where
|
||||
toEnumMember RecordTypeInfo {consName, fieldInfos} = case fieldInfos of
|
||||
[] -> f consName
|
||||
_ -> error $ "Enum type with fields in constructor: " <> typeName <> ", " <> consName
|
||||
enumError constrs = error $ "Enum type with " <> show (length constrs) <> " constructor(s): " <> typeName
|
||||
|
||||
toAPIField_ :: ConsName -> (S.Set String, M.Map String APITypeDef) -> FieldInfo -> ((S.Set String, M.Map String APITypeDef), APIRecordField)
|
||||
toAPIField_ typeName tds (FieldInfo fieldName typeInfo) = second (APIRecordField fieldName) $ toAPIType typeInfo
|
||||
where
|
||||
toAPIType :: TypeInfo -> ((S.Set String, M.Map String APITypeDef), APIType)
|
||||
toAPIType = \case
|
||||
TIType (ST name _) -> apiTypeForName name
|
||||
TIOptional tInfo -> second ATOptional $ toAPIType tInfo
|
||||
TIArray {elemType, nonEmpty} -> second (`ATArray`nonEmpty) $ toAPIType elemType
|
||||
TIMap {keyType = ST name _, valueType}
|
||||
| name `elem` primitiveTypes -> second (ATMap (PT name)) $ toAPIType valueType
|
||||
| otherwise -> error $ "Non-primitive key type in " <> typeName <> ", " <> fieldName
|
||||
apiTypeForName :: String -> ((S.Set String, M.Map String APITypeDef), APIType)
|
||||
apiTypeForName name
|
||||
| name `elem` primitiveTypes = (tds, ATPrim $ PT name)
|
||||
| otherwise = case M.lookup name $ snd tds of
|
||||
Just td -> (tds, ATDef td)
|
||||
Nothing -> case find (\(STI name' _, _, _, _, _, _) -> name == name') chatTypesDocsData of
|
||||
Just sumTypeInfo ->
|
||||
let (tds', td_) = toTypeDef tds sumTypeInfo -- recursion to outer function, loops are resolved via type defs map lookup
|
||||
in case td_ of
|
||||
Just td -> (tds', ATDef td)
|
||||
Nothing -> (tds', ATRef name)
|
||||
Nothing -> error $ "Undefined type: " <> name
|
||||
|
||||
data SumTypeJsonEncoding = STRecord | STUnion | STUnion1 | STEnum | STEnum1 | STEnum' (ConsName -> String)
|
||||
|
||||
dropPfxSfx :: String -> String -> ConsName -> String
|
||||
dropPfxSfx pfx sfx = dropSuffix sfx . dropPrefix pfx
|
||||
|
||||
fstToUpper :: String -> String
|
||||
fstToUpper "" = ""
|
||||
fstToUpper (h : t) = toUpper h : t
|
||||
|
||||
consLower :: String -> ConsName -> String
|
||||
consLower pfx = map toLower . dropPrefix pfx
|
||||
|
||||
consSep :: String -> Char -> ConsName -> String
|
||||
consSep pfx sep = foldr (\c s -> if isUpper c then sep : toLower c : s else c : s) "" . dropPrefix pfx
|
||||
|
||||
dropSuffix :: String -> String -> String
|
||||
dropSuffix sfx s =
|
||||
let (s', sfx') = splitAt (length s - length sfx) s
|
||||
in fstToLower $ if sfx' == sfx then s' else s
|
||||
|
||||
normalizeConsName :: String -> ConsName -> ConsName
|
||||
normalizeConsName pfx consName
|
||||
| null pfx && uppercase = consName
|
||||
| null pfx = fstToLower consName
|
||||
| uppercase = map toUpper noPfx
|
||||
| otherwise = noPfx
|
||||
where
|
||||
uppercase = all (\c -> isUpper c || c == '_') consName
|
||||
noPfx = dropPrefix pfx consName
|
||||
|
||||
-- making chatDir optional because clients use CIDirection? instead of CIQDirection (the type is replaced in Types.hs)
|
||||
ciQuoteType :: SumTypeInfo
|
||||
ciQuoteType =
|
||||
let st@(STI _ records) = sti @(CIQuote 'CTDirect)
|
||||
optChatDir f@(FieldInfo n t) = if n == "chatDir" then FieldInfo n (TIOptional t) else f
|
||||
updateRecord (RecordTypeInfo name fields) = RecordTypeInfo name $ map optChatDir fields
|
||||
in st {recordTypes = map updateRecord records} -- need to map even though there is one constructor in this type
|
||||
|
||||
chatTypesDocsData :: [(SumTypeInfo, SumTypeJsonEncoding, String, [ConsName], Expr, Text)]
|
||||
chatTypesDocsData =
|
||||
[ ((sti @(Chat 'CTDirect)) {typeName = "AChat"}, STRecord, "", [], "", ""),
|
||||
((sti @JSONChatInfo) {typeName = "ChatInfo"}, STUnion, "JCInfo", [], "", ""),
|
||||
((sti @JSONCIContent) {typeName = "CIContent"}, STUnion, "JCI", [], "", ""),
|
||||
((sti @JSONCIDeleted) {typeName = "CIDeleted"}, STUnion, "JCID", [], "", ""),
|
||||
((sti @JSONCIDirection) {typeName = "CIDirection"}, STUnion, "JCI", [], "", ""),
|
||||
((sti @JSONCIFileStatus) {typeName = "CIFileStatus"}, STUnion, "JCIFS", [], "", ""),
|
||||
((sti @JSONCIStatus) {typeName = "CIStatus"}, STUnion, "JCIS", [], "", ""),
|
||||
(ciQuoteType, STRecord, "", [], "", ""),
|
||||
(STI "AChatItem" [RecordTypeInfo "AChatItem" [FieldInfo "chatInfo" (ti "ChatInfo"), FieldInfo "chatItem" (ti "ChatItem")]], STRecord, "", [], "", ""),
|
||||
(STI "ACIReaction" [RecordTypeInfo "ACIReaction" [FieldInfo "chatInfo" (ti "ChatInfo"), FieldInfo "chatReaction" (ti "CIReaction")]], STRecord, "", [], "", ""),
|
||||
-- (STI "JSONObject" [], STRecord, "", [], "Arbitrary JSON object."),
|
||||
-- (STI "UTCTime" [], STRecord, "", [], "Timestampe in ISO8601 format as string."),
|
||||
(STI "VersionRange" [RecordTypeInfo "VersionRange" [FieldInfo "minVersion" (ti TInt), FieldInfo "maxVersion" (ti TInt)]], STRecord, "", [], "", ""),
|
||||
(sti @(ChatItem 'CTDirect 'MDSnd), STRecord, "", [], "", ""),
|
||||
(sti @(CIFile 'MDSnd), STRecord, "", [], "", ""),
|
||||
(sti @(CIMeta 'CTDirect 'MDSnd), STRecord, "", [], "", ""),
|
||||
(sti @(CIReaction 'CTDirect 'MDSnd), STRecord, "", [], "", ""),
|
||||
(sti @(ContactUserPref SimplePreference), STUnion, "CUP", [], "", ""),
|
||||
(sti @(ContactUserPreference SimplePreference), STRecord, "", [], "", ""),
|
||||
(sti @(CreatedConnLink 'CMContact), STRecord, "", [], Param "connFullLink" <> Optional "" (" " <> Param "$0") "connShortLink", ""),
|
||||
(sti @AddressSettings, STRecord, "", [], "", ""),
|
||||
(sti @AgentCryptoError, STUnion, "", [], "", ""),
|
||||
(sti @AgentErrorType, STUnion, "", [], "", ""),
|
||||
(sti @AutoAccept, STRecord, "", [], "", ""),
|
||||
(sti @BlockingInfo, STRecord, "", [], "", ""),
|
||||
(sti @BlockingReason, STEnum, "BR", [], "", ""),
|
||||
(sti @BrokerErrorType, STUnion, "", [], "", ""),
|
||||
(sti @BusinessChatInfo, STRecord, "", [], "", ""),
|
||||
(sti @BusinessChatType, STEnum, "BC", [], "", ""),
|
||||
(sti @ChatDeleteMode, STUnion, "CDM", [], Param "type" <> Choice "self" [("messages", "")] (OnOffParam "notify" "notify" (Just True)), ""),
|
||||
(sti @ChatError, STUnion, "Chat", ["ChatErrorDatabase", "ChatErrorRemoteHost", "ChatErrorRemoteCtrl"], "", ""),
|
||||
(sti @ChatErrorType, STUnion, "CE", ["CEContactNotFound", "CEServerProtocol", "CECallState", "CEInvalidChatMessage"], "", ""),
|
||||
(sti @ChatFeature, STEnum, "CF", [], "", ""),
|
||||
(sti @ChatItemDeletion, STRecord, "", [], "", "Message deletion result."),
|
||||
(sti @ChatRef, STRecord, "", [], Param "chatType" <> Param "chatId" <> Optional "" (Param "$0") "chatScope", "Used in API commands. Chat scope can only be passed with groups."),
|
||||
(sti @ChatSettings, STRecord, "", [], "", ""),
|
||||
(sti @ChatStats, STRecord, "", [], "", ""),
|
||||
(sti @ChatType, STEnum, "CT", ["CTContactRequest", "CTContactConnection"], Choice "self" [("contact", "@"), ("group", "#"), ("local", "*")] "", ""),
|
||||
(sti @ChatWallpaper, STRecord, "", [], "", ""),
|
||||
(sti @ChatWallpaperScale, STEnum, "CWS", [], "", ""),
|
||||
(sti @CICallStatus, STEnum, "CISCall", [], "", ""),
|
||||
(sti @CIDeleteMode, STEnum, "CIDM", [], "", ""),
|
||||
(sti @CIForwardedFrom, STUnion, "CIFF", [], "", ""),
|
||||
(sti @CIGroupInvitation, STRecord, "", [], "", ""),
|
||||
(sti @CIGroupInvitationStatus, STEnum, "CIGIS", [], "", ""),
|
||||
(sti @CIMention, STRecord, "", [], "", ""),
|
||||
(sti @CIMentionMember, STRecord, "", [], "", ""),
|
||||
(sti @CIReactionCount, STRecord, "", [], "", ""),
|
||||
(sti @CITimed, STRecord, "", [], "", ""),
|
||||
(sti @Color, STEnum, "", [], "", ""),
|
||||
(sti @CommandError, STUnion, "", [], "", ""),
|
||||
(sti @CommandErrorType, STUnion, "", [], "", ""),
|
||||
(sti @ComposedMessage, STRecord, "", [], "", ""),
|
||||
(sti @Connection, STRecord, "", [], "", ""),
|
||||
(sti @ConnectionEntity, STUnion, "", [], "", ""),
|
||||
(sti @ConnectionErrorType, STUnion, "", [], "", ""),
|
||||
(sti @ConnectionMode, (STEnum' $ take 3 . consLower "CM"), "", [], "", ""),
|
||||
(sti @ConnectionPlan, STUnion, "CP", [], "", ""),
|
||||
(sti @ConnStatus, (STEnum' $ consSep "Conn" '-'), "", [], "", ""),
|
||||
(sti @ConnType, (STEnum' $ consSep "Conn" '_'), "", ["ConnSndFile", "ConnRcvFile"], "", ""),
|
||||
(sti @Contact, STRecord, "", [], "", ""),
|
||||
(sti @ContactAddressPlan, STUnion, "CAP", [], "", ""),
|
||||
(sti @ContactShortLinkData, STRecord, "", [], "", ""),
|
||||
(sti @ContactStatus, STEnum, "CS", [], "", ""),
|
||||
(sti @ContactUserPreferences, STRecord, "", [], "", ""),
|
||||
(sti @CryptoFile, STRecord, "", [], "", ""),
|
||||
(sti @CryptoFileArgs, STRecord, "", [], "", ""),
|
||||
(sti @E2EInfo, STRecord, "", [], "", ""),
|
||||
(sti @ErrorType, STUnion, "", [], "", ""),
|
||||
(sti @FeatureAllowed, STEnum, "FA", [], "", ""),
|
||||
(sti @FileDescr, STRecord, "", [], "", ""),
|
||||
(sti @FileError, STUnion, "FileErr", [], "", ""),
|
||||
(sti @FileErrorType, STUnion, "", [], "", ""),
|
||||
(sti @FileInvitation, STRecord, "", [], "", ""),
|
||||
(sti @FileProtocol, (STEnum' $ consLower "FP"), "", [], "", ""),
|
||||
(sti @FileStatus, STEnum, "FS", [], "", ""),
|
||||
(sti @FileTransferMeta, STRecord, "", [], "", ""),
|
||||
(sti @Format, STUnion, "", [], "", ""),
|
||||
(sti @FormattedText, STRecord, "", [], "", ""),
|
||||
(sti @FullGroupPreferences, STRecord, "", [], "", ""),
|
||||
(sti @FullPreferences, STRecord, "", [], "", ""),
|
||||
(sti @GroupChatScope, STUnion1, "GCS", [], "(_support" <> Optional "" (":" <> Param "$0") "groupMemberId_" <> ")", ""),
|
||||
(sti @GroupChatScopeInfo, STUnion1, "GCSI", [], "", ""),
|
||||
(sti @GroupFeature, STEnum, "GF", [], "", ""),
|
||||
(sti @GroupFeatureEnabled, STEnum, "FE", [], "", ""),
|
||||
(sti @GroupInfo, STRecord, "", [], "", ""),
|
||||
(sti @GroupInfoSummary, STRecord, "", [], "", ""),
|
||||
(sti @GroupLink, STRecord, "", [], "", ""),
|
||||
(sti @GroupLinkPlan, STUnion, "GLP", [], "", ""),
|
||||
(sti @GroupMember, STRecord, "", [], "", ""),
|
||||
(sti @GroupMemberAdmission, STRecord, "", [], "", ""),
|
||||
(sti @GroupMemberCategory, (STEnum' $ dropPfxSfx "GC" "Member"), "", [], "", ""),
|
||||
(sti @GroupMemberRef, STRecord, "", [], "", ""),
|
||||
(sti @GroupMemberRole, STEnum, "GR", [], "", ""),
|
||||
(sti @GroupMemberSettings, STRecord, "", [], "", ""),
|
||||
(sti @GroupMemberStatus, (STEnum' $ (\case "group_deleted" -> "deleted"; "intro_invited" -> "intro-inv"; s -> s) . consSep "GSMem" '_'), "", [], "", ""),
|
||||
(sti @GroupPreference, STRecord, "", [], "", ""),
|
||||
(sti @GroupPreferences, STRecord, "", [], "", ""),
|
||||
(sti @GroupProfile, STRecord, "", [], "", ""),
|
||||
(sti @GroupShortLinkData, STRecord, "", [], "", ""),
|
||||
(sti @GroupSummary, STRecord, "", [], "", ""),
|
||||
(sti @GroupSupportChat, STRecord, "", [], "", ""),
|
||||
(sti @HandshakeError, STEnum, "", [], "", ""),
|
||||
(sti @InlineFileMode, STEnum, "IFM", [], "", ""),
|
||||
(sti @InvitationLinkPlan, STUnion, "ILP", [], "", ""),
|
||||
(sti @InvitedBy, STUnion, "IB", [], "", ""),
|
||||
(sti @LinkContent, STUnion, "LC", [], "", ""),
|
||||
(sti @LinkPreview, STRecord, "", [], "", ""),
|
||||
(sti @LocalProfile, STRecord, "", [], "", ""),
|
||||
(sti @MemberCriteria, STEnum1, "MC", [], "", ""),
|
||||
(sti @MsgChatLink, STUnion, "MCL", [], "", "Connection link sent in a message - only short links are allowed."),
|
||||
(sti @MsgContent, STUnion, "MC", [], "", ""),
|
||||
(sti @MsgDecryptError, STEnum, "MDE", [], "", ""),
|
||||
(sti @MsgDirection, STEnum, "MD", [], "", ""),
|
||||
(sti @MsgErrorType, STUnion, "", [], "", ""), -- check, may be correct?
|
||||
(sti @MsgFilter, STEnum, "MF", [], "", ""),
|
||||
(sti @MsgReaction, STUnion, "MR", [], "", ""),
|
||||
(sti @MsgReceiptStatus, STEnum, "MR", [], "", ""),
|
||||
(sti @NewUser, STRecord, "", [], "", ""),
|
||||
(sti @NoteFolder, STRecord, "", [], "", ""),
|
||||
(sti @PendingContactConnection, STRecord, "", [], "", ""),
|
||||
(sti @PrefEnabled, STRecord, "", [], "", ""),
|
||||
(sti @Preferences, STRecord, "", [], "", ""),
|
||||
(sti @PreparedContact, STRecord, "", [], "", ""),
|
||||
(sti @PreparedGroup, STRecord, "", [], "", ""),
|
||||
(sti @Profile, STRecord, "", [], "", ""),
|
||||
(sti @ProxyClientError, STUnion, "Proxy", [], "", ""),
|
||||
(sti @ProxyError, STUnion, "", [], "", ""),
|
||||
(sti @RatchetSyncState, STEnum, "RS", [], "", ""),
|
||||
(sti @RCErrorType, STUnion, "RCE", [], "", ""),
|
||||
(sti @RcvConnEvent, STUnion, "RCE", [], "", ""),
|
||||
(sti @RcvDirectEvent, STUnion, "RDE", [], "", ""),
|
||||
(sti @RcvFileDescr, STRecord, "", [], "", ""),
|
||||
(sti @RcvFileInfo, STRecord, "", [], "", ""),
|
||||
(sti @RcvFileStatus, STUnion, "RFS", [], "", ""),
|
||||
(sti @RcvFileTransfer, STRecord, "", [], "", ""),
|
||||
(sti @RcvGroupEvent, STUnion, "RGE", [], "", ""),
|
||||
(sti @ReportReason, (STEnum' $ dropPfxSfx "RR" ""), "", ["RRUnknown"], "", ""),
|
||||
(sti @RoleGroupPreference, STRecord, "", [], "", ""),
|
||||
(sti @SecurityCode, STRecord, "", [], "", ""),
|
||||
(sti @SimplePreference, STRecord, "", [], "", ""),
|
||||
(sti @SimplexLinkType, STEnum, "XL", [], "", ""),
|
||||
(sti @SMPAgentError, STUnion, "", [], "", ""),
|
||||
(sti @SndCIStatusProgress, STEnum, "SSP", [], "", ""),
|
||||
(sti @SndConnEvent, STUnion, "SCE", [], "", ""),
|
||||
(sti @SndError, STUnion, "SndErr", [], "", ""),
|
||||
(sti @SndFileTransfer, STRecord, "", [], "", ""),
|
||||
(sti @SndGroupEvent, STUnion, "SGE", [], "", ""),
|
||||
(sti @SrvError, STUnion, "SrvErr", [], "", ""),
|
||||
(sti @StoreError, STUnion, "SE", [], "", ""),
|
||||
(sti @SwitchPhase, STEnum, "SP", [], "", ""),
|
||||
(sti @TimedMessagesGroupPreference, STRecord, "", [], "", ""),
|
||||
(sti @TimedMessagesPreference, STRecord, "", [], "", ""),
|
||||
(sti @TransportError, STUnion, "TE", [], "", ""),
|
||||
(sti @UIColorMode, STEnum, "UCM", [], "", ""),
|
||||
(sti @UIColors, STRecord, "", [], "", ""),
|
||||
(sti @UIThemeEntityOverride, STRecord, "", [], "", ""),
|
||||
(sti @UIThemeEntityOverrides, STRecord, "", [], "", ""),
|
||||
(sti @UpdatedMessage, STRecord, "", [], "", ""),
|
||||
(sti @User, STRecord, "", [], "", ""),
|
||||
(sti @UserContact, STRecord, "", [], "", ""),
|
||||
(sti @UserContactLink, STRecord, "", [], "", ""),
|
||||
(sti @UserContactRequest, STRecord, "", [], "", ""),
|
||||
(sti @UserInfo, STRecord, "", [], "", ""),
|
||||
(sti @UserProfileUpdateSummary, STRecord, "", [], "", ""),
|
||||
(sti @UserPwdHash, STRecord, "", [], "", ""),
|
||||
(sti @XFTPErrorType, STUnion, "", [], "", ""),
|
||||
(sti @XFTPRcvFile, STRecord, "", [], "", ""),
|
||||
(sti @XFTPSndFile, STRecord, "", [], "", "")
|
||||
|
||||
-- (sti @DatabaseError, STUnion, "DB", [], "", ""),
|
||||
-- (sti @ChatItemInfo, STRecord, "", [], "", ""),
|
||||
-- (sti @ChatItemVersion, STRecord, "", [], "", ""),
|
||||
-- (sti @ChatListQuery, STUnion, "CLQ", [], "", ""),
|
||||
-- (sti @ChatName, STRecord, "", [], "", ""),
|
||||
-- (sti @ChatPagination, STRecord, "CP", [], "", ""),
|
||||
-- (sti @ConnectionStats, STRecord, "", [], "", ""),
|
||||
-- (sti @Group, STRecord, "", [], "", ""),
|
||||
-- (sti @GroupSndStatus, STUnion, "GSS", [], "", ""),
|
||||
-- (sti @MemberDeliveryStatus, STRecord, "", [], "", ""),
|
||||
-- (sti @MemberReaction, STRecord, "", [], "", ""),
|
||||
-- (sti @MsgContentTag, (STEnum' $ dropPfxSfx "MC" '_'), "", ["MCUnknown_"], "", ""),
|
||||
-- (sti @NavigationInfo, STRecord, "", [], "", ""),
|
||||
-- (sti @PaginationByTime, STRecord, "", [], "", ""),
|
||||
-- (sti @RcvQueueInfo, STRecord, "", [], "", ""),
|
||||
-- (sti @RcvSwitchStatus, STEnum, "", [], "", ""), -- incorrect
|
||||
-- (sti @SendRef, STRecord, "", [], "", ""),
|
||||
-- (sti @SndQueueInfo, STRecord, "", [], "", ""),
|
||||
-- (sti @SndSwitchStatus, STEnum, "", [], "", ""), -- incorrect
|
||||
]
|
||||
|
||||
data SimplePreference = SimplePreference {allow :: FeatureAllowed} deriving (Generic)
|
||||
|
||||
data RoleGroupPreference = RoleGroupPreference {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Generic)
|
||||
|
||||
deriving instance Generic (Chat c)
|
||||
deriving instance Generic (ChatItem c d)
|
||||
deriving instance Generic (CIFile d)
|
||||
deriving instance Generic (CIMeta c d)
|
||||
deriving instance Generic (CIQuote d)
|
||||
deriving instance Generic (CIReaction c d)
|
||||
deriving instance Generic (ContactUserPref p)
|
||||
deriving instance Generic (ContactUserPreference p)
|
||||
deriving instance Generic (CreatedConnLink m)
|
||||
deriving instance Generic AddressSettings
|
||||
deriving instance Generic AgentCryptoError
|
||||
deriving instance Generic AgentErrorType
|
||||
deriving instance Generic AutoAccept
|
||||
deriving instance Generic BlockingInfo
|
||||
deriving instance Generic BlockingReason
|
||||
deriving instance Generic BrokerErrorType
|
||||
deriving instance Generic BusinessChatInfo
|
||||
deriving instance Generic BusinessChatType
|
||||
deriving instance Generic ChatDeleteMode
|
||||
deriving instance Generic ChatError
|
||||
deriving instance Generic ChatErrorType
|
||||
deriving instance Generic ChatFeature
|
||||
deriving instance Generic ChatItemDeletion
|
||||
deriving instance Generic ChatRef
|
||||
deriving instance Generic ChatSettings
|
||||
deriving instance Generic ChatStats
|
||||
deriving instance Generic ChatType
|
||||
deriving instance Generic ChatWallpaper
|
||||
deriving instance Generic ChatWallpaperScale
|
||||
deriving instance Generic CICallStatus
|
||||
deriving instance Generic CIDeleteMode
|
||||
deriving instance Generic CIForwardedFrom
|
||||
deriving instance Generic CIGroupInvitation
|
||||
deriving instance Generic CIGroupInvitationStatus
|
||||
deriving instance Generic CIMention
|
||||
deriving instance Generic CIMentionMember
|
||||
deriving instance Generic CIReactionCount
|
||||
deriving instance Generic CITimed
|
||||
deriving instance Generic Color
|
||||
deriving instance Generic CommandError
|
||||
deriving instance Generic CommandErrorType
|
||||
deriving instance Generic ComposedMessage
|
||||
deriving instance Generic Connection
|
||||
deriving instance Generic ConnectionEntity
|
||||
deriving instance Generic ConnectionErrorType
|
||||
deriving instance Generic ConnectionMode
|
||||
deriving instance Generic ConnectionPlan
|
||||
deriving instance Generic ConnStatus
|
||||
deriving instance Generic ConnType
|
||||
deriving instance Generic Contact
|
||||
deriving instance Generic ContactAddressPlan
|
||||
deriving instance Generic ContactShortLinkData
|
||||
deriving instance Generic ContactStatus
|
||||
deriving instance Generic ContactUserPreferences
|
||||
deriving instance Generic CryptoFile
|
||||
deriving instance Generic CryptoFileArgs
|
||||
deriving instance Generic E2EInfo
|
||||
deriving instance Generic ErrorType
|
||||
deriving instance Generic FeatureAllowed
|
||||
deriving instance Generic FileDescr
|
||||
deriving instance Generic FileError
|
||||
deriving instance Generic FileErrorType
|
||||
deriving instance Generic FileInvitation
|
||||
deriving instance Generic FileProtocol
|
||||
deriving instance Generic FileStatus
|
||||
deriving instance Generic FileTransferMeta
|
||||
deriving instance Generic Format
|
||||
deriving instance Generic FormattedText
|
||||
deriving instance Generic FullGroupPreferences
|
||||
deriving instance Generic FullPreferences
|
||||
deriving instance Generic GroupChatScope
|
||||
deriving instance Generic GroupChatScopeInfo
|
||||
deriving instance Generic GroupFeature
|
||||
deriving instance Generic GroupFeatureEnabled
|
||||
deriving instance Generic GroupInfo
|
||||
deriving instance Generic GroupInfoSummary
|
||||
deriving instance Generic GroupLink
|
||||
deriving instance Generic GroupLinkPlan
|
||||
deriving instance Generic GroupMember
|
||||
deriving instance Generic GroupMemberAdmission
|
||||
deriving instance Generic GroupMemberCategory
|
||||
deriving instance Generic GroupMemberRef
|
||||
deriving instance Generic GroupMemberRole
|
||||
deriving instance Generic GroupMemberSettings
|
||||
deriving instance Generic GroupMemberStatus
|
||||
deriving instance Generic GroupPreference
|
||||
deriving instance Generic GroupPreferences
|
||||
deriving instance Generic GroupProfile
|
||||
deriving instance Generic GroupShortLinkData
|
||||
deriving instance Generic GroupSummary
|
||||
deriving instance Generic GroupSupportChat
|
||||
deriving instance Generic HandshakeError
|
||||
deriving instance Generic InlineFileMode
|
||||
deriving instance Generic InvitationLinkPlan
|
||||
deriving instance Generic InvitedBy
|
||||
deriving instance Generic JSONChatInfo
|
||||
deriving instance Generic JSONCIContent
|
||||
deriving instance Generic JSONCIDeleted
|
||||
deriving instance Generic JSONCIDirection
|
||||
deriving instance Generic JSONCIFileStatus
|
||||
deriving instance Generic JSONCIStatus
|
||||
deriving instance Generic LinkContent
|
||||
deriving instance Generic LinkPreview
|
||||
deriving instance Generic LocalProfile
|
||||
deriving instance Generic MemberCriteria
|
||||
deriving instance Generic MsgChatLink
|
||||
deriving instance Generic MsgContent
|
||||
deriving instance Generic MsgDecryptError
|
||||
deriving instance Generic MsgDirection
|
||||
deriving instance Generic MsgErrorType
|
||||
deriving instance Generic MsgFilter
|
||||
deriving instance Generic MsgReaction
|
||||
deriving instance Generic MsgReceiptStatus
|
||||
deriving instance Generic NewUser
|
||||
deriving instance Generic NoteFolder
|
||||
deriving instance Generic PendingContactConnection
|
||||
deriving instance Generic PrefEnabled
|
||||
deriving instance Generic Preferences
|
||||
deriving instance Generic PreparedContact
|
||||
deriving instance Generic PreparedGroup
|
||||
deriving instance Generic Profile
|
||||
deriving instance Generic ProxyClientError
|
||||
deriving instance Generic ProxyError
|
||||
deriving instance Generic RatchetSyncState
|
||||
deriving instance Generic RCErrorType
|
||||
deriving instance Generic RcvConnEvent
|
||||
deriving instance Generic RcvDirectEvent
|
||||
deriving instance Generic RcvFileDescr
|
||||
deriving instance Generic RcvFileInfo
|
||||
deriving instance Generic RcvFileStatus
|
||||
deriving instance Generic RcvFileTransfer
|
||||
deriving instance Generic RcvGroupEvent
|
||||
deriving instance Generic ReportReason
|
||||
deriving instance Generic SecurityCode
|
||||
deriving instance Generic SimplexLinkType
|
||||
deriving instance Generic SMPAgentError
|
||||
deriving instance Generic SndCIStatusProgress
|
||||
deriving instance Generic SndConnEvent
|
||||
deriving instance Generic SndError
|
||||
deriving instance Generic SndFileTransfer
|
||||
deriving instance Generic SndGroupEvent
|
||||
deriving instance Generic SrvError
|
||||
deriving instance Generic StoreError
|
||||
deriving instance Generic SwitchPhase
|
||||
deriving instance Generic TimedMessagesGroupPreference
|
||||
deriving instance Generic TimedMessagesPreference
|
||||
deriving instance Generic TransportError
|
||||
deriving instance Generic UIColorMode
|
||||
deriving instance Generic UIColors
|
||||
deriving instance Generic UIThemeEntityOverride
|
||||
deriving instance Generic UIThemeEntityOverrides
|
||||
deriving instance Generic UpdatedMessage
|
||||
deriving instance Generic User
|
||||
deriving instance Generic UserContact
|
||||
deriving instance Generic UserContactLink
|
||||
deriving instance Generic UserContactRequest
|
||||
deriving instance Generic UserInfo
|
||||
deriving instance Generic UserProfileUpdateSummary
|
||||
deriving instance Generic UserPwdHash
|
||||
deriving instance Generic XFTPErrorType
|
||||
deriving instance Generic XFTPRcvFile
|
||||
deriving instance Generic XFTPSndFile
|
||||
|
||||
-- deriving instance Generic DatabaseError
|
||||
-- deriving instance Generic ChatItemInfo
|
||||
-- deriving instance Generic ChatItemVersion
|
||||
-- deriving instance Generic ChatListQuery
|
||||
-- deriving instance Generic ChatName
|
||||
-- deriving instance Generic ChatPagination
|
||||
-- deriving instance Generic ConnectionStats
|
||||
-- deriving instance Generic Group
|
||||
-- deriving instance Generic GroupSndStatus
|
||||
-- deriving instance Generic MemberDeliveryStatus
|
||||
-- deriving instance Generic MemberReaction
|
||||
-- deriving instance Generic MsgContentTag
|
||||
-- deriving instance Generic NavigationInfo
|
||||
-- deriving instance Generic PaginationByTime
|
||||
-- deriving instance Generic RcvQueueInfo
|
||||
-- deriving instance Generic RcvSwitchStatus
|
||||
-- deriving instance Generic SendRef
|
||||
-- deriving instance Generic SndQueueInfo
|
||||
-- deriving instance Generic SndSwitchStatus
|
||||
@@ -0,0 +1,224 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE TypeSynonymInstances #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module API.TypeInfo where
|
||||
|
||||
import Data.Kind (Type)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Typeable
|
||||
import GHC.Generics
|
||||
import Simplex.Messaging.Parsers (fstToLower)
|
||||
|
||||
data APIType
|
||||
= ATPrim PrimitiveType
|
||||
| ATDef APITypeDef
|
||||
| ATRef String -- to support recursive types
|
||||
| ATOptional APIType
|
||||
| ATArray {elemType :: APIType, nonEmpty :: Bool}
|
||||
| ATMap {keyType :: PrimitiveType, valueType :: APIType}
|
||||
|
||||
data APITypeDef = APITypeDef {typeName' :: String, typeDef :: APITypeDefinition}
|
||||
|
||||
data APITypeDefinition
|
||||
= ATDRecord (NonEmpty APIRecordField)
|
||||
| ATDUnion (NonEmpty ATUnionMember)
|
||||
| ATDEnum (NonEmpty String)
|
||||
|
||||
type TypeAndFields = (String, [APIRecordField])
|
||||
|
||||
data APIRecordField = APIRecordField {fieldName' :: String, typeInfo :: APIType}
|
||||
|
||||
data ATUnionMember = ATUnionMember {memberTag :: String, memberFields :: [APIRecordField]}
|
||||
|
||||
newtype PrimitiveType = PT String
|
||||
|
||||
-- data PrimitiveType = PTBool | PTString StringFormat | PTInt | PTInt64 | PTWord32 | PTDouble
|
||||
|
||||
data StringFormat = SFTimestamp | SFBase64 | SFSimpleXLink | SFServerAddr | SFHexColor
|
||||
|
||||
pattern TBool :: String
|
||||
pattern TBool = "bool"
|
||||
|
||||
pattern TString :: String
|
||||
pattern TString = "string"
|
||||
|
||||
pattern TInt :: String
|
||||
pattern TInt = "int"
|
||||
|
||||
pattern TInt64 :: String
|
||||
pattern TInt64 = "int64"
|
||||
|
||||
primitiveTypes :: [ConsName]
|
||||
primitiveTypes = [TBool, TString, TInt, TInt64, "word32", "double", "JSONObject", "UTCTime"]
|
||||
|
||||
data SumTypeInfo = STI {typeName :: String, recordTypes :: [RecordTypeInfo]}
|
||||
deriving (Show)
|
||||
|
||||
sti :: forall t. (GTypeInfo (Rep t), GetDatatypeName (Rep t)) => SumTypeInfo
|
||||
sti = STI {typeName = getDatatypeName @(Rep t), recordTypes = gTypeInfo @(Rep t)}
|
||||
|
||||
class GetDatatypeName (f :: Type -> Type) where getDatatypeName :: String
|
||||
|
||||
instance (Datatype d) => GetDatatypeName (D1 d g) where
|
||||
getDatatypeName = datatypeName (undefined :: D1 d g p)
|
||||
|
||||
recordTypesInfo :: forall t. (GTypeInfo (Rep t)) => [RecordTypeInfo]
|
||||
recordTypesInfo = gTypeInfo @(Rep t)
|
||||
|
||||
data RecordTypeInfo = RecordTypeInfo {consName :: ConsName, fieldInfos :: [FieldInfo]}
|
||||
deriving (Show)
|
||||
|
||||
class ConstructorName t where consName' :: t -> ConsName
|
||||
|
||||
instance ConstructorName RecordTypeInfo where consName' RecordTypeInfo {consName} = consName
|
||||
|
||||
type ConsName = String
|
||||
|
||||
data FieldInfo = FieldInfo {fieldName :: String, typeInfo :: TypeInfo}
|
||||
deriving (Show)
|
||||
|
||||
data SimpleType = ST {tcName :: ConsName, tcParams :: [String]}
|
||||
deriving (Show)
|
||||
|
||||
data TypeInfo
|
||||
= TIType SimpleType -- for simple types
|
||||
| TIOptional TypeInfo -- for Maybe
|
||||
| TIArray {elemType :: TypeInfo, nonEmpty :: Bool} -- for [] and NonEmpty
|
||||
| TIMap {keyType :: SimpleType, valueType :: TypeInfo} -- keys are only base types
|
||||
deriving (Show)
|
||||
|
||||
ti :: ConsName -> TypeInfo
|
||||
ti n = TIType $ ST n []
|
||||
|
||||
class GTypeInfo (f :: Type -> Type) where
|
||||
gTypeInfo :: [RecordTypeInfo]
|
||||
|
||||
instance GTypeInfo U1 where
|
||||
gTypeInfo = []
|
||||
|
||||
instance GTypeInfo V1 where
|
||||
gTypeInfo = []
|
||||
|
||||
instance (GTypeInfo f) => GTypeInfo (D1 d f) where
|
||||
gTypeInfo = gTypeInfo @f
|
||||
|
||||
instance (Constructor c, GFieldsInfo f) => GTypeInfo (C1 c f) where
|
||||
gTypeInfo = [RecordTypeInfo {consName = conName (undefined :: M1 C c f p), fieldInfos = gfieldsInfo @f}]
|
||||
|
||||
instance (GTypeInfo l, GTypeInfo r) => GTypeInfo (l :+: r) where
|
||||
gTypeInfo = gTypeInfo @l ++ gTypeInfo @r
|
||||
|
||||
class GFieldsInfo (f :: Type -> Type) where
|
||||
gfieldsInfo :: [FieldInfo]
|
||||
|
||||
instance GFieldsInfo U1 where
|
||||
gfieldsInfo = []
|
||||
|
||||
instance GFieldsInfo V1 where
|
||||
gfieldsInfo = []
|
||||
|
||||
instance (GFieldsInfo l, GFieldsInfo r) => GFieldsInfo (l :*: r) where
|
||||
gfieldsInfo = gfieldsInfo @l ++ gfieldsInfo @r
|
||||
|
||||
instance forall s i c. (Selector s, Typeable c) => GFieldsInfo (S1 s (K1 i c)) where
|
||||
gfieldsInfo = [FieldInfo {fieldName = selName (undefined :: S1 s (K1 i c) p), typeInfo = toTypeInfo $ typeRep (Proxy :: Proxy c)}]
|
||||
|
||||
toTypeInfo :: TypeRep -> TypeInfo
|
||||
toTypeInfo tr =
|
||||
let tc = typeRepTyCon tr
|
||||
args = typeRepArgs tr
|
||||
name = tyConName tc
|
||||
in case name of
|
||||
"List" -> case args of
|
||||
[elemTr]
|
||||
| elemTr == typeRep (Proxy @Char) -> TIType (ST TString [])
|
||||
| otherwise -> TIArray {elemType = toTypeInfo elemTr, nonEmpty = False}
|
||||
_ -> TIType (simpleType tr)
|
||||
"NonEmpty" -> case args of
|
||||
[elemTr] -> TIArray {elemType = toTypeInfo elemTr, nonEmpty = True}
|
||||
_ -> TIType (simpleType tr)
|
||||
"Maybe" -> case args of
|
||||
[innerTr] -> TIOptional (toTypeInfo innerTr)
|
||||
_ -> TIType (simpleType tr)
|
||||
"Map" -> case args of
|
||||
[keyTr, valTr] -> TIMap {keyType = simpleType keyTr, valueType = toTypeInfo valTr}
|
||||
_ -> TIType (simpleType tr)
|
||||
_ -> TIType (simpleType tr)
|
||||
where
|
||||
simpleType tr' = primitiveToLower $ case tyConName (typeRepTyCon tr') of
|
||||
"AgentUserId" -> ST TInt64 []
|
||||
"Integer" -> ST TInt64 []
|
||||
"Version" -> ST TInt []
|
||||
"PQEncryption" -> ST TBool []
|
||||
"PQSupport" -> ST TBool []
|
||||
"ACreatedConnLink" -> ST "CreatedConnLink" []
|
||||
"CChatItem" -> ST "ChatItem" []
|
||||
"FormatColor" -> ST "Color" []
|
||||
"CustomData" -> ST "JSONObject" []
|
||||
"KeyMap" -> ST "JSONObject" []
|
||||
"CIQDirection" -> ST "CIDirection" []
|
||||
"SendRef" -> ST "ChatRef" []
|
||||
t
|
||||
| t `elem` stringTypes -> ST TString []
|
||||
| t `elem` simplePrefTypes -> ST "SimplePreference" []
|
||||
| t `elem` groupPrefTypes -> ST "GroupPreference" []
|
||||
| t `elem` roleGroupPrefTypes -> ST "RoleGroupPreference" []
|
||||
| otherwise -> case words $ show tr' of
|
||||
(tcName : tcParams) -> ST {tcName, tcParams}
|
||||
_ -> ST "" []
|
||||
primitiveToLower st@(ST t ps) = let t' = fstToLower t in if t' `elem` primitiveTypes then ST t' ps else st
|
||||
stringTypes =
|
||||
[ "AConnectionLink",
|
||||
"AgentConnId",
|
||||
"AgentInvId",
|
||||
"AgentRcvFileId",
|
||||
"AgentSndFileId",
|
||||
"B64UrlByteString",
|
||||
"CbNonce",
|
||||
"ConnectionLink",
|
||||
"ConnShortLink",
|
||||
"ConnectionRequestUri",
|
||||
"FileDigest",
|
||||
"GroupLinkId",
|
||||
"ImageData",
|
||||
"MemberId",
|
||||
"Text",
|
||||
"MREmojiChar",
|
||||
"ProtocolServer",
|
||||
"SbKey",
|
||||
"SharedMsgId",
|
||||
"UIColor",
|
||||
"UserPwd",
|
||||
"XContactId"
|
||||
]
|
||||
simplePrefTypes =
|
||||
[ "CallsPreference",
|
||||
"FullDeletePreference",
|
||||
"ReactionsPreference",
|
||||
"VoicePreference"
|
||||
]
|
||||
groupPrefTypes =
|
||||
[ "FullDeleteGroupPreference",
|
||||
"ReactionsGroupPreference",
|
||||
"ReportsGroupPreference",
|
||||
"HistoryGroupPreference"
|
||||
]
|
||||
roleGroupPrefTypes =
|
||||
[ "DirectMessagesGroupPreference",
|
||||
"VoiceGroupPreference",
|
||||
"FilesGroupPreference",
|
||||
"SimplexLinksGroupPreference"
|
||||
]
|
||||
@@ -488,6 +488,7 @@ test-suite simplex-chat-test
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Test.hs
|
||||
other-modules:
|
||||
APIDocs
|
||||
Bots.BroadcastTests
|
||||
Bots.DirectoryTests
|
||||
ChatClient
|
||||
@@ -511,6 +512,14 @@ test-suite simplex-chat-test
|
||||
RemoteTests
|
||||
ValidNames
|
||||
ViewTests
|
||||
API.Docs.Commands
|
||||
API.Docs.Events
|
||||
API.Docs.Generate
|
||||
API.Docs.Responses
|
||||
API.Docs.Syntax
|
||||
API.Docs.Syntax.Types
|
||||
API.Docs.Types
|
||||
API.TypeInfo
|
||||
Broadcast.Bot
|
||||
Broadcast.Options
|
||||
Directory.BlockedWords
|
||||
@@ -532,6 +541,7 @@ test-suite simplex-chat-test
|
||||
SchemaDump
|
||||
WebRTCTests
|
||||
hs-source-dirs:
|
||||
bots/src
|
||||
tests
|
||||
apps/simplex-broadcast-bot/src
|
||||
apps/simplex-directory-service/src
|
||||
|
||||
@@ -41,7 +41,6 @@ import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Set (Set)
|
||||
import Data.String
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
@@ -259,9 +258,9 @@ data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSIn
|
||||
|
||||
data ChatCommand
|
||||
= ShowActiveUser
|
||||
| CreateActiveUser NewUser
|
||||
| CreateActiveUser {newUser :: NewUser}
|
||||
| ListUsers
|
||||
| APISetActiveUser UserId (Maybe UserPwd)
|
||||
| APISetActiveUser {userId :: UserId, viewPwd :: Maybe UserPwd}
|
||||
| SetActiveUser UserName (Maybe UserPwd)
|
||||
| SetAllContactReceipts Bool
|
||||
| APISetUserContactReceipts UserId UserMsgReceiptSettings
|
||||
@@ -276,7 +275,7 @@ data ChatCommand
|
||||
| UnhideUser UserPwd
|
||||
| MuteUser
|
||||
| UnmuteUser
|
||||
| APIDeleteUser UserId Bool (Maybe UserPwd)
|
||||
| APIDeleteUser {userId :: UserId, delSMPQueues :: Bool, viewPwd :: Maybe UserPwd}
|
||||
| DeleteUser UserName Bool (Maybe UserPwd)
|
||||
| StartChat {mainApp :: Bool, enableSndFiles :: Bool, largeLinkData :: Bool} -- enableSndFiles has no effect when mainApp is True
|
||||
| CheckChatRunning
|
||||
@@ -305,9 +304,9 @@ data ChatCommand
|
||||
| APIGetAppSettings (Maybe AppSettings)
|
||||
| APIGetChatTags UserId
|
||||
| APIGetChats {userId :: UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery}
|
||||
| APIGetChat ChatRef (Maybe MsgContentTag) ChatPagination (Maybe String)
|
||||
| APIGetChatItems ChatPagination (Maybe String)
|
||||
| APIGetChatItemInfo ChatRef ChatItemId
|
||||
| APIGetChat {chatRef :: ChatRef, contentTag :: Maybe MsgContentTag, chatPagination :: ChatPagination, search :: Maybe String}
|
||||
| APIGetChatItems {chatPagination :: ChatPagination, search :: Maybe String}
|
||||
| APIGetChatItemInfo {chatRef :: ChatRef, chatItemId :: ChatItemId}
|
||||
| APISendMessages {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage}
|
||||
| APICreateChatTag ChatTagData
|
||||
| APISetChatTags ChatRef (Maybe (NonEmpty ChatTagId))
|
||||
@@ -318,23 +317,23 @@ data ChatCommand
|
||||
| APIReportMessage {groupId :: GroupId, chatItemId :: ChatItemId, reportReason :: ReportReason, reportText :: Text}
|
||||
| ReportMessage {groupName :: GroupName, contactName_ :: Maybe ContactName, reportReason :: ReportReason, reportedMessage :: Text}
|
||||
| APIUpdateChatItem {chatRef :: ChatRef, chatItemId :: ChatItemId, liveMessage :: Bool, updatedMessage :: UpdatedMessage}
|
||||
| APIDeleteChatItem ChatRef (NonEmpty ChatItemId) CIDeleteMode
|
||||
| APIDeleteMemberChatItem GroupId (NonEmpty ChatItemId)
|
||||
| APIDeleteChatItem {chatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, deleteMode :: CIDeleteMode}
|
||||
| APIDeleteMemberChatItem {groupId :: GroupId, chatItemIds :: NonEmpty ChatItemId}
|
||||
| APIArchiveReceivedReports GroupId
|
||||
| APIDeleteReceivedReports GroupId (NonEmpty ChatItemId) CIDeleteMode
|
||||
| APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction}
|
||||
| APIGetReactionMembers UserId GroupId ChatItemId MsgReaction
|
||||
| APIGetReactionMembers {userId :: UserId, groupId :: GroupId, chatItemId :: ChatItemId, reaction :: MsgReaction}
|
||||
| APIPlanForwardChatItems {fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId}
|
||||
| APIForwardChatItems {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int}
|
||||
| APIUserRead UserId
|
||||
| UserRead
|
||||
| APIChatRead ChatRef
|
||||
| APIChatItemsRead ChatRef (NonEmpty ChatItemId)
|
||||
| APIChatUnread ChatRef Bool
|
||||
| APIDeleteChat ChatRef ChatDeleteMode -- currently delete mode settings are only applied to direct chats
|
||||
| APIClearChat ChatRef
|
||||
| APIAcceptContact IncognitoEnabled Int64
|
||||
| APIRejectContact Int64
|
||||
| APIChatRead {chatRef :: ChatRef}
|
||||
| APIChatItemsRead {chatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId}
|
||||
| APIChatUnread {chatRef :: ChatRef, unreadChat :: Bool}
|
||||
| APIDeleteChat {chatRef :: ChatRef, chatDeleteMode :: ChatDeleteMode} -- currently delete mode settings are only applied to direct chats
|
||||
| APIClearChat {chatRef :: ChatRef}
|
||||
| APIAcceptContact {incognito :: IncognitoEnabled, contactReqId :: Int64}
|
||||
| APIRejectContact {contactReqId :: Int64}
|
||||
| APISendCallInvitation ContactId CallType
|
||||
| SendCallInvitation ContactName CallType
|
||||
| APIRejectCall ContactId
|
||||
@@ -345,11 +344,11 @@ data ChatCommand
|
||||
| APIGetCallInvitations
|
||||
| APICallStatus ContactId WebRTCCallStatus
|
||||
| APIGetNetworkStatuses
|
||||
| APIUpdateProfile UserId Profile
|
||||
| APISetContactPrefs ContactId Preferences
|
||||
| APISetContactAlias ContactId LocalAlias
|
||||
| APISetGroupAlias GroupId LocalAlias
|
||||
| APISetConnectionAlias Int64 LocalAlias
|
||||
| APIUpdateProfile {userId :: UserId, profile :: Profile}
|
||||
| APISetContactPrefs {contactId :: ContactId, preferences :: Preferences}
|
||||
| APISetContactAlias {contactId :: ContactId, localAlias :: LocalAlias}
|
||||
| APISetGroupAlias {groupId :: GroupId, localAlias :: LocalAlias}
|
||||
| APISetConnectionAlias {connectionId :: Int64, localAlias :: LocalAlias}
|
||||
| APISetUserUIThemes UserId (Maybe UIThemeEntityOverrides)
|
||||
| APISetChatUIThemes ChatRef (Maybe UIThemeEntityOverrides)
|
||||
| APIGetNtfToken
|
||||
@@ -359,22 +358,20 @@ data ChatCommand
|
||||
| APIDeleteToken DeviceToken
|
||||
| APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString}
|
||||
| APIGetConnNtfMessages (NonEmpty ConnMsgReq)
|
||||
| APIAddMember GroupId ContactId GroupMemberRole
|
||||
| APIAddMember {groupId :: GroupId, contactId :: ContactId, memberRole :: GroupMemberRole}
|
||||
| APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter}
|
||||
| APIAcceptMember GroupId GroupMemberId GroupMemberRole
|
||||
| APIAcceptMember {groupId :: GroupId, groupMemberId :: GroupMemberId, memberRole :: GroupMemberRole}
|
||||
| APIDeleteMemberSupportChat GroupId GroupMemberId
|
||||
| APIMembersRole GroupId (NonEmpty GroupMemberId) GroupMemberRole
|
||||
| APIBlockMembersForAll GroupId (NonEmpty GroupMemberId) Bool
|
||||
| APIRemoveMembers {groupId :: GroupId, groupMemberIds :: Set GroupMemberId, withMessages :: Bool}
|
||||
| APILeaveGroup GroupId
|
||||
| APIListMembers GroupId
|
||||
-- | APIDeleteGroupConversations GroupId (NonEmpty GroupConversationId)
|
||||
-- | APIArchiveGroupConversations GroupId (NonEmpty GroupConversationId)
|
||||
| APIUpdateGroupProfile GroupId GroupProfile
|
||||
| APICreateGroupLink GroupId GroupMemberRole
|
||||
| APIGroupLinkMemberRole GroupId GroupMemberRole
|
||||
| APIDeleteGroupLink GroupId
|
||||
| APIGetGroupLink GroupId
|
||||
| APIMembersRole {groupId :: GroupId, groupMemberIds :: NonEmpty GroupMemberId, memberRole :: GroupMemberRole}
|
||||
| APIBlockMembersForAll {groupId :: GroupId, groupMemberIds :: NonEmpty GroupMemberId, blocked :: Bool}
|
||||
| APIRemoveMembers {groupId :: GroupId, groupMemberIds :: NonEmpty GroupMemberId, withMessages :: Bool}
|
||||
| APILeaveGroup {groupId :: GroupId}
|
||||
| APIListMembers {groupId :: GroupId}
|
||||
| APIUpdateGroupProfile {groupId :: GroupId, groupProfile :: GroupProfile}
|
||||
| APICreateGroupLink {groupId :: GroupId, memberRole :: GroupMemberRole}
|
||||
| APIGroupLinkMemberRole {groupId :: GroupId, memberRole :: GroupMemberRole}
|
||||
| APIDeleteGroupLink {groupId :: GroupId}
|
||||
| APIGetGroupLink {groupId :: GroupId}
|
||||
| APIAddGroupShortLink GroupId
|
||||
| APICreateMemberContact GroupId GroupMemberId
|
||||
| APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent}
|
||||
@@ -395,7 +392,7 @@ data ChatCommand
|
||||
| SetChatItemTTL Int64
|
||||
| APIGetChatItemTTL UserId
|
||||
| GetChatItemTTL
|
||||
| APISetChatTTL UserId ChatRef (Maybe Int64)
|
||||
| APISetChatTTL {userId :: UserId, chatRef :: ChatRef, seconds :: Maybe Int64}
|
||||
| SetChatTTL ChatName (Maybe Int64)
|
||||
| GetChatTTL ChatName
|
||||
| APISetNetworkConfig NetworkConfig
|
||||
@@ -404,7 +401,7 @@ data ChatCommand
|
||||
| APISetNetworkInfo UserNetworkInfo
|
||||
| ReconnectAllServers
|
||||
| ReconnectServer UserId SMPServer
|
||||
| APISetChatSettings ChatRef ChatSettings
|
||||
| APISetChatSettings {chatRef :: ChatRef, chatSettings :: ChatSettings}
|
||||
| APISetMemberSettings GroupId GroupMemberId GroupMemberSettings
|
||||
| APIContactInfo ContactId
|
||||
| APIGroupInfo GroupId
|
||||
@@ -415,8 +412,8 @@ data ChatCommand
|
||||
| APISwitchGroupMember GroupId GroupMemberId
|
||||
| APIAbortSwitchContact ContactId
|
||||
| APIAbortSwitchGroupMember GroupId GroupMemberId
|
||||
| APISyncContactRatchet ContactId Bool
|
||||
| APISyncGroupMemberRatchet GroupId GroupMemberId Bool
|
||||
| APISyncContactRatchet {contactId :: ContactId, force :: Bool}
|
||||
| APISyncGroupMemberRatchet {groupId :: GroupId, groupMemberId :: GroupMemberId, force :: Bool}
|
||||
| APIGetContactCode ContactId
|
||||
| APIGetGroupMemberCode GroupId GroupMemberId
|
||||
| APIVerifyContact ContactId (Maybe Text)
|
||||
@@ -445,35 +442,35 @@ data ChatCommand
|
||||
| EnableGroupMember GroupName ContactName
|
||||
| ChatHelp HelpSection
|
||||
| Welcome
|
||||
| APIAddContact UserId IncognitoEnabled
|
||||
| APIAddContact {userId :: UserId, incognito :: IncognitoEnabled}
|
||||
| AddContact IncognitoEnabled
|
||||
| APISetConnectionIncognito Int64 IncognitoEnabled
|
||||
| APIChangeConnectionUser Int64 UserId -- new user id to switch connection to
|
||||
| APIConnectPlan UserId AConnectionLink
|
||||
| APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink} -- Maybe is used to report link parsing failure as special error
|
||||
| APIPrepareContact UserId ACreatedConnLink ContactShortLinkData
|
||||
| APIPrepareGroup UserId CreatedLinkContact GroupShortLinkData
|
||||
| APIChangePreparedContactUser ContactId UserId
|
||||
| APIChangePreparedGroupUser GroupId UserId
|
||||
| APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent}
|
||||
| APIConnectPreparedGroup GroupId IncognitoEnabled (Maybe MsgContent)
|
||||
| APIConnect UserId IncognitoEnabled ACreatedConnLink
|
||||
| APIConnect {userId :: UserId, incognito :: IncognitoEnabled, connLink_ :: Maybe ACreatedConnLink} -- Maybe is used to report link parsing failure as special error
|
||||
| Connect IncognitoEnabled (Maybe AConnectionLink)
|
||||
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId
|
||||
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
|
||||
| DeleteContact ContactName ChatDeleteMode
|
||||
| ClearContact ContactName
|
||||
| APIListContacts UserId
|
||||
| APIListContacts {userId :: UserId}
|
||||
| ListContacts
|
||||
| APICreateMyAddress UserId
|
||||
| APICreateMyAddress {userId :: UserId}
|
||||
| CreateMyAddress
|
||||
| APIDeleteMyAddress UserId
|
||||
| APIDeleteMyAddress {userId :: UserId}
|
||||
| DeleteMyAddress
|
||||
| APIShowMyAddress UserId
|
||||
| APIShowMyAddress {userId :: UserId}
|
||||
| ShowMyAddress
|
||||
| APIAddMyAddressShortLink UserId
|
||||
| APISetProfileAddress UserId Bool
|
||||
| APISetProfileAddress {userId :: UserId, enable :: Bool}
|
||||
| SetProfileAddress Bool
|
||||
| APISetAddressSettings UserId AddressSettings
|
||||
| APISetAddressSettings {userId :: UserId, settings :: AddressSettings}
|
||||
| SetAddressSettings AddressSettings
|
||||
| AcceptContact IncognitoEnabled ContactName
|
||||
| RejectContact ContactName
|
||||
@@ -490,20 +487,20 @@ data ChatCommand
|
||||
| EditMessage {chatName :: ChatName, editedMsg :: Text, message :: Text}
|
||||
| UpdateLiveMessage {chatName :: ChatName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: Text}
|
||||
| ReactToMessage {add :: Bool, reaction :: MsgReaction, chatName :: ChatName, reactToMessage :: Text}
|
||||
| APINewGroup UserId IncognitoEnabled GroupProfile
|
||||
| APINewGroup {userId :: UserId, incognito :: IncognitoEnabled, groupProfile :: GroupProfile}
|
||||
| NewGroup IncognitoEnabled GroupProfile
|
||||
| AddMember GroupName ContactName GroupMemberRole
|
||||
| JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter}
|
||||
| AcceptMember GroupName ContactName GroupMemberRole
|
||||
| MemberRole GroupName ContactName GroupMemberRole
|
||||
| BlockForAll GroupName ContactName Bool
|
||||
| RemoveMembers {groupName :: GroupName, members :: Set ContactName, withMessages :: Bool}
|
||||
| RemoveMembers {groupName :: GroupName, members :: NonEmpty ContactName, withMessages :: Bool}
|
||||
| LeaveGroup GroupName
|
||||
| DeleteGroup GroupName
|
||||
| ClearGroup GroupName
|
||||
| ListMembers GroupName
|
||||
| ListMemberSupportChats GroupName
|
||||
| APIListGroups UserId (Maybe ContactId) (Maybe String)
|
||||
| APIListGroups {userId :: UserId, contactId_ :: Maybe ContactId, search :: Maybe String}
|
||||
| ListGroups (Maybe ContactName) (Maybe String)
|
||||
| UpdateGroupNames GroupName GroupProfile
|
||||
| ShowGroupProfile GroupName
|
||||
@@ -528,7 +525,7 @@ data ChatCommand
|
||||
| SendFileDescription ChatName FilePath
|
||||
| ReceiveFile {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
|
||||
| SetFileToReceive {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool}
|
||||
| CancelFile FileTransferId
|
||||
| CancelFile {fileId :: FileTransferId}
|
||||
| FileStatus FileTransferId
|
||||
| ShowProfile -- UserId (not used in UI)
|
||||
| UpdateProfile ContactName (Maybe Text) -- UserId (not used in UI)
|
||||
@@ -642,9 +639,9 @@ data ChatResponse
|
||||
| CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats}
|
||||
| CRQueueInfo {user :: User, rcvMsgInfo :: Maybe RcvMsgInfo, queueInfo :: ServerQueueInfo}
|
||||
| CRContactSwitchStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats}
|
||||
| CEvtGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRContactSwitchAborted {user :: User, contact :: Contact, connectionStats :: ConnectionStats}
|
||||
| CEvtGroupMemberSwitchAborted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRGroupMemberSwitchAborted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRContactRatchetSyncStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats}
|
||||
| CRGroupMemberRatchetSyncStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRContactCode {user :: User, contact :: Contact, connectionCode :: Text}
|
||||
@@ -673,7 +670,7 @@ data ChatResponse
|
||||
| CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest, contact_ :: Maybe Contact}
|
||||
| CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact}
|
||||
| CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool}
|
||||
| CRGroupsList {user :: User, groups :: [(GroupInfo, GroupSummary)]}
|
||||
| CRGroupsList {user :: User, groups :: [GroupInfoSummary]}
|
||||
| CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
|
||||
| CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
|
||||
| CRFileTransferStatusXFTP User AChatItem
|
||||
@@ -804,12 +801,10 @@ data ChatEvent
|
||||
| CEvtSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CEvtSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CEvtSndFileRcvCancelled {user :: User, chatItem_ :: Maybe AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CEvtSndFileStartXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} -- not used
|
||||
| CEvtSndFileProgressXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, sentSize :: Int64, totalSize :: Int64}
|
||||
| CEvtSndFileRedirectStartXFTP {user :: User, fileTransferMeta :: FileTransferMeta, redirectMeta :: FileTransferMeta}
|
||||
| CEvtSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
|
||||
| CEvtSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]}
|
||||
| CEvtSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta}
|
||||
| CEvtSndFileError {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text}
|
||||
| CEvtSndFileWarning {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text}
|
||||
| CEvtContactConnecting {user :: User, contact :: Contact}
|
||||
|
||||
@@ -693,6 +693,7 @@ processChatCommand vr nm = \case
|
||||
let msgIds = itemsMsgIds items
|
||||
events = L.nonEmpty $ map (\msgId -> XMsgDel msgId Nothing $ toMsgScope gInfo <$> chatScopeInfo) msgIds
|
||||
mapM_ (sendGroupMessages user gInfo Nothing recipients) events
|
||||
-- TODO delGroupChatItems sends deletion events too. Are they needed?
|
||||
delGroupChatItems user gInfo chatScopeInfo items False
|
||||
pure $ CRChatItemsDeleted user deletions True False
|
||||
CTLocal -> do
|
||||
@@ -1580,7 +1581,7 @@ processChatCommand vr nm = \case
|
||||
case memberConnId m of
|
||||
Just connId -> do
|
||||
connectionStats <- withAgent (\a -> switchConnectionAsync a "" connId)
|
||||
pure $ CEvtGroupMemberSwitchStarted user g m connectionStats
|
||||
pure $ CRGroupMemberSwitchStarted user g m connectionStats
|
||||
_ -> throwChatError CEGroupMemberNotActive
|
||||
APIAbortSwitchContact contactId -> withUser $ \user -> do
|
||||
ct <- withFastStore $ \db -> getContact db vr user contactId
|
||||
@@ -1594,7 +1595,7 @@ processChatCommand vr nm = \case
|
||||
case memberConnId m of
|
||||
Just connId -> do
|
||||
connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId
|
||||
pure $ CEvtGroupMemberSwitchAborted user g m connectionStats
|
||||
pure $ CRGroupMemberSwitchAborted user g m connectionStats
|
||||
_ -> throwChatError CEGroupMemberNotActive
|
||||
APISyncContactRatchet contactId force -> withUser $ \user -> withContactLock "syncContactRatchet" contactId $ do
|
||||
ct <- withFastStore $ \db -> getContact db vr user contactId
|
||||
@@ -1754,8 +1755,9 @@ processChatCommand vr nm = \case
|
||||
createDirectConnection db newUser agConnId ccLink' Nothing ConnNew Nothing subMode initialChatVersion PQSupportOn
|
||||
deleteAgentConnectionAsync (aConnId' conn)
|
||||
pure conn'
|
||||
APIConnectPlan userId cLink -> withUserId userId $ \user ->
|
||||
APIConnectPlan userId (Just cLink) -> withUserId userId $ \user ->
|
||||
uncurry (CRConnectionPlan user) <$> connectPlan user cLink
|
||||
APIConnectPlan _ Nothing -> throwChatError CEInvalidConnReq
|
||||
APIPrepareContact userId accLink contactSLinkData -> withUserId userId $ \user -> do
|
||||
let ContactShortLinkData {profile, message, business} = contactSLinkData
|
||||
welcomeSharedMsgId <- forM message $ \_ -> getSharedMsgId
|
||||
@@ -1885,7 +1887,7 @@ processChatCommand vr nm = \case
|
||||
toView $ CEvtNewChatItems user [ci]
|
||||
pure $ CRStartedConnectionToGroup user gInfo' customUserProfile
|
||||
CVRConnectedContact _ct -> throwChatError $ CEException "contact already exists when connecting to group"
|
||||
APIConnect userId incognito acl -> withUserId userId $ \user -> case acl of
|
||||
APIConnect userId incognito (Just acl) -> withUserId userId $ \user -> case acl of
|
||||
ACCL SCMInvitation ccLink -> do
|
||||
(conn, incognitoProfile) <- connectViaInvitation user incognito ccLink Nothing
|
||||
let pcc = mkPendingContactConnection conn $ Just ccLink
|
||||
@@ -1894,6 +1896,7 @@ processChatCommand vr nm = \case
|
||||
connectViaContact user Nothing incognito ccLink Nothing Nothing >>= \case
|
||||
CVRConnectedContact ct -> pure $ CRContactAlreadyExists user ct
|
||||
CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile
|
||||
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
|
||||
Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do
|
||||
(ccLink, plan) <- connectPlan user cLink `catchChatError` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing)); _ -> throwError e
|
||||
connectWithPlan user incognito ccLink plan
|
||||
@@ -1918,6 +1921,10 @@ processChatCommand vr nm = \case
|
||||
ListContacts -> withUser $ \User {userId} ->
|
||||
processChatCommand vr nm $ APIListContacts userId
|
||||
APICreateMyAddress userId -> withUserId userId $ \user -> do
|
||||
withFastStore' (\db -> runExceptT $ getUserAddress db user) >>= \case
|
||||
Left SEUserContactLinkNotFound -> pure ()
|
||||
Left e -> throwError $ ChatErrorStore e
|
||||
Right _ -> throwError $ ChatErrorStore SEDuplicateContactLink
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
userData <- contactShortLinkData (userProfileToSend user Nothing Nothing False) Nothing
|
||||
-- TODO [certs rcv]
|
||||
@@ -2186,6 +2193,7 @@ processChatCommand vr nm = \case
|
||||
Nothing -> throwChatError $ CEContactNotActive ct
|
||||
APIAcceptMember groupId gmId role -> withUser $ \user@User {userId} -> do
|
||||
(gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user gmId
|
||||
-- TODO check that user's role is > role, possibly restrict role to only observer and member
|
||||
assertUserGroupRole gInfo GRModerator
|
||||
case memberStatus m of
|
||||
GSMemPendingApproval | memberCategory m == GCInviteeMember -> do -- only host can approve
|
||||
@@ -2365,8 +2373,9 @@ processChatCommand vr nm = \case
|
||||
APIRemoveMembers {groupId, groupMemberIds, withMessages} -> withUser $ \user ->
|
||||
withGroupLock "removeMembers" groupId $ do
|
||||
Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId
|
||||
let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin) = selectMembers members
|
||||
memCount = S.size groupMemberIds
|
||||
let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin) = selectMembers gmIds members
|
||||
gmIds = S.fromList $ L.toList groupMemberIds
|
||||
memCount = length groupMemberIds
|
||||
when (count /= memCount) $ throwChatError CEGroupMemberNotFound
|
||||
when (memCount > 1 && anyAdmin) $ throwCmdError "can't remove multiple members when admins selected"
|
||||
assertUserGroupRole gInfo $ max GRAdmin maxRole
|
||||
@@ -2389,11 +2398,11 @@ processChatCommand vr nm = \case
|
||||
when withMessages $ deleteMessages user gInfo' deleted
|
||||
pure $ CRUserDeletedMembers user gInfo' deleted withMessages -- same order is not guaranteed
|
||||
where
|
||||
selectMembers :: [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool)
|
||||
selectMembers = foldl' addMember (0, [], [], [], [], GRObserver, False)
|
||||
selectMembers :: S.Set GroupMemberId -> [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool)
|
||||
selectMembers gmIds = foldl' addMember (0, [], [], [], [], GRObserver, False)
|
||||
where
|
||||
addMember acc@(n, invited, pendingApprv, pendingRvw, current, maxRole, anyAdmin) m@GroupMember {groupMemberId, memberStatus, memberRole}
|
||||
| groupMemberId `S.member` groupMemberIds =
|
||||
| groupMemberId `S.member` gmIds =
|
||||
let maxRole' = max maxRole memberRole
|
||||
anyAdmin' = anyAdmin || memberRole >= GRAdmin
|
||||
n' = n + 1
|
||||
@@ -2483,7 +2492,7 @@ processChatCommand vr nm = \case
|
||||
RemoveMembers gName gMemberNames withMessages -> withUser $ \user -> do
|
||||
(gId, gMemberIds) <- withStore $ \db -> do
|
||||
gId <- getGroupIdByName db user gName
|
||||
gMemberIds <- S.fromList <$> mapM (getGroupMemberIdByName db user gId) (S.toList gMemberNames)
|
||||
gMemberIds <- mapM (getGroupMemberIdByName db user gId) gMemberNames
|
||||
pure (gId, gMemberIds)
|
||||
processChatCommand vr nm $ APIRemoveMembers gId gMemberIds withMessages
|
||||
LeaveGroup gName -> withUser $ \user -> do
|
||||
@@ -3452,7 +3461,7 @@ processChatCommand vr nm = \case
|
||||
case plan of
|
||||
CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
|
||||
processChatCommand vr nm $ APIConnectContactViaAddress userId incognito contactId
|
||||
_ -> processChatCommand vr nm $ APIConnect userId incognito ccLink
|
||||
_ -> processChatCommand vr nm $ APIConnect userId incognito $ Just ccLink
|
||||
| otherwise = pure $ CRConnectionPlan user ccLink plan
|
||||
invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> CM ConnectionPlan
|
||||
invitationRequestPlan user cReq contactSLinkData_ = do
|
||||
@@ -4518,7 +4527,7 @@ chatCommandP =
|
||||
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> memberRole),
|
||||
"/block for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure True),
|
||||
"/unblock for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure False),
|
||||
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMembers <$> displayNameP <* A.space <*> (S.fromList <$> (char_ '@' *> displayNameP) `A.sepBy1'` A.char ',') <*> (" messages=" *> onOffP <|> pure False)),
|
||||
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMembers <$> displayNameP <* A.space <*> (L.fromList <$> (char_ '@' *> displayNameP) `A.sepBy1'` A.char ',') <*> (" messages=" *> onOffP <|> pure False)),
|
||||
("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayNameP),
|
||||
("/delete #" <|> "/d #") *> (DeleteGroup <$> displayNameP),
|
||||
("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayNameP <*> chatDeleteMode),
|
||||
@@ -4551,7 +4560,7 @@ chatCommandP =
|
||||
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <* char_ '@' <*> (Just <$> displayNameP) <* A.space <*> quotedMsg <*> msgTextP),
|
||||
"/_contacts " *> (APIListContacts <$> A.decimal),
|
||||
"/contacts" $> ListContacts,
|
||||
"/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP),
|
||||
"/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing)),
|
||||
"/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP),
|
||||
"/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <* A.space <*> jsonP),
|
||||
"/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal),
|
||||
@@ -4559,7 +4568,7 @@ chatCommandP =
|
||||
"/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)),
|
||||
"/_connect group #" *> (APIConnectPreparedGroup <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)),
|
||||
"/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP),
|
||||
"/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> connLinkP),
|
||||
"/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> connLinkP_),
|
||||
"/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP),
|
||||
"/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal),
|
||||
("/connect" <|> "/c") *> (AddContact <$> incognitoP),
|
||||
@@ -4677,6 +4686,8 @@ chatCommandP =
|
||||
cReq <- strP
|
||||
sLink_ <- optional (A.space *> strP)
|
||||
pure $ CCLink cReq sLink_
|
||||
connLinkP_ =
|
||||
((Just <$> connLinkP) <|> A.takeTill (== ' ') $> Nothing)
|
||||
incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False
|
||||
incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False
|
||||
imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,")
|
||||
|
||||
@@ -311,7 +311,7 @@ markdownText (FormattedText f_ t) = case f_ of
|
||||
Just cStr -> cStr <> t `T.snoc` '!'
|
||||
Nothing -> t
|
||||
colorStr = \case
|
||||
Red -> Just "!1 "
|
||||
Red -> Just "!1 "
|
||||
Green -> Just "!2 "
|
||||
Blue -> Just "!3 "
|
||||
Yellow -> Just "!4 "
|
||||
|
||||
@@ -104,7 +104,7 @@ chatTypeStr = \case
|
||||
chatNameStr :: ChatName -> String
|
||||
chatNameStr (ChatName cType name) = T.unpack $ chatTypeStr cType <> if T.any isSpace name then "'" <> name <> "'" else name
|
||||
|
||||
data ChatRef = ChatRef ChatType Int64 (Maybe GroupChatScope)
|
||||
data ChatRef = ChatRef {chatType :: ChatType, chatId :: Int64, chatScope :: Maybe GroupChatScope}
|
||||
deriving (Eq, Show, Ord)
|
||||
|
||||
data ChatInfo (c :: ChatType) where
|
||||
@@ -1499,6 +1499,7 @@ instance (ChatTypeI c, MsgDirectionI d) => ToJSON (JSONAnyChatItem c d) where
|
||||
toJSON = $(JQ.mkToJSON defaultJSON ''JSONAnyChatItem)
|
||||
toEncoding = $(JQ.mkToEncoding defaultJSON ''JSONAnyChatItem)
|
||||
|
||||
-- if JSON encoding changes, update AChatItem type definition in bots/src/API/Docs/Types.hs
|
||||
instance FromJSON AChatItem where
|
||||
parseJSON = J.withObject "AChatItem" $ \o -> do
|
||||
AChatInfo c chatInfo <- o .: "chatInfo"
|
||||
@@ -1560,6 +1561,7 @@ instance ChatTypeI c => ToJSON (JSONCIReaction c d) where
|
||||
toJSON = $(JQ.mkToJSON defaultJSON ''JSONCIReaction)
|
||||
toEncoding = $(JQ.mkToEncoding defaultJSON ''JSONCIReaction)
|
||||
|
||||
-- if JSON encoding changes, update ACIReaction type definition in bots/src/API/Docs/Types.hs
|
||||
instance FromJSON ACIReaction where
|
||||
parseJSON = J.withObject "ACIReaction" $ \o -> do
|
||||
ACIR c d reaction <- o .: "chatReaction"
|
||||
|
||||
@@ -968,10 +968,10 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do
|
||||
where
|
||||
search = maybe "" (map toLower) search_
|
||||
|
||||
getUserGroupsWithSummary :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [(GroupInfo, GroupSummary)]
|
||||
getUserGroupsWithSummary :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfoSummary]
|
||||
getUserGroupsWithSummary db vr user _contactId_ search_ =
|
||||
getUserGroupDetails db vr user _contactId_ search_
|
||||
>>= mapM (\g@GroupInfo {groupId} -> (g,) <$> getGroupSummary db user groupId)
|
||||
>>= mapM (\g@GroupInfo {groupId} -> GIS g <$> getGroupSummary db user groupId)
|
||||
|
||||
-- the statuses on non-current members should match memberCurrent' function
|
||||
getGroupSummary :: DB.Connection -> User -> GroupId -> IO GroupSummary
|
||||
|
||||
@@ -952,10 +952,6 @@ Plan:
|
||||
Query: INSERT INTO xftp_servers (xftp_host, xftp_port, xftp_key_hash) VALUES (?,?,?)
|
||||
Plan:
|
||||
|
||||
Query: SELECT 1 FROM connections WHERE conn_id = ? AND deleted_at_wait_delivery < ? LIMIT 1
|
||||
Plan:
|
||||
SEARCH connections USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query: SELECT 1 FROM encrypted_rcv_message_hashes WHERE conn_id = ? AND hash = ? LIMIT 1
|
||||
Plan:
|
||||
SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_hash (conn_id=? AND hash=?)
|
||||
|
||||
@@ -509,6 +509,9 @@ data GroupSummary = GroupSummary
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data GroupInfoSummary = GIS {groupInfo :: GroupInfo, groupSummary :: GroupSummary}
|
||||
deriving (Show)
|
||||
|
||||
data ContactOrGroup = CGContact Contact | CGGroup GroupInfo [GroupMember]
|
||||
|
||||
data PreparedChatEntity = PCEContact Contact | PCEGroup {groupInfo :: GroupInfo, hostMember :: GroupMember}
|
||||
@@ -1350,10 +1353,10 @@ data RcvFileDescr = RcvFileDescr
|
||||
|
||||
data RcvFileStatus
|
||||
= RFSNew
|
||||
| RFSAccepted RcvFileInfo
|
||||
| RFSConnected RcvFileInfo
|
||||
| RFSComplete RcvFileInfo
|
||||
| RFSCancelled (Maybe RcvFileInfo)
|
||||
| RFSAccepted {fileInfo :: RcvFileInfo}
|
||||
| RFSConnected {fileInfo :: RcvFileInfo}
|
||||
| RFSComplete {fileInfo :: RcvFileInfo}
|
||||
| RFSCancelled {fileInfo_ :: Maybe RcvFileInfo}
|
||||
deriving (Eq, Show)
|
||||
|
||||
rcvFileComplete :: RcvFileStatus -> Bool
|
||||
@@ -2005,6 +2008,8 @@ $(JQ.deriveJSON defaultJSON ''Group)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''GroupSummary)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''GroupInfoSummary)
|
||||
|
||||
instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP
|
||||
|
||||
instance ToField MsgFilter where toField = toField . msgFilterInt
|
||||
|
||||
@@ -135,9 +135,9 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
|
||||
"server queue info: " <> viewJSON qInfo
|
||||
]
|
||||
CRContactSwitchStarted {} -> ["switch started"]
|
||||
CEvtGroupMemberSwitchStarted {} -> ["switch started"]
|
||||
CRGroupMemberSwitchStarted {} -> ["switch started"]
|
||||
CRContactSwitchAborted {} -> ["switch aborted"]
|
||||
CEvtGroupMemberSwitchAborted {} -> ["switch aborted"]
|
||||
CRGroupMemberSwitchAborted {} -> ["switch aborted"]
|
||||
CRContactRatchetSyncStarted {} -> ["connection synchronization started"]
|
||||
CRGroupMemberRatchetSyncStarted {} -> ["connection synchronization started"]
|
||||
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
|
||||
@@ -432,12 +432,10 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
|
||||
CEvtRcvFileWarning u Nothing e ft -> ttyUser u $ receivingFileStandalone "warning: " ft <> [sShow e]
|
||||
CEvtSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
|
||||
CEvtSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
|
||||
CEvtSndFileStartXFTP {} -> []
|
||||
CEvtSndFileProgressXFTP {} -> []
|
||||
CEvtSndFileRedirectStartXFTP u ft ftRedirect -> ttyUser u $ standaloneUploadRedirect ft ftRedirect
|
||||
CEvtSndStandaloneFileComplete u ft uris -> ttyUser u $ standaloneUploadComplete ft uris
|
||||
CEvtSndFileCompleteXFTP u ci _ -> ttyUser u $ uploadingFile "completed" ci
|
||||
CEvtSndFileCancelledXFTP {} -> []
|
||||
CEvtSndFileError u Nothing ft e -> ttyUser u $ uploadingFileStandalone "error" ft <> [plain e]
|
||||
CEvtSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e]
|
||||
CEvtSndFileWarning u Nothing ft e -> ttyUser u $ uploadingFileStandalone "warning: " ft <> [plain e]
|
||||
@@ -1343,13 +1341,13 @@ viewContactConnected ct userIncognitoProfile testView =
|
||||
Nothing ->
|
||||
[ttyFullContact ct <> ": contact is connected"]
|
||||
|
||||
viewGroupsList :: [(GroupInfo, GroupSummary)] -> [StyledString]
|
||||
viewGroupsList :: [GroupInfoSummary] -> [StyledString]
|
||||
viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g <name>"]
|
||||
viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs
|
||||
viewGroupsList gs = map groupSS $ sortOn ldn_ gs
|
||||
where
|
||||
ldn_ :: GroupInfo -> Text
|
||||
ldn_ GroupInfo {localDisplayName} = T.toLower localDisplayName
|
||||
groupSS (g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}}, GroupSummary {currentMembers}) =
|
||||
ldn_ :: GroupInfoSummary -> Text
|
||||
ldn_ (GIS GroupInfo {localDisplayName} _) = T.toLower localDisplayName
|
||||
groupSS (GIS g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} GroupSummary {currentMembers}) =
|
||||
case memberStatus membership of
|
||||
GSMemInvited -> groupInvitation' g
|
||||
s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s <> alias g
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module APIDocs where
|
||||
|
||||
import API.Docs.Commands
|
||||
import API.Docs.Events
|
||||
import API.Docs.Generate
|
||||
import API.Docs.Responses
|
||||
import API.Docs.Types
|
||||
import API.TypeInfo
|
||||
import Control.Monad
|
||||
import Data.Containers.ListUtils (nubOrd)
|
||||
import Data.List (foldl', intercalate, sort, (\\))
|
||||
import qualified Data.Set as S
|
||||
import qualified Data.Text.IO as T
|
||||
import Simplex.Messaging.Util (ifM)
|
||||
import System.Directory (doesFileExist)
|
||||
import Test.Hspec
|
||||
|
||||
apiDocsTest :: Spec
|
||||
apiDocsTest = do
|
||||
describe "API commands" $ do
|
||||
it "should be documented" testCommandsHaveDocs
|
||||
it "should have field names" testCommandsHaveNamedFields
|
||||
it "should have defined responses" testCommandsHaveResponses
|
||||
it "generate markdown" testGenerateCommandsMD
|
||||
describe "API responses" $ do
|
||||
it "should be documented" testResponsesHaveDocs
|
||||
describe "API events" $ do
|
||||
it "should be documented" testEventsHaveDocs
|
||||
it "generate markdown" testGenerateEventsMD
|
||||
describe "API types" $ do
|
||||
it "should be documented" testTypesHaveDocs
|
||||
it "generate markdown" testGenerateTypesMD
|
||||
|
||||
documentedCmds :: [String]
|
||||
documentedCmds = concatMap (map consName' . commands) chatCommandsDocs
|
||||
|
||||
documentedCmdTypes :: [ATUnionMember]
|
||||
documentedCmdTypes = concatMap (map commandType . commands) chatCommandsDocs
|
||||
|
||||
documentedResps :: [String]
|
||||
documentedResps = map consName' chatResponsesDocs
|
||||
|
||||
documentedRespTypes :: [ATUnionMember]
|
||||
documentedRespTypes = map responseType chatResponsesDocs
|
||||
|
||||
documentedEvts :: [String]
|
||||
documentedEvts = concatMap (\cat -> map consName' $ mainEvents cat ++ otherEvents cat) chatEventsDocs
|
||||
|
||||
documentedEvtTypes :: [ATUnionMember]
|
||||
documentedEvtTypes = concatMap (\cat -> map eventType $ mainEvents cat ++ otherEvents cat) chatEventsDocs
|
||||
|
||||
documentedTypes :: [String]
|
||||
documentedTypes = map docTypeName chatTypesDocs
|
||||
|
||||
testCommandsHaveDocs :: IO ()
|
||||
testCommandsHaveDocs = do
|
||||
let typeCmds = sort $ map consName' chatCommandsTypeInfo
|
||||
allCmds = sort $ documentedCmds ++ cliCommands ++ undocumentedCommands
|
||||
missingCmds = typeCmds \\ allCmds
|
||||
extraCmds = allCmds \\ typeCmds
|
||||
unless (null missingCmds) $ expectationFailure $ "Undocumented commands: " <> intercalate ", " missingCmds
|
||||
unless (null extraCmds) $ expectationFailure $ "Unused commands: " <> intercalate ", " extraCmds
|
||||
putStrLn $ "Documented commands: " <> show (length documentedCmds) <> "/" <> show (length allCmds)
|
||||
allCmds `shouldBe` typeCmds -- sanity check
|
||||
|
||||
testCommandsHaveNamedFields :: IO ()
|
||||
testCommandsHaveNamedFields = do
|
||||
let docCmds = S.fromList documentedCmds
|
||||
unnamedFields = filter (\RecordTypeInfo {consName, fieldInfos} -> consName `S.member` docCmds && any (\FieldInfo {fieldName} -> null fieldName) fieldInfos) chatCommandsTypeInfo
|
||||
unless (null unnamedFields) $ expectationFailure $ "Commands with unnamed fields: " <> intercalate ", " (map consName' unnamedFields)
|
||||
|
||||
testResponsesHaveDocs :: IO ()
|
||||
testResponsesHaveDocs = do
|
||||
let typeResps = sort $ "CRChatCmdError" : map consName' chatResponsesTypeInfo
|
||||
allResps = sort $ documentedResps ++ undocumentedResponses
|
||||
missingResps = typeResps \\ allResps
|
||||
extraResps = allResps \\ typeResps
|
||||
unless (null missingResps) $ expectationFailure $ "Undocumented responses: " <> intercalate ", " missingResps
|
||||
unless (null extraResps) $ expectationFailure $ "Unused responses: " <> intercalate ", " extraResps
|
||||
putStrLn $ "Documented responses: " <> show (length documentedResps) <> "/" <> show (length allResps)
|
||||
allResps `shouldBe` typeResps -- sanity check
|
||||
|
||||
testEventsHaveDocs :: IO ()
|
||||
testEventsHaveDocs = do
|
||||
let typeEvts = sort $ "CEvtChatError" : map consName' chatEventsTypeInfo
|
||||
allEvts = sort $ documentedEvts ++ undocumentedEvents
|
||||
missingEvts = typeEvts \\ allEvts
|
||||
extraEvts = allEvts \\ typeEvts
|
||||
unless (null missingEvts) $ expectationFailure $ "Undocumented events: " <> intercalate ", " missingEvts
|
||||
unless (null extraEvts) $ expectationFailure $ "Unused events: " <> intercalate ", " extraEvts
|
||||
putStrLn $ "Documented events: " <> show (length documentedEvts) <> "/" <> show (length allEvts)
|
||||
allEvts `shouldBe` typeEvts -- sanity check
|
||||
|
||||
testTypesHaveDocs :: IO ()
|
||||
testTypesHaveDocs = do
|
||||
let allDocTypes = sort $ documentedTypes ++ primitiveTypes
|
||||
apiTypes = sort $ nubOrd $ concatMap unionMemberTypes $ documentedCmdTypes ++ documentedRespTypes ++ documentedEvtTypes
|
||||
extraTypes = allDocTypes \\ apiTypes
|
||||
missingTypes = apiTypes \\ allDocTypes
|
||||
unless (null extraTypes) $ expectationFailure $ "Unused types: " <> intercalate ", " extraTypes
|
||||
unless (null missingTypes) $ expectationFailure $ "Undocumented types: " <> intercalate ", " missingTypes
|
||||
allDocTypes `shouldBe` apiTypes
|
||||
putStrLn $ "Documented types: " <> show (length allDocTypes)
|
||||
where
|
||||
unionMemberTypes :: ATUnionMember -> [ConsName]
|
||||
unionMemberTypes (ATUnionMember _ fields) = concatMap recordFiledTypes fields
|
||||
recordFiledTypes :: APIRecordField -> [ConsName]
|
||||
recordFiledTypes (APIRecordField _ t) = apiTypeTypes t
|
||||
apiTypeTypes :: APIType -> [ConsName]
|
||||
apiTypeTypes = \case
|
||||
ATPrim (PT t) -> [t]
|
||||
ATDef td -> typeDefTypes td
|
||||
ATRef t -> [t] -- ??
|
||||
ATOptional t -> apiTypeTypes t
|
||||
ATArray t _ -> apiTypeTypes t
|
||||
ATMap (PT t) v -> t : apiTypeTypes v
|
||||
typeDefTypes :: APITypeDef -> [ConsName]
|
||||
typeDefTypes (APITypeDef t td) = t : case td of
|
||||
ATDRecord fields -> concatMap recordFiledTypes fields
|
||||
ATDUnion members -> concatMap unionMemberTypes members
|
||||
ATDEnum _ -> []
|
||||
|
||||
testCommandsHaveResponses :: IO ()
|
||||
testCommandsHaveResponses = do
|
||||
let analyzeCmd (cmdsNoResp, rs) CCDoc {consName, responses}
|
||||
| null responses = (consName : cmdsNoResp, rs)
|
||||
| otherwise = (cmdsNoResp, rs `S.union` S.fromList (map consName' responses))
|
||||
(cmdsNoResponses, cmdResponses) = foldl' analyzeCmd ([], S.empty) $ concatMap commands chatCommandsDocs
|
||||
undocResps = S.toList $ cmdResponses `S.difference` S.fromList documentedResps
|
||||
extraResps = S.toList $ S.fromList documentedResps `S.difference` cmdResponses
|
||||
unless (null cmdsNoResponses) $ expectationFailure $ "Commands without responses: " <> intercalate ", " (reverse cmdsNoResponses)
|
||||
unless (null undocResps) $ expectationFailure $ "Undocumented command responses: " <> intercalate ", " undocResps
|
||||
unless (null extraResps) $ expectationFailure $ "Unused documented command responses: " <> intercalate ", " extraResps
|
||||
|
||||
testGenerateCommandsMD :: IO ()
|
||||
testGenerateCommandsMD = do
|
||||
cmdsDoc <- ifM (doesFileExist commandsDocFile) (T.readFile commandsDocFile) (pure "")
|
||||
T.writeFile commandsDocFile commandsDocText
|
||||
commandsDocText `shouldBe` cmdsDoc
|
||||
|
||||
testGenerateEventsMD :: IO ()
|
||||
testGenerateEventsMD = do
|
||||
evtsDoc <- ifM (doesFileExist eventsDocFile) (T.readFile eventsDocFile) (pure "")
|
||||
T.writeFile eventsDocFile eventsDocText
|
||||
eventsDocText `shouldBe` evtsDoc
|
||||
|
||||
testGenerateTypesMD :: IO ()
|
||||
testGenerateTypesMD = do
|
||||
typesDoc <- ifM (doesFileExist typesDocFile) (T.readFile typesDocFile) (pure "")
|
||||
T.writeFile typesDocFile typesDocText
|
||||
typesDocText `shouldBe` typesDoc
|
||||
@@ -1215,20 +1215,20 @@ testOperators =
|
||||
alice <##. "Current conditions: 2."
|
||||
alice ##> "/_operators"
|
||||
alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required"
|
||||
alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: disabled, conditions: required"
|
||||
alice <##. "The new conditions will be accepted for SimpleX Chat Ltd at "
|
||||
alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: required"
|
||||
alice <##. "The new conditions will be accepted for SimpleX Chat Ltd, InFlux Technologies Limited at "
|
||||
-- set conditions notified
|
||||
alice ##> "/_conditions_notified 2"
|
||||
alice <## "ok"
|
||||
alice ##> "/_operators"
|
||||
alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required"
|
||||
alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: disabled, conditions: required"
|
||||
alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: required"
|
||||
alice ##> "/_conditions"
|
||||
alice <##. "Current conditions: 2 (notified)."
|
||||
-- accept conditions
|
||||
alice ##> "/_accept_conditions 2 1,2"
|
||||
alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted ("
|
||||
alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: disabled, conditions: accepted ("
|
||||
alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: accepted ("
|
||||
-- update operators
|
||||
alice ##> "/operators 2:on:smp=proxy:xftp=off"
|
||||
alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted ("
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
import APIDocs
|
||||
import Bots.BroadcastTests
|
||||
import Bots.DirectoryTests
|
||||
import ChatClient
|
||||
@@ -58,6 +59,7 @@ main = do
|
||||
"src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql"
|
||||
#else
|
||||
describe "Schema dump" schemaDumpTest
|
||||
describe "API docs" apiDocsTest
|
||||
around tmpBracket $ describe "WebRTC encryption" webRTCTests
|
||||
#endif
|
||||
describe "SimpleX chat markdown" markdownTests
|
||||
|
||||
Reference in New Issue
Block a user