跳转到主要内容
Whoop 不是已发布的 vendor 客户端。 src/health/vendor/device/ 中已确认的设备品牌客户端仅有 Fitbit、Garmin 和 Withings。Whoop 没有第一方 vendor::Vendor —— 它需通过聚合平台(如 Terra、Rook、Spike)接入,或由你自己添加一个客户端。(verify — Whoop 可能只能经聚合器接入。) 因此本页以 Fitbit 作为 OAuth 2.0 示例,因为它是一个真实、已完整实现的设备客户端。

概览

Fitbitsrc/health/vendor/device/fitbit.cpp,id 为 fitbit)是对照 vendor 契约、拥有完整 OAuth 2.0 实现的设备品牌的最佳范例:它实现了 fetchauthorize_urlrevoke 以及带签名的 handle_webhook。可将它与 Garmin 示例对照,后者因 Garmin 的数据 API 需合作方授权而大多为桩。
这是一个 vendor::Vendor。它所实现的接口参见 Vendor 系统

authorize_url —— 构建同意 URL

保密型服务端客户端在 token 步骤用其 secret 认证,因此不使用 PKCEauthorize_url 是纯字符串构建(无网络调用),需要 OAuth client_id
std::string authorize_url(const std::string& redirect_uri, const std::string& state,
                          const std::string& /*user_id*/, const std::string& /*provider*/) override {
    if (config().client_id.empty()) {
        throw VendorError(info_.id + ": authorize_url requires an OAuth client_id — set "
                          "MIROBODY_VENDOR_FITBIT_CLIENT_ID");
    }
    std::string url = std::string(kAuthorizeUrl) +   // https://www.fitbit.com/oauth2/authorize
                      "?response_type=code&client_id=" + url_encode(config().client_id) +
                      "&scope=" + url_encode(kDefaultScope);  // "activity heartrate sleep weight profile"
    if (!redirect_uri.empty()) url += "&redirect_uri=" + url_encode(redirect_uri);
    if (!state.empty())        url += "&state=" + url_encode(state);
    return url;
}
token 交换本身在带外于 /oauth2/token 完成;所得的 access token 存为 config.api_key

fetch —— 按域的时间序列 GET

Fitbit 的 Web API 以整天区间提供数据(yyyy-MM-dd)。fetch 把一个 DataDomain 映射到有文档的端点并返回 Fitbit 的 JSON:
static std::string endpoint(DataDomain d, const std::string& u,
                            const std::string& s, const std::string& e) {
    switch (d) {
        case DataDomain::Activity:    return "/1/user/" + u + "/activities/steps/date/" + s + "/" + e + ".json";
        case DataDomain::HeartRate:   return "/1/user/" + u + "/activities/heart/date/" + s + "/" + e + ".json";
        case DataDomain::Sleep:       return "/1.2/user/" + u + "/sleep/date/" + s + "/" + e + ".json";
        case DataDomain::BodyMetrics: return "/1/user/" + u + "/body/log/weight/date/" + s + "/" + e + ".json";
        default:
            throw VendorError("fitbit: unsupported domain (brokers: activity, heart_rate, sleep, body_metrics)");
    }
}
用户路径段是 Fitbit 用户 id,或 -(该 API 中 token 拥有者的别名)。access token(config.api_key)以 bearer 头携带。

handle_webhook —— 验证签名

Fitbit 用 X-Fitbit-Signature = base64(HMAC-SHA1(raw_body))(以 client secret + "&" 为密钥)对订阅通知签名。客户端在解析前用恒定时间比较验证它:
const std::string key = config().client_secret + "&";
std::array<unsigned char, 20> mac = storage::hmac_sha1(key, body);
const std::string expected = storage::base64_encode(mac.data(), mac.size());
if (!ct_equal(expected, sig)) {
    throw VendorError(info_.id + ": webhook signature verification failed (X-Fitbit-Signature)");
}
由于签名方案是公开有文档的,这能被忠实实现 —— 这正是 Fitbit 的 webhook 为真、而 Garmin 的为桩的原因。

revoke —— 撤销授权

revoke 把 token POST 到 /oauth2/revoke,以保密客户端身份认证(HTTP Basic client_id:client_secret)。

配置

MIROBODY_VENDOR_FITBIT_CLIENT_ID=your_client_id
MIROBODY_VENDOR_FITBIT_CLIENT_SECRET=your_client_secret
# after the OAuth token exchange, the access token is used as the API key:
MIROBODY_VENDOR_FITBIT_API_KEY=fitbit_oauth2_access_token
# optional host override (default https://api.fitbit.com)
MIROBODY_VENDOR_FITBIT_BASE_URL=https://api.fitbit.com

如果你确实需要 Whoop

两条诚实的路径:
  1. 经聚合器 —— 通过 Terra / Rook / Spike(哪个在其 provider 目录中列出了 Whoop 就用哪个)连接 Whoop,并经该聚合器的 vendor::Vendor 获取。(verify the aggregator’s current Whoop support.)
  2. 编写一个第一方客户端 —— 添加 src/health/vendor/device/whoop.cpp,对照 Whoop 的公开 OAuth 2.0 API 实现契约并注册它。参照 Provider 集成;上面的 Fitbit 是最接近的模板。

下一步

Garmin 示例

一个大多为桩的设备客户端及其原因

Provider 集成

构建一个新的 vendor::Vendor

OAuth 实现

各 vendor 客户端中的 OAuth 模式

Provider 概览

每一个已发布的 vendor