From 1d33c889de2fcd6ffa07ca28665b32fcc6858ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Tue, 28 Jan 2025 13:50:24 +0200 Subject: [PATCH 1/2] Add option for copying troubleshooting information to clipboard This allows users to share relevant information when asking for support e.g. on Discord. --- .../settings-components/SettingsView.vue | 8 +++++++ src/pages/Manager.vue | 8 +++++++ src/r2mm/manager/PackageDexieStore.ts | 4 ++++ src/store/modules/ProfileModule.ts | 23 +++++++++++++++++++ src/store/modules/TsModsModule.ts | 4 ++++ 5 files changed, 47 insertions(+) diff --git a/src/components/settings-components/SettingsView.vue b/src/components/settings-components/SettingsView.vue index 2f345573..5ade507b 100644 --- a/src/components/settings-components/SettingsView.vue +++ b/src/components/settings-components/SettingsView.vue @@ -157,6 +157,14 @@ import CdnProvider from '../../providers/generic/connection/CdnProvider'; 'fa-clipboard', () => this.emitInvoke('CopyLogToClipboard') ), + new SettingsRow( + 'Debugging', + 'Copy troubleshooting information to clipboard', + 'Copy settings and other information to the clipboard, with Discord formatting.', + async () => 'Share this information when requesting support on Discord.', + 'fa-clipboard', + () => this.emitInvoke('CopyTroubleshootingInfoToClipboard') + ), new SettingsRow( 'Debugging', 'Toggle download cache', diff --git a/src/pages/Manager.vue b/src/pages/Manager.vue index b3638808..652f4e3e 100644 --- a/src/pages/Manager.vue +++ b/src/pages/Manager.vue @@ -513,6 +513,11 @@ import ModalCard from '../components/ModalCard.vue'; } } + async copyTroubleshootingInfoToClipboard() { + const content = await this.$store.dispatch('profile/generateTroubleshootingString'); + InteractionProvider.instance.copyToClipboard('```' + content + '```'); + } + async changeDataFolder() { try { const folder = await DataFolderProvider.instance.showSelectionDialog(); @@ -551,6 +556,9 @@ import ModalCard from '../components/ModalCard.vue'; case "CopyLogToClipboard": this.copyLogToClipboard(); break; + case "CopyTroubleshootingInfoToClipboard": + this.copyTroubleshootingInfoToClipboard(); + break; case "ToggleDownloadCache": this.toggleIgnoreCache(); break; diff --git a/src/r2mm/manager/PackageDexieStore.ts b/src/r2mm/manager/PackageDexieStore.ts index c870a267..04c2ce08 100644 --- a/src/r2mm/manager/PackageDexieStore.ts +++ b/src/r2mm/manager/PackageDexieStore.ts @@ -88,6 +88,10 @@ export async function getPackageVersionNumbers(community: string, packageName: s return pkg.versions.map((v) => v.version_number); } +export async function getPackageCount(community: string) { + return await db.packages.where({community}).count(); +} + /** * @param game Game (community) which package listings should be used in the lookup. * @param dependencies Lookup targets as Thunderstore dependency strings. diff --git a/src/store/modules/ProfileModule.ts b/src/store/modules/ProfileModule.ts index b65f6299..0115278d 100644 --- a/src/store/modules/ProfileModule.ts +++ b/src/store/modules/ProfileModule.ts @@ -2,6 +2,7 @@ import { ActionTree, GetterTree } from 'vuex'; import { CachedMod } from './TsModsModule'; import { State as RootState } from '../index'; +import ManagerInformation from '../../_managerinf/ManagerInformation'; import R2Error from '../../model/errors/R2Error'; import ManifestV2 from '../../model/ManifestV2'; import Profile, { ImmutableProfile } from "../../model/Profile"; @@ -11,6 +12,7 @@ import { SortNaming } from '../../model/real_enums/sort/SortNaming'; import ThunderstoreCombo from '../../model/ThunderstoreCombo'; import ThunderstoreMod from '../../model/ThunderstoreMod'; import ConflictManagementProvider from '../../providers/generic/installing/ConflictManagementProvider'; +import GameDirectoryResolverProvider from '../../providers/ror2/game/GameDirectoryResolverProvider'; import ProfileInstallerProvider from '../../providers/ror2/installing/ProfileInstallerProvider'; import ManagerSettings from '../../r2mm/manager/ManagerSettings'; import * as PackageDb from '../../r2mm/manager/PackageDexieStore'; @@ -297,6 +299,27 @@ export default { return await PackageDb.getCombosByDependencyStrings(game, outdated, useLatestVersion); }, + async generateTroubleshootingString({dispatch, getters, rootGetters, rootState}): Promise { + const steamDirectory = await GameDirectoryResolverProvider.instance.getSteamDirectory(); + const steamPath = steamDirectory instanceof R2Error ? '-' : steamDirectory; + const gameDirectory = await GameDirectoryResolverProvider.instance.getDirectory(rootState.activeGame); + const gamePath = gameDirectory instanceof R2Error ? '-' : gameDirectory; + const packageCacheDate = await PackageDb.getLastPackageListUpdateTime(rootState.activeGame.internalFolderName); + const packageCacheSize = await PackageDb.getPackageCount(rootState.activeGame.internalFolderName); + const packageVuexState: string = await dispatch('tsMods/generateTroubleshootingString', null, {root: true}); + + const content = ` + App: ${ManagerInformation.APP_NAME} v${ManagerInformation.VERSION} + Game: ${rootState.activeGame.displayName} (${rootState.activeGame.activePlatform.storePlatform}) + Steam path: ${steamPath} + Game path: ${gamePath} + Profile path: ${getters.activeProfile.getProfilePath()} + Custom launch arguments: ${rootGetters.settings.getContext().gameSpecific.launchParameters || '-'} + Mod list in cache: ${packageCacheSize} mods, hash updated ${packageCacheDate || 'never'} + Mod list in memory: ${packageVuexState} + `; + return content.replace(/(\n)\s+/g, '$1'); // Remove indentation but keep newlines + }, async loadLastSelectedProfile({commit, rootGetters}): Promise { const profileName = rootGetters['settings'].getContext().gameSpecific.lastSelectedProfile; diff --git a/src/store/modules/TsModsModule.ts b/src/store/modules/TsModsModule.ts index d0315cf6..5a98518d 100644 --- a/src/store/modules/TsModsModule.ts +++ b/src/store/modules/TsModsModule.ts @@ -301,6 +301,10 @@ export const TsModsModule = { return updated !== undefined; }, + async generateTroubleshootingString({state}): Promise { + return `${state.mods.length} mods, updated ${state.modsLastUpdated || 'never'}`; + }, + async getActiveGameCacheStatus({commit, state, rootState}): Promise { if (state.isThunderstoreModListUpdateInProgress) { return "Online mod list is currently updating, please wait for the operation to complete"; From bc47bc5fad530444b0abcff4ff5b15d78908269d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Wed, 29 Jan 2025 12:27:18 +0200 Subject: [PATCH 2/2] Obfuscate Windows usernames from troubleshooting info --- src/store/modules/ProfileModule.ts | 7 +- src/utils/FileUtils.ts | 9 +++ .../utils/utils.FileUtils.ts.spec.ts | 69 +++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 test/jest/__tests__/utils/utils.FileUtils.ts.spec.ts diff --git a/src/store/modules/ProfileModule.ts b/src/store/modules/ProfileModule.ts index 0115278d..cbb9cb16 100644 --- a/src/store/modules/ProfileModule.ts +++ b/src/store/modules/ProfileModule.ts @@ -18,6 +18,7 @@ import ManagerSettings from '../../r2mm/manager/ManagerSettings'; import * as PackageDb from '../../r2mm/manager/PackageDexieStore'; import ModListSort from '../../r2mm/mods/ModListSort'; import ProfileModList from '../../r2mm/mods/ProfileModList'; +import FileUtils from '../../utils/FileUtils'; import SearchUtils from '../../utils/SearchUtils'; interface State { @@ -301,9 +302,9 @@ export default { async generateTroubleshootingString({dispatch, getters, rootGetters, rootState}): Promise { const steamDirectory = await GameDirectoryResolverProvider.instance.getSteamDirectory(); - const steamPath = steamDirectory instanceof R2Error ? '-' : steamDirectory; + const steamPath = steamDirectory instanceof R2Error ? '-' : FileUtils.hideWindowsUsername(steamDirectory); const gameDirectory = await GameDirectoryResolverProvider.instance.getDirectory(rootState.activeGame); - const gamePath = gameDirectory instanceof R2Error ? '-' : gameDirectory; + const gamePath = gameDirectory instanceof R2Error ? '-' : FileUtils.hideWindowsUsername(gameDirectory); const packageCacheDate = await PackageDb.getLastPackageListUpdateTime(rootState.activeGame.internalFolderName); const packageCacheSize = await PackageDb.getPackageCount(rootState.activeGame.internalFolderName); const packageVuexState: string = await dispatch('tsMods/generateTroubleshootingString', null, {root: true}); @@ -313,7 +314,7 @@ export default { Game: ${rootState.activeGame.displayName} (${rootState.activeGame.activePlatform.storePlatform}) Steam path: ${steamPath} Game path: ${gamePath} - Profile path: ${getters.activeProfile.getProfilePath()} + Profile path: ${FileUtils.hideWindowsUsername(getters.activeProfile.getProfilePath())} Custom launch arguments: ${rootGetters.settings.getContext().gameSpecific.launchParameters || '-'} Mod list in cache: ${packageCacheSize} mods, hash updated ${packageCacheDate || 'never'} Mod list in memory: ${packageVuexState} diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 2be531fd..3c1d0ee8 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -31,6 +31,15 @@ export default class FileUtils { return Promise.resolve(); } + // Obfuscates the Windows username if it's part of the path. + public static hideWindowsUsername(dir: string) { + const separator = dir.includes('/') ? '/' : '\\'; + return dir.replace( + /([A-Za-z]:)[\\\/]Users[\\\/][^\\\/]+[\\\/]/, + `$1${separator}Users${separator}***${separator}` + ); + } + public static humanReadableSize(bytes: number) { // NumberFormat renders GBs as BBs ("billion bytes") when using "byte" unit type. if (bytes > 999999999 && bytes < 1000000000000) { diff --git a/test/jest/__tests__/utils/utils.FileUtils.ts.spec.ts b/test/jest/__tests__/utils/utils.FileUtils.ts.spec.ts new file mode 100644 index 00000000..21c389a9 --- /dev/null +++ b/test/jest/__tests__/utils/utils.FileUtils.ts.spec.ts @@ -0,0 +1,69 @@ +import FileUtils from "../../../../src/utils/FileUtils"; + +describe("FileUtils.hideWindowsUsername", () => { + it("Doesn't change slashes", () => { + expect( + FileUtils.hideWindowsUsername('C:\\Users\\Alice\\appData') + ).toStrictEqual('C:\\Users\\***\\appData'); + + expect( + FileUtils.hideWindowsUsername('C:/Users/Bob/appData') + ).toStrictEqual('C:/Users/***/appData'); + }); + + it("Doesn't change drive letters", () => { + expect( + FileUtils.hideWindowsUsername('C:\\Users\\Charlie\\appData') + ).toStrictEqual('C:\\Users\\***\\appData'); + + expect( + FileUtils.hideWindowsUsername('x:\\Users\\David\\appData') + ).toStrictEqual('x:\\Users\\***\\appData'); + }); + + it("Doesn't change the rest of the path", () => { + expect( + FileUtils.hideWindowsUsername('C:\\Users\\Eve\\') + ).toStrictEqual('C:\\Users\\***\\'); + + expect( + FileUtils.hideWindowsUsername('C:\\Users\\Frank\\Desktop') + ).toStrictEqual('C:\\Users\\***\\Desktop'); + + expect( + FileUtils.hideWindowsUsername('C:\\Users\\Grace\\Desktop\\') + ).toStrictEqual('C:\\Users\\***\\Desktop\\'); + + expect( + FileUtils.hideWindowsUsername('C:\\Users\\Heidi\\Desktop\\file.txt') + ).toStrictEqual('C:\\Users\\***\\Desktop\\file.txt'); + }); + + it("Doesn't affect other paths", () => { + expect( + FileUtils.hideWindowsUsername('C:\\LUsers\\Ivan\\') + ).toStrictEqual('C:\\LUsers\\Ivan\\'); + + expect( + FileUtils.hideWindowsUsername('C:\\temp\\Users\\Judy\\') + ).toStrictEqual('C:\\temp\\Users\\Judy\\'); + }); + + it("Isn't tricked by odd usernames", () => { + expect( + FileUtils.hideWindowsUsername('C:\\Users\\123\\') + ).toStrictEqual('C:\\Users\\***\\'); + + expect( + FileUtils.hideWindowsUsername('C:\\Users\\@admin\\') + ).toStrictEqual('C:\\Users\\***\\'); + + expect( + FileUtils.hideWindowsUsername('C:\\Users\\_\\') + ).toStrictEqual('C:\\Users\\***\\'); + + expect( + FileUtils.hideWindowsUsername('C:\\Users\\***\\') + ).toStrictEqual('C:\\Users\\***\\'); + }); +});