commit e939bc5c9de9210530ffc551cde4d0d347fc2826 Author: Andriy Oblivantsev Date: Fri Feb 13 13:16:36 2026 +0100 Initial release: OnlyOffice Project Management API client diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b6b4b8a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Example Author + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0acc816 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# go-onlyoffice + +Go client library for the [OnlyOffice](https://www.onlyoffice.com/) Project Management API. + +## Features + +- Token-based authentication with automatic renewal +- Project CRUD operations (create, read, update, delete) +- Task management (create, update, list, filter) +- Milestone management +- User listing +- Query parameter serialization via struct tags + +## Installation + +```bash +go get github.com/eslider/go-onlyoffice +``` + +## Usage + +```go +package main + +import ( + "fmt" + "log" + + onlyoffice "github.com/eslider/go-onlyoffice" +) + +func main() { + client := onlyoffice.NewClient(onlyoffice.Credentials{ + Url: "https://your-onlyoffice.example.com", + User: "admin@example.com", + Password: "your-password", + }) + + // List projects + projects, err := client.GetProjects() + if err != nil { + log.Fatal(err) + } + for _, p := range projects { + fmt.Printf("Project: %s (ID: %d)\n", *p.Title, *p.ID) + } +} +``` + +Or load credentials from environment variables: + +```go +client := onlyoffice.NewClient(onlyoffice.GetEnvironmentCredentials()) +``` + +## Environment Variables + +| Variable | Description | +|---|---| +| `ONLYOFFICE_URL` | OnlyOffice instance URL | +| `ONLYOFFICE_USER` | Login email or username | +| `ONLYOFFICE_PASS` | Password | + +## API Reference + +### Client + +- `NewClient(credentials)` - Create a new client +- `GetEnvironmentCredentials()` - Load credentials from env vars +- `Auth(credentials)` - Authenticate and get token +- `Query(request, result)` - Execute an API request + +### Projects + +- `GetProjects()` - List all projects +- `CreateProject(req)` - Create a new project +- `UpdateProject(req)` - Update a project +- `DeleteProject(id)` - Delete a project +- `GetProjectMilestones(project)` - Get milestones for a project + +### Tasks + +- `GetTasks(req)` - List tasks with filtering +- `CreateProjectTask(req)` - Create a new task +- `UpdateProjectTask(req)` - Update a task + +### Users + +- `GetUsers()` - List all users + +## License + +[MIT](LICENSE) diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..68c58ab --- /dev/null +++ b/client_test.go @@ -0,0 +1,128 @@ +package onlyoffice + +import ( + "os" + "strings" + "testing" + "time" + + "github.com/joho/godotenv" +) + +// Load environment variables +func init() { + // Load from .env file in the project root + _ = godotenv.Load(".env") +} + +func skipWithoutCredentials(t *testing.T) { + t.Helper() + if os.Getenv("ONLYOFFICE_URL") == "" { + t.Skip("ONLYOFFICE_URL is not set, skipping integration test") + } +} + +func TestNewClient(t *testing.T) { + skipWithoutCredentials(t) + + // Create a new OnlyOffice client + credentials := GetEnvironmentCredentials() + client := NewClient(credentials) + var token *Token + var err error + token, err = client.Auth(&credentials) + + if err != nil { + t.Errorf("Failed to get token: %v", err) + } + if token == nil { + t.Error("Value is empty") + } + + // Clean up: remove all projects start with "Test project" + projects, err := client.GetProjects() + if err != nil { + t.Errorf("Failed to get projects: %v", err) + } + + for _, project := range projects { + if strings.HasPrefix(*project.Title, "Test project") { + prjStatus, err := client.DeleteProject(*project.ID) + if err != nil { + t.Errorf("Failed to delete project: %v", err) + } + if *prjStatus.ID != *project.ID { + t.Errorf("Project ID is not equal: %v != %v", prjStatus.ID, project.ID) + } + } + } + + // Test create project + project, err := client.CreateProject(NewProjectRequest{ + Title: "Test project", + Description: "Test project description", + }) + + if err != nil { + t.Errorf("Failed to create project: %v", err) + } + + // Update project + project, err = client.UpdateProject(ProjectUpdateRequest{ + ID: *project.ID, + Title: "Test project updated", + ResponsibleID: *project.Responsible.ID, + }) + + var task *Task + // Test create project task + task, err = client.CreateProjectTask(NewProjectTaskRequest{ + ProjectId: *project.ID, + Title: "Test task", + Description: "Test task description", + Notify: true, + MilestoneId: 0, + Priority: 0, + // Deadline +2 days + StartDate: Time(time.Now().AddDate(0, 0, -2)), + Deadline: Time(time.Now().AddDate(0, 0, 2)), + }) + + // Update project task + startDate := Time(time.Now().AddDate(0, 0, -14)) + deadline := Time(time.Now().AddDate(0, 0, 3)) + task, err = client.UpdateProjectTask(ProjectTaskUpdateRequest{ + ID: *task.ID, + Title: "Test task updated", + Description: "Test task description updated", + StartDate: &startDate, + Deadline: &deadline, + }) + + if err != nil { + t.Errorf("Failed to create task: %v", err) + } + + if task == nil { + t.Error("Value is empty") + } + + // Delete project + prj, err := client.DeleteProject(*project.ID) + if err != nil { + t.Errorf("Failed to delete project: %v", err) + } + + if *prj.ID != *project.ID { + t.Errorf("Project ID is not equal: %v != %v", prj.ID, project.ID) + } + + // Test list projects + projects, err = client.GetProjects() + if err != nil { + t.Errorf("Failed to get projects: %v", err) + } + if len(projects) == 0 { + t.Error("Value is empty") + } +} diff --git a/examples/basic/main.go b/examples/basic/main.go new file mode 100644 index 0000000..8732705 --- /dev/null +++ b/examples/basic/main.go @@ -0,0 +1,92 @@ +// Package main demonstrates basic usage of the onlyoffice client library. +// +// Set environment variables before running: +// +// export ONLYOFFICE_URL="https://your-instance.onlyoffice.com" +// export ONLYOFFICE_USER="admin@example.com" +// export ONLYOFFICE_PASS="your-password" +// go run ./examples/basic/ +package main + +import ( + "fmt" + "log" + "os" + "time" + + onlyoffice "github.com/eslider/go-onlyoffice" +) + +func main() { + // Load credentials from environment + creds := onlyoffice.GetEnvironmentCredentials() + if creds.Url == "" { + fmt.Fprintln(os.Stderr, "ONLYOFFICE_URL is not set") + os.Exit(1) + } + + client := onlyoffice.NewClient(creds) + + // List all projects + projects, err := client.GetProjects() + if err != nil { + log.Fatalf("Failed to get projects: %v", err) + } + + fmt.Printf("Found %d projects:\n", len(projects)) + for _, p := range projects { + fmt.Printf(" - [%d] %s (tasks: %d)\n", *p.ID, *p.Title, safeInt(p.TaskCountTotal)) + } + + // List users + users, err := client.GetUsers() + if err != nil { + log.Fatalf("Failed to get users: %v", err) + } + + fmt.Printf("\nFound %d users:\n", len(users)) + for _, u := range users { + fmt.Printf(" - %s (%s)\n", safeStr(u.DisplayName), safeStr(u.Email)) + } + + // Create a demo project (uncomment to test) + // project, err := client.CreateProject(onlyoffice.NewProjectRequest{ + // Title: "Demo Project", + // Description: "Created via API example", + // }) + // if err != nil { + // log.Fatalf("Failed to create project: %v", err) + // } + // fmt.Printf("\nCreated project: %s (ID: %d)\n", *project.Title, *project.ID) + + // Create a task in first project (uncomment to test) + // if len(projects) > 0 { + // task, err := client.CreateProjectTask(onlyoffice.NewProjectTaskRequest{ + // Title: "Demo Task", + // Description: "Created via API example", + // ProjectId: *projects[0].ID, + // StartDate: onlyoffice.Time(time.Now()), + // Deadline: onlyoffice.Time(time.Now().AddDate(0, 0, 7)), + // }) + // if err != nil { + // log.Fatalf("Failed to create task: %v", err) + // } + // fmt.Printf("Created task: %s (ID: %d)\n", *task.Title, *task.ID) + // } + + _ = time.Now // suppress unused import if examples are commented out +} + +func safeStr(s *string) string { + if s == nil { + return "" + } + return *s +} + +func safeInt(i *int) int { + if i == nil { + return 0 + } + return *i +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..560cf6d --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/eslider/go-onlyoffice + +go 1.22.2 + +require ( + github.com/google/go-querystring v1.2.0 + github.com/joho/godotenv v1.5.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..42afc94 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/onlyoffice.go b/onlyoffice.go new file mode 100644 index 0000000..d381032 --- /dev/null +++ b/onlyoffice.go @@ -0,0 +1,650 @@ +package onlyoffice + +// OnlyOffice client package + +import ( + "encoding/json" + "fmt" + "github.com/google/go-querystring/query" + "io" + "net/http" + "os" + regexp "regexp" + "strings" + "time" +) + +// NewClient API +func NewClient(c Credentials) *Client { + return &Client{ + client: http.DefaultClient, + credentials: &c, + } +} + +// GetEnvironmentCredentials when using environment variables +func GetEnvironmentCredentials() Credentials { + return Credentials{ + Url: os.Getenv("ONLYOFFICE_URL"), + User: os.Getenv("ONLYOFFICE_USER"), + Password: os.Getenv("ONLYOFFICE_PASS"), + } + +} + +// Credentials of OnlyOffice User +type Credentials struct { + Url string `json:"-"` + User string `json:"userName"` + Password string `json:"password"` +} + +// ToJson for Credentials +func (c Credentials) ToJson() []byte { + b, err := json.Marshal(c) + if err != nil { + return nil + } + return b +} + +// Client of OnlyOffice API uses credentials to get a token and query the API by every request +type Client struct { + client *http.Client // HTTP client + credentials *Credentials // OnlyOffice credentials + token *Token // Authentication token +} + +// MetaResponse Response +type MetaResponse struct { + Count int `json:"count"` + Total int `json:"total"` + Status int `json:"status"` + StatusCode int `json:"statusCode"` +} + +type Permissions struct { + CanEdit *bool `json:"canEdit,omitempty"` // Is user can edit + CanDelete *bool `json:"canDelete,omitempty"` // Is user can delete +} + +// Project struct +type Project struct { + ID *int `json:"id"` + Title *string `json:"title"` + Security map[string]bool `json:"security,omitempty"` + ProjectFolder *json.Number `json:"projectFolder,omitempty"` + Description *string `json:"description"` + Status *int `json:"status"` + + ResponsibleID *string `json:"responsibleId,omitempty"` + Responsible *User `json:"responsible,omitempty"` + + IsPrivate *bool `json:"isPrivate"` + + TaskCount *int `json:"taskCount,omitempty"` + TaskCountTotal *int `json:"taskCountTotal,omitempty"` + MilestoneCount *int `json:"milestoneCount,omitempty"` + DiscussionCount *int `json:"discussionCount,omitempty"` + ParticipantCount *int `json:"participantCount,omitempty"` + TimeTrackingTotal *string `json:"timeTrackingTotal,omitempty"` + DocumentsCount *int `json:"documentsCount,omitempty"` + + IsFollow *bool `json:"isFollow,omitempty"` + + Created *time.Time `json:"created"` + CreatedBy *User `json:"createdBy,omitempty"` + CreatedByID *string `json:"createdById"` + Updated *time.Time `json:"updated"` + UpdatedByID *string `json:"updatedById"` + + Permissions *Permissions `json:",inline,omitempty"` +} + +type Milestone struct { + ID *int64 `json:"id,omitempty"` + Description *string `json:"description,omitempty"` + Title *string `json:"title,omitempty"` + Deadline *time.Time `json:"deadline,omitempty"` + + IsKey *bool `json:"isKey,omitempty"` + IsNotify *bool `json:"isNotify,omitempty"` + + ProjectOwner *ProjectOwner `json:"projectOwner,omitempty"` + Responsible *User `json:"responsible,omitempty"` + + ActiveTaskCount *int64 `json:"activeTaskCount,omitempty"` + ClosedTaskCount *int64 `json:"closedTaskCount,omitempty"` + Status *int64 `json:"status,omitempty"` + + Created *time.Time `json:"created,omitempty"` + CreatedBy *User `json:"createdBy,omitempty"` + + Updated *time.Time `json:"updated,omitempty"` + + *Permissions `json:",inline,omitempty"` +} + +type User struct { + ID *string `json:"id,omitempty"` + UserName *string `json:"userName,omitempty"` + IsVisitor *bool `json:"isVisitor,omitempty"` + FirstName *string `json:"firstName,omitempty"` + LastName *string `json:"lastName,omitempty"` + Email *string `json:"email,omitempty"` + Status *int `json:"status,omitempty"` + ActivationStatus *int `json:"activationStatus,omitempty"` + Terminated any `json:"terminated,omitempty"` + Department *string `json:"department,omitempty"` + WorkFrom *time.Time `json:"workFrom,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + AvatarMedium *string `json:"avatarMedium,omitempty"` + Avatar *string `json:"avatar,omitempty"` + IsAdmin *bool `json:"isAdmin,omitempty"` + IsLDAP *bool `json:"isLDAP,omitempty"` + ListAdminModules []string `json:"listAdminModules,omitempty"` + IsOwner *bool `json:"isOwner,omitempty"` + CultureName *string `json:"cultureName,omitempty"` + IsSSO *bool `json:"isSSO,omitempty"` + AvatarSmall *string `json:"avatarSmall,omitempty"` + QuotaLimit *int `json:"quotaLimit,omitempty"` + UsedSpace *int `json:"usedSpace,omitempty"` + DocsSpace *int `json:"docsSpace,omitempty"` + MailSpace *int `json:"mailSpace,omitempty"` + TalkSpace *int `json:"talkSpace,omitempty"` + ProfileURL *string `json:"profileUrl,omitempty"` + Title *string `json:"title,omitempty"` + Sex *string `json:"sex,omitempty"` + Lead *string `json:"lead,omitempty"` + Birthday *time.Time `json:"birthday,omitempty"` + Location *string `json:"location,omitempty"` + Notes *string `json:"notes,omitempty"` + Contacts []Contact `json:"contacts,omitempty"` + Groups []Group `json:"groups,omitempty"` +} + +type Contact struct { + Type *string `json:"type,omitempty"` + Value *string `json:"value,omitempty"` +} + +type Group struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Manager interface{} `json:"manager,omitempty"` +} + +type Task struct { + ID *int `json:"id,omitempty"` + Title *string `json:"title,omitempty"` + StartDate *time.Time `json:"startDate,omitempty"` + Deadline *time.Time `json:"deadline,omitempty"` + Description *string `json:"description,omitempty"` + Priority *int `json:"priority,omitempty"` + ProjectOwner *ProjectOwner `json:"projectOwner,omitempty"` + + Subtasks []any `json:"subtasks,omitempty"` + + Status *ProjectTaskStatus `json:"status,omitempty"` + + Created *time.Time `json:"created,omitempty"` + CreatedBy *User `json:"createdBy,omitempty"` + CreatedByID *string `json:"createdById,omitempty"` // UUID + + Updated *time.Time `json:"updated,omitempty"` + UpdatedBy *User `json:"updatedBy,omitempty"` + UpdatedById *string `json:"updatedById,omitempty"` // UUID + + Responsibles []*User `json:"responsibles,omitempty"` + ResponsibleIDS []string `json:"responsibleIds,omitempty"` // UUID list + + CanEdit *bool `json:"canEdit,omitempty"` + CanCreateSubtask *bool `json:"canCreateSubtask,omitempty"` + CanCreateTimeSpend *bool `json:"canCreateTimeSpend,omitempty"` + CanDelete *bool `json:"canDelete,omitempty"` + CanReadFiles *bool `json:"canReadFiles,omitempty"` + + MilestoneID *int64 `json:"milestoneId,omitempty"` + Milestone *Milestone `json:"milestone,omitempty"` +} + +var GiteaIssue2OnlyOfficeMappingRegExp = regexp.MustCompile(`URL:(.*)$`) +var TestReg = ` + +URL:https://git.markets-platform.com/TradePlatform/email-templates/issues/1` + +// GetGiteaIssueLink from task description +func (t *Task) GetGiteaIssueLink() string { + + // Get issue from description + if t.Description != nil { + var match = GiteaIssue2OnlyOfficeMappingRegExp.FindStringSubmatch(*t.Description) + if len(match) > 1 { + return strings.TrimSpace(match[1]) + } + } + return "" +} + +// TaskPriority High = 1, Normal = 0, Low = -1 +type TaskPriority int + +const ( + TaskPriorityHigh TaskPriority = 1 // High + TaskPriorityNormal TaskPriority = 0 // Normal + TaskPriorityLow TaskPriority = -1 +) + +type ProjectOwner struct { + ID *int `json:"id,omitempty"` + Title *string `json:"title,omitempty"` + Status *int `json:"status,omitempty"` + IsPrivate *bool `json:"isPrivate,omitempty"` +} + +// String for Project to return title +func (p Project) String() string { + return fmt.Sprintf(*p.Title) +} + +// Token OnlyOffice +type Token struct { + Value string `json:"token"` + Expires Time `json:"expires"` +} + +// Time for OnlyOffice +type Time time.Time + +// To String for Time +func (t Time) String() string { + return time.Time(t).Format("2006-01-02T15:04:05") +} + +func (t Time) Before(u Time) bool { + return time.Time(t).Before(time.Time(u)) +} + +func (t Time) After(u Time) bool { + return time.Time(t).After(time.Time(u)) +} + +// UnmarshalJSON for Time +func (r *Time) UnmarshalJSON(data []byte) error { + // Trim quotes + data = data[1 : len(data)-1] + t, err := time.Parse("2006-01-02T15:04:05.0000000-07:00", string(data)) + if err != nil { + return err + } + *r = Time(t) + return nil +} + +// Request for OnlyOffice API +type Request struct { + Uri string // URI is the path to the API endpoint e.g. /api/2.0/project.json + Method string // Method is the HTTP method e.g. GET, POST, PUT, DELETE + Params any // Params is the query parameters + Body any // Body could be a struct, map, []byte, or string. It will be marshaled to JSON + Token *string // Token is the Authorization header, if not set then it will get a token + NoAuth bool // NoAuth for skipping automatic authentication + Debug bool +} + +// GetMethod for Request +func (r Request) GetMethod() string { + if r.Method == "" { + return "GET" + } + return r.Method +} + +// Query the OnlyOffice API +// - If request.Method is not set then it will default to GET +// - If request.Code is not nil then it will be marshaled to JSON +// - If request.Token is nil then it will get a token +// - If request.Token is not nil then it will be used as Authorization header +// - If request.NoAuth is true then it will skip automatic authentication +func (c *Client) Query(request Request, result interface{}) (err error) { + var url = fmt.Sprintf("%s%s", c.credentials.Url, request.Uri) + var rdr io.Reader = nil + var jsonRequestBody string + + // Add query parameters if available + if request.Params != nil { + v, err := query.Values(request.Params) + if err != nil { + return err + } + url = fmt.Sprintf("%s?%s", url, v.Encode()) + } + + // Create request + if request.Body != nil { + // Check request body if type is not []byte or string the marshal it to JSON + switch request.Body.(type) { + case []byte: + jsonRequestBody = string(request.Body.([]byte)) + rdr = strings.NewReader(string(request.Body.([]byte))) + case string: + jsonRequestBody = request.Body.(string) + rdr = strings.NewReader(request.Body.(string)) + default: + // Marshal to JSON + b, err := json.Marshal(request.Body) + if err != nil { + return fmt.Errorf("failed to marshal request body: %v", err) + } + jsonRequestBody = string(b) + rdr = strings.NewReader(string(b)) + } + } + + req, err := http.NewRequest(request.GetMethod(), url, rdr) + if err != nil { + return err + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Pragma", "no-cache") + + // Get token if not set? + if !request.NoAuth { + // Get token if not set or expired + if c.token == nil || time.Time(c.token.Expires).Before(time.Now()) { + c.token, err = c.Auth(c.credentials) + if err != nil { + return fmt.Errorf("failed to authenticate: %v", err) + } + } + } + + // Set token if available + if request.Token != nil { + req.Header.Set("Authorization", *request.Token) + } else { + // Set token from a client if available + if c.token != nil { + req.Header.Set("Authorization", c.token.Value) + } + } + + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + if result == nil { + return nil + } + if request.Debug { + // Rewind reader + _ = jsonRequestBody + var buf = new(strings.Builder) + io.Copy(buf, resp.Body) + js := buf.String() + return json.Unmarshal([]byte(js), result) + } else { + // Unmarshal response using reader + return json.NewDecoder(resp.Body).Decode(result) + } + +} + +// Auth to authenticate by getting a token using credentials +func (c *Client) Auth(creds *Credentials) (t *Token, err error) { + t = &Token{} + return t, c.Query(Request{ + Uri: "/api/2.0/authentication.json", + Method: "POST", + Body: creds, + NoAuth: true, + }, &struct { // Unmarshal response into a struct + MetaResponse `json:",inline"` + Response *Token `json:"response"` + }{ + Response: t, + }) +} + +// Projects list +type Projects []*Project + +// Get Project by title +func (p Projects) Get(title string) *Project { + for _, prj := range p { + if *prj.Title == title { + return prj + } + } + return nil +} + +// GetProjects +// - Get all projects, not excluding private projects +func (c *Client) GetProjects() (list Projects, err error) { + return list, c.Query(Request{Uri: `/api/2.0/project/filter.json?simple=true`}, + &struct { + MetaResponse `json:",inline"` + Response *Projects + }{Response: &list}) +} + +// GetProjectMilestones Get project milestones +// https://api1.onlyoffice.com/portals/method/project/post/api/2.0/project/%7bid%7d/milestone +func (c *Client) GetProjectMilestones(project *Project) ([]*Milestone, error) { + var list []*Milestone + + err := c.Query(Request{Uri: fmt.Sprintf(`/api/2.0/project/%d/milestone`, *project.ID), Debug: false}, + &struct { + MetaResponse `json:",inline"` + //Response *[]*Milestone + Response *[]*Milestone + }{Response: &list}) + return list, err +} + +// GetUsers List users +func (c *Client) GetUsers() (list []*User, err error) { + return list, c.Query(Request{Uri: "/api/2.0/people/filter.json"}, + &struct { + MetaResponse `json:",inline"` + Response *[]*User `json:"response"` + }{Response: &list}) +} + +// NewProjectRequest +type NewProjectRequest struct { + Title string `json:"title"` + Description string `json:"description"` + ResponsibleID string `json:"responsibleId"` +} + +// ProjectUpdateRequest +type ProjectUpdateRequest struct { + ID int `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + ResponsibleID string `json:"responsibleId,omitempty"` +} + +// NewProjectTaskRequest +type NewProjectTaskRequest struct { + Title string `url:"title"` + Description string `url:"description"` + Notify bool `url:"notify"` + MilestoneId int `url:"milestoneId"` + Priority int `url:"priority"` + ProjectId int `url:"projectId"` + StartDate Time `url:"startDate"` // 2024-04-16T00:00:00 + Deadline Time `url:"deadline"` // 2024-04-20T00:00:00 + Status ProjectTaskStatus +} + +type ProjectTaskStatus int + +const ( + ProjectTaskStatusNotAccept ProjectTaskStatus = 0 + ProjectTaskStatusOpen ProjectTaskStatus = 1 + ProjectTaskStatusClosed ProjectTaskStatus = 2 + ProjectTaskStatusDisable ProjectTaskStatus = 3 + ProjectTaskStatusUnclassified ProjectTaskStatus = 4 + ProjectTaskStatusNotInMilestone ProjectTaskStatus = 5 +) + +// ProjectTaskUpdateRequest - Update a task +// See: https://api1.onlyoffice.com/portals/method/project/put/api/2.0/project/task/%7btaskid%7d +type ProjectTaskUpdateRequest struct { + ID int `json:"id"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Priority *int `json:"priority,omitempty"` // optional, New task priority {"0"} High = 1, Normal = 0, Low = -1 + StartDate *Time `json:"startDate,omitempty"` + Deadline *Time `json:"deadline,omitempty"` + //Deadline *Time `json:"closed,omitempty"` + ProjectID *int64 `json:"projectID,omitempty"` // New task project ID (optional) + MilestoneId *int64 `json:"milestoneid,omitempty"` + Responsible []string `json:"responsibles,omitempty"` // New list of task responsibles {"9924256A-739C-462b-AF15-E652A3B1B6EB"} + Notify bool `json:"notify,omitempty"` + Status ProjectTaskStatus `json:"status,omitempty"` + //Progress int64 `json:"progress,omitempty"` // New task progress +} + +// MarshalJSON to quote Time as string +func (t Time) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf( + `"%s"`, + time.Time(t).Format("2006-01-02T15:04:05"), + )), nil +} + +// CreateProject with a new project request +// - if responsibleID is empty then it will use the first user as responsible +// - if responsibleID is empty and user is not found then it will use the current user +func (c *Client) CreateProject(np NewProjectRequest) (prj *Project, err error) { + // We need a responsible user + if np.ResponsibleID == "" { + var users []*User + users, err = c.GetUsers() + if err != nil { + return + } + + // Find user by email + for _, user := range users { + if *user.Email == c.credentials.User { + np.ResponsibleID = *user.ID + break + } + } + + // Has no user? + if np.ResponsibleID == "" { + // Set first user as responsible + np.ResponsibleID = *users[0].ID + } + } + + prj = new(Project) + return prj, c.Query(Request{ + Uri: "/api/2.0/project.json", + Method: "POST", + Body: np, + }, &struct { + MetaResponse `json:",inline"` + Response *Project `json:"response"` + }{ + Response: prj, + }) +} + +func (c *Client) CreateProjectTask(req NewProjectTaskRequest) (task *Task, err error) { + task = &Task{} + return task, c.Query(Request{ + Uri: fmt.Sprintf("/api/2.0/project/%d/task.json", req.ProjectId), + Method: "POST", + Body: req, + }, &struct { + //MetaResponse `json:",inline"` + Response *Task `json:"response"` + }{task}) +} + +func (c *Client) DeleteProject(id int) (p *Project, err error) { + p = &Project{} + return p, c.Query( + Request{ + Uri: fmt.Sprintf("/api/2.0/project/%d.json", id), + Method: "DELETE"}, + &struct { + //MetaResponse `json:",inline"` + Response *Project `json:"response"` + }{p}) +} + +func (c *Client) UpdateProject(req ProjectUpdateRequest) (p *Project, err error) { + p = &Project{} + return p, c.Query(Request{ + Uri: fmt.Sprintf("/api/2.0/project/%d.json", req.ID), + Method: "PUT", + Body: req, + }, + &struct { + //MetaResponse `json:",inline"` + Response *Project `json:"response"` + }{p}) +} + +func (c *Client) UpdateProjectTask(req ProjectTaskUpdateRequest) (task *Task, err error) { + task = &Task{} + return task, c.Query( + Request{ + Uri: fmt.Sprintf("/api/2.0/project/task/%d.json", req.ID), + Method: "PUT", + Body: req, + Debug: true, + }, &struct { + //MetaResponse `json:",inline"` + Response *Task `json:"response"` + }{task}) +} + +// ProjectGetTasksRequest +// See: https://api.onlyoffice.com/workspace/api-backend/usage-api/project/tasks/get-tasks-by-status/ +type ProjectGetTasksRequest struct { + ProjectId int `url:"projectId"` + Count int `url:"count"` + StartIndex int `url:"startIndex"` + SortBy string `url:"sortBy"` + SortOrder string `url:"sortOrder"` + Simple bool `url:"simple"` +} + +// NewProjectGetTasksRequest to get a simple task list +func NewProjectGetTasksRequest(projectId int) ProjectGetTasksRequest { + return ProjectGetTasksRequest{ + ProjectId: projectId, + Count: 1000, + StartIndex: 0, + SortBy: "title", + SortOrder: "ascending", + Simple: true, + } +} + +// GetTasks returns a list of all the tasks from a project with the ID specified in the request. +func (c *Client) GetTasks(req ProjectGetTasksRequest) (tasks []*Task, err error) { + return tasks, c.Query( + Request{ + Uri: "/api/2.0/project/task/filter.json", + Params: req, + Debug: true, + }, + &struct { + //MetaResponse `json:",inline"` + Response *[]*Task `json:"response"` + }{&tasks}) +}