rpfm_server/
translation_hub.rs1use anyhow::{anyhow, Result};
17use keyring::{Entry, Error as KeyringError};
18
19use std::fs;
20
21use rpfm_extensions::translator::PackTranslation;
22use rpfm_extensions::translator::hub::SubmissionResult;
23
24use rpfm_ipc::messages::GitHubSignInState;
25use rpfm_ipc::settings_keys::GITHUB_LOGIN;
26
27use rpfm_lib::error::RLibError;
28use rpfm_lib::games::{TRANSLATIONS_REPO_NAME, TRANSLATIONS_REPO_OWNER};
29use rpfm_lib::integrations::github::{self, DeviceCode, DeviceFlowPoll, GitHubClient, PUBLIC_REPO_SCOPE};
30
31use crate::GITHUB_OAUTH_CLIENT_ID;
32use crate::settings::{mutate_settings, translations_local_path, SETTINGS};
33
34const KEYRING_SERVICE: &str = "rpfm";
36
37const KEYRING_GITHUB_TOKEN: &str = "github_token";
39
40#[cfg(debug_assertions)]
42const HUB_OVERRIDE_VAR: &str = "RPFM_TRANSLATION_HUB";
43
44pub enum SubmitOutcome {
50
51 Submitted(SubmissionResult),
53
54 SignInRequired,
56}
57
58pub fn sign_in_start() -> Result<DeviceCode> {
68 if GITHUB_OAUTH_CLIENT_ID.is_empty() {
69 return Err(anyhow!("This build of RPFM can't sign in to GitHub, as it was built without a GitHub client ID."));
70 }
71
72 github::request_device_code(GITHUB_OAUTH_CLIENT_ID, PUBLIC_REPO_SCOPE).map_err(From::from)
73}
74
75pub fn sign_in_poll(device_code: &str) -> Result<GitHubSignInState> {
85 Ok(match github::poll_device_flow(GITHUB_OAUTH_CLIENT_ID, device_code)? {
86 DeviceFlowPoll::Pending => GitHubSignInState::Pending,
87 DeviceFlowPoll::SlowDown(interval) => GitHubSignInState::SlowDown(interval),
88 DeviceFlowPoll::Expired => GitHubSignInState::Expired,
89 DeviceFlowPoll::Denied => GitHubSignInState::Denied,
90 DeviceFlowPoll::Granted(token) => {
91 let login = GitHubClient::new(&token)?.user_login()?;
92 keyring_entry()?.set_password(&token).map_err(keyring_error)?;
93 mutate_settings(|settings| settings.set_string(GITHUB_LOGIN, &login))?;
94 GitHubSignInState::SignedIn(login)
95 },
96 })
97}
98
99pub fn account() -> Result<Option<String>> {
109 if token()?.is_none() {
110 return Ok(None);
111 }
112
113 let login = SETTINGS.read().unwrap().string(GITHUB_LOGIN);
114 Ok(Some(login))
115}
116
117pub fn sign_out() -> Result<()> {
123 match keyring_entry()?.delete_credential() {
124 Ok(()) | Err(KeyringError::NoEntry) => {},
125 Err(error) => return Err(keyring_error(error)),
126 }
127
128 mutate_settings(|settings| settings.set_string(GITHUB_LOGIN, ""))?;
129 Ok(())
130}
131
132pub fn submit(game_key: &str, pack_name: &str, src_lang: &str, language: &str) -> Result<SubmitOutcome> {
147 let Some(token) = token()? else {
148 return Ok(SubmitOutcome::SignInRequired);
149 };
150
151 let local_path = translations_local_path()?;
153 let translation = PackTranslation::load(std::slice::from_ref(&local_path), pack_name, game_key, src_lang, language)?;
154 let content = fs::read_to_string(local_path.join(translation.relative_path(game_key)))?;
155
156 let (hub_owner, hub_name) = hub_repository();
157 let client = GitHubClient::new(&token)?;
158 match translation.submit_to_hub(&client, &hub_owner, &hub_name, game_key, &content) {
159 Ok(result) => Ok(SubmitOutcome::Submitted(result)),
160 Err(RLibError::GitHubUnauthorized) => {
161 sign_out()?;
162 Ok(SubmitOutcome::SignInRequired)
163 },
164 Err(error) => Err(error.into()),
165 }
166}
167
168fn token() -> Result<Option<String>> {
170 match keyring_entry()?.get_password() {
171 Ok(token) => Ok(Some(token)),
172 Err(KeyringError::NoEntry) => Ok(None),
173 Err(error) => Err(keyring_error(error)),
174 }
175}
176
177fn keyring_entry() -> Result<Entry> {
179 Entry::new(KEYRING_SERVICE, KEYRING_GITHUB_TOKEN).map_err(keyring_error)
180}
181
182fn keyring_error(error: KeyringError) -> anyhow::Error {
184 anyhow!("Couldn't access the system's keyring, where RPFM stores your GitHub sign-in: {error}")
185}
186
187fn hub_repository() -> (String, String) {
191 #[cfg(debug_assertions)]
192 if let Ok(value) = std::env::var(HUB_OVERRIDE_VAR) {
193 if let Some((owner, name)) = value.split_once('/') {
194 return (owner.to_owned(), name.to_owned());
195 }
196 }
197
198 (TRANSLATIONS_REPO_OWNER.to_owned(), TRANSLATIONS_REPO_NAME.to_owned())
199}