]> git.basschouten.com Git - openhab-addons.git/blob
5c8b884f468628b34bf490cf7601693b2c53b06e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.webexteams.internal;
14
15 import static org.openhab.binding.webexteams.internal.WebexTeamsBindingConstants.*;
16
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;
25
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;
50
51 /**
52  * The {@link WebexTeamsHandler} is responsible for handling commands, which are
53  * sent to one of the channels.
54  *
55  * @author Tom Deckers - Initial contribution
56  */
57 @NonNullByDefault
58 public class WebexTeamsHandler extends BaseThingHandler implements AccessTokenRefreshListener {
59
60     private final Logger logger = LoggerFactory.getLogger(WebexTeamsHandler.class);
61
62     // Object to synchronize refresh on
63     private final Object refreshSynchronization = new Object();
64
65     private @NonNullByDefault({}) WebexTeamsConfiguration config;
66
67     private final OAuthFactory oAuthFactory;
68     private final HttpClient httpClient;
69     private @Nullable WebexTeamsApi client;
70
71     private @Nullable OAuthClientService authService;
72
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?
76
77     private @Nullable Future<?> refreshFuture;
78
79     public WebexTeamsHandler(Thing thing, OAuthFactory oAuthFactory, HttpClient httpClient) {
80         super(thing);
81         this.oAuthFactory = oAuthFactory;
82         this.httpClient = httpClient;
83     }
84
85     @Override
86     public void handleCommand(ChannelUID channelUID, Command command) {
87         // No commands supported on any channel
88     }
89
90     // creates list of available Actions
91     @Override
92     public Collection<Class<? extends ThingHandlerService>> getServices() {
93         return List.of(WebexTeamsActions.class);
94     }
95
96     @Override
97     public void initialize() {
98         logger.debug("Initializing thing {}", this.getThing().getUID());
99         active = true;
100         this.configured = false;
101         config = getConfigAs(WebexTeamsConfiguration.class);
102
103         final String token = config.token;
104         final String clientId = config.clientId;
105         final String clientSecret = config.clientSecret;
106
107         if (!token.isBlank()) { // For bots
108             logger.debug("I think I'm a bot.");
109             try {
110                 createBotOAuthClientService(config);
111             } catch (WebexTeamsException e) {
112                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/confErrorNotAuth");
113                 return;
114             }
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");
119                 return;
120             }
121             createIntegrationOAuthClientService(config);
122         } else { // If no bot or integration credentials, go offline
123             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/confErrorTokenOrId");
124             return;
125         }
126
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");
132             return;
133         }
134
135         updateStatus(ThingStatus.UNKNOWN);
136
137         this.client = new WebexTeamsApi(localAuthService, httpClient);
138
139         // Start with update status by calling Webex. If no credentials available no polling should be started.
140         scheduler.execute(this::startRefresh);
141     }
142
143     @Override
144     public void dispose() {
145         logger.debug("Disposing thing {}", this.getThing().getUID());
146         active = false;
147         OAuthClientService authService = this.authService;
148         if (authService != null) {
149             authService.removeAccessTokenRefreshListener(this);
150             oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
151         }
152         cancelSchedulers();
153     }
154
155     @Override
156     public void handleRemoval() {
157         oAuthFactory.deleteServiceAndAccessToken(thing.getUID().getAsString());
158         super.handleRemoval();
159     }
160
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;
169     }
170
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);
181         try {
182             service.importAccessTokenResponse(response);
183         } catch (OAuthException e) {
184             throw new WebexTeamsException("Failed to create oauth client with bot token", e);
185         }
186         this.authService = service;
187         this.configured = true;
188     }
189
190     boolean isConfigured() {
191         return configured;
192     }
193
194     protected String authorize(String redirectUri, String reqCode) throws WebexTeamsException {
195         try {
196             logger.debug("Make call to Webex to get access token.");
197
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);
202             }
203
204             startRefresh();
205             final String user = getUser();
206             logger.info("Authorized for user: {}", user);
207
208             return 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);
215         }
216     }
217
218     public boolean isAuthorized() {
219         final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
220         if (accessTokenResponse == null) {
221             return false;
222         }
223
224         if ("person".equals(this.accountType)) {
225             return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null
226                     && accessTokenResponse.getRefreshToken() != null;
227         } else {
228             // bots don't need no refreshToken!
229             return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null;
230         }
231     }
232
233     private @Nullable AccessTokenResponse getAccessTokenResponse() {
234         try {
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);
239             return null;
240         }
241     }
242
243     public boolean equalsThingUID(String thingUID) {
244         return getThing().getUID().getAsString().equals(thingUID);
245     }
246
247     public String formatAuthorizationUrl(String redirectUri) {
248         try {
249             if (this.configured) {
250                 OAuthClientService authService = this.authService;
251                 if (authService != null) {
252                     return authService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString());
253                 } else {
254                     logger.warn("AuthService not properly initialized");
255                     return "";
256                 }
257             } else {
258                 return "";
259             }
260         } catch (final OAuthException e) {
261             logger.warn("Error constructing AuthorizationUrl: ", e);
262             return "";
263         }
264     }
265
266     // mainly used to refresh the auth token when using OAuth
267     private boolean refresh() {
268         synchronized (refreshSynchronization) {
269             Person person;
270             try {
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");
276                     return false;
277                 }
278                 person = client.getPerson();
279                 String type = person.getType();
280                 if (type == null) {
281                     type = "?";
282                 }
283                 updateProperty(PROPERTY_WEBEX_TYPE, type);
284                 this.accountType = type;
285                 updateProperty(PROPERTY_WEBEX_NAME, person.getDisplayName());
286
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));
295                     }
296                 }
297                 updateStatus(ThingStatus.ONLINE);
298                 return true;
299             } catch (WebexTeamsException e) {
300                 logger.warn("Failed to refresh: {}.  Did you authorize?", e.getMessage());
301                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
302             }
303             return false;
304         }
305     }
306
307     private void startRefresh() {
308         synchronized (refreshSynchronization) {
309             if (refresh()) {
310                 cancelSchedulers();
311                 if (active) {
312                     refreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshPeriod,
313                             TimeUnit.SECONDS);
314                 }
315             }
316         }
317     }
318
319     /**
320      * Cancels all running schedulers.
321      */
322     private synchronized void cancelSchedulers() {
323         Future<?> future = this.refreshFuture;
324         if (future != null) {
325             future.cancel(true);
326             this.refreshFuture = null;
327         }
328     }
329
330     public String getUser() {
331         return thing.getProperties().getOrDefault(PROPERTY_WEBEX_NAME, "");
332     }
333
334     public ThingUID getUID() {
335         return thing.getUID();
336     }
337
338     public String getLabel() {
339         return Objects.requireNonNullElse(thing.getLabel(), "");
340     }
341
342     /**
343      * Sends a message to the default room.
344      *
345      * @param msg markdown text string to be sent
346      *
347      * @return <code>true</code>, if sending the message has been successful and
348      *         <code>false</code> in all other cases.
349      */
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);
356     }
357
358     /**
359      * Sends a message with file attachment to the default room.
360      *
361      * @param msg markdown text string to be sent
362      * @param attach URL of the attachment
363      *
364      * @return <code>true</code>, if sending the message has been successful and
365      *         <code>false</code> in all other cases.
366      */
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);
374     }
375
376     /**
377      * Send a message to a specific room
378      *
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.
383      */
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);
390     }
391
392     /**
393      * Send a message to a specific room, with attachment
394      *
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
398      *
399      * @return <code>true</code>, if sending the message has been successful and
400      *         <code>false</code> in all other cases.
401      */
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);
409     }
410
411     /**
412      * Sends a message to a specific person, identified by email
413      *
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.
418      */
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);
425     }
426
427     /**
428      * Sends a message to a specific person, identified by email, with attachment
429      *
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.
435      */
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);
443     }
444
445     /**
446      * Sends a <code>Message</code>
447      *
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.
451      */
452     private boolean sendMessage(Message msg) {
453         try {
454             WebexTeamsApi client = this.client;
455             if (client != null) {
456                 client.sendMessage(msg);
457                 return true;
458             } else {
459                 logger.warn("Client not properly initialized");
460                 return false;
461             }
462         } catch (WebexTeamsException e) {
463             logger.warn("Failed to send message: {}", e.getMessage());
464         }
465         return false;
466     }
467
468     @Override
469     public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
470     }
471 }