Skip to content

Linear-integration#11

Closed
SeanJA239 wants to merge 12 commits into
mainfrom
linear
Closed

Linear-integration#11
SeanJA239 wants to merge 12 commits into
mainfrom
linear

Conversation

@SeanJA239

Copy link
Copy Markdown
Member

Later updates for linear integration will be updated here.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands the webhook/notification pipeline beyond Linear by adding GitHub webhook ingestion and X/Twitter link previews, along with related Lark sink updates, debounce improvements, and deployment/CI configuration updates.

Changes:

  • Add a GitHub webhook source that verifies signatures, normalizes events into the unified Event model, and dispatches to Lark (including optional DMs).
  • Add X/Twitter link preview support in the Lark event handler, including optional decryption for encrypted Lark callback payloads.
  • Add a max-wait cap to debounce behavior (native + Durable Object) and update Cloudflare Workers build/deploy settings.

Reviewed changes

Copilot reviewed 27 out of 29 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
wrangler.toml Updates Worker build command, debounce defaults, migrations, and observability settings.
tests/fixtures/github_pr_opened.json Adds a GitHub webhook fixture payload for testing/dev.
src/utils.rs Adds shared HMAC-SHA256 verification helper.
src/sources/x/mod.rs Introduces X/Twitter client + URL parsing + multi-source fetch fallback.
src/sources/mod.rs Exposes new github and x sources.
src/sources/linear/utils.rs Reuses shared HMAC verifier; improves status change messaging.
src/sources/linear/models.rs Extends UpdatedFrom to support state_id-only payloads.
src/sources/linear/handler.rs Threads debounce delay/max-wait into debounce scheduling + payload.
src/sources/github/utils.rs Adds GitHub signature header parsing and HMAC delegation.
src/sources/github/mod.rs Wires GitHub source module exports.
src/sources/github/handler.rs Implements GitHub webhook handler (native via octocrab, CF via thin structs).
src/sinks/lark/mod.rs Splits Linear vs GitHub webhook notifications; refines DM logic.
src/sinks/lark/event_handler.rs Adds encrypted payload support + routes link previews to X or Linear.
src/sinks/lark/cards.rs Adds GitHub card builders + X preview card builder; refactors shared helpers.
src/sinks/lark/bot.rs Refactors bot API calls into typed request/response structs; adds chat send helper.
src/main.rs Adds /github/webhook route for native server.
src/lib.rs Adds /github/webhook routing for CF Worker entrypoint.
src/event.rs Extends Event enum with GitHub event variants and commit summaries.
src/dispatch.rs Adds dedicated GitHub dispatch path (separate webhook + bot fallback).
src/debounce_do.rs Adds max-wait enforcement in DO debounce + improved webhook/DM sending.
src/debounce.rs Adds max-wait enforcement for in-memory debounce and returns actual delay.
src/config.rs Adds GitHub config, X client, multiple Lark webhook/bot configs, and debounce max-wait config.
invocation-log.json Adds a runtime invocation log artifact.
Cargo.toml Adds deps for octocrab (native), AES-CBC decrypt, base64.
Cargo.lock Locks new dependencies introduced by the PR.
.wrangler/cache/wrangler-account.json Adds Wrangler local cache artifact containing account metadata.
.gitignore Ignores additional local/dev artifacts including .wrangler/.
.github/workflows/deploy.yml Adds GitHub Actions workflow to deploy to Cloudflare Workers on main.
.github/workflows/ci.yml Adjusts clippy invocation in CI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/utils.rs
Comment on lines +11 to +12
let computed = hex::encode(mac.finalize().into_bytes());
computed == expected_hex

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verify_hmac_sha256 compares the computed and expected MAC using normal string equality, which is not constant-time and also ties verification to a specific hex formatting (case, leading zeros). Prefer decoding expected_hex to raw bytes and using the HMAC crate’s constant-time verification (e.g., verify_slice) so signature checks are timing-safe and format-robust.

Suggested change
let computed = hex::encode(mac.finalize().into_bytes());
computed == expected_hex
let Ok(expected_bytes) = hex::decode(expected_hex) else {
return false;
};
let result = mac.finalize();
result.verify_slice(&expected_bytes).is_ok()

Copilot uses AI. Check for mistakes.
Comment thread src/sinks/lark/cards.rs
}));
if !tweet.text.is_empty() {
elements.push(json!({
"tag": "markdown",

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_x_preview_card adds an element with { "tag": "markdown" }, while the rest of the code builds interactive cards using div + lark_md. If Lark interactive cards don’t support a top-level markdown element, this preview card will render incorrectly or be rejected. Consider using the existing md_div(...) helper (or whichever element type Lark expects) for tweet text to keep the card schema consistent.

Suggested change
"tag": "markdown",
"tag": "div",
"lark_md": true,

Copilot uses AI. Check for mistakes.
Comment thread src/sources/x/mod.rs
Comment on lines +171 to +175
async fn fetch_oembed(&self, tweet_url: &str) -> Result<TweetData, String> {
let url = format!(
"https://publish.twitter.com/oembed?url={}&omit_script=true",
tweet_url
);

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch_oembed interpolates tweet_url directly into the url= query parameter without percent-encoding. Real tweet URLs often contain ?/& query strings, which will break the oEmbed request (or change its semantics). Encode tweet_url before building the request URL (e.g., via url::form_urlencoded / percent-encoding) so the fallback works reliably.

Copilot uses AI. Check for mistakes.
Comment thread src/main.rs
Comment on lines +31 to +34
.route(
"/github/webhook",
post(larkstack::sources::github::webhook_handler),
)

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR title/description mention only “Linear integration” updates, but this change set also adds a full GitHub webhook source, X/Twitter link previews, new CI/deploy workflows, and Lark encryption handling. Please update the PR title/description to reflect the actual scope so reviewers/operators can assess impact correctly.

Copilot uses AI. Check for mistakes.
Comment thread src/debounce_do.rs
Comment on lines +119 to 124
if let Some(email) = &dm_email {
let app_id = self.env.var("LARK_APP_ID").ok().map(|v| v.to_string());
let app_secret = self
.env
.secret("LARK_APP_SECRET")
.ok()

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Durable Object alarm path, LARK_APP_ID is read via env.var("LARK_APP_ID"), but LarkConfig::from_worker_env reads LARK_APP_ID via env.secret("LARK_APP_ID"). With the current setup, one side will fail depending on how the Worker env var is configured, and Linear DMs may silently stop working. Align these to use the same mechanism (prefer secret(...) for credentials).

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --all-targets --all-features -- -D warnings
- run: cargo clippy --all-targets -- -D warnings

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI clippy no longer runs with --all-features. Since this repo has feature-gated targets (native vs cf-worker), dropping --all-features can let feature-specific compilation/lint issues slip through (notably for the Cloudflare Worker build). Consider adding a second clippy step for --no-default-features --features cf-worker or re-enabling --all-features.

Suggested change
- run: cargo clippy --all-targets -- -D warnings
- name: Clippy (default features)
run: cargo clippy --all-targets -- -D warnings
- name: Clippy (cf-worker features)
run: cargo clippy --all-targets --no-default-features --features cf-worker -- -D warnings

Copilot uses AI. Check for mistakes.
Comment thread invocation-log.json Outdated
Comment on lines +1 to +6
{
"level": "info",
"message": "POST https://larkstack.arcbox.workers.dev/lark/event",
"$workers": {
"event": {
"request": {

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This invocation log appears to be an environment-generated diagnostic artifact and includes detailed request metadata. It shouldn’t be committed to the repo; please remove it and add it to .gitignore to avoid leaking operational details and noisy diffs in future PRs.

Copilot uses AI. Check for mistakes.
Comment thread .wrangler/cache/wrangler-account.json Outdated
Comment on lines +2 to +5
"account": {
"id": "35fd6722a9c33a03ec19dc5ed74771bc",
"name": "ArcBox Labs"
}

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains a Cloudflare account id/name and is under .wrangler/cache/, which is typically local, auto-generated state. It should not be committed; please remove it from the repo (and ensure .wrangler/ stays ignored, as in the updated .gitignore).

Suggested change
"account": {
"id": "35fd6722a9c33a03ec19dc5ed74771bc",
"name": "ArcBox Labs"
}

Copilot uses AI. Check for mistakes.
- Introduced octocrab models for type-safe GitHub webhook handling
- Replaced json!() macros with proper struct serialization

refactor: introduced octocrab models for Github webhook
…te card

feat: X link preview with fxtwitter, AES decryption, and card improvements

- Add fxtwitter.com as primary tweet source (no auth, rich metrics)
- Add AES-256-CBC decryption for encrypted Lark payloads (LARK_X_ENCRYPT_KEY)
- Support separate verification tokens for Linear and X Lark apps
- Show tweet text, likes, and retweets in X preview card
- Fix metrics condition to show counts even when only one metric is present
- Truncate inline title to 30 chars (better for CJK text)
- Add like_count, retweet_count, reply_count to TweetData
- Enable CF Workers observability in wrangler.toml

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 48 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread src/utils.rs
Comment on lines +6 to +12
pub fn verify_hmac_sha256(secret: &str, body: &[u8], expected_hex: &str) -> bool {
let Ok(mut mac) = Hmac::<Sha256>::new_from_slice(secret.as_bytes()) else {
return false;
};
mac.update(body);
let expected = hex::encode(mac.finalize().into_bytes());
expected == signature
}

// ---------------------------------------------------------------------------
// Priority helpers
// ---------------------------------------------------------------------------

pub fn priority_color(priority: u8) -> &'static str {
match priority {
1 => "red",
2 => "orange",
3 => "yellow",
_ => "blue", // 0 (No priority) and 4 (Low)
}
let computed = hex::encode(mac.finalize().into_bytes());
computed == expected_hex
Comment on lines +213 to +235
// Extract repo name and full_name in one pass (used for whitelist + display).
let (repo, repo_name) = match serde_json::from_slice::<RepoProbe>(&body) {
Ok(probe) => {
let full = probe
.repository
.full_name
.clone()
.unwrap_or_else(|| probe.repository.name.clone());
(full, probe.repository.name)
}
Err(_) => {
warn!("could not extract repository name from payload");
(String::new(), String::new())
}
};

if !github.repo_whitelist.is_empty()
&& !repo_name.is_empty()
&& !github.repo_whitelist.contains(&repo_name)
{
info!("ignoring event from non-whitelisted repo: {repo_name}");
return StatusCode::OK;
}
Comment thread src/debounce_do.rs
Comment on lines +118 to +127
// Send DM via the Linear DM bot (enterprise self-built app).
if let Some(email) = &dm_email {
let app_id = self.env.var("LARK_APP_ID").ok().map(|v| v.to_string());
let app_secret = self
.env
.secret("LARK_APP_SECRET")
.ok()
.map(|s| s.to_string());
if let (Some(id), Some(secret)) = (app_id, app_secret) {
let bot = crate::sinks::lark::LarkBotClient::new(id, secret, http.clone());
Comment thread src/sources/x/mod.rs
Comment on lines +22 to +36
/// Extracts `(username, tweet_id)` from an X or Twitter URL.
///
/// Handles `https://x.com/user/status/1234567890` and
/// `https://twitter.com/user/status/1234567890`.
pub fn extract_tweet_info(url: &str) -> Option<(&str, &str)> {
let parts: Vec<&str> = url.split('/').collect();
for (i, &part) in parts.iter().enumerate() {
if part == "status" {
let id = parts.get(i + 1).copied()?;
let id = id.split('?').next().unwrap_or(id);
let id = id.split('#').next().unwrap_or(id);
if !id.is_empty() && id.chars().all(|c| c.is_ascii_digit()) {
let username = parts.get(i.wrapping_sub(1)).copied().unwrap_or("");
return Some((username, id));
}
Comment on lines +139 to +145
);
}
}

info!("lark event received: {body_value}");

let event_type = body_value
Comment thread docs/configuration.md
| `LINEAR_API_KEY` | Optional | GraphQL API access — enables link previews |
| `LARK_VERIFICATION_TOKEN` | Optional | Lark event callback verification |
| `PORT` | No | Listen port, defaults to `3000` (ignored on CF Workers) |
| `DEBOUNCE_DELAY_MS` | No | Debounce window in ms, defaults to `5000` |
- Replace note/plain_text with md_div for X preview metrics so emoji
  (❤️ 🔁) render correctly in Lark
- Fix repo whitelist check to also reject events when repo name cannot
  be extracted from payload (was previously bypassed on parse failure)

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants