# Relation ONE IM 介绍

<figure><img src="/files/SjOzVHA7hZIH98NusWhY" alt=""><figcaption><p>图 1-1 IM Architecture</p></figcaption></figure>

Relation ONE IM是一个去中心化的隐私聊天协议。

* 实现了[Relation Protocol](https://docs.relationlabs.ai/protocol/)，通过[Relation Name Service](https://docs.relationlabs.ai/protocol/open-standard-api/overview/name-service)来管理身份，通过[Relationship](https://docs.relationlabs.ai/protocol/open-standard-api/overview/relationship)来管理好友和群组。
* 使用[Lit Protocol](https://litprotocol.com/)来进行消息加密和解密。
* 使用[Arweave](https://www.arweave.org/)来做去中心化存储。

Relation ONE IM真正实现了去中心化和隐私性。

它主要有四部分组成（见图 1-1）

* Client: 是一个前端应用程序，帮助用户加密/解密消息、发送消息至Proxy。并且需要调用Relation Protocol的标准接口来进行好友和群组管理。
* Proxy: 是一个简单的后端程序，仅用于接收Client发来的消息，存储消息至Arweave并且推送至消息接收者。
* Decentralized Storage(Arweave): 用于永久化存储加密的消息。
* 合约：一系列符合Relation Protocol的合约（Relation Name Service、Follow、DAO），用于存储用户信息、Follow信息、DAO信息。

## 1. 消息发送主要流程

> 流程见图 1-1

1. 发消息的用户使用钱包连接至Client
2. 在Client端使用[Lit Protocol](https://developer.litprotocol.com/)对消息进行加密
3. Client将加密消息发生至Proxy
4. Proxy将消息保存至永久化存储
5. Proxy通过websocket将消息推送至需要接收消息的Client
6. 收消息用户在Client端使用Lit Protocol对消息进行解密

## 2. 身份

每一个以太坊的公私钥对就是一个身份。可以使用MetaMask等钱包来连接Client。

以太坊地址相当于用户的id，聊天的时候在消息体中指定接收人的地址，Proxy会将消息推送至目标地址登录的Client。

私钥用于对消息体进行签名，保证了消息是由该地址发出去的，并且不能被任何人修改。

该身份也用于登录[Lit Protocol](https://litprotocol.com/)，用来在Client中加密或解密消息。

该身份需要在[Relation Name Service](#2.1.-relation-name-service)注册一个SBT Token作为隐私聊天的准入条件。

### 2.1. Relation Name Service

Relation Name Service是由Relation部署的一个符合Relation Protocol的[Name Service](https://docs.relationlabs.ai/protocol/contract-open-standard/identity/name-service)标准的合约。

Relation Name Service作为隐私聊天的准入条件，使用隐私聊天之前需要在`Relation Name Service`合约注册，用户将会获得一个SBT Token。

在聊天时，从`Relation Name Service`合约中获取对方的Name和头像等信息，可以更方便的我们找到和认出好友。

Relation Name Service合约主要提供以下接口：

* 注册：`register(address owner, string calldata name, bool reverseRecord) external returns (uint)`
* 设置头像：`setProfileURI(string memory profileURI) external`
* 获取用户名: `nameOf(address addr) external view returns (string memory)`
* 获取头像：`profileURI(address addr) external view returns (string memory)`

<figure><img src="/files/ksStbSfEEYMJ8Awp1i62" alt=""><figcaption><p>图 1-2 Relation Name Service</p></figcaption></figure>

## 3. Client

Client是一个前端应用程序，可以是web应用、Chrome插件、桌面应用程序，移动应用程序等，在Relation ONE中是一个[Chrome 插件](https://chrome.google.com/webstore/detail/relation-one/bmabahhenimmnfijaiccmonalfhpcndh)。

用户在登录到Client后，Client会通过WebSocket连接至Proxy。当有人给自己发消息时，Proxy主动会推送消息到Client，Client使用Lit对消息解密后就可以看到明文了。

用户在发消息前，Client按照[消息结构](#7.1.-message-jie-gou)组装消息体，使用Lit对消息体进行加密后再发送至Proxy。

由于消息是在Client进行加密和解密的，保证了数据的隐私性。

Client还需通过调用合约来管理[好友](#5.1.-hao-you-guan-li)和[群组](#5.2.-qun-zu-guan-li)。

## 4. Proxy

Relation ONE Proxy是一个后端应用程序，提供了一套REST Api接口（用于接收Client发送的消息、修改状态等）和一个WebSocket端口（用于推送消息至Client）。

Proxy会将Client发过来的消息存储至Arweave，由于所有消息都记录在去中心化存储中，因此Client并不是强依赖于某一个Proxy。即使Proxy宕机，Client在更换Proxy后，消息数据依然不会丢失。

### 4.1. 接收/发送消息

Client通过REST Api接口将(加密)消息发送至Proxy，Proxy会根据永久化存储策略将消息存储至Arweave，同时通过WebSocket将消息推送至接收者登录的Client上。

<figure><img src="/files/qel64lIiS5lmbV96JkRN" alt=""><figcaption><p>图 1-3 Send Message</p></figcaption></figure>

### 4.2. 状态保存

未读消息数、屏蔽群消息、置顶消息等状态信息，由Proxy记录在自己的数据库中，用户连接到不同的Proxy时未读消息数不共享。

### 4.3. 消息永久化存储

Proxy会将消息打包上传至Arweave，每个[消息包](#7.2.-xiao-xi-bao)里包含1条或多条消息。每个包上传至Arweave时，都会打一个tag，指向前一个交易的Hash。

永久化存储消息有三个策略：

1. 每12小时将消息打包上传到Arweave上。
2. 如果12小时内没有消息，将不会打包，然后重新计时12小时。
3. 如果没有到定时任务触发的时候，消息的大小超过了100MB，立即将消息打包上传到Arweave。

### 4.4. REST接口

Proxy需提供以下接口

| 接口描述   | Http Method | 接口地址                                           | Request Body                     | Response Body                       |
| ------ | ----------- | ---------------------------------------------- | -------------------------------- | ----------------------------------- |
| 发送消息   | POST        | /message/send                                  | [Message](#7.1.-message-jie-gou) |                                     |
| 查询聊天内容 | GET         | /message/list?limit=${limit}\&offset=${offset} |                                  | \[[Message](#7.1.-message-jie-gou)] |
| 标记为已读  | POST        | /message/read?messageId=${messageId}           |                                  |                                     |
| 未读消息数  | GET         | /message/unreadCount                           |                                  | Long                                |
| 聊天列表   | GET         | /chats/list?limit=${limit}\&offset=${offset}   |                                  | \[[Chat](#7.4.-chat-jie-gou)]       |

## 5. 关系管理

### 5.1. 好友管理

使用符合Relation Protocol的[Follow合约](https://docs.relationlabs.ai/protocol/open-standard-api/overview/relationship#follow)来管理好友。

每个[身份](#2.-shen-fen)都会通过`FollowRegister`创建一个Follow合约（见图 1-4），当Alice`follow`Bob的时候，只用调用Bob的`Follow合约`的`follow()`方法即可。

<figure><img src="/files/nZB3mQnXn7HiGE9cPy0K" alt=""><figcaption><p>图 1-4 Follow</p></figcaption></figure>

* 好友列表：通过`FollowRegister`遍历出所有的Follow合约，再遍历出每个用户的follow信息。
* 关注：调用对方合约的`follow()`方法。
* 取关：调用对方合约的`unfollow()`方法。

### 5.2. 群组管理

使用符合Relation Protocol的[DAO合约](https://docs.relationlabs.ai/protocol/open-standard-api/overview/relationship#dao)来管理群聊，该合约中所有持有Semantic SBT的地址即为群成员。

Client通过调用[标准的DAO接口](https://docs.relationlabs.ai/protocol/open-standard-api/overview/relationship#dao-1)来进行成员管理。

例如：发起人通过`DaoRegister`创建一个DAO合约。通过`isFreeJoin()`方法设置群聊是否可以自由加入。通过调用`join()`方法来加入群聊，通过调用`remove(address addr)`方法离开群聊。（见图1-5）

<figure><img src="/files/0ejLjUB6JgAX1rVaHn9Q" alt=""><figcaption><p>图 1-5 DAO</p></figcaption></figure>

* 创建群聊：在Client端调用`DaoRegister`合约的`deployDaoContract(address owner,string memory name) external returns (uint256)`方法创建一个新的群聊（见图 1-5）。
* 加入群聊：如果`isFreeJoin()`设置为`True`。可以通过`join()`方法自由加入；如果为`False`需要管理员通过`addMember(address[] memory addr) external`来添加成员。
* 离开群聊：自己调用`remove(address addr)`方法离开群聊。或者管理员调用`remove(address addr)`方法移除某个成员。
* 设置群公告：群公告由群主向群里发送一条特殊的[消息](#7.1.-message-jie-gou)，发送时将`kind`设置为`3`。
* 群名称：通过`name()`方法获取群名称。
* 群头像：通过`daoURI()`方法获取群头像。
* 群成员：合约里所有持有SBT Token的地址即为群成员。

## 6. 聊天

### 6.1. 点对点聊天

可以与[好友](#5.1.-hao-you-guan-li)进行聊天，也可以给非好友的以太坊地址发送消息。具体步骤：

Client（A）使用Lit对消息的`payload`进行加密（[accessCondition](https://developer.litprotocol.com/accesscontrol/evm/basicexamples/#a-specific-wallet-address)为接收方的以太坊地址），`receiver`设置为对方的以太坊地址，按照[消息结构](#7.1.-message-jie-gou)组装成消息体后发送至Proxy。Proxy会将消息推送至接收方地址登录的Client（B）。

### 6.2. 群聊

发送群消息时，用户A登录到Client（A）设置Lit对消息的`payload`进行加密（[accessCondition](https://developer.litprotocol.com/accesscontrol/evm/basicexamples/#must-posess-any-token-in-an-erc721-collection-nft-collection)为群成员可见），`receiver`设置为群聊的合约地址，按照[消息结构](#7.1.-message-jie-gou)组装成消息体后发送至Proxy。Proxy会将消息推送至每个群成员所在的Client（C）、Client（D）、Client（E）……

### 6.3. 加密/解密

[Client](#3.-client)会集成[Lit Protocol SDK](https://developer.litprotocol.com/sdk/explanation/encryption/)，每条消息都在发送者的CLient加密，在接收者的Client解密。

<figure><img src="/files/Sy9kFIhoUqKB2UOxIJLl" alt=""><figcaption><p>图 1-6 消息加密解密</p></figcaption></figure>

如图 1-6，`Alice`要给`Bob`发送一条加密消息：`Hi`。

1. Alice的Client会使用Lit SDK在本地对`Hi`进行加密，得到消息密文`encryptedString`和秘钥`symmetricKey`：

```js
const messageToEncrypt = "Hi";
// 1. Encryption
const { encryptedString, symmetricKey } = await LitJsSdk.encryptString(messageToEncrypt);
```

2. Alice的Client设置`accessControlConditions`为Bob的地址可见，并将`accessControlConditions`和秘钥`symmetricKey`上传到Lit的节点

```js
const accessControlConditions = [
    {
      "contractAddress": "",
      "standardContractType": "",
      "chain": "ethereum",
      "method": "",
      "parameters": [
        ":userAddress",
      ],
      "returnValueTest": {
        "comparator": "=",
        //Bob's wallet address
        "value": "0x50e2dac5e78B5905CB09495547452cEE64426db2"
      }
    }
];
// 2. Saving the Encrypted Content to the Lit Nodes
const encryptedSymmetricKey = await litNodeClient.saveEncryptionKey({
  accessControlConditions,
  symmetricKey,
  authSig,
  chain,
});
```

3. Alice的Client组装`Message`，并发送至Proxy。

```js
const message = {
  ...
  "sender": "Alice's wallet address",
  "receiver": "Bob's wallet address",
  "payload": "${encryptedString}",
  "encryptedSymmetricKey": "${encryptedSymmetricKey}",
  ...
}
```

4. Proxy将消息转发至Bob的Client。
5. Bob接收到Proxy推送的`Message`后，去Lit节点校验通过后拿到加密秘钥`_symmetricKey`：

```js
// 5. Decrypt it
// <String> toDecrypt
const toDecrypt = LitJsSdk.uint8arrayToString(encryptedSymmetricKey, 'base16');

// <Uint8Array(32)> _symmetricKey 
const _symmetricKey = await litNodeClient.getEncryptionKey({
    accessControlConditions,
    toDecrypt,
    chain,
    authSig
})
```

6. Bob在本地对消息解密：

```js
// <String> decryptedString
let decryptedString;

try{
    // 得到原文：Hi
    decryptedString = await LitJsSdk.decryptString(
        encryptedString,
        _symmetricKey
    );
}catch(e){
    console.log(e);
}
```

## 7. 消息定义

### 7.1. Message结构

```json
{
  "id": "${messageId}",
  "encryptedBy": "Lit",
  "sender": "${senderWalletAddress}",
  "receiver": "${receiverWalletAddress/receiverDaoContractAddress}",
  "timestamp": ${the message create timestamp in millisecond},
  "kind": ${kind},
  "tags": [
    ["quoteId", "${messageId of the quoted message}"],
    ["mentionedUsers", "${The wallet address of the mentioned user}"]
  ],
  "type": "${TEXT/CARD/NFT/IMAGE/EMOJI/BATCH_TRANSFER/ANNOUNCEMENT}",
  "payload": "${message encrypted by Lit}",
  "encryptedSymmetricKey": "${a hex string that LIT will use to decrypt payload as long as you satisfy the conditions}",
  "delegationMessage": "授权 ${地址} 代理签名，创建时间：${}, 过期时间：${}",
  "delegationSig": "${私钥对${delegationMessage}的签名}",
  "sig": "${授权私钥对${messageId}的签名}"
}
```

* `messageId`生成方法： 组装下面格式的json字符串，计算其Keccak256

```json
[
  <encryptedBy>,
  <sender>,
  <receiver>,
  <timestamp>,
  <kind>,
  <tags>,
  <payload>,
  <delegationMessage>,
  <delegationSig>
]
```

* `kind`
  * 0: p2p message。
  * 1: group message。
  * 2: dao message。
  * 3: announcement。
  * 4: hide message。
* Session Key：

在客户端生成一个公私钥对作为Session Key，把Session Key的公钥组装成`delegationMessage`，使用用户的私钥对`delegationMessage`进行签名，得到`delegationSig`。

用户发消息时，使用Session Key的私钥对消息进行签名，并且带上`delegationMessage`和`delegationSig`，通过这个delegationChain，就能验证消息是由用户本人签名的。

* `sig`生成方法：

通过Session Key的私钥对`messageId`进行签名即得到`sig`。

### 7.2. 消息包

1条或多条Message组成的数组称为一个消息包(Messages)，它是以下格式：

```
Messages = [Message,Message,...] 
```

### 7.3. Payload

* 文本消息

```json5
{
  //  ...
  "type": "TEXT",
  "payload": "Hi"
  //  ...
}
```

* 卡片消息

```json5
{
  //  ...
  "type": "CARD",
  "payload": "type:href;action:shareFavorite;p1(Text):https%3A%2F%2Fspace.id%2Fvoyage,p2(Text):,p3(Text):SPACE ID,p4(Text):https%3A%2F%2Fspace.id%2Ffavicon.ico"
  //type:${href};action:${shareFavorite};p1(Text):${URL},p2(Text):,p3(Text):${title},p4(Text):${icon}
  //  ...
}
```

* NFT

```json5
{
  //  ...
  "type": "NFT",
  "payload": "type:image;action:nftImage;p1(Text):https%3A%2F%2F3fypb-gqaaa-aaaag-aaedq-cai.ic1.io%2Fnft%2Fpolygon%2F0x4b5c095380e8e5f016a70a28af570c3bca93b811%2F25908.jpeg,p2(Text):polygon"
  //type:image;action:nftImage;p1(Text):${imageURL},p2(Text):${chainName}
  //  ...
}
```

* 本地图片

```json5
{
  //  ...
  "type": "IMAGE",
  "payload": "type:image;action:localImage;p1(Text):https%3A%2F%2F3fypb-gqaaa-aaaag-aaedq-cai.ic1.io%2Fim%2Fimage%2Fnymb5-kqaaa-aaaaj-4thiq-cai%2F2f3537d1a2474f2fb92cb5f7de2ed70a.png,p2(Text):im/image/nymb5-kqaaa-aaaaj-4thiq-cai/2f3537d1a2474f2fb92cb5f7de2ed70a.png"
  //type:image;action:localImage;p1(Text):${imageURL},p2(Text):${s3ObjectKey}
  //  ...
}
```

* 表情包

```json5
{
  //  ...
  "type": "EMOJI",
  "payload": "type:image;action:customEmoji;p1(Text):https%3A%2F%2F3fypb-gqaaa-aaaag-aaedq-cai.ic1.io%2Fim%2Femoji%2Flov6e-qqaaa-aaaaj-xykxq-cai%2Fac1dc2bf6bad48aeb1a502c0a28ee942.jpg,p2(Text):im/emoji/lov6e-qqaaa-aaaaj-xykxq-cai/ac1dc2bf6bad48aeb1a502c0a28ee942.jpg"
  //type:image;action:customEmoji;p1(Text):${imageURL},p2(Text):${s3ObjectKey}
  //  ...
}
```

* 群转账

```json5
{
  //  ...
  "type": "BATCH_TRANSFER",
  "payload": "0xd84ed2b4deacbad8a568747fea45f3fc7f2d204595101f99f43006712f3ff290::USDT::20.0000::0x969f85053b44d7eCE8108094420Aba45149b7A3a::qzoo5-ryaaa-aaaaj-xs6ma-cai"
  //payload: 0xbaf915e778b044dbf62974d17bd2ab8875bccce8bd02c40dfc241d2893755851::USDT::5.0000::aukee-3yaaa-aaaah-qc3ya-cai::4xtgo-viaaa-aaaaj-aavyq-cai,ms5ob-iqaaa-aaaaj-aa2za-cai,kppoo-ozrgz-rtkn3-cgqyq-cai,meohx-szymu-ytezr-ugiza-cai,oazqd-ndeg4-ytqyj-zga2q-cai
  //payload: 0x71de31f73f34fc9af20f7ce3b35ec4e56bf25af476f99cb376779761c2fe91fa::USDT::1.0000::aukee-3yaaa-aaaah-qc3ya-cai::7zshb-ryaaa-aaaaj-aag3a-cai
  //  ...
} 
```

* 公告

```json5
{
  //  ...
  "type": "TEXT",
  "payload": "It's an announcement"
  //  ...
}
```

### 7.4. Chat结构

```json
{
  "id": "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5",
  "name": "test-group",
  "avatar": "",
  "channelType": "GROUP",
  "unreadMessageCount": 0,
  "type": "TEXT",
  "payload": "the message payload",
  "lastPostAt": "1665219499000"
}
```

## 8. 消息存储

使用永久存储Arweave来存储加密消息，保证了数据的可靠性和去中心化。

[Message](#7.1.-message-jie-gou)由[Proxy](#4.-proxy)打包成[Messages](#7.2.-xiao-xi-bao)，上传至rweave上。每个Transaction都被打上一个tag，指向前一个Transaction的Hash。可以方便的根据最新Hash找到所有历史消息。

```
Messages = [Message, Message, Message]
```

<figure><img src="/files/kixVH2Dkiy85GMfbl9K0" alt=""><figcaption><p>图 1-7 消息存储</p></figcaption></figure>

## 9. 合约

| Name                  | Docs                                                                                                    | Mumbai                                     | Goerli                                     | zkSync Era Testnet                         |
| --------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ |
| Relation Name Service | [name-service docs](https://docs.relationlabs.ai/protocol/contract-open-standard/identity/name-service) | 0x04C23dDfE813d8D208bc8120b73F8EF30f423850 | 0xe4AA1f1E5be0A4E7F80CAC26C2Db1611C3E70c41 | -                                          |
| FollowRegister        | [follow docs](https://docs.relationlabs.ai/protocol/contract-open-standard/relationship/follow)         | 0xA0cECa90d7A2033f5162dc73391cdB72272456E7 | -                                          | 0x734dE4f1b3a1c0619ECB80e3a676a6cFabe72Adc |
| DaoRegister           | [DAO docs](https://docs.relationlabs.ai/protocol/contract-open-standard/relationship/dao)               | 0x153C3eF051C7665c9A9c1a718bF12bC8EE6b5115 | -                                          | 0x7229Dc0117E8484D61137B7304b02b163beC912c |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://relationlabs.gitbook.io/relation-one-api/api-zh/im.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
