]> git.basschouten.com Git - openhab-addons.git/blob
b317f0c3f463767175715739e096eb9304c557cd
[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.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; // 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         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         }
151         oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
152         cancelSchedulers();
153     }
154
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;
163     }
164
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);
175         try {
176             service.importAccessTokenResponse(response);
177         } catch (OAuthException e) {
178             throw new WebexTeamsException("Failed to create oauth client with bot token", e);
179         }
180         this.authService = service;
181         this.configured = true;
182     }
183
184     boolean isConfigured() {
185         return configured;
186     }
187
188     protected String authorize(String redirectUri, String reqCode) throws WebexTeamsException {
189         try {
190             logger.debug("Make call to Webex to get access token.");
191
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);
196             }
197
198             startRefresh();
199             final String user = getUser();
200             logger.info("Authorized for user: {}", user);
201
202             return 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);
209         }
210     }
211
212     public boolean isAuthorized() {
213         final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
214         if (accessTokenResponse == null) {
215             return false;
216         }
217
218         if ("person".equals(this.accountType)) {
219             return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null
220                     && accessTokenResponse.getRefreshToken() != null;
221         } else {
222             // bots don't need no refreshToken!
223             return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null;
224         }
225     }
226
227     private @Nullable AccessTokenResponse getAccessTokenResponse() {
228         try {
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);
233             return null;
234         }
235     }
236
237     public boolean equalsThingUID(String thingUID) {
238         return getThing().getUID().getAsString().equals(thingUID);
239     }
240
241     public String formatAuthorizationUrl(String redirectUri) {
242         try {
243             if (this.configured) {
244                 OAuthClientService authService = this.authService;
245                 if (authService != null) {
246                     return authService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString());
247                 } else {
248                     logger.warn("AuthService not properly initialized");
249                     return "";
250                 }
251             } else {
252                 return "";
253             }
254         } catch (final OAuthException e) {
255             logger.warn("Error constructing AuthorizationUrl: ", e);
256             return "";
257         }
258     }
259
260     // mainly used to refresh the auth token when using OAuth
261     private boolean refresh() {
262         synchronized (refreshSynchronization) {
263             Person person;
264             try {
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");
270                     return false;
271                 }
272                 person = client.getPerson();
273                 String type = person.getType();
274                 if (type == null) {
275                     type = "?";
276                 }
277                 updateProperty(PROPERTY_WEBEX_TYPE, type);
278                 this.accountType = type;
279                 updateProperty(PROPERTY_WEBEX_NAME, person.getDisplayName());
280
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));
289                     }
290                 }
291                 updateStatus(ThingStatus.ONLINE);
292                 return true;
293             } catch (WebexTeamsException e) {
294                 logger.warn("Failed to refresh: {}.  Did you authorize?", e.getMessage());
295                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
296             }
297             return false;
298         }
299     }
300
301     private void startRefresh() {
302         synchronized (refreshSynchronization) {
303             if (refresh()) {
304                 cancelSchedulers();
305                 if (active) {
306                     refreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshPeriod,
307                             TimeUnit.SECONDS);
308                 }
309             }
310         }
311     }
312
313     /**
314      * Cancels all running schedulers.
315      */
316     private synchronized void cancelSchedulers() {
317         Future<?> future = this.refreshFuture;
318         if (future != null) {
319             future.cancel(true);
320             this.refreshFuture = null;
321         }
322     }
323
324     public String getUser() {
325         return thing.getProperties().getOrDefault(PROPERTY_WEBEX_NAME, "");
326     }
327
328     public ThingUID getUID() {
329         return thing.getUID();
330     }
331
332     public String getLabel() {
333         return Objects.requireNonNullElse(thing.getLabel(), "");
334     }
335
336     /**
337      * Sends a message to the default room.
338      *
339      * @param msg markdown text string to be sent
340      *
341      * @return <code>true</code>, if sending the message has been successful and
342      *         <code>false</code> in all other cases.
343      */
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);
350     }
351
352     /**
353      * Sends a message with file attachment to the default room.
354      *
355      * @param msg markdown text string to be sent
356      * @param attach URL of the attachment
357      *
358      * @return <code>true</code>, if sending the message has been successful and
359      *         <code>false</code> in all other cases.
360      */
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);
368     }
369
370     /**
371      * Send a message to a specific room
372      * 
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.
377      */
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);
384     }
385
386     /**
387      * Send a message to a specific room, with attachment
388      * 
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
392      * 
393      * @return <code>true</code>, if sending the message has been successful and
394      *         <code>false</code> in all other cases.
395      */
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);
403     }
404
405     /**
406      * Sends a message to a specific person, identified by email
407      * 
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.
412      */
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);
419     }
420
421     /**
422      * Sends a message to a specific person, identified by email, with attachment
423      * 
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.
429      */
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);
437     }
438
439     /**
440      * Sends a <code>Message</code>
441      * 
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.
445      */
446     private boolean sendMessage(Message msg) {
447         try {
448             WebexTeamsApi client = this.client;
449             if (client != null) {
450                 client.sendMessage(msg);
451                 return true;
452             } else {
453                 logger.warn("Client not properly initialized");
454                 return false;
455             }
456         } catch (WebexTeamsException e) {
457             logger.warn("Failed to send message: {}", e.getMessage());
458         }
459         return false;
460     }
461
462     @Override
463     public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
464     }
465 }