2 * Copyright (c) 2010-2022 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.webexteams.internal;
15 import static org.openhab.binding.webexteams.internal.WebexTeamsBindingConstants.*;
17 import java.io.IOException;
18 import java.text.DateFormat;
19 import java.text.SimpleDateFormat;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.Objects;
23 import java.util.concurrent.Future;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.openhab.binding.webexteams.internal.api.Message;
30 import org.openhab.binding.webexteams.internal.api.Person;
31 import org.openhab.binding.webexteams.internal.api.WebexTeamsApi;
32 import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
33 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
34 import org.openhab.core.auth.client.oauth2.OAuthClientService;
35 import org.openhab.core.auth.client.oauth2.OAuthException;
36 import org.openhab.core.auth.client.oauth2.OAuthFactory;
37 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
38 import org.openhab.core.library.types.DateTimeType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.ThingUID;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.thing.binding.ThingHandlerService;
47 import org.openhab.core.types.Command;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The {@link WebexTeamsHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Tom Deckers - Initial contribution
58 public class WebexTeamsHandler extends BaseThingHandler implements AccessTokenRefreshListener {
60 private final Logger logger = LoggerFactory.getLogger(WebexTeamsHandler.class);
62 // Object to synchronize refresh on
63 private final Object refreshSynchronization = new Object();
65 private @NonNullByDefault({}) WebexTeamsConfiguration config;
67 private final OAuthFactory oAuthFactory;
68 private final HttpClient httpClient;
69 private @Nullable WebexTeamsApi client;
71 private @Nullable OAuthClientService authService;
73 private boolean configured = false; // is the handler instance properly configured?
74 private volatile boolean active; // is the handler instance active?
75 String accountType = ""; // bot or person?
77 private @Nullable Future<?> refreshFuture;
79 public WebexTeamsHandler(Thing thing, OAuthFactory oAuthFactory, HttpClient httpClient) {
81 this.oAuthFactory = oAuthFactory;
82 this.httpClient = httpClient;
86 public void handleCommand(ChannelUID channelUID, Command command) {
87 // No commands supported on any channel
90 // creates list of available Actions
92 public Collection<Class<? extends ThingHandlerService>> getServices() {
93 return Collections.singletonList(WebexTeamsActions.class);
97 public void initialize() {
98 logger.debug("Initializing thing {}", this.getThing().getUID());
100 config = getConfigAs(WebexTeamsConfiguration.class);
102 final String token = config.token;
103 final String clientId = config.clientId;
104 final String clientSecret = config.clientSecret;
106 if (!token.isBlank()) { // For bots
107 logger.debug("I think I'm a bot.");
109 createBotOAuthClientService(config);
110 } catch (WebexTeamsException e) {
111 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/confErrorNotAuth");
114 } else if (!clientId.isBlank()) { // For integrations
115 logger.debug("I think I'm a person.");
116 if (clientSecret.isBlank()) {
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/confErrorNoSecret");
120 createIntegrationOAuthClientService(config);
121 } else { // If no bot or integration credentials, go offline
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/confErrorTokenOrId");
126 OAuthClientService localAuthService = this.authService;
127 if (localAuthService == null) {
128 logger.warn("authService not properly initialized");
129 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
130 "authService not properly initialized");
134 updateStatus(ThingStatus.UNKNOWN);
136 this.client = new WebexTeamsApi(localAuthService, httpClient);
138 // Start with update status by calling Webex. If no credentials available no polling should be started.
139 scheduler.execute(this::startRefresh);
143 public void dispose() {
144 logger.debug("Disposing thing {}", this.getThing().getUID());
146 OAuthClientService authService = this.authService;
147 if (authService != null) {
148 authService.removeAccessTokenRefreshListener(this);
150 oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
154 private void createIntegrationOAuthClientService(WebexTeamsConfiguration config) {
155 String thingUID = this.getThing().getUID().getAsString();
156 logger.debug("Creating OAuth Client Service for {}", thingUID);
157 OAuthClientService service = oAuthFactory.createOAuthClientService(thingUID, OAUTH_TOKEN_URL, OAUTH_AUTH_URL,
158 config.clientId, config.clientSecret, OAUTH_SCOPE, false);
159 service.addAccessTokenRefreshListener(this);
160 this.authService = service;
161 this.configured = true;
164 private void createBotOAuthClientService(WebexTeamsConfiguration config) throws WebexTeamsException {
165 String thingUID = this.getThing().getUID().getAsString();
166 AccessTokenResponse response = new AccessTokenResponse();
167 response.setAccessToken(config.token);
168 response.setScope(OAUTH_SCOPE);
169 response.setTokenType("Bearer");
170 response.setExpiresIn(Long.MAX_VALUE); // Bot access tokens don't expire
171 logger.debug("Creating OAuth Client Service for {}", thingUID);
172 OAuthClientService service = oAuthFactory.createOAuthClientService(thingUID, OAUTH_TOKEN_URL,
173 OAUTH_AUTHORIZATION_URL, "not used", null, OAUTH_SCOPE, false);
175 service.importAccessTokenResponse(response);
176 } catch (OAuthException e) {
177 throw new WebexTeamsException("Failed to create oauth client with bot token", e);
179 this.authService = service;
180 this.configured = true;
183 boolean isConfigured() {
187 protected String authorize(String redirectUri, String reqCode) throws WebexTeamsException {
189 logger.debug("Make call to Webex to get access token.");
191 // Not doing anything with the token. It's used indirectly through authService.
192 OAuthClientService authService = this.authService;
193 if (authService != null) {
194 authService.getAccessTokenResponseByAuthorizationCode(reqCode, redirectUri);
198 final String user = getUser();
199 logger.info("Authorized for user: {}", user);
202 } catch (RuntimeException | OAuthException | IOException e) {
203 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
204 throw new WebexTeamsException("Failed to authorize", e);
205 } catch (final OAuthResponseException e) {
206 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
207 throw new WebexTeamsException("OAuth exception", e);
211 public boolean isAuthorized() {
212 final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
214 if ("person".equals(this.accountType)) {
215 return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null
216 && accessTokenResponse.getRefreshToken() != null;
218 // bots don't need no refreshToken!
219 return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null;
223 private @Nullable AccessTokenResponse getAccessTokenResponse() {
225 OAuthClientService authService = this.authService;
226 return authService == null ? null : authService.getAccessTokenResponse();
227 } catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) {
228 logger.debug("Exception checking authorization: ", e);
233 public boolean equalsThingUID(String thingUID) {
234 return getThing().getUID().getAsString().equals(thingUID);
237 public String formatAuthorizationUrl(String redirectUri) {
239 if (this.configured) {
240 OAuthClientService authService = this.authService;
241 if (authService != null) {
242 return authService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString());
244 logger.warn("AuthService not properly initialized");
250 } catch (final OAuthException e) {
251 logger.warn("Error constructing AuthorizationUrl: ", e);
256 // mainly used to refresh the auth token when using OAuth
257 private boolean refresh() {
258 synchronized (refreshSynchronization) {
261 WebexTeamsApi client = this.client;
262 if (client == null) {
263 logger.warn("Client not properly initialized");
264 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
265 "Client not properly initialized");
268 person = client.getPerson();
269 String type = person.getType();
273 updateProperty(PROPERTY_WEBEX_TYPE, type);
274 this.accountType = type;
275 updateProperty(PROPERTY_WEBEX_NAME, person.getDisplayName());
277 // Only when the identity is a person:
278 if ("person".equalsIgnoreCase(person.getType())) {
279 String status = person.getStatus();
280 updateState(CHANNEL_STATUS, StringType.valueOf(status));
281 DateFormat df = new SimpleDateFormat(ISO8601_FORMAT);
282 String lastActivity = df.format(person.getLastActivity());
283 if (lastActivity != null) {
284 updateState(CHANNEL_LASTACTIVITY, new DateTimeType(lastActivity));
287 updateStatus(ThingStatus.ONLINE);
289 } catch (WebexTeamsException e) {
290 logger.warn("Failed to refresh: {}", e.getMessage());
291 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
297 private void startRefresh() {
298 synchronized (refreshSynchronization) {
302 refreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshPeriod,
310 * Cancels all running schedulers.
312 private synchronized void cancelSchedulers() {
313 Future<?> future = this.refreshFuture;
314 if (future != null) {
316 this.refreshFuture = null;
320 public String getUser() {
321 return thing.getProperties().getOrDefault(PROPERTY_WEBEX_NAME, "");
324 public ThingUID getUID() {
325 return thing.getUID();
328 public String getLabel() {
329 return Objects.requireNonNullElse(thing.getLabel(), "");
333 * Sends a message to the default room.
335 * @param msg markdown text string to be sent
337 * @return <code>true</code>, if sending the message has been successful and
338 * <code>false</code> in all other cases.
340 public boolean sendMessage(String msg) {
341 Message message = new Message();
342 message.setRoomId(config.roomId);
343 message.setMarkdown(msg);
344 logger.debug("Sending message to default room ({})", config.roomId);
345 return sendMessage(message);
349 * Sends a message with file attachment to the default room.
351 * @param msg markdown text string to be sent
352 * @param attach URL of the attachment
354 * @return <code>true</code>, if sending the message has been successful and
355 * <code>false</code> in all other cases.
357 public boolean sendMessage(String msg, String attach) {
358 Message message = new Message();
359 message.setRoomId(config.roomId);
360 message.setMarkdown(msg);
361 message.setFile(attach);
362 logger.debug("Sending message with attachment to default room ({})", config.roomId);
363 return sendMessage(message);
367 * Send a message to a specific room
369 * @param roomId roomId of the room to send to
370 * @param msg markdown text string to be sent
371 * @return <code>true</code>, if sending the message has been successful and
372 * <code>false</code> in all other cases.
374 public boolean sendRoomMessage(String roomId, String msg) {
375 Message message = new Message();
376 message.setRoomId(roomId);
377 message.setMarkdown(msg);
378 logger.debug("Sending message to room {}", roomId);
379 return sendMessage(message);
383 * Send a message to a specific room, with attachment
385 * @param roomId roomId of the room to send to
386 * @param msg markdown text string to be sent
387 * @param attach URL of the attachment
389 * @return <code>true</code>, if sending the message has been successful and
390 * <code>false</code> in all other cases.
392 public boolean sendRoomMessage(String roomId, String msg, String attach) {
393 Message message = new Message();
394 message.setRoomId(roomId);
395 message.setMarkdown(msg);
396 message.setFile(attach);
397 logger.debug("Sending message with attachment to room {}", roomId);
398 return sendMessage(message);
402 * Sends a message to a specific person, identified by email
404 * @param personEmail email address of the person to send to
405 * @param msg markdown text string to be sent
406 * @return <code>true</code>, if sending the message has been successful and
407 * <code>false</code> in all other cases.
409 public boolean sendPersonMessage(String personEmail, String msg) {
410 Message message = new Message();
411 message.setToPersonEmail(personEmail);
412 message.setMarkdown(msg);
413 logger.debug("Sending message to {}", personEmail);
414 return sendMessage(message);
418 * Sends a message to a specific person, identified by email, with attachment
420 * @param personEmail email address of the person to send to
421 * @param msg markdown text string to be sent
422 * @param attach URL of the attachment*
423 * @return <code>true</code>, if sending the message has been successful and
424 * <code>false</code> in all other cases.
426 public boolean sendPersonMessage(String personEmail, String msg, String attach) {
427 Message message = new Message();
428 message.setToPersonEmail(personEmail);
429 message.setMarkdown(msg);
430 message.setFile(attach);
431 logger.debug("Sending message to {}", personEmail);
432 return sendMessage(message);
436 * Sends a <code>Message</code>
438 * @param msg the <code>Message</code> to be sent
439 * @return <code>true</code>, if sending the message has been successful and
440 * <code>false</code> in all other cases.
442 private boolean sendMessage(Message msg) {
444 WebexTeamsApi client = this.client;
445 if (client != null) {
446 client.sendMessage(msg);
449 logger.warn("Client not properly initialized");
452 } catch (WebexTeamsException e) {
453 logger.warn("Failed to send message: {}", e.getMessage());
459 public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {