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, } } // Defaults holds optional fallbacks used by package-level helpers when callers // pass an empty identifier (calendar or project). Set via (*Client).SetDefaults. type Defaults struct { CalendarID string ProjectID string } // SetDefaults configures optional identifiers used as fallbacks by methods such // as AddEvent (when calendarID == "") or AddTask (when projectID == ""). func (c *Client) SetDefaults(d Defaults) { c.defaults = d } // firstNonEmpty returns the first non-empty trimmed value, or "" if none found. func firstNonEmpty(vals ...string) string { for _, v := range vals { if s := strings.TrimSpace(v); s != "" { return s } } return "" } // GetEnvironmentCredentials reads OnlyOffice credentials from environment. // // Primary variables (documented): // - ONLYOFFICE_URL // - ONLYOFFICE_USER // - ONLYOFFICE_PASS // // Additional aliases accepted for interoperability with sibling tools: // - ONLYOFFICE_HOST (alias for ONLYOFFICE_URL) // - ONLYOFFICE_NAME (alias for ONLYOFFICE_USER) // - ONLYOFFICE_PASSWORD (alias for ONLYOFFICE_PASS) func GetEnvironmentCredentials() Credentials { url := firstNonEmpty(os.Getenv("ONLYOFFICE_URL"), os.Getenv("ONLYOFFICE_HOST")) url = strings.TrimRight(url, "/") return Credentials{ Url: url, User: firstNonEmpty(os.Getenv("ONLYOFFICE_USER"), os.Getenv("ONLYOFFICE_NAME")), Password: firstNonEmpty(os.Getenv("ONLYOFFICE_PASS"), os.Getenv("ONLYOFFICE_PASSWORD")), } } // GetEnvironmentDefaults reads optional library defaults from environment: // // - ONLYOFFICE_CALENDAR_ID (default: "1") // - ONLYOFFICE_PROJECT_ID (alias: ONLYOFFICE_CALENDAR_PROJECT_ID; default: "33") func GetEnvironmentDefaults() Defaults { return Defaults{ CalendarID: firstNonEmpty(os.Getenv("ONLYOFFICE_CALENDAR_ID"), "1"), ProjectID: firstNonEmpty( os.Getenv("ONLYOFFICE_PROJECT_ID"), os.Getenv("ONLYOFFICE_CALENDAR_PROJECT_ID"), "33", ), } } // 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 defaults Defaults // optional fallbacks for calendar/project IDs selfID string // cached /api/2.0/people/@self id noteCatID int // cached CRM history category id for "note" } // 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}) }