Towns 백서

Towns

원문

bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 17쪽 중 7쪽

Creating a Space Once connected, you can start interacting with Towns. Here's how to create a space: <Note> You can only create a space with a Web3 wallet. If you're using a bearer token, you can't create spaces. </Note> ```tsx theme={null} import { useCreateSpace } from "@towns-protocol/react-sdk"; import { getEthersSigner } from "@/utils/viem-to-ethers"; import { wagmiConfig } from "@/config/wagmi"; function CreateSpace({ onCreateSpace }: { onCreateSpace: (spaceId: string) => void }) { const { createSpace, isPending } = useCreateSpace(); const [spaceName, setSpaceName] = useState(''); return ( <form onSubmit={async (e) => { e.preventDefault(); const signer = await getEthersSigner(wagmiConfig); const { spaceId } = await createSpace({ spaceName }, signer); // Let a parent component handle the spaceId, keep reading the guide! onCreateSpace(spaceId); // Reset form setSpaceName(''); }} > <div> <input value={spaceName} onChange={(e) => setSpaceName(e.target.value)} placeholder="Space name" required /> </div> <button type="submit" disabled={isPending}> {isPending ? "Creating..." : "Create Space"} </button> </form> ); } ``` ## Sending Your First Message With the space created, we can send a message to the `#general` channel. Here's how to create a form ```tsx theme={null} import { useChannel, useSendMessage, useSpace, useTimeline, useUserSpaces, } from '@towns-protocol/react-sdk' import { RiverTimelineEvent } from '@towns-protocol/sdk' import { useState } from 'react' // Form to send a message to a channel function ChatInput({ spaceId, channelId }: { spaceId: string; channelId: string }) { const { sendMessage, isPending } = useSendMessage(channelId) const { data: channel } = useChannel(spaceId, channelId) const [message, setMessage] = useState('') return ( <form onSubmit={(e) => { e.preventDefault() sendMessage(message) setMessage('') }} > <input value={message} placeholder={`Type a message in ${channel?.metadata?.name || 'unnamed channel'}`} onChange={(e) => setMessage(e.target.value)} /> <button type="submit" disabled={isPending}> {isPending ? 'Sending...' : 'Send'} </button> </form> ) } // Display messages from a channel function ChatMessages({ channelId }: { channelId: string }) { const { data: events } = useTimeline(channelId) return ( <div> {/* Filter out non-message events */} {events ?.filter((event) => event.content?.kind === RiverTimelineEvent.ChannelMessage) .map((message) => ( <p key={message.eventId}> {message.content?.kind === RiverTimelineEvent.ChannelMessage ? message.content?.body : ''} </p> ))} </div> ) } // Chat component that renders the chat input and messages, given a spaceId function Chat({ spaceId }: { spaceId: string }) { const { data: space } = useSpace(spaceId) const channelId = space?.channelIds?.[0] // Spaces have a default channel called `general` return ( <> {channelId && <ChatInput spaceId={spaceId} channelId={channelId} />} {channelId && <ChatMessages channelId={channelId} />} </> ) } // Renders the name of a space const SpaceName = ({ spaceId }: { spaceId: string }) => { const { data: space } = useSpace(spaceId) return <>{space?.metadata?.name}</> } // Chat app that renders the create space and chat components export const ChatApp = () => { // Get the list of spaces we're a member of const { spaceIds } = useUserSpaces() const [spaceId, setSpaceId] = useState('') return ( <div> {/* Show a list of spaces we're a member of, click to select one */} {spaceIds.map((spaceId) => ( <button type="button" key={spaceId} onClick={() => setSpaceId(spaceId)}> <SpaceName spaceId={spaceId} /> </button> ))} {/* Show the create space form if we don't have a space */} {!spaceId && <CreateSpace onCreateSpace={(spaceId) => setSpaceId(spaceId)} />} {/* Render the chat component with the current selected space */} {spaceId && ( <h1> Chatting in <SpaceName spaceId={spaceId} /> </h1> )} {spaceId && <Chat spaceId={spaceId} />} </div> ) } ``` Add the `ChatApp` component to your app and you're ready to start chatting! ## Next Steps Now that you have the basics set up, you can: * [Create and join spaces](/build/react-sdk/api/useCreateSpace) * [Create channels](/build/react-sdk/api/useCreateChannel) * [Send messages](/build/react-sdk/api/useSendMessage) * [Read messages](/build/react-sdk/api/useTimeline) * [Send reactions](/build/react-sdk/api/useSendReaction) * [Set username](/build/react-sdk/api/useSetUsername) * [Read message threads](/build/react-sdk/api/useThreads) * much more! You can find the full list of hooks in the [API Reference](/build/react-sdk/api). Check out our [Playground template](https://river-sample-app.vercel.app) for a full-featured example application. # Towns Encryption Protocol Source: https://docs.towns.com/build/towns-encryption How the Towns encryption and decryption protocol works. ### What is the Towns Encryption Protocol? The Towns protocol supports end-to-end encryption and decryption between a group of user devices. When a user (Bob) wants to send a new message to a group of users, Bob's device first creates a new group session. Using the outbound session key, it encrypts the message. Then the encrypted message is sent to the group. When a recipient (Alice) gets the encrypted message, her device will start a new group session to import the inbound session key, and then use it to decrypt the message. But how does Alice's device gets the inbound session key in the first place? The following section describes the inner workings of the group encryption protocol. It explains how Alice's device makes a **`key solicitation request`** to get missing session keys. It also covers how other devices in the group can **`share session keys`** in a process called **`key fulfillment`** after checking that Bob is a group member. Before diving into the `key solicitation`, `key fulfillment`, and `key sharing` algorithms, let's first take a look at the core entities in the group encryption protocol. ### Core entities in the Towns Encryption Protocol **`GroupEncryptionCrypto`** : The main interface of the protocol. It initializes the **`EncryptionDelegate`**. This class "delegates" the encryption and decryption operations to the [`olm` library from the Matrix.org foundation](https://www.npmjs.com/package/@matrix-org/olm). This library implements the Double Ratchet algorithm. See notes on [supported algorithm](#supported-algorithm) for future plans. The Towns Encryption Protocol uses this library to create a group session, and perform device-to-device encryption using the session keys. The `GroupEncryptionCrypto` creates a `GroupEncryption`, a `GroupDecryption`, and an `EncryptionDevice` to handle the group encryption protocol: ```mermaid theme={null} --- title: GroupEncryptionCrypto and its embedded components --- erDiagram GroupEncryptionCrypto ||--|| EncryptionDelegate : "initializes" GroupEncryptionCrypto ||--|| EncryptionDevice : "creates" GroupEncryptionCrypto ||--|| GroupEncryption : "creates" GroupEncryptionCrypto ||--|| GroupDecryption : "creates" GroupEncryptionCrypto ||--|| CryptoStore : "has reference to" ``` * **`GroupEncryption`** : handles group encryption using session keys. Outgoing messages are encrypted with outbound session keys. * **`GroupDecryption`** : handles group decryption using session keys. Incoming messages are decrypted with inbound session keys. * **`EncryptionDevice`** : interfaces with the `EncryptionDelegate` to perform cryptographic operations. It also uses the `CryptoStore` to get and save the inbound / outbound session keys. ```mermaid theme={null} erDiagram GroupEncryption ||--|| EncryptionDevice : "uses for encryption" GroupDecryption ||--|| EncryptionDevice : "uses for decryption" GroupEncryption ||--|| IGroupEncryptionClient : "uses for key sharing" EncryptionDevice ||--|| EncryptionDelegate : "encrypts and decrypts with" EncryptionDevice ||--|| CryptoStore : "gets and stores keys in" ``` ### Encrypting a message with GroupEncryption A device will need an outgoing session to encrypt a message to the group. If it does not already have a session, it must create one. In addition, it will also create an inbound session. The inbound session is encrypted and sent to other devices in the group. ```mermaid theme={null} flowchart TB groupEncryption(encrypt) --> outboundSession{"outbound session exists?"} outboundSession -- no --> createOutboundSession("create outbound session") createOutboundSession --> saveOutboundSession("save outbound session in CryptoStore") outboundSession -- yes --> encrypt("encrypt plaintext with outbound session key") encrypt -- returns --> EncryptedData saveOutboundSession --> createInboundSession("create inbound session") createInboundSession --> saveInboundSession("save inbound session in CryptoStore") saveInboundSession --> encryptAndShareInboundSession("encrypt and share inbound session with other devices in group") encryptAndShareInboundSession --> encrypt ``` ### Decrypting a message with GroupDecryption ```mermaid theme={null} flowchart TB groupDecryption(decrypt) --> inboundSession{"inbound session exists?"} inboundSession -- no --> decryptionError("decryption error") inboundSession -- yes --> decrypt("decrypt ciphertext") decrypt -- returns --> plaintext ``` ### Key solicitation and key fulfillment If a device does not have any session keys, it can make a `KeySolicitation` request to the group. Any device that is "online" at that moment can share its known session keys, and send a `KeyFulfillment` response to inform others in the group that the `KeySolicitation` request has already been fulfilled. Continuing our example, suppose Alice's device does not have the session key to decrypt a message from Bob. Alice's device posts a `KeySolicitation` request to the stream. Bob's device happens to be online at the moment. When it sees the request, it processes the request as follows: ```mermaid theme={null} flowchart TB onKeySolicitation --> userIsEntitledToStream{"is user entitled to read from this stream?"} userIsEntitledToStream -- yes --> getSessionKeys("get session keys") userIsEntitledToStream -- no --> stop getSessionKeys --> sendKeyFulfillment("send key fulfillment to inform the group that the key solicitation was handled") sendKeyFulfillment --> encryptAndShareSession("encrypt and share the session keys with the requester") ``` Thus, Alice's device is able to get the required session keys for decryption. ## Build See [towns-tutorials/encryption-import](https://github.com/towns-protocol/river-tutorials/blob/main/encryption-import/README.md) for a tutorial on how to import the Towns Encryption npm package. *** ## Footnotes ### Supported algorithm > The Towns Encryption Protocol is designed to support new algorithms. There is > an `algorithm` field in the protocol definition. The current value is > `r.group-encryption.v1.aes-sha2`. This means that the protocol is using the > `olm` Double Ratchet library for device-to-device session encryption. > This field can be set to new algorithms to support future needs. ```protobuf theme={null} // protocol.proto message EncryptedData { // ... /** * Encryption algorithm used to encrypt this event. */ string algorithm = 2; // ... } ``` # Messaging Data Structures Source: https://docs.towns.com/build/towns-messaging-datastructures ### Messaging Data Structures The core application supported by Towns Protocol is messaging. Towns Stream Nodes are tasked with serving stream events, which are represented as data using protobufs. Stream events themselves may contain message-like events or metadata to support event validation. ### Protocol Messaging Protobuf The data schema of the Towns protocol can be defined declaratively by [protobuf.proto](https://github.com/towns-protocol/towns/blob/main/protocol/protocol.proto). Below is an elided snapshot of `protocol.proto` as of January 2024 containing only schema definitions used in a messaging context within the Towns protocol. ```proto theme={null} syntax = "proto3"; package river; option go_package = "github.com/towns-protocol/towns/protocol"; import "google/protobuf/timestamp.proto"; import "google/protobuf/empty.proto"; /** * StreamEvent is a single event in the stream. */ message StreamEvent { /** * Address of the creator of the event. * For user - address of the user's primary wallet. * For server - address of the server's keypair in staking smart contract. * * For the event to be valid: * If delegate_sig is present, creator_address must match delegate_sig. * If delegate_sig is not present, creator_address must match event signature in the Envelope. */ bytes creator_address = 1; /** * delegate_sig allows event to be signed by device keypair * which is linked to the user's primary wallet. * * delegate_sig contains signature of the public key of the device keypair. * User's primary wallet is used to produce this signature. * * If present, for the event to be valid: * 1. creator_address must match delegate_sig's signer public key * 2. delegate_sig should be the signature of Envelope.signature's public key. * * Server nodes sign node-produced events with their own keypair and do not * need to use delegate_sig. */ bytes delegate_sig = 2; /** Salt ensures that similar messages are not hashed to the same value. genId() from id.ts may be used. */ bytes salt = 3; /** Hash of a preceding miniblock. Null for the inception event. Must be a recent miniblock */ optional bytes prev_miniblock_hash = 4; /** CreatedAt is the time when the event was created. NOTE: this value is set by clients and is not reliable for anything other than displaying the value to the user. Never use this value to sort events from different users. */ int64 created_at_epoch_ms = 5; /** Variable-type payload. * Payloads should obey the following rules: * - payloads should have their own unique type * - each payload should have a oneof content field * - each payload should have an inception field inside the content oneof * - each payload should have a unique Inception type * - payloads can't violate previous type recursively to inception payload */ oneof payload { MiniblockHeader miniblock_header = 100; CommonPayload common_payload = 101; SpacePayload space_payload = 102; ChannelPayload channel_payload = 103; UserPayload user_payload = 104; UserSettingsPayload user_settings_payload = 105; UserDeviceKeyPayload user_device_key_payload = 106; UserToDevicePayload user_to_device_payload = 107; MediaPayload media_payload = 108; DmChannelPayload dm_channel_payload = 109; GdmChannelPayload gdm_channel_payload = 110; } } /** * SpacePayload */ message SpacePayload { message Snapshot { // inception Inception inception = 1; // streamId: Channel map<string, Channel> channels = 2; // userId: Membership map<string, Membership> memberships = 3; // userId: Username map<string, WrappedEncryptedData> usernames = 4; // userId: Displayname map<string, WrappedEncryptedData> display_names = 5; } message Inception { string stream_id = 1; StreamSettings settings = 2; } message Channel { ChannelOp op = 1; string channel_id = 2; EventRef origin_event = 3; EncryptedData channel_properties = 4; } oneof content { Inception inception = 1; Channel channel = 2; Membership membership = 3; EncryptedData username = 4; EncryptedData display_name = 5; } } /** * ChannelPayload */ message ChannelPayload { message Snapshot { // inception Inception inception = 1; // userId: Membership map<string, Membership> memberships = 2; } message Inception { string stream_id = 1; string space_id = 3; /** * channel_name and channel_topic from this payload will be used to * create associated with that channel space event for stream as we agreed * that channel names and topics will be delivered using space stream */ EncryptedData channel_properties = 4; StreamSettings settings = 5; } oneof content { Inception inception = 1; EncryptedData message = 2; Membership membership = 3; } } /** * DmChannelPayload */ message DmChannelPayload { message Snapshot { Inception inception = 1; map<string, Membership> memberships = 2; map<string, WrappedEncryptedData> usernames = 3; map<string, WrappedEncryptedData> display_names = 4; } message Inception { string stream_id = 1; string first_party_id = 2; string second_party_id = 3; StreamSettings settings = 4; } oneof content { Inception inception = 1; Membership membership = 2; EncryptedData message = 3; EncryptedData username = 4; EncryptedData display_name = 5; } } /** * GdmChannelPayload */ message GdmChannelPayload { message Snapshot { Inception inception = 1; map<string, Membership> memberships = 2; map<string, WrappedEncryptedData> usernames = 3; map<string, WrappedEncryptedData> display_names = 4; WrappedEncryptedData channel_properties = 5; } message Inception { string stream_id = 1; EncryptedData channel_properties = 2; StreamSettings settings = 3; } oneof content { Inception inception = 1; Membership membership = 2; EncryptedData message = 3; EncryptedData username = 4; EncryptedData display_name = 5; EncryptedData channel_properties = 6; } } /** * MediaPayload */ message MediaPayload { message Snapshot { Inception inception = 1; } message Inception { string stream_id = 1; string channel_id = 2; int32 chunk_count = 3; StreamSettings settings = 4; } message Chunk { bytes data = 1; int32 chunk_index = 2; } oneof content { Inception inception = 1; Chunk chunk = 2; } } message Membership { MembershipOp op = 1; string user_id = 2; } message EncryptedData { /** * Ciphertext of the encryption envelope. */ string ciphertext = 1; /** * Encryption algorithm used to encrypt this event. */ string algorithm = 2; /** * Sender device public key identifying the sender's device. */ string sender_key = 3; /** * The ID of the session used to encrypt the message. */ string session_id = 4; /** * Optional checksum of the cleartext data. */ optional string checksum = 5; } message WrappedEncryptedData { EncryptedData data = 1; int64 event_num = 2; bytes event_hash = 3; } enum MembershipOp { SO_UNSPECIFIED = 0; SO_INVITE = 1; SO_JOIN = 2; SO_LEAVE = 3; } enum ChannelOp { CO_UNSPECIFIED = 0; CO_CREATED = 1; CO_DELETED = 2; CO_UPDATED = 4; } ``` #### SpacePayload `StreamEvent` messages encapsulate all message types handled by Towns Nodes. The primary unit of account for messaging is described in `SpacePayload`. A newly minted Space is represented on-chain with the SpaceFactory contract as well of in Towns Nodes and the Towns Chain, which stores the `streamId` of the Space. There exists a 1-to-many relation between Spaces as defined in data by the `SpacePayload` message and `Channels`. Furthermore, `Memberships`, `usernames`, and `display_names` are stored and snapshotted within the `streamId` of a Space. `SpacePayload` is made polymorphic using `oneof` in the above definition. Towns Nodes apply rules to each message after unpacking the `payload` field from the `StreamEvent`, which is the message transmitted to the Towns Node over the wire. These rules validate the legality of the `event` for writes that `addEvent` to the stream. ### Channel messages Channel messages are the primary messages used to facilitate group chat within Spaces. They are defined in `ChannelPayload` in `protocol.proto`. Each `ChannelPayload` message is typed as `EncryptedData`, which is a message that defines the payload of ciphertext. > Remember, Towns nodes never see plain text of messages. The protocol defines any message-like event or event that requires encryption with an `EncryptedData` or `WrappedEncryptedData` message type in `protocol.proto`. ### DM Channel messages Direct messages are the second messaging primitive supported by the Town
프로젝트로 돌아가기