Skip to content

Commit f1ae8be

Browse files
committed
Rough pass at scheduled types team update dms
1 parent b3af9c1 commit f1ae8be

File tree

4 files changed

+142
-5
lines changed

4 files changed

+142
-5
lines changed

src/github.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,21 @@ impl Issue {
492492
.await?)
493493
}
494494

495+
// returns an array of one element
496+
pub async fn get_first100_comments(
497+
&self,
498+
client: &GithubClient,
499+
) -> anyhow::Result<Vec<Comment>> {
500+
let comment_url = format!(
501+
"{}/issues/{}/comments?page=1&per_page=100",
502+
self.repository().url(),
503+
self.number,
504+
);
505+
Ok(client
506+
.json::<Vec<Comment>>(client.get(&comment_url))
507+
.await?)
508+
}
509+
495510
pub async fn edit_body(&self, client: &GithubClient, body: &str) -> anyhow::Result<()> {
496511
let edit_url = format!("{}/issues/{}", self.repository().url(), self.number);
497512
#[derive(serde::Serialize)]

src/handlers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ mod review_submitted;
4545
mod rfc_helper;
4646
pub mod rustc_commits;
4747
mod shortcut;
48+
mod types_planning_updates;
4849

4950
pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
5051
let config = config::get(&ctx.github, event.repo()).await;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use crate::github;
2+
use crate::jobs::Job;
3+
use crate::zulip::BOT_EMAIL;
4+
use crate::zulip::{to_zulip_id, MembersApiResponse};
5+
use anyhow::{format_err, Context as _};
6+
use async_trait::async_trait;
7+
use chrono::{Duration, Utc};
8+
9+
pub struct TypesPlanningUpdatesJob;
10+
11+
#[async_trait]
12+
impl Job for TypesPlanningUpdatesJob {
13+
fn name(&self) -> &'static str {
14+
"types_planning_updates"
15+
}
16+
17+
async fn run(&self, ctx: &super::Context, _metadata: &serde_json::Value) -> anyhow::Result<()> {
18+
request_updates(ctx).await?;
19+
Ok(())
20+
}
21+
}
22+
23+
const TYPES_REPO: &'static str = "rust-lang/types-team";
24+
25+
pub async fn request_updates(ctx: &super::Context) -> anyhow::Result<()> {
26+
let gh = &ctx.github;
27+
let types_repo = gh.repository(TYPES_REPO).await?;
28+
29+
let tracking_issues_query = github::Query {
30+
filters: vec![("state", "open"), ("is", "issue")],
31+
include_labels: vec!["roadmap-tracking-issue"],
32+
exclude_labels: vec![],
33+
};
34+
let issues = types_repo
35+
.get_issues(&gh, &tracking_issues_query)
36+
.await
37+
.with_context(|| "Unable to get issues.")?;
38+
39+
for issue in issues {
40+
let comments = issue.get_first100_comments(gh).await?;
41+
if comments.len() >= 100 {
42+
anyhow::bail!(
43+
"Encountered types tracking issue with 100 or more comments; needs implementation."
44+
);
45+
}
46+
let older_than_28_days = comments
47+
.last()
48+
.map_or(true, |c| c.updated_at < (Utc::now() - Duration::days(28)));
49+
if !older_than_28_days {
50+
continue;
51+
}
52+
let mut dmed_assignee = false;
53+
for assignee in issue.assignees {
54+
let zulip_id_and_email = zulip_id_and_email(ctx, assignee.id.unwrap()).await?;
55+
let (zulip_id, email) = match zulip_id_and_email {
56+
Some(id) => id,
57+
None => continue,
58+
};
59+
let message = format!(
60+
"Type team tracking issue needs an update. [Issue #{}]({})",
61+
issue.number, issue.html_url
62+
);
63+
let zulip_req = crate::zulip::MessageApiRequest {
64+
recipient: crate::zulip::Recipient::Private {
65+
id: zulip_id,
66+
email: &email,
67+
},
68+
content: &message,
69+
};
70+
zulip_req.send(&ctx.github.raw()).await?;
71+
dmed_assignee = true;
72+
}
73+
if !dmed_assignee {
74+
let message = format!(
75+
"Type team tracking issue needs an update, and was unable to reach an assignee. \
76+
[Issue #{}]({})",
77+
issue.number, issue.html_url
78+
);
79+
let zulip_req = crate::zulip::MessageApiRequest {
80+
recipient: crate::zulip::Recipient::Stream {
81+
id: 144729,
82+
topic: "tracking issue updates",
83+
},
84+
content: &message,
85+
};
86+
zulip_req.send(&ctx.github.raw()).await?;
87+
}
88+
}
89+
90+
Ok(())
91+
}
92+
93+
async fn zulip_id_and_email(
94+
ctx: &super::Context,
95+
github_id: i64,
96+
) -> anyhow::Result<Option<(u64, String)>> {
97+
let bot_api_token = std::env::var("ZULIP_API_TOKEN").expect("ZULIP_API_TOKEN");
98+
99+
let members = ctx
100+
.github
101+
.raw()
102+
.get("https://rust-lang.zulipchat.com/api/v1/users")
103+
.basic_auth(BOT_EMAIL, Some(&bot_api_token))
104+
.send()
105+
.await
106+
.map_err(|e| format_err!("Failed to get list of zulip users: {e:?}."))?;
107+
let members = members
108+
.json::<MembersApiResponse>()
109+
.await
110+
.map_err(|e| format_err!("Failed to get list of zulip users: {e:?}."))?;
111+
112+
let zulip_id = match to_zulip_id(&ctx.github, github_id).await {
113+
Ok(Some(id)) => id as u64,
114+
Ok(None) => return Ok(None),
115+
Err(e) => anyhow::bail!("Could not find Zulip ID for GitHub id {github_id}: {e:?}"),
116+
};
117+
118+
let user = members.members.iter().find(|m| m.user_id == zulip_id);
119+
120+
Ok(user.map(|m| (m.user_id, m.email.clone())))
121+
}

src/zulip.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -299,14 +299,14 @@ async fn execute_for_other_user(
299299
}
300300

301301
#[derive(serde::Deserialize)]
302-
struct MembersApiResponse {
303-
members: Vec<Member>,
302+
pub struct MembersApiResponse {
303+
pub members: Vec<Member>,
304304
}
305305

306306
#[derive(serde::Deserialize)]
307-
struct Member {
308-
email: String,
309-
user_id: u64,
307+
pub struct Member {
308+
pub email: String,
309+
pub user_id: u64,
310310
}
311311

312312
#[derive(serde::Serialize)]

0 commit comments

Comments
 (0)