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.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; // 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 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);
151 oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
155 private void createIntegrationOAuthClientService(WebexTeamsConfiguration config) {
156 String thingUID = this.getThing().getUID().getAsString();
157 logger.debug("Creating OAuth Client Service for {}", thingUID);
158 OAuthClientService service = oAuthFactory.createOAuthClientService(thingUID, OAUTH_TOKEN_URL, OAUTH_AUTH_URL,
159 config.clientId, config.clientSecret, OAUTH_SCOPE, false);
160 service.addAccessTokenRefreshListener(this);
161 this.authService = service;
162 this.configured = true;
165 private void createBotOAuthClientService(WebexTeamsConfiguration config) throws WebexTeamsException {
166 String thingUID = this.getThing().getUID().getAsString();
167 AccessTokenResponse response = new AccessTokenResponse();
168 response.setAccessToken(config.token);
169 response.setScope(OAUTH_SCOPE);
170 response.setTokenType("Bearer");
171 response.setExpiresIn(Long.MAX_VALUE); // Bot access tokens don't expire
172 logger.debug("Creating OAuth Client Service for {}", thingUID);
173 OAuthClientService service = oAuthFactory.createOAuthClientService(thingUID, OAUTH_TOKEN_URL,
174 OAUTH_AUTHORIZATION_URL, "not used", null, OAUTH_SCOPE, false);
176 service.importAccessTokenResponse(response);
177 } catch (OAuthException e) {
178 throw new WebexTeamsException("Failed to create oauth client with bot token", e);
180 this.authService = service;
181 this.configured = true;
184 boolean isConfigured() {
188 protected String authorize(String redirectUri, String reqCode) throws WebexTeamsException {
190 logger.debug("Make call to Webex to get access token.");
192 // Not doing anything with the token. It's used indirectly through authService.
193 OAuthClientService authService = this.authService;
194 if (authService != null) {
195 authService.getAccessTokenResponseByAuthorizationCode(reqCode, redirectUri);
199 final String user = getUser();
200 logger.info("Authorized for user: {}", user);
203 } catch (RuntimeException | OAuthException | IOException e) {
204 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
205 throw new WebexTeamsException("Failed to authorize", e);
206 } catch (final OAuthResponseException e) {
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
208 throw new WebexTeamsException("OAuth exception", e);
212 public boolean isAuthorized() {
213 final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
214 if (accessTokenResponse == null) {
218 if ("person".equals(this.accountType)) {
219 return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null
220 && accessTokenResponse.getRefreshToken() != null;
222 // bots don't need no refreshToken!
223 return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null;
227 private @Nullable AccessTokenResponse getAccessTokenResponse() {
229 OAuthClientService authService = this.authService;
230 return authService == null ? null : authService.getAccessTokenResponse();
231 } catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) {
232 logger.debug("Exception checking authorization: ", e);
237 public boolean equalsThingUID(String thingUID) {
238 return getThing().getUID().getAsString().equals(thingUID);
241 public String formatAuthorizationUrl(String redirectUri) {
243 if (this.configured) {
244 OAuthClientService authService = this.authService;
245 if (authService != null) {
246 return authService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString());
248 logger.warn("AuthService not properly initialized");
254 } catch (final OAuthException e) {
255 logger.warn("Error constructing AuthorizationUrl: ", e);
260 // mainly used to refresh the auth token when using OAuth
261 private boolean refresh() {
262 synchronized (refreshSynchronization) {
265 WebexTeamsApi client = this.client;
266 if (client == null) {
267 logger.warn("Client not properly initialized");
268 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
269 "Client not properly initialized");
272 person = client.getPerson();
273 String type = person.getType();
277 updateProperty(PROPERTY_WEBEX_TYPE, type);
278 this.accountType = type;
279 updateProperty(PROPERTY_WEBEX_NAME, person.getDisplayName());
281 // Only when the identity is a person:
282 if ("person".equalsIgnoreCase(person.getType())) {
283 String status = person.getStatus();
284 updateState(CHANNEL_STATUS, StringType.valueOf(status));
285 DateFormat df = new SimpleDateFormat(ISO8601_FORMAT);
286 String lastActivity = df.format(person.getLastActivity());
287 if (lastActivity != null) {
288 updateState(CHANNEL_LASTACTIVITY, new DateTimeType(lastActivity));
291 updateStatus(ThingStatus.ONLINE);
293 } catch (WebexTeamsException e) {
294 logger.warn("Failed to refresh: {}. Did you authorize?", e.getMessage());
295 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
301 private void startRefresh() {
302 synchronized (refreshSynchronization) {
306 refreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshPeriod,
314 * Cancels all running schedulers.
316 private synchronized void cancelSchedulers() {
317 Future<?> future = this.refreshFuture;
318 if (future != null) {
320 this.refreshFuture = null;
324 public String getUser() {
325 return thing.getProperties().getOrDefault(PROPERTY_WEBEX_NAME, "");
328 public ThingUID getUID() {
329 return thing.getUID();
332 public String getLabel() {
333 return Objects.requireNonNullElse(thing.getLabel(), "");
337 * Sends a message to the default room.
339 * @param msg markdown text string to be sent
341 * @return <code>true</code>, if sending the message has been successful and
342 * <code>false</code> in all other cases.
344 public boolean sendMessage(String msg) {
345 Message message = new Message();
346 message.setRoomId(config.roomId);
347 message.setMarkdown(msg);
348 logger.debug("Sending message to default room ({})", config.roomId);
349 return sendMessage(message);
353 * Sends a message with file attachment to the default room.
355 * @param msg markdown text string to be sent
356 * @param attach URL of the attachment
358 * @return <code>true</code>, if sending the message has been successful and
359 * <code>false</code> in all other cases.
361 public boolean sendMessage(String msg, String attach) {
362 Message message = new Message();
363 message.setRoomId(config.roomId);
364 message.setMarkdown(msg);
365 message.setFile(attach);
366 logger.debug("Sending message with attachment to default room ({})", config.roomId);
367 return sendMessage(message);
371 * Send a message to a specific room
373 * @param roomId roomId of the room to send to
374 * @param msg markdown text string to be sent
375 * @return <code>true</code>, if sending the message has been successful and
376 * <code>false</code> in all other cases.
378 public boolean sendRoomMessage(String roomId, String msg) {
379 Message message = new Message();
380 message.setRoomId(roomId);
381 message.setMarkdown(msg);
382 logger.debug("Sending message to room {}", roomId);
383 return sendMessage(message);
387 * Send a message to a specific room, with attachment
389 * @param roomId roomId of the room to send to
390 * @param msg markdown text string to be sent
391 * @param attach URL of the attachment
393 * @return <code>true</code>, if sending the message has been successful and
394 * <code>false</code> in all other cases.
396 public boolean sendRoomMessage(String roomId, String msg, String attach) {
397 Message message = new Message();
398 message.setRoomId(roomId);
399 message.setMarkdown(msg);
400 message.setFile(attach);
401 logger.debug("Sending message with attachment to room {}", roomId);
402 return sendMessage(message);
406 * Sends a message to a specific person, identified by email
408 * @param personEmail email address of the person to send to
409 * @param msg markdown text string to be sent
410 * @return <code>true</code>, if sending the message has been successful and
411 * <code>false</code> in all other cases.
413 public boolean sendPersonMessage(String personEmail, String msg) {
414 Message message = new Message();
415 message.setToPersonEmail(personEmail);
416 message.setMarkdown(msg);
417 logger.debug("Sending message to {}", personEmail);
418 return sendMessage(message);
422 * Sends a message to a specific person, identified by email, with attachment
424 * @param personEmail email address of the person to send to
425 * @param msg markdown text string to be sent
426 * @param attach URL of the attachment*
427 * @return <code>true</code>, if sending the message has been successful and
428 * <code>false</code> in all other cases.
430 public boolean sendPersonMessage(String personEmail, String msg, String attach) {
431 Message message = new Message();
432 message.setToPersonEmail(personEmail);
433 message.setMarkdown(msg);
434 message.setFile(attach);
435 logger.debug("Sending message to {}", personEmail);
436 return sendMessage(message);
440 * Sends a <code>Message</code>
442 * @param msg the <code>Message</code> to be sent
443 * @return <code>true</code>, if sending the message has been successful and
444 * <code>false</code> in all other cases.
446 private boolean sendMessage(Message msg) {
448 WebexTeamsApi client = this.client;
449 if (client != null) {
450 client.sendMessage(msg);
453 logger.warn("Client not properly initialized");
456 } catch (WebexTeamsException e) {
457 logger.warn("Failed to send message: {}", e.getMessage());
463 public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {