Skip to content

Commit

Permalink
SAK-48083 portal: Add push capability to Sakai (sakaiproject#11038)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianfish authored Feb 22, 2023
1 parent 131a1fc commit b3aefc2
Show file tree
Hide file tree
Showing 36 changed files with 913 additions and 268 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.entity.api.EntityManager;
import org.sakaiproject.event.api.Event;
import org.sakaiproject.messaging.api.UserNotification;
import org.sakaiproject.messaging.api.UserNotificationData;
import org.sakaiproject.messaging.api.AbstractUserNotificationHandler;
import org.sakaiproject.messaging.api.model.UserNotification;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.site.api.ToolConfiguration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
import org.sakaiproject.assignment.api.model.Assignment;
import org.sakaiproject.assignment.api.model.AssignmentSubmission;
import org.sakaiproject.event.api.Event;
import org.sakaiproject.messaging.api.UserNotification;
import org.sakaiproject.messaging.api.UserNotificationData;
import org.sakaiproject.messaging.api.AbstractUserNotificationHandler;
import org.sakaiproject.messaging.api.model.UserNotification;

import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4641,7 +4641,7 @@ private void sendGradeReleaseNotification(AssignmentSubmission submission) {
// send the message immediately
userMessagingService.message(filteredUsers,
Message.builder().tool(AssignmentConstants.TOOL_ID).type("releasegrade").build(),
Arrays.asList(new MessageMedium[] {MessageMedium.EMAIL}), emailUtil.getReleaseGradeReplacements(assignment, siteId), NotificationService.NOTI_REQUIRED);
Arrays.asList(new MessageMedium[] { MessageMedium.EMAIL }), emailUtil.getReleaseGradeReplacements(assignment, siteId), NotificationService.NOTI_REQUIRED);
}
}
if (StringUtils.isNotBlank(resubmitNumber) && StringUtils.equals(AssignmentConstants.ASSIGNMENT_RELEASERESUBMISSION_NOTIFICATION_EACH, assignmentProperties.get(AssignmentConstants.ASSIGNMENT_RELEASERESUBMISSION_NOTIFICATION_VALUE))) {
Expand All @@ -4650,7 +4650,7 @@ private void sendGradeReleaseNotification(AssignmentSubmission submission) {
// send the message immidiately
userMessagingService.message(filteredUsers,
Message.builder().tool(AssignmentConstants.TOOL_ID).type("releaseresubmission").build(),
Arrays.asList(new MessageMedium[] {MessageMedium.EMAIL}), emailUtil.getReleaseResubmissionReplacements(submission), NotificationService.NOTI_REQUIRED);
Arrays.asList(new MessageMedium[] { MessageMedium.EMAIL }), emailUtil.getReleaseResubmissionReplacements(submission), NotificationService.NOTI_REQUIRED);
}
}
}
Expand All @@ -4672,7 +4672,7 @@ private void notificationToInstructors(AssignmentSubmission submission, Assignme
// send the message immediately
userMessagingService.message(new HashSet<>(receivers),
Message.builder().tool(AssignmentConstants.TOOL_ID).type("submission").build(),
Arrays.asList(new MessageMedium[] {MessageMedium.EMAIL}), replacements, NotificationService.NOTI_REQUIRED);
Arrays.asList(new MessageMedium[] { MessageMedium.EMAIL }), replacements, NotificationService.NOTI_REQUIRED);
} else if (notiOption.equals(AssignmentConstants.ASSIGNMENT_INSTRUCTOR_NOTIFICATIONS_DIGEST)) {
// digest the message to each user
userMessagingService.message(new HashSet<>(receivers),
Expand Down Expand Up @@ -4708,7 +4708,7 @@ private void notificationToStudent(AssignmentSubmission submission, Assignment a

userMessagingService.message(users,
Message.builder().tool(AssignmentConstants.TOOL_ID).type("submission").build(),
Arrays.asList(new MessageMedium[] {MessageMedium.EMAIL}), emailUtil.getSubmissionReplacements(submission), NotificationService.NOTI_REQUIRED);
Arrays.asList(new MessageMedium[] { MessageMedium.EMAIL }), emailUtil.getSubmissionReplacements(submission), NotificationService.NOTI_REQUIRED);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ private void sendEmailReminder(Site site, Assignment assignment, Member submitte
assignmentTitle = assignment.getTitle().substring(0, 11) + "[...]";
}

replacements.put("assignmentUrl", getAssignmentUrl(assignment));
replacements.put("assignmentTitle", assignmentTitle);
replacements.put("url", getAssignmentUrl(assignment));
replacements.put("title", assignmentTitle);

replacements.put("siteUrl", site.getUrl());
replacements.put("siteTitle", site.getTitle());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5403,19 +5403,44 @@
# DEFAULT: false
#quartz.seedsites.autorun=true

# ###############################################################
# SAK-30461 Add academic and social alerts icons to portal topbar
# Set this to false to hide the bullhorn icons in the topbar and
# disable bullhorn event harvesting.
# true/false Defaults to true (on)
# ###############################################################
# Set this to false to hide the bullhorn icon in the topbar and disable bullhorn event harvesting.
# DEFAULT: true
#portal.bullhorns.enabled=false

# Turn on notifications debugging in the browser console.
# Turn on notifications debugging in the browser console, for both server sent events and push.
#
# DEFAULT: false
#portal.notifications.debug=true

# Enable push notifications. This has to be enabled for bullhorn alerts - setting this to false will
# effectively halt any bullhorn alerts. When set to true (the default) the browser will request a
# push subscription from the browser vendor's push service, followed by the browser sending those
# details on to the Sakai server. Any PUSH calls to the user messaging service, from then on, will
# make a call to that subscription endpoint and the browser will get a push event.
# DEFAULT: true
# portal.notifications.push.enabled=false

# To enable push, you need to generate a VAPID keypair. This is the private one.
#
# DEFAULT: sakai_push.key
# portal.notifications.push.privatekey=another.key

# To enable push, you need to generate a VAPID keypair. This is the public one.
#
# DEFAULT: sakai_push.key.pub
# portal.notifications.push.publickey=anotherkey.pub

# The push service at Mozilla requires VAPID signatures, with a sub field. When you generate a
# VAPID keypair, you will use an email as the subject - specify that email here. The email is
# useful as it gives the push service supplier a way of contacting you in case of issues. Google is
# a bit more lenient and doesn't require the sub field to be populated (at the time of writing
# this!).
#
# One of the many services for generating VAPID keypairs: https://vapidkeys.com/
#
# DEFAULT: ""
# portal.notifications.push.subject=mailto: <[email protected]>

# ###############################################################
# SAK-43903 Configurable Favicon
# Defaults to /library/icon/favicon.ico
Expand Down
16 changes: 0 additions & 16 deletions deploy/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -575,22 +575,6 @@
<artifactId>spring-webmvc</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams</artifactId>
<version>1.0.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.opensource.org/licenses/ECL-2.0
* http://www.opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
Expand Down Expand Up @@ -38,21 +38,29 @@ public interface EntityManager
*/
List<EntityProducer> getEntityProducers();

default Optional<Entity> getEntity(String ref) {
return Optional.<Entity>empty();
}

default Optional<String> getTool(String ref) {
return Optional.<String>empty();
}

/**
* Register this as an EntityProducer.
*
* @param manager
* The EntityProducer manager to register.
* The EntityProducer manager to register.
* @param referenceRoot
* The prefix of all entity references handeled by this producer (i.e. "content" if you handle "/content/..." references)
* The prefix of all entity references handeled by this producer (i.e. "content" if you handle "/content/..." references)
*/
void registerEntityProducer(EntityProducer manager, String referenceRoot);

/**
* Create a new Reference object, from the given reference string.
*
* @param refString
* The reference string.
* The reference string.
* @return a new reference object made from the given reference string.
*/
Reference newReference(String refString);
Expand All @@ -61,7 +69,7 @@ public interface EntityManager
* Create a new Reference object, as a copy of the given Reference object.
*
* @param copyMe
* The Reference object to copy
* The Reference object to copy
* @return a new Reference object, as a copy of the given Reference object.
*/
Reference newReference(Reference copyMe);
Expand All @@ -77,7 +85,7 @@ public interface EntityManager
* Create a new List specially designed to hold References, as a copy of another.
*
* @param copyMe
* Make the new list contain a copy of this list.
* Make the new list contain a copy of this list.
* @return a new List specially designed to hold References, as a copy of another.
*/
List<Reference> newReferenceList(List<Reference> copyMe);
Expand All @@ -86,7 +94,7 @@ public interface EntityManager
* Check for a valid reference.
*
* @param ref
* a reference string.
* a reference string.
* @return true if the reference is valid, false if not.
*/
boolean checkReference(String ref);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,13 @@ default Collection<String> getEntityAuthzGroups(Reference ref, String userId) {
default HttpAccess getHttpAccess() {
return null;
}

/**
* Get the common id of the tool responsible for these entities
*
* @return An optional of the tool's common id
*/
default Optional<String> getTool() {
return Optional.<String>empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
*/
package org.sakaiproject.messaging.api;

import org.sakaiproject.messaging.api.model.UserNotification;

public interface MessageListener {

public void read(UserNotification ba);
public void read(UserNotification un);
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Set;

import org.sakaiproject.user.api.User;
import org.sakaiproject.messaging.api.model.UserNotification;

/**
* The user messaging service is intendened to the the single interface via which tools
Expand All @@ -31,6 +32,9 @@
*/
public interface UserMessagingService {

public static final String PUSH_PUBKEY_PROPERTY = "portal.notifications.push.publickey";
public static final String PUSH_PRIVKEY_PROPERTY = "portal.notifications.push.privatekey";

/**
* Send a message to a set of users, via 1 to many message media. If a template is involved
* you can supply a map of token replacements. User preferences are queried by the
Expand Down Expand Up @@ -96,4 +100,14 @@ public interface UserMessagingService {
* @return boolean to indicate success
*/
public boolean markAllNotificationsViewed();

/**
* Subscribe the current user to the push service. This is related to browser push and the
* parameters come from the browser vendor's push service via the Sakai client js.
*
* @param endpoint The browser push service supplied endpoint
* @param auth The browser push service supplied auth
* @param auth The browser push service supplied userKey
*/
public void subscribeToPush(String endpoint, String auth, String userKey, String browserFingerprint);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Copyright (c) 2003-2019 The Apereo Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/ecl2
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sakaiproject.messaging.api.model;

import java.time.Instant;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

import org.sakaiproject.springframework.data.PersistableEntity;

import org.hibernate.annotations.Type;

import lombok.Data;
import lombok.EqualsAndHashCode;

@Entity
@Table(name = "PUSH_SUBSCRIPTIONS",
indexes = {
@Index(name = "IDX_PUSH_SUBSCRIPTIONS_USER", columnList = "USER_ID")
}
)
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class PushSubscription implements PersistableEntity<Long> {

@Id
@Column(name = "ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO, generator = "push_subscription_id_sequence")
@SequenceGenerator(name = "push_subscription_id_sequence", sequenceName = "PUSH_SUBSCRIPTION_S")
@EqualsAndHashCode.Include
private Long id;

@Column(name = "USER_ID", length = 99, nullable = false)
private String userId;

@Column(name = "USER_KEY", length = 255, nullable = false)
private String userKey;

@Column(name = "AUTH", length = 255, nullable = false)
private String auth;

@Column(name = "ENDPOINT", length = 255, nullable = false)
private String endpoint;

@Column(name = "FINGERPRINT", length = 255, nullable = false, unique = true)
private String fingerprint;

@Type(type = "org.hibernate.type.InstantType")
@Column(name = "CREATED", nullable = false)
private Instant created;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sakaiproject.messaging.api;
package org.sakaiproject.messaging.api.model;

import java.time.Instant;

Expand Down Expand Up @@ -89,4 +89,7 @@ public class UserNotification implements PersistableEntity<Long> {

@Transient
private String siteTitle;

@Transient
private String tool;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2003-2021 The Apereo Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/ecl2
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sakaiproject.messaging.api.repository;

import java.util.List;
import java.util.Optional;

import org.sakaiproject.messaging.api.model.PushSubscription;

import org.sakaiproject.springframework.data.SpringCrudRepository;

public interface PushSubscriptionRepository extends SpringCrudRepository<PushSubscription, Long> {

List<PushSubscription> findByUser(String user);
int deleteByFingerprint(String browserFingerprint);
}
Loading

0 comments on commit b3aefc2

Please sign in to comment.