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::api::github::GitHubSignInState;
25
26use rpfm_lib::error::RLibError;
27use rpfm_lib::games::{TRANSLATIONS_REPO_NAME, TRANSLATIONS_REPO_OWNER};
28use rpfm_lib::integrations::github::{self, DeviceCode, DeviceFlowPoll, GitHubClient, PUBLIC_REPO_SCOPE};
29
30use crate::GITHUB_OAUTH_CLIENT_ID;
31use rpfm_ipc::settings::translations_local_path;
32
33const KEYRING_SERVICE: &str = "rpfm";
35
36const KEYRING_GITHUB_TOKEN: &str = "github_token";
38
39const KEYRING_GITHUB_LOGIN: &str = "github_login";
41
42#[cfg(debug_assertions)]
44const HUB_OVERRIDE_VAR: &str = "RPFM_TRANSLATION_HUB";
45
46pub enum SubmitOutcome {
52
53 Submitted(SubmissionResult),
55
56 SignInRequired,
58}
59
60pub fn sign_in_start() -> Result<DeviceCode> {
70 if GITHUB_OAUTH_CLIENT_ID.is_empty() {
71 return Err(anyhow!("This build of RPFM can't sign in to GitHub, as it was built without a GitHub client ID."));
72 }
73
74 github::request_device_code(GITHUB_OAUTH_CLIENT_ID, PUBLIC_REPO_SCOPE).map_err(From::from)
75}
76
77pub fn sign_in_poll(device_code: &str) -> Result<GitHubSignInState> {
87 Ok(match github::poll_device_flow(GITHUB_OAUTH_CLIENT_ID, device_code)? {
88 DeviceFlowPoll::Pending => GitHubSignInState::Pending,
89 DeviceFlowPoll::SlowDown(interval) => GitHubSignInState::SlowDown(interval),
90 DeviceFlowPoll::Expired => GitHubSignInState::Expired,
91 DeviceFlowPoll::Denied => GitHubSignInState::Denied,
92 DeviceFlowPoll::Granted(token) => {
93 let login = GitHubClient::new(&token)?.user_login()?;
94 keyring_entry(KEYRING_GITHUB_TOKEN)?.set_password(&token).map_err(keyring_error)?;
95 keyring_entry(KEYRING_GITHUB_LOGIN)?.set_password(&login).map_err(keyring_error)?;
96 GitHubSignInState::SignedIn(login)
97 },
98 })
99}
100
101pub fn account() -> Result<Option<String>> {
111 let Some(token) = token()? else {
112 return Ok(None);
113 };
114
115 if let Some(login) = keyring_password(KEYRING_GITHUB_LOGIN)? {
117 return Ok(Some(login));
118 }
119
120 let login = GitHubClient::new(&token)?.user_login()?;
121 keyring_entry(KEYRING_GITHUB_LOGIN)?.set_password(&login).map_err(keyring_error)?;
122 Ok(Some(login))
123}
124
125pub fn sign_out() -> Result<()> {
131 for name in [KEYRING_GITHUB_TOKEN, KEYRING_GITHUB_LOGIN] {
132 match keyring_entry(name)?.delete_credential() {
133 Ok(()) | Err(KeyringError::NoEntry) => {},
134 Err(error) => return Err(keyring_error(error)),
135 }
136 }
137
138 Ok(())
139}
140
141pub fn submit(game_key: &str, pack_name: &str, src_lang: &str, language: &str) -> Result<SubmitOutcome> {
156 let Some(token) = token()? else {
157 return Ok(SubmitOutcome::SignInRequired);
158 };
159
160 let local_path = translations_local_path()?;
162 let translation = PackTranslation::load(std::slice::from_ref(&local_path), pack_name, game_key, src_lang, language)?;
163 let content = fs::read_to_string(local_path.join(translation.relative_path(game_key)))?;
164
165 let (hub_owner, hub_name) = hub_repository();
166 let client = GitHubClient::new(&token)?;
167 match translation.submit_to_hub(&client, &hub_owner, &hub_name, game_key, &content) {
168 Ok(result) => Ok(SubmitOutcome::Submitted(result)),
169 Err(RLibError::GitHubUnauthorized) => {
170 sign_out()?;
171 Ok(SubmitOutcome::SignInRequired)
172 },
173 Err(error) => Err(error.into()),
174 }
175}
176
177fn token() -> Result<Option<String>> {
179 keyring_password(KEYRING_GITHUB_TOKEN)
180}
181
182fn keyring_password(name: &str) -> Result<Option<String>> {
184 match keyring_entry(name)?.get_password() {
185 Ok(password) => Ok(Some(password)),
186 Err(KeyringError::NoEntry) => Ok(None),
187 Err(error) => Err(keyring_error(error)),
188 }
189}
190
191fn keyring_entry(name: &str) -> Result<Entry> {
193 Entry::new(KEYRING_SERVICE, name).map_err(keyring_error)
194}
195
196fn keyring_error(error: KeyringError) -> anyhow::Error {
198 anyhow!("Couldn't access the system's keyring, where RPFM stores your GitHub sign-in: {error}")
199}
200
201fn hub_repository() -> (String, String) {
205 #[cfg(debug_assertions)]
206 if let Ok(value) = std::env::var(HUB_OVERRIDE_VAR) {
207 if let Some((owner, name)) = value.split_once('/') {
208 return (owner.to_owned(), name.to_owned());
209 }
210 }
211
212 (TRANSLATIONS_REPO_OWNER.to_owned(), TRANSLATIONS_REPO_NAME.to_owned())
213}