package cmd import ( "bufio" "context" "fmt" "os" "strings" "time" "github.com/ddworken/hishtory/client/data" "github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/lib" "github.com/ddworken/hishtory/shared" "github.com/spf13/cobra" ) var GROUP_ID_MANAGEMENT string = "group_id_management" var redactCmd = &cobra.Command{ Use: "redact", Aliases: []string{"delete"}, Short: "Query for matching commands and remove them from your shell history", Long: "This removes history entries on the current machine and on all remote machines. Supports the same query format as 'hishtory query'.", GroupID: GROUP_ID_MANAGEMENT, DisableFlagParsing: true, Run: func(cmd *cobra.Command, args []string) { ctx := hctx.MakeContext() skipOnlineRedaction := false if !lib.CanReachHishtoryServer(ctx) { fmt.Printf("Cannot reach hishtory backend (is this device offline?) so redaction will only apply to this device and not other synced devices. Would you like to continue with a local-only redaction anyways? [y/N] ") reader := bufio.NewReader(os.Stdin) resp, err := reader.ReadString('\n') lib.CheckFatalError(err) if strings.TrimSpace(resp) != "y" { fmt.Printf("Aborting redact per user response of %#v\n", strings.TrimSpace(resp)) return } skipOnlineRedaction = true } lib.CheckFatalError(lib.RetrieveAdditionalEntriesFromRemote(ctx, "redact")) lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) query := strings.Join(args, " ") lib.CheckFatalError(redact(ctx, query, os.Getenv("HISHTORY_REDACT_FORCE") != "", skipOnlineRedaction)) }, } func redact(ctx context.Context, query string, skipUserConfirmation, skipOnlineRedaction bool) error { tx, err := lib.MakeWhereQueryFromSearch(ctx, hctx.GetDb(ctx), query) if err != nil { return err } var historyEntries []*data.HistoryEntry res := tx.Find(&historyEntries) if res.Error != nil { return res.Error } if skipUserConfirmation { fmt.Printf("Permanently deleting %d entries\n", len(historyEntries)) } else { fmt.Printf("This will permanently delete %d entries, are you sure? [y/N] ", len(historyEntries)) reader := bufio.NewReader(os.Stdin) resp, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("failed to read response: %w", err) } if strings.TrimSpace(resp) != "y" { fmt.Printf("Aborting redact per user response of %#v\n", strings.TrimSpace(resp)) return nil } } tx, err = lib.MakeWhereQueryFromSearch(ctx, hctx.GetDb(ctx), query) if err != nil { return err } res = tx.Delete(&data.HistoryEntry{}) if res.Error != nil { return res.Error } if res.RowsAffected > int64(len(historyEntries))+1 || res.RowsAffected < int64(len(historyEntries))-1 { return fmt.Errorf("DB deleted %d rows, when we only expected to delete %d rows, something may have gone wrong", res.RowsAffected, len(historyEntries)) } err = deleteOnRemoteInstances(ctx, historyEntries) if err != nil && !skipOnlineRedaction { return err } return nil } func deleteOnRemoteInstances(ctx context.Context, historyEntries []*data.HistoryEntry) error { config := hctx.GetConf(ctx) if config.IsOffline { return nil } var deletionRequest shared.DeletionRequest deletionRequest.SendTime = time.Now() deletionRequest.UserId = data.UserId(config.UserSecret) for _, entry := range historyEntries { deletionRequest.Messages.Ids = append(deletionRequest.Messages.Ids, shared.MessageIdentifier{DeviceId: entry.DeviceId, EndTime: entry.EndTime, EntryId: entry.EntryId}, ) } return lib.SendDeletionRequest(ctx, deletionRequest) } func init() { rootCmd.AddCommand(redactCmd) }