// Shared plumbing used by every platform adapter.
//
// Keeps the per-platform files small: header construction, `SendResult`
// mapping, config accessors and the chunked-send bookkeeping that all
// adapters share (Hermes `gateway/platforms/base.py` equivalents).
///|
/// `Content-Type: application/json` header map.
pub fn json_headers() -> Map[String, String] {
let m : Map[String, String] = Map([])
m.set("Content-Type", "application/json")
m
}
///|
/// JSON headers plus `Authorization: Bearer `.
pub fn bearer_headers(token : String) -> Map[String, String] {
let m = json_headers()
m.set("Authorization", "Bearer \{token}")
m
}
///|
/// JSON headers plus `Authorization: Bot ` (Discord style).
pub fn bot_headers(token : String) -> Map[String, String] {
let m = json_headers()
m.set("Authorization", "Bot \{token}")
m
}
///|
/// Human-readable message for an `ApiError` (negative code = transport error).
pub fn api_error_message(e : ApiError) -> String {
if e.code < 0 {
e.body
} else {
"HTTP \{e.code}: \{e.body}"
}
}
///|
/// Map a failed HTTP exchange onto a classified `SendResult`.
pub fn send_failure(prefix : String, e : ApiError) -> SendResult {
SendResult::failed(
"\{prefix}: \{api_error_message(e)}",
retryable=e.is_transient(),
kind=classify_send_error(e.body),
)
}
///|
/// Build a failure from a platform-level error string (HTTP 200 + error body).
pub fn send_body_failure(prefix : String, body : String) -> SendResult {
SendResult::failed("\{prefix}: \{body}", kind=classify_send_error(body))
}
///|
/// `SendResult` for a missing credential.
pub fn missing_config(platform : Platform, what : String) -> SendResult {
SendResult::failed("\{platform.value()}: missing \{what}", kind=Forbidden)
}
///|
/// Non-empty `token`, if configured.
pub fn PlatformConfig::token_of(self : PlatformConfig) -> String? {
match self.token {
Some(t) => if t.is_empty() { None } else { Some(t) }
None => None
}
}
///|
/// Non-empty `api_key`, if configured.
pub fn PlatformConfig::api_key_of(self : PlatformConfig) -> String? {
match self.api_key {
Some(k) => if k.is_empty() { None } else { Some(k) }
None => None
}
}
///|
/// Read a string from `[platform.extra]` (or the legacy flat section).
pub fn PlatformConfig::extra_str(
self : PlatformConfig,
key : String,
default : String,
) -> String {
self.extra.get(key).unwrap_or(default)
}
///|
/// Read an integer from `[platform.extra]`.
pub fn PlatformConfig::extra_int(
self : PlatformConfig,
key : String,
default : Int32,
) -> Int32 {
match self.extra.get(key) {
Some(s) => @string.parse_int(s) catch { _ => default }
None => default
}
}
///|
/// Read a boolean from `[platform.extra]` (`true`/`1`/`yes`).
pub fn PlatformConfig::extra_bool(
self : PlatformConfig,
key : String,
default : Bool,
) -> Bool {
match self.extra.get(key) {
Some(s) =>
match s.to_lower() {
"true" | "1" | "yes" | "on" => true
"false" | "0" | "no" | "off" => false
_ => default
}
None => default
}
}
///|
/// First non-empty value among ``keys`` in `[platform.extra]`.
pub fn PlatformConfig::extra_first(
self : PlatformConfig,
keys : Array[String],
) -> String? {
for k in keys {
match self.extra.get(k) {
Some(v) => if !v.is_empty() { return Some(v) }
None => ()
}
}
None
}
///|
/// Pull a nested object out of `result` (e.g. Telegram `{"ok":true,"result":{…}}`).
pub fn result_object(
res : Result[Json, ApiError],
prefix : String,
) -> Result[Json, SendResult] {
match res {
Err(e) => Err(send_failure(prefix, e))
Ok(j) => Ok(j)
}
}
///|
/// Success result carrying an optional platform message id and raw body.
pub fn send_ok(message_id : String?, raw : Json) -> SendResult {
SendResult::ok(message_id?, raw=raw.stringify())
}
///|
/// `true` when ``s`` looks like a WhatsApp/Graph error payload.
pub fn graph_error_message(j : Json) -> String? {
match json_field_obj(j, "error") {
Some(e) =>
match json_field_string(e, "message") {
Some(m) => Some(m)
None => Some(e.stringify())
}
None => None
}
}
///|
/// Deliver ``text`` in platform-sized chunks, calling ``send_one`` per chunk
/// and stopping at the first failure. A successful single-chunk send returns
/// the chunk's `SendResult`; multi-chunk sends return the last success with
/// the first chunk's id preserved.
pub async fn send_chunks(
text : String,
max_len : Int32,
send_one : async (String) -> SendResult,
) -> SendResult {
let chunks = chunk_text(text, max_len)
let mut last = SendResult::ok()
let mut first_id : String? = None
for c in chunks {
let r = send_one(c)
if !r.success {
return r
}
if first_id is None {
first_id = r.message_id
}
last = r
}
{ ..last, message_id: first_id, }
}
///|
/// Retry a send up to ``max_attempts`` times while the failure is transient
/// (Hermes `_send_with_retry`). Honours `retry_after` seconds when the
/// platform supplied one (e.g. Telegram 429 `parameters.retry_after`).
pub async fn send_with_retry(
send_one : async () -> SendResult,
max_attempts~ : Int32 = 3,
) -> SendResult {
let mut attempt = 1
let mut r = SendResult::ok()
while attempt <= max_attempts {
r = send_one()
if r.success || !r.retryable {
return r
}
let wait_ms = match r.retry_after {
Some(s) => (s * 1000.0).to_int()
None => 500 * attempt
}
@async.sleep(wait_ms)
attempt = attempt + 1
}
r
}