wilsonsilva/nostr

View on GitHub
lib/nostr/client.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# frozen_string_literal: true

require 'event_emitter'
require 'faye/websocket'

module Nostr
  # Clients can talk with relays and can subscribe to any set of events using a subscription filters.
  # The filter represents all the set of nostr events that a client is interested in.
  #
  # There is no sign-up or account creation for a client. Every time a client connects to a relay, it submits its
  # subscription filters and the relay streams the "interested events" to the client as long as they are connected.
  #
  class Client
    include EventEmitter

    # Instantiates a new Client
    #
    # @api public
    #
    # @example Instantiating a client that logs all the events it sends and receives
    #   client = Nostr::Client.new
    #
    # @example Instantiating a client with no logging
    #   client = Nostr::Client.new(logger: nil)
    #
    # @example Instantiating a client with your own logger
    #   client = Nostr::Client.new(logger: YourLogger.new)
    #
    def initialize(logger: ColorLogger.new)
      @subscriptions = {}
      @logger = logger

      logger&.attach_to(self)
      initialize_channels
    end

    # Connects to the Relay's websocket endpoint
    #
    # @api public
    #
    # @example Connecting to a relay
    #   relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
    #   client.connect(relay)
    #
    # @param [Relay] relay The relay to connect to
    #
    # @return [void]
    #
    def connect(relay)
      execute_within_an_em_thread do
        client = build_websocket_client(relay.url)
        parent_to_child_channel.subscribe { |msg| client.send(msg) && emit(:send, msg) }

        client.on :open do
          child_to_parent_channel.push(type: :open, relay:)
        end

        client.on :message do |event|
          child_to_parent_channel.push(type: :message, data: event.data)
        end

        client.on :error do |event|
          child_to_parent_channel.push(type: :error, message: event.message)
        end

        client.on :close do |event|
          child_to_parent_channel.push(type: :close, code: event.code, reason: event.reason)
        end
      end
    end

    # Subscribes to a set of events using a filter
    #
    # @api public
    #
    # @example Creating a subscription with no id and no filters
    #   subscription = client.subscribe
    #
    # @example Creating a subscription with an ID
    #   subscription = client.subscribe(subscription_id: 'my-subscription')
    #
    # @example Subscribing to all events created after a certain time
    #   subscription = client.subscribe(filter: Nostr::Filter.new(since: 1230981305))
    #
    # @param subscription_id [String] The subscription id. An arbitrary, non-empty string of max length 64
    #   chars used to represent a subscription.
    # @param filter [Filter] A set of attributes that represent the events that the client is interested in.
    #
    # @return [Subscription] The subscription object
    #
    def subscribe(subscription_id: SecureRandom.hex, filter: Filter.new)
      subscriptions[subscription_id] = Subscription.new(id: subscription_id, filter:)
      parent_to_child_channel.push([ClientMessageType::REQ, subscription_id, filter.to_h].to_json)
      subscriptions[subscription_id]
    end

    # Stops a previous subscription
    #
    # @api public
    #
    # @example Stopping a subscription
    #  client.unsubscribe(subscription.id)
    #
    # @example Stopping a subscription
    #  client.unsubscribe('my-subscription')
    #
    # @param subscription_id [String] ID of a previously created subscription.
    #
    # @return [void]
    #
    def unsubscribe(subscription_id)
      subscriptions.delete(subscription_id)
      parent_to_child_channel.push([ClientMessageType::CLOSE, subscription_id].to_json)
    end

    # Sends an event to a Relay
    #
    # @api public
    #
    # @example Sending an event to a relay
    #  client.publish(event)
    #
    # @param event [Event] The event to be sent to a Relay
    #
    # @return [void]
    #
    def publish(event)
      parent_to_child_channel.push([ClientMessageType::EVENT, event.to_h].to_json)
    end

    private

    # The logger that prints all the events that the client sends and receives
    #
    # @api private
    #
    # @return [ClientLogger]
    #
    attr_reader :logger

    # The subscriptions that the client has created
    #
    # @api private
    #
    # @return [Hash{String=>Subscription}>]
    #
    attr_reader :subscriptions

    # The channel that the parent thread uses to send messages to the child thread
    #
    # @api private
    #
    # @return [EventMachine::Channel]
    #
    attr_reader :parent_to_child_channel

    # The channel that the child thread uses to send messages to the parent thread
    #
    # @api private
    #
    # @return [EventMachine::Channel]
    #
    attr_reader :child_to_parent_channel

    # Executes a block of code within the EventMachine thread
    #
    # @api private
    #
    # @return [Thread]
    #
    def execute_within_an_em_thread(&block)
      Thread.new { EventMachine.run(block) }
    end

    # Creates the communication channels between threads
    #
    # @api private
    #
    # @return [void]
    #
    def initialize_channels
      @parent_to_child_channel = EventMachine::Channel.new
      @child_to_parent_channel = EventMachine::Channel.new

      child_to_parent_channel.subscribe do |msg|
        emit :connect, msg[:relay]              if msg[:type] == :open
        emit :message, msg[:data]               if msg[:type] == :message
        emit :error,   msg[:message]            if msg[:type] == :error
        emit :close,   msg[:code], msg[:reason] if msg[:type] == :close
      end
    end

    # Builds a websocket client
    #
    # @api private
    #
    # @return [Faye::WebSocket::Client]
    #
    def build_websocket_client(relay_url)
      Faye::WebSocket::Client.new(relay_url, [], { tls: { verify_peer: false } })
    end
  end
end