Class: Basecamp::Webhooks::Receiver

Inherits:
Object
  • Object
show all
Defined in:
lib/basecamp/webhooks/receiver.rb

Overview

Receives and routes webhook events from Basecamp. Framework-agnostic: works with raw body strings and a header accessor.

Constant Summary collapse

DEFAULT_SIGNATURE_HEADER =
"X-Basecamp-Signature"
DEFAULT_DEDUP_WINDOW_SIZE =
1000

Instance Method Summary collapse

Constructor Details

#initialize(secret: nil, signature_header: DEFAULT_SIGNATURE_HEADER, dedup_window_size: DEFAULT_DEDUP_WINDOW_SIZE) ⇒ Receiver

Returns a new instance of Receiver.



13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/basecamp/webhooks/receiver.rb', line 13

def initialize(secret: nil, signature_header: DEFAULT_SIGNATURE_HEADER, dedup_window_size: DEFAULT_DEDUP_WINDOW_SIZE)
  @secret = secret
  @signature_header = signature_header
  @dedup_window_size = dedup_window_size
  @handlers = {}
  @any_handlers = []
  @middleware = []
  @dedup_seen = {}
  @dedup_pending = {}
  @dedup_order = []
  @mutex = Mutex.new
end

Instance Method Details

#handle_request(raw_body:, headers:) ⇒ Object

Process a raw webhook request. Returns the parsed Event. Raises VerificationError if signature is invalid.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/basecamp/webhooks/receiver.rb', line 50

def handle_request(raw_body:, headers:)
  # Verify signature
  if @secret && !@secret.empty?
    signature = extract_header(headers, @signature_header)
    unless Verify.valid?(payload: raw_body, signature: signature, secret: @secret)
      raise VerificationError, "invalid webhook signature"
    end
  end

  # Parse event
  hash = JSON.parse(raw_body)
  event = Event.new(hash)

  # Atomic dedup: claim before handlers, commit on success, release on error
  return event unless claim(event.id)

  begin
    # Build middleware chain
    run_handlers = -> { dispatch_handlers(event) }
    chain = @middleware.reverse.reduce(run_handlers) do |next_fn, mw|
      -> { mw.call(event, next_fn) }
    end

    chain.call

    # Promote from pending to seen on success
    commit_seen(event.id)
  rescue => e
    # Release claim so retries can re-attempt
    release_claim(event.id)
    raise e
  end

  event
end

#on(pattern, &handler) ⇒ Object

Register a handler for a specific event kind pattern. Supports glob patterns: "todo_*" matches "todo_created", etc.



28
29
30
31
32
# File 'lib/basecamp/webhooks/receiver.rb', line 28

def on(pattern, &handler)
  @handlers[pattern] ||= []
  @handlers[pattern] << handler
  self
end

#on_any(&handler) ⇒ Object

Register a handler for all events.



35
36
37
38
# File 'lib/basecamp/webhooks/receiver.rb', line 35

def on_any(&handler)
  @any_handlers << handler
  self
end

#use(&middleware) ⇒ Object

Add middleware to the processing chain. Middleware receives (event, next_proc) and must call next_proc.call to continue.



42
43
44
45
# File 'lib/basecamp/webhooks/receiver.rb', line 42

def use(&middleware)
  @middleware << middleware
  self
end