logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

74
crates/collab/src/api.rs Normal file
View File

@@ -0,0 +1,74 @@
pub mod events;
pub mod extensions;
use crate::Result;
use axum::{headers::Header, http::HeaderName};
use std::sync::OnceLock;
pub use extensions::fetch_extensions_from_blob_store_periodically;
pub struct CloudflareIpCountryHeader(String);
impl Header for CloudflareIpCountryHeader {
fn name() -> &'static HeaderName {
static CLOUDFLARE_IP_COUNTRY_HEADER: OnceLock<HeaderName> = OnceLock::new();
CLOUDFLARE_IP_COUNTRY_HEADER.get_or_init(|| HeaderName::from_static("cf-ipcountry"))
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
let country_code = values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?;
Ok(Self(country_code.to_string()))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
unimplemented!()
}
}
impl std::fmt::Display for CloudflareIpCountryHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub struct SystemIdHeader(String);
impl Header for SystemIdHeader {
fn name() -> &'static HeaderName {
static SYSTEM_ID_HEADER: OnceLock<HeaderName> = OnceLock::new();
SYSTEM_ID_HEADER.get_or_init(|| HeaderName::from_static("x-zed-system-id"))
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
let system_id = values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?;
Ok(Self(system_id.to_string()))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
unimplemented!()
}
}
impl std::fmt::Display for SystemIdHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

View File

@@ -0,0 +1,232 @@
use crate::api::CloudflareIpCountryHeader;
use crate::{AppState, Error, Result};
use anyhow::anyhow;
use axum::{
Extension, Router, TypedHeader,
body::Bytes,
headers::Header,
http::{HeaderName, StatusCode},
routing::post,
};
use chrono::Duration;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};
use std::sync::{Arc, OnceLock};
use telemetry_events::{Event, EventRequestBody};
use util::ResultExt;
use uuid::Uuid;
pub fn router() -> Router {
Router::new()
.route("/telemetry/events", post(post_events))
.route("/telemetry/crashes", post(post_panic))
.route("/telemetry/panics", post(post_panic))
.route("/telemetry/hangs", post(post_panic))
}
pub struct ZedChecksumHeader(Vec<u8>);
impl Header for ZedChecksumHeader {
fn name() -> &'static HeaderName {
static ZED_CHECKSUM_HEADER: OnceLock<HeaderName> = OnceLock::new();
ZED_CHECKSUM_HEADER.get_or_init(|| HeaderName::from_static("x-zed-checksum"))
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
let checksum = values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?;
let bytes = hex::decode(checksum).map_err(|_| axum::headers::Error::invalid())?;
Ok(Self(bytes))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
unimplemented!()
}
}
pub async fn post_panic() -> Result<()> {
// as of v0.201.x crash/panic reporting is now done via Sentry.
// The endpoint returns OK to avoid spurious errors for old clients.
Ok(())
}
pub async fn post_events(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
body: Bytes,
) -> Result<()> {
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
return Err(Error::http(
StatusCode::INTERNAL_SERVER_ERROR,
"events not enabled".into(),
))?;
};
let checksum_matched = checksum == expected;
let request_body: telemetry_events::EventRequestBody =
serde_json::from_slice(&body).map_err(|err| {
log::error!("can't parse event json: {err}");
Error::Internal(anyhow!(err))
})?;
let Some(last_event) = request_body.events.last() else {
return Err(Error::http(StatusCode::BAD_REQUEST, "no events".into()))?;
};
let country_code = country_code_header.map(|h| h.to_string());
let first_event_at = chrono::Utc::now()
- chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
if let Some(kinesis_client) = app.kinesis_client.clone()
&& let Some(stream) = app.config.kinesis_stream.clone()
{
let mut request = kinesis_client.put_records().stream_name(stream);
let mut has_records = false;
for row in for_snowflake(
request_body.clone(),
first_event_at,
country_code.clone(),
checksum_matched,
) {
if let Some(data) = serde_json::to_vec(&row).log_err() {
request = request.records(
aws_sdk_kinesis::types::PutRecordsRequestEntry::builder()
.partition_key(request_body.system_id.clone().unwrap_or_default())
.data(data.into())
.build()
.unwrap(),
);
has_records = true;
}
}
if has_records {
request.send().await.log_err();
}
};
Ok(())
}
pub fn calculate_json_checksum(app: Arc<AppState>, json: &impl AsRef<[u8]>) -> Option<Vec<u8>> {
let checksum_seed = app.config.zed_client_checksum_seed.as_ref()?;
let mut summer = Sha256::new();
summer.update(checksum_seed);
summer.update(json);
summer.update(checksum_seed);
Some(summer.finalize().into_iter().collect())
}
fn for_snowflake(
body: EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
country_code: Option<String>,
checksum_matched: bool,
) -> impl Iterator<Item = SnowflakeRow> {
body.events.into_iter().map(move |event| {
let timestamp =
first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
let (event_type, mut event_properties) = match &event.event {
Event::Flexible(e) => (
e.event_type.clone(),
serde_json::to_value(&e.event_properties).unwrap(),
),
};
if let serde_json::Value::Object(ref mut map) = event_properties {
map.insert("app_version".to_string(), body.app_version.clone().into());
map.insert("os_name".to_string(), body.os_name.clone().into());
map.insert("os_version".to_string(), body.os_version.clone().into());
map.insert("architecture".to_string(), body.architecture.clone().into());
map.insert(
"release_channel".to_string(),
body.release_channel.clone().into(),
);
map.insert("signed_in".to_string(), event.signed_in.into());
map.insert("checksum_matched".to_string(), checksum_matched.into());
if let Some(country_code) = country_code.as_ref() {
map.insert("country".to_string(), country_code.clone().into());
}
}
// NOTE: most amplitude user properties are read out of our event_properties
// dictionary. See https://app.amplitude.com/data/zed/Zed/sources/detail/production/falcon%3A159998
// for how that is configured.
let user_properties = body.is_staff.map(|is_staff| {
serde_json::json!({
"is_staff": is_staff,
})
});
SnowflakeRow {
time: timestamp,
user_id: body.metrics_id.clone(),
device_id: body.system_id.clone(),
event_type,
event_properties,
user_properties,
insert_id: Some(Uuid::new_v4().to_string()),
}
})
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SnowflakeRow {
pub time: chrono::DateTime<chrono::Utc>,
pub user_id: Option<String>,
pub device_id: Option<String>,
pub event_type: String,
pub event_properties: serde_json::Value,
pub user_properties: Option<serde_json::Value>,
pub insert_id: Option<String>,
}
impl SnowflakeRow {
pub fn new(
event_type: impl Into<String>,
metrics_id: Option<Uuid>,
is_staff: bool,
system_id: Option<String>,
event_properties: serde_json::Value,
) -> Self {
Self {
time: chrono::Utc::now(),
event_type: event_type.into(),
device_id: system_id,
user_id: metrics_id.map(|id| id.to_string()),
insert_id: Some(uuid::Uuid::new_v4().to_string()),
event_properties,
user_properties: Some(json!({"is_staff": is_staff})),
}
}
pub async fn write(
self,
client: &Option<aws_sdk_kinesis::Client>,
stream: &Option<String>,
) -> anyhow::Result<()> {
let Some((client, stream)) = client.as_ref().zip(stream.as_ref()) else {
return Ok(());
};
let row = serde_json::to_vec(&self)?;
client
.put_record()
.stream_name(stream)
.partition_key(&self.user_id.unwrap_or_default())
.data(row.into())
.send()
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,345 @@
use crate::db::ExtensionVersionConstraints;
use crate::{AppState, Error, Result, db::NewExtensionVersion};
use anyhow::Context as _;
use aws_sdk_s3::presigning::PresigningConfig;
use axum::{
Extension, Json, Router,
extract::{Path, Query},
http::StatusCode,
response::Redirect,
routing::get,
};
use cloud_api_types::{ExtensionApiManifest, GetExtensionsResponse};
use collections::HashMap;
use semver::Version as SemanticVersion;
use serde::Deserialize;
use std::{sync::Arc, time::Duration};
use time::PrimitiveDateTime;
use util::{ResultExt, maybe};
pub fn router() -> Router {
Router::new()
.route("/extensions/updates", get(get_extension_updates))
.route("/extensions/:extension_id", get(get_extension_versions))
.route(
"/extensions/:extension_id/download",
get(download_latest_extension),
)
.route(
"/extensions/:extension_id/:version/download",
get(download_extension),
)
}
#[derive(Debug, Deserialize)]
struct GetExtensionUpdatesParams {
ids: String,
min_schema_version: i32,
max_schema_version: i32,
min_wasm_api_version: semver::Version,
max_wasm_api_version: semver::Version,
}
async fn get_extension_updates(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<GetExtensionUpdatesParams>,
) -> Result<Json<GetExtensionsResponse>> {
let constraints = ExtensionVersionConstraints {
schema_versions: params.min_schema_version..=params.max_schema_version,
wasm_api_versions: params.min_wasm_api_version..=params.max_wasm_api_version,
};
let extension_ids = params.ids.split(',').map(|s| s.trim()).collect::<Vec<_>>();
let extensions = app
.db
.get_extensions_by_ids(&extension_ids, Some(&constraints))
.await?;
Ok(Json(GetExtensionsResponse { data: extensions }))
}
#[derive(Debug, Deserialize)]
struct GetExtensionVersionsParams {
extension_id: String,
}
async fn get_extension_versions(
Extension(app): Extension<Arc<AppState>>,
Path(params): Path<GetExtensionVersionsParams>,
) -> Result<Json<GetExtensionsResponse>> {
let extension_versions = app.db.get_extension_versions(&params.extension_id).await?;
Ok(Json(GetExtensionsResponse {
data: extension_versions,
}))
}
#[derive(Debug, Deserialize)]
struct DownloadLatestExtensionPathParams {
extension_id: String,
}
#[derive(Debug, Deserialize)]
struct DownloadLatestExtensionQueryParams {
min_schema_version: Option<i32>,
max_schema_version: Option<i32>,
min_wasm_api_version: Option<SemanticVersion>,
max_wasm_api_version: Option<SemanticVersion>,
}
async fn download_latest_extension(
Extension(app): Extension<Arc<AppState>>,
Path(params): Path<DownloadLatestExtensionPathParams>,
Query(query): Query<DownloadLatestExtensionQueryParams>,
) -> Result<Redirect> {
let constraints = maybe!({
let min_schema_version = query.min_schema_version?;
let max_schema_version = query.max_schema_version?;
let min_wasm_api_version = query.min_wasm_api_version?;
let max_wasm_api_version = query.max_wasm_api_version?;
Some(ExtensionVersionConstraints {
schema_versions: min_schema_version..=max_schema_version,
wasm_api_versions: min_wasm_api_version..=max_wasm_api_version,
})
});
let extension = app
.db
.get_extension(&params.extension_id, constraints.as_ref())
.await?
.context("unknown extension")?;
download_extension(
Extension(app),
Path(DownloadExtensionParams {
extension_id: params.extension_id,
version: extension.manifest.version.to_string(),
}),
)
.await
}
#[derive(Debug, Deserialize)]
struct DownloadExtensionParams {
extension_id: String,
version: String,
}
async fn download_extension(
Extension(app): Extension<Arc<AppState>>,
Path(params): Path<DownloadExtensionParams>,
) -> Result<Redirect> {
let Some((blob_store_client, bucket)) = app
.blob_store_client
.clone()
.zip(app.config.blob_store_bucket.clone())
else {
Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
let DownloadExtensionParams {
extension_id,
version,
} = params;
let version_exists = app
.db
.record_extension_download(&extension_id, &version)
.await?;
if !version_exists {
Err(Error::http(
StatusCode::NOT_FOUND,
"unknown extension version".into(),
))?;
}
let url = blob_store_client
.get_object()
.bucket(bucket)
.key(format!(
"extensions/{extension_id}/{version}/archive.tar.gz"
))
.presigned(PresigningConfig::expires_in(EXTENSION_DOWNLOAD_URL_LIFETIME).unwrap())
.await
.context("creating presigned extension download url")?;
Ok(Redirect::temporary(url.uri()))
}
const EXTENSION_FETCH_INTERVAL: Duration = Duration::from_secs(5 * 60);
const EXTENSION_DOWNLOAD_URL_LIFETIME: Duration = Duration::from_secs(3 * 60);
pub fn fetch_extensions_from_blob_store_periodically(app_state: Arc<AppState>) {
let Some(blob_store_client) = app_state.blob_store_client.clone() else {
log::info!("no blob store client");
return;
};
let Some(blob_store_bucket) = app_state.config.blob_store_bucket.clone() else {
log::info!("no blob store bucket");
return;
};
let executor = app_state.executor.clone();
executor.spawn_detached({
let executor = executor.clone();
async move {
loop {
fetch_extensions_from_blob_store(
&blob_store_client,
&blob_store_bucket,
&app_state,
)
.await
.log_err();
executor.sleep(EXTENSION_FETCH_INTERVAL).await;
}
}
});
}
async fn fetch_extensions_from_blob_store(
blob_store_client: &aws_sdk_s3::Client,
blob_store_bucket: &String,
app_state: &Arc<AppState>,
) -> anyhow::Result<()> {
log::info!("fetching extensions from blob store");
let mut next_marker = None;
let mut published_versions = HashMap::<String, Vec<String>>::default();
loop {
let list = blob_store_client
.list_objects()
.bucket(blob_store_bucket)
.prefix("extensions/")
.set_marker(next_marker.clone())
.send()
.await?;
let objects = list.contents.unwrap_or_default();
log::info!("fetched {} object(s) from blob store", objects.len());
for object in &objects {
let Some(key) = object.key.as_ref() else {
continue;
};
let mut parts = key.split('/');
let Some(_) = parts.next().filter(|part| *part == "extensions") else {
continue;
};
let Some(extension_id) = parts.next() else {
continue;
};
let Some(version) = parts.next() else {
continue;
};
if parts.next() == Some("manifest.json") {
published_versions
.entry(extension_id.to_owned())
.or_default()
.push(version.to_owned());
}
}
if let (Some(true), Some(last_object)) = (list.is_truncated, objects.last()) {
next_marker.clone_from(&last_object.key);
} else {
break;
}
}
log::info!("found {} published extensions", published_versions.len());
let known_versions = app_state.db.get_known_extension_versions().await?;
let mut new_versions = HashMap::<&str, Vec<NewExtensionVersion>>::default();
let empty = Vec::new();
for (extension_id, published_versions) in &published_versions {
let known_versions = known_versions.get(extension_id).unwrap_or(&empty);
for published_version in published_versions {
if known_versions
.binary_search_by_key(&published_version, |known_version| known_version)
.is_err()
&& let Some(extension) = fetch_extension_manifest(
blob_store_client,
blob_store_bucket,
extension_id,
published_version,
)
.await
.log_err()
{
new_versions
.entry(extension_id)
.or_default()
.push(extension);
}
}
}
app_state
.db
.insert_extension_versions(&new_versions)
.await?;
log::info!(
"fetched {} new extensions from blob store",
new_versions.values().map(|v| v.len()).sum::<usize>()
);
Ok(())
}
async fn fetch_extension_manifest(
blob_store_client: &aws_sdk_s3::Client,
blob_store_bucket: &String,
extension_id: &str,
version: &str,
) -> anyhow::Result<NewExtensionVersion> {
let object = blob_store_client
.get_object()
.bucket(blob_store_bucket)
.key(format!("extensions/{extension_id}/{version}/manifest.json"))
.send()
.await?;
let manifest_bytes = object
.body
.collect()
.await
.map(|data| data.into_bytes())
.with_context(|| {
format!("failed to download manifest for extension {extension_id} version {version}")
})?
.to_vec();
let manifest =
serde_json::from_slice::<ExtensionApiManifest>(&manifest_bytes).with_context(|| {
format!(
"invalid manifest for extension {extension_id} version {version}: {}",
String::from_utf8_lossy(&manifest_bytes)
)
})?;
let published_at = object.last_modified.with_context(|| {
format!("missing last modified timestamp for extension {extension_id} version {version}")
})?;
let published_at = time::OffsetDateTime::from_unix_timestamp_nanos(published_at.as_nanos())?;
let published_at = PrimitiveDateTime::new(published_at.date(), published_at.time());
let version = semver::Version::parse(&manifest.version).with_context(|| {
format!("invalid version for extension {extension_id} version {version}")
})?;
Ok(NewExtensionVersion {
name: manifest.name,
version,
description: manifest.description.unwrap_or_default(),
authors: manifest.authors,
repository: manifest.repository,
schema_version: manifest.schema_version.unwrap_or(0),
wasm_api_version: manifest.wasm_api_version,
provides: manifest.provides,
published_at,
})
}

86
crates/collab/src/auth.rs Normal file
View File

@@ -0,0 +1,86 @@
use crate::entities::User;
use crate::{AppState, Error, db::UserId, rpc::Principal};
use anyhow::Context as _;
use axum::{
http::{self, Request, StatusCode},
middleware::Next,
response::IntoResponse,
};
use cloud_api_types::GetAuthenticatedUserResponse;
pub use rpc::auth::random_token;
use std::sync::Arc;
/// Validates the authorization header and adds an Extension<Principal> to the request.
/// Authorization: <user-id> <token>
/// <token> is the access_token attached to that user.
/// Authorization: "dev-server-token" <token>
pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl IntoResponse {
let mut auth_header = req
.headers()
.get(http::header::AUTHORIZATION)
.and_then(|header| header.to_str().ok())
.ok_or_else(|| {
Error::http(
StatusCode::UNAUTHORIZED,
"missing authorization header".to_string(),
)
})?
.split_whitespace();
let state = req.extensions().get::<Arc<AppState>>().unwrap();
let first = auth_header.next().unwrap_or("");
if first == "dev-server-token" {
Err(Error::http(
StatusCode::UNAUTHORIZED,
"Dev servers were removed in Zed 0.157 please upgrade to SSH remoting".to_string(),
))?;
}
let user_id = UserId(first.parse().map_err(|_| {
Error::http(
StatusCode::BAD_REQUEST,
"missing user id in authorization header".to_string(),
)
})?);
let access_token = auth_header.next().ok_or_else(|| {
Error::http(
StatusCode::BAD_REQUEST,
"missing access token in authorization header".to_string(),
)
})?;
let http_client = state.http_client.clone().expect("no HTTP client");
let response = http_client
.get(format!("{}/client/users/me", state.config.zed_cloud_url()))
.header("Content-Type", "application/json")
.header("Authorization", format!("{user_id} {access_token}"))
.send()
.await
.context("failed to validate access token")?;
if let Ok(response) = response.error_for_status() {
let response_body: GetAuthenticatedUserResponse = response
.json()
.await
.context("failed to parse response body")?;
let user = User {
id: UserId(response_body.user.id),
github_login: response_body.user.github_login,
avatar_url: response_body.user.avatar_url,
name: response_body.user.name,
admin: response_body.user.is_staff,
connected_once: response_body.user.has_connected_to_collab_once,
};
req.extensions_mut().insert(Principal::User(user));
return Ok::<_, Error>(next.run(req).await);
}
Err(Error::http(
StatusCode::UNAUTHORIZED,
"invalid credentials".to_string(),
))
}

View File

@@ -0,0 +1,12 @@
use collab::env::get_dotenv_vars;
fn main() -> anyhow::Result<()> {
for (key, value) in get_dotenv_vars(".")? {
if option_env!("POWERSHELL").is_some() {
println!("$env:{}=\"{}\"", key, value);
} else {
println!("export {}=\"{}\"", key, value);
}
}
Ok(())
}

View File

@@ -0,0 +1,2 @@
use anyhow::{Result, anyhow};
use rpc::proto;

777
crates/collab/src/db.rs Normal file
View File

@@ -0,0 +1,777 @@
mod ids;
pub mod queries;
mod tables;
use crate::{Error, Result};
use anyhow::{Context as _, anyhow};
use cloud_api_types::{ExtensionMetadata, ExtensionProvides};
use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use dashmap::DashMap;
use futures::StreamExt;
use project_repository_statuses::StatusKind;
use rpc::{
ConnectionId,
proto::{self},
};
use sea_orm::{
ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction,
FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement,
TransactionTrait,
entity::prelude::*,
sea_query::{Alias, Expr, OnConflict},
};
use semver::Version;
use serde::{Deserialize, Serialize};
use std::ops::RangeInclusive;
use std::{
future::Future,
marker::PhantomData,
ops::{Deref, DerefMut},
rc::Rc,
sync::Arc,
};
use time::PrimitiveDateTime;
use tokio::sync::{Mutex, OwnedMutexGuard};
use util::paths::PathStyle;
use worktree_settings_file::LocalSettingsKind;
pub use ids::*;
pub use sea_orm::ConnectOptions;
pub use tables::*;
#[cfg(feature = "test-support")]
pub struct DatabaseTestOptions {
pub executor: gpui::BackgroundExecutor,
pub runtime: tokio::runtime::Runtime,
pub query_failure_probability: parking_lot::Mutex<f64>,
}
/// Database gives you a handle that lets you access the database.
/// It handles pooling internally.
pub struct Database {
pub options: ConnectOptions,
pub pool: DatabaseConnection,
rooms: DashMap<RoomId, Arc<Mutex<()>>>,
projects: DashMap<ProjectId, Arc<Mutex<()>>>,
notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
notification_kinds_by_name: HashMap<String, NotificationKindId>,
#[cfg(feature = "test-support")]
pub test_options: Option<DatabaseTestOptions>,
}
// The `Database` type has so many methods that its impl blocks are split into
// separate files in the `queries` folder.
impl Database {
/// Connects to the database with the given options
pub async fn new(options: ConnectOptions) -> Result<Self> {
sqlx::any::install_default_drivers();
Ok(Self {
options: options.clone(),
pool: sea_orm::Database::connect(options).await?,
rooms: DashMap::with_capacity(16384),
projects: DashMap::with_capacity(16384),
notification_kinds_by_id: HashMap::default(),
notification_kinds_by_name: HashMap::default(),
#[cfg(feature = "test-support")]
test_options: None,
})
}
pub fn options(&self) -> &ConnectOptions {
&self.options
}
#[cfg(feature = "test-support")]
pub fn reset(&self) {
self.rooms.clear();
self.projects.clear();
}
pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let body = async {
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(result) => match tx.commit().await.map_err(Into::into) {
Ok(()) => Ok(result),
Err(error) => Err(error),
},
Err(error) => {
tx.rollback().await?;
Err(error)
}
}
};
self.run(body).await
}
/// The same as room_transaction, but if you need to only optionally return a Room.
async fn optional_room_transaction<F, Fut, T>(
&self,
f: F,
) -> Result<Option<TransactionGuard<T>>>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
{
let body = async {
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(Some((room_id, data))) => {
let lock = self.rooms.entry(room_id).or_default().clone();
let _guard = lock.lock_owned().await;
match tx.commit().await.map_err(Into::into) {
Ok(()) => Ok(Some(TransactionGuard {
data,
_guard,
_not_send: PhantomData,
})),
Err(error) => Err(error),
}
}
Ok(None) => match tx.commit().await.map_err(Into::into) {
Ok(()) => Ok(None),
Err(error) => Err(error),
},
Err(error) => {
tx.rollback().await?;
Err(error)
}
}
};
self.run(body).await
}
async fn project_transaction<F, Fut, T>(
&self,
project_id: ProjectId,
f: F,
) -> Result<TransactionGuard<T>>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let room_id = Database::room_id_for_project(self, project_id).await?;
let body = async {
let lock = if let Some(room_id) = room_id {
self.rooms.entry(room_id).or_default().clone()
} else {
self.projects.entry(project_id).or_default().clone()
};
let _guard = lock.lock_owned().await;
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(data) => match tx.commit().await.map_err(Into::into) {
Ok(()) => Ok(TransactionGuard {
data,
_guard,
_not_send: PhantomData,
}),
Err(error) => Err(error),
},
Err(error) => {
tx.rollback().await?;
Err(error)
}
}
};
self.run(body).await
}
/// room_transaction runs the block in a transaction. It returns a RoomGuard, that keeps
/// the database locked until it is dropped. This ensures that updates sent to clients are
/// properly serialized with respect to database changes.
async fn room_transaction<F, Fut, T>(
&self,
room_id: RoomId,
f: F,
) -> Result<TransactionGuard<T>>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let body = async {
let lock = self.rooms.entry(room_id).or_default().clone();
let _guard = lock.lock_owned().await;
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(data) => match tx.commit().await.map_err(Into::into) {
Ok(()) => Ok(TransactionGuard {
data,
_guard,
_not_send: PhantomData,
}),
Err(error) => Err(error),
},
Err(error) => {
tx.rollback().await?;
Err(error)
}
}
};
self.run(body).await
}
async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let tx = self
.pool
.begin_with_config(Some(IsolationLevel::ReadCommitted), None)
.await?;
let mut tx = Arc::new(Some(tx));
let result = f(TransactionHandle(tx.clone())).await;
let tx = Arc::get_mut(&mut tx)
.and_then(|tx| tx.take())
.context("couldn't complete transaction because it's still in use")?;
Ok((tx, result))
}
async fn run<F, T>(&self, future: F) -> Result<T>
where
F: Future<Output = Result<T>>,
{
#[cfg(feature = "test-support")]
{
let test_options = self.test_options.as_ref().unwrap();
test_options.executor.simulate_random_delay().await;
let fail_probability = *test_options.query_failure_probability.lock();
if test_options.executor.rng().random_bool(fail_probability) {
return Err(anyhow!("simulated query failure"))?;
}
test_options.runtime.block_on(future)
}
#[cfg(not(feature = "test-support"))]
{
future.await
}
}
}
/// A handle to a [`DatabaseTransaction`].
pub struct TransactionHandle(pub(crate) Arc<Option<DatabaseTransaction>>);
impl Deref for TransactionHandle {
type Target = DatabaseTransaction;
fn deref(&self) -> &Self::Target {
self.0.as_ref().as_ref().unwrap()
}
}
/// [`TransactionGuard`] keeps a database transaction alive until it is dropped.
/// It wraps data that depends on the state of the database and prevents an additional
/// transaction from starting that would invalidate that data.
pub struct TransactionGuard<T> {
data: T,
_guard: OwnedMutexGuard<()>,
_not_send: PhantomData<Rc<()>>,
}
impl<T> Deref for TransactionGuard<T> {
type Target = T;
fn deref(&self) -> &T {
&self.data
}
}
impl<T> DerefMut for TransactionGuard<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.data
}
}
impl<T> TransactionGuard<T> {
/// Returns the inner value of the guard.
pub fn into_inner(self) -> T {
self.data
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Contact {
Accepted { user_id: UserId, busy: bool },
Outgoing { user_id: UserId },
Incoming { user_id: UserId },
}
impl Contact {
pub fn user_id(&self) -> UserId {
match self {
Contact::Accepted { user_id, .. } => *user_id,
Contact::Outgoing { user_id } => *user_id,
Contact::Incoming { user_id, .. } => *user_id,
}
}
}
pub type NotificationBatch = Vec<(UserId, proto::Notification)>;
pub struct CreatedChannelMessage {
pub message_id: MessageId,
pub participant_connection_ids: HashSet<ConnectionId>,
pub notifications: NotificationBatch,
}
pub struct UpdatedChannelMessage {
pub message_id: MessageId,
pub participant_connection_ids: Vec<ConnectionId>,
pub notifications: NotificationBatch,
pub reply_to_message_id: Option<MessageId>,
pub timestamp: PrimitiveDateTime,
pub deleted_mention_notification_ids: Vec<NotificationId>,
pub updated_mention_notifications: Vec<rpc::proto::Notification>,
}
#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)]
pub struct Invite {
pub email_address: String,
pub email_confirmation_code: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct NewSignup {
pub email_address: String,
pub platform_mac: bool,
pub platform_windows: bool,
pub platform_linux: bool,
pub editor_features: Vec<String>,
pub programming_languages: Vec<String>,
pub device_id: Option<String>,
pub added_to_mailing_list: bool,
pub created_at: Option<DateTime>,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)]
pub struct WaitlistSummary {
pub count: i64,
pub linux_count: i64,
pub mac_count: i64,
pub windows_count: i64,
pub unknown_count: i64,
}
/// The parameters to create a new user.
#[derive(Debug, Serialize, Deserialize)]
pub struct NewUserParams {
pub github_login: String,
pub github_user_id: i32,
}
/// The result of creating a new user.
#[derive(Debug)]
pub struct NewUserResult {
pub user_id: UserId,
}
/// The result of updating a channel membership.
#[derive(Debug)]
pub struct MembershipUpdated {
pub channel_id: ChannelId,
pub new_channels: ChannelsForUser,
pub removed_channels: Vec<ChannelId>,
}
/// The result of setting a member's role.
#[derive(Debug)]
pub enum SetMemberRoleResult {
InviteUpdated(Channel),
MembershipUpdated(MembershipUpdated),
}
/// The result of inviting a member to a channel.
#[derive(Debug)]
pub struct InviteMemberResult {
pub channel: Channel,
pub notifications: NotificationBatch,
}
#[derive(Debug)]
pub struct RespondToChannelInvite {
pub membership_update: Option<MembershipUpdated>,
pub notifications: NotificationBatch,
}
#[derive(Debug)]
pub struct RemoveChannelMemberResult {
pub membership_update: MembershipUpdated,
pub notification_id: Option<NotificationId>,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Channel {
pub id: ChannelId,
pub name: String,
pub visibility: ChannelVisibility,
/// parent_path is the channel ids from the root to this one (not including this one)
pub parent_path: Vec<ChannelId>,
pub channel_order: i32,
}
impl Channel {
pub fn from_model(value: channel::Model) -> Self {
Channel {
id: value.id,
visibility: value.visibility,
name: value.clone().name,
parent_path: value.ancestors().collect(),
channel_order: value.channel_order,
}
}
pub fn to_proto(&self) -> proto::Channel {
proto::Channel {
id: self.id.to_proto(),
name: self.name.clone(),
visibility: self.visibility.into(),
parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
channel_order: self.channel_order,
}
}
pub fn root_id(&self) -> ChannelId {
self.parent_path.first().copied().unwrap_or(self.id)
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ChannelMember {
pub role: ChannelRole,
pub user_id: UserId,
pub kind: proto::channel_member::Kind,
}
impl ChannelMember {
pub fn to_proto(&self) -> proto::ChannelMember {
proto::ChannelMember {
role: self.role.into(),
user_id: self.user_id.to_proto(),
kind: self.kind.into(),
}
}
}
#[derive(Debug, PartialEq)]
pub struct ChannelsForUser {
pub channels: Vec<Channel>,
pub channel_memberships: Vec<channel_member::Model>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub invited_channels: Vec<Channel>,
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
pub latest_buffer_versions: Vec<proto::ChannelBufferVersion>,
}
#[derive(Debug)]
pub struct RejoinedChannelBuffer {
pub buffer: proto::RejoinedChannelBuffer,
pub old_connection_id: ConnectionId,
}
#[derive(Clone)]
pub struct JoinRoom {
pub room: proto::Room,
pub channel: Option<channel::Model>,
}
pub struct RejoinedRoom {
pub room: proto::Room,
pub rejoined_projects: Vec<RejoinedProject>,
pub reshared_projects: Vec<ResharedProject>,
pub channel: Option<channel::Model>,
}
pub struct ResharedProject {
pub id: ProjectId,
pub old_connection_id: ConnectionId,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: Vec<proto::WorktreeMetadata>,
}
pub struct RejoinedProject {
pub id: ProjectId,
pub old_connection_id: ConnectionId,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: Vec<RejoinedWorktree>,
pub updated_repositories: Vec<proto::UpdateRepository>,
pub removed_repositories: Vec<u64>,
pub language_servers: Vec<LanguageServer>,
}
impl RejoinedProject {
pub fn to_proto(&self) -> proto::RejoinedProject {
let (language_servers, language_server_capabilities) = self
.language_servers
.clone()
.into_iter()
.map(|server| (server.server, server.capabilities))
.unzip();
proto::RejoinedProject {
id: self.id.to_proto(),
worktrees: self
.worktrees
.iter()
.map(|worktree| proto::WorktreeMetadata {
id: worktree.id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
abs_path: worktree.abs_path.clone(),
root_repo_common_dir: None,
})
.collect(),
collaborators: self
.collaborators
.iter()
.map(|collaborator| collaborator.to_proto())
.collect(),
language_servers,
language_server_capabilities,
}
}
}
#[derive(Debug)]
pub struct RejoinedWorktree {
pub id: u64,
pub abs_path: String,
pub root_name: String,
pub visible: bool,
pub updated_entries: Vec<proto::Entry>,
pub removed_entries: Vec<u64>,
pub updated_repositories: Vec<proto::RepositoryEntry>,
pub removed_repositories: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
pub completed_scan_id: u64,
pub root_repo_common_dir: Option<String>,
}
pub struct LeftRoom {
pub room: proto::Room,
pub channel: Option<channel::Model>,
pub left_projects: HashMap<ProjectId, LeftProject>,
pub canceled_calls_to_user_ids: Vec<UserId>,
pub deleted: bool,
}
pub struct RefreshedRoom {
pub room: proto::Room,
pub channel: Option<channel::Model>,
pub stale_participant_user_ids: Vec<UserId>,
pub canceled_calls_to_user_ids: Vec<UserId>,
}
pub struct RefreshedChannelBuffer {
pub connection_ids: Vec<ConnectionId>,
pub collaborators: Vec<proto::Collaborator>,
}
pub struct Project {
pub id: ProjectId,
pub role: ChannelRole,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: BTreeMap<u64, Worktree>,
pub repositories: Vec<proto::UpdateRepository>,
pub language_servers: Vec<LanguageServer>,
pub path_style: PathStyle,
pub features: Vec<String>,
}
pub struct ProjectCollaborator {
pub connection_id: ConnectionId,
pub user_id: UserId,
pub replica_id: ReplicaId,
pub is_host: bool,
pub committer_name: Option<String>,
pub committer_email: Option<String>,
}
impl ProjectCollaborator {
pub fn to_proto(&self) -> proto::Collaborator {
proto::Collaborator {
peer_id: Some(self.connection_id.into()),
replica_id: self.replica_id.0 as u32,
user_id: self.user_id.to_proto(),
is_host: self.is_host,
committer_name: self.committer_name.clone(),
committer_email: self.committer_email.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct LanguageServer {
pub server: proto::LanguageServer,
pub capabilities: String,
}
#[derive(Debug)]
pub struct LeftProject {
pub id: ProjectId,
pub should_unshare: bool,
pub connection_ids: Vec<ConnectionId>,
}
pub struct Worktree {
pub id: u64,
pub abs_path: String,
pub root_name: String,
pub visible: bool,
pub entries: Vec<proto::Entry>,
pub legacy_repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
pub completed_scan_id: u64,
pub root_repo_common_dir: Option<String>,
}
#[derive(Debug)]
pub struct WorktreeSettingsFile {
pub path: String,
pub content: String,
pub kind: LocalSettingsKind,
pub outside_worktree: bool,
}
pub struct NewExtensionVersion {
pub name: String,
pub version: semver::Version,
pub description: String,
pub authors: Vec<String>,
pub repository: String,
pub schema_version: i32,
pub wasm_api_version: Option<String>,
pub provides: BTreeSet<ExtensionProvides>,
pub published_at: PrimitiveDateTime,
}
pub struct ExtensionVersionConstraints {
pub schema_versions: RangeInclusive<i32>,
pub wasm_api_versions: RangeInclusive<semver::Version>,
}
impl LocalSettingsKind {
pub fn from_proto(proto_kind: proto::LocalSettingsKind) -> Self {
match proto_kind {
proto::LocalSettingsKind::Settings => Self::Settings,
proto::LocalSettingsKind::Tasks => Self::Tasks,
proto::LocalSettingsKind::Editorconfig => Self::Editorconfig,
proto::LocalSettingsKind::Debug => Self::Debug,
}
}
pub fn to_proto(self) -> proto::LocalSettingsKind {
match self {
Self::Settings => proto::LocalSettingsKind::Settings,
Self::Tasks => proto::LocalSettingsKind::Tasks,
Self::Editorconfig => proto::LocalSettingsKind::Editorconfig,
Self::Debug => proto::LocalSettingsKind::Debug,
}
}
}
fn db_status_to_proto(
entry: project_repository_statuses::Model,
) -> anyhow::Result<proto::StatusEntry> {
use proto::git_file_status::{Tracked, Unmerged, Variant};
let (simple_status, variant) =
match (entry.status_kind, entry.first_status, entry.second_status) {
(StatusKind::Untracked, None, None) => (
proto::GitStatus::Added as i32,
Variant::Untracked(Default::default()),
),
(StatusKind::Ignored, None, None) => (
proto::GitStatus::Added as i32,
Variant::Ignored(Default::default()),
),
(StatusKind::Unmerged, Some(first_head), Some(second_head)) => (
proto::GitStatus::Conflict as i32,
Variant::Unmerged(Unmerged {
first_head,
second_head,
}),
),
(StatusKind::Tracked, Some(index_status), Some(worktree_status)) => {
let simple_status = if worktree_status != proto::GitStatus::Unmodified as i32 {
worktree_status
} else if index_status != proto::GitStatus::Unmodified as i32 {
index_status
} else {
proto::GitStatus::Unmodified as i32
};
(
simple_status,
Variant::Tracked(Tracked {
index_status,
worktree_status,
}),
)
}
_ => {
anyhow::bail!("Unexpected combination of status fields: {entry:?}");
}
};
Ok(proto::StatusEntry {
repo_path: entry.repo_path,
simple_status,
status: Some(proto::GitFileStatus {
variant: Some(variant),
}),
diff_stat_added: entry.lines_added.map(|v| v as u32),
diff_stat_deleted: entry.lines_deleted.map(|v| v as u32),
})
}
fn proto_status_to_db(
status_entry: proto::StatusEntry,
) -> (String, StatusKind, Option<i32>, Option<i32>) {
use proto::git_file_status::{Tracked, Unmerged, Variant};
let (status_kind, first_status, second_status) = status_entry
.status
.clone()
.and_then(|status| status.variant)
.map_or(
(StatusKind::Untracked, None, None),
|variant| match variant {
Variant::Untracked(_) => (StatusKind::Untracked, None, None),
Variant::Ignored(_) => (StatusKind::Ignored, None, None),
Variant::Unmerged(Unmerged {
first_head,
second_head,
}) => (StatusKind::Unmerged, Some(first_head), Some(second_head)),
Variant::Tracked(Tracked {
index_status,
worktree_status,
}) => (
StatusKind::Tracked,
Some(index_status),
Some(worktree_status),
),
},
);
(
status_entry.repo_path,
status_kind,
first_status,
second_status,
)
}

315
crates/collab/src/db/ids.rs Normal file
View File

@@ -0,0 +1,315 @@
use crate::Result;
use rpc::proto;
use sea_orm::{DbErr, entity::prelude::*};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[macro_export]
macro_rules! id_type {
($name:ident) => {
#[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
DeriveValueType,
)]
#[allow(missing_docs)]
#[serde(transparent)]
pub struct $name(pub i32);
impl $name {
#[allow(unused)]
#[allow(missing_docs)]
pub const MAX: Self = Self(i32::MAX);
#[allow(unused)]
#[allow(missing_docs)]
pub fn from_proto(value: u64) -> Self {
debug_assert!(value != 0);
Self(value as i32)
}
#[allow(unused)]
#[allow(missing_docs)]
pub fn to_proto(self) -> u64 {
self.0 as u64
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl sea_orm::TryFromU64 for $name {
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
Ok(Self(n.try_into().map_err(|_| {
DbErr::ConvertFromU64(concat!(
"error converting ",
stringify!($name),
" to u64"
))
})?))
}
}
impl sea_orm::sea_query::Nullable for $name {
fn null() -> Value {
Value::Int(None)
}
}
};
}
id_type!(BufferId);
id_type!(ChannelBufferCollaboratorId);
id_type!(ChannelChatParticipantId);
id_type!(ChannelId);
id_type!(ChannelMemberId);
id_type!(ContactId);
id_type!(ExtensionId);
id_type!(FlagId);
id_type!(FollowerId);
id_type!(HostedProjectId);
id_type!(MessageId);
id_type!(NotificationId);
id_type!(NotificationKindId);
id_type!(ProjectCollaboratorId);
id_type!(ProjectId);
id_type!(ReplicaId);
id_type!(RoomId);
id_type!(RoomParticipantId);
id_type!(ServerId);
id_type!(SignupId);
id_type!(UserId);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)]
pub struct SharedThreadId(pub Uuid);
impl SharedThreadId {
pub fn from_proto(id: String) -> Option<Self> {
Uuid::parse_str(&id).ok().map(SharedThreadId)
}
pub fn to_proto(self) -> String {
self.0.to_string()
}
}
impl sea_orm::TryFromU64 for SharedThreadId {
fn try_from_u64(_n: u64) -> std::result::Result<Self, DbErr> {
Err(DbErr::ConvertFromU64(
"SharedThreadId uses UUID and cannot be converted from u64",
))
}
}
impl sea_orm::sea_query::Nullable for SharedThreadId {
fn null() -> Value {
Value::Uuid(None)
}
}
impl std::fmt::Display for SharedThreadId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
}
}
/// ChannelRole gives you permissions for both channels and calls.
#[derive(
Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash, Serialize,
)]
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
pub enum ChannelRole {
/// Admin can read/write and change permissions.
#[sea_orm(string_value = "admin")]
Admin,
/// Member can read/write, but not change permissions.
#[sea_orm(string_value = "member")]
#[default]
Member,
/// Talker can read, but not write.
/// They can use microphones and the channel chat
#[sea_orm(string_value = "talker")]
Talker,
/// Guest can read, but not write.
/// They can not use microphones but can use the chat.
#[sea_orm(string_value = "guest")]
Guest,
/// Banned may not read.
#[sea_orm(string_value = "banned")]
Banned,
}
impl ChannelRole {
/// Returns true if this role is more powerful than the other role.
pub fn should_override(&self, other: Self) -> bool {
use ChannelRole::*;
match self {
Admin => matches!(other, Member | Banned | Talker | Guest),
Member => matches!(other, Banned | Talker | Guest),
Talker => matches!(other, Guest),
Banned => matches!(other, Guest),
Guest => false,
}
}
/// Returns the maximal role between the two
pub fn max(&self, other: Self) -> Self {
if self.should_override(other) {
*self
} else {
other
}
}
pub fn can_see_channel(&self, visibility: ChannelVisibility) -> bool {
use ChannelRole::*;
match self {
Admin | Member => true,
Guest | Talker => visibility == ChannelVisibility::Public,
Banned => false,
}
}
/// True if the role allows access to all descendant channels
pub fn can_see_all_descendants(&self) -> bool {
use ChannelRole::*;
match self {
Admin | Member => true,
Guest | Talker | Banned => false,
}
}
/// True if the role only allows access to public descendant channels
pub fn can_only_see_public_descendants(&self) -> bool {
use ChannelRole::*;
match self {
Guest | Talker => true,
Admin | Member | Banned => false,
}
}
/// True if the role can share screen/microphone/projects into rooms.
pub fn can_use_microphone(&self) -> bool {
use ChannelRole::*;
match self {
Admin | Member | Talker => true,
Guest | Banned => false,
}
}
/// True if the role can edit shared projects.
pub fn can_edit_projects(&self) -> bool {
use ChannelRole::*;
match self {
Admin | Member => true,
Talker | Guest | Banned => false,
}
}
/// True if the role can read shared projects.
pub fn can_read_projects(&self) -> bool {
use ChannelRole::*;
match self {
Admin | Member | Guest | Talker => true,
Banned => false,
}
}
pub fn requires_cla(&self) -> bool {
use ChannelRole::*;
match self {
Admin | Member => true,
Banned | Guest | Talker => false,
}
}
}
impl From<proto::ChannelRole> for ChannelRole {
fn from(value: proto::ChannelRole) -> Self {
match value {
proto::ChannelRole::Admin => ChannelRole::Admin,
proto::ChannelRole::Member => ChannelRole::Member,
proto::ChannelRole::Talker => ChannelRole::Talker,
proto::ChannelRole::Guest => ChannelRole::Guest,
proto::ChannelRole::Banned => ChannelRole::Banned,
}
}
}
impl From<ChannelRole> for proto::ChannelRole {
fn from(val: ChannelRole) -> Self {
match val {
ChannelRole::Admin => proto::ChannelRole::Admin,
ChannelRole::Member => proto::ChannelRole::Member,
ChannelRole::Talker => proto::ChannelRole::Talker,
ChannelRole::Guest => proto::ChannelRole::Guest,
ChannelRole::Banned => proto::ChannelRole::Banned,
}
}
}
impl From<ChannelRole> for i32 {
fn from(val: ChannelRole) -> Self {
let proto: proto::ChannelRole = val.into();
proto.into()
}
}
/// ChannelVisibility controls whether channels are public or private.
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
pub enum ChannelVisibility {
/// Public channels are visible to anyone with the link. People join with the Guest role by default.
#[sea_orm(string_value = "public")]
Public,
/// Members channels are only visible to members of this channel or its parents.
#[sea_orm(string_value = "members")]
#[default]
Members,
}
impl From<proto::ChannelVisibility> for ChannelVisibility {
fn from(value: proto::ChannelVisibility) -> Self {
match value {
proto::ChannelVisibility::Public => ChannelVisibility::Public,
proto::ChannelVisibility::Members => ChannelVisibility::Members,
}
}
}
impl From<ChannelVisibility> for proto::ChannelVisibility {
fn from(val: ChannelVisibility) -> Self {
match val {
ChannelVisibility::Public => proto::ChannelVisibility::Public,
ChannelVisibility::Members => proto::ChannelVisibility::Members,
}
}
}
impl From<ChannelVisibility> for i32 {
fn from(val: ChannelVisibility) -> Self {
let proto: proto::ChannelVisibility = val.into();
proto.into()
}
}
/// Indicate whether a [Buffer] has permissions to edit.
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum Capability {
/// The buffer is a mutable replica.
ReadWrite,
/// The buffer is a read-only replica.
ReadOnly,
}

View File

@@ -0,0 +1,13 @@
use super::*;
pub mod buffers;
pub mod channels;
pub mod contacts;
pub mod contributors;
pub mod extensions;
pub mod notifications;
pub mod projects;
pub mod rooms;
pub mod servers;
pub mod shared_threads;
pub mod users;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,363 @@
use anyhow::Context as _;
use super::*;
impl Database {
/// Retrieves the contacts for the user with the given ID.
pub async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> {
#[derive(Debug, FromQueryResult)]
struct ContactWithUserBusyStatuses {
user_id_a: UserId,
user_id_b: UserId,
a_to_b: bool,
accepted: bool,
user_a_busy: bool,
user_b_busy: bool,
}
self.transaction(|tx| async move {
let user_a_participant = Alias::new("user_a_participant");
let user_b_participant = Alias::new("user_b_participant");
let mut db_contacts = contact::Entity::find()
.column_as(
Expr::col((user_a_participant.clone(), room_participant::Column::Id))
.is_not_null(),
"user_a_busy",
)
.column_as(
Expr::col((user_b_participant.clone(), room_participant::Column::Id))
.is_not_null(),
"user_b_busy",
)
.filter(
contact::Column::UserIdA
.eq(user_id)
.or(contact::Column::UserIdB.eq(user_id)),
)
.join_as(
JoinType::LeftJoin,
contact::Relation::UserARoomParticipant.def(),
user_a_participant,
)
.join_as(
JoinType::LeftJoin,
contact::Relation::UserBRoomParticipant.def(),
user_b_participant,
)
.into_model::<ContactWithUserBusyStatuses>()
.stream(&*tx)
.await?;
let mut contacts = Vec::new();
while let Some(db_contact) = db_contacts.next().await {
let db_contact = db_contact?;
if db_contact.user_id_a == user_id {
if db_contact.accepted {
contacts.push(Contact::Accepted {
user_id: db_contact.user_id_b,
busy: db_contact.user_b_busy,
});
} else if db_contact.a_to_b {
contacts.push(Contact::Outgoing {
user_id: db_contact.user_id_b,
})
} else {
contacts.push(Contact::Incoming {
user_id: db_contact.user_id_b,
});
}
} else if db_contact.accepted {
contacts.push(Contact::Accepted {
user_id: db_contact.user_id_a,
busy: db_contact.user_a_busy,
});
} else if db_contact.a_to_b {
contacts.push(Contact::Incoming {
user_id: db_contact.user_id_a,
});
} else {
contacts.push(Contact::Outgoing {
user_id: db_contact.user_id_a,
});
}
}
contacts.sort_unstable_by_key(|contact| contact.user_id());
Ok(contacts)
})
.await
}
/// Returns whether the given user is a busy (on a call).
pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
self.transaction(|tx| async move {
let participant = room_participant::Entity::find()
.filter(room_participant::Column::UserId.eq(user_id))
.one(&*tx)
.await?;
Ok(participant.is_some())
})
.await
}
/// Returns whether the user with `user_id_1` has the user with `user_id_2` as a contact.
///
/// In order for this to return `true`, `user_id_2` must have an accepted invite from `user_id_1`.
pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result<bool> {
self.transaction(|tx| async move {
let (id_a, id_b) = if user_id_1 < user_id_2 {
(user_id_1, user_id_2)
} else {
(user_id_2, user_id_1)
};
Ok(contact::Entity::find()
.filter(
contact::Column::UserIdA
.eq(id_a)
.and(contact::Column::UserIdB.eq(id_b))
.and(contact::Column::Accepted.eq(true)),
)
.one(&*tx)
.await?
.is_some())
})
.await
}
/// Invite the user with `receiver_id` to be a contact of the user with `sender_id`.
pub async fn send_contact_request(
&self,
sender_id: UserId,
receiver_id: UserId,
) -> Result<NotificationBatch> {
self.transaction(|tx| async move {
let (id_a, id_b, a_to_b) = if sender_id < receiver_id {
(sender_id, receiver_id, true)
} else {
(receiver_id, sender_id, false)
};
let rows_affected = contact::Entity::insert(contact::ActiveModel {
user_id_a: ActiveValue::set(id_a),
user_id_b: ActiveValue::set(id_b),
a_to_b: ActiveValue::set(a_to_b),
accepted: ActiveValue::set(false),
should_notify: ActiveValue::set(true),
..Default::default()
})
.on_conflict(
OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB])
.values([
(contact::Column::Accepted, true.into()),
(contact::Column::ShouldNotify, false.into()),
])
.action_and_where(
contact::Column::Accepted.eq(false).and(
contact::Column::AToB
.eq(a_to_b)
.and(contact::Column::UserIdA.eq(id_b))
.or(contact::Column::AToB
.ne(a_to_b)
.and(contact::Column::UserIdA.eq(id_a))),
),
)
.to_owned(),
)
.exec_without_returning(&*tx)
.await?;
if rows_affected == 0 {
Err(anyhow!("contact already requested"))?;
}
Ok(self
.create_notification(
receiver_id,
rpc::Notification::ContactRequest {
sender_id: sender_id.to_proto(),
},
true,
&tx,
)
.await?
.into_iter()
.collect())
})
.await
}
/// Returns a bool indicating whether the removed contact had originally accepted or not
///
/// Deletes the contact identified by the requester and responder ids, and then returns
/// whether the deleted contact had originally accepted or was a pending contact request.
///
/// # Arguments
///
/// * `requester_id` - The user that initiates this request
/// * `responder_id` - The user that will be removed
pub async fn remove_contact(
&self,
requester_id: UserId,
responder_id: UserId,
) -> Result<(bool, Option<NotificationId>)> {
self.transaction(|tx| async move {
let (id_a, id_b) = if responder_id < requester_id {
(responder_id, requester_id)
} else {
(requester_id, responder_id)
};
let contact = contact::Entity::find()
.filter(
contact::Column::UserIdA
.eq(id_a)
.and(contact::Column::UserIdB.eq(id_b)),
)
.one(&*tx)
.await?
.context("no such contact")?;
contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;
let mut deleted_notification_id = None;
if !contact.accepted {
deleted_notification_id = self
.remove_notification(
responder_id,
rpc::Notification::ContactRequest {
sender_id: requester_id.to_proto(),
},
&tx,
)
.await?;
}
Ok((contact.accepted, deleted_notification_id))
})
.await
}
/// Dismisses a contact notification for the given user.
pub async fn dismiss_contact_notification(
&self,
user_id: UserId,
contact_user_id: UserId,
) -> Result<()> {
self.transaction(|tx| async move {
let (id_a, id_b, a_to_b) = if user_id < contact_user_id {
(user_id, contact_user_id, true)
} else {
(contact_user_id, user_id, false)
};
let result = contact::Entity::update_many()
.set(contact::ActiveModel {
should_notify: ActiveValue::set(false),
..Default::default()
})
.filter(
contact::Column::UserIdA
.eq(id_a)
.and(contact::Column::UserIdB.eq(id_b))
.and(
contact::Column::AToB
.eq(a_to_b)
.and(contact::Column::Accepted.eq(true))
.or(contact::Column::AToB
.ne(a_to_b)
.and(contact::Column::Accepted.eq(false))),
),
)
.exec(&*tx)
.await?;
if result.rows_affected == 0 {
Err(anyhow!("no such contact request"))?
} else {
Ok(())
}
})
.await
}
/// Accept or decline a contact request
pub async fn respond_to_contact_request(
&self,
responder_id: UserId,
requester_id: UserId,
accept: bool,
) -> Result<NotificationBatch> {
self.transaction(|tx| async move {
let (id_a, id_b, a_to_b) = if responder_id < requester_id {
(responder_id, requester_id, false)
} else {
(requester_id, responder_id, true)
};
let rows_affected = if accept {
let result = contact::Entity::update_many()
.set(contact::ActiveModel {
accepted: ActiveValue::set(true),
should_notify: ActiveValue::set(true),
..Default::default()
})
.filter(
contact::Column::UserIdA
.eq(id_a)
.and(contact::Column::UserIdB.eq(id_b))
.and(contact::Column::AToB.eq(a_to_b)),
)
.exec(&*tx)
.await?;
result.rows_affected
} else {
let result = contact::Entity::delete_many()
.filter(
contact::Column::UserIdA
.eq(id_a)
.and(contact::Column::UserIdB.eq(id_b))
.and(contact::Column::AToB.eq(a_to_b))
.and(contact::Column::Accepted.eq(false)),
)
.exec(&*tx)
.await?;
result.rows_affected
};
if rows_affected == 0 {
Err(anyhow!("no such contact request"))?
}
let mut notifications = Vec::new();
notifications.extend(
self.mark_notification_as_read_with_response(
responder_id,
&rpc::Notification::ContactRequest {
sender_id: requester_id.to_proto(),
},
accept,
&tx,
)
.await?,
);
if accept {
notifications.extend(
self.create_notification(
requester_id,
rpc::Notification::ContactRequestAccepted {
responder_id: responder_id.to_proto(),
},
true,
&tx,
)
.await?,
);
}
Ok(notifications)
})
.await
}
}

View File

@@ -0,0 +1,23 @@
use super::*;
impl Database {
/// Records that a given user has signed the CLA.
#[cfg(feature = "test-support")]
pub async fn add_contributor(&self, user_id: UserId) -> Result<()> {
self.transaction(|tx| async move {
contributor::Entity::insert(contributor::ActiveModel {
user_id: ActiveValue::Set(user_id),
signed_at: ActiveValue::NotSet,
})
.on_conflict(
OnConflict::column(contributor::Column::UserId)
.do_nothing()
.to_owned(),
)
.exec_without_returning(&*tx)
.await?;
Ok(())
})
.await
}
}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,403 @@
use std::str::FromStr;
use anyhow::Context;
use chrono::Utc;
use sea_orm::sea_query::IntoCondition;
use util::ResultExt;
use super::*;
impl Database {
pub async fn get_extensions_by_ids(
&self,
ids: &[&str],
constraints: Option<&ExtensionVersionConstraints>,
) -> Result<Vec<ExtensionMetadata>> {
self.transaction(|tx| async move {
let extensions = extension::Entity::find()
.filter(extension::Column::ExternalId.is_in(ids.iter().copied()))
.all(&*tx)
.await?;
let mut max_versions = self
.get_latest_versions_for_extensions(&extensions, constraints, &tx)
.await?;
Ok(extensions
.into_iter()
.filter_map(|extension| {
let (version, _) = max_versions.remove(&extension.id)?;
Some(metadata_from_extension_and_version(extension, version))
})
.collect())
})
.await
}
async fn get_latest_versions_for_extensions(
&self,
extensions: &[extension::Model],
constraints: Option<&ExtensionVersionConstraints>,
tx: &DatabaseTransaction,
) -> Result<HashMap<ExtensionId, (extension_version::Model, Version)>> {
let mut versions = extension_version::Entity::find()
.filter(
extension_version::Column::ExtensionId
.is_in(extensions.iter().map(|extension| extension.id)),
)
.stream(tx)
.await?;
let mut max_versions =
HashMap::<ExtensionId, (extension_version::Model, Version)>::default();
while let Some(version) = versions.next().await {
let version = version?;
let Some(extension_version) = Version::from_str(&version.version).log_err() else {
continue;
};
if let Some((_, max_extension_version)) = &max_versions.get(&version.extension_id)
&& max_extension_version > &extension_version
{
continue;
}
if let Some(constraints) = constraints {
if !constraints
.schema_versions
.contains(&version.schema_version)
{
continue;
}
if let Some(wasm_api_version) = version.wasm_api_version.as_ref() {
if let Some(version) = Version::from_str(wasm_api_version).log_err() {
if !constraints.wasm_api_versions.contains(&version) {
continue;
}
} else {
continue;
}
}
}
max_versions.insert(version.extension_id, (version, extension_version));
}
Ok(max_versions)
}
/// Returns all of the versions for the extension with the given ID.
pub async fn get_extension_versions(
&self,
extension_id: &str,
) -> Result<Vec<ExtensionMetadata>> {
self.transaction(|tx| async move {
let condition = extension::Column::ExternalId
.eq(extension_id)
.into_condition();
self.get_extensions_where(condition, None, &tx).await
})
.await
}
async fn get_extensions_where(
&self,
condition: Condition,
limit: Option<u64>,
tx: &DatabaseTransaction,
) -> Result<Vec<ExtensionMetadata>> {
let extensions = extension::Entity::find()
.inner_join(extension_version::Entity)
.select_also(extension_version::Entity)
.filter(condition)
.order_by_desc(extension::Column::TotalDownloadCount)
.order_by_asc(extension::Column::Name)
.limit(limit)
.all(tx)
.await?;
Ok(extensions
.into_iter()
.filter_map(|(extension, version)| {
Some(metadata_from_extension_and_version(extension, version?))
})
.collect())
}
pub async fn get_extension(
&self,
extension_id: &str,
constraints: Option<&ExtensionVersionConstraints>,
) -> Result<Option<ExtensionMetadata>> {
self.transaction(|tx| async move {
let extension = extension::Entity::find()
.filter(extension::Column::ExternalId.eq(extension_id))
.one(&*tx)
.await?
.with_context(|| format!("no such extension: {extension_id}"))?;
let extensions = [extension];
let mut versions = self
.get_latest_versions_for_extensions(&extensions, constraints, &tx)
.await?;
let [extension] = extensions;
Ok(versions.remove(&extension.id).map(|(max_version, _)| {
metadata_from_extension_and_version(extension, max_version)
}))
})
.await
}
pub async fn get_extension_version(
&self,
extension_id: &str,
version: &str,
) -> Result<Option<ExtensionMetadata>> {
self.transaction(|tx| async move {
let extension = extension::Entity::find()
.filter(extension::Column::ExternalId.eq(extension_id))
.filter(extension_version::Column::Version.eq(version))
.inner_join(extension_version::Entity)
.select_also(extension_version::Entity)
.one(&*tx)
.await?;
Ok(extension.and_then(|(extension, version)| {
Some(metadata_from_extension_and_version(extension, version?))
}))
})
.await
}
pub async fn get_known_extension_versions(&self) -> Result<HashMap<String, Vec<String>>> {
self.transaction(|tx| async move {
let mut extension_external_ids_by_id = HashMap::default();
let mut rows = extension::Entity::find().stream(&*tx).await?;
while let Some(row) = rows.next().await {
let row = row?;
extension_external_ids_by_id.insert(row.id, row.external_id);
}
drop(rows);
let mut known_versions_by_extension_id: HashMap<String, Vec<String>> =
HashMap::default();
let mut rows = extension_version::Entity::find().stream(&*tx).await?;
while let Some(row) = rows.next().await {
let row = row?;
let Some(extension_id) = extension_external_ids_by_id.get(&row.extension_id) else {
continue;
};
let versions = known_versions_by_extension_id
.entry(extension_id.clone())
.or_default();
if let Err(ix) = versions.binary_search(&row.version) {
versions.insert(ix, row.version);
}
}
drop(rows);
Ok(known_versions_by_extension_id)
})
.await
}
pub async fn insert_extension_versions(
&self,
versions_by_extension_id: &HashMap<&str, Vec<NewExtensionVersion>>,
) -> Result<()> {
self.transaction(|tx| async move {
for (external_id, versions) in versions_by_extension_id {
if versions.is_empty() {
continue;
}
let latest_version = versions
.iter()
.max_by_key(|version| &version.version)
.unwrap();
let insert = extension::Entity::insert(extension::ActiveModel {
name: ActiveValue::Set(latest_version.name.clone()),
external_id: ActiveValue::Set((*external_id).to_owned()),
id: ActiveValue::NotSet,
latest_version: ActiveValue::Set(latest_version.version.to_string()),
total_download_count: ActiveValue::NotSet,
})
.on_conflict(
OnConflict::columns([extension::Column::ExternalId])
.update_column(extension::Column::ExternalId)
.to_owned(),
);
let extension = if tx.support_returning() {
insert.exec_with_returning(&*tx).await?
} else {
// Sqlite
insert.exec_without_returning(&*tx).await?;
extension::Entity::find()
.filter(extension::Column::ExternalId.eq(*external_id))
.one(&*tx)
.await?
.context("failed to insert extension")?
};
extension_version::Entity::insert_many(versions.iter().map(|version| {
extension_version::ActiveModel {
extension_id: ActiveValue::Set(extension.id),
published_at: ActiveValue::Set(version.published_at),
version: ActiveValue::Set(version.version.to_string()),
authors: ActiveValue::Set(version.authors.join(", ")),
repository: ActiveValue::Set(version.repository.clone()),
description: ActiveValue::Set(version.description.clone()),
schema_version: ActiveValue::Set(version.schema_version),
wasm_api_version: ActiveValue::Set(version.wasm_api_version.clone()),
provides_themes: ActiveValue::Set(
version.provides.contains(&ExtensionProvides::Themes),
),
provides_icon_themes: ActiveValue::Set(
version.provides.contains(&ExtensionProvides::IconThemes),
),
provides_languages: ActiveValue::Set(
version.provides.contains(&ExtensionProvides::Languages),
),
provides_grammars: ActiveValue::Set(
version.provides.contains(&ExtensionProvides::Grammars),
),
provides_language_servers: ActiveValue::Set(
version
.provides
.contains(&ExtensionProvides::LanguageServers),
),
provides_context_servers: ActiveValue::Set(
version
.provides
.contains(&ExtensionProvides::ContextServers),
),
provides_agent_servers: ActiveValue::Set(
version.provides.contains(&ExtensionProvides::AgentServers),
),
provides_slash_commands: ActiveValue::Set(
version.provides.contains(&ExtensionProvides::SlashCommands),
),
provides_indexed_docs_providers: ActiveValue::Set(
version
.provides
.contains(&ExtensionProvides::IndexedDocsProviders),
),
provides_snippets: ActiveValue::Set(
version.provides.contains(&ExtensionProvides::Snippets),
),
provides_debug_adapters: ActiveValue::Set(
version.provides.contains(&ExtensionProvides::DebugAdapters),
),
download_count: ActiveValue::NotSet,
}
}))
.on_conflict(OnConflict::new().do_nothing().to_owned())
.exec_without_returning(&*tx)
.await?;
if let Ok(db_version) = semver::Version::parse(&extension.latest_version)
&& db_version >= latest_version.version
{
continue;
}
let mut extension = extension.into_active_model();
extension.latest_version = ActiveValue::Set(latest_version.version.to_string());
extension.name = ActiveValue::set(latest_version.name.clone());
extension::Entity::update(extension).exec(&*tx).await?;
}
Ok(())
})
.await
}
pub async fn record_extension_download(&self, extension: &str, version: &str) -> Result<bool> {
self.transaction(|tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryId {
Id,
}
let extension_id: Option<ExtensionId> = extension::Entity::find()
.filter(extension::Column::ExternalId.eq(extension))
.select_only()
.column(extension::Column::Id)
.into_values::<_, QueryId>()
.one(&*tx)
.await?;
let Some(extension_id) = extension_id else {
return Ok(false);
};
extension_version::Entity::update_many()
.col_expr(
extension_version::Column::DownloadCount,
extension_version::Column::DownloadCount.into_expr().add(1),
)
.filter(
extension_version::Column::ExtensionId
.eq(extension_id)
.and(extension_version::Column::Version.eq(version)),
)
.exec(&*tx)
.await?;
extension::Entity::update_many()
.col_expr(
extension::Column::TotalDownloadCount,
extension::Column::TotalDownloadCount.into_expr().add(1),
)
.filter(extension::Column::Id.eq(extension_id))
.exec(&*tx)
.await?;
Ok(true)
})
.await
}
}
fn metadata_from_extension_and_version(
extension: extension::Model,
version: extension_version::Model,
) -> ExtensionMetadata {
let provides = version.provides();
ExtensionMetadata {
id: extension.external_id.into(),
manifest: cloud_api_types::ExtensionApiManifest {
name: extension.name,
version: version.version.into(),
authors: version
.authors
.split(',')
.map(|author| author.trim().to_string())
.collect::<Vec<_>>(),
description: Some(version.description),
repository: version.repository,
schema_version: Some(version.schema_version),
wasm_api_version: version.wasm_api_version,
provides,
},
published_at: convert_time_to_chrono(version.published_at),
download_count: extension.total_download_count as u64,
}
}
pub fn convert_time_to_chrono(time: time::PrimitiveDateTime) -> chrono::DateTime<Utc> {
chrono::DateTime::from_naive_utc_and_offset(
#[allow(deprecated)]
chrono::NaiveDateTime::from_timestamp_opt(time.assume_utc().unix_timestamp(), 0).unwrap(),
Utc,
)
}

View File

@@ -0,0 +1,281 @@
use super::*;
use anyhow::Context as _;
use rpc::Notification;
use util::ResultExt;
impl Database {
/// Initializes the different kinds of notifications by upserting records for them.
pub async fn initialize_notification_kinds(&mut self) -> Result<()> {
let all_kinds = Notification::all_variant_names();
let existing_kinds = notification_kind::Entity::find().all(&self.pool).await?;
let kinds_to_create: Vec<_> = all_kinds
.iter()
.filter(|&kind| {
!existing_kinds
.iter()
.any(|existing| existing.name == **kind)
})
.map(|kind| notification_kind::ActiveModel {
name: ActiveValue::Set((*kind).to_owned()),
..Default::default()
})
.collect();
if !kinds_to_create.is_empty() {
notification_kind::Entity::insert_many(kinds_to_create)
.exec_without_returning(&self.pool)
.await?;
}
let mut rows = notification_kind::Entity::find().stream(&self.pool).await?;
while let Some(row) = rows.next().await {
let row = row?;
self.notification_kinds_by_name.insert(row.name, row.id);
}
for name in Notification::all_variant_names() {
if let Some(id) = self.notification_kinds_by_name.get(*name).copied() {
self.notification_kinds_by_id.insert(id, name);
}
}
Ok(())
}
/// Returns the notifications for the given recipient.
pub async fn get_notifications(
&self,
recipient_id: UserId,
limit: usize,
before_id: Option<NotificationId>,
) -> Result<Vec<proto::Notification>> {
self.transaction(|tx| async move {
let mut result = Vec::new();
let mut condition =
Condition::all().add(notification::Column::RecipientId.eq(recipient_id));
if let Some(before_id) = before_id {
condition = condition.add(notification::Column::Id.lt(before_id));
}
let mut rows = notification::Entity::find()
.filter(condition)
.order_by_desc(notification::Column::Id)
.limit(limit as u64)
.stream(&*tx)
.await?;
while let Some(row) = rows.next().await {
let row = row?;
if let Some(proto) = model_to_proto(self, row).log_err() {
result.push(proto);
}
}
result.reverse();
Ok(result)
})
.await
}
/// Creates a notification. If `avoid_duplicates` is set to true, then avoid
/// creating a new notification if the given recipient already has an
/// unread notification with the given kind and entity id.
pub async fn create_notification(
&self,
recipient_id: UserId,
notification: Notification,
avoid_duplicates: bool,
tx: &DatabaseTransaction,
) -> Result<Option<(UserId, proto::Notification)>> {
if avoid_duplicates
&& self
.find_notification(recipient_id, &notification, tx)
.await?
.is_some()
{
return Ok(None);
}
let proto = notification.to_proto();
let kind = notification_kind_from_proto(self, &proto)?;
let model = notification::ActiveModel {
recipient_id: ActiveValue::Set(recipient_id),
kind: ActiveValue::Set(kind),
entity_id: ActiveValue::Set(proto.entity_id.map(|id| id as i32)),
content: ActiveValue::Set(proto.content.clone()),
..Default::default()
}
.save(tx)
.await?;
Ok(Some((
recipient_id,
proto::Notification {
id: model.id.as_ref().to_proto(),
kind: proto.kind,
timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64,
is_read: false,
response: None,
content: proto.content,
entity_id: proto.entity_id,
},
)))
}
/// Remove an unread notification with the given recipient, kind and
/// entity id.
pub async fn remove_notification(
&self,
recipient_id: UserId,
notification: Notification,
tx: &DatabaseTransaction,
) -> Result<Option<NotificationId>> {
let id = self
.find_notification(recipient_id, &notification, tx)
.await?;
if let Some(id) = id {
notification::Entity::delete_by_id(id).exec(tx).await?;
}
Ok(id)
}
/// Populate the response for the notification with the given kind and
/// entity id.
pub async fn mark_notification_as_read_with_response(
&self,
recipient_id: UserId,
notification: &Notification,
response: bool,
tx: &DatabaseTransaction,
) -> Result<Option<(UserId, proto::Notification)>> {
self.mark_notification_as_read_internal(recipient_id, notification, Some(response), tx)
.await
}
/// Marks the given notification as read.
pub async fn mark_notification_as_read(
&self,
recipient_id: UserId,
notification: &Notification,
tx: &DatabaseTransaction,
) -> Result<Option<(UserId, proto::Notification)>> {
self.mark_notification_as_read_internal(recipient_id, notification, None, tx)
.await
}
/// Marks the notification with the given ID as read.
pub async fn mark_notification_as_read_by_id(
&self,
recipient_id: UserId,
notification_id: NotificationId,
) -> Result<NotificationBatch> {
self.transaction(|tx| async move {
let row = notification::Entity::update(notification::ActiveModel {
id: ActiveValue::Unchanged(notification_id),
recipient_id: ActiveValue::Unchanged(recipient_id),
is_read: ActiveValue::Set(true),
..Default::default()
})
.exec(&*tx)
.await?;
Ok(model_to_proto(self, row)
.map(|notification| (recipient_id, notification))
.into_iter()
.collect())
})
.await
}
async fn mark_notification_as_read_internal(
&self,
recipient_id: UserId,
notification: &Notification,
response: Option<bool>,
tx: &DatabaseTransaction,
) -> Result<Option<(UserId, proto::Notification)>> {
if let Some(id) = self
.find_notification(recipient_id, notification, tx)
.await?
{
let row = notification::Entity::update(notification::ActiveModel {
id: ActiveValue::Unchanged(id),
recipient_id: ActiveValue::Unchanged(recipient_id),
is_read: ActiveValue::Set(true),
response: if let Some(response) = response {
ActiveValue::Set(Some(response))
} else {
ActiveValue::NotSet
},
..Default::default()
})
.exec(tx)
.await?;
Ok(model_to_proto(self, row)
.map(|notification| (recipient_id, notification))
.ok())
} else {
Ok(None)
}
}
/// Find an unread notification by its recipient, kind and entity id.
async fn find_notification(
&self,
recipient_id: UserId,
notification: &Notification,
tx: &DatabaseTransaction,
) -> Result<Option<NotificationId>> {
let proto = notification.to_proto();
let kind = notification_kind_from_proto(self, &proto)?;
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryIds {
Id,
}
Ok(notification::Entity::find()
.select_only()
.column(notification::Column::Id)
.filter(
Condition::all()
.add(notification::Column::RecipientId.eq(recipient_id))
.add(notification::Column::IsRead.eq(false))
.add(notification::Column::Kind.eq(kind))
.add(if proto.entity_id.is_some() {
notification::Column::EntityId.eq(proto.entity_id)
} else {
notification::Column::EntityId.is_null()
}),
)
.into_values::<_, QueryIds>()
.one(tx)
.await?)
}
}
pub fn model_to_proto(this: &Database, row: notification::Model) -> Result<proto::Notification> {
let kind = this
.notification_kinds_by_id
.get(&row.kind)
.context("Unknown notification kind")?;
Ok(proto::Notification {
id: row.id.to_proto(),
kind: (*kind).to_owned(),
timestamp: row.created_at.assume_utc().unix_timestamp() as u64,
is_read: row.is_read,
response: row.response,
content: row.content,
entity_id: row.entity_id.map(|id| id as u64),
})
}
fn notification_kind_from_proto(
this: &Database,
proto: &proto::Notification,
) -> Result<NotificationKindId> {
Ok(this
.notification_kinds_by_name
.get(&proto.kind)
.copied()
.with_context(|| format!("invalid notification kind {:?}", proto.kind))?)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,230 @@
use super::*;
impl Database {
/// Creates a new server in the given environment.
pub async fn create_server(&self, environment: &str) -> Result<ServerId> {
self.transaction(|tx| async move {
let server = server::ActiveModel {
environment: ActiveValue::set(environment.into()),
..Default::default()
}
.insert(&*tx)
.await?;
Ok(server.id)
})
.await
}
/// Returns the IDs of resources associated with stale servers.
///
/// A server is stale if it is in the specified `environment` and does not
/// match the provided `new_server_id`.
pub async fn stale_server_resource_ids(
&self,
environment: &str,
new_server_id: ServerId,
) -> Result<(Vec<RoomId>, Vec<ChannelId>)> {
self.transaction(|tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryRoomIds {
RoomId,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryChannelIds {
ChannelId,
}
let stale_server_epochs = self
.stale_server_ids(environment, new_server_id, &tx)
.await?;
let room_ids = room_participant::Entity::find()
.select_only()
.column(room_participant::Column::RoomId)
.distinct()
.filter(
room_participant::Column::AnsweringConnectionServerId
.is_in(stale_server_epochs.iter().copied()),
)
.into_values::<_, QueryRoomIds>()
.all(&*tx)
.await?;
let channel_ids = channel_buffer_collaborator::Entity::find()
.select_only()
.column(channel_buffer_collaborator::Column::ChannelId)
.distinct()
.filter(
channel_buffer_collaborator::Column::ConnectionServerId
.is_in(stale_server_epochs.iter().copied()),
)
.into_values::<_, QueryChannelIds>()
.all(&*tx)
.await?;
Ok((room_ids, channel_ids))
})
.await
}
/// Delete all channel chat participants from previous servers
pub async fn delete_stale_channel_chat_participants(
&self,
environment: &str,
new_server_id: ServerId,
) -> Result<()> {
self.transaction(|tx| async move {
let stale_server_epochs = self
.stale_server_ids(environment, new_server_id, &tx)
.await?;
channel_chat_participant::Entity::delete_many()
.filter(
channel_chat_participant::Column::ConnectionServerId
.is_in(stale_server_epochs.iter().copied()),
)
.exec(&*tx)
.await?;
Ok(())
})
.await
}
pub async fn clear_old_worktree_entries(&self, server_id: ServerId) -> Result<()> {
self.transaction(|tx| async move {
use sea_orm::Statement;
use sea_orm::sea_query::{Expr, Query};
loop {
let delete_query = Query::delete()
.from_table(worktree_entry::Entity)
.and_where(
Expr::tuple([
Expr::col((worktree_entry::Entity, worktree_entry::Column::ProjectId))
.into(),
Expr::col((worktree_entry::Entity, worktree_entry::Column::WorktreeId))
.into(),
Expr::col((worktree_entry::Entity, worktree_entry::Column::Id)).into(),
])
.in_subquery(
Query::select()
.columns([
(worktree_entry::Entity, worktree_entry::Column::ProjectId),
(worktree_entry::Entity, worktree_entry::Column::WorktreeId),
(worktree_entry::Entity, worktree_entry::Column::Id),
])
.from(worktree_entry::Entity)
.inner_join(
project::Entity,
Expr::col((project::Entity, project::Column::Id)).equals((
worktree_entry::Entity,
worktree_entry::Column::ProjectId,
)),
)
.and_where(project::Column::HostConnectionServerId.ne(server_id))
.limit(10000)
.to_owned(),
),
)
.to_owned();
let statement = Statement::from_sql_and_values(
tx.get_database_backend(),
delete_query
.to_string(sea_orm::sea_query::PostgresQueryBuilder)
.as_str(),
vec![],
);
let result = tx.execute(statement).await?;
if result.rows_affected() == 0 {
break;
}
}
loop {
let delete_query = Query::delete()
.from_table(project_repository_statuses::Entity)
.and_where(
Expr::tuple([Expr::col((
project_repository_statuses::Entity,
project_repository_statuses::Column::ProjectId,
))
.into()])
.in_subquery(
Query::select()
.columns([(
project_repository_statuses::Entity,
project_repository_statuses::Column::ProjectId,
)])
.from(project_repository_statuses::Entity)
.inner_join(
project::Entity,
Expr::col((project::Entity, project::Column::Id)).equals((
project_repository_statuses::Entity,
project_repository_statuses::Column::ProjectId,
)),
)
.and_where(project::Column::HostConnectionServerId.ne(server_id))
.limit(10000)
.to_owned(),
),
)
.to_owned();
let statement = Statement::from_sql_and_values(
tx.get_database_backend(),
delete_query
.to_string(sea_orm::sea_query::PostgresQueryBuilder)
.as_str(),
vec![],
);
let result = tx.execute(statement).await?;
if result.rows_affected() == 0 {
break;
}
}
Ok(())
})
.await
}
/// Deletes any stale servers in the environment that don't match the `new_server_id`.
pub async fn delete_stale_servers(
&self,
environment: &str,
new_server_id: ServerId,
) -> Result<()> {
self.transaction(|tx| async move {
server::Entity::delete_many()
.filter(
Condition::all()
.add(server::Column::Environment.eq(environment))
.add(server::Column::Id.ne(new_server_id)),
)
.exec(&*tx)
.await?;
Ok(())
})
.await
}
pub async fn stale_server_ids(
&self,
environment: &str,
new_server_id: ServerId,
tx: &DatabaseTransaction,
) -> Result<Vec<ServerId>> {
let stale_servers = server::Entity::find()
.filter(
Condition::all()
.add(server::Column::Environment.eq(environment))
.add(server::Column::Id.ne(new_server_id)),
)
.all(tx)
.await?;
Ok(stale_servers.into_iter().map(|server| server.id).collect())
}
}

View File

@@ -0,0 +1,77 @@
use chrono::Utc;
use super::*;
use crate::db::tables::shared_thread;
impl Database {
pub async fn upsert_shared_thread(
&self,
id: SharedThreadId,
user_id: UserId,
title: &str,
data: Vec<u8>,
) -> Result<()> {
let title = title.to_string();
self.transaction(|tx| {
let title = title.clone();
let data = data.clone();
async move {
let now = Utc::now().naive_utc();
let existing = shared_thread::Entity::find_by_id(id).one(&*tx).await?;
match existing {
Some(existing) => {
if existing.user_id != user_id {
Err(anyhow!("Cannot update shared thread owned by another user"))?;
}
let mut active: shared_thread::ActiveModel = existing.into();
active.title = ActiveValue::Set(title);
active.data = ActiveValue::Set(data);
active.updated_at = ActiveValue::Set(now);
active.update(&*tx).await?;
}
None => {
shared_thread::ActiveModel {
id: ActiveValue::Set(id),
user_id: ActiveValue::Set(user_id),
title: ActiveValue::Set(title),
data: ActiveValue::Set(data),
created_at: ActiveValue::Set(now),
updated_at: ActiveValue::Set(now),
}
.insert(&*tx)
.await?;
}
}
Ok(())
}
})
.await
}
pub async fn get_shared_thread(
&self,
share_id: SharedThreadId,
) -> Result<Option<(shared_thread::Model, String)>> {
self.transaction(|tx| async move {
let Some(thread) = shared_thread::Entity::find_by_id(share_id)
.one(&*tx)
.await?
else {
return Ok(None);
};
let user = user::Entity::find_by_id(thread.user_id).one(&*tx).await?;
let username = user
.map(|u| u.github_login)
.unwrap_or_else(|| "Unknown".to_string());
Ok(Some((thread, username)))
})
.await
}
}

View File

@@ -0,0 +1,176 @@
use chrono::NaiveDateTime;
use super::*;
impl Database {
/// Creates a new user.
pub async fn create_user(
&self,
email_address: &str,
name: Option<&str>,
admin: bool,
params: NewUserParams,
) -> Result<NewUserResult> {
self.transaction(|tx| async {
let tx = tx;
let user = user::Entity::insert(user::ActiveModel {
email_address: ActiveValue::set(Some(email_address.into())),
name: ActiveValue::set(name.map(|s| s.into())),
github_login: ActiveValue::set(params.github_login.clone()),
github_user_id: ActiveValue::set(params.github_user_id),
admin: ActiveValue::set(admin),
..Default::default()
})
.on_conflict(
OnConflict::column(user::Column::GithubUserId)
.update_columns([
user::Column::Admin,
user::Column::EmailAddress,
user::Column::GithubLogin,
])
.to_owned(),
)
.exec_with_returning(&*tx)
.await?;
Ok(NewUserResult { user_id: user.id })
})
.await
}
pub async fn update_or_create_user_by_github_account(
&self,
github_login: &str,
github_user_id: i32,
github_email: Option<&str>,
github_name: Option<&str>,
github_user_created_at: DateTimeUtc,
initial_channel_id: Option<ChannelId>,
) -> Result<user::Model> {
self.transaction(|tx| async move {
self.update_or_create_user_by_github_account_tx(
github_login,
github_user_id,
github_email,
github_name,
github_user_created_at.naive_utc(),
initial_channel_id,
&tx,
)
.await
})
.await
}
pub async fn update_or_create_user_by_github_account_tx(
&self,
github_login: &str,
github_user_id: i32,
github_email: Option<&str>,
github_name: Option<&str>,
github_user_created_at: NaiveDateTime,
initial_channel_id: Option<ChannelId>,
tx: &DatabaseTransaction,
) -> Result<user::Model> {
if let Some(existing_user) = self
.get_user_by_github_user_id_or_github_login(github_user_id, github_login, tx)
.await?
{
let mut existing_user = existing_user.into_active_model();
existing_user.github_login = ActiveValue::set(github_login.into());
existing_user.github_user_created_at = ActiveValue::set(Some(github_user_created_at));
if let Some(github_email) = github_email {
existing_user.email_address = ActiveValue::set(Some(github_email.into()));
}
if let Some(github_name) = github_name {
existing_user.name = ActiveValue::set(Some(github_name.into()));
}
Ok(existing_user.update(tx).await?)
} else {
let user = user::Entity::insert(user::ActiveModel {
email_address: ActiveValue::set(github_email.map(|email| email.into())),
name: ActiveValue::set(github_name.map(|name| name.into())),
github_login: ActiveValue::set(github_login.into()),
github_user_id: ActiveValue::set(github_user_id),
github_user_created_at: ActiveValue::set(Some(github_user_created_at)),
admin: ActiveValue::set(false),
..Default::default()
})
.exec_with_returning(tx)
.await?;
if let Some(channel_id) = initial_channel_id {
channel_member::Entity::insert(channel_member::ActiveModel {
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_id),
user_id: ActiveValue::Set(user.id),
accepted: ActiveValue::Set(true),
role: ActiveValue::Set(ChannelRole::Guest),
})
.exec(tx)
.await?;
}
Ok(user)
}
}
/// Tries to retrieve a user, first by their GitHub user ID, and then by their GitHub login.
///
/// Returns `None` if a user is not found with this GitHub user ID or GitHub login.
pub async fn get_user_by_github_user_id_or_github_login(
&self,
github_user_id: i32,
github_login: &str,
tx: &DatabaseTransaction,
) -> Result<Option<user::Model>> {
if let Some(user_by_github_user_id) = user::Entity::find()
.filter(user::Column::GithubUserId.eq(github_user_id))
.one(tx)
.await?
{
return Ok(Some(user_by_github_user_id));
}
if let Some(user_by_github_login) = user::Entity::find()
.filter(user::Column::GithubLogin.eq(github_login))
.one(tx)
.await?
{
return Ok(Some(user_by_github_login));
}
Ok(None)
}
/// get_all_users returns the next page of users. To get more call again with
/// the same limit and the page incremented by 1.
pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<user::Model>> {
self.transaction(|tx| async move {
Ok(user::Entity::find()
.order_by_asc(user::Column::GithubLogin)
.limit(limit as u64)
.offset(page as u64 * limit as u64)
.all(&*tx)
.await?)
})
.await
}
/// Sets "connected_once" on the user for analytics.
pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
self.transaction(|tx| async move {
user::Entity::update_many()
.filter(user::Column::Id.eq(id))
.set(user::ActiveModel {
connected_once: ActiveValue::set(connected_once),
..Default::default()
})
.exec(&*tx)
.await?;
Ok(())
})
.await
}
}

View File

@@ -0,0 +1,29 @@
pub mod buffer;
pub mod buffer_operation;
pub mod buffer_snapshot;
pub mod channel;
pub mod channel_buffer_collaborator;
pub mod channel_chat_participant;
pub mod channel_member;
pub mod contact;
pub mod contributor;
pub mod extension;
pub mod extension_version;
pub mod follower;
pub mod language_server;
pub mod notification;
pub mod notification_kind;
pub mod observed_buffer_edits;
pub mod project;
pub mod project_collaborator;
pub mod project_repository;
pub mod project_repository_statuses;
pub mod room;
pub mod room_participant;
pub mod server;
pub mod shared_thread;
pub mod user;
pub mod worktree;
pub mod worktree_diagnostic_summary;
pub mod worktree_entry;
pub mod worktree_settings_file;

View File

@@ -0,0 +1,48 @@
use crate::db::{BufferId, ChannelId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "buffers")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: BufferId,
pub epoch: i32,
pub channel_id: ChannelId,
pub latest_operation_epoch: Option<i32>,
pub latest_operation_lamport_timestamp: Option<i32>,
pub latest_operation_replica_id: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::buffer_operation::Entity")]
Operations,
#[sea_orm(has_many = "super::buffer_snapshot::Entity")]
Snapshots,
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
}
impl Related<super::buffer_operation::Entity> for Entity {
fn to() -> RelationDef {
Relation::Operations.def()
}
}
impl Related<super::buffer_snapshot::Entity> for Entity {
fn to() -> RelationDef {
Relation::Snapshots.def()
}
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,34 @@
use crate::db::BufferId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "buffer_operations")]
pub struct Model {
#[sea_orm(primary_key)]
pub buffer_id: BufferId,
#[sea_orm(primary_key)]
pub epoch: i32,
#[sea_orm(primary_key)]
pub lamport_timestamp: i32,
#[sea_orm(primary_key)]
pub replica_id: i32,
pub value: Vec<u8>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::buffer::Entity",
from = "Column::BufferId",
to = "super::buffer::Column::Id"
)]
Buffer,
}
impl Related<super::buffer::Entity> for Entity {
fn to() -> RelationDef {
Relation::Buffer.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,31 @@
use crate::db::BufferId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "buffer_snapshots")]
pub struct Model {
#[sea_orm(primary_key)]
pub buffer_id: BufferId,
#[sea_orm(primary_key)]
pub epoch: i32,
pub text: String,
pub operation_serialization_version: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::buffer::Entity",
from = "Column::BufferId",
to = "super::buffer::Column::Id"
)]
Buffer,
}
impl Related<super::buffer::Entity> for Entity {
fn to() -> RelationDef {
Relation::Buffer.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,95 @@
use crate::db::{ChannelId, ChannelVisibility};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channels")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ChannelId,
pub name: String,
pub visibility: ChannelVisibility,
pub parent_path: String,
pub requires_zed_cla: bool,
/// The order of this channel relative to its siblings within the same parent.
/// Lower values appear first. Channels are sorted by parent_path first, then by channel_order.
pub channel_order: i32,
}
impl Model {
pub fn parent_id(&self) -> Option<ChannelId> {
self.ancestors().last()
}
pub fn is_root(&self) -> bool {
self.parent_path.is_empty()
}
pub fn root_id(&self) -> ChannelId {
self.ancestors().next().unwrap_or(self.id)
}
pub fn ancestors(&self) -> impl Iterator<Item = ChannelId> + '_ {
self.parent_path
.trim_end_matches('/')
.split('/')
.filter_map(|id| Some(ChannelId::from_proto(id.parse().ok()?)))
}
pub fn ancestors_including_self(&self) -> impl Iterator<Item = ChannelId> + '_ {
self.ancestors().chain(Some(self.id))
}
pub fn path(&self) -> String {
format!("{}{}/", self.parent_path, self.id)
}
pub fn descendant_path_filter(&self) -> String {
format!("{}{}/%", self.parent_path, self.id)
}
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_one = "super::room::Entity")]
Room,
#[sea_orm(has_one = "super::buffer::Entity")]
Buffer,
#[sea_orm(has_many = "super::channel_member::Entity")]
Member,
#[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")]
BufferCollaborators,
#[sea_orm(has_many = "super::channel_chat_participant::Entity")]
ChatParticipants,
}
impl Related<super::channel_member::Entity> for Entity {
fn to() -> RelationDef {
Relation::Member.def()
}
}
impl Related<super::room::Entity> for Entity {
fn to() -> RelationDef {
Relation::Room.def()
}
}
impl Related<super::buffer::Entity> for Entity {
fn to() -> RelationDef {
Relation::Buffer.def()
}
}
impl Related<super::channel_buffer_collaborator::Entity> for Entity {
fn to() -> RelationDef {
Relation::BufferCollaborators.def()
}
}
impl Related<super::channel_chat_participant::Entity> for Entity {
fn to() -> RelationDef {
Relation::ChatParticipants.def()
}
}

View File

@@ -0,0 +1,43 @@
use crate::db::{ChannelBufferCollaboratorId, ChannelId, ReplicaId, ServerId, UserId};
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channel_buffer_collaborators")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ChannelBufferCollaboratorId,
pub channel_id: ChannelId,
pub connection_id: i32,
pub connection_server_id: ServerId,
pub connection_lost: bool,
pub user_id: UserId,
pub replica_id: ReplicaId,
}
impl Model {
pub fn connection(&self) -> ConnectionId {
ConnectionId {
owner_id: self.connection_server_id.0 as u32,
id: self.connection_id as u32,
}
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,41 @@
use crate::db::{ChannelChatParticipantId, ChannelId, ServerId, UserId};
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channel_chat_participants")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ChannelChatParticipantId,
pub channel_id: ChannelId,
pub user_id: UserId,
pub connection_id: i32,
pub connection_server_id: ServerId,
}
impl Model {
pub fn connection(&self) -> ConnectionId {
ConnectionId {
owner_id: self.connection_server_id.0 as u32,
id: self.connection_id as u32,
}
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,75 @@
use crate::db::{ChannelId, ChannelMemberId, ChannelRole, UserId, channel_member};
use rpc::proto;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channel_members")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ChannelMemberId,
pub channel_id: ChannelId,
pub user_id: UserId,
pub accepted: bool,
pub role: ChannelRole,
}
impl From<Model> for proto::ChannelMember {
fn from(member: Model) -> Self {
Self {
role: member.role.into(),
user_id: member.user_id.to_proto(),
kind: if member.accepted {
proto::channel_member::Kind::Member
} else {
proto::channel_member::Kind::Invitee
}
.into(),
}
}
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
#[derive(Debug)]
pub struct UserToChannel;
impl Linked for UserToChannel {
type FromEntity = super::user::Entity;
type ToEntity = super::channel::Entity;
fn link(&self) -> Vec<RelationDef> {
vec![
channel_member::Relation::User.def().rev(),
channel_member::Relation::Channel.def(),
]
}
}

View File

@@ -0,0 +1,32 @@
use crate::db::{ContactId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "contacts")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ContactId,
pub user_id_a: UserId,
pub user_id_b: UserId,
pub a_to_b: bool,
pub should_notify: bool,
pub accepted: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::room_participant::Entity",
from = "Column::UserIdA",
to = "super::room_participant::Column::UserId"
)]
UserARoomParticipant,
#[sea_orm(
belongs_to = "super::room_participant::Entity",
from = "Column::UserIdB",
to = "super::room_participant::Column::UserId"
)]
UserBRoomParticipant,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,30 @@
use crate::db::UserId;
use sea_orm::entity::prelude::*;
use serde::Serialize;
/// A user who has signed the CLA.
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "contributors")]
pub struct Model {
#[sea_orm(primary_key)]
pub user_id: UserId,
pub signed_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl ActiveModelBehavior for ActiveModel {}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}

View File

@@ -0,0 +1,27 @@
use crate::db::ExtensionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "extensions")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ExtensionId,
pub external_id: String,
pub name: String,
pub latest_version: String,
pub total_download_count: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_one = "super::extension_version::Entity")]
LatestVersion,
}
impl Related<super::extension_version::Entity> for Entity {
fn to() -> RelationDef {
Relation::LatestVersion.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,102 @@
use crate::db::ExtensionId;
use cloud_api_types::ExtensionProvides;
use collections::BTreeSet;
use sea_orm::entity::prelude::*;
use time::PrimitiveDateTime;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "extension_versions")]
pub struct Model {
#[sea_orm(primary_key)]
pub extension_id: ExtensionId,
#[sea_orm(primary_key)]
pub version: String,
pub published_at: PrimitiveDateTime,
pub authors: String,
pub repository: String,
pub description: String,
pub schema_version: i32,
pub wasm_api_version: Option<String>,
pub download_count: i64,
pub provides_themes: bool,
pub provides_icon_themes: bool,
pub provides_languages: bool,
pub provides_grammars: bool,
pub provides_language_servers: bool,
pub provides_context_servers: bool,
pub provides_agent_servers: bool,
pub provides_slash_commands: bool,
pub provides_indexed_docs_providers: bool,
pub provides_snippets: bool,
pub provides_debug_adapters: bool,
}
impl Model {
pub fn provides(&self) -> BTreeSet<ExtensionProvides> {
let mut provides = BTreeSet::default();
if self.provides_themes {
provides.insert(ExtensionProvides::Themes);
}
if self.provides_icon_themes {
provides.insert(ExtensionProvides::IconThemes);
}
if self.provides_languages {
provides.insert(ExtensionProvides::Languages);
}
if self.provides_grammars {
provides.insert(ExtensionProvides::Grammars);
}
if self.provides_language_servers {
provides.insert(ExtensionProvides::LanguageServers);
}
if self.provides_context_servers {
provides.insert(ExtensionProvides::ContextServers);
}
if self.provides_agent_servers {
provides.insert(ExtensionProvides::AgentServers);
}
if self.provides_slash_commands {
provides.insert(ExtensionProvides::SlashCommands);
}
if self.provides_indexed_docs_providers {
provides.insert(ExtensionProvides::IndexedDocsProviders);
}
if self.provides_snippets {
provides.insert(ExtensionProvides::Snippets);
}
if self.provides_debug_adapters {
provides.insert(ExtensionProvides::DebugAdapters);
}
provides
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::extension::Entity",
from = "Column::ExtensionId",
to = "super::extension::Column::Id"
on_condition = r#"super::extension::Column::LatestVersion.into_expr().eq(Column::Version.into_expr())"#
)]
Extension,
}
impl Related<super::extension::Entity> for Entity {
fn to() -> RelationDef {
Relation::Extension.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,50 @@
use crate::db::{FollowerId, ProjectId, RoomId, ServerId};
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "followers")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: FollowerId,
pub room_id: RoomId,
pub project_id: ProjectId,
pub leader_connection_server_id: ServerId,
pub leader_connection_id: i32,
pub follower_connection_server_id: ServerId,
pub follower_connection_id: i32,
}
impl Model {
pub fn leader_connection(&self) -> ConnectionId {
ConnectionId {
owner_id: self.leader_connection_server_id.0 as u32,
id: self.leader_connection_id as u32,
}
}
pub fn follower_connection(&self) -> ConnectionId {
ConnectionId {
owner_id: self.follower_connection_server_id.0 as u32,
id: self.follower_connection_id as u32,
}
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::room::Entity",
from = "Column::RoomId",
to = "super::room::Column::Id"
)]
Room,
}
impl Related<super::room::Entity> for Entity {
fn to() -> RelationDef {
Relation::Room.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,32 @@
use crate::db::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "language_servers")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub id: i64,
pub name: String,
pub capabilities: String,
pub worktree_id: Option<i64>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::project::Entity",
from = "Column::ProjectId",
to = "super::project::Column::Id"
)]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,29 @@
use crate::db::{NotificationId, NotificationKindId, UserId};
use sea_orm::entity::prelude::*;
use time::PrimitiveDateTime;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "notifications")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: NotificationId,
pub created_at: PrimitiveDateTime,
pub recipient_id: UserId,
pub kind: NotificationKindId,
pub entity_id: Option<i32>,
pub content: String,
pub is_read: bool,
pub response: Option<bool>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::RecipientId",
to = "super::user::Column::Id"
)]
Recipient,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,15 @@
use crate::db::NotificationKindId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "notification_kinds")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: NotificationKindId,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,43 @@
use crate::db::{BufferId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "observed_buffer_edits")]
pub struct Model {
#[sea_orm(primary_key)]
pub user_id: UserId,
pub buffer_id: BufferId,
pub epoch: i32,
pub lamport_timestamp: i32,
pub replica_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::buffer::Entity",
from = "Column::BufferId",
to = "super::buffer::Column::Id"
)]
Buffer,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::buffer::Entity> for Entity {
fn to() -> RelationDef {
Relation::Buffer.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,94 @@
use crate::db::{ProjectId, Result, RoomId, ServerId, UserId};
use anyhow::Context as _;
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "projects")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ProjectId,
pub room_id: Option<RoomId>,
pub host_user_id: Option<UserId>,
pub host_connection_id: Option<i32>,
pub host_connection_server_id: Option<ServerId>,
pub windows_paths: bool,
pub features: String,
}
impl Model {
pub fn host_connection(&self) -> Result<ConnectionId> {
let host_connection_server_id = self
.host_connection_server_id
.context("empty host_connection_server_id")?;
let host_connection_id = self
.host_connection_id
.context("empty host_connection_id")?;
Ok(ConnectionId {
owner_id: host_connection_server_id.0 as u32,
id: host_connection_id as u32,
})
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::HostUserId",
to = "super::user::Column::Id"
)]
HostUser,
#[sea_orm(
belongs_to = "super::room::Entity",
from = "Column::RoomId",
to = "super::room::Column::Id"
)]
Room,
#[sea_orm(has_many = "super::worktree::Entity")]
Worktrees,
#[sea_orm(has_many = "super::project_repository::Entity")]
Repositories,
#[sea_orm(has_many = "super::project_collaborator::Entity")]
Collaborators,
#[sea_orm(has_many = "super::language_server::Entity")]
LanguageServers,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::HostUser.def()
}
}
impl Related<super::room::Entity> for Entity {
fn to() -> RelationDef {
Relation::Room.def()
}
}
impl Related<super::worktree::Entity> for Entity {
fn to() -> RelationDef {
Relation::Worktrees.def()
}
}
impl Related<super::project_repository::Entity> for Entity {
fn to() -> RelationDef {
Relation::Repositories.def()
}
}
impl Related<super::project_collaborator::Entity> for Entity {
fn to() -> RelationDef {
Relation::Collaborators.def()
}
}
impl Related<super::language_server::Entity> for Entity {
fn to() -> RelationDef {
Relation::LanguageServers.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,45 @@
use crate::db::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId};
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "project_collaborators")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ProjectCollaboratorId,
pub project_id: ProjectId,
pub connection_id: i32,
pub connection_server_id: ServerId,
pub user_id: UserId,
pub replica_id: ReplicaId,
pub is_host: bool,
pub committer_name: Option<String>,
pub committer_email: Option<String>,
}
impl Model {
pub fn connection(&self) -> ConnectionId {
ConnectionId {
owner_id: self.connection_server_id.0 as u32,
id: self.connection_id as u32,
}
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::project::Entity",
from = "Column::ProjectId",
to = "super::project::Column::Id"
)]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,49 @@
use crate::db::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "project_repositories")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub id: i64,
pub abs_path: String,
pub legacy_worktree_id: Option<i64>,
// JSON array containing 1 or more integer project entry ids
pub entry_ids: String,
pub scan_id: i64,
pub is_deleted: bool,
// JSON array typed string
pub current_merge_conflicts: Option<String>,
// The suggested merge commit message
pub merge_message: Option<String>,
// A JSON object representing the current Branch values
pub branch_summary: Option<String>,
// A JSON object representing the current Head commit values
pub head_commit_details: Option<String>,
pub remote_upstream_url: Option<String>,
pub remote_origin_url: Option<String>,
pub repository_dir_abs_path: Option<String>,
pub common_dir_abs_path: Option<String>,
// JSON array of linked worktree objects
pub linked_worktrees: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::project::Entity",
from = "Column::ProjectId",
to = "super::project::Column::Id"
)]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,38 @@
use crate::db::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "project_repository_statuses")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub repository_id: i64,
#[sea_orm(primary_key)]
pub repo_path: String,
/// Old single-code status field, no longer used but kept here to mirror the DB schema.
pub status: i64,
pub status_kind: StatusKind,
/// For unmerged entries, this is the `first_head` status. For tracked entries, this is the `index_status`.
pub first_status: Option<i32>,
/// For unmerged entries, this is the `second_head` status. For tracked entries, this is the `worktree_status`.
pub second_status: Option<i32>,
pub lines_added: Option<i32>,
pub lines_deleted: Option<i32>,
pub scan_id: i64,
pub is_deleted: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "i32", db_type = "Integer")]
pub enum StatusKind {
Untracked = 0,
Ignored = 1,
Unmerged = 2,
Tracked = 3,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,53 @@
use crate::db::{ChannelId, RoomId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Default, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "rooms")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: RoomId,
pub live_kit_room: String,
pub channel_id: Option<ChannelId>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::room_participant::Entity")]
RoomParticipant,
#[sea_orm(has_many = "super::project::Entity")]
Project,
#[sea_orm(has_many = "super::follower::Entity")]
Follower,
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
}
impl Related<super::room_participant::Entity> for Entity {
fn to() -> RelationDef {
Relation::RoomParticipant.def()
}
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
impl Related<super::follower::Entity> for Entity {
fn to() -> RelationDef {
Relation::Follower.def()
}
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,62 @@
use crate::db::{ChannelRole, ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "room_participants")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: RoomParticipantId,
pub room_id: RoomId,
pub user_id: UserId,
pub answering_connection_id: Option<i32>,
pub answering_connection_server_id: Option<ServerId>,
pub answering_connection_lost: bool,
pub location_kind: Option<i32>,
pub location_project_id: Option<ProjectId>,
pub initial_project_id: Option<ProjectId>,
pub calling_user_id: UserId,
pub calling_connection_id: i32,
pub calling_connection_server_id: Option<ServerId>,
pub participant_index: Option<i32>,
pub role: Option<ChannelRole>,
}
impl Model {
pub fn answering_connection(&self) -> Option<ConnectionId> {
Some(ConnectionId {
owner_id: self.answering_connection_server_id?.0 as u32,
id: self.answering_connection_id? as u32,
})
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
#[sea_orm(
belongs_to = "super::room::Entity",
from = "Column::RoomId",
to = "super::room::Column::Id"
)]
Room,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl Related<super::room::Entity> for Entity {
fn to() -> RelationDef {
Relation::Room.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,15 @@
use crate::db::ServerId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "servers")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ServerId,
pub environment: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,32 @@
use crate::db::{SharedThreadId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "shared_threads")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: SharedThreadId,
pub user_id: UserId,
pub title: String,
pub data: Vec<u8>,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,50 @@
use crate::db::UserId;
use chrono::NaiveDateTime;
use sea_orm::entity::prelude::*;
use serde::Serialize;
/// A user model.
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: UserId,
pub github_login: String,
pub github_user_id: i32,
pub github_user_created_at: Option<NaiveDateTime>,
pub email_address: Option<String>,
pub name: Option<String>,
pub admin: bool,
pub connected_once: bool,
pub created_at: NaiveDateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_one = "super::room_participant::Entity")]
RoomParticipant,
#[sea_orm(has_many = "super::project::Entity")]
HostedProjects,
#[sea_orm(has_many = "super::channel_member::Entity")]
ChannelMemberships,
}
impl Related<super::room_participant::Entity> for Entity {
fn to() -> RelationDef {
Relation::RoomParticipant.def()
}
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::HostedProjects.def()
}
}
impl Related<super::channel_member::Entity> for Entity {
fn to() -> RelationDef {
Relation::ChannelMemberships.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,37 @@
use crate::db::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktrees")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
#[sea_orm(primary_key)]
pub project_id: ProjectId,
pub abs_path: String,
pub root_name: String,
pub visible: bool,
/// The last scan for which we've observed entries. It may be in progress.
pub scan_id: i64,
/// The last scan that fully completed.
pub completed_scan_id: i64,
pub root_repo_common_dir: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::project::Entity",
from = "Column::ProjectId",
to = "super::project::Column::Id"
)]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,21 @@
use crate::db::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_diagnostic_summaries")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub path: String,
pub language_server_id: i64,
pub error_count: i32,
pub warning_count: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,31 @@
use crate::db::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_entries")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub id: i64,
pub is_dir: bool,
pub path: String,
pub inode: i64,
pub mtime_seconds: i64,
pub mtime_nanos: i32,
pub git_status: Option<i64>,
pub is_ignored: bool,
pub is_external: bool,
pub is_deleted: bool,
pub is_hidden: bool,
pub scan_id: i64,
pub is_fifo: bool,
pub canonical_path: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,38 @@
use crate::db::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_settings_files")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub path: String,
pub content: String,
pub kind: LocalSettingsKind,
pub outside_worktree: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
#[derive(
Copy, Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Default, Hash, serde::Serialize,
)]
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
#[serde(rename_all = "snake_case")]
pub enum LocalSettingsKind {
#[default]
#[sea_orm(string_value = "settings")]
Settings,
#[sea_orm(string_value = "tasks")]
Tasks,
#[sea_orm(string_value = "editorconfig")]
Editorconfig,
#[sea_orm(string_value = "debug")]
Debug,
}

View File

@@ -0,0 +1,3 @@
mod user;
pub use user::*;

View File

@@ -0,0 +1,11 @@
use crate::db::UserId;
#[derive(Debug, Clone)]
pub struct User {
pub id: UserId,
pub github_login: String,
pub avatar_url: String,
pub name: Option<String>,
pub admin: bool,
pub connected_once: bool,
}

41
crates/collab/src/env.rs Normal file
View File

@@ -0,0 +1,41 @@
use anyhow::{Context as _, Result};
use std::fs;
use std::path::Path;
pub fn get_dotenv_vars(current_dir: impl AsRef<Path>) -> Result<Vec<(String, String)>> {
let current_dir = current_dir.as_ref();
let mut vars = Vec::new();
let env_content =
fs::read_to_string(current_dir.join(".env.toml")).context("no .env.toml file found")?;
add_vars(env_content, &mut vars)?;
if let Ok(secret_content) = fs::read_to_string(current_dir.join(".env.secret.toml")) {
add_vars(secret_content, &mut vars)?;
}
Ok(vars)
}
pub fn load_dotenv() -> Result<()> {
for (key, value) in get_dotenv_vars("./crates/collab")? {
unsafe { std::env::set_var(key, value) };
}
Ok(())
}
fn add_vars(env_content: String, vars: &mut Vec<(String, String)>) -> Result<()> {
let env: toml::map::Map<String, toml::Value> = toml::de::from_str(&env_content)?;
for (key, value) in env {
let value = match value {
toml::Value::String(value) => value,
toml::Value::Integer(value) => value.to_string(),
toml::Value::Float(value) => value.to_string(),
toml::Value::Boolean(value) => value.to_string(),
_ => panic!("unsupported TOML value in .env.toml for key {}", key),
};
vars.push((key, value));
}
Ok(())
}

View File

@@ -0,0 +1,29 @@
// Allow tide Results to accept context like other Results do when
// using anyhow.
pub trait TideResultExt {
fn context<C>(self, cx: C) -> Self
where
C: std::fmt::Display + Send + Sync + 'static;
fn with_context<C, F>(self, f: F) -> Self
where
C: std::fmt::Display + Send + Sync + 'static,
F: FnOnce() -> C;
}
impl<T> TideResultExt for tide::Result<T> {
fn context<C>(self, cx: C) -> Self
where
C: std::fmt::Display + Send + Sync + 'static,
{
self.map_err(|e| tide::Error::new(e.status(), e.into_inner().context(cx)))
}
fn with_context<C, F>(self, f: F) -> Self
where
C: std::fmt::Display + Send + Sync + 'static,
F: FnOnce() -> C,
{
self.map_err(|e| tide::Error::new(e.status(), e.into_inner().context(f())))
}
}

View File

@@ -0,0 +1,39 @@
use std::{future::Future, time::Duration};
#[cfg(feature = "test-support")]
use gpui::BackgroundExecutor;
#[derive(Clone)]
pub enum Executor {
Production,
#[cfg(feature = "test-support")]
Deterministic(BackgroundExecutor),
}
impl Executor {
pub fn spawn_detached<F>(&self, future: F)
where
F: 'static + Send + Future<Output = ()>,
{
match self {
Executor::Production => {
tokio::spawn(future);
}
#[cfg(feature = "test-support")]
Executor::Deterministic(background) => {
background.spawn(future).detach();
}
}
}
pub fn sleep(&self, duration: Duration) -> impl Future<Output = ()> + use<> {
let this = self.clone();
async move {
match this {
Executor::Production => tokio::time::sleep(duration).await,
#[cfg(feature = "test-support")]
Executor::Deterministic(background) => background.timer(duration).await,
}
}
}
}

339
crates/collab/src/lib.rs Normal file
View File

@@ -0,0 +1,339 @@
pub mod api;
pub mod auth;
pub mod db;
pub mod entities;
pub mod env;
pub mod executor;
pub mod rpc;
pub mod seed;
pub mod services;
use anyhow::Context as _;
use aws_config::{BehaviorVersion, Region};
use axum::{
http::{HeaderMap, StatusCode},
response::IntoResponse,
};
use db::Database;
use executor::Executor;
use serde::Deserialize;
use std::{path::PathBuf, sync::Arc};
use util::ResultExt;
use crate::services::{CloudUserService, UserService};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub enum Error {
Http(StatusCode, String, HeaderMap),
Database(sea_orm::error::DbErr),
Internal(anyhow::Error),
}
impl From<anyhow::Error> for Error {
fn from(error: anyhow::Error) -> Self {
Self::Internal(error)
}
}
impl From<sea_orm::error::DbErr> for Error {
fn from(error: sea_orm::error::DbErr) -> Self {
Self::Database(error)
}
}
impl From<axum::Error> for Error {
fn from(error: axum::Error) -> Self {
Self::Internal(error.into())
}
}
impl From<axum::http::Error> for Error {
fn from(error: axum::http::Error) -> Self {
Self::Internal(error.into())
}
}
impl From<serde_json::Error> for Error {
fn from(error: serde_json::Error) -> Self {
Self::Internal(error.into())
}
}
impl Error {
fn http(code: StatusCode, message: String) -> Self {
Self::Http(code, message, HeaderMap::default())
}
}
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
match self {
Error::Http(code, message, headers) => {
log::error!("HTTP error {}: {}", code, &message);
(code, headers, message).into_response()
}
Error::Database(error) => {
log::error!(
"HTTP error {}: {:?}",
StatusCode::INTERNAL_SERVER_ERROR,
&error
);
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
}
Error::Internal(error) => {
log::error!(
"HTTP error {}: {:?}",
StatusCode::INTERNAL_SERVER_ERROR,
&error
);
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
}
}
}
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Http(code, message, _headers) => (code, message).fmt(f),
Error::Database(error) => error.fmt(f),
Error::Internal(error) => error.fmt(f),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Http(code, message, _) => write!(f, "{code}: {message}"),
Error::Database(error) => error.fmt(f),
Error::Internal(error) => error.fmt(f),
}
}
}
impl std::error::Error for Error {}
#[derive(Clone, Deserialize)]
pub struct Config {
pub http_port: u16,
pub database_url: String,
pub seed_path: Option<PathBuf>,
pub database_max_connections: u32,
pub livekit_server: Option<String>,
pub livekit_key: Option<String>,
pub livekit_secret: Option<String>,
pub rust_log: Option<String>,
pub log_json: Option<bool>,
pub blob_store_url: Option<String>,
pub blob_store_region: Option<String>,
pub blob_store_access_key: Option<String>,
pub blob_store_secret_key: Option<String>,
pub blob_store_bucket: Option<String>,
pub kinesis_region: Option<String>,
pub kinesis_stream: Option<String>,
pub kinesis_access_key: Option<String>,
pub kinesis_secret_key: Option<String>,
pub zed_environment: Arc<str>,
pub zed_cloud_internal_api_key: String,
pub zed_client_checksum_seed: Option<String>,
}
impl Config {
pub fn is_development(&self) -> bool {
self.zed_environment == "development".into()
}
/// Returns the base `zed.dev` URL.
pub fn zed_dot_dev_url(&self) -> &str {
match self.zed_environment.as_ref() {
"development" => "http://localhost:3000",
"staging" => "https://staging.zed.dev",
_ => "https://zed.dev",
}
}
/// Returns the base Zed Cloud URL.
pub fn zed_cloud_url(&self) -> &str {
match self.zed_environment.as_ref() {
"development" => "http://localhost:8787",
_ => "https://cloud.zed.dev",
}
}
#[cfg(feature = "test-support")]
pub fn test() -> Self {
Self {
http_port: 0,
database_url: "".into(),
database_max_connections: 0,
livekit_server: None,
livekit_key: None,
livekit_secret: None,
rust_log: None,
log_json: None,
zed_environment: "test".into(),
zed_cloud_internal_api_key: "test-internal-api-key".into(),
blob_store_url: None,
blob_store_region: None,
blob_store_access_key: None,
blob_store_secret_key: None,
blob_store_bucket: None,
zed_client_checksum_seed: None,
seed_path: None,
kinesis_region: None,
kinesis_access_key: None,
kinesis_secret_key: None,
kinesis_stream: None,
}
}
}
/// The service mode that collab should run in.
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum ServiceMode {
Api,
Collab,
All,
}
impl ServiceMode {
pub fn is_collab(&self) -> bool {
matches!(self, Self::Collab | Self::All)
}
pub fn is_api(&self) -> bool {
matches!(self, Self::Api | Self::All)
}
}
pub struct AppState {
pub db: Arc<Database>,
pub http_client: Option<reqwest::Client>,
pub livekit_client: Option<Arc<dyn livekit_api::Client>>,
pub blob_store_client: Option<aws_sdk_s3::Client>,
pub executor: Executor,
pub kinesis_client: Option<::aws_sdk_kinesis::Client>,
pub user_service: Arc<dyn UserService>,
pub config: Config,
}
impl AppState {
pub async fn new(config: Config, executor: Executor) -> Result<Arc<Self>> {
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
db_options.max_connections(config.database_max_connections);
let mut db = Database::new(db_options).await?;
db.initialize_notification_kinds().await?;
let livekit_client = if let Some(((server, key), secret)) = config
.livekit_server
.as_ref()
.zip(config.livekit_key.as_ref())
.zip(config.livekit_secret.as_ref())
{
Some(Arc::new(livekit_api::LiveKitClient::new(
server.clone(),
key.clone(),
secret.clone(),
)) as Arc<dyn livekit_api::Client>)
} else {
None
};
let user_agent = format!("Collab/{VERSION} ({})", REVISION.unwrap_or("unknown"));
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
.build()
.context("failed to construct HTTP client")?;
let db = Arc::new(db);
let this = Self {
db: db.clone(),
http_client: Some(http_client.clone()),
livekit_client,
blob_store_client: build_blob_store_client(&config).await.log_err(),
executor,
kinesis_client: if config.kinesis_access_key.is_some() {
build_kinesis_client(&config).await.log_err()
} else {
None
},
user_service: Arc::new(CloudUserService::new(
http_client,
config.zed_cloud_url().to_string(),
config.zed_cloud_internal_api_key.clone(),
)),
config,
};
Ok(Arc::new(this))
}
}
async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::Client> {
let keys = aws_sdk_s3::config::Credentials::new(
config
.blob_store_access_key
.clone()
.context("missing blob_store_access_key")?,
config
.blob_store_secret_key
.clone()
.context("missing blob_store_secret_key")?,
None,
None,
"env",
);
let s3_config = aws_config::defaults(BehaviorVersion::latest())
.endpoint_url(
config
.blob_store_url
.as_ref()
.context("missing blob_store_url")?,
)
.region(Region::new(
config
.blob_store_region
.clone()
.context("missing blob_store_region")?,
))
.credentials_provider(keys)
.load()
.await;
Ok(aws_sdk_s3::Client::new(&s3_config))
}
async fn build_kinesis_client(config: &Config) -> anyhow::Result<aws_sdk_kinesis::Client> {
let keys = aws_sdk_s3::config::Credentials::new(
config
.kinesis_access_key
.clone()
.context("missing kinesis_access_key")?,
config
.kinesis_secret_key
.clone()
.context("missing kinesis_secret_key")?,
None,
None,
"env",
);
let kinesis_config = aws_config::defaults(BehaviorVersion::latest())
.region(Region::new(
config
.kinesis_region
.clone()
.context("missing kinesis_region")?,
))
.credentials_provider(keys)
.load()
.await;
Ok(aws_sdk_kinesis::Client::new(&kinesis_config))
}

268
crates/collab/src/main.rs Normal file
View File

@@ -0,0 +1,268 @@
use anyhow::anyhow;
use axum::headers::HeaderMapExt;
use axum::{
Extension, Router,
extract::MatchedPath,
http::{Request, Response},
routing::get,
};
use collab::api::CloudflareIpCountryHeader;
use collab::{
AppState, Config, Result, api::fetch_extensions_from_blob_store_periodically, db, env,
executor::Executor,
};
use collab::{REVISION, ServiceMode, VERSION};
use db::Database;
use std::{
env::args,
net::{SocketAddr, TcpListener},
sync::Arc,
time::Duration,
};
#[cfg(unix)]
use tokio::signal::unix::SignalKind;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{
Layer, filter::EnvFilter, fmt::format::JsonFields, util::SubscriberInitExt,
};
use util::ResultExt as _;
#[expect(clippy::result_large_err)]
#[tokio::main]
async fn main() -> Result<()> {
if let Err(error) = env::load_dotenv() {
eprintln!(
"error loading .env.toml (this is expected in production): {}",
error
);
}
let mut args = args().skip(1);
match args.next().as_deref() {
Some("version") => {
println!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown"));
}
Some("seed") => {
let config = envy::from_env::<Config>().expect("error loading config");
let db_options = db::ConnectOptions::new(config.database_url.clone());
let mut db = Database::new(db_options).await?;
db.initialize_notification_kinds().await?;
collab::seed::seed(&config, &db, false).await?;
}
Some("serve") => {
let mode = match args.next().as_deref() {
Some("collab") => ServiceMode::Collab,
Some("api") => ServiceMode::Api,
Some("all") => ServiceMode::All,
_ => {
return Err(anyhow!(
"usage: collab <version | seed | serve <api|collab|all>>"
))?;
}
};
let config = envy::from_env::<Config>().expect("error loading config");
init_tracing(&config);
init_panic_hook();
let mut app = Router::new()
.route("/", get(handle_root))
.route("/healthz", get(handle_liveness_probe))
.layer(Extension(mode));
let listener = TcpListener::bind(format!("0.0.0.0:{}", config.http_port))
.expect("failed to bind TCP listener");
let mut on_shutdown = None;
if mode.is_collab() || mode.is_api() {
setup_app_database(&config).await?;
let state = AppState::new(config, Executor::Production).await?;
if mode.is_collab() {
let epoch = state
.db
.create_server(&state.config.zed_environment)
.await?;
let rpc_server = collab::rpc::Server::new(epoch, state.clone());
rpc_server.start().await?;
app = app.merge(collab::rpc::routes(rpc_server.clone()));
on_shutdown = Some(Box::new(move || rpc_server.teardown()));
}
if mode.is_api() {
fetch_extensions_from_blob_store_periodically(state.clone());
app = app
.merge(collab::api::events::router())
.merge(collab::api::extensions::router())
}
app = app.layer(Extension(state.clone()));
}
app = app.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<_>| {
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
let geoip_country_code = request
.headers()
.typed_get::<CloudflareIpCountryHeader>()
.map(|header| header.to_string());
tracing::info_span!(
"http_request",
method = ?request.method(),
matched_path,
geoip_country_code,
user_id = tracing::field::Empty,
login = tracing::field::Empty,
authn.jti = tracing::field::Empty,
is_staff = tracing::field::Empty
)
})
.on_response(
|response: &Response<_>, latency: Duration, _: &tracing::Span| {
let duration_ms = latency.as_micros() as f64 / 1000.;
tracing::info!(
duration_ms,
status = response.status().as_u16(),
"finished processing request"
);
},
),
);
#[cfg(unix)]
let signal = async move {
let mut sigterm = tokio::signal::unix::signal(SignalKind::terminate())
.expect("failed to listen for interrupt signal");
let mut sigint = tokio::signal::unix::signal(SignalKind::interrupt())
.expect("failed to listen for interrupt signal");
let sigterm = sigterm.recv();
let sigint = sigint.recv();
futures::pin_mut!(sigterm, sigint);
futures::future::select(sigterm, sigint).await;
};
#[cfg(windows)]
let signal = async move {
// todo(windows):
// `ctrl_close` does not work well, because tokio's signal handler always returns soon,
// but system terminates the application soon after returning CTRL+CLOSE handler.
// So we should implement blocking handler to treat CTRL+CLOSE signal.
let mut ctrl_break = tokio::signal::windows::ctrl_break()
.expect("failed to listen for interrupt signal");
let mut ctrl_c = tokio::signal::windows::ctrl_c()
.expect("failed to listen for interrupt signal");
let ctrl_break = ctrl_break.recv();
let ctrl_c = ctrl_c.recv();
futures::pin_mut!(ctrl_break, ctrl_c);
futures::future::select(ctrl_break, ctrl_c).await;
};
axum::Server::from_tcp(listener)
.map_err(|e| anyhow!(e))?
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
.with_graceful_shutdown(async move {
signal.await;
tracing::info!("Received interrupt signal");
if let Some(on_shutdown) = on_shutdown {
on_shutdown();
}
})
.await
.map_err(|e| anyhow!(e))?;
}
_ => {
Err(anyhow!(
"usage: collab <version | migrate | seed | serve <api|collab|llm|all>>"
))?;
}
}
Ok(())
}
async fn setup_app_database(config: &Config) -> Result<()> {
let db_options = db::ConnectOptions::new(config.database_url.clone());
let mut db = Database::new(db_options).await?;
db.initialize_notification_kinds().await?;
if config.seed_path.is_some() {
collab::seed::seed(config, &db, false).await?;
}
Ok(())
}
async fn handle_root(Extension(mode): Extension<ServiceMode>) -> String {
format!("zed:{mode} v{VERSION} ({})", REVISION.unwrap_or("unknown"))
}
async fn handle_liveness_probe(app_state: Option<Extension<Arc<AppState>>>) -> Result<String> {
if let Some(state) = app_state {
state.db.get_all_users(0, 1).await?;
}
Ok("ok".to_string())
}
pub fn init_tracing(config: &Config) -> Option<()> {
use std::str::FromStr;
use tracing_subscriber::layer::SubscriberExt;
let filter = EnvFilter::from_str(config.rust_log.as_deref()?).log_err()?;
tracing_subscriber::registry()
.with(if config.log_json.unwrap_or(false) {
Box::new(
tracing_subscriber::fmt::layer()
.fmt_fields(JsonFields::default())
.event_format(
tracing_subscriber::fmt::format()
.json()
.flatten_event(true)
.with_span_list(false),
)
.with_filter(filter),
) as Box<dyn Layer<_> + Send + Sync>
} else {
Box::new(
tracing_subscriber::fmt::layer()
.event_format(tracing_subscriber::fmt::format().pretty())
.with_filter(filter),
)
})
.init();
None
}
fn init_panic_hook() {
std::panic::set_hook(Box::new(move |panic_info| {
let panic_message = match panic_info.payload().downcast_ref::<&'static str>() {
Some(message) => *message,
None => match panic_info.payload().downcast_ref::<String>() {
Some(message) => message.as_str(),
None => "Box<Any>",
},
};
let backtrace = std::backtrace::Backtrace::force_capture();
let location = panic_info
.location()
.map(|loc| format!("{}:{}", loc.file(), loc.line()));
tracing::error!(panic = true, ?location, %panic_message, %backtrace, "Server Panic");
}));
}

4234
crates/collab/src/rpc.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,253 @@
use crate::db::{ChannelId, ChannelRole, UserId};
use anyhow::{Context as _, Result};
use collections::{BTreeMap, HashMap, HashSet};
use rpc::ConnectionId;
use semver::Version;
use serde::Serialize;
use std::fmt;
use tracing::instrument;
#[derive(Default, Serialize)]
pub struct ConnectionPool {
connections: BTreeMap<ConnectionId, Connection>,
connected_users: BTreeMap<UserId, ConnectedPrincipal>,
channels: ChannelPool,
}
#[derive(Default, Serialize)]
struct ConnectedPrincipal {
connection_ids: HashSet<ConnectionId>,
}
#[derive(Clone, Debug, Serialize, PartialOrd, PartialEq, Eq, Ord)]
pub struct ZedVersion(pub Version);
impl fmt::Display for ZedVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl ZedVersion {
pub fn can_collaborate(&self) -> bool {
// v0.204.1 was the first version after the auto-update bug.
// We reject any clients older than that to hope we can persuade them to upgrade.
if self.0 < Version::new(0, 204, 1) {
return false;
}
true
}
}
#[derive(Serialize)]
pub struct Connection {
pub user_id: UserId,
pub admin: bool,
pub zed_version: ZedVersion,
}
impl ConnectionPool {
pub fn reset(&mut self) {
self.connections.clear();
self.connected_users.clear();
self.channels.clear();
}
pub fn connection(&mut self, connection_id: ConnectionId) -> Option<&Connection> {
self.connections.get(&connection_id)
}
#[instrument(skip(self))]
pub fn add_connection(
&mut self,
connection_id: ConnectionId,
user_id: UserId,
admin: bool,
zed_version: ZedVersion,
) {
self.connections.insert(
connection_id,
Connection {
user_id,
admin,
zed_version,
},
);
let connected_user = self.connected_users.entry(user_id).or_default();
connected_user.connection_ids.insert(connection_id);
}
#[instrument(skip(self))]
pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Result<()> {
let connection = self
.connections
.get_mut(&connection_id)
.context("no such connection")?;
let user_id = connection.user_id;
let connected_user = self.connected_users.get_mut(&user_id).unwrap();
connected_user.connection_ids.remove(&connection_id);
if connected_user.connection_ids.is_empty() {
self.connected_users.remove(&user_id);
self.channels.remove_user(&user_id);
};
self.connections.remove(&connection_id).unwrap();
Ok(())
}
pub fn connections(&self) -> impl Iterator<Item = &Connection> {
self.connections.values()
}
pub fn user_connections(&self, user_id: UserId) -> impl Iterator<Item = &Connection> + '_ {
self.connected_users
.get(&user_id)
.into_iter()
.flat_map(|state| {
state
.connection_ids
.iter()
.flat_map(|cid| self.connections.get(cid))
})
}
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
self.connected_users
.get(&user_id)
.into_iter()
.flat_map(|state| &state.connection_ids)
.copied()
}
pub fn channel_user_ids(
&self,
channel_id: ChannelId,
) -> impl Iterator<Item = (UserId, ChannelRole)> + '_ {
self.channels.users_to_notify(channel_id)
}
pub fn channel_connection_ids(
&self,
channel_id: ChannelId,
) -> impl Iterator<Item = (ConnectionId, ChannelRole)> + '_ {
self.channels
.users_to_notify(channel_id)
.flat_map(|(user_id, role)| {
self.user_connection_ids(user_id)
.map(move |connection_id| (connection_id, role))
})
}
pub fn subscribe_to_channel(
&mut self,
user_id: UserId,
channel_id: ChannelId,
role: ChannelRole,
) {
self.channels.subscribe(user_id, channel_id, role);
}
pub fn unsubscribe_from_channel(&mut self, user_id: &UserId, channel_id: &ChannelId) {
self.channels.unsubscribe(user_id, channel_id);
}
pub fn is_user_online(&self, user_id: UserId) -> bool {
!self
.connected_users
.get(&user_id)
.unwrap_or(&Default::default())
.connection_ids
.is_empty()
}
#[cfg(feature = "test-support")]
pub fn check_invariants(&self) {
for (connection_id, connection) in &self.connections {
assert!(
self.connected_users
.get(&connection.user_id)
.unwrap()
.connection_ids
.contains(connection_id)
);
}
for (user_id, state) in &self.connected_users {
for connection_id in &state.connection_ids {
assert_eq!(
self.connections.get(connection_id).unwrap().user_id,
*user_id
);
}
}
}
}
#[derive(Default, Serialize)]
pub struct ChannelPool {
by_user: HashMap<UserId, HashMap<ChannelId, ChannelRole>>,
by_channel: HashMap<ChannelId, HashSet<UserId>>,
}
impl ChannelPool {
pub fn clear(&mut self) {
self.by_user.clear();
self.by_channel.clear();
}
pub fn subscribe(&mut self, user_id: UserId, channel_id: ChannelId, role: ChannelRole) {
self.by_user
.entry(user_id)
.or_default()
.insert(channel_id, role);
self.by_channel
.entry(channel_id)
.or_default()
.insert(user_id);
}
pub fn unsubscribe(&mut self, user_id: &UserId, channel_id: &ChannelId) {
if let Some(channels) = self.by_user.get_mut(user_id) {
channels.remove(channel_id);
if channels.is_empty() {
self.by_user.remove(user_id);
}
}
if let Some(users) = self.by_channel.get_mut(channel_id) {
users.remove(user_id);
if users.is_empty() {
self.by_channel.remove(channel_id);
}
}
}
pub fn remove_user(&mut self, user_id: &UserId) {
if let Some(channels) = self.by_user.remove(user_id) {
for channel_id in channels.keys() {
self.unsubscribe(user_id, channel_id)
}
}
}
pub fn users_to_notify(
&self,
channel_id: ChannelId,
) -> impl '_ + Iterator<Item = (UserId, ChannelRole)> {
self.by_channel
.get(&channel_id)
.into_iter()
.flat_map(move |users| {
users.iter().flat_map(move |user_id| {
Some((
*user_id,
self.by_user
.get(user_id)
.and_then(|channels| channels.get(&channel_id))
.copied()?,
))
})
})
}
}

136
crates/collab/src/seed.rs Normal file
View File

@@ -0,0 +1,136 @@
use crate::db::{self, ChannelRole, NewUserParams};
use anyhow::Context as _;
use chrono::{DateTime, Utc};
use db::Database;
use serde::{Deserialize, de::DeserializeOwned};
use std::{fs, path::Path};
use crate::Config;
/// A GitHub user.
///
/// This representation corresponds to the entries in the `seed/github_users.json` file.
#[derive(Debug, Deserialize)]
struct GithubUser {
id: i32,
login: String,
email: Option<String>,
name: Option<String>,
created_at: DateTime<Utc>,
}
#[derive(Deserialize)]
struct SeedConfig {
/// Which users to create as admins.
admins: Vec<String>,
/// Which channels to create (all admins are invited to all channels).
channels: Vec<String>,
}
pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result<()> {
let client = reqwest::Client::new();
if !db.get_all_users(0, 1).await?.is_empty() && !force {
return Ok(());
}
let seed_path = config
.seed_path
.as_ref()
.context("called seed with no SEED_PATH")?;
let seed_config = load_admins(seed_path)
.context(format!("failed to load {}", seed_path.to_string_lossy()))?;
let mut first_user = None;
let mut others = vec![];
for admin_login in seed_config.admins {
let user = fetch_github::<GithubUser>(
&client,
&format!("https://api.github.com/users/{admin_login}"),
)
.await;
let user = db
.create_user(
&user.email.unwrap_or(format!("{admin_login}@example.com")),
user.name.as_deref(),
true,
NewUserParams {
github_login: user.login,
github_user_id: user.id,
},
)
.await
.context("failed to create admin user")?;
if first_user.is_none() {
first_user = Some(user.user_id);
} else {
others.push(user.user_id)
}
}
for channel in seed_config.channels {
let (channel, _) = db
.create_channel(&channel, None, first_user.unwrap())
.await
.context("failed to create channel")?;
for user_id in &others {
db.invite_channel_member(
channel.id,
*user_id,
first_user.unwrap(),
ChannelRole::Admin,
)
.await
.context("failed to add user to channel")?;
}
}
let github_users_filepath = seed_path.parent().unwrap().join("seed/github_users.json");
let github_users: Vec<GithubUser> =
serde_json::from_str(&fs::read_to_string(github_users_filepath)?)?;
for github_user in github_users {
log::info!("Seeding {:?} from GitHub", github_user.login);
db.update_or_create_user_by_github_account(
&github_user.login,
github_user.id,
github_user.email.as_deref(),
github_user.name.as_deref(),
github_user.created_at,
None,
)
.await
.expect("failed to insert user");
}
Ok(())
}
fn load_admins(path: impl AsRef<Path>) -> anyhow::Result<SeedConfig> {
let file_content = fs::read_to_string(path)?;
Ok(serde_json::from_str(&file_content)?)
}
async fn fetch_github<T: DeserializeOwned>(client: &reqwest::Client, url: &str) -> T {
let mut request_builder = client.get(url);
if let Ok(github_token) = std::env::var("GITHUB_TOKEN") {
request_builder =
request_builder.header("Authorization", format!("Bearer {}", github_token));
}
let response = request_builder
.header("user-agent", "zed")
.send()
.await
.unwrap_or_else(|error| panic!("failed to fetch '{url}': {error}"));
let response_text = response.text().await.unwrap_or_else(|error| {
panic!("failed to fetch '{url}': {error}");
});
serde_json::from_str(&response_text).unwrap_or_else(|error| {
panic!("failed to deserialize github user from '{url}'. Error: '{error}', text: '{response_text}'");
})
}

View File

@@ -0,0 +1,3 @@
mod user_service;
pub use user_service::*;

View File

@@ -0,0 +1,377 @@
use anyhow::{Context as _, anyhow};
use async_trait::async_trait;
use cloud_api_types::internal_api::{
self, FuzzySearchChannelMembersByGithubLoginBody,
FuzzySearchChannelMembersByGithubLoginResponse, FuzzySearchUsersBody, FuzzySearchUsersResponse,
LookUpUserByGithubLoginBody, LookUpUserByGithubLoginResponse, LookUpUsersByLegacyIdBody,
LookUpUsersByLegacyIdResponse,
};
use reqwest::RequestBuilder;
use rpc::proto;
use serde::de::DeserializeOwned;
use crate::Result;
use crate::db::{Channel, UserId};
use crate::entities::User;
#[cfg(feature = "test-support")]
pub use self::fake_user_service::*;
#[async_trait]
pub trait UserService: Send + Sync + 'static {
async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>>;
async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>>;
async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result<Vec<User>>;
// NOTE: This method is only tangentially related to users, but we're putting it on the `UserService` to avoid
// introducing a separate service.
//
// We're also using the `proto::ChannelMember` representation in the return type, as we don't yet have a domain
// representation of a channel member (and doesn't seem necessary to introduce one, at this point).
async fn search_channel_members(
&self,
channel: &Channel,
query: &str,
limit: u32,
) -> Result<(Vec<proto::ChannelMember>, Vec<User>)>;
#[cfg(feature = "test-support")]
fn as_fake(&self) -> std::sync::Arc<FakeUserService> {
panic!("called as_fake on a real `UserService`");
}
}
/// A [`UserService`] implementation backed by Cloud.
pub struct CloudUserService {
http_client: reqwest::Client,
zed_cloud_url: String,
internal_api_key: String,
}
impl CloudUserService {
pub fn new(
http_client: reqwest::Client,
zed_cloud_url: String,
internal_api_key: String,
) -> Self {
Self {
http_client,
zed_cloud_url,
internal_api_key,
}
}
async fn send_request<T: DeserializeOwned + 'static>(
&self,
request: RequestBuilder,
) -> Result<T> {
let request = request
.header("Content-Type", "application/json")
.header(
"Authorization",
format!("Bearer {}", &self.internal_api_key),
)
.build()
.context("failed to build request")?;
let response = self
.http_client
.execute(request)
.await
.context("failed to send request to Cloud")?;
let status = response.status();
match response.error_for_status() {
Ok(response) => {
let response_body: T = response
.json()
.await
.context("failed to parse response body")?;
Ok(response_body)
}
Err(_err) => Err(anyhow!("request to Cloud failed with status {status}",))?,
}
}
}
#[async_trait]
impl UserService for CloudUserService {
async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>> {
let response_body: LookUpUsersByLegacyIdResponse = self
.send_request(
self.http_client
.post(format!(
"{}/internal/users/look_up_by_legacy_id",
&self.zed_cloud_url
))
.json(&LookUpUsersByLegacyIdBody {
legacy_user_ids: ids.into_iter().map(|id| id.0).collect(),
}),
)
.await?;
Ok(response_body.users.into_iter().map(User::from).collect())
}
async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
let response_body: LookUpUserByGithubLoginResponse = self
.send_request(
self.http_client
.post(format!(
"{}/internal/users/look_up_by_github_login",
&self.zed_cloud_url
))
.json(&LookUpUserByGithubLoginBody {
github_login: github_login.to_string(),
}),
)
.await?;
Ok(response_body.user.map(User::from))
}
async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result<Vec<User>> {
let response_body: FuzzySearchUsersResponse = self
.send_request(
self.http_client
.post(format!(
"{}/internal/users/fuzzy_search",
&self.zed_cloud_url
))
.json(&FuzzySearchUsersBody {
query: query.to_string(),
limit,
}),
)
.await?;
Ok(response_body.users.into_iter().map(User::from).collect())
}
async fn search_channel_members(
&self,
channel: &Channel,
query: &str,
limit: u32,
) -> Result<(Vec<proto::ChannelMember>, Vec<User>)> {
let response_body: FuzzySearchChannelMembersByGithubLoginResponse = self
.send_request(
self.http_client
.post(format!(
"{}/internal/channel_members/fuzzy_search_by_github_login",
&self.zed_cloud_url
))
.json(&FuzzySearchChannelMembersByGithubLoginBody {
channel_id: channel.root_id().0,
query: query.to_string(),
limit,
}),
)
.await?;
let members = response_body
.channel_members
.into_iter()
.map(channel_member_to_proto)
.collect::<Vec<_>>();
let users = response_body
.users
.into_iter()
.map(User::from)
.collect::<Vec<_>>();
Ok((members, users))
}
}
fn channel_member_to_proto(member: internal_api::ChannelMember) -> proto::ChannelMember {
let kind = match member.kind {
internal_api::ChannelMemberKind::Member => proto::channel_member::Kind::Member,
internal_api::ChannelMemberKind::Invitee => proto::channel_member::Kind::Invitee,
};
let role = match member.role {
internal_api::ChannelMemberRole::Admin => proto::ChannelRole::Admin,
internal_api::ChannelMemberRole::Member => proto::ChannelRole::Member,
internal_api::ChannelMemberRole::Talker => proto::ChannelRole::Talker,
internal_api::ChannelMemberRole::Guest => proto::ChannelRole::Guest,
internal_api::ChannelMemberRole::Banned => proto::ChannelRole::Banned,
};
proto::ChannelMember {
user_id: UserId(member.legacy_user_id).to_proto(),
kind: kind.into(),
role: role.into(),
}
}
impl From<internal_api::User> for User {
fn from(user: internal_api::User) -> Self {
Self {
id: UserId(user.legacy_user_id),
avatar_url: user.avatar_url,
github_login: user.github_login,
name: user.name,
admin: user.admin,
connected_once: user.connected_once,
}
}
}
#[cfg(feature = "test-support")]
mod fake_user_service {
use std::sync::{Arc, Weak};
use collections::HashMap;
use tokio::sync::Mutex;
use crate::db::Database;
use super::*;
#[derive(Debug)]
pub struct NewUserParams {
pub github_login: String,
pub github_user_id: i32,
}
pub struct FakeUserService {
this: Weak<Self>,
state: Arc<Mutex<FakeUserServiceState>>,
database: Arc<Database>,
}
struct FakeUserServiceState {
next_user_id: UserId,
users: HashMap<UserId, User>,
}
impl Default for FakeUserServiceState {
fn default() -> Self {
Self {
next_user_id: UserId(1),
users: HashMap::default(),
}
}
}
impl FakeUserService {
pub fn new(database: Arc<Database>) -> Arc<Self> {
Arc::new_cyclic(|this| Self {
this: this.clone(),
state: Arc::new(Mutex::default()),
database,
})
}
pub async fn create_user(
&self,
email_address: &str,
name: Option<&str>,
admin: bool,
params: NewUserParams,
) -> UserId {
let mut state = self.state.lock().await;
let user_id = state.next_user_id;
let _ = email_address;
state.users.insert(
user_id,
User {
id: user_id,
avatar_url: format!("https://github.com/{}.png?size=128", params.github_login),
github_login: params.github_login,
name: name.map(|name| name.to_string()),
admin,
connected_once: false,
},
);
state.next_user_id = UserId(state.next_user_id.0 + 1);
user_id
}
pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<User>> {
let state = self.state.lock().await;
let user = state.users.get(&id).cloned();
Ok(user)
}
}
#[async_trait]
impl UserService for FakeUserService {
async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>> {
let state = self.state.lock().await;
let users = state
.users
.values()
.filter(|user| ids.contains(&user.id))
.cloned()
.collect();
Ok(users)
}
async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
let state = self.state.lock().await;
let user = state
.users
.values()
.find(|user| user.github_login == github_login)
.cloned();
Ok(user)
}
async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result<Vec<User>> {
let _ = query;
let _ = limit;
unimplemented!("not currently exercised by any tests")
}
async fn search_channel_members(
&self,
channel: &Channel,
query: &str,
limit: u32,
) -> Result<(Vec<proto::ChannelMember>, Vec<User>)> {
let state = self.state.lock().await;
let users = state
.users
.values()
.filter(|user| user.github_login.contains(query))
.take(limit as usize)
.cloned()
.collect::<Vec<_>>();
let members = self
.database
.get_channel_memberships_for_user_ids(
channel,
users.iter().map(|user| user.id).collect(),
)
.await?;
Ok((
members
.into_iter()
.map(proto::ChannelMember::from)
.collect(),
users,
))
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> Arc<FakeUserService> {
self.this.upgrade().unwrap()
}
}
}