Do not enqueue no-op read receipt jobs.

This commit is contained in:
Cody Henthorne 2024-08-16 15:39:33 -04:00 committed by mtang-signal
parent cda029cd93
commit 4c9b5926b9
5 changed files with 313 additions and 362 deletions

View file

@ -98,6 +98,10 @@ public class SendReadReceiptJob extends BaseJob {
* maximum size. * maximum size.
*/ */
public static void enqueue(long threadId, @NonNull RecipientId recipientId, List<MarkedMessageInfo> markedMessageInfos) { public static void enqueue(long threadId, @NonNull RecipientId recipientId, List<MarkedMessageInfo> markedMessageInfos) {
if (!TextSecurePreferences.isReadReceiptsEnabled(AppDependencies.getApplication())) {
return;
}
if (recipientId.equals(Recipient.self().getId())) { if (recipientId.equals(Recipient.self().getId())) {
return; return;
} }

View file

@ -1,247 +0,0 @@
package org.thoughtcrime.securesms.dependencies;
import androidx.annotation.NonNull;
import org.signal.core.util.concurrent.DeadlockDetector;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
import org.thoughtcrime.securesms.billing.GooglePlayBillingApi;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.PendingRetryReceiptCache;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.payments.Payments;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
import org.thoughtcrime.securesms.service.DeletedCallEventManager;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.ExpiringStoriesManager;
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
import org.thoughtcrime.securesms.service.ScheduledMessageManager;
import org.thoughtcrime.securesms.service.TrimThreadsByDateManager;
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
import org.thoughtcrime.securesms.shakereport.ShakeToReport;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.EarlyMessageCache;
import org.thoughtcrime.securesms.util.FrameRateTracker;
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.services.CallLinksService;
import org.whispersystems.signalservice.api.services.DonationsService;
import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import java.util.function.Supplier;
import static org.mockito.Mockito.mock;
@SuppressWarnings("ConstantConditions")
public class MockApplicationDependencyProvider implements AppDependencies.Provider {
@Override
public @NonNull GroupsV2Operations provideGroupsV2Operations(@NonNull SignalServiceConfiguration signalServiceConfiguration) {
return null;
}
@Override
public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) {
return null;
}
@Override
public @NonNull SignalServiceMessageSender provideSignalServiceMessageSender(@NonNull SignalWebSocket signalWebSocket, @NonNull SignalServiceDataStore protocolStore, @NonNull SignalServiceConfiguration signalServiceConfiguration) {
return null;
}
@Override
public @NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver(@NonNull SignalServiceConfiguration signalServiceConfiguration) {
return null;
}
@Override
public @NonNull SignalServiceNetworkAccess provideSignalServiceNetworkAccess() {
return null;
}
@Override
public @NonNull LiveRecipientCache provideRecipientCache() {
return null;
}
@Override
public @NonNull JobManager provideJobManager() {
return null;
}
@Override
public @NonNull FrameRateTracker provideFrameRateTracker() {
return null;
}
@Override
public @NonNull MegaphoneRepository provideMegaphoneRepository() {
return null;
}
@Override
public @NonNull EarlyMessageCache provideEarlyMessageCache() {
return null;
}
@Override
public @NonNull MessageNotifier provideMessageNotifier() {
return null;
}
@Override
public @NonNull IncomingMessageObserver provideIncomingMessageObserver() {
return null;
}
@Override
public @NonNull TrimThreadsByDateManager provideTrimThreadsByDateManager() {
return null;
}
@Override
public @NonNull ViewOnceMessageManager provideViewOnceMessageManager() {
return null;
}
@Override
public @NonNull ExpiringStoriesManager provideExpiringStoriesManager() {
return null;
}
@Override
public @NonNull ExpiringMessageManager provideExpiringMessageManager() {
return null;
}
@Override
public @NonNull DeletedCallEventManager provideDeletedCallEventManager() {
return null;
}
@Override
public @NonNull TypingStatusRepository provideTypingStatusRepository() {
return null;
}
@Override
public @NonNull TypingStatusSender provideTypingStatusSender() {
return null;
}
@Override
public @NonNull DatabaseObserver provideDatabaseObserver() {
return mock(DatabaseObserver.class);
}
@Override
public @NonNull Payments providePayments(@NonNull SignalServiceAccountManager signalServiceAccountManager) {
return null;
}
@Override
public @NonNull ShakeToReport provideShakeToReport() {
return null;
}
@Override
public @NonNull AppForegroundObserver provideAppForegroundObserver() {
return mock(AppForegroundObserver.class);
}
@Override
public @NonNull SignalCallManager provideSignalCallManager() {
return null;
}
@Override
public @NonNull PendingRetryReceiptManager providePendingRetryReceiptManager() {
return null;
}
@Override
public @NonNull PendingRetryReceiptCache providePendingRetryReceiptCache() {
return null;
}
@Override
public @NonNull SignalWebSocket provideSignalWebSocket(@NonNull Supplier<SignalServiceConfiguration> signalServiceConfigurationSupplier, @NonNull Supplier<Network> libSignalNetworkSupplier) {
return null;
}
@Override
public @NonNull SignalServiceDataStoreImpl provideProtocolStore() {
return null;
}
@Override
public @NonNull GiphyMp4Cache provideGiphyMp4Cache() {
return null;
}
@Override
public @NonNull SimpleExoPlayerPool provideExoPlayerPool() {
return null;
}
@Override
public @NonNull AudioManagerCompat provideAndroidCallAudioManager() {
return null;
}
@Override
public @NonNull DonationsService provideDonationsService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) {
return null;
}
@NonNull @Override public CallLinksService provideCallLinksService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) {
return null;
}
@Override
public @NonNull ProfileService provideProfileService(@NonNull ClientZkProfileOperations profileOperations, @NonNull SignalServiceMessageReceiver signalServiceMessageReceiver, @NonNull SignalWebSocket signalWebSocket) {
return null;
}
@Override
public @NonNull DeadlockDetector provideDeadlockDetector() {
return null;
}
@Override
public @NonNull ClientZkReceiptOperations provideClientZkReceiptOperations(@NonNull SignalServiceConfiguration signalServiceConfiguration) {
return null;
}
@Override
public @NonNull ScheduledMessageManager provideScheduledMessageManager() {
return null;
}
@Override
public @NonNull Network provideLibsignalNetwork(@NonNull SignalServiceConfiguration config) {
return null;
}
@Override
public @NonNull GooglePlayBillingApi provideBillingApi() {
return null;
}
}

View file

@ -0,0 +1,205 @@
package org.thoughtcrime.securesms.dependencies
import io.mockk.mockk
import org.mockito.Mockito
import org.signal.core.util.concurrent.DeadlockDetector
import org.signal.libsignal.net.Network
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
import org.thoughtcrime.securesms.billing.GooglePlayBillingApi
import org.thoughtcrime.securesms.components.TypingStatusRepository
import org.thoughtcrime.securesms.components.TypingStatusSender
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.PendingRetryReceiptCache
import org.thoughtcrime.securesms.jobmanager.JobManager
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository
import org.thoughtcrime.securesms.messages.IncomingMessageObserver
import org.thoughtcrime.securesms.notifications.MessageNotifier
import org.thoughtcrime.securesms.payments.Payments
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
import org.thoughtcrime.securesms.recipients.LiveRecipientCache
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager
import org.thoughtcrime.securesms.service.DeletedCallEventManager
import org.thoughtcrime.securesms.service.ExpiringMessageManager
import org.thoughtcrime.securesms.service.ExpiringStoriesManager
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager
import org.thoughtcrime.securesms.service.ScheduledMessageManager
import org.thoughtcrime.securesms.service.TrimThreadsByDateManager
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager
import org.thoughtcrime.securesms.shakereport.ShakeToReport
import org.thoughtcrime.securesms.util.AppForegroundObserver
import org.thoughtcrime.securesms.util.EarlyMessageCache
import org.thoughtcrime.securesms.util.FrameRateTracker
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.SignalServiceDataStore
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver
import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.SignalWebSocket
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
import org.whispersystems.signalservice.api.services.CallLinksService
import org.whispersystems.signalservice.api.services.DonationsService
import org.whispersystems.signalservice.api.services.ProfileService
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import java.util.function.Supplier
class MockApplicationDependencyProvider : AppDependencies.Provider {
override fun provideGroupsV2Operations(signalServiceConfiguration: SignalServiceConfiguration): GroupsV2Operations {
return mockk()
}
override fun provideSignalServiceAccountManager(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): SignalServiceAccountManager {
return mockk()
}
override fun provideSignalServiceMessageSender(signalWebSocket: SignalWebSocket, protocolStore: SignalServiceDataStore, signalServiceConfiguration: SignalServiceConfiguration): SignalServiceMessageSender {
return mockk()
}
override fun provideSignalServiceMessageReceiver(signalServiceConfiguration: SignalServiceConfiguration): SignalServiceMessageReceiver {
return mockk()
}
override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess {
return mockk()
}
override fun provideRecipientCache(): LiveRecipientCache {
return mockk()
}
override fun provideJobManager(): JobManager {
return mockk()
}
override fun provideFrameRateTracker(): FrameRateTracker {
return mockk()
}
override fun provideMegaphoneRepository(): MegaphoneRepository {
return mockk()
}
override fun provideEarlyMessageCache(): EarlyMessageCache {
return mockk()
}
override fun provideMessageNotifier(): MessageNotifier {
return mockk()
}
override fun provideIncomingMessageObserver(): IncomingMessageObserver {
return mockk()
}
override fun provideTrimThreadsByDateManager(): TrimThreadsByDateManager {
return mockk()
}
override fun provideViewOnceMessageManager(): ViewOnceMessageManager {
return mockk()
}
override fun provideExpiringStoriesManager(): ExpiringStoriesManager {
return mockk()
}
override fun provideExpiringMessageManager(): ExpiringMessageManager {
return mockk()
}
override fun provideDeletedCallEventManager(): DeletedCallEventManager {
return mockk()
}
override fun provideTypingStatusRepository(): TypingStatusRepository {
return mockk()
}
override fun provideTypingStatusSender(): TypingStatusSender {
return mockk()
}
override fun provideDatabaseObserver(): DatabaseObserver {
return Mockito.mock(DatabaseObserver::class.java)
}
override fun providePayments(signalServiceAccountManager: SignalServiceAccountManager): Payments {
return mockk()
}
override fun provideShakeToReport(): ShakeToReport {
return mockk()
}
override fun provideAppForegroundObserver(): AppForegroundObserver {
return Mockito.mock(AppForegroundObserver::class.java)
}
override fun provideSignalCallManager(): SignalCallManager {
return mockk()
}
override fun providePendingRetryReceiptManager(): PendingRetryReceiptManager {
return mockk()
}
override fun providePendingRetryReceiptCache(): PendingRetryReceiptCache {
return mockk()
}
override fun provideSignalWebSocket(signalServiceConfigurationSupplier: Supplier<SignalServiceConfiguration>, libSignalNetworkSupplier: Supplier<Network>): SignalWebSocket {
return mockk()
}
override fun provideProtocolStore(): SignalServiceDataStoreImpl {
return mockk()
}
override fun provideGiphyMp4Cache(): GiphyMp4Cache {
return mockk()
}
override fun provideExoPlayerPool(): SimpleExoPlayerPool {
return mockk()
}
override fun provideAndroidCallAudioManager(): AudioManagerCompat {
return mockk()
}
override fun provideDonationsService(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): DonationsService {
return mockk()
}
override fun provideCallLinksService(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): CallLinksService {
return mockk()
}
override fun provideProfileService(profileOperations: ClientZkProfileOperations, signalServiceMessageReceiver: SignalServiceMessageReceiver, signalWebSocket: SignalWebSocket): ProfileService {
return mockk()
}
override fun provideDeadlockDetector(): DeadlockDetector {
return mockk()
}
override fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations {
return mockk()
}
override fun provideScheduledMessageManager(): ScheduledMessageManager {
return mockk()
}
override fun provideLibsignalNetwork(config: SignalServiceConfiguration): Network {
return mockk()
}
override fun provideBillingApi(): GooglePlayBillingApi {
return mockk()
}
}

View file

@ -1,115 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.content.Context;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.stubbing.Answer;
import org.signal.libsignal.protocol.util.Pair;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Util;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MarkReadReceiverTest {
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Mock
private MockedStatic<AppDependencies> applicationDependenciesMockedStatic;
@Mock
private MockedStatic<Recipient> recipientMockedStatic;
private final Context mockContext = mock(Context.class);
private final JobManager mockJobManager = mock(JobManager.class);
private final Recipient mockSelf = mock(Recipient.class);
private final List<Job> jobs = new LinkedList<>();
@Before
public void setUp() {
applicationDependenciesMockedStatic.when(AppDependencies::getJobManager).thenReturn(mockJobManager);
doAnswer((Answer<Void>) invocation -> {
jobs.add((Job) invocation.getArguments()[0]);
return null;
}).when(mockJobManager).add(any());
recipientMockedStatic.when(Recipient::self).thenReturn(mockSelf);
when(mockSelf.getId()).thenReturn(RecipientId.from(-1));
}
@Test
public void givenMultipleThreadsWithMultipleMessagesEach_whenIProcess_thenIProperlyGroupByThreadAndRecipient() {
// GIVEN
List<RecipientId> recipients = Stream.range(1L, 4L).map(RecipientId::from).toList();
List<Long> threads = Stream.range(4L, 7L).toList();
int expected = recipients.size() * threads.size() + 1;
List<MessageTable.MarkedMessageInfo> infoList = Stream.of(threads)
.flatMap(threadId -> Stream.of(recipients)
.map(recipientId -> createMarkedMessageInfo(threadId, recipientId)))
.toList();
List<MessageTable.MarkedMessageInfo> duplicatedList = Util.concatenatedList(infoList, infoList);
// WHEN
MarkReadReceiver.process(duplicatedList);
// THEN
assertEquals("Should have 10 total jobs, including MultiDeviceReadUpdateJob", expected, jobs.size());
Set<Pair<Long, String>> threadRecipientPairs = new HashSet<>();
Stream.of(jobs).forEach(job -> {
if (job instanceof MultiDeviceReadUpdateJob) {
return;
}
JsonJobData data = JsonJobData.deserialize(job.serialize());
long threadId = data.getLong("thread");
String recipientId = data.getString("recipient");
long[] messageIds = data.getLongArray("message_ids");
assertEquals("Each job should contain two messages.", 2, messageIds.length);
assertTrue("Each thread recipient pair should only exist once.", threadRecipientPairs.add(new Pair<>(threadId, recipientId)));
});
assertEquals("Should have 9 total combinations.", 9, threadRecipientPairs.size());
}
private MessageTable.MarkedMessageInfo createMarkedMessageInfo(long threadId, @NonNull RecipientId recipientId) {
return new MessageTable.MarkedMessageInfo(threadId,
new MessageTable.SyncMessageId(recipientId, 0),
new MessageId(1),
new MessageTable.ExpirationInfo(0, 0, 0, false),
StoryType.NONE);
}
}

View file

@ -0,0 +1,104 @@
package org.thoughtcrime.securesms.notifications
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.libsignal.protocol.util.Pair
import org.thoughtcrime.securesms.database.MessageTable.ExpirationInfo
import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo
import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.MockApplicationDependencyProvider
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JobManager
import org.thoughtcrime.securesms.jobmanager.JsonJobData
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.TextSecurePreferences
import java.util.LinkedList
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class MarkReadReceiverTest {
private val jobs: MutableList<Job> = LinkedList()
@Before
fun setUp() {
if (!AppDependencies.isInitialized) {
AppDependencies.init(
ApplicationProvider.getApplicationContext(),
MockApplicationDependencyProvider()
)
}
val jobManager: JobManager = AppDependencies.jobManager
every { jobManager.add(capture(jobs)) } returns Unit
mockkObject(Recipient)
every { Recipient.self() } returns Recipient()
mockkStatic(TextSecurePreferences::class)
every { TextSecurePreferences.isReadReceiptsEnabled(any()) } returns true
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun givenMultipleThreadsWithMultipleMessagesEach_whenIProcess_thenIProperlyGroupByThreadAndRecipient() {
// GIVEN
val recipients = (1L until 4L).map { id -> RecipientId.from(id) }
val threads = (4L until 7L).toList()
val expected = recipients.size * threads.size + 1
val infoList = threads.map { threadId -> recipients.map { recipientId -> createMarkedMessageInfo(threadId, recipientId) } }.flatten()
// WHEN
MarkReadReceiver.process(infoList + infoList)
// THEN
Assert.assertEquals("Should have 10 total jobs, including MultiDeviceReadUpdateJob", expected.toLong(), jobs.size.toLong())
val threadRecipientPairs: MutableSet<Pair<Long, String>> = HashSet()
jobs.forEach { job ->
if (job is MultiDeviceReadUpdateJob) {
return@forEach
}
val data = JsonJobData.deserialize(job.serialize())
val threadId = data.getLong("thread")
val recipientId = data.getString("recipient")
val messageIds = data.getLongArray("message_ids")
Assert.assertEquals("Each job should contain two messages.", 2, messageIds.size.toLong())
Assert.assertTrue("Each thread recipient pair should only exist once.", threadRecipientPairs.add(Pair(threadId, recipientId)))
}
Assert.assertEquals("Should have 9 total combinations.", 9, threadRecipientPairs.size.toLong())
}
private fun createMarkedMessageInfo(threadId: Long, recipientId: RecipientId): MarkedMessageInfo {
return MarkedMessageInfo(
threadId,
SyncMessageId(recipientId, 0),
MessageId(1),
ExpirationInfo(0, 0, 0, false),
StoryType.NONE
)
}
}