use anyhow::{bail, Result};
use crate::config::{self, Config, DESTINATION_IDS, SOURCE_IDS};
pub fn list(config: &Config) {
println!("sources:");
for id in SOURCE_IDS {
row(
id,
config::source_enabled(config, id),
config::source_creds(config, id),
);
}
println!("\ndestinations:");
for id in DESTINATION_IDS {
row(
id,
config::dest_enabled(config, id),
config::dest_creds(config, id),
);
}
println!("\nrun `navi providers setup <name>` for setup steps.");
}
fn row(name: &str, enabled: bool, creds: bool) {
let state = if enabled { "on" } else { "off" };
let creds = if creds {
"credentials found"
} else {
"no credentials"
};
println!(" {name:<8} {state:<3} {creds}");
}
pub fn setup(name: &str) -> Result<()> {
match setup_text(name) {
Some(text) => {
println!("{text}");
Ok(())
}
None => {
bail!("unknown provider `{name}` (github | gitlab | gitea | slack | discord | email)")
}
}
}
const SLACK_SETUP: &str = concat!(
"Slack setup:\n\
1. Create the app from a manifest: https://api.slack.com/apps -> \"Create New App\"\n\
-> \"From an app manifest\" -> pick your workspace -> paste the manifest below.\n\
2. Install to your workspace, then copy the bot token (xoxb-...).\n\
3. Export it as NAVI_SLACK_TOKEN (or put it in navi.env next to your config).\n\
4. Set `slack.dm_to` to \"self\" or your Slack user id (U...).\n\
5. `navi config set slack.enabled true`, then `navi test --destination slack`.\n\
\n\
Manifest (already has the chat:write + im:write scopes):\n",
include_str!("../../../assets/slack-manifest.json")
);
pub const DISCORD_PERMISSIONS: u32 = 0x400 | 0x800 | 0x4000;
pub fn discord_invite_url(client_id: &str) -> String {
format!(
"https://discord.com/api/oauth2/authorize?client_id={client_id}&scope=bot&permissions={DISCORD_PERMISSIONS}"
)
}
const DISCORD_SETUP: &str = "Discord setup (pick one mode):\n\
• Webhook (simplest, no token): create a channel webhook and set `discord.dm_to`\n\
to its https URL.\n\
• Bot (DM or channel): create an app at https://discord.com/developers/applications,\n\
add a bot, copy its token as NAVI_DISCORD_TOKEN, then invite it with (replace\n\
<CLIENT_ID> with your app's Client ID):\n\
https://discord.com/api/oauth2/authorize?client_id=<CLIENT_ID>&scope=bot&permissions=19456\n\
Set `discord.dm_to` to your user id (a numeric snowflake) for a DM, or a channel id.\n\
(`navi init` fills the Client ID into this link for you.)\n\
Then `navi config set discord.enabled true` and `navi test --destination discord`.";
pub fn setup_text(name: &str) -> Option<&'static str> {
let text = match name {
"github" => {
"GitHub setup:\n\
1. Create a Personal Access Token: https://github.com/settings/tokens\n\
2. Scopes: `notifications` + `repo` (read), plus `read:org` for team-review detection.\n\
3. Export it as NAVI_GITHUB_TOKEN (or put it in navi.env next to your config).\n\
4. `navi config set github.enabled true`, then `navi test --source github`."
}
"gitlab" => {
"GitLab setup:\n\
1. Create a token: https://gitlab.com/-/user_settings/personal_access_tokens\n\
2. Scope: `read_api`.\n\
3. Export it as NAVI_GITLAB_TOKEN. For self-hosted, set `gitlab.api_base` (…/api/v4).\n\
4. `navi config set gitlab.enabled true`, then `navi test --source gitlab`."
}
"gitea" => {
"Gitea/Forgejo setup:\n\
1. Create a token in your instance's Settings → Applications.\n\
2. Export it as NAVI_GITEA_TOKEN, and set `gitea.api_base` (…/api/v1).\n\
3. `navi config set gitea.enabled true`, then `navi test --source gitea`."
}
"slack" => SLACK_SETUP,
"discord" => DISCORD_SETUP,
"email" => {
"Email (SMTP) setup:\n\
1. Set `email.smtp_host`, `email.smtp_port`, and `email.tls` (none | starttls | implicit).\n\
2. Set `email.from` and `email.to`; export the SMTP password as NAVI_EMAIL_PASSWORD.\n\
3. `navi config set email.enabled true`, then `navi test --destination email`."
}
_ => return None,
};
Some(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn setup_text_covers_every_provider() {
for id in SOURCE_IDS.iter().chain(DESTINATION_IDS.iter()) {
assert!(setup_text(id).is_some(), "missing setup text for {id}");
}
assert!(setup_text("nope").is_none());
}
#[test]
fn slack_setup_embeds_a_valid_manifest_with_the_scopes() {
const MANIFEST_SENTINEL: &str = "scopes):\n";
let text = setup_text("slack").unwrap();
let manifest_json = text
.split_once(MANIFEST_SENTINEL)
.unwrap_or_else(|| {
panic!("slack setup text must contain `{MANIFEST_SENTINEL}` before the manifest")
})
.1;
let manifest: serde_json::Value =
serde_json::from_str(manifest_json).expect("embedded manifest is valid JSON");
let scopes = &manifest["oauth_config"]["scopes"]["bot"];
assert!(scopes.as_array().unwrap().iter().any(|s| s == "chat:write"));
assert!(scopes.as_array().unwrap().iter().any(|s| s == "im:write"));
for id in SOURCE_IDS.iter().chain(DESTINATION_IDS.iter()) {
assert!(
!setup_text(id).unwrap().contains("assets/"),
"{id} setup text references a repo path installed users won't have"
);
}
}
#[test]
fn discord_invite_url_matches_the_documented_permissions() {
assert_eq!(DISCORD_PERMISSIONS, 19456);
let url = discord_invite_url("123");
assert!(url.contains("client_id=123"));
assert!(url.contains("scope=bot"));
assert!(url.contains("permissions=19456"));
assert!(setup_text("discord").unwrap().contains("permissions=19456"));
}
}