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.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.List;
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; // 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 List.of(WebexTeamsActions.class);
97 public void initialize() {
98 logger.debug("Initializing thing {}", this.getThing().getUID());
100 this.configured = false;
101 config = getConfigAs(WebexTeamsConfiguration.class);
103 final String token = config.token;
104 final String clientId = config.clientId;
105 final String clientSecret = config.clientSecret;
107 if (!token.isBlank()) { // For bots
108 logger.debug("I think I'm a bot.");
110 createBotOAuthClientService(config);
111 } catch (WebexTeamsException e) {
112 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/confErrorNotAuth");
115 } else if (!clientId.isBlank()) { // For integrations
116 logger.debug("I think I'm a person.");
117 if (clientSecret.isBlank()) {
118 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/confErrorNoSecret");
121 createIntegrationOAuthClientService(config);
122 } else { // If no bot or integration credentials, go offline
123 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/confErrorTokenOrId");
127 OAuthClientService localAuthService = this.authService;
128 if (localAuthService == null) {
129 logger.warn("authService not properly initialized");
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
131 "authService not properly initialized");
135 updateStatus(ThingStatus.UNKNOWN);
137 this.client = new WebexTeamsApi(localAuthService, httpClient);
139 // Start with update status by calling Webex. If no credentials available no polling should be started.
140 scheduler.execute(this::startRefresh);
144 public void dispose() {
145 logger.debug("Disposing thing {}", this.getThing().getUID());
147 OAuthClientService authService = this.authService;
148 if (authService != null) {
149 authService.removeAccessTokenRefreshListener(this);
150 oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
156 public void handleRemoval() {
157 oAuthFactory.deleteServiceAndAccessToken(thing.getUID().getAsString());
158 super.handleRemoval();
161 private void createIntegrationOAuthClientService(WebexTeamsConfiguration config) {
162 String thingUID = this.getThing().getUID().getAsString();
163 logger.debug("Creating OAuth Client Service for {}", thingUID);
164 OAuthClientService service = oAuthFactory.createOAuthClientService(thingUID, OAUTH_TOKEN_URL, OAUTH_AUTH_URL,
165 config.clientId, config.clientSecret, OAUTH_SCOPE, false);
166 service.addAccessTokenRefreshListener(this);
167 this.authService = service;
168 this.configured = true;
171 private void createBotOAuthClientService(WebexTeamsConfiguration config) throws WebexTeamsException {
172 String thingUID = this.getThing().getUID().getAsString();
173 AccessTokenResponse response = new AccessTokenResponse();
174 response.setAccessToken(config.token);
175 response.setScope(OAUTH_SCOPE);
176 response.setTokenType("Bearer");
177 response.setExpiresIn(Long.MAX_VALUE); // Bot access tokens don't expire
178 logger.debug("Creating OAuth Client Service for {}", thingUID);
179 OAuthClientService service = oAuthFactory.createOAuthClientService(thingUID, OAUTH_TOKEN_URL,
180 OAUTH_AUTHORIZATION_URL, "not used", null, OAUTH_SCOPE, false);
182 service.importAccessTokenResponse(response);
183 } catch (OAuthException e) {
184 throw new WebexTeamsException("Failed to create oauth client with bot token", e);
186 this.authService = service;
187 this.configured = true;
190 boolean isConfigured() {
194 protected String authorize(String redirectUri, String reqCode) throws WebexTeamsException {
196 logger.debug("Make call to Webex to get access token.");
198 // Not doing anything with the token. It's used indirectly through authService.
199 OAuthClientService authService = this.authService;
200 if (authService != null) {
201 authService.getAccessTokenResponseByAuthorizationCode(reqCode, redirectUri);
205 final String user = getUser();
206 logger.info("Authorized for user: {}", user);
209 } catch (RuntimeException | OAuthException | IOException e) {
210 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
211 throw new WebexTeamsException("Failed to authorize", e);
212 } catch (final OAuthResponseException e) {
213 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
214 throw new WebexTeamsException("OAuth exception", e);
218 public boolean isAuthorized() {
219 final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
220 if (accessTokenResponse == null) {
224 if ("person".equals(this.accountType)) {
225 return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null
226 && accessTokenResponse.getRefreshToken() != null;
228 // bots don't need no refreshToken!
229 return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null;
233 private @Nullable AccessTokenResponse getAccessTokenResponse() {
235 OAuthClientService authService = this.authService;
236 return authService == null ? null : authService.getAccessTokenResponse();
237 } catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) {
238 logger.debug("Exception checking authorization: ", e);
243 public boolean equalsThingUID(String thingUID) {
244 return getThing().getUID().getAsString().equals(thingUID);
247 public String formatAuthorizationUrl(String redirectUri) {
249 if (this.configured) {
250 OAuthClientService authService = this.authService;
251 if (authService != null) {
252 return authService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString());
254 logger.warn("AuthService not properly initialized");
260 } catch (final OAuthException e) {
261 logger.warn("Error constructing AuthorizationUrl: ", e);
266 // mainly used to refresh the auth token when using OAuth
267 private boolean refresh() {
268 synchronized (refreshSynchronization) {
271 WebexTeamsApi client = this.client;
272 if (client == null) {
273 logger.warn("Client not properly initialized");
274 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
275 "Client not properly initialized");
278 person = client.getPerson();
279 String type = person.getType();
283 updateProperty(PROPERTY_WEBEX_TYPE, type);
284 this.accountType = type;
285 updateProperty(PROPERTY_WEBEX_NAME, person.getDisplayName());
287 // Only when the identity is a person:
288 if ("person".equalsIgnoreCase(person.getType())) {
289 String status = person.getStatus();
290 updateState(CHANNEL_STATUS, StringType.valueOf(status));
291 DateFormat df = new SimpleDateFormat(ISO8601_FORMAT);
292 String lastActivity = df.format(person.getLastActivity());
293 if (lastActivity != null) {
294 updateState(CHANNEL_LASTACTIVITY, new DateTimeType(lastActivity));
297 updateStatus(ThingStatus.ONLINE);
299 } catch (WebexTeamsException e) {
300 logger.warn("Failed to refresh: {}. Did you authorize?", e.getMessage());
301 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
307 private void startRefresh() {
308 synchronized (refreshSynchronization) {
312 refreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshPeriod,
320 * Cancels all running schedulers.
322 private synchronized void cancelSchedulers() {
323 Future<?> future = this.refreshFuture;
324 if (future != null) {
326 this.refreshFuture = null;
330 public String getUser() {
331 return thing.getProperties().getOrDefault(PROPERTY_WEBEX_NAME, "");
334 public ThingUID getUID() {
335 return thing.getUID();
338 public String getLabel() {
339 return Objects.requireNonNullElse(thing.getLabel(), "");
343 * Sends a message to the default room.
345 * @param msg markdown text string to be sent
347 * @return <code>true</code>, if sending the message has been successful and
348 * <code>false</code> in all other cases.
350 public boolean sendMessage(String msg) {
351 Message message = new Message();
352 message.setRoomId(config.roomId);
353 message.setMarkdown(msg);
354 logger.debug("Sending message to default room ({})", config.roomId);
355 return sendMessage(message);
359 * Sends a message with file attachment to the default room.
361 * @param msg markdown text string to be sent
362 * @param attach URL of the attachment
364 * @return <code>true</code>, if sending the message has been successful and
365 * <code>false</code> in all other cases.
367 public boolean sendMessage(String msg, String attach) {
368 Message message = new Message();
369 message.setRoomId(config.roomId);
370 message.setMarkdown(msg);
371 message.setFile(attach);
372 logger.debug("Sending message with attachment to default room ({})", config.roomId);
373 return sendMessage(message);
377 * Send a message to a specific room
379 * @param roomId roomId of the room to send to
380 * @param msg markdown text string to be sent
381 * @return <code>true</code>, if sending the message has been successful and
382 * <code>false</code> in all other cases.
384 public boolean sendRoomMessage(String roomId, String msg) {
385 Message message = new Message();
386 message.setRoomId(roomId);
387 message.setMarkdown(msg);
388 logger.debug("Sending message to room {}", roomId);
389 return sendMessage(message);
393 * Send a message to a specific room, with attachment
395 * @param roomId roomId of the room to send to
396 * @param msg markdown text string to be sent
397 * @param attach URL of the attachment
399 * @return <code>true</code>, if sending the message has been successful and
400 * <code>false</code> in all other cases.
402 public boolean sendRoomMessage(String roomId, String msg, String attach) {
403 Message message = new Message();
404 message.setRoomId(roomId);
405 message.setMarkdown(msg);
406 message.setFile(attach);
407 logger.debug("Sending message with attachment to room {}", roomId);
408 return sendMessage(message);
412 * Sends a message to a specific person, identified by email
414 * @param personEmail email address of the person to send to
415 * @param msg markdown text string to be sent
416 * @return <code>true</code>, if sending the message has been successful and
417 * <code>false</code> in all other cases.
419 public boolean sendPersonMessage(String personEmail, String msg) {
420 Message message = new Message();
421 message.setToPersonEmail(personEmail);
422 message.setMarkdown(msg);
423 logger.debug("Sending message to {}", personEmail);
424 return sendMessage(message);
428 * Sends a message to a specific person, identified by email, with attachment
430 * @param personEmail email address of the person to send to
431 * @param msg markdown text string to be sent
432 * @param attach URL of the attachment*
433 * @return <code>true</code>, if sending the message has been successful and
434 * <code>false</code> in all other cases.
436 public boolean sendPersonMessage(String personEmail, String msg, String attach) {
437 Message message = new Message();
438 message.setToPersonEmail(personEmail);
439 message.setMarkdown(msg);
440 message.setFile(attach);
441 logger.debug("Sending message to {}", personEmail);
442 return sendMessage(message);
446 * Sends a <code>Message</code>
448 * @param msg the <code>Message</code> to be sent
449 * @return <code>true</code>, if sending the message has been successful and
450 * <code>false</code> in all other cases.
452 private boolean sendMessage(Message msg) {
454 WebexTeamsApi client = this.client;
455 if (client != null) {
456 client.sendMessage(msg);
459 logger.warn("Client not properly initialized");
462 } catch (WebexTeamsException e) {
463 logger.warn("Failed to send message: {}", e.getMessage());
469 public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {