---
# System prepended metadata

title: API Chat

---

# API Chat

api prefix: `api/v1/`

## internal (api for internal call)
API-Token in the header of all internal apis
`Service` is the application that call our API, we need this to debug & trace where requests from.

```yaml=
API-Token: secretkey***
Service: user-account (optional)
```
missing this key will result in  - `403 Forbidden`


Client (android, ios, web) only need to care about private and public apis


## private (api for logined user call)
Authorization key in the header of private API
```yaml=
Authorization: Bearer jwtToken****
Service: user-account (optional)
```
missing this key will result in  - `401 unauthorized`


Error reponse format example
```json
{
    "error": "Not Found", // error code
    "message": "Route GET:/users/123 not found",
    "display_message":  "translate to local language, if possible",
    "code": 401
}
```

```json
{
    "error": "Internal Server Error",
    "message": "Cannot connect to Redis",
    "display_message":  "Không thể kết nối tới redis",
    "code": 500
}
```


If API response error, frontend display error message by the following priority:
```
--> display_message
--> message (if display_message is missing)
--> error(if message is missing)
``` 

##### List available `error` codes:

```
"ErrFailedBinding": invalid request body or headers, invalid json
"ErrFailedValidation": invalid request
"ErrUserNotFound": user is not found
"ErrRefreshTokenExpired": refresh token has expired
"ErrRefreshTokenInvalid": refresh token is invalid
"ErrUserChannelNotFound": user channel is not found
"ErrBannedUser": user has been banned
"ErrBlockedUser": user has been blocked (by receiver)
"ErrBlockedReceiver": receiver has been blocked (by user)
"ErrBannedReceiver": receiver has been banned
"ErrTooManyRequests": rate limit
"ErrSameBuyerSeller": same seller and buyer id when creating channel
"ErrMessageFailedModeration": message has been failed validation (contains fraud keywords, phone is not verified...)
"ErrDuplicateUserSettings": same seller and buyer id when syncing channel
"ErrBlockedOrUnblockedUserNotFound": some user are not found when call blocks api.
"ErrMessageNotFound": message could not be found
```

### How to generate JWT key (in backend)
- Use a JWT library to encode the payload with a provided secret key (the encoding process should be happened in the server side).
- Store that generated jwt key in the client side
- JWT Payload
    ```json
    {
      "exp": 1604556004, // expiration
      "iss": "company-name", // your company name
      "sub": "10347987" // accountID (subject)
    }
    ```


- Code to generate token in Golang
```go
package token

import (
	"time"
	"github.com/dgrijalva/jwt-go"
)

// GenerateToken generate jwt token for an accountID
func GenerateToken(accountID, app, secretKey string, expiresAfter time.Duration) (string, error) {
	expirationTime := time.Now().Add(expiresAfter)
	claims := jwt.StandardClaims{
		Subject:   accountID,
		ExpiresAt: expirationTime.Unix(),
		Issuer:    app,
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// Create the JWT string
	return token.SignedString([]byte(secretKey))
}

```

```go
token, _ := GenerateToken("17470094", "company-name", "secretKey", time.Hour)
```



## public
Does not require auth.



# API list


### Frontend reading
- [Get Me](#get-user-info)
- [List Channels](#list-all-channels)
- [Get channel info](#get-channel-info)
- [Create channel](#create-a-channel)
- [Send message](#send-message)


## insert or update user
- PUT internal/users/:user_id
- Request body
```json
{
    "full_name":"Michael",
    "avatar":"https://cdn.piktina.com/12345.jpg",
    "metadata":{
        "phone":"0912312300"
    }
}

```
- Response:
    - success - 200 success


## insert or update multiple users
- PUT internal/users
- The max size is 100.
- Request body
```json
{
   "users":[
        {
            "user_id":"userid",
            "full_name":"Michael",
            "avatar":"https://cdn.piktina.com/12345.jpg",
            "metadata":{
                "phone":"0912312300"
            }
        }
   ]
}
```
- Response:
    - success - 200 success

## delete user
- DELETE internal/users/user_id
- Response:
    - success - 200 success
   
## delete multiple users
- DELETE internal/users
- Request body
```json
{
    "user_ids": [ "user3" ]
}
```
- Response:
    - success - 200 success

## get user info
- GET internal/users/:user_id
- GET private/me
- Response:
    - user does not exist - 404 Not Found
    - success - 200
    ```json
    {
        "code": 200,
        "data": {
            "id": "user1",
            "full_name":"Michael",
            "avatar":"https://cdn.piktina.com/12345.jpg",
            "updated_at": "2021-01-08 19:02:30 +0000",
            "metadata":{
                "phone":"0912312300"
            },
            "access_token": "short-lived token",
            "refresh_token": "long-lived token"
        }
    }
    ```


## get user online status
- GET public/users/:user_id/online_status
- Header: Cache-Control: max-age=0
- Response:
    - user does not exist - 404 Not Found
    - success - 200
    ```json
    {
        "code": 200,
        "data": {
         "is_online": false,
         "last_online_at": 1610693806662
        }
    }
    ```

## ban/unban user
- PATCH internal/ban_users/:user_id
- Request body
```json
{
    "is_banned": true,
    "by":"admin1",
    "reason":"fraud"
}
```

```json
{
    "is_banned": false,
    "by":"admin1",
    "reason":"my mistake"
}
```

- Reponse:
    - success - 200
    ```json
    {
        "code": 200
    }
    ```


## block/unblock user
- Private
    - PATCH private/block
        - `blocks`: add users to block list (optional,max=30)
        - `unblocks`: remove users from block list (optional,max=30)
- Request body
```json
{
    "blocks": ["user1","user2"], 
    "unblocks": ["user3", "user4"]
}
```

- Internal
    - PATCH internal/users/:user_id/block
        - `blocks`: add users to block list (optional,max=30)
        - `unblocks`: remove users from block list (optional,max=30)
```json
{
    "blocks": ["user1","user2"], 
    "unblocks": ["user3", "user4"]
}
```

- Reponse:
    - if any users does not exist - 404 Not Found
    - success - 200
    ```json
    {
        "code": 200
    }

## get ban/block user
- GET internal/ban_users/:user_id
- GET private/block
- Header: Cache-Control: max-age=0
- Response:
    - success - 200
    ```json
    {
        "code": 200,
        "data": {
            "id":"user1",
            "is_banned":true,
            "by":"admin1",
            "reason":"fraud",
            "blocks":[]
        }
    }
    ```
    ```json
    {
        "code": 200,
        "data": {
            "id":"user1",
            "is_banned":false,
            "by":"",
            "reason":"",
            "blocks":["user3", "user4"]
        }
    }
    ```




## create a channel

- POST private/channels
- Request body
```json
{
    "to_user_id": "user2",
    "message": "", // optional
    "message_type": "text", // optional
    "channel_type": "direct"
}
```

- POST internal/channels
- Request body
```json
{
    "from_user_id": "user1",
    "to_user_id": "user2",
    "message": "", // optional
    "message_type": "text", // optional
    "channel_type": "direct"
}
```

- Response:
    - 400: invalid request body; receiver (seller) is banned; receiver has been blocked by sender.
    - 403: sender (buyer) is banned; sender has been blocked by receiver.
    - success 200
    ```json
        {
            "code": 200,
            "data":
            {
                "channel_id": "channel1"
            }
        }
    ```
    
## list all channels
- GET internal/user_channel/:user_id/channels?limit=20&before_ts=1656749676347
- GET private/channels?limit=20
Query options: 
    - limit: max number of messages [min:1 max:100 required]
    - order: order of messages in response `"data"` [default:desc in:asc,desc]
    - after_ts: get channels has `last_message_created_at` after a timestamp (unix mili)
    - before_ts: get channels has `last_message_created_at` before a timestamp [default:now() excluded_with:after_ts]

- NOTE: 
    - User info in channels that have no activity in last 2 months may not contains latest user info.
    - When you call api get channel detail, it will trigger to compare and update user info in the channel. so next time you call api list channels, it returns latest info for triggered channels.
- Response
```json
{
    "code": 200,
    "data":
    [
        {
            "channel_id": "channel_1",
            "unread_count": 12,
            "last_message": "hello",
            "last_message_type": "buyer",
            "last_message_created_at": 1610693806662,
            "channel_name": "user2", // display as name for channel
            "channel_avatar": "https://cdn.piktina.com/user2.jpg", // display as avatar for channel
            "is_muted": false,
        }
    ]
}
```


## get channel info
- GET internal/user_channel/:user_id/:channel_id
- GET private/channels/:channel_id
- user_id=user1, channel_id=channel_1
- Response
```json
{
    "code": 200,
    "data": {
      "channel": {
        "channel_id": "channel_1",
        "channel_name": "name of this chat room, this can be ad title",
        "channel_avatar": "http://cdn.piktina.com/useravatar.jpg",
        "unread_count": 12,
        "last_message": "hello",
        "last_message_type": "buyer",
        "last_message_created_at": 1610693806662,
        // status of :channel_id for :user_id
        "status": "blocked", // banned, others_banned,...
        "status_text": "you are blocked by user2",
        "is_muted": false, // to show toggle button mute/unmute
      },
      "users": [
        {
          "full_name": "abc",
          "avatar": "https://cdn.piktina.com/user1.jpg",
          "metadata": {"phone":"0123"},
          "user_id": "user1",
          "last_read_at": 1610693806665,
          "joined_at": 1610693806662,
        },
        {
          "full_name": "abcd",
          "avatar": "https://cdn.piktina.com/user2.jpg",
          "metadata": {"phone":"0123"},
          "user_id": "user2",
          "last_read_at": 1610693806665,
          "joined_at": 1610693806662,
        }
      ] 
    }
}
```
#### channel.status
  - active: normal
  - blocked: sender (:user_id) is blocked
  - banned: sender (:user_id) is banned
  - others_blocked: receiver is blocked (sender block receiver)
  - others_banned: receiver is banned (by admin)
  - all_blocked: sender and receiver are blocking each other.

## hide channel
- PATCH internal/user_channel/:user_id/:channel_id
- PATCH private/channels/:channel_id
- Request body
```json
{
  "is_hidden": true
}
```
## mute/unmute channel (on/off notification)
- PATCH internal/user_channel/:user_id/:channel_id
- PATCH private/channels/:channel_id
- Request body
```json
{
  "is_muted": true
}
```
- Reponse:
    - success - 200
    ```json
    {
        "code": 200
    }
    ```
## get total unread count
- GET internal/users/:user_id/unread_count
- GET private/unread_count
- Response:
    - success - 200 updated
    ```json
    {
        "code": 200,
        "data": {
            "unread_count": 1
        }
    }
    ```

## set read
- PUT internal/set_read
- Request body
    ```json
    {
        "user_id": "user1",
        "channel_id": "channel1",
        "send_socket_event": true,
        "to_user_ids": ["user1", "user2"] // query to DB if this field is missing
    }
    ```
- Reponse:
    - success - 200
    ```json
    {
        "code": 200,
        "data": {
            "unread_count": 11 // total unread count of all channels
        }
    }
    ```

> set read api is called from chat-socket. client sends read event to chat-socket, then chat-socket call set read api. Core chat api clear unread message and call api of chat-socket to send event read to all online clients in the channels

## set unread count
- PUT internal/user_channel/:user_id/:channel_id/unread_count
- Request body
    ```json
    {
        "count": 7
    }
    ```
- Reponse:
    - success - 200
    ```json
    {
        "code": 200
    }
    ```


## get messages
- GET private/channels/:channel_id/messages?after_ts=123&limit=20
- GET private/channels/:channel_id/messages?before_ts=1234&limit=20
- GET internal/user_channel/:channel_id/:user_id/messages?before_ts=1234&limit=20&order=asc

Query options: 
    - limit: max number of messages [min:1 max:100 required]
    - order: order of messages in response `"data"` [default:desc in:asc,desc]
    - after_ts: get next messages after a timestamp
    - before_ts: get previous messages before a timestamp [default:now() excluded_with:after_ts]

- Response
    - sucess - 200
``` json
{
    "code": 200,
    "data": [
        {
            "channel_id": "channel1",
            "created_at": 1610693806665,
            "sender_id": "user1",
            "message": "hello world",
            "image_urls": ["https://cdn/image1.jpg", "https://cdn/image2.jpg"],
            "type": "image",
            "is_removed": false,
            "is_edited": false,
            "is_hidden": false,
        },
        {
            "channel_id": "channel1",
            "created_at": 1610693806665,
            "sender_id": "user1",
            "message": "hello world",
            "image_urls": [],
            "type": "message",
            "is_removed": false,
            "is_edited": false,
            "is_hidden": true,
        }
    ]
}
```


## get changes
- GET private/channels/:channel_id/changes?after_ts=123&limit=20  
 
Query options: 
    - limit: max number of changes [min:1 max:100 required]
    - order: order of changes in response `"data"` [default:desc in:asc,desc]
    - after_ts: get new changes after a timestamp 
    - before_ts: get previous changes before a timestamp [default:now()  excluded_with:after_ts]

- Response
    - success - 200
```json
{
    "code": 200,
    "data": [
        {
            "channel_id": "channel1",
            "message_created_at": 1610693806665,
            "action": "create", // mesage is created so the created_at of changes = message_created_at
            "message": "",
            "image_urls": [],
            "type": "image",
            "sender_id": "user1",
            "is_hidden": true,
            "created_at": 1610693806665 // = message_created_at
        },
        {
            "channel_id": "channel1",
            "message_created_at": 1610693806665,
            "action": "create", // mesage is created so the created_at of changes = message_created_at
            "message": "hello world1",
            "image_urls": ["https://cdn/image1.jpg", "https://cdn/image2.jpg"],
            "type": "image",
            "sender_id": "user1",
            "is_hidden": false,
            "created_at": 1610693806665 // = message_created_at
        },
        {
            "channel_id": "channel1",
            "message_created_at": 1610693806665,
            "action": "edit", // remove, create
            "message": "hello world2",
            "image_urls": ["https://cdn/image1.jpg", "https://cdn/image2.jpg"],
            "is_hidden": false, // message hidden for user2
            "type": "message",
            "sender_id": "user1",
            "created_at": 1610693906665 // > message_created_at because message is edited
        },
        {
            "channel_id": "channel1",
            "message_created_at": 1610693806665,
            "action": "delete", // remove, create
            "message": "",
            "image_urls": [],
            "is_hidden": false,
            "sender_id": "user1",
            "created_at": 1610693909665 // > message_created_at
        }
    ]
}
```


## send message
- POST private/messages
- Request body
    - send text
    ```json
    {
        "channel_id": "channel1",
        "message": "hello world",
        "image_urls": [],
        "type": "text"
    }
    ```
    - send image
    ```json
    {
        "channel_id": "channel1",
        "message": "hello world",
        "image_urls": ["https://cdn.piktina.com/image1.jpg"],
        "type": "image"
    }
    ```


    - send attachment
    ```json
    {
        "channel_id": "channel1",
        "message": "hello world",
        "image_urls": [],
        "type": "attachment",
        "attachment": {
            "id": "123",
            "type": "minicv",
            "data": {
                "key": "value"
                // flexible fields
            }
        }
    }
    ```



- POST internal/messages
- Request body
```json
{
    "send_socket_event": false,
    "push_notification": true,
    "add_unread_count": true,
    "channel_id": "channel1",
    "sender_id": "sender1", // for internal api only
    "message": "hello world",
    "image_urls": [],
    "type": "text",
    "created_at": 1610693806665, // in ms, is optional
}
```

- Response
    - 400: invalid request body; receiver is banned; receiver has been blocked by sender.
    - 403: sender is banned; sender has been blocked by receiver.
    - 200: success.
```json
{
    "code": 200,
    "data": {
        "channel_id": "ch1",
        "sender_id": "user1",
        "created_at": 1610693806665,
        "type": "text",
        "message": "Hello",
        "receiver_ids": ["user1", "user2", "user3"]
        // list of users can see this messages, excluding users in hidden field
    }
}
```


<!-- - Request body
```json=
{
    "channel_id": "channel1",
    "sender_id": "user1",
    "data": {
        "id": 1
        "location": "2 Ngo Duc Ke, District 1, HCMC"
    }
    "type": "delivery",
    "replaceable": true
}
```
> Note: replaceable=true, add primary key of this message with id and type of data to table attachments, so we can hide this message later
- Request body
```json=
{
    "channel_id": "channel1",
    "sender_id": "user1",
    "data": {
        "id": 1
        "location": "2 Ngo Duc Ke, District 1, HCMC"
    }
    "type": "delivery",
    "replaceable": true,
    "remove_old": true
}
```
> Note: `remove_old=true` query to table attachments to get all messages with type delivery and id of data is 1, then hide those messages by setting is_removed to true in table message -->

## update/edit message
NOTE: Only sender can edit their message.
- PATCH private/channels/:channel_id/messages/:message_created_at
- PATCH internal/users/:user_id/channels/:channel_id/messages/:message_created_at
- Request body
```json
{
    "message": "new message content"
}
```

## delete/unsend message
NOTE: Only sender can remove their message.

- DELETE private/channels/:channel_id/messages/:message_created_at
- DELETE internal/users/:user_id/channels/:channel_id/messages/:message_created_at

- Request body: internal only
```json
{
    "send_socket_event": false // default is true
}
```

## get new token
- POST internal/tokens/new
- Request:
```json
{
    "user_id": "user1",
    "access_token_ttl": 3600, // Value is in in seconds, optional
    "refresh_token_ttl": 7776000 // Value is in in seconds, optional
}
```

> The access_token and refresh_token are both JWT token containing the same payload, but with different expiration duration.

> We use different secrets for access_token and refresh_token. This is to differentiate between the two.

- Response
```json
{
    "code": 200,
    "data": {
        "access_token": "accesstoken",
        "refresh_token": "refreshtoken"
    }
}
```

## refresh token
- POST public/tokens/refresh
> For private endpoint, the default TTLs will be used.
- Request:
```json
{
    "refresh_token": "refreshtoken"
}
```

- POST internal/tokens/refresh
> For internal endpoint, the caller can pass the TTLs in the request, otherwise the default TTLs will be used.
- Request:
```json
{
    "refresh_token": "refreshtoken",
    "access_token_ttl": 3600, // Value is in in seconds, optional
    "refresh_token_ttl": 7776000 // Value is in seconds, optional
}
```

> Both private and internal endpoints have the same response.

- Success response
```json
{
    "code": 200,
    "data": {
        "access_token": "accesstoken",
        "refresh_token": "refreshtoken"
    }
}
```

- If the token is expired.
```json
{
    "code": 401,
    "error": "Unauthorized"
    "message": "Refresh Token is expired",
}
```


## sync channel

- API sync channels with user settings (sync APIs only validate request body, do not verify data before write and have no post events)
- POST /api/v1/internal/sync/channels

```json 
{
  "list_id": "ad3",
  "seller_id": "123",
  "buyer_id": "{{user_id}}",
  "channel_id": "{{channel_id}}",
  "last_message": "good",
  "last_message_type": "text",
  "last_message_created_at": 1637574017322,
  "item_name": "Hat",
  "item_image": "https://picsum.photos/536/354",
  "item_price": "50000 vnd",
  "settings": [{
    "user_id": "123",
    "name": "Buyer name",
    "avatar": "https://picsum.photos/536/354",
    "is_muted": false,
    "is_hidden": false,
    "role": "seller",
    "joined_at": 1637574017320,
    "last_read_at": 1637574017325,
    "unread_count": 0
  }, {
    "user_id": "{{user_id}}",
    "name": "seller name",
    "role": "seller",
    "avatar": "https://picsum.photos/536/354",
    "is_muted": false,
    "is_hidden": false,
    "joined_at": 1637574017320,
    "last_read_at": 1637574017325,
    "unread_count": 0
  }]
}
```
- Reponse:
    - success - 200
    ```json=
    {
        "code": 200
    }
    ```

## sync message

- POST /api/v1/internal/sync/messages

```json
{
    "channel_id": "{{channel_id}}",
    "sender_id": "{{user_id}}",
    "message": "hello world 5",
    "filter_message": "hello w***d 5",
    "type": "attachment", // text image attachment
    "created_at": 1637574017326,
    "is_edited": false,
    "is_removed": false,
    "hidden": null,
    "image_urls": [],
    "attachment": {
      "id": "id1",
      "type": "minicv",
      "data": {}
    },
    "metadata": {}
}

```

- Reponse:
    - success - 200
    ```json
    {
        "code": 200
    }
    ```