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:
Evgeny
2025-07-24 13:12:53 +01:00
committed by GitHub
parent 052b9ad628
commit cf8bd7f6ac
32 changed files with 8447 additions and 131 deletions
+13 -5
View File
@@ -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.
-6
View File
@@ -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)"
+1 -1
View File
@@ -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):
@@ -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
View File
@@ -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)
+1432
View File
File diff suppressed because it is too large Load Diff
+725
View File
@@ -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)]
---
+5
View File
@@ -0,0 +1,5 @@
# SimpleX Chat API types reference
- [API Commands and Responses](./COMMANDS.md)
- [API Events](./EVENTS.md)
- [API Types](./TYPES.md)
+3768
View File
File diff suppressed because it is too large Load Diff
+462
View File
@@ -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"
]
+216
View File
@@ -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"
]
+164
View File
@@ -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
+206
View File
@@ -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"
]
+220
View File
@@ -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
+30
View File
@@ -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
+557
View File
@@ -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
+224
View File
@@ -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"
]
+10
View File
@@ -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
+53 -58
View File
@@ -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}
+25 -14
View File
@@ -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,")
+1 -1
View File
@@ -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 "
+3 -1
View File
@@ -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"
+2 -2
View File
@@ -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=?)
+9 -4
View File
@@ -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
+7 -9
View File
@@ -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
+156
View File
@@ -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
+4 -4
View File
@@ -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 ("
+2
View File
@@ -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