diff --git a/build.gradle b/build.gradle index d13fc0cf1c..ce4e782621 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ buildscript { ext.kotlin_version = "1.3.31" ext.kovenant_version = "3.3.0" ext.identicon_version = "v11" + ext.rss_parser_version = "2.0.4" repositories { google() @@ -179,6 +180,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "nl.komponents.kovenant:kovenant:$kovenant_version" implementation "com.github.lelloman:android-identicons:$identicon_version" + implementation "com.prof.rssparser:rssparser:$rss_parser_version" } def canonicalVersionCode = 9 diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 34fd77ebc0..58d0ee2f61 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -57,6 +57,7 @@ import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.loki.BackgroundPollWorker; import org.thoughtcrime.securesms.loki.LokiAPIDatabase; import org.thoughtcrime.securesms.loki.LokiGroupChatPoller; +import org.thoughtcrime.securesms.loki.LokiRSSFeedPoller; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.providers.BlobProvider; @@ -83,16 +84,16 @@ import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; import org.whispersystems.signalservice.loki.api.LokiLongPoller; import org.whispersystems.signalservice.loki.api.LokiP2PAPI; import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate; +import org.whispersystems.signalservice.loki.api.LokiRSSFeed; import java.security.Security; +import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import dagger.ObjectGraph; import kotlin.Unit; -import kotlin.jvm.functions.Function1; import network.loki.messenger.BuildConfig; /** @@ -118,6 +119,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc // Loki private LokiLongPoller lokiLongPoller = null; private LokiGroupChatPoller lokiPublicChatPoller = null; + private LokiRSSFeedPoller lokiNewsFeedPoller = null; + private LokiRSSFeedPoller lokiMessengerUpdatesFeedPoller = null; public SignalCommunicationModule communicationModule; private volatile boolean isAppVisible; @@ -409,15 +412,11 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc if (userHexEncodedPublicKey == null) return; LokiAPIDatabase lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(this); Context context = this; - lokiLongPoller = new LokiLongPoller(userHexEncodedPublicKey, lokiAPIDatabase, new Function1, Unit>() { - - @Override - public Unit invoke(List protos) { - for (SignalServiceProtos.Envelope proto : protos) { - new PushContentReceiveJob(context).processEnvelope(new SignalServiceEnvelope(proto)); - } - return Unit.INSTANCE; + lokiLongPoller = new LokiLongPoller(userHexEncodedPublicKey, lokiAPIDatabase, protos -> { + for (SignalServiceProtos.Envelope proto : protos) { + new PushContentReceiveJob(context).processEnvelope(new SignalServiceEnvelope(proto)); } + return Unit.INSTANCE; }); } @@ -430,19 +429,54 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc return new LokiGroupChat(LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer(), "Loki Public Chat", true); } - private void setUpPublicChatIfNeeded() { - if (lokiPublicChatPoller != null) return; - LokiGroupChat lokiPublicChat = this.lokiPublicChat(); - lokiPublicChatPoller = new LokiGroupChatPoller(this, lokiPublicChat); - boolean isPublicChatSetUp = TextSecurePreferences.isPublicChatSetUp(this); - if (isPublicChatSetUp) return; - GroupManager.createGroup(lokiPublicChat.getId(), this, new HashSet<>(), null, "Loki Public Chat", false); - TextSecurePreferences.markPublicChatSetUp(this); + private LokiRSSFeed lokiNewsFeed() { + return new LokiRSSFeed("loki.network.feed", "https://loki.network/feed/", "Loki News", true); } - public void startPublicChatPollingIfNeeded() { - setUpPublicChatIfNeeded(); + private LokiRSSFeed lokiMessengerUpdatesFeed() { + return new LokiRSSFeed("loki.network.messenger-updates.feed", "https://loki.network/category/messenger-updates/feed", "Loki Messenger Updates", false); + } + + public void createGroupChatsIfNeeded() { + LokiGroupChat publicChat = lokiPublicChat(); + boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publicChat.getId()); + if (!isChatSetUp || !publicChat.isDeletable()) { + GroupManager.createGroup(publicChat.getId(), this, new HashSet<>(), null, publicChat.getDisplayName(), false); + TextSecurePreferences.markChatSetUp(this, publicChat.getId()); + } + } + + public void createRSSFeedsIfNeeded() { + ArrayList feeds = new ArrayList<>(); + feeds.add(lokiNewsFeed()); + feeds.add(lokiMessengerUpdatesFeed()); + for (LokiRSSFeed feed : feeds) { + boolean isFeedSetUp = TextSecurePreferences.isChatSetUp(this, feed.getId()); + if (!isFeedSetUp || !feed.isDeletable()) { + GroupManager.createGroup(feed.getId(), this, new HashSet<>(), null, feed.getDisplayName(), false); + TextSecurePreferences.markChatSetUp(this, feed.getId()); + } + } + } + + private void createGroupChatPollersIfNeeded() { + if (lokiPublicChatPoller == null) lokiPublicChatPoller = new LokiGroupChatPoller(this, lokiPublicChat()); + } + + private void createRSSFeedPollersIfNeeded() { + if (lokiNewsFeedPoller == null) lokiNewsFeedPoller = new LokiRSSFeedPoller(this, lokiNewsFeed()); + if (lokiMessengerUpdatesFeedPoller == null) lokiMessengerUpdatesFeedPoller = new LokiRSSFeedPoller(this, lokiMessengerUpdatesFeed()); + } + + public void startGroupChatPollersIfNeeded() { + createGroupChatPollersIfNeeded(); lokiPublicChatPoller.startIfNeeded(); } + + public void startRSSFeedPollersIfNeeded() { + createRSSFeedPollersIfNeeded(); + lokiNewsFeedPoller.startIfNeeded(); + lokiMessengerUpdatesFeedPoller.startIfNeeded(); + } // endregion } diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index c0be4a30eb..42c4d4bbdd 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -82,7 +82,11 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit dynamicTheme.onCreate(this); dynamicLanguage.onCreate(this); if (TextSecurePreferences.getLocalNumber(this) != null) { - ApplicationContext.getInstance(this).startPublicChatPollingIfNeeded(); + ApplicationContext application = ApplicationContext.getInstance(this); + application.createGroupChatsIfNeeded(); + application.createRSSFeedsIfNeeded(); + application.startGroupChatPollersIfNeeded(); + application.startRSSFeedPollersIfNeeded(); } } diff --git a/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt new file mode 100644 index 0000000000..831943eec9 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt @@ -0,0 +1,71 @@ +package org.thoughtcrime.securesms.loki + +import android.content.Context +import android.os.Handler +import android.util.Log +import com.prof.rssparser.Parser +import kotlinx.coroutines.* +import org.thoughtcrime.securesms.jobs.PushDecryptJob +import org.whispersystems.libsignal.util.guava.Optional +import org.whispersystems.signalservice.api.messages.SignalServiceContent +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage +import org.whispersystems.signalservice.api.messages.SignalServiceGroup +import org.whispersystems.signalservice.api.push.SignalServiceAddress +import org.whispersystems.signalservice.loki.api.LokiRSSFeed +import java.text.SimpleDateFormat + +class LokiRSSFeedPoller(private val context: Context, private val feed: LokiRSSFeed) { + private val handler = Handler() + private val job = Job() + private var hasStarted = false + + private val task = object : Runnable { + + override fun run() { + poll() + handler.postDelayed(this, interval) + } + } + + companion object { + private val interval: Long = 8 * 60 * 1000 + } + + fun startIfNeeded() { + if (hasStarted) return + task.run() + hasStarted = true + } + + fun stop() { + handler.removeCallbacks(task) + job.cancel() + hasStarted = false + } + + private fun poll() { + CoroutineScope(Dispatchers.Main).launch { + try { + val url = feed.url + val parser = Parser() + val items = parser.getArticles(url) + items.reversed().forEach { item -> + val title = item.title ?: return@forEach + val description = item.description ?: return@forEach + val dateAsString = item.pubDate ?: return@forEach + val formatter = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z") // e.g. Tue, 27 Aug 2019 03:52:05 +0000 + val date = formatter.parse(dateAsString) + val timestamp = date.time + val body = "$title
$description" + val id = feed.id.toByteArray() + val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null) + val x2 = SignalServiceDataMessage(timestamp, x1, null, body) + val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false) + PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent()) + } + } catch (exception: Exception) { + Log.d("Loki", "Couldn't update RSS feed with ID: $feed.id.") + } + } + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 31e1908c31..f251a9824a 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -1162,12 +1162,12 @@ public class TextSecurePreferences { setLongPreference(context, "background_poll_time", backgroundPollTime); } - public static boolean isPublicChatSetUp(Context context) { - return getBooleanPreference(context, "is_public_chat_set_up", false); + public static boolean isChatSetUp(Context context, String id) { + return getBooleanPreference(context, "is_chat_set_up" + "?chat=" + id, false); } - public static void markPublicChatSetUp(Context context) { - setBooleanPreference(context, "is_public_chat_set_up", true); + public static void markChatSetUp(Context context, String id) { + setBooleanPreference(context, "is_chat_set_up" + "?chat=" + id, true); } // endregion }