1use crate::db::{self, ChannelRole, NewUserParams};
2
3use anyhow::Context as _;
4use chrono::{DateTime, Utc};
5use db::Database;
6use serde::{Deserialize, de::DeserializeOwned};
7use std::{fs, path::Path};
8
9use crate::Config;
10
11/// A GitHub user.
12///
13/// This representation corresponds to the entries in the `seed/github_users.json` file.
14#[derive(Debug, Deserialize)]
15struct GithubUser {
16 id: i32,
17 login: String,
18 email: Option<String>,
19 name: Option<String>,
20 created_at: DateTime<Utc>,
21}
22
23#[derive(Deserialize)]
24struct SeedConfig {
25 /// Which users to create as admins.
26 admins: Vec<String>,
27 /// Which channels to create (all admins are invited to all channels).
28 channels: Vec<String>,
29}
30
31pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result<()> {
32 let client = reqwest::Client::new();
33
34 if !db.get_all_users(0, 1).await?.is_empty() && !force {
35 return Ok(());
36 }
37
38 let seed_path = config
39 .seed_path
40 .as_ref()
41 .context("called seed with no SEED_PATH")?;
42
43 let seed_config = load_admins(seed_path)
44 .context(format!("failed to load {}", seed_path.to_string_lossy()))?;
45
46 let mut first_user = None;
47 let mut others = vec![];
48
49 for admin_login in seed_config.admins {
50 let user = fetch_github::<GithubUser>(
51 &client,
52 &format!("https://api.github.com/users/{admin_login}"),
53 )
54 .await;
55 let user = db
56 .create_user(
57 &user.email.unwrap_or(format!("{admin_login}@example.com")),
58 user.name.as_deref(),
59 true,
60 NewUserParams {
61 github_login: user.login,
62 github_user_id: user.id,
63 },
64 )
65 .await
66 .context("failed to create admin user")?;
67 if first_user.is_none() {
68 first_user = Some(user.user_id);
69 } else {
70 others.push(user.user_id)
71 }
72 }
73
74 for channel in seed_config.channels {
75 let (channel, _) = db
76 .create_channel(&channel, None, first_user.unwrap())
77 .await
78 .context("failed to create channel")?;
79
80 for user_id in &others {
81 db.invite_channel_member(
82 channel.id,
83 *user_id,
84 first_user.unwrap(),
85 ChannelRole::Admin,
86 )
87 .await
88 .context("failed to add user to channel")?;
89 }
90 }
91
92 let github_users_filepath = seed_path.parent().unwrap().join("seed/github_users.json");
93 let github_users: Vec<GithubUser> =
94 serde_json::from_str(&fs::read_to_string(github_users_filepath)?)?;
95
96 for github_user in github_users {
97 log::info!("Seeding {:?} from GitHub", github_user.login);
98
99 db.update_or_create_user_by_github_account(
100 &github_user.login,
101 github_user.id,
102 github_user.email.as_deref(),
103 github_user.name.as_deref(),
104 github_user.created_at,
105 None,
106 )
107 .await
108 .expect("failed to insert user");
109 }
110
111 Ok(())
112}
113
114fn load_admins(path: impl AsRef<Path>) -> anyhow::Result<SeedConfig> {
115 let file_content = fs::read_to_string(path)?;
116 Ok(serde_json::from_str(&file_content)?)
117}
118
119async fn fetch_github<T: DeserializeOwned>(client: &reqwest::Client, url: &str) -> T {
120 let mut request_builder = client.get(url);
121 if let Ok(github_token) = std::env::var("GITHUB_TOKEN") {
122 request_builder =
123 request_builder.header("Authorization", format!("Bearer {}", github_token));
124 }
125 let response = request_builder
126 .header("user-agent", "zed")
127 .send()
128 .await
129 .unwrap_or_else(|error| panic!("failed to fetch '{url}': {error}"));
130 let response_text = response.text().await.unwrap_or_else(|error| {
131 panic!("failed to fetch '{url}': {error}");
132 });
133 serde_json::from_str(&response_text).unwrap_or_else(|error| {
134 panic!("failed to deserialize github user from '{url}'. Error: '{error}', text: '{response_text}'");
135 })
136}