From c7e122141bf512310a98f46eccdae62ded2ca347 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Thu, 2 Oct 2025 10:41:22 +0200
Subject: [PATCH] groupware: add mock endpoints for tasklists and tasks
---
pkg/jmap/jmap_api_email.go | 2 +-
pkg/jmap/jmap_model.go | 148 ++++++++++---
services/groupware/api-params.yaml | 2 +
services/groupware/apidoc.yml | 10 +
.../pkg/groupware/groupware_api_calendars.go | 4 +-
.../pkg/groupware/groupware_api_tasklists.go | 89 ++++++++
.../pkg/groupware/groupware_framework.go | 17 --
.../pkg/groupware/groupware_mock_tasks.go | 207 ++++++++++++++++++
.../pkg/groupware/groupware_route.go | 6 +
9 files changed, 440 insertions(+), 45 deletions(-)
create mode 100644 services/groupware/pkg/groupware/groupware_api_tasklists.go
create mode 100644 services/groupware/pkg/groupware/groupware_mock_tasks.go
diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go
index 8aeb73e48c..0f42197a94 100644
--- a/pkg/jmap/jmap_api_email.go
+++ b/pkg/jmap/jmap_api_email.go
@@ -799,7 +799,7 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx
invocations[i*2+0] = invocation(CommandEmailQuery, EmailQueryCommand{
AccountId: accountId,
Filter: filter,
- Sort: []EmailComparator{EmailComparator{Property: emailSortByReceivedAt, IsAscending: false}},
+ Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
Limit: limit,
//CalculateTotal: false,
}, mcid(accountId, "0"))
diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go
index 29b9f6aeda..6335671544 100644
--- a/pkg/jmap/jmap_model.go
+++ b/pkg/jmap/jmap_model.go
@@ -114,7 +114,20 @@ type ResourceType string
// The Scope data type is used to represent the entities the quota applies to.
type Scope string
+// Determines which action must be performed by the MUA or MTA upon receiption.
+//
+// !- `manual-action`: the disposition described by the disposition type was a result of an
+// explicit instruction by the user rather than some sort of automatically performed action.
+// (This might include the case when the user has manually configured her MUA to automatically
+// respond to valid MDN requests.) Unless prescribed otherwise in a particular mail environment,
+// in order to preserve the user's privacy, this MUST be the default for MUAs.
+// !- `automatic-action`: the disposition described by the disposition type was a result of an
+// automatic action rather than an explicit instruction by the user for this message. This
+// is typically generated by a Mail Delivery Agent (e.g., MDN generations by Sieve reject action
+// [RFC5429], Fax-over-Email [RFC3249], voice message system (see Voice Profile for Internet
+// Mail (VPIM) [RFC3801]), or upon delivery to a mailing list).
type ActionMode string
+
type SendingMode string
type DispositionTypeOption string
@@ -184,11 +197,16 @@ const (
CalendarEventNotificationTypeOptionUpdated = CalendarEventNotificationTypeOption("updated")
CalendarEventNotificationTypeOptionDestroyed = CalendarEventNotificationTypeOption("destroyed")
+ // This represents a single person.
PrincipalTypeOptionIndividual = PrincipalTypeOption("individual")
- PrincipalTypeOptionGroup = PrincipalTypeOption("group")
- PrincipalTypeOptionResource = PrincipalTypeOption("resource")
- PrincipalTypeOptionLocation = PrincipalTypeOption("location")
- PrincipalTypeOptionOther = PrincipalTypeOption("other")
+ // This represents a group of people.
+ PrincipalTypeOptionGroup = PrincipalTypeOption("group")
+ // This represents some resource, e.g. a projector.
+ PrincipalTypeOptionResource = PrincipalTypeOption("resource")
+ // This represents a location.
+ PrincipalTypeOptionLocation = PrincipalTypeOption("location")
+ // This represents some other undefined principal.
+ PrincipalTypeOptionOther = PrincipalTypeOption("other")
HttpDigestAlgorithmAdler32 = HttpDigestAlgorithm("adler32")
HttpDigestAlgorithmCrc32c = HttpDigestAlgorithm("crc32c")
@@ -216,20 +234,64 @@ const (
// The quota information applies to all accounts belonging to the server.
ScopeGlobal = Scope("global")
- ActionModeManualAction = ActionMode("manual-action")
+ // The disposition described by the disposition type was a result of an explicit instruction by the
+ // user rather than some sort of automatically performed action.
+ //
+ // (This might include the case when the user has manually configured her MUA to automatically
+ // respond to valid MDN requests.)
+ //
+ // Unless prescribed otherwise in a particular mail environment, in order to preserve the user's
+ // privacy, this MUST be the default for MUAs.
+ ActionModeManualAction = ActionMode("manual-action")
+
+ // The disposition described by the disposition type was a result of an automatic action rather than
+ // an explicit instruction by the user for this message.
+ //
+ // This is typically generated by a Mail Delivery Agent (e.g., MDN generations by Sieve reject
+ // action [RFC5429], Fax-over-Email [RFC3249], voice message system (see Voice Profile
+ // for Internet Mail (VPIM) [RFC3801]), or upon delivery to a mailing list).
ActionModeAutomaticAction = ActionMode("automatic-action")
- SendingModeMdnSentManually = SendingMode("mdn-sent-manually")
+ // The user explicitly gave permission for this particular MDN to be sent.
+ //
+ // Unless prescribed otherwise in a particular mail environment, in order to preserve the
+ // user's privacy, this MUST be the default for MUAs.
+ SendingModeMdnSentManually = SendingMode("mdn-sent-manually")
+
+ // The MDN was sent because the MUA had previously been configured to do so automatically.
SendingModeMdnSentAutomatically = SendingMode("mdn-sent-automatically")
- DispositionTypeOptionDeleted = DispositionTypeOption("deleted")
- DispositionTypeOptionDispatched = DispositionTypeOption("dispatched")
- DispositionTypeOptionDisplayed = DispositionTypeOption("displayed")
- DispositionTypeOptionProcessed = DispositionTypeOption("processed")
+ // The message has been deleted.
+ //
+ // The recipient may or may not have seen the message.
+ //
+ // The recipient might "undelete" the message at a later time and read the message.
+ DispositionTypeOptionDeleted = DispositionTypeOption("deleted")
- IncludeInAvailabilityAll = IncludeInAvailability("all")
+ // The message has been sent somewhere in some manner (e.g., printed, faxed, forwarded) without
+ // necessarily having been previously displayed to the user.
+ //
+ // The user may or may not see the message later.
+ DispositionTypeOptionDispatched = DispositionTypeOption("dispatched")
+
+ // The message has been displayed by the MUA to someone reading the recipient's mailbox.
+ //
+ // There is no guarantee that the content has been read or understood.
+ DispositionTypeOptionDisplayed = DispositionTypeOption("displayed")
+
+ // The message has been processed in some manner (i.e., by some sort of rules or server)
+ // without being displayed to the user.
+ //
+ // The user may or may not see the message later, or there may not even be a human user
+ // associated with the mailbox.
+ DispositionTypeOptionProcessed = DispositionTypeOption("processed")
+
+ // All events are considered.
+ IncludeInAvailabilityAll = IncludeInAvailability("all")
+ // Events the user is a confirmed or tentative participant of are considered.
IncludeInAvailabilityAttending = IncludeInAvailability("attending")
- IncludeInAvailabilityNone = IncludeInAvailability("none")
+ // All events are ignored (but may be considered if also in another calendar).
+ IncludeInAvailabilityNone = IncludeInAvailability("none")
)
var (
@@ -540,16 +602,16 @@ type SessionMDNAccountCapabilities struct {
}
type SessionAccountCapabilities struct {
- Mail SessionMailAccountCapabilities `json:"urn:ietf:params:jmap:mail"`
- Submission SessionSubmissionAccountCapabilities `json:"urn:ietf:params:jmap:submission"`
- VacationResponse SessionVacationResponseAccountCapabilities `json:"urn:ietf:params:jmap:vacationresponse"`
- Sieve SessionSieveAccountCapabilities `json:"urn:ietf:params:jmap:sieve"`
- Blob SessionBlobAccountCapabilities `json:"urn:ietf:params:jmap:blob"`
- Quota SessionQuotaAccountCapabilities `json:"urn:ietf:params:jmap:quota"`
- Contacts SessionContactsAccountCapabilities `json:"urn:ietf:params:jmap:contacts"`
- Principals *SessionPrincipalsAccountCapabilities `json:"urn:ietf:params:jmap:principals,omitempty"`
- PrincipalsOwner *SessionPrincipalsOwnerAccountCapabilities `json:"urn:ietf:params:jmap:principals:owner,omitempty"`
- MDN *SessionMDNAccountCapabilities `json:"urn:ietf:params:jmap:mdn,omitempty"`
+ Mail *SessionMailAccountCapabilities `json:"urn:ietf:params:jmap:mail,omitempty"`
+ Submission *SessionSubmissionAccountCapabilities `json:"urn:ietf:params:jmap:submission,omitempty"`
+ VacationResponse *SessionVacationResponseAccountCapabilities `json:"urn:ietf:params:jmap:vacationresponse,omitempty"`
+ Sieve *SessionSieveAccountCapabilities `json:"urn:ietf:params:jmap:sieve,omitempty"`
+ Blob *SessionBlobAccountCapabilities `json:"urn:ietf:params:jmap:blob,omitempty"`
+ Quota *SessionQuotaAccountCapabilities `json:"urn:ietf:params:jmap:quota,omitempty"`
+ Contacts *SessionContactsAccountCapabilities `json:"urn:ietf:params:jmap:contacts,omitempty"`
+ Principals *SessionPrincipalsAccountCapabilities `json:"urn:ietf:params:jmap:principals,omitempty"`
+ PrincipalsOwner *SessionPrincipalsOwnerAccountCapabilities `json:"urn:ietf:params:jmap:principals:owner,omitempty"`
+ MDN *SessionMDNAccountCapabilities `json:"urn:ietf:params:jmap:mdn,omitempty"`
}
type Account struct {
@@ -558,7 +620,8 @@ type Account struct {
// This is true if the account belongs to the authenticated user rather than a group account or a personal account of another user that has been shared with them.
IsPersonal bool `json:"isPersonal"`
// This is true if the entire account is read-only.
- IsReadOnly bool `json:"isReadOnly"`
+ IsReadOnly bool `json:"isReadOnly"`
+ // Account capabilities.
AccountCapabilities SessionAccountCapabilities `json:"accountCapabilities"`
}
@@ -4119,10 +4182,45 @@ type Principal struct {
Accounts map[string]Account `json:"accounts,omitempty"`
}
-// TODO https://jmap.io/spec-sharing.html#object-properties
-type ShareNotification struct {
+type ShareChangePerson struct {
+ // The name of the person who made the change.
+ Name string `json:"name"`
+ // The email of the person who made the change, or null if no email is available.
+ Email string `json:"email,omitempty"`
+ // The id of the Principal corresponding to the person who made the change, or null if no associated principal.
+ PrincipalId string `json:"principalId,omitempty"`
}
+type ShareNotification struct {
+ // The id of the `ShareNotification`.
+ Id string `json:"id"`
+
+ // The time this notification was created.
+ Created UTCDate `json:"created,omitzero"`
+
+ // Who made the change.
+ ChangedBy ShareChangePerson `json:"changedBy"`
+
+ // The name of the data type for the object whose permissions have changed, e.g. `Calendar` or `Mailbox`.
+ ObjectType ObjectType `json:"objectType"`
+
+ // The id of the account where this object exists.
+ ObjectAccountId string `json:"objectAccountId"`
+
+ // The id of the object that this notification is about.
+ ObjectId string `json:"objectId"`
+
+ // The name of the object at the time the notification was made.
+ Name string `json:"name"`
+
+ // The `myRights` property of the object for the user before the change.
+ OldRights map[string]bool `json:"oldRights,omitempty"`
+
+ // The `myRights` property of the object for the user after the change.
+ NewRights map[string]bool `json:"newRights,omitempty"`
+}
+
+// TODO unused
type Shareable struct {
// Has the user indicated they wish to see this data?
//
diff --git a/services/groupware/api-params.yaml b/services/groupware/api-params.yaml
index 2b5b56ed1e..20a2e78c63 100644
--- a/services/groupware/api-params.yaml
+++ b/services/groupware/api-params.yaml
@@ -9,3 +9,5 @@ params:
description: The identifier of the AddressBook to perform this operation on
calendarid:
description: The identifier of the Calendar to perform this operation on
+ tasklistid:
+ description: The identifier of the TaskList to perform this operation on
diff --git a/services/groupware/apidoc.yml b/services/groupware/apidoc.yml
index c4bcbc26f3..d0fd057647 100644
--- a/services/groupware/apidoc.yml
+++ b/services/groupware/apidoc.yml
@@ -29,6 +29,12 @@ tags:
- name: event
x-displayName: Events
description: APIs about calendar events
+ - name: tasklist
+ x-displayName: TaskLists
+ description: APIs about task lists
+ - name: task
+ x-displayName: Tasks
+ description: APIs about tasks
- name: vacation
x-displayName: Vacation Responses
description: APIs about vacation responses
@@ -53,6 +59,10 @@ x-tagGroups:
tags:
- calendar
- event
+ - name: Tasks
+ tags:
+ - tasklist
+ - task
components:
securitySchemes:
api:
diff --git a/services/groupware/pkg/groupware/groupware_api_calendars.go b/services/groupware/pkg/groupware/groupware_api_calendars.go
index 2cab9bf5e1..c9bfa351f3 100644
--- a/services/groupware/pkg/groupware/groupware_api_calendars.go
+++ b/services/groupware/pkg/groupware/groupware_api_calendars.go
@@ -39,7 +39,7 @@ type SwaggerGetCalendarById200 struct {
}
// swagger:route GET /groupware/accounts/{account}/calendars/{calendarid} calendar calendar_by_id
-// Get all calendars of an account.
+// Get a calendar of an account by its identifier.
//
// responses:
//
@@ -68,7 +68,7 @@ type SwaggerGetEventsInCalendar200 struct {
}
// swagger:route GET /groupware/accounts/{account}/calendars/{calendarid}/events event events_in_addressbook
-// Get all the events in a calendarof an account by its identifier.
+// Get all the events in a calendar of an account by its identifier.
//
// responses:
//
diff --git a/services/groupware/pkg/groupware/groupware_api_tasklists.go b/services/groupware/pkg/groupware/groupware_api_tasklists.go
new file mode 100644
index 0000000000..878935cc1f
--- /dev/null
+++ b/services/groupware/pkg/groupware/groupware_api_tasklists.go
@@ -0,0 +1,89 @@
+package groupware
+
+import (
+ "net/http"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/opencloud-eu/opencloud/pkg/jmap"
+)
+
+// When the request succeeds.
+// swagger:response GetTaskLists200
+type SwaggerGetTaskLists200 struct {
+ // in: body
+ Body []jmap.TaskList
+}
+
+// swagger:route GET /groupware/accounts/{account}/tasklists tasklist tasklists
+// Get all tasklists of an account.
+//
+// responses:
+//
+// 200: GetTaskLists200
+// 400: ErrorResponse400
+// 404: ErrorResponse404
+// 500: ErrorResponse500
+func (g *Groupware) GetTaskLists(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ return response(AllTaskLists, req.session.State)
+ })
+}
+
+// When the request succeeds.
+// swagger:response GetTaskListById200
+type SwaggerGetTaskListById200 struct {
+ // in: body
+ Body struct {
+ *jmap.TaskList
+ }
+}
+
+// swagger:route GET /groupware/accounts/{account}/tasklists/{tasklistid} tasklist tasklist_by_id
+// Get a tasklist by its identifier.
+//
+// responses:
+//
+// 200: GetTaskListById200
+// 400: ErrorResponse400
+// 404: ErrorResponse404
+// 500: ErrorResponse500
+func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ tasklistId := chi.URLParam(r, UriParamTaskListId)
+ // TODO replace with proper implementation
+ for _, tasklist := range AllTaskLists {
+ if tasklist.Id == tasklistId {
+ return response(tasklist, req.session.State)
+ }
+ }
+ return notFoundResponse(req.session.State)
+ })
+}
+
+// When the request succeeds.
+// swagger:response GetTasksInTaskList200
+type SwaggerGetTasksInTaskList200 struct {
+ // in: body
+ Body []jmap.Task
+}
+
+// swagger:route GET /groupware/accounts/{account}/tasklists/{tasklistid}/tasks task tasks_in_tasklist
+// Get all the tasks in a tasklist of an account by its identifier.
+//
+// responses:
+//
+// 200: GetTasksInTaskList200
+// 400: ErrorResponse400
+// 404: ErrorResponse404
+// 500: ErrorResponse500
+func (g *Groupware) GetTasksInTaskList(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ tasklistId := chi.URLParam(r, UriParamTaskListId)
+ // TODO replace with proper implementation
+ tasks, ok := TaskMapByTaskListId[tasklistId]
+ if !ok {
+ return notFoundResponse(req.session.State)
+ }
+ return response(tasks, req.session.State)
+ })
+}
diff --git a/services/groupware/pkg/groupware/groupware_framework.go b/services/groupware/pkg/groupware/groupware_framework.go
index 68ec68929c..8f475906b9 100644
--- a/services/groupware/pkg/groupware/groupware_framework.go
+++ b/services/groupware/pkg/groupware/groupware_framework.go
@@ -497,23 +497,6 @@ func (g *Groupware) serveError(w http.ResponseWriter, r *http.Request, error *Er
render.Render(w, r, errorResponses(*error))
}
-func (g *Groupware) getSession(user user) (*jmap.Session, *GroupwareError) {
- session, ok, gwerr := g.session(user, g.logger)
- if gwerr != nil {
- g.metrics.SessionFailureCounter.Inc()
- g.logger.Error().Str("code", gwerr.Code).Str("error", gwerr.Title).Str("detail", gwerr.Detail).Msg("failed to determine JMAP session")
- return nil, gwerr
- }
- if !ok {
- // no session = authentication failed
- gwerr = &ErrorInvalidAuthentication
- g.metrics.SessionFailureCounter.Inc()
- g.logger.Error().Msg("could not authenticate, failed to find Session")
- return nil, gwerr
- }
- return &session, nil
-}
-
func (g *Groupware) withSession(w http.ResponseWriter, r *http.Request, handler func(r Request) Response) (Response, bool) {
ctx := r.Context()
sl := g.logger.SubloggerWithRequestID(ctx)
diff --git a/services/groupware/pkg/groupware/groupware_mock_tasks.go b/services/groupware/pkg/groupware/groupware_mock_tasks.go
new file mode 100644
index 0000000000..a9a8c8ddd2
--- /dev/null
+++ b/services/groupware/pkg/groupware/groupware_mock_tasks.go
@@ -0,0 +1,207 @@
+package groupware
+
+import (
+ "github.com/opencloud-eu/opencloud/pkg/jmap"
+ "github.com/opencloud-eu/opencloud/pkg/jscalendar"
+)
+
+var TL1 = jmap.TaskList{
+ Id: "aemua9ai",
+ Role: jmap.TaskListRoleInbox,
+ Name: "Your Tasks",
+ Description: "Your default list of tasks",
+ Color: "purple",
+ KeywordColors: map[string]string{
+ "todo": "blue",
+ "done": "green",
+ },
+ CategoryColors: map[string]string{
+ "work": "magenta",
+ },
+ SortOrder: 1,
+ IsSubscribed: true,
+ TimeZone: "CEST",
+ WorkflowStatuses: []string{
+ "new", "todo", "in-progress", "done",
+ },
+ ShareWith: map[string]jmap.TaskRights{
+ "eefeeb4p": {
+ MayReadItems: true,
+ MayWriteAll: false,
+ MayWriteOwn: true,
+ MayUpdatePrivate: false,
+ MayRSVP: false,
+ MayAdmin: false,
+ MayDelete: false,
+ },
+ },
+ MyRights: &jmap.TaskRights{
+ MayReadItems: true,
+ MayWriteAll: true,
+ MayWriteOwn: true,
+ MayUpdatePrivate: true,
+ MayRSVP: true,
+ MayAdmin: false,
+ MayDelete: false,
+ },
+ DefaultAlertsWithTime: map[string]jscalendar.Alert{
+ "saenee7a": {
+ Type: jscalendar.AlertType,
+ Trigger: jscalendar.OffsetTrigger{
+ Type: jscalendar.OffsetTriggerType,
+ Offset: "-PT10M",
+ RelativeTo: jscalendar.RelativeToStart,
+ },
+ Action: jscalendar.AlertActionEmail,
+ },
+ },
+ DefaultAlertsWithoutTime: map[string]jscalendar.Alert{
+ "xiipaew9": {
+ Type: jscalendar.AlertType,
+ Trigger: jscalendar.OffsetTrigger{
+ Type: jscalendar.OffsetTriggerType,
+ Offset: "-PT12H",
+ RelativeTo: jscalendar.RelativeToStart,
+ },
+ Action: jscalendar.AlertActionDisplay,
+ },
+ },
+}
+
+var T1 = jmap.Task{
+ Id: "laoj0ahk",
+ TaskListId: TL1.Id,
+ IsDraft: false,
+ UtcStart: jmap.UTCDate{Time: mustParseTime("2025-10-02T10:00:00Z")},
+ UtcDue: jmap.UTCDate{Time: mustParseTime("2025-10-12T18:00:00Z")},
+ SortOrder: 1,
+ WorkflowStatus: "new",
+ Task: jscalendar.Task{
+ Type: jscalendar.TaskType,
+ Object: jscalendar.Object{
+ CommonObject: jscalendar.CommonObject{
+ Uid: "7da0d4a2-385c-430f-9022-61db302734d9",
+ ProdId: "Mock 0.0",
+ Created: mustParseTime("2025-10-01T17:31:49Z"),
+ Updated: mustParseTime("2025-10-01T17:35:12Z"),
+ Title: "Crossing the Ring",
+ Description: "We need to cross the Ring the protomolecule opened.",
+ DescriptionContentType: "text/plain",
+ Links: map[string]jscalendar.Link{
+ "theisha5": {
+ Type: jscalendar.LinkType,
+ Href: "https://static.wikia.nocookie.net/expanse/images/e/ed/S03E09-SlowZone_01.jpg/revision/latest/scale-to-width-down/1000?cb=20180611184722",
+ ContentType: "image/jpeg",
+ Size: 109212,
+ Rel: jscalendar.RelIcon,
+ Display: "sol gate",
+ Title: "The Sol Ring Gate",
+ },
+ },
+ Locale: "en-GB",
+ Keywords: map[string]bool{
+ "todo": true,
+ },
+ Categories: map[string]bool{
+ "work": true,
+ },
+ Color: "yellow",
+ },
+ Sequence: 1,
+ ShowWithoutTime: false,
+ Locations: map[string]jscalendar.Location{
+ "ruoth5uu": {
+ Type: jscalendar.LocationType,
+ Name: "Sol Gate",
+ Description: "We meet at the Sol gate",
+ LocationTypes: map[jscalendar.LocationTypeOption]bool{
+ jscalendar.LocationTypeOptionLandmarkAddress: true,
+ },
+ RelativeTo: jscalendar.LocationRelationStart,
+ TimeZone: "UTC",
+ Coordinates: "geo:40.4165583,-3.7063595",
+ Links: map[string]jscalendar.Link{
+ "jeeshei5": {
+ Type: jscalendar.LinkType,
+ Href: "https://expanse.fandom.com/wiki/Sol_gate",
+ ContentType: "text/html",
+ Title: "The Sol Gate",
+ },
+ },
+ },
+ },
+ Priority: 1,
+ FreeBusyStatus: jscalendar.FreeBusyStatusBusy,
+ Privacy: jscalendar.PrivacySecret,
+ Alerts: map[string]jscalendar.Alert{
+ "eiphuw4a": {
+ Type: jscalendar.AlertType,
+ Trigger: jscalendar.AbsoluteTrigger{
+ Type: jscalendar.AbsoluteTriggerType,
+ When: mustParseTime("2025-12-01T10:11:12Z"),
+ },
+ Action: jscalendar.AlertActionDisplay,
+ },
+ },
+ TimeZone: "UTC",
+ MayInviteSelf: true,
+ MayInviteOthers: true,
+ HideAttendees: true,
+ },
+ Due: jscalendar.LocalDateTime{Time: mustParseTime("2025-12-01T10:11:12Z")},
+ Start: jscalendar.LocalDateTime{Time: mustParseTime("2025-10-01T08:00:00Z")},
+ EstimatedDuration: "PT8W",
+ PercentComplete: 5,
+ Progress: jscalendar.ProgressNeedsAction,
+ ProgressUpdated: mustParseTime("2025-10-01T08:12:39Z"),
+ },
+ EstimatedWork: 4,
+ Impact: "block",
+ IsOrigin: true,
+ MayInviteSelf: true,
+ MayInviteOthers: true,
+ HideAttendees: false,
+ Checklists: map[string]jmap.Checklist{
+ "sae9aimu": {
+ Type: jmap.ChecklistType,
+ Title: "Prerequisites",
+ CheckItems: []jmap.CheckItem{
+ {
+ Type: jmap.CheckItemType,
+ Title: "Control Medina Station",
+ SortOrder: 1,
+ IsComplete: true,
+ Updated: jmap.UTCDate{Time: mustParseTime("2025-04-01T09:32:10Z")},
+ Assignee: &jmap.TaskPerson{
+ Type: jmap.TaskPersonType,
+ Name: "Fred Johnson",
+ Uri: "mailto:johnson@opa.org",
+ PrincipalId: "nae5hu9t",
+ },
+ Comments: map[string]jmap.Comment{
+ "ooze1iet": {
+ Type: jmap.CommentType,
+ Message: "We first need to control Medina Station before we can get through the Sol Gate",
+ Created: jmap.UTCDate{Time: mustParseTime("2025-04-01T12:11:10Z")},
+ Updated: jmap.UTCDate{Time: mustParseTime("2025-04-01T12:29:19Z")},
+ Author: &jmap.TaskPerson{
+ Type: jmap.TaskPersonType,
+ Name: "Anderson Dawes",
+ Uri: "mailto:adawes@opa.org",
+ PrincipalId: "eshi9oot",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+}
+
+var AllTaskLists = []jmap.TaskList{TL1}
+
+var TaskMapByTaskListId = map[string][]jmap.Task{
+ TL1.Id: {
+ T1,
+ },
+}
diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go
index 82f717af74..8a49cc1bc6 100644
--- a/services/groupware/pkg/groupware/groupware_route.go
+++ b/services/groupware/pkg/groupware/groupware_route.go
@@ -18,6 +18,7 @@ const (
UriParamRole = "role"
UriParamAddressBookId = "addressbookid"
UriParamCalendarId = "calendarid"
+ UriParamTaskListId = "tasklistid"
QueryParamMailboxSearchName = "name"
QueryParamMailboxSearchRole = "role"
QueryParamMailboxSearchSubscribed = "subscribed"
@@ -101,6 +102,11 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/{calendarid}", g.GetCalendarById)
r.Get("/{calendarid}/events", g.GetEventsInCalendar)
})
+ r.Route("/tasklists", func(r chi.Router) {
+ r.Get("/", g.GetTaskLists)
+ r.Get("/{tasklistid}", g.GetTaskListById)
+ r.Get("/{tasklistid}/tasks", g.GetTasksInTaskList)
+ })
})
r.HandleFunc("/events/{stream}", g.ServeSSE)