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 }