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_textonBaseare stubs thatraise 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>.logfrom insidestart. Long-polling bugs are hard to spot any other way. channel_verifyonly checks whether the adapter loads — it does not actually connect to the service. Use your ownvalidate_configto 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 aprivateblock.