# Relation ONE IM 介绍

<figure><img src="https://533586317-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaIXykAjtdDiU26AqKCRu%2Fuploads%2Fgit-blob-e0d9e95208b8f327294a92453411316b4839de51%2Fim.png?alt=media" 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="https://533586317-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaIXykAjtdDiU26AqKCRu%2Fuploads%2Fgit-blob-9af4633b00e29e2da43c3c9869b63ded22f4df4d%2Fname-service.png?alt=media" 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="https://533586317-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaIXykAjtdDiU26AqKCRu%2Fuploads%2Fgit-blob-d9c5426ed4543f2a6cf4bb32e11d4a966af9276f%2Fsend-message.png?alt=media" 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="https://533586317-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaIXykAjtdDiU26AqKCRu%2Fuploads%2Fgit-blob-5b9e1b25833ebd90d9d4ae0044917cd138e2dfd3%2Ffollow.png?alt=media" 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="https://533586317-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaIXykAjtdDiU26AqKCRu%2Fuploads%2Fgit-blob-a702ec0db2f190923e0dd918637e9bbba5bd6911%2Fdao.png?alt=media" 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="https://533586317-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaIXykAjtdDiU26AqKCRu%2Fuploads%2Fgit-blob-400b49432a50e95df0f081fa67c54e2ba90988f4%2Fencrypt-decrypt.png?alt=media" 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="https://533586317-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaIXykAjtdDiU26AqKCRu%2Fuploads%2Fgit-blob-e9350d41965885bb30dd514b472502d895814a12%2Fmessage-data-storage.png?alt=media" 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 |
