2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.twitter.internal;
15 import static org.openhab.binding.twitter.internal.TwitterBindingConstants.CHANNEL_LASTTWEET;
18 import java.io.FileNotFoundException;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.nio.file.Files;
22 import java.nio.file.Path;
23 import java.util.Collection;
24 import java.util.List;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.twitter.internal.action.TwitterActions;
31 import org.openhab.binding.twitter.internal.config.TwitterConfig;
32 import org.openhab.core.io.net.http.HttpUtil;
33 import org.openhab.core.library.types.RawType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.thing.binding.ThingHandlerService;
40 import org.openhab.core.types.Command;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
44 import twitter4j.DirectMessage;
45 import twitter4j.ResponseList;
46 import twitter4j.Status;
47 import twitter4j.StatusUpdate;
48 import twitter4j.Twitter;
49 import twitter4j.TwitterException;
50 import twitter4j.TwitterFactory;
51 import twitter4j.auth.AccessToken;
54 * The {@link TwitterHandler} is responsible for handling commands, which are
55 * sent to one of the channels.
57 * @author Scott Hanson - Initial contribution
61 public class TwitterHandler extends BaseThingHandler {
63 private final Logger logger = LoggerFactory.getLogger(TwitterHandler.class);
65 private TwitterConfig config = new TwitterConfig();
67 private @Nullable ScheduledFuture<?> refreshTask;
69 private static final int CHARACTER_LIMIT = 280;
71 private static @Nullable Twitter client = null;
72 boolean isProperlyConfigured = false;
74 public TwitterHandler(Thing thing) {
79 public void handleCommand(ChannelUID channelUID, Command command) {
82 // creates list of available Actions
84 public Collection<Class<? extends ThingHandlerService>> getServices() {
85 return List.of(TwitterActions.class);
89 public void initialize() {
90 config = getConfigAs(TwitterConfig.class);
92 // create a New Twitter Client
93 Twitter localClient = createClient();
95 refresh();// Get latest status
96 isProperlyConfigured = true;
97 refreshTask = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refresh, TimeUnit.MINUTES);
98 updateStatus(ThingStatus.ONLINE);
102 public void dispose() {
103 ScheduledFuture<?> localRefreshTask = refreshTask;
104 if (localRefreshTask != null) {
105 localRefreshTask.cancel(true);
110 * Internal method for Getting Twitter Status
113 private void refresh() {
115 if (!checkPrerequisites()) {
118 Twitter localClient = client;
119 if (localClient != null) {
120 ResponseList<Status> statuses = localClient.getUserTimeline();
121 if (!statuses.isEmpty()) {
122 updateState(CHANNEL_LASTTWEET, StringType.valueOf(statuses.get(0).getText()));
124 logger.debug("No Statuses Found");
127 } catch (TwitterException e) {
128 logger.debug("Error when trying to refresh Twitter Account: {}", e.getMessage());
133 * Internal method for sending a tweet, with or without image
136 * text string to be sent as a Tweet
137 * @param fileToAttach
138 * the file to attach. May be null if no attached file.
140 * @return <code>true</code>, if sending the tweet has been successful and
141 * <code>false</code> in all other cases.
143 private boolean sendTweet(final String tweetTxt, final @Nullable File fileToAttach) {
144 if (!checkPrerequisites()) {
147 // abbreviate the Tweet to meet the 280 character limit ...
148 String abbreviatedTweetTxt = abbreviateString(tweetTxt, CHARACTER_LIMIT);
150 Twitter localClient = client;
151 if (localClient != null) {
153 StatusUpdate status = new StatusUpdate(abbreviatedTweetTxt);
154 if (fileToAttach != null && fileToAttach.isFile()) {
155 status.setMedia(fileToAttach);
157 Status updatedStatus = localClient.updateStatus(status);
158 logger.debug("Successfully sent Tweet '{}'", updatedStatus.getText());
159 updateState(CHANNEL_LASTTWEET, StringType.valueOf(updatedStatus.getText()));
162 } catch (TwitterException e) {
163 logger.warn("Failed to send Tweet '{}' because of : {}", abbreviatedTweetTxt, e.getLocalizedMessage());
169 * Sends a standard Tweet.
172 * text string to be sent as a Tweet
174 * @return <code>true</code>, if sending the tweet has been successful and
175 * <code>false</code> in all other cases.
177 public boolean sendTweet(String tweetTxt) {
178 if (!checkPrerequisites()) {
181 return sendTweet(tweetTxt, (File) null);
185 * Sends a Tweet with an image
188 * text string to be sent as a Tweet
189 * @param tweetPicture
190 * the path of the picture that needs to be attached (either an url,
191 * either a path pointing to a local file)
193 * @return <code>true</code>, if sending the tweet has been successful and
194 * <code>false</code> in all other cases.
196 public boolean sendTweet(String tweetTxt, String tweetPicture) {
197 if (!checkPrerequisites()) {
201 // prepare the image attachment
202 File fileToAttach = null;
203 boolean deleteTemporaryFile = false;
204 if (tweetPicture.startsWith("http://") || tweetPicture.startsWith("https://")) {
206 // we have a remote url and need to download the remote file to a temporary location
207 Path tDir = Files.createTempDirectory("TempDirectory");
208 String path = tDir + File.separator + "openhab-twitter-remote_attached_file" + "."
209 + getExtension(tweetPicture);
211 // URL url = new URL(tweetPicture);
212 fileToAttach = new File(path);
213 deleteTemporaryFile = true;
215 RawType rawPicture = HttpUtil.downloadImage(tweetPicture);
216 if (rawPicture != null) {
217 try (FileOutputStream fos = new FileOutputStream(path)) {
218 fos.write(rawPicture.getBytes(), 0, rawPicture.getBytes().length);
219 } catch (FileNotFoundException ex) {
220 logger.debug("Could not create {} in temp dir. {}", path, ex.getMessage());
221 } catch (IOException ex) {
222 logger.debug("Could not write {} to temp dir. {}", path, ex.getMessage());
225 logger.debug("Could not download tweet file from {}", tweetPicture);
227 } catch (IOException ex) {
228 logger.debug("Could not write {} to temp dir. {}", tweetPicture, ex.getMessage());
231 // we have a local file and can just use it directly
232 fileToAttach = new File(tweetPicture);
235 if (fileToAttach != null && fileToAttach.isFile()) {
236 logger.debug("Image '{}' correctly found, will be included in tweet", tweetPicture);
238 logger.warn("Image '{}' not found, will only tweet text", tweetPicture);
242 boolean result = sendTweet(tweetTxt, fileToAttach);
243 // delete temp file (if needed)
244 if (deleteTemporaryFile) {
245 if (fileToAttach != null) {
247 fileToAttach.delete();
248 } catch (final Exception ignored) {
257 * Sends a DirectMessage
260 * recipient ID of the twitter user
262 * text string to be sent as a Direct Message
264 * @return <code>true</code>, if sending the direct message has been successful and
265 * <code>false</code> in all other cases.
267 public boolean sendDirectMessage(String recipientId, String messageTxt) {
268 if (!checkPrerequisites()) {
273 Twitter localClient = client;
274 if (localClient != null) {
275 // abbreviate the Tweet to meet the allowed character limit ...
276 String abbreviatedMessageTxt = abbreviateString(messageTxt, CHARACTER_LIMIT);
277 // send the direct message
278 DirectMessage message = localClient.sendDirectMessage(recipientId, abbreviatedMessageTxt);
279 logger.debug("Successfully sent direct message '{}' to @'{}'", message.getText(),
280 message.getRecipientId());
283 } catch (TwitterException e) {
284 logger.warn("Failed to send Direct Message '{}' because of :'{}'", messageTxt, e.getLocalizedMessage());
290 * check if twitter account was created with prerequisites
292 * @return <code>true</code>, if twitter account was initialized
293 * <code>false</code> in all other cases.
295 private boolean checkPrerequisites() {
296 if (client == null) {
297 logger.debug("Twitter client is not yet configured > execution aborted!");
300 if (!isProperlyConfigured) {
301 logger.debug("Twitter client is not yet configured > execution aborted!");
308 * Creates and returns a Twitter4J Twitter client.
310 * @return a new instance of a Twitter4J Twitter client.
312 private twitter4j.Twitter createClient() {
313 twitter4j.Twitter client = TwitterFactory.getSingleton();
314 client.setOAuthConsumer(config.consumerKey, config.consumerSecret);
315 client.setOAuthAccessToken(new AccessToken(config.accessToken, config.accessTokenSecret));
319 public static String abbreviateString(String input, int maxLength) {
320 if (input.length() <= maxLength) {
323 return input.substring(0, maxLength);
327 public static String getExtension(String filename) {
328 if (filename.contains(".")) {
329 return filename.substring(filename.lastIndexOf(".") + 1);