// Vikunja is a to-do list application to facilitate your life. // Copyright 2018-2021 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 Licensee 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 Licensee for more details. // // You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . package models import ( "time" "xorm.io/builder" "code.vikunja.io/api/pkg/user" "code.vikunja.io/web" "xorm.io/xorm" ) // SubscriptionEntityType represents all entities which can be subscribed to type SubscriptionEntityType int const ( SubscriptionEntityUnknown = iota SubscriptionEntityNamespace SubscriptionEntityProject SubscriptionEntityTask ) const ( entityNamespace = `namespace` entityProject = `project` entityTask = `task` ) // Subscription represents a subscription for an entity type Subscription struct { // The numeric ID of the subscription ID int64 `xorm:"autoincr not null unique pk" json:"id"` EntityType SubscriptionEntityType `xorm:"index not null" json:"-"` Entity string `xorm:"-" json:"entity" param:"entity"` // The id of the entity to subscribe to. EntityID int64 `xorm:"bigint index not null" json:"entity_id" param:"entityID"` // The user who made this subscription User *user.User `xorm:"-" json:"user"` UserID int64 `xorm:"bigint index not null" json:"-"` // A timestamp when this subscription was created. You cannot change this value. Created time.Time `xorm:"created not null" json:"created"` web.CRUDable `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"` } // TableName gives us a better tabel name for the subscriptions table func (sb *Subscription) TableName() string { return "subscriptions" } func getEntityTypeFromString(entityType string) SubscriptionEntityType { switch entityType { case entityNamespace: return SubscriptionEntityNamespace case entityProject: return SubscriptionEntityProject case entityTask: return SubscriptionEntityTask } return SubscriptionEntityUnknown } // String returns a human-readable string of an entity func (et SubscriptionEntityType) String() string { switch et { case SubscriptionEntityNamespace: return entityNamespace case SubscriptionEntityProject: return entityProject case SubscriptionEntityTask: return entityTask } return "" } func (et SubscriptionEntityType) validate() error { if et == SubscriptionEntityNamespace || et == SubscriptionEntityProject || et == SubscriptionEntityTask { return nil } return &ErrUnknownSubscriptionEntityType{EntityType: et} } // Create subscribes the current user to an entity // @Summary Subscribes the current user to an entity. // @Description Subscribes the current user to an entity. // @tags subscriptions // @Accept json // @Produce json // @Security JWTKeyAuth // @Param entity path string true "The entity the user subscribes to. Can be either `namespace`, `project` or `task`." // @Param entityID path string true "The numeric id of the entity to subscribe to." // @Success 201 {object} models.Subscription "The subscription" // @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity." // @Failure 412 {object} web.HTTPError "The subscription already exists." // @Failure 412 {object} web.HTTPError "The subscription entity is invalid." // @Failure 500 {object} models.Message "Internal error" // @Router /subscriptions/{entity}/{entityID} [put] func (sb *Subscription) Create(s *xorm.Session, auth web.Auth) (err error) { // Rights method alread does the validation of the entity type so we don't need to do that here sb.UserID = auth.GetID() sub, err := GetSubscription(s, sb.EntityType, sb.EntityID, auth) if err != nil { return err } if sub != nil { return &ErrSubscriptionAlreadyExists{ EntityID: sb.EntityID, EntityType: sb.EntityType, UserID: sb.UserID, } } _, err = s.Insert(sb) if err != nil { return } sb.User, err = user.GetFromAuth(auth) return } // Delete unsubscribes the current user to an entity // @Summary Unsubscribe the current user from an entity. // @Description Unsubscribes the current user to an entity. // @tags subscriptions // @Accept json // @Produce json // @Security JWTKeyAuth // @Param entity path string true "The entity the user subscribed to. Can be either `namespace`, `project` or `task`." // @Param entityID path string true "The numeric id of the subscribed entity to." // @Success 200 {object} models.Subscription "The subscription" // @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity." // @Failure 404 {object} web.HTTPError "The subscription does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /subscriptions/{entity}/{entityID} [delete] func (sb *Subscription) Delete(s *xorm.Session, auth web.Auth) (err error) { sb.UserID = auth.GetID() _, err = s. Where("entity_id = ? AND entity_type = ? AND user_id = ?", sb.EntityID, sb.EntityType, sb.UserID). Delete(&Subscription{}) return } func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int64) (cond builder.Cond) { if entityType == SubscriptionEntityNamespace { cond = builder.And( builder.Eq{"entity_id": entityID}, builder.Eq{"entity_type": SubscriptionEntityNamespace}, ) } if entityType == SubscriptionEntityProject { cond = builder.Or( builder.And( builder.Eq{"entity_id": entityID}, builder.Eq{"entity_type": SubscriptionEntityProject}, ), builder.And( builder.Eq{"entity_id": builder. Select("namespace_id"). From("projects"). Where(builder.Eq{"id": entityID}), }, builder.Eq{"entity_type": SubscriptionEntityNamespace}, ), ) } if entityType == SubscriptionEntityTask { cond = builder.Or( builder.And( builder.Eq{"entity_id": entityID}, builder.Eq{"entity_type": SubscriptionEntityTask}, ), builder.And( builder.Eq{"entity_id": builder. Select("namespace_id"). From("projects"). Join("INNER", "tasks", "projects.id = tasks.project_id"). Where(builder.Eq{"tasks.id": entityID}), }, builder.Eq{"entity_type": SubscriptionEntityNamespace}, ), builder.And( builder.Eq{"entity_id": builder. Select("project_id"). From("tasks"). Where(builder.Eq{"id": entityID}), }, builder.Eq{"entity_type": SubscriptionEntityProject}, ), ) } return } // GetSubscription returns a matching subscription for an entity and user. // It will return the next parent of a subscription. That means for tasks, it will first look for a subscription for // that task, if there is none it will look for a subscription on the project the task belongs to and if that also // doesn't exist it will check for a subscription for the namespace the project is belonging to. func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) { subs, err := GetSubscriptions(s, entityType, []int64{entityID}, a) if err != nil || len(subs) == 0 { return nil, err } if sub, exists := subs[entityID]; exists { return sub, nil // Take exact match first, if available } for _, sub := range subs { return sub, nil // For parents, take next available } return nil, nil } // GetSubscriptions returns a map of subscriptions to a set of given entity IDs func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (projectsToSubscriptions map[int64]*Subscription, err error) { u, is := a.(*user.User) if !is { return } if err := entityType.validate(); err != nil { return nil, err } var entitiesFilter builder.Cond for _, eID := range entityIDs { if entitiesFilter == nil { entitiesFilter = getSubscriberCondForEntity(entityType, eID) continue } entitiesFilter = entitiesFilter.Or(getSubscriberCondForEntity(entityType, eID)) } var subscriptions []*Subscription err = s. Where("user_id = ?", u.ID). And(entitiesFilter). Find(&subscriptions) if err != nil { return nil, err } projectsToSubscriptions = make(map[int64]*Subscription) for _, sub := range subscriptions { sub.Entity = sub.EntityType.String() projectsToSubscriptions[sub.EntityID] = sub } return projectsToSubscriptions, nil } func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType, entityID int64) (subscriptions []*Subscription, err error) { if err := entityType.validate(); err != nil { return nil, err } cond := getSubscriberCondForEntity(entityType, entityID) err = s. Where(cond). Find(&subscriptions) if err != nil { return } userIDs := []int64{} for _, subscription := range subscriptions { userIDs = append(userIDs, subscription.UserID) } users, err := user.GetUsersByIDs(s, userIDs) if err != nil { return } for _, subscription := range subscriptions { subscription.User = users[subscription.UserID] } return }