// Vikunja is a to-do list application to facilitate your life. // Copyright 2018-present Vikunja and contributors. All rights reserved. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package commands import ( "context" "errors" "fmt" "strconv" "strings" "code.vikunja.io/veans/internal/client" "code.vikunja.io/veans/internal/config" "code.vikunja.io/veans/internal/credentials" "code.vikunja.io/veans/internal/output" ) // runtime bundles the artifacts every non-init command needs: parsed config, // credential store, and an authed HTTP client. Loaded lazily by loadRuntime // at command start. type runtime struct { cfg *config.Config store credentials.Store client *client.Client } func loadRuntime() (*runtime, error) { path, err := config.Find("") if err != nil { if errors.Is(err, config.ErrNotFound) { return nil, output.Wrap(output.CodeNotConfigured, err, "no .veans.yml found — run `veans init` in your repo first") } return nil, err } cfg, err := config.Load(path) if err != nil { return nil, err } store := credentials.Default() tok, err := store.Get(cfg.Server, cfg.Bot.Username) if err != nil { return nil, output.Wrap(output.CodeAuth, err, "no token for %s on %s — run `veans login` to mint a fresh one", cfg.Bot.Username, cfg.Server) } c := client.New(cfg.Server, tok) if cfg.HTTPTimeout > 0 { c.HTTPClient.Timeout = cfg.HTTPTimeout } return &runtime{ cfg: cfg, store: store, client: c, }, nil } // resolveTaskID accepts PROJ-NN, #NN, or a bare integer and returns the // numeric task ID. The project identifier from .veans.yml is used to verify // the prefix matches; mismatches error out so an agent can't accidentally // poke a task in the wrong project. func (r *runtime) resolveTaskID(ctx context.Context, raw string) (int64, error) { raw = strings.TrimSpace(raw) if raw == "" { return 0, output.New(output.CodeValidation, "empty task ID") } // #NN form if strings.HasPrefix(raw, "#") { n, err := strconv.ParseInt(raw[1:], 10, 64) if err != nil { return 0, output.Wrap(output.CodeValidation, err, "invalid task ID %q", raw) } return r.lookupByIndex(ctx, n) } // Bare integer — treat as task index in the configured project. if n, err := strconv.ParseInt(raw, 10, 64); err == nil { return r.lookupByIndex(ctx, n) } // PROJ-NN form idx := strings.LastIndex(raw, "-") if idx > 0 && idx < len(raw)-1 { prefix := raw[:idx] num := raw[idx+1:] if r.cfg.ProjectIdentifier != "" && !strings.EqualFold(prefix, r.cfg.ProjectIdentifier) { return 0, output.New(output.CodeValidation, "task %q has identifier %q, but this repo's .veans.yml uses %q", raw, prefix, r.cfg.ProjectIdentifier) } n, err := strconv.ParseInt(num, 10, 64) if err != nil { return 0, output.Wrap(output.CodeValidation, err, "invalid task ID %q", raw) } return r.lookupByIndex(ctx, n) } return 0, output.New(output.CodeValidation, "invalid task ID %q (expected PROJ-NN, #NN, or NN)", raw) } // lookupByIndex resolves a 1-based per-project task index (the NN in // PROJ-NN / #NN) to a numeric task ID by listing the project's tasks and // matching on Index. The cost is one paged GET; we tolerate it because // resolving by index without a dedicated endpoint is the only stable path. func (r *runtime) lookupByIndex(ctx context.Context, index int64) (int64, error) { tasks, err := r.client.ListProjectTasks(ctx, r.cfg.ProjectID, &client.TaskListOptions{ Filter: fmt.Sprintf("index = %d", index), }) if err != nil { return 0, err } for _, t := range tasks { if t.Index == index { return t.ID, nil } } return 0, output.New(output.CodeNotFound, "task %s not found in project %d", r.cfg.FormatTaskID(index), r.cfg.ProjectID) }