2026-01-15 18:18:47 +01:00
|
|
|
package kanboard
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
2026-01-15 20:23:53 +01:00
|
|
|
"fmt"
|
2026-01-15 18:18:47 +01:00
|
|
|
"strconv"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TaskStatus represents the status of a task.
|
|
|
|
|
type TaskStatus int
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// StatusActive represents an open/active task.
|
|
|
|
|
StatusActive TaskStatus = 1
|
|
|
|
|
// StatusInactive represents a closed/inactive task.
|
|
|
|
|
StatusInactive TaskStatus = 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// StringBool is a bool that can be unmarshaled from a string "0" or "1".
|
|
|
|
|
type StringBool bool
|
|
|
|
|
|
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
|
|
|
func (b *StringBool) UnmarshalJSON(data []byte) error {
|
2026-01-15 20:11:58 +01:00
|
|
|
// Try as string first (most common from Kanboard)
|
2026-01-15 18:18:47 +01:00
|
|
|
var s string
|
2026-01-15 20:11:58 +01:00
|
|
|
if err := json.Unmarshal(data, &s); err == nil {
|
|
|
|
|
*b = s == "1" || s == "true"
|
2026-01-15 18:18:47 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-01-15 20:11:58 +01:00
|
|
|
|
|
|
|
|
// Try as number (some Kanboard versions return 0/1)
|
|
|
|
|
var n int
|
|
|
|
|
if err := json.Unmarshal(data, &n); err == nil {
|
|
|
|
|
*b = n != 0
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try as raw bool
|
|
|
|
|
var boolVal bool
|
|
|
|
|
if err := json.Unmarshal(data, &boolVal); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
*b = StringBool(boolVal)
|
2026-01-15 18:18:47 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MarshalJSON implements json.Marshaler.
|
|
|
|
|
func (b StringBool) MarshalJSON() ([]byte, error) {
|
|
|
|
|
if b {
|
|
|
|
|
return []byte(`"1"`), nil
|
|
|
|
|
}
|
|
|
|
|
return []byte(`"0"`), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StringInt is an int that can be unmarshaled from a JSON string.
|
|
|
|
|
type StringInt int
|
|
|
|
|
|
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
|
|
|
func (i *StringInt) UnmarshalJSON(data []byte) error {
|
|
|
|
|
var s string
|
|
|
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
|
|
|
// Try as raw int
|
|
|
|
|
var intVal int
|
|
|
|
|
if err := json.Unmarshal(data, &intVal); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
*i = StringInt(intVal)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if s == "" {
|
|
|
|
|
*i = 0
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
val, err := strconv.Atoi(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
*i = StringInt(val)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StringInt64 is an int64 that can be unmarshaled from a JSON string.
|
|
|
|
|
type StringInt64 int64
|
|
|
|
|
|
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
|
|
|
func (i *StringInt64) UnmarshalJSON(data []byte) error {
|
|
|
|
|
var s string
|
|
|
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
|
|
|
// Try as raw int64
|
|
|
|
|
var intVal int64
|
|
|
|
|
if err := json.Unmarshal(data, &intVal); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
*i = StringInt64(intVal)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if s == "" {
|
|
|
|
|
*i = 0
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
val, err := strconv.ParseInt(s, 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
*i = StringInt64(val)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 20:23:53 +01:00
|
|
|
// IntOrFalse is an int that can be unmarshaled from a JSON int or false.
|
|
|
|
|
// Kanboard API returns false on failure, int (ID) on success for create operations.
|
|
|
|
|
type IntOrFalse int
|
|
|
|
|
|
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
|
|
|
func (i *IntOrFalse) UnmarshalJSON(data []byte) error {
|
|
|
|
|
// Try as int first (success case)
|
|
|
|
|
var n int
|
|
|
|
|
if err := json.Unmarshal(data, &n); err == nil {
|
|
|
|
|
*i = IntOrFalse(n)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try as bool (failure case: false)
|
|
|
|
|
var b bool
|
|
|
|
|
if err := json.Unmarshal(data, &b); err == nil {
|
|
|
|
|
if b {
|
|
|
|
|
*i = 1 // true shouldn't happen, but handle it
|
|
|
|
|
} else {
|
|
|
|
|
*i = 0
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fmt.Errorf("cannot unmarshal %s into IntOrFalse", data)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 18:18:47 +01:00
|
|
|
// Project represents a Kanboard project (board).
|
|
|
|
|
type Project struct {
|
|
|
|
|
ID StringInt `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
IsActive StringBool `json:"is_active"`
|
|
|
|
|
Token string `json:"token"`
|
|
|
|
|
LastModified Timestamp `json:"last_modified"`
|
|
|
|
|
IsPublic StringBool `json:"is_public"`
|
|
|
|
|
IsPrivate StringBool `json:"is_private"`
|
|
|
|
|
DefaultSwimlane string `json:"default_swimlane"`
|
|
|
|
|
ShowDefaultSwimlane StringBool `json:"show_default_swimlane"`
|
|
|
|
|
Identifier string `json:"identifier"`
|
|
|
|
|
StartDate Timestamp `json:"start_date"`
|
|
|
|
|
EndDate Timestamp `json:"end_date"`
|
|
|
|
|
OwnerID StringInt `json:"owner_id"`
|
|
|
|
|
PriorityDefault StringInt `json:"priority_default"`
|
|
|
|
|
PriorityStart StringInt `json:"priority_start"`
|
|
|
|
|
PriorityEnd StringInt `json:"priority_end"`
|
|
|
|
|
Email string `json:"email"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Task represents a Kanboard task.
|
|
|
|
|
type Task struct {
|
|
|
|
|
ID StringInt `json:"id"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
DateCreation Timestamp `json:"date_creation"`
|
|
|
|
|
DateModification Timestamp `json:"date_modification"`
|
|
|
|
|
DateCompleted Timestamp `json:"date_completed"`
|
|
|
|
|
DateStarted Timestamp `json:"date_started"`
|
|
|
|
|
DateDue Timestamp `json:"date_due"`
|
|
|
|
|
ColorID string `json:"color_id"`
|
|
|
|
|
ProjectID StringInt `json:"project_id"`
|
|
|
|
|
ColumnID StringInt `json:"column_id"`
|
|
|
|
|
OwnerID StringInt `json:"owner_id"`
|
|
|
|
|
CreatorID StringInt `json:"creator_id"`
|
|
|
|
|
Position StringInt `json:"position"`
|
|
|
|
|
IsActive StringBool `json:"is_active"`
|
|
|
|
|
Score StringInt `json:"score"`
|
|
|
|
|
CategoryID StringInt `json:"category_id"`
|
|
|
|
|
SwimlaneID StringInt `json:"swimlane_id"`
|
|
|
|
|
Priority StringInt `json:"priority"`
|
|
|
|
|
Reference string `json:"reference"`
|
|
|
|
|
RecurrenceStatus StringInt `json:"recurrence_status"`
|
|
|
|
|
RecurrenceTrigger StringInt `json:"recurrence_trigger"`
|
|
|
|
|
RecurrenceFactor StringInt `json:"recurrence_factor"`
|
|
|
|
|
RecurrenceTimeframe StringInt `json:"recurrence_timeframe"`
|
|
|
|
|
RecurrenceBasedate StringInt `json:"recurrence_basedate"`
|
|
|
|
|
RecurrenceParent StringInt `json:"recurrence_parent"`
|
|
|
|
|
RecurrenceChild StringInt `json:"recurrence_child"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Column represents a Kanboard column.
|
|
|
|
|
type Column struct {
|
|
|
|
|
ID StringInt `json:"id"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Position StringInt `json:"position"`
|
|
|
|
|
ProjectID StringInt `json:"project_id"`
|
|
|
|
|
TaskLimit StringInt `json:"task_limit"`
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Category represents a Kanboard category.
|
|
|
|
|
type Category struct {
|
|
|
|
|
ID StringInt `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
ProjectID StringInt `json:"project_id"`
|
|
|
|
|
ColorID string `json:"color_id"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Comment represents a Kanboard comment.
|
|
|
|
|
type Comment struct {
|
|
|
|
|
ID StringInt `json:"id"`
|
|
|
|
|
TaskID StringInt `json:"task_id"`
|
|
|
|
|
UserID StringInt `json:"user_id"`
|
|
|
|
|
DateCreation Timestamp `json:"date_creation"`
|
|
|
|
|
Content string `json:"comment"`
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Email string `json:"email"`
|
|
|
|
|
AvatarPath string `json:"avatar_path"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TaskLink represents a link between two tasks.
|
|
|
|
|
type TaskLink struct {
|
|
|
|
|
ID StringInt `json:"id"`
|
|
|
|
|
LinkID StringInt `json:"link_id"`
|
|
|
|
|
TaskID StringInt `json:"task_id"`
|
|
|
|
|
OppositeTaskID StringInt `json:"opposite_task_id"`
|
|
|
|
|
Label string `json:"label"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TaskFile represents a file attached to a task.
|
|
|
|
|
type TaskFile struct {
|
|
|
|
|
ID StringInt `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Path string `json:"path"`
|
|
|
|
|
IsImage StringBool `json:"is_image"`
|
|
|
|
|
TaskID StringInt `json:"task_id"`
|
|
|
|
|
DateCreation Timestamp `json:"date_creation"`
|
|
|
|
|
UserID StringInt `json:"user_id"`
|
|
|
|
|
Size StringInt64 `json:"size"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tag represents a Kanboard tag.
|
|
|
|
|
type Tag struct {
|
|
|
|
|
ID StringInt `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
ProjectID StringInt `json:"project_id"`
|
|
|
|
|
ColorID string `json:"color_id"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateTaskRequest is the request payload for creating a task.
|
|
|
|
|
type CreateTaskRequest struct {
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
ProjectID int `json:"project_id"`
|
|
|
|
|
Description string `json:"description,omitempty"`
|
|
|
|
|
ColumnID int `json:"column_id,omitempty"`
|
|
|
|
|
OwnerID int `json:"owner_id,omitempty"`
|
|
|
|
|
CreatorID int `json:"creator_id,omitempty"`
|
|
|
|
|
ColorID string `json:"color_id,omitempty"`
|
|
|
|
|
CategoryID int `json:"category_id,omitempty"`
|
|
|
|
|
DateDue int64 `json:"date_due,omitempty"`
|
|
|
|
|
Score int `json:"score,omitempty"`
|
|
|
|
|
SwimlaneID int `json:"swimlane_id,omitempty"`
|
|
|
|
|
Priority int `json:"priority,omitempty"`
|
|
|
|
|
Reference string `json:"reference,omitempty"`
|
|
|
|
|
Tags []string `json:"tags,omitempty"`
|
|
|
|
|
DateStarted int64 `json:"date_started,omitempty"`
|
|
|
|
|
RecurrenceStatus int `json:"recurrence_status,omitempty"`
|
|
|
|
|
RecurrenceTrigger int `json:"recurrence_trigger,omitempty"`
|
|
|
|
|
RecurrenceFactor int `json:"recurrence_factor,omitempty"`
|
|
|
|
|
RecurrenceTimeframe int `json:"recurrence_timeframe,omitempty"`
|
|
|
|
|
RecurrenceBasedate int `json:"recurrence_basedate,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UpdateTaskRequest is the request payload for updating a task.
|
|
|
|
|
// Pointer fields allow distinguishing between "not set" and "set to zero value".
|
|
|
|
|
type UpdateTaskRequest struct {
|
|
|
|
|
ID int `json:"id"`
|
|
|
|
|
Title *string `json:"title,omitempty"`
|
|
|
|
|
Description *string `json:"description,omitempty"`
|
|
|
|
|
ColorID *string `json:"color_id,omitempty"`
|
|
|
|
|
OwnerID *int `json:"owner_id,omitempty"`
|
|
|
|
|
CategoryID *int `json:"category_id,omitempty"`
|
|
|
|
|
DateDue *int64 `json:"date_due,omitempty"`
|
|
|
|
|
Score *int `json:"score,omitempty"`
|
|
|
|
|
Priority *int `json:"priority,omitempty"`
|
|
|
|
|
Reference *string `json:"reference,omitempty"`
|
|
|
|
|
DateStarted *int64 `json:"date_started,omitempty"`
|
|
|
|
|
RecurrenceStatus *int `json:"recurrence_status,omitempty"`
|
|
|
|
|
RecurrenceTrigger *int `json:"recurrence_trigger,omitempty"`
|
|
|
|
|
RecurrenceFactor *int `json:"recurrence_factor,omitempty"`
|
|
|
|
|
RecurrenceTimeframe *int `json:"recurrence_timeframe,omitempty"`
|
|
|
|
|
RecurrenceBasedate *int `json:"recurrence_basedate,omitempty"`
|
|
|
|
|
Tags []string `json:"tags,omitempty"`
|
|
|
|
|
}
|