Projects tigase _server server-core Commits da6ec98e
ctrl k
  • src/main/java/tigase/server/xmppsession/SessionManager.java
    ■ ■ ■ ■
    skipped 1149 lines
    1150 1150   }
    1151 1151   
    1152 1152   @SuppressWarnings("deprecation")
    1153  - protected XMPPResourceConnection loginUserSession(JID conn_id, String domain, BareJID user_id, String resource,
     1153 + public XMPPResourceConnection loginUserSession(JID conn_id, String domain, BareJID user_id, String resource,
    1154 1154   String xmpp_sessionId, boolean tmpSession) {
    1155 1155   try {
    1156 1156   XMPPResourceConnection conn = createUserSession(conn_id, domain);
    skipped 1834 lines
  • src/main/java/tigase/xmpp/XMPPSession.java
    ■ ■ ■ ■ ■ ■
    skipped 103 lines
    104 104   log.finest("Found old resource connection for: " + username + ", id: " + old_res);
    105 105   }
    106 106   try {
    107  - old_res.putSessionData(XMPPResourceConnection.ERROR_KEY, "conflict");
    108  - old_res.logout();
     107 + if (!"PUSH_OFFLINE_PRESENCE".equals(old_res.getSessionId())) {
     108 + old_res.putSessionData(XMPPResourceConnection.ERROR_KEY, "conflict");
     109 + old_res.logout();
     110 + }
    109 111   } catch (NotAuthorizedException e) {
    110 112   log.log(Level.CONFIG, "Exception during closing old connection, ignoring.", e);
    111 113   }
    skipped 331 lines
  • src/main/java/tigase/xmpp/impl/PresenceAbstract.java
    ■ ■ ■ ■ ■
    skipped 432 lines
    433 433  // }
    434 434  // }
    435 435   
     436 + protected boolean forceSendingProbe() {
     437 + return true;
     438 + }
     439 + 
    436 440   /**
    437 441   * <code>sendPresenceBroadcast</code> method broadcasts given presence to all buddies from roster and to all users
    438 442   * to which direct presence was sent. Before sending presence method calls {@code requiresPresenceSending()},
    skipped 26 lines
    465 469   }
    466 470   if (buddies != null) {
    467 471   for (JID buddy : buddies) {
    468  - if (requiresPresenceSending(roster_util, buddy, session, true)) {
     472 + if (forceSendingProbe() || requiresPresenceSending(roster_util, buddy, session, true)) {
    469 473   if (log.isLoggable(Level.FINEST)) {
    470 474   log.log(Level.FINEST, session.getBareJID() + " | Sending presence probe to: " + buddy);
    471 475   }
    skipped 22 lines
    494 498   
    495 499   if (buddies_to != null) {
    496 500   for (JID buddy : buddies_to) {
    497  - if (requiresPresenceSending(roster_util, buddy, session, true)) {
     501 + if (forceSendingProbe() || requiresPresenceSending(roster_util, buddy, session, true)) {
    498 502   if (log.isLoggable(Level.FINEST)) {
    499 503   log.log(Level.FINEST, session.getBareJID() + " | Sending probe to: " + buddy);
    500 504   }
    skipped 44 lines
  • src/main/java/tigase/xmpp/impl/PresenceState.java
    ■ ■ ■ ■ ■ ■
    skipped 31 lines
    32 32  import tigase.xmpp.impl.annotation.Handle;
    33 33  import tigase.xmpp.impl.annotation.Handles;
    34 34  import tigase.xmpp.impl.annotation.Id;
     35 +import tigase.xmpp.impl.push.PushPresence;
    35 36  import tigase.xmpp.impl.roster.*;
    36 37  import tigase.xmpp.impl.roster.RosterAbstract.PresenceType;
    37 38  import tigase.xmpp.impl.roster.RosterAbstract.SubscriptionType;
    38 39  import tigase.xmpp.jid.JID;
    39 40   
    40 41  import java.util.*;
     42 +import java.util.function.Consumer;
    41 43  import java.util.logging.Level;
    42 44  import java.util.logging.Logger;
    43 45   
    skipped 48 lines
    92 94   @ConfigField(desc = "Enable roster lazy loading", alias = ENABLE_ROSTER_LAZY_LOADING_KEY)
    93 95   private boolean rosterLazyLoading = true;
    94 96   private long usersStatusChanges = 0;
     97 + @Inject(nullAllowed = true)
     98 + private PushPresence pushDevicesPresence;
     99 + 
     100 + private void withPushDevicesPresence(Consumer<PushPresence> consumer) {
     101 + if (pushDevicesPresence != null) {
     102 + consumer.accept(pushDevicesPresence);
     103 + }
     104 + }
     105 + 
     106 + @Override
     107 + protected boolean forceSendingProbe() {
     108 + return pushDevicesPresence != null;
     109 + }
    95 110   
    96 111   /**
    97 112   * Add JID to collection of JIDs to which direct presence was sent. To all these addresses unavailable presence must
    skipped 240 lines
    338 353   if (log.isLoggable(Level.FINE)) {
    339 354   log.log(Level.FINE, "Session is null, ignoring packet: {0}", packet);
    340 355   }
    341  - 
     356 + withPushDevicesPresence(
     357 + pushPresence -> pushPresence.processPresenceToOffline(packet.getStanzaTo(), packet.getStanzaFrom(),
     358 + packet.getType(), results::offer));
    342 359   return;
    343 360   } // end of if (session == null)
    344 361   if (!session.isAuthorized()) {
    345 362   if (log.isLoggable(Level.FINE)) {
    346 363   log.log(Level.FINE, "Session is not authorized, ignoring packet: {0}", packet);
    347 364   }
    348  - 
     365 + withPushDevicesPresence(
     366 + pushPresence -> pushPresence.processPresenceToOffline(packet.getStanzaTo(), packet.getStanzaFrom(),
     367 + packet.getType(), results::offer));
    349 368   return;
    350 369   }
    351 370   
    skipped 57 lines
    409 428   
    410 429   break;
    411 430   case in_probe:
     431 + withPushDevicesPresence(pushPresence -> pushPresence.processPresenceProbe(packet.getStanzaTo(),
     432 + packet.getStanzaFrom(),
     433 + results::offer));
    412 434   if (session.getPresence() == null) {
    413 435   
    414 436   // If the user has not yet sent initial presence then ignore the
    skipped 590 lines
  • src/main/java/tigase/xmpp/impl/push/AbstractPushNotifications.java
    ■ ■ ■ ■ ■
    skipped 21 lines
    22 22  import tigase.kernel.beans.config.ConfigField;
    23 23  import tigase.server.*;
    24 24  import tigase.server.amp.db.MsgRepository;
     25 +import tigase.util.datetime.TimestampHelper;
    25 26  import tigase.util.stringprep.TigaseStringprepException;
    26 27  import tigase.xml.DomBuilderHandler;
    27 28  import tigase.xml.Element;
    skipped 8 lines
    36 37  import tigase.xmpp.jid.JID;
    37 38   
    38 39  import java.time.Duration;
     40 +import java.time.Instant;
    39 41  import java.util.*;
    40 42  import java.util.concurrent.ConcurrentHashMap;
    41 43  import java.util.function.Consumer;
    skipped 24 lines
    66 68   protected boolean withSender = true;
    67 69   @ConfigField(desc = "Max notification timeout", alias = "max-timeout")
    68 70   protected Duration maxTimeout = Duration.ofMinutes(6);
     71 + @ConfigField(desc = "Device registration TTL")
     72 + private Duration deviceRegistrationTTL = null;
    69 73   
    70 74   @Inject
    71 75   private MsgRepositoryIfc msgRepository;
    skipped 4 lines
    76 80   @Inject(bean = "sess-man")
    77 81   private PacketWriterWithTimeout writer;
    78 82   
     83 + @Inject(nullAllowed = true)
     84 + private PushPresence pushDevicesPresence;
     85 + 
    79 86   @ConfigField(desc = "Notification to display for encrypted messages", alias = "encrypted-message-body")
    80 87   private String encryptedMessageBody = "New secure message. Open to see the message.";
    81 88   
     89 + public PushPresence getPushDevicesPresence() {
     90 + return pushDevicesPresence;
     91 + }
     92 + 
     93 + public void setPushDevicesPresence(PushPresence pushDevicesPresence) {
     94 + this.pushDevicesPresence = pushDevicesPresence;
     95 + }
     96 + 
    82 97   protected boolean shouldDisablePush(Authorization error) {
    83 98   if (error == null) {
    84 99   return false;
    skipped 41 lines
    126 141   switch (actionEl.getName()) {
    127 142   case "enable":
    128 143   enableNotifications(session, jid, node, actionEl, actionEl.findChild(
    129  - element -> element.getXMLNS() == JABBER_X_DATA_XMLNS && element.getName() == "x"));
     144 + element -> element.getXMLNS() == JABBER_X_DATA_XMLNS && element.getName() == "x"), results::offer);
    130 145   break;
    131 146   case "disable":
    132  - disableNotifications(session, jid, node);
     147 + disableNotifications(session, session.getBareJID(), jid, node, results::offer);
    133 148   break;
    134 149   default:
    135 150   results.offer(Authorization.BAD_REQUEST.getResponseMessage(packet, null, true));
    skipped 23 lines
    159 174   String userJid = affiliationEl.getAttributeStaticStr("jid");
    160 175   if ("none".equals(affiliationEl.getAttributeStaticStr("affiliation"))) {
    161 176   if (userJid != null) {
    162  - userRepository.removeData(BareJID.bareJIDInstanceNS(userJid), ID,
     177 + BareJID bareJid = BareJID.bareJIDInstanceNS(userJid);
     178 + userRepository.removeData(bareJid, ID,
    163 179   packet.getStanzaFrom().toString() + "/" + node);
     180 + if (getPushServices(bareJid).isEmpty() && pushDevicesPresence != null) {
     181 + pushDevicesPresence.pushAvailabilityChanged(bareJid, false, results);
     182 + }
    164 183   }
    165 184   }
    166 185   }
    skipped 2 lines
    169 188   }
    170 189   
    171 190   protected void enableNotifications(XMPPResourceConnection session, JID jid, String node, Element enableElem,
    172  - Element optionsForm) throws NotAuthorizedException, TigaseDBException {
     191 + Element optionsForm, Consumer<Packet> packetConsumer) throws NotAuthorizedException, TigaseDBException {
    173 192   Element settings = createSettingsElement(jid, node, enableElem, optionsForm);
    174 193   
    175  - enableNotifications(session, jid, node, settings);
     194 + enableNotifications(session, jid, node, settings, packetConsumer);
    176 195   }
     196 + 
     197 + private static final TimestampHelper timestampHelper = new TimestampHelper();
    177 198   
    178 199   protected Element createSettingsElement(JID jid, String node, Element enableElem, Element optionsForm) {
    179  - Element settings = new Element("settings", new String[]{"jid", "node"},
    180  - new String[]{jid.toString(), node.toString()});
     200 + Element settings = new Element("settings", new String[]{"jid", "node", "createdAt"},
     201 + new String[]{jid.toString(), node.toString(), timestampHelper.format(new Date())});
    181 202   if (optionsForm != null) {
    182 203   settings.addChild(optionsForm);
    183 204   }
    184 205   return settings;
    185 206   }
    186 207  
    187  - protected void enableNotifications(XMPPResourceConnection session, JID jid, String node, Element settings)
     208 + protected void enableNotifications(XMPPResourceConnection session, JID jid, String node, Element settings, Consumer<Packet> packetConsumer)
    188 209   throws NotAuthorizedException, TigaseDBException {
    189 210   String key = jid.toString() + "/" + node;
     211 + Map<String, Element> pushServices = getPushServices(session);
    190 212   session.setData(ID, key, settings.toString());
    191  - Map<String, Element> pushServices = getPushServices(session);
    192 213   if (log.isLoggable(Level.FINEST)) {
    193 214   log.log(Level.FINEST, "Enabled push notifications for JID: {0}, node: {1}, settings: {2}",
    194 215   new Object[]{jid, node, settings.toString()});
    195 216   }
    196 217   
     218 + boolean hadPushServices = !pushServices.isEmpty();
    197 219   pushServices.put(key, settings);
     220 + if (!hadPushServices && pushDevicesPresence != null) {
     221 + pushDevicesPresence.pushAvailabilityChanged(session.getBareJID(), true, packetConsumer);
     222 + }
    198 223   }
    199 224   
    200  - protected void disableNotifications(XMPPResourceConnection session, JID jid, String node)
     225 + protected void disableNotifications(XMPPResourceConnection session, BareJID userJid, JID jid, String node, Consumer<Packet> packetConsumer)
    201 226   throws NotAuthorizedException, TigaseDBException {
    202  - Map<String, Element> pushServices = getPushServices(session);
     227 + Map<String, Element> pushServices = session != null ? getPushServices(session) : getPushServices(userJid);
    203 228   if (log.isLoggable(Level.FINEST)) {
    204 229   log.log(Level.FINEST, "Disabled push notifications for JID: {0}, node: {1}, pushServices: {2}",
    205 230   new Object[]{jid, node, pushServices});
    206 231   }
    207 232   
    208 233   if (pushServices != null) {
     234 + boolean hadPushServices = !pushServices.isEmpty();
    209 235   if (node != null) {
    210 236   String key = jid.toString() + "/" + node;
    211 237   pushServices.remove(key);
    212  - session.removeData(ID, key);
     238 + if (session != null) {
     239 + session.removeData(ID, key);
     240 + } else {
     241 + userRepository.removeData(userJid, ID, key);
     242 + }
    213 243   } else {
    214 244   String prefix = jid.toString() + "/";
    215 245   List<String> removed = new ArrayList<>();
    skipped 5 lines
    221 251   return false;
    222 252   });
    223 253   for (String key : removed) {
    224  - session.removeData(ID, key);
     254 + if (session != null) {
     255 + session.removeData(ID, key);
     256 + } else {
     257 + userRepository.removeData(userJid, ID, key);
     258 + }
    225 259   }
     260 + }
     261 + if (hadPushServices && pushServices.isEmpty() && pushDevicesPresence != null) {
     262 + pushDevicesPresence.pushAvailabilityChanged(session != null ? session.getBareJID() : userJid, false, packetConsumer);
    226 263   }
    227 264   }
    228 265   }
    skipped 56 lines
    285 322   }
    286 323   
    287 324   protected void sendPushNotification(BareJID userJid, Collection<Element> pushServices,
    288  - XMPPResourceConnection session, Packet packet, Map<Enum, Long> notificationData) {
    289  - pushServices.forEach(settings -> {
     325 + XMPPResourceConnection session, Packet packet, Map<Enum, Long> notificationData, Consumer<Packet> packetConsumer) {
     326 + for (Element settings : pushServices) {
    290 327   try {
    291 328   if (packet != null && !isSendingNotificationAllowed(userJid, session, settings, packet)) {
    292 329   return;
    293 330   }
     331 + JID pushService = JID.jidInstance(settings.getAttributeStaticStr("jid"));
     332 + String pushNode = settings.getAttributeStaticStr("node");
     333 + if (deviceRegistrationTTL != null) {
     334 + // check device registration TTL
     335 + Date createdAt = timestampHelper.parseTimestamp(settings.getAttributeStaticStr("createdAt"));
     336 + if (createdAt != null && Duration.between(createdAt.toInstant(), Instant.now()).compareTo(deviceRegistrationTTL) > 0) {
     337 + // registration is expired
     338 + log.log(Level.WARNING, "disabling push service " + pushService + "/" + pushNode + " for user " + userJid + ", expired due to TTL");
     339 + disableNotifications(session, userJid, pushService, pushNode, packetConsumer);
     340 + continue;
     341 + }
     342 + }
    294 343   final Element notification = prepareNotificationPayload(settings, packet, notificationData.getOrDefault(
    295 344   MsgRepository.MSG_TYPES.message, 0l));
    296  - JID pushService = JID.jidInstance(settings.getAttributeStaticStr("jid"));
    297  - String pushNode = settings.getAttributeStaticStr("node");
    298 345   Element publishOptionsForm = settings.findChild(
    299 346   element -> element.getXMLNS() == JABBER_X_DATA_XMLNS && element.getName() == "x");
    300 347   
    skipped 7 lines
    308 355   log.log(Level.FINE, "Could not publish notification for " + userJid + " to " +
    309 356   settings.getAttributeStaticStr("jid") + " at " + settings.getAttributeStaticStr("node"));
    310 357   }
    311  - });
     358 + }
    312 359   }
    313 360   
    314 361   protected Map<String, Element> getPushServices(BareJID userJid) throws TigaseDBException {
    315 362   return userRepository.getDataMap(userJid, ID, this::parseElement);
    316 363   }
    317 364   
    318  - protected void sendPushNotification(XMPPResourceConnection session, Packet packet)
     365 + protected void sendPushNotification(XMPPResourceConnection session, Packet packet, Consumer<Packet> packetConsumer)
    319 366   throws TigaseDBException {
    320 367   final BareJID userJid = packet.getStanzaTo().getBareJID();
    321 368   Map<String, Element> pushServices = (session != null && session.isAuthorized())
    skipped 9 lines
    331 378   
    332 379   Map<Enum, Long> typesCount = msgRepository.getMessagesCount(packet.getStanzaTo());
    333 380   
    334  - sendPushNotification(userJid, pushServices.values(), session, packet, typesCount);
     381 + sendPushNotification(userJid, pushServices.values(), session, packet, typesCount, packetConsumer);
    335 382   }
    336 383   
    337 384   protected boolean isSendingNotificationAllowed(BareJID userJid, XMPPResourceConnection session,
    skipped 57 lines
  • src/main/java/tigase/xmpp/impl/push/AwayPushNotificationsExtension.java
    ■ ■ ■ ■
    skipped 117 lines
    118 118   }
    119 119   
    120 120   try {
    121  - pushNotifications.notifyOfflineMessagesRetrieved(conn.getBareJID(), services);
     121 + pushNotifications.notifyOfflineMessagesRetrieved(conn.getBareJID(), services, packet -> {});
    122 122   } catch (NotAuthorizedException ex) {
    123 123   log.log(Level.FINEST, "Connection {0} not yet authorized, ignoring..", conn);
    124 124   }
    skipped 106 lines
  • src/main/java/tigase/xmpp/impl/push/PushNotifications.java
    ■ ■ ■ ■ ■ ■
    skipped 16 lines
    17 17   */
    18 18  package tigase.xmpp.impl.push;
    19 19   
     20 +import tigase.component.adhoc.AdHocCommand;
     21 +import tigase.component.adhoc.AdHocCommandException;
     22 +import tigase.component.adhoc.AdHocResponse;
     23 +import tigase.component.adhoc.AdhHocRequest;
    20 24  import tigase.db.NonAuthUserRepository;
    21 25  import tigase.db.TigaseDBException;
    22 26  import tigase.db.UserNotFoundException;
     27 +import tigase.form.Field;
     28 +import tigase.form.Form;
    23 29  import tigase.kernel.beans.Bean;
    24 30  import tigase.kernel.beans.Inject;
    25 31  import tigase.kernel.beans.RegistrarBean;
    skipped 3 lines
    29 35  import tigase.server.Packet;
    30 36  import tigase.server.amp.db.MsgRepository;
    31 37  import tigase.server.xmppsession.SessionManager;
     38 +import tigase.util.stringprep.TigaseStringprepException;
    32 39  import tigase.xml.Element;
    33 40  import tigase.xmpp.*;
    34 41  import tigase.xmpp.impl.OfflineMessages;
    skipped 85 lines
    120 127   if (session == null || !session.isAuthorized() || !shouldSendNotification(packet, session.getBareJID(), session)) {
    121 128   return;
    122 129   }
    123  - sendPushNotification(session, packet);
     130 + sendPushNotification(session, packet, consumer);
    124 131   }
    125 132   
    126 133   @Override
    skipped 8 lines
    135 142   }
    136 143   
    137 144   try {
    138  - sendPushNotification(session, packet);
     145 + sendPushNotification(session, packet, results::offer);
    139 146   } catch (UserNotFoundException ex) {
    140 147   log.log(Level.FINEST, "Could not send push notification for message " + packet, ex);
    141 148   } catch (TigaseDBException ex) {
    skipped 14 lines
    156 163   return;
    157 164   }
    158 165   
    159  - notifyOfflineMessagesRetrieved(userJid, pushServices.values());
     166 + notifyOfflineMessagesRetrieved(userJid, pushServices.values(), results::offer);
    160 167   } catch (UserNotFoundException | NotAuthorizedException ex) {
    161 168   log.log(Level.FINEST, "Could not send push notification about offline message retrieval by " + session, ex);
    162 169   } catch (TigaseDBException ex) {
    skipped 15 lines
    178 185   @Override
    179 186   protected Element createSettingsElement(JID jid, String node, Element enableElem, Element optionsForm) {
    180 187   Element settingsEl = super.createSettingsElement(jid, node, enableElem, optionsForm);
     188 + String name = enableElem.getAttributeStaticStr("name");
     189 + if (name != null && !name.isBlank()) {
     190 + settingsEl.setAttribute("name", name);
     191 + }
    181 192   for (PushNotificationsAware trigger : awares) {
    182 193   trigger.processEnableElement(enableElem, settingsEl);
    183 194   }
    184 195   return settingsEl;
    185 196   }
    186 197   
    187  - protected void notifyOfflineMessagesRetrieved(BareJID userJid, Collection<Element> pushServices) {
     198 + protected void notifyOfflineMessagesRetrieved(BareJID userJid, Collection<Element> pushServices, Consumer<Packet> packetConsumer) {
    188 199   Map<Enum, Long> map = new HashMap<>();
    189 200   map.put(MsgRepository.MSG_TYPES.message, 0l);
    190  - sendPushNotification(userJid, pushServices, null, null, map);
     201 + sendPushNotification(userJid, pushServices, null, null, map, packetConsumer);
    191 202   }
    192 203   
    193 204   @Override
    skipped 37 lines
    231 242   }
    232 243   
    233 244   return false;
     245 + }
     246 + 
     247 + protected static abstract class AbstractAdhocCommand implements AdHocCommand {
     248 + 
     249 + private final String node;
     250 + private final String name;
     251 + 
     252 + @Inject
     253 + private SessionManager component;
     254 + @Inject
     255 + private AbstractPushNotifications pushNotifications;
     256 + 
     257 + protected AbstractAdhocCommand(String node, String name) {
     258 + this.node = node;
     259 + this.name = name;
     260 + }
     261 + 
     262 + @Override
     263 + public String getName() {
     264 + return name;
     265 + }
     266 + 
     267 + @Override
     268 + public String getNode() {
     269 + return node;
     270 + }
     271 + 
     272 + @Override
     273 + public void execute(AdhHocRequest request, AdHocResponse response) throws AdHocCommandException {
     274 + try {
     275 + final Element data = request.getCommand().getChild("x", "jabber:x:data");
     276 + 
     277 + if (request.isAction("cancel")) {
     278 + response.cancelSession();
     279 + } else {
     280 + if (data == null) {
     281 + response.getElements().add(prepareForm(request, response).getElement());
     282 + response.startSession();
     283 + } else {
     284 + Form form = new Form(data);
     285 + if (form.isType("submit")) {
     286 + Form responseForm = submitForm(request, response, form);
     287 + if (responseForm != null) {
     288 + response.getElements().add(responseForm.getElement());
     289 + }
     290 + }
     291 + }
     292 + }
     293 + } catch (AdHocCommandException ex) {
     294 + throw ex;
     295 + } catch (Exception e) {
     296 + log.log(Level.FINE, "Exception during execution of adhoc command " + getNode(), e);
     297 + throw new AdHocCommandException(Authorization.INTERNAL_SERVER_ERROR, e.getMessage());
     298 + }
     299 + }
     300 + 
     301 + protected abstract Form prepareForm(AdhHocRequest request, AdHocResponse response) throws AdHocCommandException;
     302 + protected abstract Form submitForm(AdhHocRequest request, AdHocResponse response, Form form)
     303 + throws AdHocCommandException;
     304 + 
     305 + protected boolean isEmpty(String input) {
     306 + return input == null || input.isBlank();
     307 + }
     308 + 
     309 + protected String assertNotEmpty(String input, String message) throws AdHocCommandException {
     310 + if (isEmpty(input)) {
     311 + throw new AdHocCommandException(Authorization.BAD_REQUEST, message);
     312 + }
     313 + return input.trim();
     314 + }
     315 + 
     316 + @Override
     317 + public boolean isAllowedFor(JID jid) {
     318 + return component.isAdmin(jid);
     319 + }
     320 + 
     321 + public SessionManager getComponent() {
     322 + return component;
     323 + }
     324 + 
     325 + public AbstractPushNotifications getPushNotifications() {
     326 + return pushNotifications;
     327 + }
     328 + }
     329 + 
     330 + @Bean(name = "push-list-devices", parent = SessionManager.class, active = true)
     331 + public static class ListDevicesAdhocCommand extends AbstractAdhocCommand {
     332 + 
     333 + public ListDevicesAdhocCommand() {
     334 + super("push-list-devices", "List push devices");
     335 + }
     336 + 
     337 + @Override
     338 + protected Form prepareForm(AdhHocRequest request, AdHocResponse response) throws AdHocCommandException {
     339 + Form form = new Form("form", "Unregister device", "Fill out and submit this form to list enabled devices with push notifications");
     340 + form.addField(Field.fieldJidSingle("userJid", "", "Account JID"));
     341 + return form;
     342 + }
     343 + 
     344 + @Override
     345 + protected Form submitForm(AdhHocRequest request, AdHocResponse response, Form form)
     346 + throws AdHocCommandException {
     347 + Form result = new Form("result", "List of push devices", null);
     348 + try {
     349 + BareJID accountJid = BareJID.bareJIDInstance(
     350 + assertNotEmpty(form.getAsString("userJid"), "Account JID is required!"));
     351 + Map<String, Element> pushServices = getPushNotifications().getPushServices(accountJid);
     352 + String[] deviceIds = pushServices.keySet().stream().sorted().toArray(String[]::new);
     353 + result.addField(Field.fieldTextMulti("deviceIds", deviceIds, "List of devices"));
     354 + return result;
     355 + } catch (TigaseStringprepException|TigaseDBException e) {
     356 + throw new RuntimeException(e);
     357 + }
     358 + }
     359 + }
     360 + 
     361 + @Bean(name = "push-unregister-device", parent = SessionManager.class, active = true)
     362 + public static class DisableDeviceAdHocCommand
     363 + extends AbstractAdhocCommand {
     364 + 
     365 + 
     366 + public DisableDeviceAdHocCommand() {
     367 + super("push-disable-device", "Disable push notifications");
     368 + }
     369 + 
     370 + protected Form prepareForm(AdhHocRequest request, AdHocResponse response) throws AdHocCommandException {
     371 + try {
     372 + return prepareForm(null);
     373 + } catch (TigaseDBException ex) {
     374 + throw new RuntimeException(ex);
     375 + }
     376 + }
     377 + 
     378 + protected Form submitForm(AdhHocRequest request, AdHocResponse response, Form form)
     379 + throws AdHocCommandException {
     380 + try {
     381 + BareJID accountJid = BareJID.bareJIDInstance(assertNotEmpty(form.getAsString("userJid"), "Account JID is required!"));
     382 + String key = form.getAsString("deviceId");
     383 + if (isEmpty(key)) {
     384 + return prepareForm(accountJid);
     385 + }
     386 + int idx = key.indexOf('/');
     387 + if (idx < 0) {
     388 + throw new RuntimeException("Invalid device ID: " + key);
     389 + }
     390 + JID jid = JID.jidInstance(key.substring(0, idx));
     391 + String node = key.substring(idx + 1);
     392 + getPushNotifications().disableNotifications(null, accountJid, jid, node, getComponent()::addOutPacket);
     393 + return null;
     394 + } catch (TigaseStringprepException|TigaseDBException|NotAuthorizedException e) {
     395 + throw new RuntimeException(e);
     396 + }
     397 + }
     398 + 
     399 + protected Form prepareForm(BareJID accountJid) throws TigaseDBException {
     400 + Form form = new Form("form", "Unregister device", "Fill out and submit this form to disable sending push notifications to selected device");
     401 + form.addField(Field.fieldJidSingle("userJid", accountJid == null ? "" : accountJid.toString(), "Account JID"));
     402 + if (accountJid != null) {
     403 + Map<String, Element> pushServices = getPushNotifications().getPushServices(accountJid);
     404 + List<Map.Entry<String,Element>> entries = pushServices.entrySet().stream().sorted(
     405 + Map.Entry.comparingByKey()).toList();
     406 + form.addField(Field.fieldListSingle("deviceId", "", "Device", entries.stream()
     407 + .map(Map.Entry::getValue)
     408 + .map(settings -> Optional.ofNullable(settings.getAttributeStaticStr("name"))
     409 + .orElseGet(() -> settings.getAttributeStaticStr("jid") + " / " +
     410 + settings.getAttributeStaticStr("node")))
     411 + .toArray(String[]::new), entries.stream().map(Map.Entry::getKey).toArray(String[]::new)));
     412 + }
     413 + return form;
     414 + }
     415 +
    234 416   }
    235 417   
    236 418  }
    skipped 1 lines
  • src/main/java/tigase/xmpp/impl/push/PushPresence.java
    ■ ■ ■ ■ ■ ■
     1 +/*
     2 + * Tigase XMPP Server - The instant messaging server
     3 + * Copyright (C) 2004 Tigase, Inc. (office@tigase.com)
     4 + *
     5 + * This program is free software: you can redistribute it and/or modify
     6 + * it under the terms of the GNU Affero General Public License as published by
     7 + * the Free Software Foundation, version 3 of the License.
     8 + *
     9 + * This program is distributed in the hope that it will be useful,
     10 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     12 + * GNU Affero General Public License for more details.
     13 + *
     14 + * You should have received a copy of the GNU Affero General Public License
     15 + * along with this program. Look for COPYING file in the top folder.
     16 + * If not, see http://www.gnu.org/licenses/.
     17 + */
     18 +package tigase.xmpp.impl.push;
     19 + 
     20 +import tigase.cluster.strategy.ClusteringStrategyIfc;
     21 +import tigase.cluster.strategy.ConnectionRecordIfc;
     22 +import tigase.component.PacketWriter;
     23 +import tigase.db.AuthRepository;
     24 +import tigase.db.TigaseDBException;
     25 +import tigase.db.UserRepository;
     26 +import tigase.eventbus.EventBus;
     27 +import tigase.eventbus.HandleEvent;
     28 +import tigase.kernel.beans.Bean;
     29 +import tigase.kernel.beans.Initializable;
     30 +import tigase.kernel.beans.Inject;
     31 +import tigase.kernel.beans.UnregisterAware;
     32 +import tigase.kernel.beans.config.ConfigField;
     33 +import tigase.map.ClusterMapFactory;
     34 +import tigase.server.Packet;
     35 +import tigase.server.Presence;
     36 +import tigase.server.xmppsession.SessionManager;
     37 +import tigase.server.xmppsession.SessionManagerHandler;
     38 +import tigase.util.cache.LRUConcurrentCache;
     39 +import tigase.util.dns.DNSResolverFactory;
     40 +import tigase.util.stringprep.TigaseStringprepException;
     41 +import tigase.vhosts.VHostItem;
     42 +import tigase.vhosts.VHostItemImpl;
     43 +import tigase.xml.Element;
     44 +import tigase.xmpp.NotAuthorizedException;
     45 +import tigase.xmpp.StanzaType;
     46 +import tigase.xmpp.XMPPResourceConnection;
     47 +import tigase.xmpp.XMPPSession;
     48 +import tigase.xmpp.impl.JabberIqPrivacy;
     49 +import tigase.xmpp.impl.annotation.AnnotatedXMPPProcessor;
     50 +import tigase.xmpp.impl.roster.RosterAbstract;
     51 +import tigase.xmpp.impl.roster.RosterFactory;
     52 +import tigase.xmpp.jid.BareJID;
     53 +import tigase.xmpp.jid.JID;
     54 + 
     55 +import java.util.*;
     56 +import java.util.concurrent.CopyOnWriteArraySet;
     57 +import java.util.function.Consumer;
     58 +import java.util.logging.Level;
     59 +import java.util.logging.Logger;
     60 + 
     61 +@Bean(name = "push-presence", parent = SessionManager.class, active = false)
     62 +public class PushPresence
     63 + extends AnnotatedXMPPProcessor
     64 + implements Initializable, UnregisterAware {
     65 + 
     66 + private static final Logger log = Logger.getLogger(PushPresence.class.getCanonicalName());
     67 +
     68 + enum PresenceStatus {
     69 + chat,
     70 + available,
     71 + away,
     72 + xa,
     73 + dnd,
     74 + unavailable
     75 + }
     76 + 
     77 + @ConfigField(desc = "Presence show value for accounts with push devices")
     78 + private PresenceStatus presenceStatus = PresenceStatus.xa;
     79 + @ConfigField(desc = "Push presence resource name")
     80 + private String resourceName = "push";
     81 + @Inject
     82 + private AuthRepository authRepository;
     83 + @Inject
     84 + private UserRepository userRepository;
     85 + @Inject
     86 + private EventBus eventBus;
     87 + @Inject
     88 + private AbstractPushNotifications pushNotifications;
     89 + @Inject(bean = "sess-man")
     90 + private SessionManagerHandler sessionManager;
     91 + @Inject
     92 + private PacketWriter packetWriter;
     93 + @Inject(nullAllowed = true)
     94 + private ClusteringStrategyIfc<ConnectionRecordIfc> strategy;
     95 + private Map<BareJID,Boolean> pushAvailability;
     96 + private final JID offlineConnectionId = JID.jidInstanceNS("push-offline-conn", DNSResolverFactory.getInstance().getDefaultHost());
     97 + private final LRUConcurrentCache<BareJID, Set<BareJID>> rosterSubscribedFromCache = new LRUConcurrentCache<>(10000);
     98 + private final SessionManagerHandler offlineSessionManagerHandler = new SessionManagerHandler() {
     99 + @Override
     100 + public JID getComponentId() {
     101 + return null;
     102 + }
     103 + 
     104 + @Override
     105 + public void handleLogin(BareJID userId, XMPPResourceConnection conn) {
     106 + 
     107 + }
     108 + 
     109 + @Override
     110 + public void handleDomainChange(String domain, XMPPResourceConnection conn) {
     111 + 
     112 + }
     113 + 
     114 + @Override
     115 + public void handleLogout(BareJID userId, XMPPResourceConnection conn) {
     116 + 
     117 + }
     118 + 
     119 + @Override
     120 + public void handlePresenceSet(XMPPResourceConnection conn) {
     121 + 
     122 + }
     123 + 
     124 + @Override
     125 + public void handleResourceBind(XMPPResourceConnection conn) {
     126 + 
     127 + }
     128 + 
     129 + @Override
     130 + public boolean isLocalDomain(String domain, boolean includeComponents) {
     131 + return false;
     132 + }
     133 + };
     134 + 
     135 + private RosterAbstract rosterUtil = RosterFactory.getRosterImplementation(true);
     136 + 
     137 + public PushPresence() {
     138 + }
     139 + 
     140 + @Override
     141 + public void initialize() {
     142 + this.pushAvailability = ClusterMapFactory.get().createMap("push-availability", BareJID.class, Boolean.class);
     143 + this.pushNotifications.setPushDevicesPresence(this);
     144 + if (eventBus != null) {
     145 + eventBus.registerAll(this);
     146 + }
     147 + }
     148 + 
     149 + @Override
     150 + public void beforeUnregister() {
     151 + if (eventBus != null) {
     152 + eventBus.unregisterAll(this);
     153 + }
     154 + }
     155 + 
     156 + protected void setRosterUtil(RosterAbstract rosterUtil) {
     157 + this.rosterUtil = rosterUtil;
     158 + }
     159 + 
     160 + public EventBus getEventBus() {
     161 + return eventBus;
     162 + }
     163 + 
     164 + public void setEventBus(EventBus eventBus) {
     165 + this.eventBus = eventBus;
     166 + }
     167 +
     168 + protected boolean isPushAvailable(BareJID userJid) throws TigaseDBException {
     169 + Boolean value = pushAvailability.get(userJid);
     170 + if (value == null) {
     171 + value = !pushNotifications.getPushServices(userJid).isEmpty();
     172 + pushAvailability.put(userJid, value);
     173 + }
     174 + return value;
     175 + }
     176 +
     177 + private boolean shouldNodeGeneratePresence(BareJID userJid) {
     178 + if (strategy == null) {
     179 + return true;
     180 + }
     181 + 
     182 + Set<ConnectionRecordIfc> records = strategy.getConnectionRecords(userJid);
     183 + if (records != null) {
     184 + List<String> nodes = new ArrayList<>();
     185 + // if we are executing, either our node has user session or there is no user session on any nodes
     186 + nodes.add(sessionManager.getComponentId().getDomain());
     187 + 
     188 + // we know nodes that may have user session
     189 + for (ConnectionRecordIfc record : records) {
     190 + String node = record.getNode().getDomain();
     191 + if (!nodes.contains(node)) {
     192 + nodes.add(node);
     193 + }
     194 + }
     195 + nodes.sort(String::compareTo);
     196 +
     197 + String selectedNode = nodes.get(userJid.hashCode() % nodes.size());
     198 + return sessionManager.getComponentId().getDomain().equals(selectedNode);
     199 + }
     200 + // If records were null, we have no idea which nodes will be able to handle request as each node may
     201 + // or may not have a user session. We are executing assuming that we should handle request supposing
     202 + // that see-other-host will reconnect all connections to the same node
     203 + return true;
     204 + }
     205 + 
     206 + public void processPresenceToOffline(JID recipient, JID sender, StanzaType stanzaType, Consumer<Packet> packetConsumer) {
     207 + if (stanzaType != StanzaType.probe) {
     208 + return;
     209 + }
     210 + processPresenceProbe(recipient, sender, packetConsumer);
     211 + }
     212 + 
     213 + public void processPresenceProbe(JID recipient, JID sender, Consumer<Packet> packetConsumer) {
     214 + if (recipient == null || recipient.getResource() != null || sender == null) {
     215 + return;
     216 + }
     217 + try {
     218 + if (!isPushAvailable(recipient.getBareJID())) {
     219 + return;
     220 + }
     221 + if (!shouldNodeGeneratePresence(recipient.getBareJID())) {
     222 + return;
     223 + }
     224 + if (!getSubscribedWithFrom(recipient.getBareJID()).contains(sender.getBareJID())) {
     225 + return;
     226 + }
     227 + sendPresenceFormPushDevices(recipient.getBareJID(), true, List.of(sender.getBareJID()), packetConsumer);
     228 + } catch (TigaseDBException ex) {
     229 + log.log(Level.FINEST, "failed to fetch push devices for jid " + recipient.getBareJID(), ex);
     230 + }
     231 + }
     232 + 
     233 + private Packet createPresenceForPushDevices(boolean hasPushDevices) throws TigaseStringprepException {
     234 + Element presenceEl = new Element("presence", new Element[]{new Element("priority", "-1"),}, null, null);
     235 + presenceEl.setXMLNS(Presence.CLIENT_XMLNS);
     236 + if (!hasPushDevices) {
     237 + presenceEl.setAttribute("type", StanzaType.unavailable.toString());
     238 + } else {
     239 + switch (presenceStatus) {
     240 + case unavailable -> presenceEl.setAttribute("type", StanzaType.unavailable.name());
     241 + case available -> {}
     242 + default -> presenceEl.withElement("show", null, presenceStatus.name());
     243 + }
     244 + }
     245 + return Packet.packetInstance(presenceEl);
     246 + }
     247 + 
     248 + private void sendPresenceFormPushDevices(BareJID userJid, boolean hasPushDevices, Collection<BareJID> recipients, Consumer<Packet> packetConsumer){
     249 + try {
     250 + JID from = JID.jidInstance(userJid, resourceName);
     251 + Packet presence = createPresenceForPushDevices(hasPushDevices);
     252 + for (BareJID jid : recipients) {
     253 + Packet clone = presence.copyElementOnly();
     254 + clone.initVars(from, JID.jidInstance(jid));
     255 + packetConsumer.accept(clone);
     256 + }
     257 + } catch (TigaseStringprepException ex) {
     258 + log.log(Level.FINEST, "failed to prepare JID for push presence", ex);
     259 + }
     260 + }
     261 + 
     262 + public void broadcastPresenceFromPushDevices(BareJID userJid, boolean hasPushDevices, Consumer<Packet> packetConsumer) {
     263 + sendPresenceFormPushDevices(userJid, hasPushDevices, getSubscribedWithFrom(userJid), packetConsumer);
     264 + }
     265 +
     266 + public void pushAvailabilityChanged(BareJID userJid, boolean newValue, Consumer<Packet> packetConsumer) {
     267 + pushAvailability.put(userJid, newValue);
     268 + broadcastPresenceFromPushDevices(userJid, newValue, packetConsumer);
     269 + }
     270 + 
     271 + private Set<BareJID> getSubscribedWithFrom(BareJID userJid) {
     272 + Set<BareJID> jids = rosterSubscribedFromCache.get(userJid);
     273 + if (jids == null) {
     274 + jids = new CopyOnWriteArraySet<>();
     275 + rosterSubscribedFromCache.put(userJid, jids);
     276 + synchronized (jids) {
     277 + XMPPResourceConnection session = createOfflineXMPPResourceConnection(userJid);
     278 + try {
     279 + JID[] buddies = rosterUtil.getBuddies(session, RosterAbstract.FROM_SUBSCRIBED);
     280 + if (buddies != null) {
     281 + for (JID buddy : buddies) {
     282 + jids.add(buddy.getBareJID());
     283 + }
     284 + }
     285 + } catch (NotAuthorizedException | TigaseDBException ex) {
     286 + log.log(Level.FINEST, "failed to fetch buddies subscribed 'from' for jid " + userJid, ex);
     287 + }
     288 + }
     289 + }
     290 + return jids;
     291 + }
     292 + 
     293 + @HandleEvent
     294 + public void handleRosterModified(RosterAbstract.RosterModifiedEvent event) {
     295 + BareJID userJid = event.getUserJid().getBareJID();
     296 + if (!shouldNodeGeneratePresence(userJid)) {
     297 + return;
     298 + }
     299 +
     300 + try {
     301 + if (isPushAvailable(userJid)) {
     302 + Set<BareJID> jids = rosterSubscribedFromCache.get(userJid);
     303 + switch (event.getSubscription()) {
     304 + case from:
     305 + case from_pending_out:
     306 + case both:
     307 + if (jids != null) {
     308 + jids.add(event.getJid().getBareJID());
     309 + }
     310 + sendPresenceFormPushDevices(userJid, true, List.of(event.getJid().getBareJID()),
     311 + packetWriter::write);
     312 + break;
     313 + case to:
     314 + case none:
     315 + if (jids != null) {
     316 + jids.remove(event.getJid().getBareJID());
     317 + }
     318 + sendPresenceFormPushDevices(userJid, false, List.of(event.getJid().getBareJID()),
     319 + packetWriter::write);
     320 + }
     321 + }
     322 + } catch (TigaseDBException ex) {
     323 + if (log.isLoggable(Level.FINEST)) {
     324 + log.log(Level.FINEST, "failed to check for registered push devices for user " + userJid, ex);
     325 + }
     326 + }
     327 + }
     328 +
     329 + private XMPPResourceConnection createOfflineXMPPResourceConnection(BareJID userJid) {
     330 + try {
     331 + XMPPResourceConnection session = new JabberIqPrivacy.OfflineResourceConnection(offlineConnectionId,
     332 + userRepository,
     333 + authRepository, offlineSessionManagerHandler);
     334 + VHostItem vhost = new VHostItemImpl(userJid.getDomain());
     335 + session.setDomain(vhost);
     336 + session.authorizeJID(userJid, false);
     337 + XMPPSession parentSession = new XMPPSession(userJid.getLocalpart());
     338 + session.setParentSession(parentSession);
     339 + return session;
     340 + } catch (TigaseStringprepException ex) {
     341 + if (log.isLoggable(Level.FINEST)) {
     342 + log.log(Level.FINEST, "creation of temporary session for offline user " + userJid + " failed", ex);
     343 + }
     344 + return null;
     345 + }
     346 + }
     347 +
     348 +}
     349 + 
  • src/main/restructured/Tigase_Administration/Configuration/Advanced_Options.inc
    ■ ■ ■ ■ ■ ■
    skipped 201 lines
    202 202   }
    203 203   }
    204 204  
     205 +Enabling TTL (time-to-live) for enabled push devices
     206 +
     207 +By default, each device enabled for push notifications is kept in the servers storage until it is not disabled (or disabled due to errors reported by push component. To make sure that older push devices that are no longer in use and do not return errors are removed from the list of push enabled devices, it is now possible to configure TTL for enabled push devices. After TTL, subsequent notification will remove devices that reached TTL. Value is in `Java Period format <https://docs.oracle.com/javase/8/docs/api/java/time/Period.html#parse-java.lang.CharSequence-:>`__
     208 +
     209 +Example of setting TTL to 2 days.
     210 +
     211 +.. code::
     212 +
     213 + 'sess-man' {
     214 + 'urn:xmpp:push:0' () {
     215 + 'deviceRegistrationTTL' = 'P2D'
     216 + }
     217 + }
    205 218  
    206 219  Enabling push notifications for messages received when all resources are AWAY/XA/DND
    207 220  
    skipped 33 lines
    241 254   </enable>
    242 255   </iq>
    243 256  
     257 +
     258 +Push Devices - Presence
     259 +~~~~~~~~~~~~~~~~~~~~~~~~~~~
     260 +Tigase XMPP Server comes with support for non-standard feature that allows you to configure it to send presence for configured resource for each user account for which push devices are registered with push notifications enabled. This allows XMPP users see users of your XMPP server with push capable clients be visible as connected but ie. away.
     261 +
     262 +Enabling push devices presence
     263 +
     264 +.. code::
     265 +
     266 + 'sess-man' () {
     267 + 'push-presence' () {
     268 + }
     269 + }
     270 +
     271 + Configuring sent presence ``<show/>`` value (default is ``xa``) to ``away``.
     272 +
     273 +.. code::
     274 +
     275 + 'sess-man' () {
     276 + 'push-presence' () {
     277 + presenceStatus = 'away'
     278 + }
     279 + }
  • src/test/java/tigase/xmpp/impl/push/PushPresenceTest.java
    ■ ■ ■ ■ ■ ■
     1 +/*
     2 + * Tigase XMPP Server - The instant messaging server
     3 + * Copyright (C) 2004 Tigase, Inc. (office@tigase.com)
     4 + *
     5 + * This program is free software: you can redistribute it and/or modify
     6 + * it under the terms of the GNU Affero General Public License as published by
     7 + * the Free Software Foundation, version 3 of the License.
     8 + *
     9 + * This program is distributed in the hope that it will be useful,
     10 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     12 + * GNU Affero General Public License for more details.
     13 + *
     14 + * You should have received a copy of the GNU Affero General Public License
     15 + * along with this program. Look for COPYING file in the top folder.
     16 + * If not, see http://www.gnu.org/licenses/.
     17 + */
     18 +package tigase.xmpp.impl.push;
     19 + 
     20 +import org.junit.After;
     21 +import org.junit.Before;
     22 +import org.junit.FixMethodOrder;
     23 +import org.junit.Test;
     24 +import org.junit.runners.MethodSorters;
     25 +import tigase.component.PacketWriter;
     26 +import tigase.component.responses.AsyncCallback;
     27 +import tigase.db.TigaseDBException;
     28 +import tigase.eventbus.EventBus;
     29 +import tigase.eventbus.EventBusFactory;
     30 +import tigase.kernel.core.Kernel;
     31 +import tigase.server.Packet;
     32 +import tigase.server.PolicyViolationException;
     33 +import tigase.util.stringprep.TigaseStringprepException;
     34 +import tigase.xml.Element;
     35 +import tigase.xmpp.*;
     36 +import tigase.xmpp.impl.MessageDeliveryLogic;
     37 +import tigase.xmpp.impl.PresenceState;
     38 +import tigase.xmpp.impl.ProcessorTestCase;
     39 +import tigase.xmpp.impl.roster.RosterAbstract;
     40 +import tigase.xmpp.impl.roster.RosterFactory;
     41 +import tigase.xmpp.jid.BareJID;
     42 +import tigase.xmpp.jid.JID;
     43 + 
     44 +import java.util.ArrayDeque;
     45 +import java.util.Collection;
     46 +import java.util.HashMap;
     47 +import java.util.UUID;
     48 + 
     49 +import static org.junit.Assert.*;
     50 + 
     51 +@FixMethodOrder(MethodSorters.NAME_ASCENDING)
     52 +public class PushPresenceTest
     53 + extends ProcessorTestCase {
     54 + 
     55 + private RosterAbstract rosterUtil;
     56 + private PushPresence pushPresence;
     57 + private BareJID userJid;
     58 + private BareJID buddyJid;
     59 + 
     60 + @Before
     61 + @Override
     62 + public void setUp() throws Exception {
     63 + super.setUp();
     64 + registerLocalBeans(getKernel());
     65 + rosterUtil = RosterFactory.newRosterInstance(RosterFactory.ROSTER_IMPL_PROP_VAL);
     66 + rosterUtil.setEventBus(getInstance(EventBus.class));
     67 + userJid = BareJID.bareJIDInstanceNS(UUID.randomUUID().toString(), "domain.com");
     68 + buddyJid = BareJID.bareJIDInstanceNS(UUID.randomUUID().toString(), "domain.com");
     69 + pushPresence = getInstance(PushPresence.class);
     70 + pushPresence.setRosterUtil(rosterUtil);
     71 + }
     72 + 
     73 + @After
     74 + public void tearDown() throws Exception {
     75 + getInstance(PushPresence.class).beforeUnregister();
     76 + super.tearDown();
     77 + }
     78 +
     79 + @Test
     80 + public void testSendingPresence_OnPushEnable_NoSubscription() throws TigaseStringprepException, TigaseDBException, NotAuthorizedException {
     81 + TestPacketWriter writer = getInstance(TestPacketWriter.class);
     82 + assertFalse(pushPresence.isPushAvailable(userJid));
     83 + PushNotifications pushNotifications = getInstance(PushNotifications.class);
     84 + XMPPResourceConnection session = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(userJid, "setter"));
     85 + pushNotifications.enableNotifications(session, JID.jidInstance("push.localhost"), "test-node", new Element("enable"), null, writer::write);
     86 + assertTrue(pushPresence.isPushAvailable(userJid));
     87 + assertEquals(0, writer.getQueue().size());
     88 + }
     89 + 
     90 + @Test
     91 + public void testSendingPresence_OnPushEnable_WithSubscription()
     92 + throws TigaseStringprepException, TigaseDBException, NotAuthorizedException, PolicyViolationException,
     93 + NoConnectionIdException {
     94 + TestPacketWriter writer = getInstance(TestPacketWriter.class);
     95 + assertFalse(pushPresence.isPushAvailable(userJid));
     96 + PushNotifications pushNotifications = getInstance(PushNotifications.class);
     97 + XMPPResourceConnection buddySession = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(buddyJid, "setter"));
     98 + XMPPResourceConnection userSession = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(userJid, "setter"));
     99 + 
     100 + updateRoster(userSession, JID.jidInstance(buddyJid), RosterAbstract.SubscriptionType.both);
     101 + updateRoster(buddySession, JID.jidInstance(userJid), RosterAbstract.SubscriptionType.both);
     102 + 
     103 + pushNotifications.enableNotifications(userSession, JID.jidInstance("push.localhost"), "test-node", new Element("enable"), null, writer::write);
     104 + assertTrue(pushPresence.isPushAvailable(userJid));
     105 + assertEquals(1, writer.getQueue().size());
     106 + assertPresenceAway(writer.getQueue().poll());
     107 + }
     108 + 
     109 + @Test
     110 + public void testSendingPresence_OnPushDisable_NoSubscription() throws TigaseStringprepException, TigaseDBException, NotAuthorizedException {
     111 + TestPacketWriter writer = getInstance(TestPacketWriter.class);
     112 + assertFalse(pushPresence.isPushAvailable(userJid));
     113 + PushNotifications pushNotifications = getInstance(PushNotifications.class);
     114 + XMPPResourceConnection session = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(userJid, "setter"));
     115 + pushNotifications.enableNotifications(session, JID.jidInstance("push.localhost"), "test-node", new Element("enable"), null, writer::write);
     116 + assertTrue(pushPresence.isPushAvailable(userJid));
     117 + assertEquals(0, writer.getQueue().size());
     118 + 
     119 + pushNotifications.disableNotifications(session, userJid, JID.jidInstance("push.localhost"), "test-node", writer::write);
     120 +
     121 + assertFalse(pushPresence.isPushAvailable(userJid));
     122 + assertEquals(0, writer.getQueue().size());
     123 + }
     124 + 
     125 + @Test
     126 + public void testSendingPresence_OnPushDisable_WithSubscription()
     127 + throws TigaseStringprepException, TigaseDBException, NotAuthorizedException, PolicyViolationException,
     128 + NoConnectionIdException, InterruptedException {
     129 + TestPacketWriter writer = getInstance(TestPacketWriter.class);
     130 + assertFalse(pushPresence.isPushAvailable(userJid));
     131 + PushNotifications pushNotifications = getInstance(PushNotifications.class);
     132 + 
     133 + XMPPResourceConnection buddySession = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(buddyJid, "setter"));
     134 + XMPPResourceConnection userSession = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(userJid, "setter"));
     135 + 
     136 + pushNotifications.enableNotifications(userSession, JID.jidInstance("push.localhost"), "test-node", new Element("enable"), null, writer::write);
     137 + assertTrue(pushPresence.isPushAvailable(userJid));
     138 + 
     139 + updateRoster(userSession, JID.jidInstance(buddyJid), RosterAbstract.SubscriptionType.both);
     140 + updateRoster(buddySession, JID.jidInstance(userJid), RosterAbstract.SubscriptionType.both);
     141 + 
     142 + System.out.println("packet writer " + writer);
     143 + assertEquals(1, writer.getQueue().size());
     144 + assertEquals(1, writer.getQueue().size());
     145 + assertPresenceAway(writer.getQueue().poll());
     146 + 
     147 + pushNotifications.disableNotifications(userSession, userJid, JID.jidInstance("push.localhost"), "test-node", writer::write);
     148 + assertEquals(1, writer.getQueue().size());
     149 + assertPresenceUnavailable(writer.getQueue().poll());
     150 + }
     151 + 
     152 + @Test
     153 + public void testSendingPresence_OnPresenceProbe_NoSubscription()
     154 + throws TigaseStringprepException, TigaseDBException, XMPPException {
     155 + TestPacketWriter writer = getInstance(TestPacketWriter.class);
     156 + assertFalse(pushPresence.isPushAvailable(userJid));
     157 + PushNotifications pushNotifications = getInstance(PushNotifications.class);
     158 + XMPPResourceConnection session = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(userJid, "setter"));
     159 + pushNotifications.enableNotifications(session, JID.jidInstance("push.localhost"), "test-node", new Element("enable"), null, writer::write);
     160 + assertTrue(pushPresence.isPushAvailable(userJid));
     161 + assertEquals(0, writer.getQueue().size());
     162 + 
     163 + PresenceState presenceState = getInstance(PresenceState.class);
     164 + Packet probe = Packet.packetInstance(new Element("presence").withAttribute("type", StanzaType.probe.name())
     165 + .withAttribute("from", buddyJid.toString())
     166 + .withAttribute("to", userJid.toString()));
     167 + presenceState.process(probe, null, null, writer.getQueue(), new HashMap<>());
     168 + assertEquals(0, writer.getQueue().size());
     169 + }
     170 + 
     171 + @Test
     172 + public void testSendingPresence_OnSubscriptionAndOnPresenceProbe_WithSubscription()
     173 + throws TigaseStringprepException, TigaseDBException, XMPPException, PolicyViolationException {
     174 + TestPacketWriter writer = getInstance(TestPacketWriter.class);
     175 + assertFalse(pushPresence.isPushAvailable(userJid));
     176 + PushNotifications pushNotifications = getInstance(PushNotifications.class);
     177 + XMPPResourceConnection userSession = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(userJid, "setter"));
     178 + pushNotifications.enableNotifications(userSession, JID.jidInstance("push.localhost"), "test-node", new Element("enable"), null, writer::write);
     179 + XMPPResourceConnection buddySession = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(buddyJid, "setter"));
     180 + 
     181 + updateRoster(userSession, JID.jidInstance(buddyJid), RosterAbstract.SubscriptionType.both);
     182 + updateRoster(buddySession, JID.jidInstance(userJid), RosterAbstract.SubscriptionType.both);
     183 + 
     184 + assertTrue(pushPresence.isPushAvailable(userJid));
     185 + // add user roster subscription triggers sending presence
     186 + assertEquals(1, writer.getQueue().size());
     187 + assertPresenceAway(writer.getQueue().poll());
     188 + assertEquals(0, writer.getQueue().size());
     189 + 
     190 + PresenceState presenceState = getInstance(PresenceState.class);
     191 + Packet probe = Packet.packetInstance(new Element("presence").withAttribute("type", StanzaType.probe.name())
     192 + .withAttribute("from", buddyJid.toString())
     193 + .withAttribute("to", userJid.toString()));
     194 + presenceState.process(probe, null, null, writer.getQueue(), new HashMap<>());
     195 + assertEquals(1, writer.getQueue().size());
     196 + assertPresenceAway(writer.getQueue().poll());
     197 + }
     198 + 
     199 + @Test
     200 + public void testSendingPresence_OnSubscriptionAndOnRemovingSubscription()
     201 + throws TigaseStringprepException, TigaseDBException, XMPPException, PolicyViolationException {
     202 + TestPacketWriter writer = getInstance(TestPacketWriter.class);
     203 + assertFalse(pushPresence.isPushAvailable(userJid));
     204 + PushNotifications pushNotifications = getInstance(PushNotifications.class);
     205 + XMPPResourceConnection userSession = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(userJid, "setter"));
     206 + pushNotifications.enableNotifications(userSession, JID.jidInstance("push.localhost"), "test-node", new Element("enable"), null, writer::write);
     207 + XMPPResourceConnection buddySession = getSession(JID.jidInstance(UUID.randomUUID().toString(), "domain", null), JID.jidInstance(buddyJid, "setter"));
     208 + 
     209 + updateRoster(userSession, JID.jidInstance(buddyJid), RosterAbstract.SubscriptionType.both);
     210 + updateRoster(buddySession, JID.jidInstance(userJid), RosterAbstract.SubscriptionType.both);
     211 + 
     212 + assertTrue(pushPresence.isPushAvailable(userJid));
     213 + // add user roster subscription triggers sending presence
     214 + System.out.println("packet writer " + writer + " - in test");
     215 + assertEquals(1, writer.getQueue().size());
     216 + assertPresenceAway(writer.getQueue().poll());
     217 + assertEquals(0, writer.getQueue().size());
     218 + 
     219 + updateRoster(userSession, JID.jidInstance(buddyJid), RosterAbstract.SubscriptionType.none);
     220 + updateRoster(buddySession, JID.jidInstance(userJid), RosterAbstract.SubscriptionType.none);
     221 + 
     222 + assertEquals(1, writer.getQueue().size());
     223 + assertPresenceUnavailable(writer.getQueue().poll());
     224 + }
     225 + 
     226 + private void assertPresenceUnavailable(Packet presence) {
     227 + assertEquals("presence", presence.getElemName());
     228 + assertEquals(StanzaType.unavailable, presence.getType());
     229 + }
     230 + 
     231 + private void assertPresenceAway(Packet presence) {
     232 + assertEquals("presence", presence.getElemName());
     233 + assertEquals(null, presence.getType());
     234 + assertEquals(JID.jidInstance(buddyJid), presence.getStanzaTo());
     235 + assertEquals("xa", presence.getElemCDataStaticStr(new String[] { "presence", "show"}));
     236 + }
     237 + 
     238 + private void updateRoster(XMPPResourceConnection session, JID buddy, RosterAbstract.SubscriptionType subscriptionType)
     239 + throws TigaseDBException, NotAuthorizedException, PolicyViolationException, NoConnectionIdException {
     240 + rosterUtil.addBuddy(session, buddy, null, null, null);
     241 + rosterUtil.setBuddySubscription(session, subscriptionType, buddy);
     242 + Element new_buddy = rosterUtil.getBuddyItem(session, buddy);
     243 + rosterUtil.updateBuddyChange(session, new ArrayDeque<>(), new_buddy);
     244 + try {
     245 + Thread.sleep(1000);
     246 + } catch (Throwable ex) {}
     247 + }
     248 +
     249 + protected void registerLocalBeans(Kernel kernel) {
     250 + super.registerBeans(kernel);
     251 + 
     252 + kernel.registerBean("eventBus").asInstance(EventBusFactory.getInstance()).exportable().exec();
     253 + kernel.registerBean("sess-man").asInstance(this.getSessionManagerHandler()).setActive(true).exportable().exec();//.asClass(DummySessionManager.class).setActive(true).exportable().exec();
     254 + kernel.registerBean(MessageDeliveryLogic.class).setActive(true).exportable().exec();
     255 + kernel.registerBean("msgRepository").asClass(PushNotificationsTest.MsgRepositoryIfcImpl.class).exportable().exec();
     256 + kernel.registerBean(PushNotifications.class).setActive(true).exec();
     257 + kernel.registerBean("writer").asInstance(new TestPacketWriter()).exec();
     258 + kernel.registerBean(PushPresence.class).setActive(true).exec();
     259 + kernel.registerBean(PresenceState.class).setActive(true).exec();
     260 + System.out.println("registering local beans done.");
     261 + }
     262 + 
     263 + protected static class TestPacketWriter implements PacketWriter {
     264 + 
     265 + private final ArrayDeque<Packet> queue = new ArrayDeque<>();
     266 + 
     267 + public TestPacketWriter() {
     268 + System.out.println("creating new instance " + this + "....");
     269 + }
     270 + 
     271 + @Override
     272 + public synchronized void write(Collection<Packet> packets) {
     273 + queue.addAll(packets);
     274 + }
     275 + 
     276 + @Override
     277 + public synchronized void write(Packet packet) {
     278 + queue.add(packet);
     279 + }
     280 + 
     281 + @Override
     282 + public synchronized void write(Packet packet, AsyncCallback callback) {
     283 + queue.add(packet);
     284 + }
     285 + 
     286 + public synchronized ArrayDeque<Packet> getQueue() {
     287 + return queue;
     288 + }
     289 + }
     290 +}
Please wait...
Page is in error, reload to recover