text: ---
  3ip: 9
  title: Ghost Threads: Ephemeral Threads on 3box
  author: Ghilia Weldesselasie @ghiliweld
  discussions-to: https://github.com/3box/3box/issues/675
  status: Draft
  created: 2019-08-17
  requires: 3IP 1
  ---
  
  ## Simple Summary
  Ephemeral messages on 3box, for when we don't want messages to persist (like in Persistent Threads).
  
  ## Abstract
  Currently 3Box supports building chat like system though the threads api. However, for building things like trollboxes, or online lists, etc. threads are not the best fit. Ipfs has a feature called pubsub, which is a system for passing messages between peers. Pubsub can be used to create an ephemeral chat system where messages are not persisted anywhere in the network, rather they are just sent across once and seen by anyone online at that point. 
  
  ## Motivation
  One could create Threads and delete message we don't want persisted, but if we already know we don't want to persist any messages in a chat it makes more sense to simply use pubsub and not save messages from the get-go. Which is why we're working on this new standard for ephemeral messaging.
  
  ## Specification
  
  ### Room Discovery
  
  Each pubsub room on ipfs has a name that serves as a topic between peers. To distinguish between different rooms within the network and within spaces themselves we employ a domain-like system for naming rooms.
  
  Let `space` be the space the chat is situated in and `room` the specific room we want to chat in. Our pubsub room name will be `3box.chat.space.room`. 
  
  ### Peer Discovery
  
  Right now 3Box peers are only connected to the 3Box pinningNode. This makes it impossible for nodes to discover each other and send each other pubsub messages. We need to set up a [Rendezvous](https://stackoverflow.com/questions/52910503/how-can-i-get-a-libp2p-rendezvous-server-running) server for peers to find other peers, we also possibly need to enable the DHT (which is still an experimental feature in js-ipfs). Another alternative is to use the **Delegated peer and content routing** described [here](https://github.com/libp2p/js-libp2p/tree/master/examples/delegated-routing) and [here](https://github.com/ipfs/js-ipfs#configuring-delegate-routers).An important thing to consider is if we want to connect to the default ipfs nodes which would make us part of the entire ipfs network, or if we want to keep our nodes only connected to each other and not the rest of the ipfs network.
  
  ### Message signing
  
  We want to make sure that the messages posted in the chat are from the user that they claim to come from. We can do this by internally signing the messages as JWT claims, which 3box already supports. These JWTs can be signed by the space-3id for easy verification. This would all happen behind the scenes in the api though so that consumers of the api wouldn't need to worry about it.
  
  ### API
  
  The public `GhostThread` api closely matches that of other thread types, however methods such as `deletePost`, `addModerator`, `listModerators` and `addMember` aren't available and will throw errors when called, since you can't delete posts that aren't persisted and ghost threads currently don't have moderator capabilities.
  
  **Joining a chat:**
  ```js
  const thread = await space.joinThread('name-of-thread', { ghost: true, ghostBacklogLimit: 20 })
  // ghostBacklogLimit is the limit we want to set for the size of our backlog
  // optional, by default we set it at 50 messages
      
  thread.on('user-joined', (did) => {
    console.log(did, ' joined the chat')
  })
  
  // or
  
  thread.onNewCapabilities((_event, did) => console.log(did, _event, ' the chat'))
  ```
  This does several things in the background:
  
  1. The user joins a room on `ipfs-pubsub-room`.
  2. The user broadcasts a message saying that it joined. We do this so that we can also transmit the space-3id we're joining with. A `user-joined` event is also emitted.
  3. The space-3id is then received by peers in the room and each peer saves that id to an array of 3ids called `_members`
  
  **Leaving a chat:**
  ```js
  await thread.close() 
      
  thread.on('user-left', (did) => {
    console.log(did, ' left the chat')
  })
  
  // or
  
  thread.onNewCapabilities((_event, did) => console.log(did, _event, ' the chat'))
  ```
  This does several things in the background:
  
  1. The user leaves a room on `ipfs-pubsub-room`
  2. A `peer left` event with the peerId that's leaving is emitted to other peers, via pubsub.
  3. The space-3id associated with the peerId is then received by peers in the room and each peer deletes that id from `_members`. A `user-left` event is also emitted.
  
  **Fetching online users in the chat:**
  ```js
  // returns an array of space-3ids of the users
  const userList = await thread.listMembers()
      
  // fetches all 3ids saved in chat._members array
  ```
  
  **Fetching old posts saved in the chat backlog:**
  ```js
  // returns an array of messages
  // varies from this._backlogLimit (default 50) last messages...
  const posts = await thread.getPosts()
  
  // to last n messages
  const posts = await thread.getPosts(20)
  ```
  
  **Broadcasting a message in the chat:**
  ```js
  await thread.post('hello world') 
  
  // you can also send a message directly to a peer
  
  await thread.post('hello there', '3id or ipfs peer id')
      
  thread.on('message', ({ author, message }) => {
    console.log(`new message from ${author}: ${message}`)
  })
  
  // or
  
  thread.onUpdate(({ author, message }) => console.log(`new message from ${author}: ${message}`))
  ```
  This does several things in the background:
  
  1. The user signs a message jwt
  2. The user broadcasts that jwt in the room or send the message directly to a peer
  3. Peers decode and verify jwt that are caught
  4. Peers reject any message that aren't signed properly or are invalid before they get passed to our `message` event listener
  5. Saves all messages in local cache
  
  **Requesting a backlog of past messages from peers:**
  ```js
  // person requesting
  const backlog = await thread._requestBacklog()
  
  // will log an array of messages to the console
  thread.on('backlog-received', ({ message }) => console.log(message))
  
  thread.onUpdate(({type, message}) => console.log(`received a ${type}: ${message}`))
  // => received a backlog: <backlog>
  ```
  In the background:
  
  1. New user broadcasts a message to other peers asking for a backlog
  2. Peers directly send the user a message containing the backlog (naive but it works)
  
  ## Implementation
  To implement this we can likely use `ipfs-pubsub-room` or something similar like [orbit-db-pubsub](https://github.com/orbitdb/orbit-db-pubsub) which is already a dependancy for us, but looks more limited.
  
  ### **Requirements**
  
  - Should extend the 3box-js library using the api defined above
  - Should be able to find all other peers on the same chat within a reasonable time bound
  - Should work well in most browsers (including mobile dapp browsers)
  
  ### **Testing**
  
  The 3box-js repo has an example-dapp which can be extended in order to test the chat functionality.**Ways to test:**
  
  - Implement tests as part of 3box-js, similar to how other tests are set up in the 3box-js repo now we can set up two ipfs nodes in the test file and have them communicate with each other.
  
  In all of the examples below different 3box accounts are used to send messages in each browser.
  - Two browsers same computer, e.g. chrome and firefox
  - Two browsers same network but different computers, e.g. phone and desktop connected to the same wifi
  - Two browsers different networks, e.g. phone not on wifi and computer on wifi etc
  - Come up with more test cases? \o/
  
  Before we go ahead and try to get everything working with discovery, we could test the api by directly connecting nodes. This would allow us to make sure the the API is fully functional by itself.
  
  ### **Questions to answer during the implementation process**
  
  - Should we connect to the rest of the ipfs network or keep our network separate?
  - What are the tradeoffs?
  - What are the differences in performance?
  
  ### Module
  ```js
  class GhostThreads extends EvenEmitter {
  
    constructor(name, opts) {
      this.room = new PubsubRoom(name)
    }
  
  }
  ```
  
  ## Architecture
  Messages and notifications are propagated through ipfs pubsub. `ipfs-pubsub-room` doesn't distinguish between types of messages, so we add a `type` to our jwt payload so that we can approprietly handle the message caught by our `ipfs-pubsub-room` instance. We introduce several message types:
  
  - `join` for when a user announces their presence in the thread
  - `chat` for when a user posts a message in the thread
  - `request_backlog` for when a user requests a 12 hour backlog of past messages
  - `backlog_response` for when a peer fulfills a backlog request and the user receives it
  
  With each message type, a jwt is signed. When verifying the jwt, a payload and associated issuer is decoded so that we can determine the DID of the user joining or talking in the thread.
  
  When a user broadcasts a backlog request, other peers that catch this request will send back the posts in their backlog to the requesting peer.
  
  Although our system uses ipfs pubsub, our peers don't connect to the main network. Instead they are all connected throuh the 3box pinning node. To circumvent the connectivity issue that comes with not being connected to the main network, we use the [`lipp2p-websocket-star-rendezvous` server](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous). This allows us to discover other nodes and route our messages to them.
  
  ## Wishlist
  This section outlines certain features we would like to include in Ghost Chat but that may be out of scope for the initial release.
  
  - Private Ghost Chats
  - Spam Protection
  - Client Side Filters
  
  ### Client Side Filters
  This is how filters would work for filtering messages in rooms, possibly enabling blocklists as well.
  ```js
  thread.addFilter(filterFunction)
      
  // filters any message passing in a room according to the filterFunction
  // any messages filtered out will not be emitted to our on-message event
  // it'll be like they were never sent
      
  /// example
  thread.addFilter(allExceptFromTroll) // all messages are valid except from troll
      
  thread.on('message', (did, message) => {
      console.log(`new message from ${did}: ${message}`)
  })
      
  // now if troll opens a space and attempts to message the room,
  // our filter won't pick up any messages from them
  ```
  Probably won't make the first release but it's a cool idea for moderation in trollboxes. We'll have to see if devs can add more filters as time goes on or if there can only be one set filter.
  
  
  ## Copyright
  Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
  