001 package org.deepamehta.plugins.twitter; 002 003 import com.sun.jersey.core.util.Base64; 004 import de.deepamehta.core.RelatedTopic; 005 import de.deepamehta.core.Topic; 006 import de.deepamehta.core.model.CompositeValueModel; 007 import de.deepamehta.core.model.SimpleValue; 008 import de.deepamehta.core.model.TopicModel; 009 import de.deepamehta.core.osgi.PluginActivator; 010 import de.deepamehta.core.service.ClientState; 011 import de.deepamehta.core.service.Directives; 012 import de.deepamehta.core.service.PluginService; 013 import de.deepamehta.core.service.ResultList; 014 import de.deepamehta.core.service.annotation.ConsumesService; 015 import de.deepamehta.core.storage.spi.DeepaMehtaTransaction; 016 import de.deepamehta.plugins.accesscontrol.model.ACLEntry; 017 import de.deepamehta.plugins.accesscontrol.model.AccessControlList; 018 import de.deepamehta.plugins.accesscontrol.model.Operation; 019 import de.deepamehta.plugins.accesscontrol.model.UserRole; 020 import de.deepamehta.plugins.accesscontrol.service.AccessControlService; 021 import java.io.BufferedReader; 022 import java.io.IOException; 023 import java.io.InputStreamReader; 024 import java.io.OutputStreamWriter; 025 import java.net.HttpURLConnection; 026 import java.net.MalformedURLException; 027 import java.net.URL; 028 import java.net.URLEncoder; 029 import java.util.Date; 030 import java.util.Iterator; 031 import java.util.logging.Logger; 032 import javax.ws.rs.*; 033 import javax.ws.rs.core.Response.Status; 034 import org.codehaus.jettison.json.JSONArray; 035 import org.codehaus.jettison.json.JSONException; 036 import org.codehaus.jettison.json.JSONObject; 037 import org.deepamehta.plugins.twitter.service.TwitterService; 038 039 /** 040 * A very basic client for researching with the public Twitter Search API v1.1 and DeepaMehta 4.1.2 041 * 042 * @author Malte Reißig (<malte@mikromedia.de>) 043 * @version 1.3.0-SNAPSHOT 044 * @website https://github.com/mukil/twitter-research 045 * 046 */ 047 048 @Path("/tweet") 049 @Consumes("application/json") 050 @Produces("application/json") 051 public class TwitterPlugin extends PluginActivator implements TwitterService { 052 053 private Logger log = Logger.getLogger(getClass().getName()); 054 055 private final String DEEPAMEHTA_VERSION = "DeepaMehta 4.1.3-SNAPSHOT"; 056 private final String TWITTER_RESEARCH_VERSION = "1.3.0-SNAPSHOT"; 057 private final String CHARSET = "UTF-8"; 058 059 private final static String CHILD_URI = "dm4.core.child"; 060 private final static String PARENT_URI = "dm4.core.parent"; 061 private final static String AGGREGATION = "dm4.core.aggregation"; 062 private final static String COMPOSITION = "dm4.core.composition"; 063 064 private final static String TWEET_URI = "org.deepamehta.twitter.tweet"; 065 private final static String TWEET_ID_URI = "org.deepamehta.twitter.tweet_id"; 066 private final static String TWEET_TIME_URI = "org.deepamehta.twitter.tweet_time"; 067 private final static String TWEET_CONTENT_URI = "org.deepamehta.twitter.tweet_content"; 068 private final static String TWEET_ENTITIES_URI = "org.deepamehta.twitter.tweet_entities"; 069 private final static String TWEET_METADATA_URI = "org.deepamehta.twitter.tweet_metadata"; 070 private final static String TWEET_SOURCE_BUTTON_URI = "org.deepamehta.twitter.tweet_source_button"; 071 private final static String TWEET_LOCATION_URI = "org.deepamehta.twitter.tweet_location"; 072 private final static String TWEET_FAVOURITE_COUNT_URI = "org.deepamehta.twitter.tweet_favourite_count"; 073 private final static String TWEET_WITHHELD_DMCA_URI = "org.deepamehta.twitter.tweet_withheld_copyright"; 074 private final static String TWEET_WITHHELD_IN_URI = "org.deepamehta.twitter.tweet_withheld_in"; 075 private final static String TWEET_WITHHELD_SCOPE_URI = "org.deepamehta.twitter.tweet_withheld_scope"; 076 private final static String TWEETED_TO_STATUS_ID = "org.deepamehta.twitter.tweeted_to_status_id"; 077 078 private final static String TWITTER_USER_URI = "org.deepamehta.twitter.user"; 079 private final static String TWITTER_USER_ID_URI = "org.deepamehta.twitter.user_id"; 080 private final static String TWITTER_USER_NAME_URI = "org.deepamehta.twitter.user_name"; 081 private final static String TWITTER_USER_IMAGE_URI = "org.deepamehta.twitter.user_image_url"; 082 083 private final static String TWITTER_SEARCH_URI = "org.deepamehta.twitter.search"; 084 private final static String TWITTER_SEARCH_LANG_URI = "org.deepamehta.twitter.search_language"; 085 private final static String TWITTER_SEARCH_LOCATION_URI = "org.deepamehta.twitter.search_location"; 086 private final static String TWITTER_SEARCH_TYPE_URI = "org.deepamehta.twitter.search_result_type"; 087 private final static String TWITTER_SEARCH_NEXT_PAGE_URI = "org.deepamehta.twitter.search_next_page"; 088 private final static String TWITTER_SEARCH_REFRESH_URL_URI = "org.deepamehta.twitter.search_refresh_url"; 089 private final static String TWITTER_SEARCH_MAX_TWEET_URI = "org.deepamehta.twitter.search_last_tweet_id"; 090 private final static String TWITTER_SEARCH_RESULT_SIZE_URI = "org.deepamehta.twitter.search_result_size"; 091 private final static String TWITTER_SEARCH_TIME_URI = "org.deepamehta.twitter.last_search_time"; 092 093 private final static String TWITTER_AUTHENTICATION_URL = "https://api.twitter.com/oauth2/token"; 094 private final static String TWITTER_SEARCH_BASE_URL = "https://api.twitter.com/1.1/search/tweets.json"; 095 096 private final String GEO_COORDINATE_TOPIC_URI = "dm4.geomaps.geo_coordinate"; 097 private final String GEO_LONGITUDE_TYPE_URI = "dm4.geomaps.longitude"; 098 private final String GEO_LATITUDE_TYPE_URI = "dm4.geomaps.latitude"; 099 100 private boolean isInitialized = false; 101 private boolean isAuthorized = false; 102 private String bearerToken = null; 103 private AccessControlService acService = null; 104 105 106 107 /** Initialize the migrated soundsets ACL-Entries. */ 108 public void init() { 109 isInitialized = true; 110 configureIfReady(); 111 } 112 113 private void configureIfReady() { 114 if (isInitialized) { 115 checkACLsOfMigration(); 116 } 117 } 118 119 private void authorizeSearchRequests () throws TwitterAPIException { 120 Topic applicationKey = dms.getTopic("uri", new SimpleValue("org.deepamehta.twitter.application_key"), true); 121 Topic applicationSecret = dms.getTopic("uri", 122 new SimpleValue("org.deepamehta.twitter.application_secret"), true); 123 try { 124 StringBuilder resultBody = new StringBuilder(); 125 URL requestUri = new URL(TWITTER_AUTHENTICATION_URL); 126 // 127 String key = URLEncoder.encode(applicationKey.getSimpleValue().toString(), CHARSET); 128 String secret = URLEncoder.encode(applicationSecret.getSimpleValue().toString(), CHARSET); 129 // get base64 encoded secrets 130 if (key.isEmpty() || secret.isEmpty()) { 131 throw new TwitterAPIException("Bad Twitter secrets, please register your application.", 132 Status.UNAUTHORIZED); 133 } 134 String values = key + ":" + secret; 135 String credentials = new String(Base64.encode(values)); 136 // initiate request 137 HttpURLConnection connection = (HttpURLConnection) requestUri.openConnection(); 138 connection.setDoOutput(true); 139 connection.setDoInput(true); 140 connection.setRequestMethod("POST"); 141 connection.setRequestProperty("User-Agent", "DeepaMehta "+DEEPAMEHTA_VERSION+" - " 142 + "Twitter Research " + TWITTER_RESEARCH_VERSION); 143 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=" + CHARSET); 144 connection.setRequestProperty("Authorization", "Basic " + credentials); 145 // 146 OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); 147 String parameters = "grant_type=client_credentials"; 148 writer.write(parameters); 149 writer.flush(); 150 // 151 int httpStatusCode = connection.getResponseCode(); 152 if (httpStatusCode != HttpURLConnection.HTTP_OK) { 153 throw new TwitterAPIException("Error with HTTPConnection.", Status.INTERNAL_SERVER_ERROR); 154 } 155 // read in the response 156 BufferedReader rd = new BufferedReader(new InputStreamReader(connection.getInputStream(), CHARSET)); 157 for (String input; (input = rd.readLine()) != null;) { 158 resultBody.append(input); 159 } 160 rd.close(); 161 writer.close(); 162 // TODO: Check if answer is something like "403: Too many requests" 163 if (resultBody.toString().isEmpty()) { 164 throw new TwitterAPIException("Twitter just handed us an empty response ("+httpStatusCode+")", 165 Status.NO_CONTENT); 166 } 167 // 168 JSONObject response = new JSONObject(resultBody.toString()); 169 bearerToken = response.getString("access_token"); 170 isAuthorized = true; 171 } catch (JSONException ex) { 172 throw new RuntimeException("Internal Server Error while parsing response " + ex.getMessage()); 173 } catch (IOException ex) { 174 throw new RuntimeException("Internal Server Error HTTP I/O Error " + ex.getMessage()); 175 } 176 } 177 178 /** 179 * This method executes an existing search-query to either: 180 * (a) fetch more (older) tweets for the same query or 181 * (b) fetch new tweets and assign them to the current search result 182 * 183 * @param {searchId} id of the "Twitter-Search"-Topic to operate on 184 * @param {nextPage} <code>true</code> for paging query to next page; 185 * <code>false</code> for fetching most recent tweets 186 */ 187 188 @GET 189 @Path("/search/public/{id}/{nextPage}") 190 @Produces("application/json") 191 public Topic searchMoreTweets(@PathParam("id") long searchId, 192 @PathParam("nextPage") boolean nextPage, @HeaderParam("Cookie") ClientState clientState) { 193 194 Topic query = dms.getTopic(searchId, true); 195 StringBuffer resultBody = new StringBuffer(); 196 URL requestUri = null; 197 DeepaMehtaTransaction tx = dms.beginTx(); 198 try { 199 // 0) Authorize request 200 if (!isAuthorized) { 201 authorizeSearchRequests(); 202 if (!isAuthorized) { // check if authorization was sucessfull 203 throw new WebApplicationException(new Throwable("Bad Twitter Secrets. " 204 + "Consider to register your application at https://dev.twitter.com/apps/new."), 500); 205 } 206 } 207 log.fine("Researching tweets for Twitter-Search (" +query.getId()+ ") next ? " + nextPage); 208 // 1) loading search configuration 209 if (nextPage) { 210 // paging to next-page (query for older-tweets) 211 String nextPageUrl = query.getCompositeValue().getString(TWITTER_SEARCH_NEXT_PAGE_URI); 212 if (nextPageUrl.isEmpty()) throw new RuntimeException("There is no next page. (204)"); 213 log.fine("Loading next page of tweets => " + TWITTER_SEARCH_BASE_URL + nextPageUrl); 214 requestUri = new URL(TWITTER_SEARCH_BASE_URL + nextPageUrl); 215 } else { 216 // refreshing (query for new-tweets) 217 String refreshPageUrl = query.getCompositeValue().getString(TWITTER_SEARCH_REFRESH_URL_URI); 218 requestUri = new URL(TWITTER_SEARCH_BASE_URL + refreshPageUrl); 219 log.fine("Loading more recent tweets => " + TWITTER_SEARCH_BASE_URL + refreshPageUrl); 220 } 221 log.fine("Requesting => " + requestUri.toString()); 222 // 2) initiate request 223 HttpURLConnection connection = (HttpURLConnection) requestUri.openConnection(); 224 connection.setRequestMethod("GET"); 225 connection.setRequestProperty("User-Agent", "DeepaMehta "+DEEPAMEHTA_VERSION+" - " 226 + "Twitter Research " + TWITTER_RESEARCH_VERSION); 227 connection.setRequestProperty("Authorization", "Bearer " + bearerToken); 228 // 3) check the response 229 int httpStatusCode = connection.getResponseCode(); 230 if (httpStatusCode != HttpURLConnection.HTTP_OK) { 231 throw new WebApplicationException(new Throwable("Error with HTTPConnection."), 232 Status.INTERNAL_SERVER_ERROR); 233 } 234 // 4) read in the response 235 BufferedReader rd = new BufferedReader(new InputStreamReader(connection.getInputStream(), CHARSET)); 236 for (String input; (input = rd.readLine()) != null;) { 237 resultBody.append(input); 238 } 239 rd.close(); 240 // 5) process response // TODO: Check if answer is something like "403: Too many requests" 241 if (resultBody.toString().isEmpty()) { 242 throw new WebApplicationException(new RuntimeException("Twitter handed just us an empty response."), 243 Status.NO_CONTENT); 244 } else { 245 processTwitterSearchResponse(query, resultBody, clientState); 246 } 247 tx.success(); 248 // update modification timestamp on parent (composite) topic to invalidate http caching 249 dms.updateTopic(new TopicModel(query.getId()), clientState); 250 } catch (TwitterAPIException ex) { 251 log.warning("TwitterApiException " + ex.getMessage()); 252 throw new WebApplicationException(new Throwable(ex.getMessage()), ex.getStatus()); 253 } catch (MalformedURLException e) { 254 throw new RuntimeException("Could not trigger existing search-topic.", e); 255 } catch (IOException ioe) { 256 throw new WebApplicationException(new Throwable("Most probably we made a mistake in constructing the query. " 257 + "We're sorry, please try again."), Status.BAD_REQUEST); 258 } finally { 259 tx.finish(); 260 } 261 return query; 262 } 263 264 265 266 /** 267 * Fetches public tweets matching the given <code>query</code>, maintains a search-query topic and 268 * references existing tweets and users, as it should be. 269 * 270 * @param {id} Twitter Search Topic Id 271 * @param {resultType} "mixed", "recent", "popular" 272 * @param {lang} ISO-639-1 Code (2 chars) (optional) 273 * @param {location} "lat,lng,radiuskm" (optional) 274 */ 275 276 @GET 277 @Path("/search/public/{id}/{query}/{resultType}/{lang}/{location}") 278 @Produces("application/json") 279 public Topic searchPublicTweets(@PathParam("id") long searchId, @PathParam("query") String query, 280 @PathParam("resultType") String resultType, @PathParam("lang") String lang, 281 @PathParam("location") String location, @HeaderParam("Cookie") ClientState clientState) { 282 283 StringBuffer resultBody = new StringBuffer(); 284 DeepaMehtaTransaction tx = dms.beginTx(); 285 try { 286 // 0) Authorize request 287 if (!isAuthorized) { 288 authorizeSearchRequests(); 289 if (!isAuthorized) { // check if authorization was sucessfull 290 throw new WebApplicationException(new Throwable("Bad Twitter Secrets. " 291 + "Consider to register your application at https://dev.twitter.com/apps/new."), 500); 292 } 293 } 294 // 1) setup search container 295 Topic twitterSearch = dms.getTopic(searchId, true); 296 log.fine("Resarching Public Tweets " +query+ " (" +resultType+ ") " 297 + "in language: " + lang + " near loc: " + location); 298 // 2) construct search query 299 String queryUrl = TWITTER_SEARCH_BASE_URL + "?q=" + URLEncoder.encode(query.toString(), CHARSET) 300 + ";&include_entities=true;&result_type=" + resultType + ";"; // ;&rpp=" + querySize + " 301 if (lang == null) lang = ""; 302 if (location == null) location = ""; 303 if (!lang.isEmpty() && !lang.equals("unspecified")) queryUrl += "&lang="+lang+";"; 304 if (!location.isEmpty() && !location.equals("none")) queryUrl += "&geocode="+location+";"; 305 URL requestUri = new URL(queryUrl); 306 // 3) initiate request 307 HttpURLConnection connection = (HttpURLConnection) requestUri.openConnection(); 308 connection.setRequestMethod("GET"); 309 connection.setRequestProperty("User-Agent", "DeepaMehta "+DEEPAMEHTA_VERSION+" - " 310 + "Twitter Research " + TWITTER_RESEARCH_VERSION); 311 connection.setRequestProperty("Authorization", "Bearer " + bearerToken); 312 // 4) check the response 313 int httpStatusCode = connection.getResponseCode(); 314 if (httpStatusCode != HttpURLConnection.HTTP_OK) { 315 throw new WebApplicationException(new Throwable("Error with HTTPConnection."), 316 Status.INTERNAL_SERVER_ERROR); 317 } 318 // 5) process response 319 BufferedReader rd = new BufferedReader(new InputStreamReader(connection.getInputStream(), CHARSET)); 320 for (String input; (input = rd.readLine()) != null;) { 321 resultBody.append(input); 322 } 323 rd.close(); 324 if (resultBody.toString().isEmpty()) { 325 throw new WebApplicationException(new RuntimeException("Twitter just handed us an empty response."), 326 Status.NO_CONTENT); 327 } else { 328 processTwitterSearchResponse(twitterSearch, resultBody, clientState); 329 } 330 tx.success(); 331 return twitterSearch; 332 } catch (TwitterAPIException ex) { 333 throw new WebApplicationException(new Throwable(ex.getMessage()), ex.getStatus()); 334 } catch (IOException e) { 335 throw new WebApplicationException(new RuntimeException("HTTP I/O Error", e)); 336 } finally { 337 tx.finish(); 338 } 339 } 340 341 342 343 /** Private Helper Methods */ 344 345 private void processTwitterSearchResponse(Topic twitterSearch, StringBuffer resultBody, ClientState clientState) { 346 DeepaMehtaTransaction tx = dms.beginTx(); 347 try { 348 // 349 JSONObject results = new JSONObject(resultBody.toString()); 350 // add or reference all new tweets and new twitter user accounts 351 for (int i=0; i < results.getJSONArray("statuses").length(); i++) { 352 JSONObject item = results.getJSONArray("statuses").getJSONObject(i); 353 // gets an existing or creates a new "Twitter User"-Topic 354 JSONObject user = item.getJSONObject("user"); 355 String userName = "", twitterUserId = "", profileImageUrl = ""; 356 if (user.has("name")) userName = user.getString("name"); 357 if (user.has("id_str")) twitterUserId = user.getString("id_str"); 358 if (user.has("profile_image_url")) profileImageUrl = user.getString("profile_image_url"); 359 360 Topic twitterUser = getTwitterUser(twitterUserId, userName, profileImageUrl, clientState); 361 // gets an existing or creates a new "Tweet"-Topic 362 Topic tweet = getTweet(item, twitterUser.getId(), clientState); 363 // associate "Tweet" with "Twitter User" fixme: check if there's already an association 364 twitterSearch.setCompositeValue(new CompositeValueModel().addRef(TWEET_URI, tweet.getId()), 365 clientState, new Directives()); 366 // old style association 367 /* dms.createAssociation(new AssociationModel(SEARCH_RESULT, 368 new TopicRoleModel(searchId, DEFAULT_URI), 369 new TopicRoleModel(tweet.getId(), DEFAULT_URI)), clientState); **/ 370 } 371 372 // get current (overall) result size 373 int size = twitterSearch.getRelatedTopics(AGGREGATION, PARENT_URI, CHILD_URI, TWEET_URI, false, false, 374 0).getTotalCount(); 375 // update our "Twitter Search"-Topic to reflect results after latest query 376 String nextPage = "", maxTweetId = "", refreshUrl = ""; 377 JSONObject search_metadata; 378 if (results.has("search_metadata")) { 379 search_metadata = results.getJSONObject("search_metadata"); 380 // 381 if (search_metadata.has("max_id_str")) maxTweetId = search_metadata.getString("max_id_str"); 382 if (search_metadata.has("next_results")) nextPage = search_metadata.getString("next_results"); 383 if (search_metadata.has("refresh_url")) refreshUrl = search_metadata.getString("refresh_url"); 384 } 385 // update search cointainer 386 twitterSearch.getCompositeValue().set(TWITTER_SEARCH_NEXT_PAGE_URI, 387 nextPage, clientState, new Directives()); 388 twitterSearch.getCompositeValue().set(TWITTER_SEARCH_RESULT_SIZE_URI, size, 389 clientState, new Directives()); 390 twitterSearch.getCompositeValue().set(TWITTER_SEARCH_TIME_URI, new Date().getTime(), clientState, 391 new Directives()); 392 twitterSearch.getCompositeValue().set(TWITTER_SEARCH_MAX_TWEET_URI, 393 maxTweetId, clientState, new Directives()); 394 twitterSearch.getCompositeValue().set(TWITTER_SEARCH_REFRESH_URL_URI, 395 refreshUrl, clientState, new Directives()); 396 tx.success(); 397 } catch (JSONException e) { 398 log.warning("ERROR: We could not parse the response properly " + e.getMessage()); 399 throw new RuntimeException("We could not parse the response properly." + e.getMessage()); 400 } finally { 401 tx.finish(); 402 } 403 } 404 405 private Topic getTweet(JSONObject item, long userTopicId, ClientState clientState) { 406 Topic tweet = null; 407 DeepaMehtaTransaction tx = dms.beginTx(); 408 try { 409 Topic tweetId = dms.getTopic(TWEET_ID_URI, new SimpleValue(item.getString("id_str")), true); 410 if (tweetId != null) { 411 tweet = tweetId.getRelatedTopic(COMPOSITION, CHILD_URI, PARENT_URI, TWEET_URI, true, false); 412 } else { 413 tweet = createTweet(item, userTopicId, clientState); 414 } 415 tx.success(); 416 } catch (Exception e) { 417 throw new RuntimeException("We could neither fetch nor create this \"Tweet\"."); 418 } finally { 419 tx.finish(); 420 } 421 return tweet; 422 } 423 424 private Topic createTweet(JSONObject item, long userTopicId, ClientState clientState) { 425 Topic tweet = null; 426 DeepaMehtaTransaction tx = dms.beginTx(); 427 try { 428 // find twitter's doc on a tweets fields (https://dev.twitter.com/docs/platform-objects/tweets) 429 TopicModel topic = new TopicModel(TWEET_URI); 430 TopicModel coordinate = null; 431 String inReplyToStatus = "", withheldCopyright = "", 432 withheldInCountries = "", withheldScope = "", metadata = "", entities = ""; 433 int favourite_count = 0; 434 if (item.has("place") && !item.isNull("place")) { 435 JSONObject place = item.getJSONObject("place"); 436 if (place.has("bounding_box")) { 437 JSONObject box = place.getJSONObject("bounding_box"); 438 JSONArray tudes = box.getJSONArray("coordinates"); 439 // first value in array is always longitude 440 JSONArray container = tudes.getJSONArray(0); 441 JSONArray lower_left = container.getJSONArray(0); 442 JSONArray lower_right = container.getJSONArray(1); 443 JSONArray upper_right = container.getJSONArray(2); 444 JSONArray upper_left = container.getJSONArray(3); 445 double lng = (upper_left.getDouble(0) + upper_right.getDouble(0) + lower_left.getDouble(0) + lower_right.getDouble(0)) / 4; 446 double lat = (upper_left.getDouble(1) + upper_right.getDouble(1) + lower_left.getDouble(1) + lower_right.getDouble(1)) / 4; 447 coordinate = createGeoCoordinateTopicModel(lng, lat); 448 } 449 } 450 // check for and create coordinates (simply overwrites place if given) 451 if (item.has("coordinates") && !item.isNull("coordinates")) { 452 JSONObject coordinates = item.getJSONObject("coordinates"); 453 if (coordinates.has("coordinates")) { 454 JSONArray tudes = coordinates.getJSONArray("coordinates"); 455 log.fine("Writing coordinates OVER place.."); 456 coordinate = createGeoCoordinateTopicModel(tudes.getDouble(0), tudes.getDouble(1)); 457 } 458 } 459 if (item.has("in_reply_to_status_id_str")) inReplyToStatus = item.getString("in_reply_to_status_id_str"); 460 if (item.has("withheld_copyright")) withheldCopyright = item.getString("withheld_copyright"); 461 if (item.has("withheld_in_countries")) withheldInCountries = item.getJSONArray("withheld_in_countries") 462 .toString(); 463 if (item.has("withheld_scope")) withheldScope = item.getString("withheld_scope"); 464 if (item.has("favorite_count")) favourite_count = item.getInt("favorite_count"); 465 if (item.has("place") && !item.isNull("place")) metadata = item.getJSONObject("place").toString(); 466 if (item.has("entities") && !item.isNull("entities")) entities = item.getJSONObject("entities").toString(); 467 CompositeValueModel content = new CompositeValueModel() 468 .put(TWEET_CONTENT_URI, item.getString("text")) 469 .put(TWEET_TIME_URI, item.getString("created_at")) // is utc-time 470 .put(TWEET_ID_URI, item.getString("id_str")) 471 .put(TWEET_ENTITIES_URI, entities) // is application-json/text 472 .put(TWEET_METADATA_URI, metadata) // is application-json/text 473 .put(TWEET_LOCATION_URI, "") // unused, to be removed (?) 474 .put(TWEET_FAVOURITE_COUNT_URI, favourite_count) 475 .put(TWEETED_TO_STATUS_ID, inReplyToStatus) 476 .put(TWEET_WITHHELD_DMCA_URI, withheldCopyright) // a boolean indicating dmca-request 477 .put(TWEET_WITHHELD_IN_URI, withheldInCountries) // is application-json/text (array) 478 .put(TWEET_WITHHELD_SCOPE_URI, withheldScope) // is application-json/text (array) 479 .put(TWEET_SOURCE_BUTTON_URI, item.getString("source")) 480 .putRef(TWITTER_USER_URI, userTopicId); 481 topic.setCompositeValue(content); 482 if (coordinate != null) { 483 content.put(GEO_COORDINATE_TOPIC_URI, coordinate.getCompositeValueModel()); 484 } 485 tweet = dms.createTopic(topic, clientState); 486 tx.success(); 487 } catch (JSONException jex) { 488 throw new RuntimeException(jex.getMessage()); 489 } finally { 490 tx.finish(); 491 } 492 return tweet; 493 } 494 495 private Topic getTwitterUser(String userId, String userName, String userImageUrl, ClientState clientState) { 496 Topic identity = null; 497 DeepaMehtaTransaction tx = dms.beginTx(); 498 try { 499 if (userId == null) throw new RuntimeException("Search Result is invalid. Missing Twitter-User-Id"); 500 Topic twitterId = dms.getTopic(TWITTER_USER_ID_URI, new SimpleValue(userId), true); 501 if (twitterId != null) { 502 identity = twitterId.getRelatedTopic(COMPOSITION, CHILD_URI, PARENT_URI, TWITTER_USER_URI, true, false); 503 } else { 504 identity = createTwitterUser(userId, userName, userImageUrl, clientState); 505 } 506 tx.success(); 507 } catch (Exception ex) { 508 log.info("Crashed query for twitter-id topic, trying to create new Twitter User Topic"); 509 throw new RuntimeException(ex); 510 } finally { 511 tx.finish(); 512 } 513 return identity; 514 } 515 516 private Topic createTwitterUser(String userId, String userName, String userImageUrl, ClientState clientState) { 517 TopicModel twitterUser = new TopicModel(TWITTER_USER_URI); 518 twitterUser.setCompositeValue(new CompositeValueModel() 519 .put(TWITTER_USER_ID_URI, userId) 520 .put(TWITTER_USER_NAME_URI, userName) 521 .put(TWITTER_USER_IMAGE_URI, userImageUrl)); 522 return dms.createTopic(twitterUser, clientState); 523 } 524 525 private TopicModel createGeoCoordinateTopicModel(double lng, double lat) { 526 TopicModel coordinates = new TopicModel(GEO_COORDINATE_TOPIC_URI); 527 CompositeValueModel model = new CompositeValueModel(); 528 model.put(GEO_LONGITUDE_TYPE_URI, lng); 529 model.put(GEO_LATITUDE_TYPE_URI, lat); 530 coordinates.setCompositeValue(model); 531 return coordinates; 532 } 533 534 /** Code running once, after plugin initialization. */ 535 536 private void checkACLsOfMigration() { 537 // secrets 538 ResultList<RelatedTopic> secrets = dms.getTopics("org.deepamehta.twitter.secret", false, 0); 539 Iterator<RelatedTopic> secs = secrets.iterator(); 540 while (secs.hasNext()) { 541 RelatedTopic secret = secs.next(); 542 DeepaMehtaTransaction dmx = dms.beginTx(); 543 try { 544 if (acService.getCreator(secret) == null) { 545 log.fine("Running initial ACL update of twitter secret topics " + secret.getSimpleValue().toString()); 546 Topic admin = acService.getUsername("admin"); 547 String adminName = admin.getSimpleValue().toString(); 548 acService.setCreator(secret, adminName); 549 acService.setOwner(secret, adminName); 550 acService.setACL(secret, new AccessControlList( // 551 new ACLEntry(Operation.WRITE, UserRole.OWNER))); 552 } 553 dmx.success(); 554 } catch (Exception ex) { 555 dmx.failure(); 556 log.warning(ex.getMessage()); 557 throw new RuntimeException(ex); 558 } finally { 559 dmx.finish(); 560 } 561 } 562 // keys 563 ResultList<RelatedTopic> keys = dms.getTopics("org.deepamehta.twitter.key", false, 0); 564 Iterator<RelatedTopic> ks = keys.iterator(); 565 while (ks.hasNext()) { 566 RelatedTopic key = ks.next(); 567 DeepaMehtaTransaction dmx = dms.beginTx(); 568 try { 569 if (acService.getCreator(key) == null) { 570 log.fine("Running initial ACL update of twitter key topics " + key.getSimpleValue().toString()); 571 Topic admin = acService.getUsername("admin"); 572 String adminName = admin.getSimpleValue().toString(); 573 acService.setCreator(key, adminName); 574 acService.setOwner(key, adminName); 575 acService.setACL(key, new AccessControlList( // 576 new ACLEntry(Operation.WRITE, UserRole.OWNER))); 577 } 578 dmx.success(); 579 } catch (Exception ex) { 580 dmx.failure(); 581 log.warning(ex.getMessage()); 582 throw new RuntimeException(ex); 583 } finally { 584 dmx.finish(); 585 } 586 } 587 } 588 589 /** --- Implementing PluginService Interfaces to consume AccessControlService --- */ 590 591 @Override 592 @ConsumesService({ 593 "de.deepamehta.plugins.accesscontrol.service.AccessControlService" 594 }) 595 public void serviceArrived(PluginService service) { 596 if (service instanceof AccessControlService) { 597 acService = (AccessControlService) service; 598 } 599 } 600 601 @Override 602 @ConsumesService({ 603 "de.deepamehta.plugins.accesscontrol.service.AccessControlService" 604 }) 605 public void serviceGone(PluginService service) { 606 if (service == acService) { 607 acService = null; 608 } 609 } 610 611 }