]> git.basschouten.com Git - openhab-addons.git/blob
d5922259b98bb8095bcf75a4afc06a96bfca9da8
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.Collections;
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 = false; // 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 Collections.singletonList(WebexTeamsActions.class);
94     }
95
96     @Override
97     public void initialize() {
98         logger.debug("Initializing thing {}", this.getThing().getUID());
99         active = true;
100         config = getConfigAs(WebexTeamsConfiguration.class);
101
102         final String token = config.token;
103         final String clientId = config.clientId;
104         final String clientSecret = config.clientSecret;
105
106         if (!token.isBlank()) { // For bots
107             logger.debug("I think I'm a bot.");
108             try {
109                 createBotOAuthClientService(config);
110             } catch (WebexTeamsException e) {
111                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/confErrorNotAuth");
112                 return;
113             }
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");
118                 return;
119             }
120             createIntegrationOAuthClientService(config);
121         } else { // If no bot or integration credentials, go offline
122             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/confErrorTokenOrId");
123             return;
124         }
125
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");
131             return;
132         }
133
134         updateStatus(ThingStatus.UNKNOWN);
135
136         this.client = new WebexTeamsApi(localAuthService, httpClient);
137
138         // Start with update status by calling Webex. If no credentials available no polling should be started.
139         scheduler.execute(this::startRefresh);
140     }
141
142     @Override
143     public void dispose() {
144         logger.debug("Disposing thing {}", this.getThing().getUID());
145         active = false;
146         OAuthClientService authService = this.authService;
147         if (authService != null) {
148             authService.removeAccessTokenRefreshListener(this);
149         }
150         oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
151         cancelSchedulers();
152     }
153
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;
162     }
163
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);
174         try {
175             service.importAccessTokenResponse(response);
176         } catch (OAuthException e) {
177             throw new WebexTeamsException("Failed to create oauth client with bot token", e);
178         }
179         this.authService = service;
180         this.configured = true;
181     }
182
183     boolean isConfigured() {
184         return configured;
185     }
186
187     protected String authorize(String redirectUri, String reqCode) throws WebexTeamsException {
188         try {
189             logger.debug("Make call to Webex to get access token.");
190
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);
195             }
196
197             startRefresh();
198             final String user = getUser();
199             logger.info("Authorized for user: {}", user);
200
201             return 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);
208         }
209     }
210
211     public boolean isAuthorized() {
212         final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
213
214         if ("person".equals(this.accountType)) {
215             return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null
216                     && accessTokenResponse.getRefreshToken() != null;
217         } else {
218             // bots don't need no refreshToken!
219             return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null;
220         }
221     }
222
223     private @Nullable AccessTokenResponse getAccessTokenResponse() {
224         try {
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);
229             return null;
230         }
231     }
232
233     public boolean equalsThingUID(String thingUID) {
234         return getThing().getUID().getAsString().equals(thingUID);
235     }
236
237     public String formatAuthorizationUrl(String redirectUri) {
238         try {
239             if (this.configured) {
240                 OAuthClientService authService = this.authService;
241                 if (authService != null) {
242                     return authService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString());
243                 } else {
244                     logger.warn("AuthService not properly initialized");
245                     return "";
246                 }
247             } else {
248                 return "";
249             }
250         } catch (final OAuthException e) {
251             logger.warn("Error constructing AuthorizationUrl: ", e);
252             return "";
253         }
254     }
255
256     // mainly used to refresh the auth token when using OAuth
257     private boolean refresh() {
258         synchronized (refreshSynchronization) {
259             Person person;
260             try {
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");
266                     return false;
267                 }
268                 person = client.getPerson();
269                 String type = person.getType();
270                 if (type == null) {
271                     type = "?";
272                 }
273                 updateProperty(PROPERTY_WEBEX_TYPE, type);
274                 this.accountType = type;
275                 updateProperty(PROPERTY_WEBEX_NAME, person.getDisplayName());
276
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));
285                     }
286                 }
287                 updateStatus(ThingStatus.ONLINE);
288                 return true;
289             } catch (WebexTeamsException e) {
290                 logger.warn("Failed to refresh: {}", e.getMessage());
291                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
292             }
293             return false;
294         }
295     }
296
297     private void startRefresh() {
298         synchronized (refreshSynchronization) {
299             if (refresh()) {
300                 cancelSchedulers();
301                 if (active) {
302                     refreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshPeriod,
303                             TimeUnit.SECONDS);
304                 }
305             }
306         }
307     }
308
309     /**
310      * Cancels all running schedulers.
311      */
312     private synchronized void cancelSchedulers() {
313         Future<?> future = this.refreshFuture;
314         if (future != null) {
315             future.cancel(true);
316             this.refreshFuture = null;
317         }
318     }
319
320     public String getUser() {
321         return thing.getProperties().getOrDefault(PROPERTY_WEBEX_NAME, "");
322     }
323
324     public ThingUID getUID() {
325         return thing.getUID();
326     }
327
328     public String getLabel() {
329         return Objects.requireNonNullElse(thing.getLabel(), "");
330     }
331
332     /**
333      * Sends a message to the default room.
334      *
335      * @param msg markdown text string to be sent
336      *
337      * @return <code>true</code>, if sending the message has been successful and
338      *         <code>false</code> in all other cases.
339      */
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);
346     }
347
348     /**
349      * Sends a message with file attachment to the default room.
350      *
351      * @param msg markdown text string to be sent
352      * @param attach URL of the attachment
353      *
354      * @return <code>true</code>, if sending the message has been successful and
355      *         <code>false</code> in all other cases.
356      */
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);
364     }
365
366     /**
367      * Send a message to a specific room
368      * 
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.
373      */
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);
380     }
381
382     /**
383      * Send a message to a specific room, with attachment
384      * 
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
388      * 
389      * @return <code>true</code>, if sending the message has been successful and
390      *         <code>false</code> in all other cases.
391      */
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);
399     }
400
401     /**
402      * Sends a message to a specific person, identified by email
403      * 
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.
408      */
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);
415     }
416
417     /**
418      * Sends a message to a specific person, identified by email, with attachment
419      * 
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.
425      */
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);
433     }
434
435     /**
436      * Sends a <code>Message</code>
437      * 
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.
441      */
442     private boolean sendMessage(Message msg) {
443         try {
444             WebexTeamsApi client = this.client;
445             if (client != null) {
446                 client.sendMessage(msg);
447                 return true;
448             } else {
449                 logger.warn("Client not properly initialized");
450                 return false;
451             }
452         } catch (WebexTeamsException e) {
453             logger.warn("Failed to send message: {}", e.getMessage());
454         }
455         return false;
456     }
457
458     @Override
459     public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
460     }
461 }