mirror of
				https://github.com/meilisearch/meilisearch.git
				synced 2025-10-31 07:56:28 +00:00 
			
		
		
		
	Merge pull request #5635 from meilisearch/prompt-for-email
Prompt for Email
This commit is contained in:
		| @@ -25,6 +25,7 @@ LABEL   org.opencontainers.image.source="https://github.com/meilisearch/meilisea | |||||||
|  |  | ||||||
| ENV     MEILI_HTTP_ADDR 0.0.0.0:7700 | ENV     MEILI_HTTP_ADDR 0.0.0.0:7700 | ||||||
| ENV     MEILI_SERVER_PROVIDER docker | ENV     MEILI_SERVER_PROVIDER docker | ||||||
|  | ENV     MEILI_CONTACT_EMAIL "" | ||||||
|  |  | ||||||
| RUN     apk add -q --no-cache libgcc tini curl | RUN     apk add -q --no-cache libgcc tini curl | ||||||
|  |  | ||||||
|   | |||||||
| @@ -45,7 +45,7 @@ macro_rules! empty_analytics { | |||||||
| /// `~/.config/Meilisearch` on *NIX or *BSD. | /// `~/.config/Meilisearch` on *NIX or *BSD. | ||||||
| /// `~/Library/ApplicationSupport` on macOS. | /// `~/Library/ApplicationSupport` on macOS. | ||||||
| /// `%APPDATA` (= `C:\Users%USERNAME%\AppData\Roaming`) on windows. | /// `%APPDATA` (= `C:\Users%USERNAME%\AppData\Roaming`) on windows. | ||||||
| static MEILISEARCH_CONFIG_PATH: Lazy<Option<PathBuf>> = | pub static MEILISEARCH_CONFIG_PATH: Lazy<Option<PathBuf>> = | ||||||
|     Lazy::new(|| AppDirs::new(Some("Meilisearch"), false).map(|appdir| appdir.config_dir)); |     Lazy::new(|| AppDirs::new(Some("Meilisearch"), false).map(|appdir| appdir.config_dir)); | ||||||
|  |  | ||||||
| fn config_user_id_path(db_path: &Path) -> Option<PathBuf> { | fn config_user_id_path(db_path: &Path) -> Option<PathBuf> { | ||||||
|   | |||||||
| @@ -277,6 +277,7 @@ impl Infos { | |||||||
|             log_level, |             log_level, | ||||||
|             indexer_options, |             indexer_options, | ||||||
|             config_file_path, |             config_file_path, | ||||||
|  |             contact_email: _, | ||||||
|             no_analytics: _, |             no_analytics: _, | ||||||
|         } = options; |         } = options; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ use actix_web::web::Data; | |||||||
| use actix_web::HttpServer; | use actix_web::HttpServer; | ||||||
| use index_scheduler::IndexScheduler; | use index_scheduler::IndexScheduler; | ||||||
| use is_terminal::IsTerminal; | use is_terminal::IsTerminal; | ||||||
| use meilisearch::analytics::Analytics; | use meilisearch::analytics::{Analytics, MEILISEARCH_CONFIG_PATH}; | ||||||
| use meilisearch::option::LogMode; | use meilisearch::option::LogMode; | ||||||
| use meilisearch::search_queue::SearchQueue; | use meilisearch::search_queue::SearchQueue; | ||||||
| use meilisearch::{ | use meilisearch::{ | ||||||
| @@ -20,11 +20,18 @@ use meilisearch::{ | |||||||
|     LogStderrType, Opt, SubscriberForSecondLayer, |     LogStderrType, Opt, SubscriberForSecondLayer, | ||||||
| }; | }; | ||||||
| use meilisearch_auth::{generate_master_key, AuthController, MASTER_KEY_MIN_SIZE}; | use meilisearch_auth::{generate_master_key, AuthController, MASTER_KEY_MIN_SIZE}; | ||||||
|  | use meilisearch_types::versioning::{VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH}; | ||||||
|  | use serde_json::json; | ||||||
| use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; | use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; | ||||||
|  | use tokio::io::{AsyncBufReadExt, BufReader}; | ||||||
| use tracing::level_filters::LevelFilter; | use tracing::level_filters::LevelFilter; | ||||||
| use tracing_subscriber::layer::SubscriberExt as _; | use tracing_subscriber::layer::SubscriberExt as _; | ||||||
| use tracing_subscriber::Layer; | use tracing_subscriber::Layer; | ||||||
|  |  | ||||||
|  | const SKIP_EMAIL_FILENAME: &str = "skip-email"; | ||||||
|  | const PORTAL_ID: &str = "25945010"; | ||||||
|  | const FORM_GUID: &str = "991e2a09-77c2-4428-9242-ebf26bfc6c64"; | ||||||
|  |  | ||||||
| #[cfg(not(windows))] | #[cfg(not(windows))] | ||||||
| #[global_allocator] | #[global_allocator] | ||||||
| static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; | static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; | ||||||
| @@ -89,7 +96,7 @@ async fn main() -> anyhow::Result<()> { | |||||||
| } | } | ||||||
|  |  | ||||||
| async fn try_main() -> anyhow::Result<()> { | async fn try_main() -> anyhow::Result<()> { | ||||||
|     let (opt, config_read_from) = Opt::try_build()?; |     let (mut opt, config_read_from) = Opt::try_build()?; | ||||||
|  |  | ||||||
|     std::panic::set_hook(Box::new(on_panic)); |     std::panic::set_hook(Box::new(on_panic)); | ||||||
|  |  | ||||||
| @@ -124,6 +131,38 @@ async fn try_main() -> anyhow::Result<()> { | |||||||
|  |  | ||||||
|     let (index_scheduler, auth_controller) = setup_meilisearch(&opt)?; |     let (index_scheduler, auth_controller) = setup_meilisearch(&opt)?; | ||||||
|  |  | ||||||
|  |     // We ask users their emails just after the data.ms is created | ||||||
|  |     let skip_email_path = | ||||||
|  |         MEILISEARCH_CONFIG_PATH.as_ref().map(|conf| conf.join(SKIP_EMAIL_FILENAME)); | ||||||
|  |     // If the config path does not exist, it means the user don't have a home directory | ||||||
|  |     let skip_email = skip_email_path.as_ref().is_none_or(|path| path.exists()); | ||||||
|  |     opt.contact_email = match opt.contact_email.as_ref().map(|email| email.as_deref()) { | ||||||
|  |         Some(Some("false")) | None if !skip_email => prompt_for_contact_email().await.map(Some)?, | ||||||
|  |         Some(Some(email)) if !skip_email => Some(Some(email.to_string())), | ||||||
|  |         _otherwise => None, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if let Some(Some(email)) = opt.contact_email.as_ref() { | ||||||
|  |         let email = email.clone(); | ||||||
|  |         // We spawn a task to register the email and create the skip email | ||||||
|  |         // file to avoid blocking the Meilisearch launch further. | ||||||
|  |         let handle = tokio::spawn(async move { | ||||||
|  |             if let Some(skip_email_path) = skip_email_path { | ||||||
|  |                 // If the analytics are disabled the directory might not exist at all | ||||||
|  |                 if let Err(e) = tokio::fs::create_dir_all(skip_email_path.parent().unwrap()).await { | ||||||
|  |                     eprintln!("Failed to create skip email file: {e}"); | ||||||
|  |                 } | ||||||
|  |                 if let Err(e) = tokio::fs::File::create_new(skip_email_path).await { | ||||||
|  |                     eprintln!("Failed to create skip email file: {e}"); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             if let Err(err) = register_contact_email(&email).await { | ||||||
|  |                 eprintln!("Failed to register email: {}", err); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         drop(handle); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let analytics = |     let analytics = | ||||||
|         analytics::Analytics::new(&opt, index_scheduler.clone(), auth_controller.clone()).await; |         analytics::Analytics::new(&opt, index_scheduler.clone(), auth_controller.clone()).await; | ||||||
|  |  | ||||||
| @@ -139,6 +178,76 @@ async fn try_main() -> anyhow::Result<()> { | |||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// Prompt the user about the contact email for support and news. | ||||||
|  | /// It only displays the prompt if the input is an interactive terminal. | ||||||
|  | async fn prompt_for_contact_email() -> anyhow::Result<Option<String>> { | ||||||
|  |     let stdin = tokio::io::stdin(); | ||||||
|  |  | ||||||
|  |     if !stdin.is_terminal() { | ||||||
|  |         return Ok(None); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     println!("Get monthly updates about new features and tips to get the most out of Meilisearch."); | ||||||
|  |     println!("Use the --contact-email option to disable this prompt."); | ||||||
|  |     print!("Enter your email or leave blank to skip> "); | ||||||
|  |     std::io::stdout().flush()?; | ||||||
|  |  | ||||||
|  |     let mut email = String::new(); | ||||||
|  |     let mut stdin = BufReader::new(stdin); | ||||||
|  |     let _ = stdin.read_line(&mut email).await?; | ||||||
|  |     let email = email.trim(); | ||||||
|  |  | ||||||
|  |     if email.is_empty() { | ||||||
|  |         Ok(None) | ||||||
|  |     } else { | ||||||
|  |         Ok(Some(email.to_string())) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn register_contact_email(email: &str) -> anyhow::Result<()> { | ||||||
|  |     let url = format!( | ||||||
|  |         "https://api.hsforms.com/submissions/v3/integration/submit/{PORTAL_ID}/{FORM_GUID}" | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     let page_name = format!( | ||||||
|  |         "Meilisearch terminal prompt v{}.{}.{}", | ||||||
|  |         VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH | ||||||
|  |     ); | ||||||
|  |     let response = reqwest::Client::new() | ||||||
|  |         .post(url) | ||||||
|  |         .json(&json!({ | ||||||
|  |           "fields": [{ | ||||||
|  |             "objectTypeId": "0-1", | ||||||
|  |             "name": "email", | ||||||
|  |             "value": email, | ||||||
|  |         }], | ||||||
|  |         "context": { | ||||||
|  |             "pageName": page_name, | ||||||
|  |         }, | ||||||
|  |         "legalConsentOptions": { | ||||||
|  |             "consent": { | ||||||
|  |                 "consentToProcess": true, | ||||||
|  |                 "text": "I agree to allow Meilisearch to store and process my personal data.", | ||||||
|  |                 "communications": [{ | ||||||
|  |                     "value": true, | ||||||
|  |                     "subscriptionTypeId": 999, | ||||||
|  |                     "text": "I agree to receive marketing communications from Meilisearch.", | ||||||
|  |                 }], | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |         })) | ||||||
|  |         .send() | ||||||
|  |         .await?; | ||||||
|  |  | ||||||
|  |     let status = response.status(); | ||||||
|  |     if status.is_client_error() || status.is_server_error() { | ||||||
|  |         let response: serde_json::Value = response.json().await?; | ||||||
|  |         eprintln!("Failed to register email: {response:?}"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
| async fn run_http( | async fn run_http( | ||||||
|     index_scheduler: Arc<IndexScheduler>, |     index_scheduler: Arc<IndexScheduler>, | ||||||
|     auth_controller: Arc<AuthController>, |     auth_controller: Arc<AuthController>, | ||||||
|   | |||||||
| @@ -66,6 +66,7 @@ const MEILI_EXPERIMENTAL_LIMIT_BATCHED_TASKS_TOTAL_SIZE: &str = | |||||||
| const MEILI_EXPERIMENTAL_EMBEDDING_CACHE_ENTRIES: &str = | const MEILI_EXPERIMENTAL_EMBEDDING_CACHE_ENTRIES: &str = | ||||||
|     "MEILI_EXPERIMENTAL_EMBEDDING_CACHE_ENTRIES"; |     "MEILI_EXPERIMENTAL_EMBEDDING_CACHE_ENTRIES"; | ||||||
| const MEILI_EXPERIMENTAL_NO_SNAPSHOT_COMPACTION: &str = "MEILI_EXPERIMENTAL_NO_SNAPSHOT_COMPACTION"; | const MEILI_EXPERIMENTAL_NO_SNAPSHOT_COMPACTION: &str = "MEILI_EXPERIMENTAL_NO_SNAPSHOT_COMPACTION"; | ||||||
|  | const MEILI_CONTACT_EMAIL: &str = "MEILI_CONTACT_EMAIL"; | ||||||
| const DEFAULT_CONFIG_FILE_PATH: &str = "./config.toml"; | const DEFAULT_CONFIG_FILE_PATH: &str = "./config.toml"; | ||||||
| const DEFAULT_DB_PATH: &str = "./data.ms"; | const DEFAULT_DB_PATH: &str = "./data.ms"; | ||||||
| const DEFAULT_HTTP_ADDR: &str = "localhost:7700"; | const DEFAULT_HTTP_ADDR: &str = "localhost:7700"; | ||||||
| @@ -347,6 +348,13 @@ pub struct Opt { | |||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     pub log_level: LogLevel, |     pub log_level: LogLevel, | ||||||
|  |  | ||||||
|  |     /// Sets the email address to contact for support and news. | ||||||
|  |     /// | ||||||
|  |     /// Use this option to disable contact email prompting. Leave | ||||||
|  |     /// blank or without value to disable contact email prompting. | ||||||
|  |     #[clap(long, env = MEILI_CONTACT_EMAIL)] | ||||||
|  |     pub contact_email: Option<Option<String>>, | ||||||
|  |  | ||||||
|     /// Experimental contains filter feature. For more information, |     /// Experimental contains filter feature. For more information, | ||||||
|     /// see: <https://github.com/orgs/meilisearch/discussions/763> |     /// see: <https://github.com/orgs/meilisearch/discussions/763> | ||||||
|     /// |     /// | ||||||
| @@ -556,6 +564,7 @@ impl Opt { | |||||||
|             ignore_dump_if_db_exists: _, |             ignore_dump_if_db_exists: _, | ||||||
|             config_file_path: _, |             config_file_path: _, | ||||||
|             no_analytics, |             no_analytics, | ||||||
|  |             contact_email, | ||||||
|             experimental_contains_filter, |             experimental_contains_filter, | ||||||
|             experimental_enable_metrics, |             experimental_enable_metrics, | ||||||
|             experimental_search_queue_size, |             experimental_search_queue_size, | ||||||
| @@ -588,6 +597,10 @@ impl Opt { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         export_to_env_if_not_present(MEILI_NO_ANALYTICS, no_analytics.to_string()); |         export_to_env_if_not_present(MEILI_NO_ANALYTICS, no_analytics.to_string()); | ||||||
|  |         export_to_env_if_not_present( | ||||||
|  |             MEILI_CONTACT_EMAIL, | ||||||
|  |             contact_email.flatten().unwrap_or_else(|| "false".to_string()), | ||||||
|  |         ); | ||||||
|         export_to_env_if_not_present( |         export_to_env_if_not_present( | ||||||
|             MEILI_HTTP_PAYLOAD_SIZE_LIMIT, |             MEILI_HTTP_PAYLOAD_SIZE_LIMIT, | ||||||
|             http_payload_size_limit.to_string(), |             http_payload_size_limit.to_string(), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user