From 10dcb7324fac83c0190ff4a842360a035449f278 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Tue, 20 Jun 2023 07:10:16 +0300 Subject: VK: read premium status using Callback API --- src/main/java/com/juick/config/SecurityConfig.java | 2 +- src/main/java/com/juick/service/UserService.java | 5 +- .../java/com/juick/service/UserServiceImpl.java | 13 ++- src/main/java/com/juick/service/VKService.java | 94 ++++++++++++++++++ .../java/com/juick/www/api/webhooks/VkWebhook.java | 76 ++++++++++++++ .../com/juick/www/controllers/SocialLogin.java | 109 +++++++-------------- 6 files changed, 218 insertions(+), 81 deletions(-) create mode 100644 src/main/java/com/juick/service/VKService.java create mode 100644 src/main/java/com/juick/www/api/webhooks/VkWebhook.java (limited to 'src/main/java/com/juick') diff --git a/src/main/java/com/juick/config/SecurityConfig.java b/src/main/java/com/juick/config/SecurityConfig.java index a8f54cfd..e02e32ed 100644 --- a/src/main/java/com/juick/config/SecurityConfig.java +++ b/src/main/java/com/juick/config/SecurityConfig.java @@ -200,7 +200,7 @@ public class SecurityConfig { "/api/swagger-ui/**", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", - "/api/tlgmbtwbhk", "/api/fbwbhk", "/api/_patreon", + "/api/tlgmbtwbhk", "/api/fbwbhk", "/api/_patreon", "/api/_vk", "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/_applelogin", "/api/signup", diff --git a/src/main/java/com/juick/service/UserService.java b/src/main/java/com/juick/service/UserService.java index fe5ce23f..4acc5b6a 100644 --- a/src/main/java/com/juick/service/UserService.java +++ b/src/main/java/com/juick/service/UserService.java @@ -25,12 +25,14 @@ import com.juick.model.ExternalToken; import com.juick.util.UsernameTakenException; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.apache.commons.lang3.tuple.Pair; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; /** @@ -151,7 +153,8 @@ public interface UserService { String getTelegramName(int uid); - List> getVkTokens(List uids); + @Nullable + Pair getVkTokens(int uid); void deleteVKUser(Integer uid); diff --git a/src/main/java/com/juick/service/UserServiceImpl.java b/src/main/java/com/juick/service/UserServiceImpl.java index c586886b..d19af067 100644 --- a/src/main/java/com/juick/service/UserServiceImpl.java +++ b/src/main/java/com/juick/service/UserServiceImpl.java @@ -741,14 +741,13 @@ public class UserServiceImpl extends BaseJdbcService implements UserService { @Transactional(readOnly = true) @Override - public List> getVkTokens(List uids) { - return getNamedParameterJdbcTemplate().query( - """ - SELECT vk_id, access_token FROM vk WHERE crosspost = 1 AND access_token <> ''""" - + (uids.isEmpty() ? "" : " AND user_id IN (:uids)"), + public Pair getVkTokens(int uid) { + var result = getNamedParameterJdbcTemplate().query( + "SELECT vk_id, access_token FROM vk WHERE user_id=:uid AND crosspost = 1 AND access_token <> ''", new MapSqlParameterSource() - .addValue("uids", uids), + .addValue("uid", uid), (rs, num) -> Pair.of(rs.getString(1), rs.getString(2))); + return result.isEmpty() ? null : result.get(0); } @Transactional @@ -885,7 +884,7 @@ public class UserServiceImpl extends BaseJdbcService implements UserService { @Transactional(readOnly = true) @Override public boolean canDeleteTelegramUser(User user) { - return getEmails(user).size() > 0 || getFbCrossPostStatus(user.getUid()).isConnected() || !getVkTokens(List.of(user.getUid())).isEmpty(); + return getEmails(user).size() > 0 || getFbCrossPostStatus(user.getUid()).isConnected() || getVkTokens(user.getUid()) != null; } private static class TokenMapper implements RowMapper { diff --git a/src/main/java/com/juick/service/VKService.java b/src/main/java/com/juick/service/VKService.java new file mode 100644 index 00000000..14d7e3e9 --- /dev/null +++ b/src/main/java/com/juick/service/VKService.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2008-2023, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.scribejava.apis.VkontakteApi; +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.inject.Inject; + +@Service +public class VKService { + private static final Logger logger = LoggerFactory.getLogger("VK"); + @Value("${vk_appid:appid}") + private String VK_APPID; + @Value("${vk_secret:secret}") + private String VK_SECRET; + private static final String VK_REDIRECT = "https://juick.com/_vklogin"; + @Inject + private ObjectMapper jsonMapper; + @Inject + private UserService userService; + private OAuth20Service vkAuthService; + + @PostConstruct + public void init() { + ServiceBuilder vkBuilder = new ServiceBuilder(VK_APPID); + setVkAuthService(vkBuilder.apiSecret(VK_SECRET) + .defaultScope("friends,wall,offline,groups") + .callback(VK_REDIRECT) + .build(VkontakteApi.instance())); + } + + public void updatePremiumStatus(Integer userId) { + var vkUser = userService.getVkTokens(userId); + if (vkUser != null) { + OAuth2AccessToken token = new OAuth2AccessToken(vkUser.getRight()); + OAuthRequest donRequest = new OAuthRequest(Verb.GET, + "https://api.vk.com/method/donut.isDon?owner_id=-67669480&v=5.131"); + getVkAuthService().signRequest(token, donRequest); + try (Response vkResponse = getVkAuthService().execute(donRequest)) { + if (vkResponse.isSuccessful()) { + logger.info(vkResponse.getBody()); + var response = jsonMapper.readTree(vkResponse.getBody()); + if (response.has("response")) { + var isDon = response.get("response").intValue() > 0; + logger.info("{} is Don: {}", vkUser.getLeft(), isDon); + userService.setPremium(userId, isDon); + } else { + // token is expired or does not have "groups" permissions + userService.updateVkToken(userId, ""); + } + } + } catch (Exception e) { + logger.error("Don request error", e); + } + } else { + logger.warn("User is not connected to VK: {}", userId); + } + } + + public OAuth20Service getVkAuthService() { + return vkAuthService; + } + + public void setVkAuthService(OAuth20Service vkAuthService) { + this.vkAuthService = vkAuthService; + } +} diff --git a/src/main/java/com/juick/www/api/webhooks/VkWebhook.java b/src/main/java/com/juick/www/api/webhooks/VkWebhook.java new file mode 100644 index 00000000..9e4477b1 --- /dev/null +++ b/src/main/java/com/juick/www/api/webhooks/VkWebhook.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2008-2023, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api.webhooks; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.service.UserService; +import com.juick.service.VKService; +import com.juick.util.HttpBadRequestException; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.inject.Inject; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +@RestController +public class VkWebhook { + private static final Logger logger = LoggerFactory.getLogger("VK"); + @Value("${vk_webhook_secret:}") + private String secretKey; + @Value("${vk_webhook_confirmation:}") + private String confirmationCode; + @Inject + private ObjectMapper jsonMapper; + @Inject + private UserService userService; + @Inject + private VKService vkService; + + @PostMapping(value = "/api/_vk") + public String processUpdate(InputStream body) throws IOException { + String data = IOUtils.toString(body, StandardCharsets.UTF_8); + logger.debug("Data: {}", data); + JsonNode json = jsonMapper.readTree(data); + var type = json.get("type").asText(); + var secret = json.get("secret").asText(""); + logger.info("Event received: {}, secret: {}", type, secret); + if (type.equals("confirmation")) { + return confirmationCode; + } else { + if (secretKey.equals(secret)) { + if (type.startsWith("donut_")) { + var vkId = json.get("object").get("user_id").asLong(0); + var userId = userService.getUIDbyVKID(vkId); + if (userId > 0) { + vkService.updatePremiumStatus(userId); + } + } + return "ok"; + } else { + throw new HttpBadRequestException(); + } + } + } +} diff --git a/src/main/java/com/juick/www/controllers/SocialLogin.java b/src/main/java/com/juick/www/controllers/SocialLogin.java index b0738fea..0cac26c5 100644 --- a/src/main/java/com/juick/www/controllers/SocialLogin.java +++ b/src/main/java/com/juick/www/controllers/SocialLogin.java @@ -27,6 +27,7 @@ import com.juick.model.ext.vk.UsersResponse; import com.juick.service.EmailService; import com.juick.service.TelegramService; import com.juick.service.UserService; +import com.juick.service.VKService; import com.juick.service.security.entities.JuickUser; import com.juick.util.HttpBadRequestException; @@ -81,21 +82,17 @@ public class SocialLogin { private String FACEBOOK_SECRET; @Value("${ap_base_uri:http://localhost:8080/}") private String baseUri; - private static final String VK_REDIRECT = "http://juick.com/_vklogin"; private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/1.1/account/verify_credentials.json"; @Inject private ObjectMapper jsonMapper; private ServiceBuilder twitterBuilder; - private OAuth20Service facebookAuthService, vkAuthService, appleSignInService; - + private OAuth20Service facebookAuthService, appleSignInService; + @Inject + private VKService vkService; @Value("${twitter_consumer_key:appid}") private String twitterConsumerKey; @Value("${twitter_consumer_secret:secret}") private String twitterConsumerSecret; - @Value("${vk_appid:appid}") - private String VK_APPID; - @Value("${vk_secret:secret}") - private String VK_SECRET; @Value("${telegram_token:secret}") private String telegramToken; @Value("${apple_app_id:appid}") @@ -115,13 +112,11 @@ public class SocialLogin { public void init() { ServiceBuilder facebookBuilder = new ServiceBuilder(FACEBOOK_APPID); twitterBuilder = new ServiceBuilder(twitterConsumerKey); - ServiceBuilder vkBuilder = new ServiceBuilder(VK_APPID); + UriComponentsBuilder redirectBuilder = UriComponentsBuilder.fromUriString(baseUri); String facebookRedirectUri = redirectBuilder.replacePath("/_fblogin").build().toUriString(); facebookAuthService = facebookBuilder.apiSecret(FACEBOOK_SECRET).callback(facebookRedirectUri) .defaultScope("email").build(FacebookApi.instance()); - vkAuthService = vkBuilder.apiSecret(VK_SECRET).defaultScope("friends,wall,offline,groups").callback(VK_REDIRECT) - .build(VkontakteApi.instance()); ServiceBuilder appleSignInBuilder = new ServiceBuilder(appleApplicationId); String appleSignInRedirectUri = redirectBuilder.replacePath("/_apple").build().toUriString(); appleSignInService = appleSignInBuilder.callback(appleSignInRedirectUri).defaultScope("email") @@ -249,7 +244,7 @@ public class SocialLogin { vkstate = UUID.randomUUID().toString(); Cookie c = new Cookie("vkstate", vkstate); response.addCookie(c); - return "redirect:" + vkAuthService.getAuthorizationUrl(vkstate); + return "redirect:" + vkService.getVkAuthService().getAuthorizationUrl(vkstate); } if (StringUtils.isBlank(vkstate) || !vkstate.equals(state)) { @@ -259,43 +254,44 @@ public class SocialLogin { c.setMaxAge(0); response.addCookie(c); } - OAuth2AccessToken token = vkAuthService.getAccessToken(code); + OAuth2AccessToken token = vkService.getVkAuthService().getAccessToken(code); OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.131"); - vkAuthService.signRequest(token, meRequest); - Response vkResponse = vkAuthService.execute(meRequest); - if (vkResponse.isSuccessful()) { - String graph = vkResponse.getBody(); - com.juick.model.ext.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).users().stream() - .findFirst().orElseThrow(HttpBadRequestException::new); - String vkName = jsonUser.firstName() + " " + jsonUser.lastName(); - String vkLink = jsonUser.screenName(); - - if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) { - logger.error("vk user error"); - throw new HttpBadRequestException(); - } + vkService.getVkAuthService().signRequest(token, meRequest); + try (Response vkResponse = vkService.getVkAuthService().execute(meRequest)) { + if (vkResponse.isSuccessful()) { + String graph = vkResponse.getBody(); + com.juick.model.ext.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).users().stream() + .findFirst().orElseThrow(HttpBadRequestException::new); + String vkName = jsonUser.firstName() + " " + jsonUser.lastName(); + String vkLink = jsonUser.screenName(); - long vkID = NumberUtils.toLong(jsonUser.id(), 0); - int uid = userService.getUIDbyVKID(vkID); - if (uid > 0) { - userService.updateVkUser(vkID, token.getAccessToken(), vkName, vkLink); - Cookie c = new Cookie("hash", userService.getHashByUID(uid)); - c.setMaxAge(50 * 24 * 60 * 60); - response.addCookie(c); - return "redirect:/" + Optional.ofNullable(referer).orElse(StringUtils.EMPTY); - } else { - String loginhash = UUID.randomUUID().toString(); - if (!userService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) { - logger.error("create vk user error"); + if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) { + logger.error("vk user error"); throw new HttpBadRequestException(); } - return "redirect:/signup?type=vk&hash=" + loginhash; + + long vkID = NumberUtils.toLong(jsonUser.id(), 0); + int uid = userService.getUIDbyVKID(vkID); + if (uid > 0) { + userService.updateVkUser(vkID, token.getAccessToken(), vkName, vkLink); + Cookie c = new Cookie("hash", userService.getHashByUID(uid)); + c.setMaxAge(50 * 24 * 60 * 60); + response.addCookie(c); + return "redirect:/" + Optional.ofNullable(referer).orElse(StringUtils.EMPTY); + } else { + String loginhash = UUID.randomUUID().toString(); + if (!userService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) { + logger.error("create vk user error"); + throw new HttpBadRequestException(); + } + return "redirect:/signup?type=vk&hash=" + loginhash; + } + } else { + logger.error("vk error {}: {}", vkResponse.getCode(), vkResponse.getBody()); + throw new HttpBadRequestException(); } - } else { - logger.error("vk error {}: {}", vkResponse.getCode(), vkResponse.getBody()); - throw new HttpBadRequestException(); } } @@ -375,35 +371,4 @@ public class SocialLogin { } throw new HttpBadRequestException(); } - - @Scheduled(fixedRate = 3600000) - public void updatePremium() { - userService.getVkTokens(List.of()) - .forEach(vkUser -> { - var userId = userService.getUIDbyVKID(Long.parseLong(vkUser.getLeft())); - if (userId > 0) { - OAuth2AccessToken token = new OAuth2AccessToken(vkUser.getRight()); - OAuthRequest donRequest = new OAuthRequest(Verb.GET, - "https://api.vk.com/method/donut.isDon?owner_id=-67669480&v=5.131"); - vkAuthService.signRequest(token, donRequest); - try { - Response vkResponse = vkAuthService.execute(donRequest); - if (vkResponse.isSuccessful()) { - logger.info(vkResponse.getBody()); - var response = jsonMapper.readTree(vkResponse.getBody()); - if (response.has("response")) { - var isDon = response.get("response").intValue() > 0; - logger.info("{} is Don: {}", vkUser.getLeft(), isDon); - userService.setPremium(userId, isDon); - } else { - // token is expired or does not have "groups" permissions - userService.updateVkToken(userId, ""); - } - } - } catch (Exception e) { - logger.error("Don request error", e); - } - } - }); - } } -- cgit v1.2.3