From aa3c1a06ed46f49b617e2956b6cf9a1b0d367fb0 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Tue, 22 Nov 2016 17:49:03 +0300 Subject: add APNS feedback component --- juick-api/src/main/java/com/juick/api/Main.java | 25 ++++++++ .../src/main/java/com/juick/api/Notifications.java | 17 ++++++ juick-notifications/build.gradle | 2 + .../main/java/com/juick/components/CleanUp.java | 52 ++++++++++++++++ .../java/com/juick/components/Notifications.java | 9 +-- .../NotificationsAppConfiguration.java | 7 +++ .../juick/components/test/NotificationTests.java | 69 ++++++++++++++++++++++ .../com/juick/configuration/DataConfiguration.java | 1 - .../main/java/com/juick/server/PushQueries.java | 4 ++ .../java/com/juick/server/helpers/TokensList.java | 30 ++++++++++ .../java/com/juick/service/PushQueriesService.java | 2 + .../com/juick/service/PushQueriesServiceImpl.java | 5 ++ 12 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 juick-notifications/src/main/java/com/juick/components/CleanUp.java create mode 100644 juick-notifications/src/test/java/com/juick/components/test/NotificationTests.java create mode 100644 juick-server/src/main/java/com/juick/server/helpers/TokensList.java diff --git a/juick-api/src/main/java/com/juick/api/Main.java b/juick-api/src/main/java/com/juick/api/Main.java index 2bb42e3a..dffb9d66 100644 --- a/juick-api/src/main/java/com/juick/api/Main.java +++ b/juick-api/src/main/java/com/juick/api/Main.java @@ -477,6 +477,31 @@ public class Main extends HttpServlet { Main.replyJSON(request, response, serializer.serialize(jmsg).toString()); } + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (req.getCharacterEncoding() == null) { + req.setCharacterEncoding("UTF-8"); + } + + int vuid = Utils.getHttpAuthUID(jdbc, req); + if (vuid == 0) { + vuid = Utils.getVisitorQueryStringUID(jdbc, req); + } + if (vuid == 0) { + resp.sendError(401); + return; + } + String uri = req.getRequestURI(); + switch (uri) { + case "/notifications": + notifications.doDelete(req, resp, vuid); + break; + default: + resp.sendError(400); + break; + } + } + public static void replyJSON(HttpServletRequest request, HttpServletResponse response, String json) throws IOException { response.setContentType("application/json; charset=UTF-8"); response.setHeader("Access-Control-Allow-Origin", "*"); diff --git a/juick-api/src/main/java/com/juick/api/Notifications.java b/juick-api/src/main/java/com/juick/api/Notifications.java index a0c9cae9..707df6e4 100644 --- a/juick-api/src/main/java/com/juick/api/Notifications.java +++ b/juick-api/src/main/java/com/juick/api/Notifications.java @@ -1,5 +1,6 @@ package com.juick.api; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.Message; import com.juick.User; @@ -7,8 +8,10 @@ import com.juick.server.MessagesQueries; import com.juick.server.PushQueries; import com.juick.server.SubscriptionsQueries; import com.juick.server.UserQueries; +import com.juick.server.helpers.TokensList; import org.apache.commons.lang3.math.NumberUtils; import org.springframework.jdbc.core.JdbcTemplate; +import spark.utils.IOUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -89,4 +92,18 @@ public class Notifications { } } } + + public void doDelete(HttpServletRequest request, HttpServletResponse response, int vuid) throws IOException { + User visitor = UserQueries.getUserByUID(jdbc, vuid).orElse(new User()); + if ((visitor.getUid() == 0) || !(visitor.getName().equals("juick"))) { + response.sendError(403); + return; + } + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); + TokensList list = mapper.readValue(IOUtils.toString(request.getInputStream()), TokensList.class); + list.getTokens().forEach(t -> PushQueries.deleteAPNSToken(jdbc, t)); + } } diff --git a/juick-notifications/build.gradle b/juick-notifications/build.gradle index 861c7234..3dc52132 100644 --- a/juick-notifications/build.gradle +++ b/juick-notifications/build.gradle @@ -12,6 +12,8 @@ dependencies { compile "org.springframework:spring-webmvc:${springFrameworkVersion}" compile 'com.mitchellbosecke:pebble-spring4:2.2.3' providedRuntime 'mysql:mysql-connector-java:5.1.40' + testCompile 'junit:junit:4.12' + testCompile "org.mockito:mockito-core:1.+" } compileJava.options.encoding = 'UTF-8' diff --git a/juick-notifications/src/main/java/com/juick/components/CleanUp.java b/juick-notifications/src/main/java/com/juick/components/CleanUp.java new file mode 100644 index 00000000..06d96471 --- /dev/null +++ b/juick-notifications/src/main/java/com/juick/components/CleanUp.java @@ -0,0 +1,52 @@ +package com.juick.components; + +import com.juick.server.helpers.TokensList; +import com.notnoop.apns.ApnsService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by vitalyster on 22.11.2016. + */ +@Component +public class CleanUp { + + private static Logger logger = LoggerFactory.getLogger(CleanUp.class); + + @Inject + ApnsService apns; + @Inject + RestTemplate rest; + + @Scheduled(fixedRate = 600000) + public void cleanupTokens() { + logger.info("initializing apns tokens cleanup"); + Collection devices = apns.getInactiveDevices().keySet(); + int count = devices.size(); + if (count > 0) { + logger.info(String.format("%d tokens to delete", count)); + TokensList list = new TokensList(); + list.setType("apns"); + list.setTokens(new ArrayList<>(devices)); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON_UTF8); + rest.exchange("http://api.juick.com/notifications", + HttpMethod.DELETE, new HttpEntity<>(list, headers), new ParameterizedTypeReference() { + }); + } else { + logger.info("No APNS tokens to delete"); + } + } +} diff --git a/juick-notifications/src/main/java/com/juick/components/Notifications.java b/juick-notifications/src/main/java/com/juick/components/Notifications.java index ebc69bd1..cb120ce9 100644 --- a/juick-notifications/src/main/java/com/juick/components/Notifications.java +++ b/juick-notifications/src/main/java/com/juick/components/Notifications.java @@ -48,6 +48,7 @@ import rocks.xmpp.core.session.XmppSessionConfiguration; import rocks.xmpp.extensions.component.accept.ExternalComponent; import javax.annotation.PostConstruct; +import javax.inject.Inject; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -70,10 +71,12 @@ public class Notifications implements AutoCloseable { private final int xmppPort; private final String xmppPushPassword; + @Inject + private ApnsService apns; + public Notifications(final Environment env, final RestTemplate rest) { this.rest = rest; - wns_application_sip = env.getProperty("wns_application_sip", ""); wns_client_secret = env.getProperty("wns_client_secret", ""); GCMSender = new Sender(env.getProperty("gcm_key", ""), Endpoint.GCM); @@ -191,12 +194,10 @@ public class Notifications implements AutoCloseable { }).getBody()); } if (!tokens.isEmpty()) { - ApnsService service = APNS.newService().withCert("/etc/juick/ios.p12", "juick") - .withSandboxDestination().build(); for (String token : tokens) { String payload = APNS.newPayload().alertTitle("@" + jmsg.getUser().getName()).alertBody(jmsg.getText()).build(); logger.info("APNS: " + token); - service.push(token, payload); + apns.push(token, payload); } } else { logger.info("APNS: no recipients"); diff --git a/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsAppConfiguration.java b/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsAppConfiguration.java index c7747aa1..1bb4694a 100644 --- a/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsAppConfiguration.java +++ b/juick-notifications/src/main/java/com/juick/components/configuration/NotificationsAppConfiguration.java @@ -2,6 +2,8 @@ package com.juick.components.configuration; import com.juick.components.Notifications; import com.juick.configuration.DataConfiguration; +import com.notnoop.apns.APNS; +import com.notnoop.apns.ApnsService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -41,4 +43,9 @@ public class NotificationsAppConfiguration { public Notifications push() { return new Notifications(env, rest()); } + @Bean + public ApnsService apns() { + return APNS.newService().withCert(env.getProperty("ios_pkcs12_file"), env.getProperty("ios_pkcs12_password")) + .withSandboxDestination().build(); + } } diff --git a/juick-notifications/src/test/java/com/juick/components/test/NotificationTests.java b/juick-notifications/src/test/java/com/juick/components/test/NotificationTests.java new file mode 100644 index 00000000..c4a4ed24 --- /dev/null +++ b/juick-notifications/src/test/java/com/juick/components/test/NotificationTests.java @@ -0,0 +1,69 @@ +package com.juick.components.test; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import com.juick.components.CleanUp; +import com.notnoop.apns.ApnsService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.slf4j.LoggerFactory; +import org.springframework.web.client.RestTemplate; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Created by vitalyster on 22.11.2016. + */ +public class NotificationTests { + @InjectMocks + CleanUp cleanUp; + + @Mock + ApnsService service; + @Mock + RestTemplate rest; + + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void CleanUpTest() { + Map inactiveDevices = new HashMap<>(); + inactiveDevices.put("yoyoyo", new Date()); + when(service.getInactiveDevices()).thenReturn(inactiveDevices); + ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + final Appender mockAppender = mock(Appender.class); + when(mockAppender.getName()).thenReturn("MOCK"); + root.addAppender(mockAppender); + cleanUp.cleanupTokens(); + verify(mockAppender).doAppend(argThat(new ArgumentMatcher() { + @Override + public boolean matches(final Object argument) { + return ((LoggingEvent)argument).getFormattedMessage().contains("1 tokens to delete"); + } + })); + when(service.getInactiveDevices()).thenReturn(new HashMap<>()); + cleanUp.cleanupTokens(); + verify(mockAppender).doAppend(argThat(new ArgumentMatcher() { + @Override + public boolean matches(final Object argument) { + return ((LoggingEvent)argument).getFormattedMessage().contains("No APNS tokens to delete"); + } + })); + } +} diff --git a/juick-server/src/main/java/com/juick/configuration/DataConfiguration.java b/juick-server/src/main/java/com/juick/configuration/DataConfiguration.java index 9f733184..89fda764 100644 --- a/juick-server/src/main/java/com/juick/configuration/DataConfiguration.java +++ b/juick-server/src/main/java/com/juick/configuration/DataConfiguration.java @@ -1,7 +1,6 @@ package com.juick.configuration; import org.apache.commons.dbcp2.BasicDataSource; -import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; diff --git a/juick-server/src/main/java/com/juick/server/PushQueries.java b/juick-server/src/main/java/com/juick/server/PushQueries.java index 904b3c0d..5ee3cef3 100644 --- a/juick-server/src/main/java/com/juick/server/PushQueries.java +++ b/juick-server/src/main/java/com/juick/server/PushQueries.java @@ -41,4 +41,8 @@ public class PushQueries { return sql.queryForList("SELECT token FROM ios INNER JOIN users " + "ON (users.id=ios.user_id) WHERE users.id IN (" + StringUtils.collectionToCommaDelimitedString(uids) + ")", String.class); } + + public static boolean deleteAPNSToken(JdbcTemplate sql, String token) { + return sql.update("DELETE FROM ios WHERE token=?", token) > 0; + } } diff --git a/juick-server/src/main/java/com/juick/server/helpers/TokensList.java b/juick-server/src/main/java/com/juick/server/helpers/TokensList.java new file mode 100644 index 00000000..cb331e04 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/helpers/TokensList.java @@ -0,0 +1,30 @@ +package com.juick.server.helpers; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Created by vitalyster on 22.11.2016. + */ +public class TokensList { + private String type; + private List tokens; + + @JsonProperty("type") + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getTokens() { + return tokens; + } + + public void setTokens(List tokens) { + this.tokens = tokens; + } +} diff --git a/juick-server/src/main/java/com/juick/service/PushQueriesService.java b/juick-server/src/main/java/com/juick/service/PushQueriesService.java index 8e7ce543..fe956c3a 100644 --- a/juick-server/src/main/java/com/juick/service/PushQueriesService.java +++ b/juick-server/src/main/java/com/juick/service/PushQueriesService.java @@ -17,4 +17,6 @@ public interface PushQueriesService { List getAPNSToken(int uid); List getAPNSTokens(List uids); + + boolean deleteAPNSToken(String token); } diff --git a/juick-server/src/main/java/com/juick/service/PushQueriesServiceImpl.java b/juick-server/src/main/java/com/juick/service/PushQueriesServiceImpl.java index 91861bd9..9bbadef6 100644 --- a/juick-server/src/main/java/com/juick/service/PushQueriesServiceImpl.java +++ b/juick-server/src/main/java/com/juick/service/PushQueriesServiceImpl.java @@ -60,6 +60,11 @@ public class PushQueriesServiceImpl extends BaseJdbcService implements PushQueri uid); } + @Override + public boolean deleteAPNSToken(String token) { + return getJdbcTemplate().update("DELETE FROM ios WHERE token=?", token) > 0; + } + @Override public List getAPNSTokens(final List uids) { return getJdbcTemplate().queryForList( -- cgit v1.2.3