1use crate::db::{self, ChannelRole, NewUserParams};
2
3use anyhow::Context;
4use chrono::{DateTime, Utc};
5use db::Database;
6use serde::{de::DeserializeOwned, Deserialize};
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 created_at: DateTime<Utc>,
20}
21
22#[derive(Deserialize)]
23struct SeedConfig {
24 /// Which users to create as admins.
25 admins: Vec<String>,
26 /// Which channels to create (all admins are invited to all channels).
27 channels: Vec<String>,
28}
29
30pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result<()> {
31 let client = reqwest::Client::new();
32
33 if !db.get_all_users(0, 1).await?.is_empty() && !force {
34 return Ok(());
35 }
36
37 let seed_path = config
38 .seed_path
39 .as_ref()
40 .context("called seed with no SEED_PATH")?;
41
42 let seed_config = load_admins(seed_path)
43 .context(format!("failed to load {}", seed_path.to_string_lossy()))?;
44
45 let mut first_user = None;
46 let mut others = vec![];
47
48 let flag_names = ["remoting", "language-models"];
49 let mut flags = Vec::new();
50
51 let existing_feature_flags = db.list_feature_flags().await?;
52
53 for flag_name in flag_names {
54 if existing_feature_flags
55 .iter()
56 .any(|flag| flag.flag == flag_name)
57 {
58 log::info!("Flag {flag_name:?} already exists");
59 continue;
60 }
61
62 let flag = db
63 .create_user_flag(flag_name, false)
64 .await
65 .unwrap_or_else(|err| panic!("failed to create flag: '{flag_name}': {err}"));
66 flags.push(flag);
67 }
68
69 for admin_login in seed_config.admins {
70 let user = fetch_github::<GithubUser>(
71 &client,
72 &format!("https://api.github.com/users/{admin_login}"),
73 )
74 .await;
75 let user = db
76 .create_user(
77 &user.email.unwrap_or(format!("{admin_login}@example.com")),
78 true,
79 NewUserParams {
80 github_login: user.login,
81 github_user_id: user.id,
82 },
83 )
84 .await
85 .context("failed to create admin user")?;
86 if first_user.is_none() {
87 first_user = Some(user.user_id);
88 } else {
89 others.push(user.user_id)
90 }
91
92 for flag in &flags {
93 db.add_user_flag(user.user_id, *flag)
94 .await
95 .context(format!(
96 "Unable to enable flag '{}' for user '{}'",
97 flag, user.user_id
98 ))?;
99 }
100 }
101
102 for channel in seed_config.channels {
103 let (channel, _) = db
104 .create_channel(&channel, None, first_user.unwrap())
105 .await
106 .context("failed to create channel")?;
107
108 for user_id in &others {
109 db.invite_channel_member(
110 channel.id,
111 *user_id,
112 first_user.unwrap(),
113 ChannelRole::Admin,
114 )
115 .await
116 .context("failed to add user to channel")?;
117 }
118 }
119
120 let github_users_filepath = seed_path.parent().unwrap().join("seed/github_users.json");
121 let github_users: Vec<GithubUser> =
122 serde_json::from_str(&fs::read_to_string(github_users_filepath)?)?;
123
124 for github_user in github_users {
125 log::info!("Seeding {:?} from GitHub", github_user.login);
126
127 let user = db
128 .get_or_create_user_by_github_account(
129 &github_user.login,
130 github_user.id,
131 github_user.email.as_deref(),
132 github_user.created_at,
133 None,
134 )
135 .await
136 .expect("failed to insert user");
137
138 for flag in &flags {
139 db.add_user_flag(user.id, *flag).await.context(format!(
140 "Unable to enable flag '{}' for user '{}'",
141 flag, user.id
142 ))?;
143 }
144 }
145
146 Ok(())
147}
148
149fn load_admins(path: impl AsRef<Path>) -> anyhow::Result<SeedConfig> {
150 let file_content = fs::read_to_string(path)?;
151 Ok(serde_json::from_str(&file_content)?)
152}
153
154async fn fetch_github<T: DeserializeOwned>(client: &reqwest::Client, url: &str) -> T {
155 let response = client
156 .get(url)
157 .header("user-agent", "zed")
158 .send()
159 .await
160 .unwrap_or_else(|error| panic!("failed to fetch '{url}': {error}"));
161 response
162 .json()
163 .await
164 .unwrap_or_else(|error| panic!("failed to deserialize github user from '{url}': {error}"))
165}