Skip to content

X integration#10

Closed
SeanJA239 wants to merge 11 commits into
mainfrom
x-integration
Closed

X integration#10
SeanJA239 wants to merge 11 commits into
mainfrom
x-integration

Conversation

@SeanJA239

Copy link
Copy Markdown
Member

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

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 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"

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 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;

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.

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",

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.

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"]

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.

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",

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.

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

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 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
);

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.

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));
}

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.

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

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 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());

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 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

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 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>
@AprilNEA AprilNEA closed this Jun 14, 2026
@AprilNEA AprilNEA deleted the x-integration branch June 14, 2026 12:09
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