001 package org.deepamehta.plugins.subscriptions; 002 003 import de.deepamehta.core.Association; 004 import de.deepamehta.core.DeepaMehtaObject; 005 import de.deepamehta.core.RelatedTopic; 006 import de.deepamehta.core.Topic; 007 import de.deepamehta.core.model.*; 008 import de.deepamehta.core.osgi.PluginActivator; 009 import de.deepamehta.core.service.Inject; 010 import de.deepamehta.core.service.ResultList; 011 import de.deepamehta.core.service.Transactional; 012 import de.deepamehta.plugins.accesscontrol.service.AccessControlService; 013 import de.deepamehta.plugins.websockets.event.WebsocketTextMessageListener; 014 import de.deepamehta.plugins.websockets.service.WebSocketsService; 015 import java.util.ArrayList; 016 import java.util.Iterator; 017 import java.util.List; 018 import java.util.logging.Logger; 019 import javax.ws.rs.*; 020 import javax.ws.rs.core.Response; 021 import org.deepamehta.plugins.subscriptions.service.SubscriptionService; 022 023 /** 024 * 025 * A DeepaMehta 4 Plugin introducing notifications on subscribed topics based on dm4-websockets. 026 * 027 * @author Malte Reißig (<malte@mikromedia.de>) 028 * @website https://github.com/mukil/dm4-subscriptions 029 * @version 1.0.3-SNAPSHOT 030 * 031 */ 032 033 @Path("/subscriptions") 034 public class SubscriptionsPlugin extends PluginActivator implements SubscriptionService, 035 WebsocketTextMessageListener { 036 037 private static Logger log = Logger.getLogger(SubscriptionsPlugin.class.getName()); 038 039 private static final String NOTIFICATION_TYPE = "org.deepamehta.subscriptions.notification"; 040 private static final String NOTIFICATION_TITLE_TYPE = "org.deepamehta.subscriptions.notification_title"; 041 private static final String NOTIFICATION_BODY_TYPE = "org.deepamehta.subscriptions.notification_body"; 042 private static final String NOTIFICATION_INVOLVED_ITEM_ID_TYPE = "org.deepamehta.subscriptions.involved_item_id"; 043 private static final String NOTIFICATION_SUB_ITEM_ID_TYPE = "org.deepamehta.subscriptions.subscribed_item_id"; 044 private static final String NOTIFICATION_RECIPIENT_EDGE_TYPE = 045 "org.deepamehta.subscriptions.notification_recipient_edge"; 046 private static final String SUBSCRIPTION_EDGE_TYPE = "org.deepamehta.subscriptions.subscription_edge"; 047 private static final String NOTIFICATION_SEEN_TYPE = "org.deepamehta.subscriptions.notification_seen"; 048 049 // These two types of information can currently be subscribed (with their special semantics) 050 private static final String USER_ACCOUNT_TYPE = "dm4.accesscontrol.user_account"; 051 private static final String DEEPAMEHTA_TAG_TYPE = "dm4.tags.tag"; 052 053 private static final String DEFAULT_ROLE_TYPE = "dm4.core.default"; 054 055 @Inject 056 private AccessControlService aclService = null; 057 @Inject 058 private WebSocketsService webSocketsService = null; 059 060 061 062 @GET 063 @Path("/subscribe/{itemId}") 064 @Transactional 065 public Response subscribeUser(@PathParam("itemId") long itemId) { 066 // 0) Check for any session 067 String logged_in_username = aclService.getUsername(); 068 if (!logged_in_username.isEmpty()) { 069 Topic account = getUserAccountTopic(logged_in_username); 070 // 1) Users can just manage their own subscriptions 071 subscribe(account.getId(), itemId); 072 return Response.ok().build(); 073 } 074 return Response.noContent().build(); 075 } 076 077 @GET 078 @Path("/unsubscribe/{itemId}") 079 @Transactional 080 public Response unsubscribeUser(@PathParam("itemId") long itemId) { 081 // 0) Check for any session 082 String logged_in_username = aclService.getUsername(); 083 if (!logged_in_username.isEmpty()) { 084 Topic account = getUserAccountTopic(logged_in_username); 085 // 1) Users can just manage their own subscriptions 086 unsubscribe(account.getId(), itemId); 087 return Response.ok().build(); 088 } 089 return Response.noContent().build(); 090 } 091 092 @GET 093 @Path("/list") 094 public ResultList<RelatedTopic> getSubscriptions() { 095 // 0) Check for any session 096 String logged_in_username = aclService.getUsername(); 097 if (logged_in_username == null || logged_in_username.isEmpty()) return null; 098 Topic account = getUserAccountTopic(logged_in_username); 099 // 1) Return results 100 log.fine("Listing all subscriptions of user " + account.getSimpleValue()); 101 return account.getRelatedTopics(SUBSCRIPTION_EDGE_TYPE, 0); 102 } 103 104 @GET 105 @Path("/notification/seen/{newsId}") 106 @Transactional 107 public boolean setNotificationSeen(@PathParam("newsId") long newsId) { 108 try { 109 // 0) Check for any session 110 String logged_in_username = aclService.getUsername(); 111 if (logged_in_username == null || logged_in_username.isEmpty()) { 112 throw new RuntimeException("Nobody logged in for whom we could set the notification as seen."); 113 } 114 Topic notification = dms.getTopic(newsId).loadChildTopics(); 115 notification.getChildTopics().set(NOTIFICATION_SEEN_TYPE, true); 116 // 1) Do operation 117 log.fine("Set notification " + newsId + " as seen!"); 118 return true; 119 } catch (Exception e) { 120 log.warning("Could NOT set notification " + newsId + " as seen! Caused by: " 121 + e.getCause().toString() + ", " + e.getMessage()); 122 return false; 123 } 124 } 125 126 @GET 127 @Path("/notifications/all") 128 public ResultList<RelatedTopic> getAllNotificationsForUser() { 129 return getAllNotifications(); 130 } 131 132 @GET 133 @Path("/notifications/unseen") 134 public ArrayList<RelatedTopic> getAllUnseenNotificationsForUser() { 135 return getAllUnseenNotifications(); 136 } 137 138 @Override 139 @Transactional 140 public void subscribe(long accountId, long itemId) { 141 try { 142 // 1) 143 Topic itemToSubscribe = dms.getTopic(itemId); 144 if (!itemToSubscribe.getTypeUri().equals(DEEPAMEHTA_TAG_TYPE) 145 && !itemToSubscribe.getTypeUri().equals(USER_ACCOUNT_TYPE)) { 146 throw new RuntimeException("Subscription are only supported for topics of type " 147 + "\"User Account\" or \"Tag\" - Skipping creation of subscription"); 148 } 149 // 2) Create subscriptions (if not alreay existent) 150 if (!associationExists(SUBSCRIPTION_EDGE_TYPE, itemId, accountId)) { 151 AssociationModel model = new AssociationModel(SUBSCRIPTION_EDGE_TYPE, 152 new TopicRoleModel(accountId, DEFAULT_ROLE_TYPE), 153 new TopicRoleModel(itemId, DEFAULT_ROLE_TYPE), 154 new ChildTopicsModel().addRef("org.deepamehta.subscriptions.subscription_type", 155 "org.deepamehta.subscriptions.in_app_subscription")); 156 dms.createAssociation(model); 157 log.info("New subscription for user:" + accountId + " to item:" + itemId); 158 } else { 159 log.info("Subscription already exists between " + accountId + " and " + itemId); 160 } 161 } catch (Exception e) { 162 throw new RuntimeException(e); 163 } 164 } 165 166 @Override 167 @Transactional 168 public void unsubscribe(long accountId, long itemId) { 169 List<Association> assocs = dms.getAssociations(accountId, itemId, SUBSCRIPTION_EDGE_TYPE); 170 Iterator<Association> iterator = assocs.iterator(); 171 while (iterator.hasNext()) { 172 Association assoc = iterator.next(); 173 dms.deleteAssociation(assoc.getId()); 174 } 175 } 176 177 @Override 178 @Transactional 179 public void createNotifications(String title, String message, long actionAccountId, DeepaMehtaObject item) { 180 if (item.getTypeUri().equals(USER_ACCOUNT_TYPE)) { 181 // 1) create notifications for all direct subscribers of this user topic 182 log.fine("Notifying subscribers of user account \"" + item.getSimpleValue() + "\""); 183 createNotifications(title, "", actionAccountId, item); 184 } else { 185 // 2) create notifications for all subscribers of all tags this topic is tagged with 186 if (item.getModel().getChildTopicsModel().has(DEEPAMEHTA_TAG_TYPE)) { 187 // 2.1) go trough all tags of this topic 188 List<TopicModel> tags = item.getModel().getChildTopicsModel().getTopics(DEEPAMEHTA_TAG_TYPE); 189 for (TopicModel tag : tags) { 190 Topic tag_node = dms.getTopic(tag.getId()).loadChildTopics(); 191 log.fine("Notifying subscribers of tag \"" + tag_node.getSimpleValue() + "\""); 192 // for all subscribers of this tag 193 createNotificationTopics(title, "", actionAccountId, item, tag_node); 194 } 195 } 196 webSocketsService.broadcast("org.deepamehta.subscriptions", "Check notifications for your logged-in user."); 197 } 198 } 199 200 @Override 201 public ResultList<RelatedTopic> getAllNotifications() { 202 String logged_in_username = aclService.getUsername(); 203 if (logged_in_username == null || logged_in_username.isEmpty()) return null; 204 Topic account = getUserAccountTopic(logged_in_username); 205 // 206 ResultList<RelatedTopic> results = account.getRelatedTopics(NOTIFICATION_RECIPIENT_EDGE_TYPE, 207 "dm4.core.default", "dm4.core.default", NOTIFICATION_TYPE, 0); 208 log.fine("Fetching " +results.getSize()+ " notifications for user " + account.getSimpleValue()); 209 return results.loadChildTopics(); 210 } 211 212 @Override 213 public ArrayList<RelatedTopic> getAllUnseenNotifications() { 214 String logged_in_username = aclService.getUsername(); 215 if (logged_in_username == null || logged_in_username.isEmpty()) return null; 216 Topic account = getUserAccountTopic(logged_in_username); 217 // 218 ArrayList<RelatedTopic> unseen = new ArrayList<RelatedTopic>(); 219 ResultList<RelatedTopic> results = account.getRelatedTopics(NOTIFICATION_RECIPIENT_EDGE_TYPE, 220 "dm4.core.default", "dm4.core.default", NOTIFICATION_TYPE, 0); 221 for (RelatedTopic notification : results.getItems()) { 222 boolean seen_child = notification.getChildTopics().getBoolean(NOTIFICATION_SEEN_TYPE); 223 if (!seen_child) { 224 unseen.add(notification); 225 } 226 } 227 log.info("Fetching " +unseen.size() + " unseen notifications for user " + account.getSimpleValue()); 228 return unseen; 229 } 230 231 @Override 232 public void websocketTextMessage(String message) { 233 log.info("### Receiving message from WebSocket client: \"" + message + "\""); 234 } 235 236 private void createNotificationTopics(String title, String text, long accountId, DeepaMehtaObject involvedItem) { 237 createNotificationTopics(title, text, accountId, involvedItem, null); 238 } 239 240 private void createNotificationTopics(String title, String text, long accountId, DeepaMehtaObject involvedItem, 241 DeepaMehtaObject subscribedItem) { 242 // 0) Fetch all subscribers of item X 243 ResultList<RelatedTopic> subscribers = null; 244 long subscribedItemId = 0; 245 if (subscribedItem != null) { // fetch subscribers of subscribedItem 246 subscribers = subscribedItem.getRelatedTopics(SUBSCRIPTION_EDGE_TYPE, 247 DEFAULT_ROLE_TYPE, DEFAULT_ROLE_TYPE, "dm4.accesscontrol.user_account", 0); 248 subscribedItemId = subscribedItem.getId(); 249 } else { // fetch subscribers of involvedItem 250 subscribers = involvedItem.getRelatedTopics(SUBSCRIPTION_EDGE_TYPE, 251 DEFAULT_ROLE_TYPE, DEFAULT_ROLE_TYPE, "dm4.accesscontrol.user_account",0); 252 } 253 for (RelatedTopic subscriber : subscribers) { 254 if (subscriber.getId() != accountId) { 255 log.fine("> subscription is valid, notifying user " + subscriber.getSimpleValue()); 256 // 1) Create notification instance 257 ChildTopicsModel message = new ChildTopicsModel() 258 .put(NOTIFICATION_SEEN_TYPE, false) 259 .put(NOTIFICATION_TITLE_TYPE, title) 260 .put(NOTIFICATION_BODY_TYPE, text) 261 .put(NOTIFICATION_SUB_ITEM_ID_TYPE, subscribedItemId) 262 .putRef(USER_ACCOUNT_TYPE, accountId) 263 .put(NOTIFICATION_INVOLVED_ITEM_ID_TYPE, involvedItem.getId()); 264 TopicModel model = new TopicModel(NOTIFICATION_TYPE, message); 265 dms.createTopic(model); // check: is system the creator? 266 // 2) Hook up notification with subscriber 267 AssociationModel recipient_model = new AssociationModel(NOTIFICATION_RECIPIENT_EDGE_TYPE, 268 model.createRoleModel(DEFAULT_ROLE_TYPE), 269 new TopicRoleModel(subscriber.getId(), DEFAULT_ROLE_TYPE)); 270 dms.createAssociation(recipient_model); // check: is system the creator? 271 } 272 } 273 } 274 275 private boolean associationExists(String edge_type, long itemId, long accountId) { 276 List<Association> results = dms.getAssociations(itemId, accountId, edge_type); 277 return (results.size() > 0) ? true : false; 278 } 279 280 private Topic getUserAccountTopic(String username) { 281 Topic user = dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username)); 282 return user.getRelatedTopic("dm4.core.composition", "dm4.core.child", 283 "dm4.core.parent", "dm4.accesscontrol.user_account"); 284 } 285 286 }