Skip to content

X integration#10

Open
SeanJA239 wants to merge 22 commits intomainfrom
x-integration
Open

X integration#10
SeanJA239 wants to merge 22 commits intomainfrom
x-integration

Conversation

@SeanJA239
Copy link
Copy Markdown
Collaborator

Integrated X/twitter link preview function
test with wrangler dev passed with status 200

AprilNEA and others added 11 commits February 26, 2026 17:18
feat: debounce every 500ms
fix debounce and env var config
show update details & comments in cards
Rename package to larkstack, bump edition from 2021 to 2024,
add authors/license fields, and split LICENSE into MIT + Apache-2.0.
Split single serial job into parallel fmt/clippy/test jobs,
add concurrency control to cancel stale runs, and skip CI
on non-Rust file changes.
Introduce a three-layer architecture (sources/ → Event → sinks/) so
that adding a new source or sink requires no changes to the other layer.

- Add `event.rs` with `Priority` enum and `Event` enum as the middle layer
- Add `dispatch.rs` to route events to all registered sinks
- Move Linear webhook models, client, and handler into `sources/linear/`
- Move Lark card builders, bot client, and webhook into `sinks/lark/`
- Generalize `debounce.rs` to hold `Event` instead of `Issue`
- Rewrite all comments to follow rustdoc conventions (//! and ///)
- Add `.claude/skills/rust-doc-comments` for reusable doc audit
Replace scattered env::var() calls with declarative LinearConfig,
LarkConfig, and ServerConfig structs loaded via figment's Env provider.
- Strip emoji headings and promotional language
- Cut significance inflation and copula avoidance
- Simplify boldface inline-header lists
- Tone down copy to be factual and direct
Introduce a `cf-worker` feature flag that compiles the same codebase as a
Cloudflare Worker (wasm32-unknown-unknown cdylib). Debounce semantics are
preserved via a Durable Object with alarms replacing the in-memory
DebounceMap + tokio::spawn pattern.

Key changes:
- Restructure into lib + bin dual target
- Cfg-gate config, debounce, handler, and bot modules
- Decouple sinks from AppState for reuse in DO alarm handler
- Add DebounceObject Durable Object implementation
- Manual request dispatch in CF entry to bypass axum Handler Send bounds
- Guard against simultaneous native + cf-worker feature activation
- Add deployment documentation for Cloudflare Workers
Move environment variable reference to docs/configuration.md and Railway
deployment steps to docs/deploy-railway.md. READMEs now serve as a concise
overview with links to the detailed guides. Updated badges and descriptions
to reflect Cloudflare Workers support.
Copy link
Copy Markdown

Copilot AI left a comment

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 service beyond Linear-only handling by adding X/Twitter link preview support in the Lark event handler, and introducing a new GitHub webhook source with corresponding Lark notifications and deployment/CI adjustments.

Changes:

  • Add X/Twitter URL unfurling in /lark/event (including optional encrypted payload handling) and an X client for fetching tweet data.
  • Add a GitHub webhook receiver (/github/webhook) that normalizes several GitHub event types into new Event variants and dispatches them to Lark (group + optional DM).
  • Update Cloudflare Worker build/deploy configuration (wrangler + GitHub Actions workflows) and refactor Lark sink/dispatch plumbing.

Reviewed changes

Copilot reviewed 25 out of 27 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
wrangler.toml Adjusts worker build flags, DO migration config, and enables observability.
src/utils.rs Adds shared HMAC-SHA256 verification helper.
src/sources/x/mod.rs Introduces X/Twitter fetching + URL parsing helpers.
src/sources/mod.rs Exposes new github and x source modules.
src/sources/linear/utils.rs Reuses shared HMAC verifier and improves status change rendering when only UUID is provided.
src/sources/linear/models.rs Adds state_id support to UpdatedFrom.
src/sources/github/* Adds GitHub webhook handler + signature verification utilities.
src/sinks/lark/* Adds GitHub notification rendering, X preview card rendering, and refactors webhook/DM sending paths.
src/main.rs / src/lib.rs Adds the /github/webhook route for native and Worker entrypoints.
src/event.rs Adds GitHub-focused event variants and CommitSummary.
src/dispatch.rs Splits dispatch into Linear vs GitHub group/DM routing.
src/debounce_do.rs Switches DO delivery to direct webhook usage and improves config handling.
src/config.rs Adds GitHub config + separate bot/webhook configuration for GitHub vs Linear; wires in XClient.
.github/workflows/deploy.yml Adds a Cloudflare Workers deploy workflow.
.github/workflows/ci.yml Changes clippy invocation (no longer --all-features).
tests/fixtures/github_pr_opened.json Adds a GitHub webhook payload fixture.
invocation-log.json Adds a runtime invocation log artifact.
.wrangler/cache/wrangler-account.json Adds local Wrangler cache artifact (account metadata).
.gitignore Ignores .wrangler/, .dev.vars, and other local artifacts.
Cargo.toml / Cargo.lock Adds dependencies (octocrab, AES/CBC/base64) and updates lockfile accordingly.

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

Comment thread .wrangler/cache/wrangler-account.json Outdated
Comment on lines +3 to +4
"id": "35fd6722a9c33a03ec19dc5ed74771bc",
"name": "ArcBox Labs"
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

This file appears to be local Wrangler cache data (includes a real Cloudflare account id/name). It shouldn’t be committed to the repo; remove it from git history and rely on .gitignore (already ignoring .wrangler/).

Suggested change
"id": "35fd6722a9c33a03ec19dc5ed74771bc",
"name": "ArcBox Labs"
"id": "YOUR_CLOUDFLARE_ACCOUNT_ID",
"name": "YOUR_CLOUDFLARE_ACCOUNT_NAME"

Copilot uses AI. Check for mistakes.
Comment thread src/sources/mod.rs
Comment on lines +3 to +5
pub mod github;
pub mod linear;
pub mod x;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

The PR title/description indicate only X/Twitter link preview integration, but this change set also adds a full GitHub webhook source, new GitHub/Lark notification paths, and CI/deploy workflow changes. Please update the PR description/title to reflect the broader scope (or split into separate PRs) so reviewers can assess risk appropriately.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +20
{
"action": "opened",
"number": 2,
"pull_request": {
"url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2",
"id": 279147437,
"node_id": "MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3",
"html_url": "https://github.com/Codertocat/Hello-World/pull/2",
"diff_url": "https://github.com/Codertocat/Hello-World/pull/2.diff",
"patch_url": "https://github.com/Codertocat/Hello-World/pull/2.patch",
"issue_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2",
"number": 2,
"state": "open",
"locked": false,
"title": "Update the README with new information.",
"user": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

tests/fixtures/github_pr_opened.json is added but not referenced by any tests or tooling in the repo, so it’s currently dead weight. Either add a test that uses this fixture (e.g., webhook handler parsing) or drop the fixture from this PR.

Copilot uses AI. Check for mistakes.
Comment thread wrangler.toml Outdated
[[migrations]]
tag = "v1"
new_classes = ["DebounceObject"]
new_sqlite_classes = ["DebounceObject"]
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

new_sqlite_classes is for Durable Objects using SQLite-backed storage. DebounceObject currently uses the default KV storage API (state.storage().get/put/set_alarm), so this migration flag is likely incorrect and can break deployments/migrations. Use new_classes instead unless the DO was intentionally migrated to SQLite and the implementation updated accordingly.

Suggested change
new_sqlite_classes = ["DebounceObject"]
new_classes = ["DebounceObject"]

Copilot uses AI. Check for mistakes.
Comment thread invocation-log.json Outdated
Comment on lines +1 to +20
{
"level": "info",
"message": "POST https://larkstack.arcbox.workers.dev/lark/event",
"$workers": {
"event": {
"request": {
"cf": {
"requestHeaderNames": {},
"isEUCountry": false,
"httpProtocol": "HTTP/1.1",
"clientAcceptEncoding": "gzip",
"requestPriority": "",
"colo": "IAD",
"asOrganization": "Amazon Technologies Inc.",
"country": "US",
"city": "Ashburn",
"continent": "NA",
"region": "Virginia",
"regionCode": "VA",
"timezone": "America/New_York",
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

invocation-log.json looks like a local runtime log / debug artifact and includes request metadata (IPs, headers, etc.). It shouldn’t be committed; please remove it from the PR and add it to .gitignore if it can be regenerated locally.

Copilot uses AI. Check for mistakes.
Comment thread src/utils.rs Outdated
Comment on lines +11 to +12
let computed = hex::encode(mac.finalize().into_bytes());
computed == expected_hex
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

verify_hmac_sha256 compares hex strings with ==, which is not constant-time and is sensitive to hex casing/formatting differences. Prefer decoding expected_hex to bytes and using mac.verify_slice(...) (or a constant-time byte comparison) to avoid timing leaks and make signature verification robust.

Suggested change
let computed = hex::encode(mac.finalize().into_bytes());
computed == expected_hex
let expected_bytes = match hex::decode(expected_hex) {
Ok(bytes) => bytes,
Err(_) => return false,
};
mac.verify_slice(&expected_bytes).is_ok()

Copilot uses AI. Check for mistakes.
Comment thread src/sources/x/mod.rs
Comment on lines +172 to +175
let url = format!(
"https://publish.twitter.com/oembed?url={}&omit_script=true",
tweet_url
);
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

The oEmbed request URL is built by interpolating tweet_url directly into a query parameter. If tweet_url contains ?, &, #, or non-ASCII characters, this can produce an invalid request or change the query semantics. Percent-encode the tweet_url parameter (or build the URL with a proper query serializer).

Copilot uses AI. Check for mistakes.
Comment thread src/sources/x/mod.rs
Comment on lines +26 to +36
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));
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

extract_tweet_info will treat URLs like https://x.com/i/web/status/<id> as having username "web", causing an unnecessary fxtwitter request that is guaranteed to fail before falling back. Consider parsing with url and only attempting the fxtwitter path when the URL matches /<username>/status/<id> (and explicitly excluding the i/web/status form).

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

Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

CI no longer runs clippy with --all-features, which means the cf-worker build (and its cfg-gated code paths) won’t be typechecked/linted in PRs. Consider adding a dedicated job that runs cargo check/clippy --no-default-features --features cf-worker --target wasm32-unknown-unknown so Worker-only code stays verified.

Suggested change
clippy-cf-worker:
name: Clippy (cf-worker)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --no-default-features --features cf-worker --target wasm32-unknown-unknown -- -D warnings

Copilot uses AI. Check for mistakes.
Comment thread src/debounce_do.rs Outdated
if let Some(ref email) = dm_email {
// 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());
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

In the Worker Durable Object alarm, LARK_APP_ID is read via env.var("LARK_APP_ID"), but LarkConfig::from_worker_env now treats LARK_APP_ID as a secret (env.secret("LARK_APP_ID")). This mismatch means DM sending from the DO will fail unless the value is duplicated as both a var and a secret. Read LARK_APP_ID via env.secret(...) here for consistency.

Suggested change
let app_id = self.env.var("LARK_APP_ID").ok().map(|v| v.to_string());
let app_id = self
.env
.secret("LARK_APP_ID")
.ok()
.map(|s| s.to_string());

Copilot uses AI. Check for mistakes.
- X: fxtwitter integration, AES-256-CBC decryption
- X: tweet metrics display (likes, retweets, replies)
- Linear: status transitions and description preview in cards
- Shared: truncate inline title to 30 chars for CJK
Copy link
Copy Markdown

Copilot AI left a comment

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 8 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 Outdated
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 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 on lines +113 to +149
let incoming_token = body_value
.get("header")
.and_then(|h| h.get("token"))
.and_then(|v| v.as_str())
.unwrap_or("");

let token_required =
state.lark.verification_token.is_some() || state.lark.x_verification_token.is_some();

if token_required {
let valid = state
.lark
.verification_token
.as_deref()
.is_some_and(|t| t == incoming_token)
|| state
.lark
.x_verification_token
.as_deref()
.is_some_and(|t| t == incoming_token);

if !valid {
warn!("lark event token mismatch");
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"error": "invalid token"})),
);
}
}

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

let event_type = body_value
.get("header")
.and_then(|h| h.get("event_type"))
.and_then(|v| v.as_str())
.unwrap_or("");
Comment thread docs/configuration.md Outdated
| `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` |
Comment thread README.md
## ⚙️ Environment Variables
| Method | Path | Purpose |
| :--- | :--- | :--- |
| `POST` | `/webhook` | Linear webhook receiver |
Comment thread README_zh.md

| Method | Path | 用途 |
| :--- | :--- | :--- |
| `POST` | `/webhook` | 接收 Linear Webhook |
Comment thread .github/workflows/ci.yml
Comment on lines +29 to +47
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --all-targets -- -D warnings

test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test
Comment thread .pre-commit-config.yaml
description: Run Linter to check code style and potential issues
entry: cargo clippy --all-targets -- -D warnings
language: system
types: [rust]
- Use constant-time HMAC comparison via mac.verify_slice() to prevent timing attacks
- Read LARK_APP_ID via env.secret() in DO alarm to fix broken DM path
- Change wrangler.toml migration from new_sqlite_classes to new_classes
- Redact sensitive event body from logs, only log event_type
- Correct DEBOUNCE_DELAY_MS default in docs (5000 → 30000)
- Add POST /github/webhook to endpoint tables in README and README_zh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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