135 lines
4.3 KiB
Go
135 lines
4.3 KiB
Go
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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)
|
|
}
|