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 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 log.warning("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) Check sanity of subscription 143 Topic itemToSubscribe = dms.getTopic(itemId); 144 if (!itemToSubscribe.getTypeUri().equals(DEEPAMEHTA_TAG_TYPE) 145 && !itemToSubscribe.getTypeUri().equals(USER_ACCOUNT_TYPE)) { 146 log.warning("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 log.warning("ROLLBACK!"); 163 log.warning("Subscription between " +accountId+ " and " +itemId+ " not created."); 164 throw new RuntimeException(e); 165 } 166 } 167 168 @Override 169 @Transactional 170 public void unsubscribe(long accountId, long itemId) { 171 List<Association> assocs = dms.getAssociations(accountId, itemId, SUBSCRIPTION_EDGE_TYPE); 172 Iterator<Association> iterator = assocs.iterator(); 173 while (iterator.hasNext()) { 174 Association assoc = iterator.next(); 175 dms.deleteAssociation(assoc.getId()); 176 } 177 } 178 179 @Override 180 @Transactional 181 public void createNotifications(String title, String message, long actionAccountId, DeepaMehtaObject item) { 182 if (item.getTypeUri().equals(USER_ACCOUNT_TYPE)) { 183 // 1) create notifications for all direct subscribers of this user topic 184 log.fine("Notifying subscribers of user account \"" + item.getSimpleValue() + "\""); 185 createNotifications(title, "", actionAccountId, item); 186 } else { 187 // 2) create notifications for all subscribers of all tags this topic is tagged with 188 if (item.getModel().getChildTopicsModel().has(DEEPAMEHTA_TAG_TYPE)) { 189 // 2.1) go trough all tags of this topic 190 List<TopicModel> tags = item.getModel().getChildTopicsModel().getTopics(DEEPAMEHTA_TAG_TYPE); 191 for (TopicModel tag : tags) { 192 Topic tag_node = dms.getTopic(tag.getId()).loadChildTopics(); 193 log.fine("Notifying subscribers of tag \"" + tag_node.getSimpleValue() + "\""); 194 // for all subscribers of this tag 195 createNotificationTopics(title, "", actionAccountId, item, tag_node); 196 } 197 } 198 webSocketsService.broadcast("org.deepamehta.subscriptions", "Check notifications for your logged-in user."); 199 } 200 } 201 202 @Override 203 public ResultList<RelatedTopic> getAllNotifications() { 204 String logged_in_username = aclService.getUsername(); 205 if (logged_in_username == null || logged_in_username.isEmpty()) return null; 206 Topic account = getUserAccountTopic(logged_in_username); 207 // 208 ResultList<RelatedTopic> results = account.getRelatedTopics(NOTIFICATION_RECIPIENT_EDGE_TYPE, 209 "dm4.core.default", "dm4.core.default", NOTIFICATION_TYPE, 0); 210 log.fine("Fetching " +results.getSize()+ " notifications for user " + account.getSimpleValue()); 211 return results.loadChildTopics(); 212 } 213 214 @Override 215 public ArrayList<RelatedTopic> getAllUnseenNotifications() { 216 String logged_in_username = aclService.getUsername(); 217 if (logged_in_username == null || logged_in_username.isEmpty()) return null; 218 Topic account = getUserAccountTopic(logged_in_username); 219 // 220 ArrayList<RelatedTopic> unseen = new ArrayList<RelatedTopic>(); 221 ResultList<RelatedTopic> results = account.getRelatedTopics(NOTIFICATION_RECIPIENT_EDGE_TYPE, 222 "dm4.core.default", "dm4.core.default", NOTIFICATION_TYPE, 0); 223 for (RelatedTopic notification : results.getItems()) { 224 boolean seen_child = notification.getChildTopics().getBoolean(NOTIFICATION_SEEN_TYPE); 225 if (!seen_child) { 226 unseen.add(notification); 227 } 228 } 229 log.info("Fetching " +unseen.size() + " unseen notifications for user " + account.getSimpleValue()); 230 return unseen; 231 } 232 233 @Override 234 public void websocketTextMessage(String message) { 235 log.info("### Receiving message from WebSocket client: \"" + message + "\""); 236 } 237 238 private void createNotificationTopics(String title, String text, long accountId, DeepaMehtaObject involvedItem) { 239 createNotificationTopics(title, text, accountId, involvedItem, null); 240 } 241 242 private void createNotificationTopics(String title, String text, long accountId, DeepaMehtaObject involvedItem, 243 DeepaMehtaObject subscribedItem) { 244 // 0) Fetch all subscribers of item X 245 ResultList<RelatedTopic> subscribers = null; 246 long subscribedItemId = 0; 247 if (subscribedItem != null) { // fetch subscribers of subscribedItem 248 subscribers = subscribedItem.getRelatedTopics(SUBSCRIPTION_EDGE_TYPE, 249 DEFAULT_ROLE_TYPE, DEFAULT_ROLE_TYPE, "dm4.accesscontrol.user_account", 0); 250 subscribedItemId = subscribedItem.getId(); 251 } else { // fetch subscribers of involvedItem 252 subscribers = involvedItem.getRelatedTopics(SUBSCRIPTION_EDGE_TYPE, 253 DEFAULT_ROLE_TYPE, DEFAULT_ROLE_TYPE, "dm4.accesscontrol.user_account",0); 254 } 255 for (RelatedTopic subscriber : subscribers) { 256 if (subscriber.getId() != accountId) { 257 log.fine("> subscription is valid, notifying user " + subscriber.getSimpleValue()); 258 // 1) Create notification instance 259 ChildTopicsModel message = new ChildTopicsModel() 260 .put(NOTIFICATION_SEEN_TYPE, false) 261 .put(NOTIFICATION_TITLE_TYPE, title) 262 .put(NOTIFICATION_BODY_TYPE, text) 263 .put(NOTIFICATION_SUB_ITEM_ID_TYPE, subscribedItemId) 264 .putRef(USER_ACCOUNT_TYPE, accountId) 265 .put(NOTIFICATION_INVOLVED_ITEM_ID_TYPE, involvedItem.getId()); 266 TopicModel model = new TopicModel(NOTIFICATION_TYPE, message); 267 dms.createTopic(model); // check: is system the creator? 268 // 2) Hook up notification with subscriber 269 AssociationModel recipient_model = new AssociationModel(NOTIFICATION_RECIPIENT_EDGE_TYPE, 270 model.createRoleModel(DEFAULT_ROLE_TYPE), 271 new TopicRoleModel(subscriber.getId(), DEFAULT_ROLE_TYPE)); 272 dms.createAssociation(recipient_model); // check: is system the creator? 273 } 274 } 275 } 276 277 private boolean associationExists(String edge_type, long itemId, long accountId) { 278 List<Association> results = dms.getAssociations(itemId, accountId, edge_type); 279 return (results.size() > 0) ? true : false; 280 } 281 282 private Topic getUserAccountTopic(String username) { 283 Topic user = dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username)); 284 return user.getRelatedTopic("dm4.core.composition", "dm4.core.child", 285 "dm4.core.parent", "dm4.accesscontrol.user_account"); 286 } 287 288 }