Custom Channel Adapter

OpenClacky's IM integrations (Feishu / WeCom / Discord, etc.) use a self-registering adapter architecture. You can plug in your own channels (Slack, in-house IM, email, SMS, …) without touching the gem source. User adapters live in ~/.clacky/channels/ and survive gem upgrades. A broken adapter is automatically isolated at startup so it can never bring down the main process.


How it works

At startup OpenClacky scans ~/.clacky/channels/<name>/adapter.rb and requires each one. Each file should define a class that inherits from Clacky::Channel::Adapters::Base and self-registers via Adapters.register at the bottom of the file. Once registered, the built-in channel router can dispatch to it just like it would to a built-in adapter.

Broken adapters are skipped (never abort startup); channel_verify shows the reason.

Directory layout

~/.clacky/channels/
└── slack/
    └── adapter.rb

The directory name is just for organization — the real platform identifier comes from platform_id in the adapter class.

Required interface

Inherit Clacky::Channel::Adapters::Base. The following 5 methods must be implemented (otherwise the loader will treat the adapter as "not really implemented" and skip it):

Method Type Purpose
self.platform_id class Returns a Symbol like :slack. The whole system uses it to locate the adapter.
self.platform_config(raw) class Maps the raw Hash from ChannelConfig to a symbol-keyed runtime config.
#start(&on_message) instance Starts listening and blocks; yields one standardized event per inbound message.
#stop instance Stops listening and releases resources.
#send_text(chat_id, text, reply_to: nil) instance Sends text/Markdown to a chat. Returns { message_id: String }.

Optional (override to enhance):

Method Default Purpose
#update_message(chat_id, message_id, text) false Edit a sent message in place (for streaming progress).
#supports_message_updates? false Whether the platform supports message edits.
#validate_config(config) [] Returns an array of error strings; empty means valid.

Note: start / stop / send_text on Base are stubs that raise NotImplementedError. If your subclass does not actually override them, the loader detects the missing implementation and skips it — it cannot silently "pretend to implement".

CLI workflow

1. Scaffold

clacky channel_new slack

Creates ~/.clacky/channels/slack/adapter.rb with the inheritance, self-registration, and TODO stubs for every required method.

2. Fill in the TODOs

Open adapter.rb and replace the # TODO markers with real logic. Do not remove the self-registration line at the bottom:

Clacky::Channel::Adapters.register(:slack, SlackAdapter)

3. Verify loading

clacky channel_verify

Sample output:

[OK]   slack
[SKIP] broken-one — unimplemented methods: start, send_text

[SKIP] adapters are not registered but never affect the others. The command exits non-zero on any SKIP — handy for CI.

Minimal example

require "clacky"

module ClackyChannels
  class SlackAdapter < Clacky::Channel::Adapters::Base
    def self.platform_id
      :slack
    end

    def self.platform_config(raw)
      {
        bot_token: raw[:bot_token],
        signing_secret: raw[:signing_secret]
      }
    end

    def initialize(config)
      @config = config
      @running = false
    end

    def start(&on_message)
      @running = true
      while @running
        # replace with a real long-poll / socket loop
        sleep 1
      end
    end

    def stop
      @running = false
    end

    def send_text(chat_id, text, reply_to: nil)
      # call Slack API; demo only
      { message_id: "demo-#{Time.now.to_i}" }
    end

    def validate_config(config)
      errors = []
      errors << "bot_token is required" if config[:bot_token].to_s.empty?
      errors
    end
  end
end

Clacky::Channel::Adapters.register(:slack, ClackyChannels::SlackAdapter)

Inbound event format

Events yielded from start should follow the convention used by built-in adapters (see Feishu / WeCom): include at minimum chat_id, message_id, user_id, text, platform, so upper-layer routing can handle them uniformly across platforms.

Debugging tips

  • Log to ~/.clacky/logs/<name>.log from inside start. Long-polling bugs are hard to spot any other way.
  • channel_verify only checks whether the adapter loads — it does not actually connect to the service. Use your own validate_config to check config values before launch.
  • If you keep seeing unimplemented methods: ..., double-check method names, instance vs class, and that the methods are not under a private block.