From c576c003716851863c832b5e0dacb9186516f167 Mon Sep 17 00:00:00 2001
From: Vitaly Takmazov
Date: Wed, 19 Jul 2023 23:50:14 +0300
Subject: Migrate to Twitter API 2.0
---
src/main/java/com/juick/TwitterManager.java | 41 ++------
.../java/com/juick/model/ext/twitter/User.java | 28 ------
.../java/com/juick/service/TwitterService.java | 105 +++++++++++++++++++++
src/main/java/com/juick/service/UserService.java | 6 +-
.../java/com/juick/service/UserServiceImpl.java | 24 ++++-
src/main/java/com/juick/www/controllers/Site.java | 1 +
.../com/juick/www/controllers/SocialLogin.java | 89 +++++++++--------
7 files changed, 180 insertions(+), 114 deletions(-)
delete mode 100644 src/main/java/com/juick/model/ext/twitter/User.java
create mode 100644 src/main/java/com/juick/service/TwitterService.java
(limited to 'src/main/java/com/juick')
diff --git a/src/main/java/com/juick/TwitterManager.java b/src/main/java/com/juick/TwitterManager.java
index f4cab89b..7a01debd 100644
--- a/src/main/java/com/juick/TwitterManager.java
+++ b/src/main/java/com/juick/TwitterManager.java
@@ -16,18 +16,15 @@
*/
package com.juick;
-import com.juick.model.Message;
-import com.juick.www.api.SystemActivity;
+import com.juick.service.TwitterService;
import com.juick.service.UserService;
-import com.juick.service.component.*;
+import com.juick.service.component.NotificationListener;
+import com.juick.service.component.PingEvent;
+import com.juick.service.component.SystemEvent;
import com.juick.util.MessageUtils;
import com.juick.util.TagUtils;
-
+import com.juick.www.api.SystemActivity;
import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
-import twitter4j.Twitter;
import javax.inject.Inject;
@@ -36,33 +33,11 @@ import javax.inject.Inject;
*/
public class TwitterManager implements NotificationListener {
- private static final Logger logger = LoggerFactory.getLogger(TwitterManager.class);
-
@Inject
private UserService userService;
- @Value("${twitter_consumer_key:12345678}")
- private String twitter_consumer_key;
- @Value("${twitter_consumer_secret:secret}")
- private String twitter_consumer_secret;
-
- void twitterPost(final Message jmsg) {
- userService.getTwitterToken(jmsg.getUser().getUid()).ifPresent(t -> {
- String status = MessageUtils.getMessageHashTags(jmsg) + StringUtils.defaultString(jmsg.getText());
- if (status.length() > 253) {
- status = status.substring(0, 252) + "…";
- }
- status += " http://juick.com/" + jmsg.getMid();
- var twitter = Twitter.newBuilder()
- .oAuthConsumer(twitter_consumer_key, twitter_consumer_secret)
- .oAuthAccessToken(t.token(), t.secret()).build();
- try {
- twitter.v1().tweets().updateStatus(status);
- } catch (Exception e) {
- logger.info("Twitter exception {}: {}", jmsg.getUser().getName(), e);
- }
- });
- }
+ @Inject
+ private TwitterService twitterService;
@Override
public void processSystemEvent(SystemEvent systemEvent) {
@@ -74,7 +49,7 @@ public class TwitterManager implements NotificationListener {
}
if (StringUtils.isNotEmpty(userService.getTwitterName(msg.getUser().getUid()))) {
if (TagUtils.hasNoTag(msg.getTags(), "notwitter")) {
- twitterPost(msg);
+ twitterService.twitterPost(msg);
}
}
}
diff --git a/src/main/java/com/juick/model/ext/twitter/User.java b/src/main/java/com/juick/model/ext/twitter/User.java
deleted file mode 100644
index 5a09556d..00000000
--- a/src/main/java/com/juick/model/ext/twitter/User.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2008-2022, 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.model.ext.twitter;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * Created by vitalyster on 28.11.2016.
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public record User( @JsonProperty("screen_name") String screenName) {
-}
diff --git a/src/main/java/com/juick/service/TwitterService.java b/src/main/java/com/juick/service/TwitterService.java
new file mode 100644
index 00000000..d0c105f2
--- /dev/null
+++ b/src/main/java/com/juick/service/TwitterService.java
@@ -0,0 +1,105 @@
+/*
+ * 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.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.scribejava.apis.TwitterApi20;
+import com.github.scribejava.core.builder.ServiceBuilder;
+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 com.juick.model.Message;
+import com.juick.util.MessageUtils;
+import jakarta.annotation.PostConstruct;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+
+@Service
+public class TwitterService {
+ private static final Logger logger = LoggerFactory.getLogger("Twitter");
+ @Value("${twitter_client_id:12345678}")
+ private String clientId;
+ @Value("${twitter_client_secret:secret}")
+ private String clientSecret;
+ private static final String redirectUri = "https://juick.com/_twitter";
+ @Inject
+ private UserService userService;
+ private OAuth20Service twitterAuthService;
+
+ @Inject
+ private ObjectMapper jsonMapper;
+
+ @PostConstruct
+ public void init() {
+ ServiceBuilder twitterBuilder = new ServiceBuilder(clientId);
+ setTwitterAuthService(twitterBuilder.apiSecret(clientSecret)
+ .defaultScope("tweet.read tweet.write users.read offline.access")
+ .callback(redirectUri)
+ .build(TwitterApi20.instance()));
+ }
+
+ public void twitterPost(final Message jmsg) {
+ userService.getTwitterToken(jmsg.getUser().getUid()).ifPresent(t -> {
+ String status = MessageUtils.getMessageHashTags(jmsg) + StringUtils.defaultString(jmsg.getText());
+ if (status.length() > 253) {
+ status = status.substring(0, 252) + "…";
+ }
+ status += " http://juick.com/" + jmsg.getMid();
+ try {
+ var token = twitterAuthService.refreshAccessToken(t.secret());
+ userService.refreshTwitterToken(jmsg.getUser(), token.getAccessToken(), token.getRefreshToken());
+ OAuthRequest postRequest = new OAuthRequest(Verb.POST,
+ "https://api.twitter.com/2/tweets");
+ var body = jsonMapper.createObjectNode();
+ body.put("text", status);
+ postRequest.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE);
+ postRequest.setPayload(jsonMapper.writeValueAsString(body));
+ twitterAuthService.signRequest(token, postRequest);
+ try (Response twitterResponse = twitterAuthService.execute(postRequest)) {
+ if (twitterResponse.isSuccessful()) {
+ logger.info(twitterResponse.getBody());
+ } else {
+ logger.warn("Twitter error {}: {}", twitterResponse.getCode(), twitterResponse.getBody());
+ }
+ } catch (Exception e) {
+ logger.error("Twitter exception {}", jmsg.getUser().getName(), e);
+ }
+ } catch (IOException | InterruptedException | ExecutionException e) {
+ logger.error("Twitter exception {}", jmsg.getUser().getName(), e);
+ }
+ });
+ }
+
+ public OAuth20Service getTwitterAuthService() {
+ return twitterAuthService;
+ }
+
+ public void setTwitterAuthService(OAuth20Service twitterAuthService) {
+ this.twitterAuthService = twitterAuthService;
+ }
+}
diff --git a/src/main/java/com/juick/service/UserService.java b/src/main/java/com/juick/service/UserService.java
index 4acc5b6a..31822b01 100644
--- a/src/main/java/com/juick/service/UserService.java
+++ b/src/main/java/com/juick/service/UserService.java
@@ -104,7 +104,11 @@ public interface UserService {
List getUserIgnoredUsers(int uid);
List getUserVipUsers(int uid);
@CacheEvict(value = "twitter_user", key="{ #user.getUid() }")
- boolean linkTwitterAccount(User user, String accessToken, String accessTokenSecret, String screenName);
+ boolean linkTwitterAccount(User user, String accessToken, String refreshToken, String screenName);
+
+ boolean refreshTwitterToken(User user, String accessToken, String refreshToken);
+
+ boolean isTwitter1User(User user);
int getStatsMessages(int uid);
diff --git a/src/main/java/com/juick/service/UserServiceImpl.java b/src/main/java/com/juick/service/UserServiceImpl.java
index d19af067..7f73e6d1 100644
--- a/src/main/java/com/juick/service/UserServiceImpl.java
+++ b/src/main/java/com/juick/service/UserServiceImpl.java
@@ -523,10 +523,24 @@ public class UserServiceImpl extends BaseJdbcService implements UserService {
@Transactional
@Override
public boolean linkTwitterAccount(
- final User user, final String accessToken, final String accessTokenSecret, final String screenName) {
- return getJdbcTemplate().update("INSERT INTO twitter(user_id,access_token,access_token_secret,uname) " +
- "VALUES (?,?,?,?)",
- user.getUid(), accessToken, accessTokenSecret, screenName) > 0;
+ final User user, final String accessToken, final String refreshToken, final String screenName) {
+ return getJdbcTemplate().update("INSERT INTO twitter(user_id,access_token,access_token_secret,refresh_token,uname) " +
+ "VALUES (?,?,'',?,?)",
+ user.getUid(), accessToken, refreshToken, screenName) > 0;
+ }
+
+ @Transactional
+ @Override
+ public boolean refreshTwitterToken(
+ final User user, final String accessToken, final String refreshToken) {
+ return getJdbcTemplate().update("UPDATE twitter SET access_token=?, refresh_token=?" +
+ " WHERE user_id=?",
+ accessToken, refreshToken, user.getUid()) > 0;
+ }
+ @Transactional(readOnly = true)
+ @Override
+ public boolean isTwitter1User(User user) {
+ return jdbcTemplate.queryForList("SELECT user_id FROM twitter WHERE user_id=? AND refresh_token=''", Integer.class, user.getUid()).size() > 0;
}
@Transactional(readOnly = true)
@@ -612,7 +626,7 @@ public class UserServiceImpl extends BaseJdbcService implements UserService {
@Override
public Optional getTwitterToken(final int uid) {
List list = getJdbcTemplate().query(
- "SELECT uname, access_token, access_token_secret FROM twitter WHERE user_id = ? AND crosspost = true",
+ "SELECT uname, access_token, refresh_token FROM twitter WHERE user_id = ? AND crosspost = true",
(rs, num) -> new ExternalToken(rs.getString(1), "twitter",
rs.getString(2), rs.getString(3)),
uid);
diff --git a/src/main/java/com/juick/www/controllers/Site.java b/src/main/java/com/juick/www/controllers/Site.java
index 8b35593c..47438900 100644
--- a/src/main/java/com/juick/www/controllers/Site.java
+++ b/src/main/java/com/juick/www/controllers/Site.java
@@ -95,6 +95,7 @@ public class Site {
tagService.getUserTagStats(user.getUid()).stream()
.sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).limit(20)
.map(t -> t.getTag().getName()).toList());
+ model.addAttribute("twitter1", userService.isTwitter1User(user));
}
@GetMapping("/login")
diff --git a/src/main/java/com/juick/www/controllers/SocialLogin.java b/src/main/java/com/juick/www/controllers/SocialLogin.java
index 0cac26c5..bae9deb2 100644
--- a/src/main/java/com/juick/www/controllers/SocialLogin.java
+++ b/src/main/java/com/juick/www/controllers/SocialLogin.java
@@ -16,18 +16,19 @@
*/
package com.juick.www.controllers;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.scribejava.apis.*;
import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.model.*;
+import com.github.scribejava.core.oauth.AccessTokenRequestParams;
+import com.github.scribejava.core.oauth.AuthorizationUrlBuilder;
import com.github.scribejava.core.oauth.OAuth10aService;
import com.github.scribejava.core.oauth.OAuth20Service;
+import com.github.scribejava.core.pkce.PKCEService;
import com.juick.model.ext.facebook.User;
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.*;
import com.juick.service.security.entities.JuickUser;
import com.juick.util.HttpBadRequestException;
@@ -82,17 +83,16 @@ public class SocialLogin {
private String FACEBOOK_SECRET;
@Value("${ap_base_uri:http://localhost:8080/}")
private String baseUri;
- private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/1.1/account/verify_credentials.json";
+ private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/2/users/me";
@Inject
private ObjectMapper jsonMapper;
private ServiceBuilder twitterBuilder;
private OAuth20Service facebookAuthService, appleSignInService;
@Inject
private VKService vkService;
- @Value("${twitter_consumer_key:appid}")
- private String twitterConsumerKey;
- @Value("${twitter_consumer_secret:secret}")
- private String twitterConsumerSecret;
+ @Inject
+ private TwitterService twitterService;
+
@Value("${telegram_token:secret}")
private String telegramToken;
@Value("${apple_app_id:appid}")
@@ -108,10 +108,11 @@ public class SocialLogin {
@Inject
private RememberMeServices rememberMeServices;
+ private AuthorizationUrlBuilder authorizationUrlBuilder;
+
@PostConstruct
public void init() {
ServiceBuilder facebookBuilder = new ServiceBuilder(FACEBOOK_APPID);
- twitterBuilder = new ServiceBuilder(twitterConsumerKey);
UriComponentsBuilder redirectBuilder = UriComponentsBuilder.fromUriString(baseUri);
String facebookRedirectUri = redirectBuilder.replacePath("/_fblogin").build().toUriString();
@@ -189,47 +190,41 @@ public class SocialLogin {
}
@GetMapping("/_twitter")
- protected void doTwitterLogin(com.juick.model.User user, HttpServletRequest request,
- HttpServletResponse response) throws IOException, ExecutionException, InterruptedException {
- String hash = StringUtils.EMPTY, request_token = StringUtils.EMPTY, request_token_secret = StringUtils.EMPTY;
- String verifier = request.getParameter("oauth_verifier");
- Cookie[] cookies = request.getCookies();
- for (Cookie cookie : cookies) {
- if (cookie.getName().equals("hash")) {
- hash = cookie.getValue();
- }
- if (cookie.getName().equals("request_token")) {
- request_token = cookie.getValue();
- }
- if (cookie.getName().equals("request_token_secret")) {
- request_token_secret = cookie.getValue();
- }
- }
- OAuth10aService oAuthService = twitterBuilder.apiSecret(twitterConsumerSecret)
- .callback("https://juick.com/_twitter").build(TwitterApi.instance());
+ protected String doTwitterLogin(@RequestParam(required = false) String code,
+ @RequestParam(required = false) String state,
+ com.juick.model.User user,
+ HttpServletRequest request)
+ throws IOException, ExecutionException, InterruptedException {
- if (request_token.isEmpty() && request_token_secret.isEmpty() && (verifier == null || verifier.isEmpty())) {
- OAuth1RequestToken requestToken = oAuthService.getRequestToken();
- String authUrl = oAuthService.getAuthorizationUrl(requestToken);
- response.addCookie(new Cookie("request_token", requestToken.getToken()));
- response.addCookie(new Cookie("request_token_secret", requestToken.getTokenSecret()));
- response.setStatus(HttpServletResponse.SC_FOUND);
- response.setHeader("Location", authUrl);
+ if (StringUtils.isBlank(code)) {
+ state = UUID.randomUUID().toString();
+ authorizationUrlBuilder = twitterService.getTwitterAuthService().createAuthorizationUrlBuilder()
+ .state(state)
+ .initPKCE();
+ return "redirect:" + authorizationUrlBuilder.build();
} else {
- if (verifier != null && verifier.length() > 0) {
- OAuth1RequestToken requestToken = new OAuth1RequestToken(request_token, request_token_secret);
- OAuth1AccessToken accessToken = oAuthService.getAccessToken(requestToken, verifier);
- OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL);
- oAuthService.signRequest(accessToken, oAuthRequest);
- com.juick.model.ext.twitter.User twitterUser = jsonMapper.readValue(
- oAuthService.execute(oAuthRequest).getBody(), com.juick.model.ext.twitter.User.class);
- if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(),
- twitterUser.screenName())) {
- response.setStatus(HttpServletResponse.SC_FOUND);
- response.setHeader("Location", "http://juick.com/settings");
+ var token = twitterService.getTwitterAuthService().getAccessToken(AccessTokenRequestParams.create(code)
+ .pkceCodeVerifier(authorizationUrlBuilder.getPkce().getCodeVerifier()));
+ var me = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL);
+ twitterService.getTwitterAuthService().signRequest(token, me);
+ try (var response = twitterService.getTwitterAuthService().execute(me)) {
+ if (response.isSuccessful()) {
+ logger.info("Twitter response: {}", response.getBody());
+ JsonNode json = jsonMapper.readTree(response.getBody());
+ var screenName = json.get("data").get("username").asText();
+ if (userService.linkTwitterAccount(user, token.getAccessToken(), token.getRefreshToken(),
+ screenName)) {
+ return "redirect:https://juick.com/settings";
+ } else {
+ throw new HttpBadRequestException();
+ }
} else {
- response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ logger.warn("Twitter error {}: {}", response.getCode(), response.getBody());
+ throw new HttpBadRequestException();
}
+ } catch (Exception e) {
+ logger.error("Twitter error", e);
+ throw new HttpBadRequestException();
}
}
}
--
cgit v1.2.3