🔀 Merge “feature/notification-handler” into “next”
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 9 Feb 2020 10:42:34 +0000 (11:42 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 9 Feb 2020 10:42:34 +0000 (11:42 +0100)
86 files changed:
build.gradle
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneFoundEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt
src/main/kotlin/net/pterodactylus/sone/freenet/FreenetURIs.kt
src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt
src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/text/SoneTextParser.kt
src/main/kotlin/net/pterodactylus/sone/utils/Booleans.kt
src/main/kotlin/net/pterodactylus/sone/utils/Freenet.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt
src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt
src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.kt
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt [new file with mode: 0644]
src/main/resources/i18n/sone.en.properties
src/main/resources/i18n/sone.es.properties
src/main/resources/i18n/sone.fr.properties
src/main/resources/i18n/sone.ja.properties
src/main/resources/i18n/sone.no.properties
src/main/resources/i18n/sone.pl.properties
src/main/resources/i18n/sone.ru.properties
src/test/kotlin/net/pterodactylus/sone/core/FreenetInterfaceTest.kt
src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/main/FreenetModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/main/SoneModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/main/SonePluginTest.kt
src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/Guice.kt
src/test/kotlin/net/pterodactylus/sone/test/Matchers.kt
src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/NotParallel.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/BooleansTest.kt
src/test/kotlin/net/pterodactylus/sone/web/PageToadletRegistryTest.kt
src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTester.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandlerTest.kt
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt [new file with mode: 0644]

index 78c3110..e5eaff7 100644 (file)
@@ -57,8 +57,23 @@ dependencies {
 
 apply from: 'version.gradle'
 
-test {
+task parallelTest(type: Test) {
     maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
+    useJUnit {
+        excludeCategories 'net.pterodactylus.sone.test.NotParallel'
+    }
+}
+
+task notParallelTest(type: Test) {
+    maxParallelForks = 1
+    useJUnit {
+        includeCategories 'net.pterodactylus.sone.test.NotParallel'
+    }
+}
+
+test {
+    exclude '**'
+    dependsOn parallelTest, notParallelTest
 }
 
 task fatJar(type: Jar) {
index a3cb385..d3dd3bd 100644 (file)
@@ -22,11 +22,15 @@ import static java.util.logging.Logger.*;
 import java.util.logging.Logger;
 import java.util.logging.*;
 
+import javax.annotation.Nonnull;
+
 import net.pterodactylus.sone.core.*;
+import net.pterodactylus.sone.core.event.*;
 import net.pterodactylus.sone.fcp.*;
 import net.pterodactylus.sone.freenet.wot.*;
 import net.pterodactylus.sone.web.*;
 import net.pterodactylus.sone.web.notification.NotificationHandler;
+import net.pterodactylus.sone.web.notification.NotificationHandlerModule;
 
 import freenet.l10n.BaseL10n.*;
 import freenet.l10n.*;
@@ -58,7 +62,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
                        private final LoadingCache<String, Class<?>> classCache = CacheBuilder.newBuilder()
                                        .build(new CacheLoader<String, Class<?>>() {
                                                @Override
-                                               public Class<?> load(String key) throws Exception {
+                                               public Class<?> load(@Nonnull String key) throws Exception {
                                                        return SonePlugin.class.getClassLoader().loadClass(key);
                                                }
                                        });
@@ -106,6 +110,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        /** The core. */
        private Core core;
 
+       /** The event bus. */
+       private EventBus eventBus;
+
        /** The web interface. */
        private WebInterface webInterface;
 
@@ -197,16 +204,33 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
 
                /* create the web interface. */
                webInterface = injector.getInstance(WebInterface.class);
-               NotificationHandler notificationHandler = injector.getInstance(NotificationHandler.class);
+
+               /* we need to request this to install all notification handlers. */
+               injector.getInstance(NotificationHandler.class);
+
+               /* and this is required to shutdown all tickers. */
+               injector.getInstance(TickerShutdown.class);
 
                /* start core! */
                core.start();
 
                /* start the web interface! */
                webInterface.start();
-               webInterface.setFirstStart(injector.getInstance(Key.get(Boolean.class, Names.named("FirstStart"))));
-               webInterface.setNewConfig(injector.getInstance(Key.get(Boolean.class, Names.named("NewConfig"))));
-               notificationHandler.start();
+
+               /* send some events on startup */
+               eventBus = injector.getInstance(EventBus.class);
+
+               /* first start? */
+               if (injector.getInstance(Key.get(Boolean.class, Names.named("FirstStart")))) {
+                       eventBus.post(new FirstStart());
+               } else {
+                       /* new config? */
+                       if (injector.getInstance(Key.get(Boolean.class, Names.named("NewConfig")))) {
+                               eventBus.post(new ConfigNotRead());
+                       }
+               }
+
+               eventBus.post(new Startup());
        }
 
        @VisibleForTesting
@@ -214,8 +238,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
                FreenetModule freenetModule = new FreenetModule(pluginRespirator);
                AbstractModule soneModule = new SoneModule(this, new EventBus());
                Module webInterfaceModule = new WebInterfaceModule();
+               Module notificationHandlerModule = new NotificationHandlerModule();
 
-               return createInjector(freenetModule, soneModule, webInterfaceModule);
+               return createInjector(freenetModule, soneModule, webInterfaceModule, notificationHandlerModule);
        }
 
        @VisibleForTesting
@@ -228,6 +253,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
         */
        @Override
        public void terminate() {
+               /* send shutdown event. */
+               eventBus.post(new Shutdown());
+
                try {
                        /* stop the web interface. */
                        webInterface.stop();
index bcc7a11..edc3bab 100644 (file)
@@ -21,25 +21,17 @@ import static com.google.common.collect.FluentIterable.from;
 import static java.util.logging.Logger.getLogger;
 
 import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.UUID;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
 import java.util.logging.Logger;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.inject.Named;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.core.ElementLoader;
 import net.pterodactylus.sone.core.event.*;
-import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
 import net.pterodactylus.sone.data.Sone;
@@ -58,9 +50,6 @@ import net.pterodactylus.sone.template.LinkedElementRenderFilter;
 import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.RenderFilter;
 import net.pterodactylus.sone.template.ShortenFilter;
-import net.pterodactylus.sone.text.Part;
-import net.pterodactylus.sone.text.SonePart;
-import net.pterodactylus.sone.text.SoneTextParser;
 import net.pterodactylus.sone.text.TimeTextConverter;
 import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
@@ -94,7 +83,6 @@ import net.pterodactylus.sone.web.page.TemplateRenderer;
 import net.pterodactylus.sone.web.pages.*;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.NotificationManager;
-import net.pterodactylus.util.notify.TemplateNotification;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContextFactory;
 import net.pterodactylus.util.web.RedirectPage;
@@ -106,7 +94,6 @@ import freenet.clients.http.ToadletContext;
 
 import com.codahale.metrics.*;
 import com.google.common.base.Optional;
-import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.eventbus.Subscribe;
 import com.google.inject.Inject;
@@ -136,9 +123,6 @@ public class WebInterface implements SessionProvider {
        private final TemplateContextFactory templateContextFactory;
        private final TemplateRenderer templateRenderer;
 
-       /** The Sone text parser. */
-       private final SoneTextParser soneTextParser;
-
        /** The parser filter. */
        private final ParserFilter parserFilter;
        private final ShortenFilter shortenFilter;
@@ -157,9 +141,6 @@ public class WebInterface implements SessionProvider {
        private final MetricRegistry metricRegistry;
        private final Translation translation;
 
-       /** The “new Sone” notification. */
-       private final ListNotification<Sone> newSoneNotification;
-
        /** The “new post” notification. */
        private final ListNotification<Post> newPostNotification;
 
@@ -172,33 +153,6 @@ public class WebInterface implements SessionProvider {
        /** The invisible “local reply” notification. */
        private final ListNotification<PostReply> localReplyNotification;
 
-       /** The “you have been mentioned” notification. */
-       private final ListNotification<Post> mentionNotification;
-
-       /** Notifications for sone inserts. */
-       private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<>();
-
-       /** Sone locked notification ticker objects. */
-       private final Map<Sone, ScheduledFuture<?>> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, ScheduledFuture<?>>());
-
-       /** The “Sone locked” notification. */
-       private final ListNotification<Sone> lockedSonesNotification;
-
-       /** The “new version” notification. */
-       private final TemplateNotification newVersionNotification;
-
-       /** The “inserting images” notification. */
-       private final ListNotification<Image> insertingImagesNotification;
-
-       /** The “inserted images” notification. */
-       private final ListNotification<Image> insertedImagesNotification;
-
-       /** The “image insert failed” notification. */
-       private final ListNotification<Image> imageInsertFailedNotification;
-
-       /** Scheduled executor for time-based notifications. */
-       private final ScheduledExecutorService ticker = Executors.newScheduledThreadPool(1);
-
        @Inject
        public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter,
                        PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter,
@@ -208,7 +162,10 @@ public class WebInterface implements SessionProvider {
                        RenderFilter renderFilter,
                        LinkedElementRenderFilter linkedElementRenderFilter,
                        PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter,
-                       NotificationManager notificationManager) {
+                       NotificationManager notificationManager, @Named("newRemotePost") ListNotification<Post> newPostNotification,
+                       @Named("newRemotePostReply") ListNotification<PostReply> newReplyNotification,
+                       @Named("localPost") ListNotification<Post> localPostNotification,
+                       @Named("localReply") ListNotification<PostReply> localReplyNotification) {
                this.sonePlugin = sonePlugin;
                this.loaders = loaders;
                this.listNotificationFilter = listNotificationFilter;
@@ -225,46 +182,15 @@ public class WebInterface implements SessionProvider {
                this.l10nFilter = l10nFilter;
                this.translation = translation;
                this.notificationManager = notificationManager;
+               this.newPostNotification = newPostNotification;
+               this.newReplyNotification = newReplyNotification;
+               this.localPostNotification = localPostNotification;
+               this.localReplyNotification = localReplyNotification;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
-               soneTextParser = new SoneTextParser(getCore(), getCore());
 
                this.templateContextFactory = templateContextFactory;
                templateContextFactory.addTemplateObject("webInterface", this);
                templateContextFactory.addTemplateObject("formPassword", formPassword);
-
-               /* create notifications. */
-               Template newSoneNotificationTemplate = loaders.loadTemplate("/templates/notify/newSoneNotification.html");
-               newSoneNotification = new ListNotification<>("new-sone-notification", "sones", newSoneNotificationTemplate, false);
-
-               Template newPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
-               newPostNotification = new ListNotification<>("new-post-notification", "posts", newPostNotificationTemplate, false);
-
-               Template localPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
-               localPostNotification = new ListNotification<>("local-post-notification", "posts", localPostNotificationTemplate, false);
-
-               Template newReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
-               newReplyNotification = new ListNotification<>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
-
-               Template localReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
-               localReplyNotification = new ListNotification<>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
-
-               Template mentionNotificationTemplate = loaders.loadTemplate("/templates/notify/mentionNotification.html");
-               mentionNotification = new ListNotification<>("mention-notification", "posts", mentionNotificationTemplate, false);
-
-               Template lockedSonesTemplate = loaders.loadTemplate("/templates/notify/lockedSonesNotification.html");
-               lockedSonesNotification = new ListNotification<>("sones-locked-notification", "sones", lockedSonesTemplate);
-
-               Template newVersionTemplate = loaders.loadTemplate("/templates/notify/newVersionNotification.html");
-               newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
-
-               Template insertingImagesTemplate = loaders.loadTemplate("/templates/notify/inserting-images-notification.html");
-               insertingImagesNotification = new ListNotification<>("inserting-images-notification", "images", insertingImagesTemplate);
-
-               Template insertedImagesTemplate = loaders.loadTemplate("/templates/notify/inserted-images-notification.html");
-               insertedImagesNotification = new ListNotification<>("inserted-images-notification", "images", insertedImagesTemplate);
-
-               Template imageInsertFailedTemplate = loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html");
-               imageInsertFailedNotification = new ListNotification<>("image-insert-failed-notification", "images", imageInsertFailedTemplate);
        }
 
        //
@@ -402,16 +328,6 @@ public class WebInterface implements SessionProvider {
                return formPassword;
        }
 
-       /**
-        * Returns the posts that have been announced as new in the
-        * {@link #newPostNotification}.
-        *
-        * @return The new posts
-        */
-       public Set<Post> getNewPosts() {
-               return ImmutableSet.<Post> builder().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).build();
-       }
-
        @Nonnull
        public Collection<Post> getNewPosts(@Nullable Sone currentSone) {
                Set<Post> allNewPosts = ImmutableSet.<Post> builder()
@@ -421,16 +337,6 @@ public class WebInterface implements SessionProvider {
                return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet();
        }
 
-       /**
-        * Returns the replies that have been announced as new in the
-        * {@link #newReplyNotification}.
-        *
-        * @return The new replies
-        */
-       public Set<PostReply> getNewReplies() {
-               return ImmutableSet.<PostReply> builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build();
-       }
-
        @Nonnull
        public Collection<PostReply> getNewReplies(@Nullable Sone currentSone) {
                Set<PostReply> allNewReplies = ImmutableSet.<PostReply>builder()
@@ -440,51 +346,6 @@ public class WebInterface implements SessionProvider {
                return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet();
        }
 
-       /**
-        * Sets whether the current start of the plugin is the first start. It is
-        * considered a first start if the configuration file does not exist.
-        *
-        * @param firstStart
-        *            {@code true} if no configuration file existed when Sone was
-        *            loaded, {@code false} otherwise
-        */
-       public void setFirstStart(boolean firstStart) {
-               if (firstStart) {
-                       Template firstStartNotificationTemplate = loaders.loadTemplate("/templates/notify/firstStartNotification.html");
-                       Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate);
-                       notificationManager.addNotification(firstStartNotification);
-               }
-       }
-
-       /**
-        * Sets whether Sone was started with a fresh configuration file.
-        *
-        * @param newConfig
-        *            {@code true} if Sone was started with a fresh configuration,
-        *            {@code false} if the existing configuration could be read
-        */
-       public void setNewConfig(boolean newConfig) {
-               if (newConfig && !hasFirstStartNotification()) {
-                       Template configNotReadNotificationTemplate = loaders.loadTemplate("/templates/notify/configNotReadNotification.html");
-                       Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate);
-                       notificationManager.addNotification(configNotReadNotification);
-               }
-       }
-
-       //
-       // PRIVATE ACCESSORS
-       //
-
-       /**
-        * Returns whether the first start notification is currently displayed.
-        *
-        * @return {@code true} if the first-start notification is currently
-        *         displayed, {@code false} otherwise
-        */
-       private boolean hasFirstStartNotification() {
-               return notificationManager.getNotification("first-start-notification") != null;
-       }
-
        //
        // ACTIONS
        //
@@ -494,36 +355,6 @@ public class WebInterface implements SessionProvider {
         */
        public void start() {
                registerToadlets();
-
-               /* notification templates. */
-               Template startupNotificationTemplate = loaders.loadTemplate("/templates/notify/startupNotification.html");
-
-               final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate);
-               notificationManager.addNotification(startupNotification);
-
-               ticker.schedule(new Runnable() {
-
-                       @Override
-                       public void run() {
-                               startupNotification.dismiss();
-                       }
-               }, 2, TimeUnit.MINUTES);
-
-               Template wotMissingNotificationTemplate = loaders.loadTemplate("/templates/notify/wotMissingNotification.html");
-               final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate);
-               ticker.scheduleAtFixedRate(new Runnable() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void run() {
-                               if (getCore().getIdentityManager().isConnected()) {
-                                       wotMissingNotification.dismiss();
-                               } else {
-                                       notificationManager.addNotification(wotMissingNotification);
-                               }
-                       }
-
-               }, 15, 15, TimeUnit.SECONDS);
        }
 
        /**
@@ -531,7 +362,6 @@ public class WebInterface implements SessionProvider {
         */
        public void stop() {
                pageToadletRegistry.unregisterToadlets();
-               ticker.shutdownNow();
        }
 
        //
@@ -627,330 +457,6 @@ public class WebInterface implements SessionProvider {
                pageToadletRegistry.registerToadlets();
        }
 
-       /**
-        * Returns all {@link Sone#isLocal() local Sone}s that are referenced by
-        * {@link SonePart}s in the given text (after parsing it using
-        * {@link SoneTextParser}).
-        *
-        * @param text
-        *            The text to parse
-        * @return All mentioned local Sones
-        */
-       private Collection<Sone> getMentionedSones(String text) {
-               /* we need no context to find mentioned Sones. */
-               Set<Sone> mentionedSones = new HashSet<>();
-               for (Part part : soneTextParser.parse(text, null)) {
-                       if (part instanceof SonePart) {
-                               mentionedSones.add(((SonePart) part).getSone());
-                       }
-               }
-               return Collections2.filter(mentionedSones, Sone.LOCAL_SONE_FILTER);
-       }
-
-       /**
-        * Returns the Sone insert notification for the given Sone. If no
-        * notification for the given Sone exists, a new notification is created and
-        * cached.
-        *
-        * @param sone
-        *            The Sone to get the insert notification for
-        * @return The Sone insert notification
-        */
-       private TemplateNotification getSoneInsertNotification(Sone sone) {
-               synchronized (soneInsertNotifications) {
-                       TemplateNotification templateNotification = soneInsertNotifications.get(sone);
-                       if (templateNotification == null) {
-                               templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
-                               templateNotification.set("insertSone", sone);
-                               soneInsertNotifications.put(sone, templateNotification);
-                       }
-                       return templateNotification;
-               }
-       }
-
-       private boolean localSoneMentionedInNewPostOrReply(Post post) {
-               if (!post.getSone().isLocal()) {
-                       if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) {
-                               return true;
-                       }
-               }
-               for (PostReply postReply : getCore().getReplies(post.getId())) {
-                       if (postReply.getSone().isLocal()) {
-                               continue;
-                       }
-                       if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) {
-                               return true;
-                       }
-               }
-               return false;
-       }
-
-       //
-       // EVENT HANDLERS
-       //
-
-       /**
-        * Notifies the web interface that a new {@link Sone} was found.
-        *
-        * @param newSoneFoundEvent
-        *            The event
-        */
-       @Subscribe
-       public void newSoneFound(NewSoneFoundEvent newSoneFoundEvent) {
-               newSoneNotification.add(newSoneFoundEvent.getSone());
-               if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(newSoneNotification);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a new {@link Post} was found.
-        *
-        * @param newPostFoundEvent
-        *            The event
-        */
-       @Subscribe
-       public void newPostFound(NewPostFoundEvent newPostFoundEvent) {
-               Post post = newPostFoundEvent.getPost();
-               boolean isLocal = post.getSone().isLocal();
-               if (isLocal) {
-                       localPostNotification.add(post);
-               } else {
-                       newPostNotification.add(post);
-               }
-               if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
-                       if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
-                               mentionNotification.add(post);
-                               notificationManager.addNotification(mentionNotification);
-                       }
-               } else {
-                       getCore().markPostKnown(post);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a new {@link PostReply} was found.
-        *
-        * @param newPostReplyFoundEvent
-        *            The event
-        */
-       @Subscribe
-       public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) {
-               PostReply reply = newPostReplyFoundEvent.getPostReply();
-               boolean isLocal = reply.getSone().isLocal();
-               if (isLocal) {
-                       localReplyNotification.add(reply);
-               } else {
-                       newReplyNotification.add(reply);
-               }
-               if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
-                       if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
-                               mentionNotification.add(reply.getPost().get());
-                               notificationManager.addNotification(mentionNotification);
-                       }
-               } else {
-                       getCore().markReplyKnown(reply);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a {@link Sone} was marked as known.
-        *
-        * @param markSoneKnownEvent
-        *            The event
-        */
-       @Subscribe
-       public void markSoneKnown(MarkSoneKnownEvent markSoneKnownEvent) {
-               newSoneNotification.remove(markSoneKnownEvent.getSone());
-       }
-
-       @Subscribe
-       public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
-               removePost(markPostKnownEvent.getPost());
-       }
-
-       @Subscribe
-       public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) {
-               removeReply(markPostReplyKnownEvent.getPostReply());
-       }
-
-       @Subscribe
-       public void soneRemoved(SoneRemovedEvent soneRemovedEvent) {
-               newSoneNotification.remove(soneRemovedEvent.getSone());
-       }
-
-       @Subscribe
-       public void postRemoved(PostRemovedEvent postRemovedEvent) {
-               removePost(postRemovedEvent.getPost());
-       }
-
-       private void removePost(Post post) {
-               newPostNotification.remove(post);
-               localPostNotification.remove(post);
-               if (!localSoneMentionedInNewPostOrReply(post)) {
-                       mentionNotification.remove(post);
-               }
-       }
-
-       @Subscribe
-       public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) {
-               removeReply(postReplyRemovedEvent.getPostReply());
-       }
-
-       private void removeReply(PostReply reply) {
-               newReplyNotification.remove(reply);
-               localReplyNotification.remove(reply);
-               if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
-                       mentionNotification.remove(reply.getPost().get());
-               }
-       }
-
-       /**
-        * Notifies the web interface that a Sone was locked.
-        *
-        * @param soneLockedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneLocked(SoneLockedEvent soneLockedEvent) {
-               final Sone sone = soneLockedEvent.getSone();
-               ScheduledFuture<?> tickerObject = ticker.schedule(new Runnable() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void run() {
-                               lockedSonesNotification.add(sone);
-                               notificationManager.addNotification(lockedSonesNotification);
-                       }
-               }, 5, TimeUnit.MINUTES);
-               lockedSonesTickerObjects.put(sone, tickerObject);
-       }
-
-       /**
-        * Notifies the web interface that a Sone was unlocked.
-        *
-        * @param soneUnlockedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneUnlocked(SoneUnlockedEvent soneUnlockedEvent) {
-               lockedSonesNotification.remove(soneUnlockedEvent.getSone());
-               lockedSonesTickerObjects.remove(soneUnlockedEvent.getSone()).cancel(false);
-       }
-
-       /**
-        * Notifies the web interface that a {@link Sone} is being inserted.
-        *
-        * @param soneInsertingEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneInserting(SoneInsertingEvent soneInsertingEvent) {
-               TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.getSone());
-               soneInsertNotification.set("soneStatus", "inserting");
-               if (soneInsertingEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
-                       notificationManager.addNotification(soneInsertNotification);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a {@link Sone} was inserted.
-        *
-        * @param soneInsertedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneInserted(SoneInsertedEvent soneInsertedEvent) {
-               TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.getSone());
-               soneInsertNotification.set("soneStatus", "inserted");
-               soneInsertNotification.set("insertDuration", soneInsertedEvent.getInsertDuration() / 1000);
-               if (soneInsertedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
-                       notificationManager.addNotification(soneInsertNotification);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a {@link Sone} insert was aborted.
-        *
-        * @param soneInsertAbortedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) {
-               TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.getSone());
-               soneInsertNotification.set("soneStatus", "insert-aborted");
-               soneInsertNotification.set("insert-error", soneInsertAbortedEvent.getCause());
-               if (soneInsertAbortedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
-                       notificationManager.addNotification(soneInsertNotification);
-               }
-       }
-
-       /**
-        * Notifies the web interface that a new Sone version was found.
-        *
-        * @param updateFoundEvent
-        *            The event
-        */
-       @Subscribe
-       public void updateFound(UpdateFoundEvent updateFoundEvent) {
-               newVersionNotification.set("latestVersion", updateFoundEvent.getVersion());
-               newVersionNotification.set("latestEdition", updateFoundEvent.getLatestEdition());
-               newVersionNotification.set("releaseTime", updateFoundEvent.getReleaseTime());
-               newVersionNotification.set("disruptive", updateFoundEvent.isDisruptive());
-               notificationManager.addNotification(newVersionNotification);
-       }
-
-       /**
-        * Notifies the web interface that an image insert was started
-        *
-        * @param imageInsertStartedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertStarted(ImageInsertStartedEvent imageInsertStartedEvent) {
-               insertingImagesNotification.add(imageInsertStartedEvent.getImage());
-               notificationManager.addNotification(insertingImagesNotification);
-       }
-
-       /**
-        * Notifies the web interface that an {@link Image} insert was aborted.
-        *
-        * @param imageInsertAbortedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertAborted(ImageInsertAbortedEvent imageInsertAbortedEvent) {
-               insertingImagesNotification.remove(imageInsertAbortedEvent.getImage());
-       }
-
-       /**
-        * Notifies the web interface that an {@link Image} insert is finished.
-        *
-        * @param imageInsertFinishedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
-               insertingImagesNotification.remove(imageInsertFinishedEvent.getImage());
-               insertedImagesNotification.add(imageInsertFinishedEvent.getImage());
-               notificationManager.addNotification(insertedImagesNotification);
-       }
-
-       /**
-        * Notifies the web interface that an {@link Image} insert has failed.
-        *
-        * @param imageInsertFailedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertFailed(ImageInsertFailedEvent imageInsertFailedEvent) {
-               insertingImagesNotification.remove(imageInsertFailedEvent.getImage());
-               imageInsertFailedNotification.add(imageInsertFailedEvent.getImage());
-               notificationManager.addNotification(imageInsertFailedNotification);
-       }
-
        @Subscribe
        public void debugActivated(@Nonnull DebugActivatedEvent debugActivatedEvent) {
                pageToadletRegistry.activateDebugMode();
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt
new file mode 100644 (file)
index 0000000..c8f1573
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Sone - ConfigNotRead.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that Sone could not read an existing configuration
+ * successfully, and a new configuration was created. This is different from
+ * [FirstStart] in that `FirstStart` signals that there *was* no existing
+ * configuration to be read.
+ */
+class ConfigNotRead
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt
new file mode 100644 (file)
index 0000000..4ff81f3
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Sone - FirstStart.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that Sone was started for the first time. This event
+ * will only be triggered once, on startup.
+ */
+class FirstStart
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneFoundEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneFoundEvent.kt
new file mode 100644 (file)
index 0000000..b63203b
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Sone - MentionOfLocalSoneFoundEvent.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a new post or reply was found that mentioned a local
+ * Sone, which happens if the [SoneTextParser] locates a [SonePart] in a post
+ * or reply.
+ */
+data class MentionOfLocalSoneFoundEvent(val post: Post)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt
new file mode 100644 (file)
index 0000000..4898de9
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Sone - MentionOfLocalSoneRemovedEvent.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Event that signals that a post or reply that mentioned a local Sone was
+ * removed so that the given post and its replies do not contain a mention of
+ * a local Sone anymore.
+ */
+data class MentionOfLocalSoneRemovedEvent(val post: Post)
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt
new file mode 100644 (file)
index 0000000..4bc2f61
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - Shutdown.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals the shutdown of Sone.
+ */
+class Shutdown
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt
new file mode 100644 (file)
index 0000000..9a9d273
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - Startup.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals the startup of Sone.
+ */
+class Startup
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt
new file mode 100644 (file)
index 0000000..97daff1
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - WebOfTrustAppeared.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that the web of trust is reachable.
+ */
+class WebOfTrustAppeared
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt
new file mode 100644 (file)
index 0000000..63b4a1f
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - WebOfTrustDisappeared.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that the web of trust is not reachable.
+ */
+class WebOfTrustDisappeared
index cc797d7..4cd0d82 100644 (file)
 
 package net.pterodactylus.sone.database
 
+import com.google.inject.*
 import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.database.memory.*
 
 /**
  * Interface for objects that can provide [PostReply]s.
  */
+@ImplementedBy(MemoryDatabase::class)
 interface PostReplyProvider {
 
        fun getPostReply(id: String): PostReply?
index 3a6d43c..6d40b17 100644 (file)
@@ -1,6 +1,6 @@
 package net.pterodactylus.sone.freenet
 
 import freenet.keys.*
-import freenet.support.Base64.*
+import net.pterodactylus.sone.utils.*
 
-val FreenetURI.routingKeyString: String get() = encode(routingKey)
+val FreenetURI.routingKeyString: String get() = routingKey.asFreenetBase64
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt
new file mode 100644 (file)
index 0000000..026389f
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Sone - WebOfTrustPinger.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.freenet.plugin.*
+import net.pterodactylus.sone.utils.*
+import java.util.concurrent.atomic.*
+import java.util.function.*
+import javax.inject.*
+
+/**
+ * [Runnable] that is scheduled via an [Executor][java.util.concurrent.Executor],
+ * checks whether the web of trust plugin can be communicated with, sends
+ * events if its status changes and reschedules itself.
+ */
+class WebOfTrustPinger @Inject constructor(
+               private val eventBus: EventBus,
+               @Named("webOfTrustReacher") private val webOfTrustReacher: Runnable,
+               @Named("webOfTrustReschedule") private val reschedule: Consumer<Runnable>) : Runnable {
+
+       private val lastState = AtomicBoolean(false)
+
+       override fun run() {
+               try {
+                       webOfTrustReacher()
+                       if (!lastState.get()) {
+                               eventBus.post(WebOfTrustAppeared())
+                               lastState.set(true)
+                       }
+               } catch (e: PluginException) {
+                       if (lastState.get()) {
+                               eventBus.post(WebOfTrustDisappeared())
+                               lastState.set(false)
+                       }
+               }
+               reschedule(this)
+       }
+
+}
index 97b342a..749de0d 100644 (file)
@@ -7,15 +7,20 @@ import com.google.inject.*
 import com.google.inject.matcher.*
 import com.google.inject.name.Names.*
 import com.google.inject.spi.*
-import freenet.l10n.*
 import net.pterodactylus.sone.database.*
 import net.pterodactylus.sone.database.memory.*
 import net.pterodactylus.sone.freenet.*
 import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.util.config.*
 import net.pterodactylus.util.config.ConfigurationException
+import net.pterodactylus.util.logging.*
 import net.pterodactylus.util.version.Version
 import java.io.*
+import java.util.concurrent.*
+import java.util.concurrent.Executors.*
+import java.util.logging.*
+import javax.inject.*
+import javax.inject.Singleton
 
 open class SoneModule(private val sonePlugin: SonePlugin, private val eventBus: EventBus) : AbstractModule() {
 
@@ -55,14 +60,26 @@ open class SoneModule(private val sonePlugin: SonePlugin, private val eventBus:
                loaders?.let { bind(Loaders::class.java).toInstance(it) }
                bind(MetricRegistry::class.java).`in`(Singleton::class.java)
                bind(WebOfTrustConnector::class.java).to(PluginWebOfTrustConnector::class.java).`in`(Singleton::class.java)
+               bind(TickerShutdown::class.java).`in`(Singleton::class.java)
 
                bindListener(Matchers.any(), object : TypeListener {
                        override fun <I> hear(typeLiteral: TypeLiteral<I>, typeEncounter: TypeEncounter<I>) {
-                               typeEncounter.register(InjectionListener { injectee -> eventBus.register(injectee) })
+                               typeEncounter.register(InjectionListener { injectee ->
+                                       logger.fine { "Injecting $injectee..." }
+                                       eventBus.register(injectee)
+                               })
                        }
                })
        }
 
+       @Provides
+       @Singleton
+       @Named("notification")
+       fun getNotificationTicker(): ScheduledExecutorService =
+                       newSingleThreadScheduledExecutor()
+
+       private val logger: Logger = Logging.getLogger(javaClass)
+
 }
 
 private fun String.parseVersion(): Version = Version.parse(this)
diff --git a/src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt b/src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt
new file mode 100644 (file)
index 0000000..c95da09
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Sone - TickerShutdown.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.main
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import java.util.concurrent.*
+import javax.inject.*
+
+/**
+ * Wrapper around all [tickers][ScheduledExecutorService] used in Sone,
+ * ensuring proper shutdown.
+ */
+class TickerShutdown @Inject constructor(@Named("notification") private val notificationTicker: ScheduledExecutorService) {
+
+       @Subscribe
+       fun shutdown(@Suppress("UNUSED_PARAMETER") shutdown: Shutdown) {
+               notificationTicker.shutdown()
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt b/src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt
new file mode 100644 (file)
index 0000000..329eedd
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * Sone - Notifications.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.notify
+
+import net.pterodactylus.util.notify.*
+
+/**
+ * Returns whether the notification manager contains a notification with the given ID.
+ */
+operator fun NotificationManager.contains(id: String) =
+               getNotification(id) != null
+
+/**
+ * Returns whether the notification manager currently has a “first start” notification.
+ */
+fun NotificationManager.hasFirstStartNotification() =
+               "first-start-notification" in this
diff --git a/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt b/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt
new file mode 100644 (file)
index 0000000..b688e86
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Sone - SoneMentionDetector.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.database.*
+import net.pterodactylus.sone.utils.*
+import javax.inject.*
+
+/**
+ * Listens to [NewPostFoundEvent]s and [NewPostReplyFoundEvent], parses the
+ * texts and emits a [MentionOfLocalSoneFoundEvent] if a [SoneTextParser]
+ * finds a [SonePart] that points to a local [Sone].
+ */
+class SoneMentionDetector @Inject constructor(private val eventBus: EventBus, private val soneTextParser: SoneTextParser, private val postReplyProvider: PostReplyProvider) {
+
+       @Subscribe
+       fun onNewPost(newPostFoundEvent: NewPostFoundEvent) {
+               newPostFoundEvent.post.let { post ->
+                       post.sone.isLocal.onFalse {
+                               if (post.text.hasLinksToLocalSones()) {
+                                       mentionedPosts += post
+                                       eventBus.post(MentionOfLocalSoneFoundEvent(post))
+                               }
+                       }
+               }
+       }
+
+       @Subscribe
+       fun onNewPostReply(event: NewPostReplyFoundEvent) {
+               event.postReply.let { postReply ->
+                       postReply.sone.isLocal.onFalse {
+                               if (postReply.text.hasLinksToLocalSones()) {
+                                       postReply.post
+                                                       .also { mentionedPosts += it }
+                                                       .let(::MentionOfLocalSoneFoundEvent)
+                                                       ?.also(eventBus::post)
+                               }
+                       }
+               }
+       }
+
+       @Subscribe
+       fun onPostRemoved(event: PostRemovedEvent) {
+               unmentionPost(event.post)
+       }
+
+       @Subscribe
+       fun onPostMarkedKnown(event: MarkPostKnownEvent) {
+               unmentionPost(event.post)
+       }
+
+       @Subscribe
+       fun onReplyRemoved(event: PostReplyRemovedEvent) {
+               event.postReply.post.let {
+                       if ((!it.text.hasLinksToLocalSones() || it.isKnown) && (it.replies.filterNot { it == event.postReply }.none { it.text.hasLinksToLocalSones() && !it.isKnown })) {
+                               unmentionPost(it)
+                       }
+               }
+       }
+
+       private fun unmentionPost(post: Post) {
+               if (post in mentionedPosts) {
+                       eventBus.post(MentionOfLocalSoneRemovedEvent(post))
+                       mentionedPosts -= post
+               }
+       }
+
+       private val mentionedPosts = mutableSetOf<Post>()
+
+       private fun String.hasLinksToLocalSones() = soneTextParser.parse(this, null)
+                       .filterIsInstance<SonePart>()
+                       .any { it.sone.isLocal }
+
+       private val Post.replies get() = postReplyProvider.getReplies(id)
+
+}
index 554c19b..b711ca8 100644 (file)
@@ -7,6 +7,7 @@ import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.database.*
 import net.pterodactylus.sone.text.LinkType.*
 import net.pterodactylus.sone.text.LinkType.USK
+import net.pterodactylus.sone.utils.*
 import org.bitpedia.util.*
 import java.net.*
 import javax.inject.*
@@ -71,7 +72,7 @@ class SoneTextParser @Inject constructor(private val soneProvider: SoneProvider?
                                                        ?.takeIf { (it.size > 1) || ((it.size == 1) && (it.single() != "")) }
                                                        ?.lastOrNull()
                                                        ?: uri.docName
-                                                       ?: "${uri.keyType}@${uri.routingKey.freenetBase64}"
+                                                       ?: "${uri.keyType}@${uri.routingKey.asFreenetBase64}"
                                }.let { FreenetLinkPart(linkWithoutBacklink.removeSuffix("/"), it, trusted = context?.routingKey?.contentEquals(FreenetURI(linkWithoutBacklink).routingKey) == true) }
                        } catch (e: MalformedURLException) {
                                PlainTextPart(linkWithoutBacklink)
@@ -115,7 +116,7 @@ private fun List<Part>.mergeAdjacentPlainTextParts() = fold(emptyList<Part>()) {
 
 private fun List<Part>.removeEmptyPlainTextParts() = filterNot { it == PlainTextPart("") }
 
-private val String.decodedId: String get() = Base64.encode(Base32.decode(this))
+private val String.decodedId: String get() = Base32.decode(this).asFreenetBase64
 private val String.withoutProtocol get() = substring(indexOf("//") + 2)
 private val String.withoutUrlParameters get() = split('?').first()
 
@@ -138,7 +139,7 @@ private val String.withoutMiddlePathComponents
        }
 private val String.withoutTrailingSlash get() = if (endsWith("/")) substring(0, length - 1) else this
 private val SoneTextParserContext.routingKey: ByteArray? get() = postingSone?.routingKey
-private val Sone.routingKey: ByteArray get() = Base64.decode(id)
+private val Sone.routingKey: ByteArray get() = id.fromFreenetBase64
 
 private enum class LinkType(private val scheme: String, private val freenetLink: Boolean) {
 
@@ -199,5 +200,3 @@ private fun isPunctuation(char: Char) = char in punctuationChars
 private val whitespace = Regex("[\\u000a\u0020\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200c\u200d\u202f\u205f\u2060\u2800\u3000]")
 
 private data class NextLink(val position: Int, val linkType: LinkType, val link: String, val remainder: String)
-
-private val ByteArray.freenetBase64 get() = Base64.encode(this)!!
index 49deb87..1d3e097 100644 (file)
@@ -11,6 +11,14 @@ fun <R> Boolean.ifTrue(block: () -> R): R? = if (this) block() else null
 fun <R> Boolean.ifFalse(block: () -> R): R? = if (!this) block() else null
 
 /**
+ * Returns `this` but runs the given block if `this`  is `true`.
+ *
+ * @param block The block to run if `this` is `true`
+ * @return `this`
+ */
+fun Boolean.onTrue(block: () -> Unit): Boolean = also { if (this) block() }
+
+/**
  * Returns `this` but runs the given block if `this`  is `false`.
  *
  * @param block The block to run if `this` is `false`
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Freenet.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Freenet.kt
new file mode 100644 (file)
index 0000000..a98117f
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - Freenet.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.utils
+
+import freenet.support.*
+
+val ByteArray.asFreenetBase64: String get() = Base64.encode(this)
+val String.fromFreenetBase64: ByteArray get() = Base64.decode(this)
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt
new file mode 100644 (file)
index 0000000..99f43b6
--- /dev/null
@@ -0,0 +1,9 @@
+package net.pterodactylus.sone.utils
+
+import java.util.function.*
+
+/** Allows easy invocation of Java Consumers. */
+operator fun <T> Consumer<T>.invoke(t: T) = accept(t)
+
+/** Allows easy invocation of Java Runnables. */
+operator fun Runnable.invoke() = run()
index 63f3fc6..3d87aa7 100644 (file)
@@ -1,6 +1,5 @@
 package net.pterodactylus.sone.web
 
-import com.google.common.eventbus.*
 import com.google.inject.*
 import freenet.support.api.*
 import net.pterodactylus.sone.core.*
@@ -11,7 +10,6 @@ import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.template.*
 import net.pterodactylus.sone.text.*
-import net.pterodactylus.sone.web.notification.*
 import net.pterodactylus.util.notify.*
 import net.pterodactylus.util.template.*
 import javax.inject.*
@@ -134,9 +132,4 @@ class WebInterfaceModule : AbstractModule() {
        fun getNotificationManager() =
                        NotificationManager()
 
-       @Provides
-       @Singleton
-       fun getNotificationHandler(eventBus: EventBus, loaders: Loaders, notificationManager: NotificationManager) =
-                       NotificationHandler(eventBus, loaders, notificationManager)
-
 }
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt
new file mode 100644 (file)
index 0000000..4ad525b
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Sone - ConfigNotReadHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for [ConfigNotRead] events.
+ */
+class ConfigNotReadHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("configNotRead") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun configNotRead(@Suppress("UNUSED_PARAMETER") configNotRead: ConfigNotRead) {
+               notificationManager.addNotification(notification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt
new file mode 100644 (file)
index 0000000..dc9c507
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Sone - FirstStartHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handles the notification shown on first start of Sone.
+ */
+class FirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("firstStart") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun firstStart(@Suppress("UNUSED_PARAMETER") firstStart: FirstStart) {
+               notificationManager.addNotification(notification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt
new file mode 100644 (file)
index 0000000..0d36de6
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Sone - ImageInsertHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Notification handler for the various image-insert-related events.
+ *
+ * @see ImageInsertStartedEvent
+ * @see ImageInsertAbortedEvent
+ * @see ImageInsertFailedEvent
+ * @see ImageInsertFinishedEvent
+ */
+class ImageInsertHandler @Inject constructor(
+               private val notificationManager: NotificationManager,
+               @Named("imageInserting") private val imageInsertingNotification: ListNotification<Image>,
+               @Named("imageFailed") private val imageFailedNotification: ListNotification<Image>,
+               @Named("imageInserted") private val imageInsertedNotification: ListNotification<Image>) {
+
+       @Subscribe
+       fun imageInsertStarted(imageInsertStartedEvent: ImageInsertStartedEvent) {
+               imageInsertingNotification.add(imageInsertStartedEvent.image)
+               notificationManager.addNotification(imageInsertingNotification)
+       }
+
+       @Subscribe
+       fun imageInsertAborted(imageInsertAbortedEvent: ImageInsertAbortedEvent) {
+               imageInsertingNotification.remove(imageInsertAbortedEvent.image)
+       }
+
+       @Subscribe
+       fun imageInsertFailed(imageInsertFailedEvent: ImageInsertFailedEvent) {
+               imageInsertingNotification.remove(imageInsertFailedEvent.image)
+               imageFailedNotification.add(imageInsertFailedEvent.image)
+               notificationManager.addNotification(imageFailedNotification)
+       }
+
+       @Subscribe
+       fun imageInsertFinished(imageInsertFinishedEvent: ImageInsertFinishedEvent) {
+               imageInsertingNotification.remove(imageInsertFinishedEvent.image)
+               imageInsertedNotification.add(imageInsertFinishedEvent.image)
+               notificationManager.addNotification(imageInsertedNotification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt
new file mode 100644 (file)
index 0000000..cd78dd0
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * Sone - NewLocalPostHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for local posts.
+ */
+class LocalPostHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("localPost") private val notification: ListNotification<Post>) {
+
+       @Subscribe
+       fun newPostFound(newPostFoundEvent: NewPostFoundEvent) {
+               newPostFoundEvent.post.onLocal { post ->
+                       notification.add(post)
+                       if (!notificationManager.hasFirstStartNotification()) {
+                               notificationManager.addNotification(notification)
+                       }
+               }
+       }
+
+       @Subscribe
+       fun postRemoved(postRemovedEvent: PostRemovedEvent) {
+               postRemovedEvent.post.onLocal { post ->
+                       notification.remove(post)
+               }
+       }
+
+       @Subscribe
+       fun postMarkedAsKnown(markPostKnownEvent: MarkPostKnownEvent) {
+               markPostKnownEvent.post.onLocal { post ->
+                       notification.remove(post)
+               }
+       }
+
+}
+
+private fun Post.onLocal(action: (Post) -> Unit) =
+               if (sone.isLocal) action(this) else Unit
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandler.kt
new file mode 100644 (file)
index 0000000..424295e
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * Sone - LocalReplyHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for local replies.
+ */
+class LocalReplyHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("localReply") private val notification: ListNotification<PostReply>) {
+
+       @Subscribe
+       fun newReplyFound(event: NewPostReplyFoundEvent) =
+                       event.postReply.onLocal {
+                               notification.add(it)
+                               if (!notificationManager.hasFirstStartNotification()) {
+                                       notificationManager.addNotification(notification)
+                               }
+                       }
+
+       @Subscribe
+       fun replyRemoved(event: PostReplyRemovedEvent) {
+               notification.remove(event.postReply)
+       }
+
+       @Subscribe
+       fun replyMarkedAsKnown(event: MarkPostReplyKnownEvent) {
+               notification.remove(event.postReply)
+       }
+
+}
+
+private fun PostReply.onLocal(action: (PostReply) -> Unit) =
+               if (sone.isLocal) action(this) else Unit
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt
new file mode 100644 (file)
index 0000000..caca76e
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Sone - MarkPostKnownDuringFirstStartHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import java.util.function.*
+import javax.inject.*
+
+/**
+ * Handler that marks a [new][NewPostFoundEvent] [post][Post] as known while
+ * the [notification manager][NotificationManager] shows a [first start notification]
+ * [NotificationManager.hasFirstStartNotification].
+ */
+class MarkPostKnownDuringFirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, private val markPostAsKnown: Consumer<Post>) {
+
+       @Subscribe
+       fun newPostFound(newPostFoundEvent: NewPostFoundEvent) {
+               if (notificationManager.hasFirstStartNotification()) {
+                       markPostAsKnown(newPostFoundEvent.post)
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandler.kt
new file mode 100644 (file)
index 0000000..6a7f083
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Sone - MarkPostReplyKnownDuringFirstStartHandlerTest.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import java.util.function.*
+import javax.inject.*
+
+/**
+ * Handler that marks post replies [as known][net.pterodactylus.sone.core.Core.markReplyKnown]
+ * while the [first start notification][net.pterodactylus.util.notify.NotificationManager.hasFirstStartNotification]
+ * is shown.
+ */
+class MarkPostReplyKnownDuringFirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, private val markAsKnown: Consumer<PostReply>) {
+
+       @Subscribe
+       fun newPostReply(event: NewPostReplyFoundEvent) {
+               if (notificationManager.hasFirstStartNotification()) {
+                       markAsKnown(event.postReply)
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt
new file mode 100644 (file)
index 0000000..6efff85
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Sone - NewRemotePostHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for [NewPostFoundEvent]s that adds the new post to the “new posts” notification and
+ * displays the notification if the “first start” notification is not being shown.
+ */
+class NewRemotePostHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newRemotePost") private val notification: ListNotification<Post>) {
+
+       @Subscribe
+       fun newPostFound(newPostFoundEvent: NewPostFoundEvent) {
+               if (!newPostFoundEvent.post.sone.isLocal) {
+                       notification.add(newPostFoundEvent.post)
+                       if (!notificationManager.hasFirstStartNotification()) {
+                               notificationManager.addNotification(notification)
+                       }
+               }
+       }
+
+       @Subscribe
+       fun postRemoved(event: PostRemovedEvent) {
+               notification.remove(event.post)
+       }
+
+       @Subscribe
+       fun postMarkedKnown(event: MarkPostKnownEvent) {
+               notification.remove(event.post)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt
new file mode 100644 (file)
index 0000000..f47d456
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Sone - NewSoneHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Notification handler for “new Sone discovered” events.
+ */
+class NewSoneHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newSone") private val notification: ListNotification<Sone>) {
+
+       @Subscribe
+       fun newSoneFound(newSoneFoundEvent: NewSoneFoundEvent) {
+               if (!notificationManager.hasFirstStartNotification()) {
+                       notification.add(newSoneFoundEvent.sone)
+                       notificationManager.addNotification(notification)
+               }
+       }
+
+       @Subscribe
+       fun markedSoneKnown(markSoneKnownEvent: MarkSoneKnownEvent) {
+               notification.remove(markSoneKnownEvent.sone)
+       }
+
+       @Subscribe
+       fun soneRemoved(soneRemovedEvent: SoneRemovedEvent) {
+               notification.remove(soneRemovedEvent.sone)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt
new file mode 100644 (file)
index 0000000..95ef0ef
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Sone - NewVersionHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for the “new version” notification.
+ */
+class NewVersionHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newVersion") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun newVersionFound(updateFoundEvent: UpdateFoundEvent) {
+               notification.set("latestVersion", updateFoundEvent.version)
+               notification.set("releaseTime", updateFoundEvent.releaseTime)
+               notification.set("latestEdition", updateFoundEvent.latestEdition)
+               notification.set("disruptive", updateFoundEvent.isDisruptive)
+               notificationManager.addNotification(notification)
+       }
+
+}
index 7f81f9a..28b82c0 100644 (file)
 
 package net.pterodactylus.sone.web.notification
 
-import com.google.common.eventbus.*
-import net.pterodactylus.sone.main.*
-import net.pterodactylus.util.notify.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.text.*
 import javax.inject.*
 
 /**
- * Handler for notifications that can create notifications and register them with an event bus.
+ * Container that causes notification handlers to be created and (more importantly) registered
+ * on creation with the event bus.
  */
-@Suppress("UnstableApiUsage")
-class NotificationHandler @Inject constructor(private val eventBus: EventBus, private val loaders: Loaders, private val notificationManager: NotificationManager) {
-
-       fun start() {
-               SoneLockedOnStartupHandler(notificationManager, loaders.loadTemplate("/templates/notify/soneLockedOnStartupNotification.html"))
-                               .also(eventBus::register)
-       }
-
-}
+@Suppress("UNUSED_PARAMETER")
+class NotificationHandler @Inject constructor(
+               markPostKnownDuringFirstStartHandler: MarkPostKnownDuringFirstStartHandler,
+               markPostReplyKnownDuringFirstStartHandler: MarkPostReplyKnownDuringFirstStartHandler,
+               newSoneHandler: NewSoneHandler,
+               newRemotePostHandler: NewRemotePostHandler,
+               remotePostReplyHandler: RemotePostReplyHandler,
+               soneLockedOnStartupHandler: SoneLockedOnStartupHandler,
+               soneLockedHandler: SoneLockedHandler,
+               localPostHandler: LocalPostHandler,
+               localReplyHandler: LocalReplyHandler,
+               newVersionHandler: NewVersionHandler,
+               imageInsertHandler: ImageInsertHandler,
+               firstStartHandler: FirstStartHandler,
+               configNotReadHandler: ConfigNotReadHandler,
+               startupHandler: StartupHandler,
+               webOfTrustPinger: WebOfTrustPinger,
+               webOfTrustHandler: WebOfTrustHandler,
+               soneMentionDetector: SoneMentionDetector,
+               soneMentionedHandler: SoneMentionedHandler,
+               soneInsertHandler: SoneInsertHandler
+)
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt
new file mode 100644 (file)
index 0000000..bdbbc32
--- /dev/null
@@ -0,0 +1,192 @@
+/**
+ * Sone - NotificationHandlerModuleTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.inject.*
+import com.google.inject.binder.*
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.text.*
+import net.pterodactylus.util.notify.*
+import java.util.concurrent.*
+import java.util.concurrent.TimeUnit.*
+import java.util.function.*
+import javax.inject.*
+import javax.inject.Singleton
+
+/**
+ * Guice module for creating all notification handlers.
+ */
+class NotificationHandlerModule : AbstractModule() {
+
+       override fun configure() {
+               bind(NotificationHandler::class.java).`in`(Singleton::class.java)
+               bind<MarkPostKnownDuringFirstStartHandler>().asSingleton()
+               bind<MarkPostReplyKnownDuringFirstStartHandler>().asSingleton()
+               bind<SoneLockedOnStartupHandler>().asSingleton()
+               bind<NewSoneHandler>().asSingleton()
+               bind<NewRemotePostHandler>().asSingleton()
+               bind<RemotePostReplyHandler>().asSingleton()
+               bind<SoneLockedHandler>().asSingleton()
+               bind<LocalPostHandler>().asSingleton()
+               bind<LocalReplyHandler>().asSingleton()
+               bind<NewVersionHandler>().asSingleton()
+               bind<ImageInsertHandler>().asSingleton()
+               bind<FirstStartHandler>().asSingleton()
+               bind<ConfigNotReadHandler>().asSingleton()
+               bind<StartupHandler>().asSingleton()
+               bind<WebOfTrustHandler>().asSingleton()
+               bind<SoneMentionDetector>().asSingleton()
+               bind<SoneMentionedHandler>().asSingleton()
+               bind<SoneInsertHandler>().asSingleton()
+       }
+
+       @Provides
+       fun getMarkPostKnownHandler(core: Core): Consumer<Post> = Consumer { core.markPostKnown(it) }
+
+       @Provides
+       fun getMarkPostReplyKnownHandler(core: Core): Consumer<PostReply> = Consumer { core.markReplyKnown(it) }
+
+       @Provides
+       @Singleton
+       @Named("soneLockedOnStartup")
+       fun getSoneLockedOnStartupNotification(loaders: Loaders) =
+                       ListNotification<Sone>("sone-locked-on-startup", "sones", loaders.loadTemplate("/templates/notify/soneLockedOnStartupNotification.html"))
+
+       @Provides
+       @Named("newSone")
+       fun getNewSoneNotification(loaders: Loaders) =
+                       ListNotification<Sone>("new-sone-notification", "sones", loaders.loadTemplate("/templates/notify/newSoneNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("newRemotePost")
+       fun getNewPostNotification(loaders: Loaders) =
+                       ListNotification<Post>("new-post-notification", "posts", loaders.loadTemplate("/templates/notify/newPostNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("newRemotePostReply")
+       fun getNewRemotePostReplyNotification(loaders: Loaders) =
+                       ListNotification<PostReply>("new-reply-notification", "replies", loaders.loadTemplate("/templates/notify/newReplyNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("soneLocked")
+       fun getSoneLockedNotification(loaders: Loaders) =
+                       ListNotification<Sone>("sones-locked-notification", "sones", loaders.loadTemplate("/templates/notify/lockedSonesNotification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("localPost")
+       fun getLocalPostNotification(loaders: Loaders) =
+                       ListNotification<Post>("local-post-notification", "posts", loaders.loadTemplate("/templates/notify/newPostNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("localReply")
+       fun getLocalReplyNotification(loaders: Loaders) =
+                       ListNotification<PostReply>("local-reply-notification", "replies", loaders.loadTemplate("/templates/notify/newReplyNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("newVersion")
+       fun getNewVersionNotification(loaders: Loaders) =
+                       TemplateNotification("new-version-notification", loaders.loadTemplate("/templates/notify/newVersionNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("imageInserting")
+       fun getImageInsertingNotification(loaders: Loaders) =
+                       ListNotification<Image>("inserting-images-notification", "images", loaders.loadTemplate("/templates/notify/inserting-images-notification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("imageFailed")
+       fun getImageInsertingFailedNotification(loaders: Loaders) =
+                       ListNotification<Image>("image-insert-failed-notification", "images", loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("imageInserted")
+       fun getImageInsertedNotification(loaders: Loaders) =
+                       ListNotification<Image>("inserted-images-notification", "images", loaders.loadTemplate("/templates/notify/inserted-images-notification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("firstStart")
+       fun getFirstStartNotification(loaders: Loaders) =
+                       TemplateNotification("first-start-notification", loaders.loadTemplate("/templates/notify/firstStartNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("configNotRead")
+       fun getConfigNotReadNotification(loaders: Loaders) =
+                       TemplateNotification("config-not-read-notification", loaders.loadTemplate("/templates/notify/configNotReadNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("startup")
+       fun getStartupNotification(loaders: Loaders) =
+                       TemplateNotification("startup-notification", loaders.loadTemplate("/templates/notify/startupNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("webOfTrust")
+       fun getWebOfTrustNotification(loaders: Loaders) =
+                       TemplateNotification("wot-missing-notification", loaders.loadTemplate("/templates/notify/wotMissingNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("webOfTrustReacher")
+       fun getWebOfTrustReacher(webOfTrustConnector: WebOfTrustConnector): Runnable =
+                       Runnable { webOfTrustConnector.ping() }
+
+       @Provides
+       @Singleton
+       @Named("webOfTrustReschedule")
+       fun getWebOfTrustReschedule(@Named("notification") ticker: ScheduledExecutorService) =
+                       Consumer<Runnable> { ticker.schedule(it, 15, SECONDS) }
+
+       @Provides
+       @Singleton
+       @Named("soneMentioned")
+       fun getSoneMentionedNotification(loaders: Loaders) =
+                       ListNotification<Post>("mention-notification", "posts", loaders.loadTemplate("/templates/notify/mentionNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       fun getSoneNotificationSupplier(loaders: Loaders): SoneInsertNotificationSupplier =
+                       mutableMapOf<Sone, TemplateNotification>()
+                                       .let { cache ->
+                                               { sone ->
+                                                       cache.computeIfAbsent(sone) {
+                                                               loaders.loadTemplate("/templates/notify/soneInsertNotification.html")
+                                                                               .let(::TemplateNotification)
+                                                                               .also { it["insertSone"] = sone }
+                                                       }
+                                               }
+                                       }
+
+       private inline fun <reified T> bind(): AnnotatedBindingBuilder<T> = bind(T::class.java)
+       private fun ScopedBindingBuilder.asSingleton() = `in`(Singleton::class.java)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandler.kt
new file mode 100644 (file)
index 0000000..7dec7c9
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Sone - RemotePostReplyHandler.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for remote replies.
+ */
+class RemotePostReplyHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newRemotePostReply") private val notification: ListNotification<PostReply>) {
+
+       @Subscribe
+       fun newPostReplyFound(event: NewPostReplyFoundEvent) {
+               event.postReply.let { postReply ->
+                       postReply.sone.isLocal.onFalse {
+                               if (!notificationManager.hasFirstStartNotification()) {
+                                       notification.add(event.postReply)
+                                       notificationManager.addNotification(notification)
+                               }
+                       }
+               }
+       }
+
+       @Subscribe
+       fun postReplyRemoved(event: PostReplyRemovedEvent) {
+               notification.remove(event.postReply)
+       }
+
+       @Subscribe
+       fun postReplyMarkedAsKnown(event: MarkPostReplyKnownEvent) {
+               notification.remove(event.postReply)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandler.kt
new file mode 100644 (file)
index 0000000..b12acb4
--- /dev/null
@@ -0,0 +1,58 @@
+/**
+ * Sone - SoneInsertHandler.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for all notifications concerning Sone-insert events.
+ */
+class SoneInsertHandler @Inject constructor(private val notificationManager: NotificationManager, private val soneNotifications: SoneInsertNotificationSupplier) {
+
+       @Subscribe
+       fun soneInserting(event: SoneInsertingEvent) {
+               showNotification(event.sone, "inserting")
+       }
+
+       @Subscribe
+       fun soneInserted(event: SoneInsertedEvent) {
+               showNotification(event.sone, "inserted", "insertDuration" to event.insertDuration / 1000)
+       }
+
+       @Subscribe
+       fun soneInsertAborted(event: SoneInsertAbortedEvent) {
+               showNotification(event.sone, "insert-aborted")
+       }
+
+       private fun showNotification(sone: Sone, status: String, vararg templateVariables: Pair<String, Any>) {
+               if (sone.options.isSoneInsertNotificationEnabled) {
+                       soneNotifications(sone).let { notification ->
+                               notification["soneStatus"] = status
+                               templateVariables.forEach { notification[it.first] = it.second }
+                               notificationManager.addNotification(notification)
+                       }
+               }
+       }
+
+}
+
+typealias SoneInsertNotificationSupplier = (@JvmSuppressWildcards Sone) -> @JvmSuppressWildcards TemplateNotification
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt
new file mode 100644 (file)
index 0000000..2217bad
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Sone - SoneLockedHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import java.util.concurrent.*
+import java.util.concurrent.atomic.*
+import javax.inject.*
+
+/**
+ * Handler for [SoneLockedEvent]s and [SoneUnlockedEvent]s that can schedule notifications after
+ * a certain timeout.
+ */
+class SoneLockedHandler @Inject constructor(
+               private val notificationManager: NotificationManager,
+               @Named("soneLocked") private val notification: ListNotification<Sone>,
+               @Named("notification") private val executor: ScheduledExecutorService) {
+
+       private val future: AtomicReference<ScheduledFuture<*>> = AtomicReference()
+
+       @Subscribe
+       fun soneLocked(soneLockedEvent: SoneLockedEvent) {
+               synchronized(future) {
+                       notification.add(soneLockedEvent.sone)
+                       future.get()?.also(this::cancelPreviousFuture)
+                       future.set(executor.schedule(::showNotification, 5, TimeUnit.MINUTES))
+               }
+       }
+
+       @Subscribe
+       fun soneUnlocked(soneUnlockedEvent: SoneUnlockedEvent) {
+               synchronized(future) {
+                       notification.remove(soneUnlockedEvent.sone)
+                       future.get()?.also(::cancelPreviousFuture)
+               }
+       }
+
+       private fun cancelPreviousFuture(future: ScheduledFuture<*>) {
+               future.cancel(true)
+       }
+
+       private fun showNotification() {
+               notificationManager.addNotification(notification)
+       }
+
+}
index d6ec08f..c6de63f 100644 (file)
@@ -22,15 +22,13 @@ import net.pterodactylus.sone.core.event.*
 import net.pterodactylus.sone.data.*
 import net.pterodactylus.sone.notify.*
 import net.pterodactylus.util.notify.*
-import net.pterodactylus.util.template.*
+import javax.inject.*
 
 /**
  * Handler for [SoneLockedOnStartup][net.pterodactylus.sone.core.event.SoneLockedOnStartup] events
  * that adds the appropriate notification to the [NotificationManager].
  */
-class SoneLockedOnStartupHandler(private val notificationManager: NotificationManager, template: Template) {
-
-       private val notification = ListNotification<Sone>("sone-locked-on-startup", "sones", template)
+class SoneLockedOnStartupHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("soneLockedOnStartup") private val notification: ListNotification<Sone>) {
 
        @Subscribe
        @Suppress("UnstableApiUsage")
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandler.kt
new file mode 100644 (file)
index 0000000..a03e490
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Sone - SoneMentionedHandler.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for the [MentionOfLocalSoneFoundEvent] and
+ * [MentionOfLocalSoneRemovedEvent] events that add the corresponding
+ * notification to the notification manager.
+ */
+class SoneMentionedHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("soneMentioned") private val notification: ListNotification<Post>) {
+
+       @Subscribe
+       fun mentionOfLocalSoneFound(event: MentionOfLocalSoneFoundEvent) {
+               if (!notificationManager.hasFirstStartNotification()) {
+                       notification.add(event.post)
+                       notificationManager.addNotification(notification)
+               }
+       }
+
+       @Subscribe
+       fun mentionOfLocalSoneRemoved(event: MentionOfLocalSoneRemovedEvent) {
+               notification.remove(event.post)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt
new file mode 100644 (file)
index 0000000..4d81576
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Sone - StartupHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import java.util.concurrent.*
+import java.util.concurrent.TimeUnit.*
+import javax.inject.*
+
+/**
+ * Handler for the [Startup] event notification.
+ */
+class StartupHandler @Inject constructor(
+               private val notificationManager: NotificationManager,
+               @Named("startup") private val notification: TemplateNotification,
+               @Named("notification") private val ticker: ScheduledExecutorService) {
+
+       @Subscribe
+       fun startup(@Suppress("UNUSED_PARAMETER") startup: Startup) {
+               notificationManager.addNotification(notification)
+               ticker.schedule({ notificationManager.removeNotification(notification) }, 2, MINUTES)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt
new file mode 100644 (file)
index 0000000..f331643
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Sone - WebOfTrustHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for web of trust-related notifications and the [WebOfTrustAppeared]
+ * and [WebOfTrustDisappeared] events.
+ */
+class WebOfTrustHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("webOfTrust") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun webOfTrustAppeared(@Suppress("UNUSED_PARAMETER") webOfTrustAppeared: WebOfTrustAppeared) {
+               notificationManager.removeNotification(notification)
+       }
+
+       @Subscribe
+       fun webOfTrustDisappeared(@Suppress("UNUSED_PARAMETER") webOfTrustDisappeared: WebOfTrustDisappeared) {
+               notificationManager.addNotification(notification)
+       }
+
+}
index 1c0f2cb..4376025 100644 (file)
@@ -463,4 +463,4 @@ Notification.Mention.Text=You have been mentioned in the following posts:
 Notification.SoneIsInserting.Text=Your Sone sone://{0} is now being inserted.
 Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have automatically been locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
index 42a62d5..7c88daf 100644 (file)
@@ -461,5 +461,5 @@ Notification.Mention.Text=Has sido mencionado en las siguientes publicaciones:
 Notification.SoneIsInserting.Text=Tu Sone sone://{0} está siendo insertado.
 Notification.SoneIsInserted.Text=Tu Sone sone://{0} ha sido insertado en {1,number} {1,choice,0#segundos|1#segundo|1<segundos}.
 Notification.SoneInsertAborted.Text=Tu Sone sone://{0} no pudo ser insertado.
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
 # 55-61, 324–328, 360, 464
index 4e6c361..24858aa 100644 (file)
@@ -462,4 +462,4 @@ Notification.SoneIsInserting.Text=Votre Sone sone://{0} va maintenant être ins
 Notification.SoneIsInserted.Text=votre Sone sone://{0} a été inséré dans {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Votre Sone sone://{0} ne peut pas être inséré.
 Notification.SoneLockedOnStartup.Text=Les versions antérieures à v81 avaient un bug vidant les Sones. Pour éviter d'insérer des Sones vides ils ont été automatiquement vérouillés. Vérifiez vos Sones, utilisez le mode récupération si nécessaire puis dévérouillez vos Sones lorsque vous êtes satisfait du résultat. Les Sones vérouillés sont:
-#
+# 464
index e2509a7..2af11b5 100644 (file)
@@ -461,5 +461,5 @@ Notification.Mention.Text=次の投稿でメンションされています:
 Notification.SoneIsInserting.Text=あなたのSone sone://{0}は現在インサート中です。
 Notification.SoneIsInserted.Text=あなたのSone sone://{0}は{1,number}{1,choice,0#秒|1#秒|1<秒}でインサートされました。
 Notification.SoneInsertAborted.Text=あなたのSone sone://{0}のインサートに失敗しました。
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
 # 55-51, 67, 103, 324–328, 360, 455, 464
index dfb49bb..4289240 100644 (file)
@@ -461,5 +461,5 @@ Notification.Mention.Text=Du har blitt nevnt i følgende innlegg:
 Notification.SoneIsInserting.Text=Your Sone sone://{0} is now being inserted.
 Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
 # 55-61, 67, 103, 123-124, 305-307, 309-311, 324–328, 360, 455, 461-464
index 0ce5740..71a4a85 100644 (file)
@@ -461,5 +461,5 @@ Notification.Mention.Text=Zostałeś oznaczony w następujących postach:
 Notification.SoneIsInserting.Text=Twoje Sone sone://{0} jest w tej chili wysyłane.
 Notification.SoneIsInserted.Text=Twoje sone://{0} zostało wysłane w {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Twoje Sone sone://{0} nie mogło zostać wysłane.
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
 # 55-61, 324–328, 360, 455, 464
index 8604ef6..2eb7f14 100644 (file)
@@ -461,5 +461,5 @@ Notification.Mention.Text=Вас упомянули в следующих соо
 Notification.SoneIsInserting.Text=Your Sone sone://{0} is now being inserted.
 Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
 # 55-61, 67, 103, 123-124, 305-307, 309-311, 324–328, 360, 455, 461-464
index 6647434..e02cddb 100644 (file)
@@ -10,7 +10,6 @@ import freenet.keys.*
 import freenet.keys.InsertableClientSSK.*
 import freenet.node.*
 import freenet.node.RequestStarter.*
-import freenet.support.Base64
 import freenet.support.api.*
 import freenet.support.io.*
 import net.pterodactylus.sone.core.FreenetInterface.*
@@ -20,6 +19,7 @@ import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.test.Matchers.*
 import net.pterodactylus.sone.test.TestUtil.*
+import net.pterodactylus.sone.utils.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.equalTo
 import org.hamcrest.Matchers.notNullValue
@@ -82,7 +82,7 @@ class FreenetInterfaceTest {
        @Before
        fun setupSone() {
                val insertSsk = createRandom(randomSource, "test-0")
-               whenever(sone.id).thenReturn(Base64.encode(insertSsk.uri.routingKey))
+               whenever(sone.id).thenReturn(insertSsk.uri.routingKey.asFreenetBase64)
                whenever(sone.requestUri).thenReturn(insertSsk.uri.uskForSSK())
        }
 
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt
new file mode 100644 (file)
index 0000000..6d302c5
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ * Sone - WebOfTrustPinger.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.freenet.plugin.*
+import net.pterodactylus.sone.utils.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.atomic.*
+import java.util.function.*
+import kotlin.test.*
+
+/**
+ * Unit test for [WebOfTrustPinger].
+ */
+class WebOfTrustPingerTest {
+
+       private val eventBus = EventBus()
+       private val webOfTrustReachable = AtomicBoolean()
+       private val webOfTrustReacher = Runnable { webOfTrustReachable.get().onFalse { throw PluginException() } }
+       private val rescheduled = AtomicBoolean()
+       private val reschedule: Consumer<Runnable> = Consumer { if (it == pinger) rescheduled.set(true) }
+       private val pinger = WebOfTrustPinger(eventBus, webOfTrustReacher, reschedule)
+
+       @Test
+       fun `pinger sends wot appeared event when run first and wot is reachable`() {
+               webOfTrustReachable.set(true)
+               val appearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) })
+               pinger()
+               assertThat(appearedReceived.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger reschedules when wot is reachable`() {
+               webOfTrustReachable.set(true)
+               pinger()
+               assertThat(rescheduled.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger sends wot disappeared event when run first and wot is not reachable`() {
+               val appearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) })
+               pinger()
+               assertThat(appearedReceived.get(), equalTo(false))
+       }
+
+       @Test
+       fun `pinger reschedules when wot is not reachable`() {
+               pinger()
+               assertThat(rescheduled.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger sends wot disappeared event when run twice and wot is not reachable on second call`() {
+               val disappearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustDisappearedCatcher { disappearedReceived.set(true) })
+               webOfTrustReachable.set(true)
+               pinger()
+               webOfTrustReachable.set(false)
+               pinger()
+               assertThat(disappearedReceived.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger sends wot appeared event only once`() {
+               webOfTrustReachable.set(true)
+               val appearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) })
+               pinger()
+               appearedReceived.set(false)
+               pinger()
+               assertThat(appearedReceived.get(), equalTo(false))
+       }
+
+       @Test
+       fun `pinger sends wot disappeared event only once`() {
+               val disappearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustDisappearedCatcher { disappearedReceived.set(true) })
+               pinger()
+               disappearedReceived.set(false)
+               pinger()
+               assertThat(disappearedReceived.get(), equalTo(false))
+       }
+
+}
+
+private class WebOfTrustAppearedCatcher(private val received: () -> Unit) {
+       @Subscribe
+       fun webOfTrustAppeared(@Suppress("UNUSED_PARAMETER") webOfTrustAppeared: WebOfTrustAppeared) {
+               received()
+       }
+}
+
+private class WebOfTrustDisappearedCatcher(private val received: () -> Unit) {
+       @Subscribe
+       fun webOfTrustDisappeared(@Suppress("UNUSED_PARAMETER") webOfTrustDisappeared: WebOfTrustDisappeared) {
+               received()
+       }
+}
index e94e34e..c6aed88 100644 (file)
@@ -34,12 +34,6 @@ class FreenetModuleTest {
        private val module = FreenetModule(pluginRespirator)
        private val injector = Guice.createInjector(module)
 
-       private inline fun <reified T : Any> verifySingletonInstance() {
-               val firstInstance = injector.getInstance<T>()
-               val secondInstance = injector.getInstance<T>()
-               assertThat(firstInstance, sameInstance(secondInstance))
-       }
-
        @Test
        fun `plugin respirator is not bound`() {
                expectedException.expect(Exception::class.java)
@@ -53,7 +47,7 @@ class FreenetModuleTest {
 
        @Test
        fun `node is returned as singleton`() {
-               verifySingletonInstance<Node>()
+               injector.verifySingletonInstance<Node>()
        }
 
        @Test
@@ -63,7 +57,7 @@ class FreenetModuleTest {
 
        @Test
        fun `high level simply client is returned as singleton`() {
-               verifySingletonInstance<HighLevelSimpleClient>()
+               injector.verifySingletonInstance<HighLevelSimpleClient>()
        }
 
        @Test
@@ -73,7 +67,7 @@ class FreenetModuleTest {
 
        @Test
        fun `session manager is returned as singleton`() {
-               verifySingletonInstance<SessionManager>()
+               injector.verifySingletonInstance<SessionManager>()
                verify(pluginRespirator).getSessionManager("Sone")
        }
 
@@ -84,7 +78,7 @@ class FreenetModuleTest {
 
        @Test
        fun `toadlet container is returned as singleten`() {
-               verifySingletonInstance<ToadletContainer>()
+               injector.verifySingletonInstance<ToadletContainer>()
        }
 
        @Test
@@ -94,7 +88,7 @@ class FreenetModuleTest {
 
        @Test
        fun `page maker is returned as singleton`() {
-               verifySingletonInstance<PageMaker>()
+               injector.verifySingletonInstance<PageMaker>()
        }
 
        @Test
@@ -106,7 +100,7 @@ class FreenetModuleTest {
 
        @Test
        fun `plugin respirator facade is returned as singleton`() {
-               verifySingletonInstance<PluginRespiratorFacade>()
+               injector.verifySingletonInstance<PluginRespiratorFacade>()
        }
 
        @Test
@@ -116,7 +110,7 @@ class FreenetModuleTest {
 
        @Test
        fun `plugin connector facade is returned as singleton`() {
-               verifySingletonInstance<PluginConnector>()
+               injector.verifySingletonInstance<PluginConnector>()
        }
 
 }
index e95955c..c628334 100644 (file)
@@ -6,7 +6,6 @@ import com.google.common.eventbus.*
 import com.google.inject.Guice.*
 import com.google.inject.name.Names.*
 import freenet.l10n.*
-import freenet.pluginmanager.*
 import net.pterodactylus.sone.core.*
 import net.pterodactylus.sone.database.*
 import net.pterodactylus.sone.database.memory.*
@@ -18,13 +17,16 @@ import net.pterodactylus.util.config.*
 import net.pterodactylus.util.version.Version
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
+import org.junit.experimental.categories.*
 import org.mockito.Mockito.*
 import java.io.*
+import java.util.concurrent.*
 import java.util.concurrent.atomic.*
 import kotlin.test.*
 
 const val versionString = "v80"
 
+@Category(NotParallel::class)
 class SoneModuleTest {
 
        private val currentDir: File = File(".")
@@ -190,9 +192,7 @@ class SoneModuleTest {
 
        @Test
        fun `core is created as singleton`() {
-               val firstCore = injector.getInstance<Core>()
-               val secondCore = injector.getInstance<Core>()
-               assertThat(secondCore, sameInstance(firstCore))
+               injector.verifySingletonInstance<Core>()
        }
 
        @Test
@@ -209,27 +209,23 @@ class SoneModuleTest {
        }
 
        @Test
-       fun `metrics registry can be created`() {
-               assertThat(injector.getInstance<MetricRegistry>(), notNullValue())
+       fun `metrics registry is created as singleton`() {
+               injector.verifySingletonInstance<MetricRegistry>()
        }
 
        @Test
-       fun `metrics registry is created as singleton`() {
-               val firstMetricRegistry = injector.getInstance<MetricRegistry>()
-               val secondMetricRegistry = injector.getInstance<MetricRegistry>()
-               assertThat(firstMetricRegistry, sameInstance(secondMetricRegistry))
+       fun `wot connector is created as singleton`() {
+               injector.verifySingletonInstance<WebOfTrustConnector>()
        }
 
        @Test
-       fun `wot connector can be created`() {
-               assertThat(injector.getInstance<WebOfTrustConnector>(), notNullValue())
+       fun `notification ticker is created as singleton`() {
+               injector.verifySingletonInstance<ScheduledExecutorService>(named("notification"))
        }
 
        @Test
-       fun `wot connector is created as singleton`() {
-               val firstWebOfTrustConnector = injector.getInstance<WebOfTrustConnector>()
-               val secondWebOfTrustConnector = injector.getInstance<WebOfTrustConnector>()
-               assertThat(firstWebOfTrustConnector, sameInstance(secondWebOfTrustConnector))
+       fun `ticker shutdown is created as singleton`() {
+               injector.verifySingletonInstance<TickerShutdown>()
        }
 
 }
index eef312f..d1e2cec 100644 (file)
@@ -1,11 +1,13 @@
 package net.pterodactylus.sone.main
 
+import com.google.common.eventbus.*
 import com.google.inject.*
 import freenet.client.async.*
 import freenet.l10n.BaseL10n.LANGUAGE.*
 import freenet.node.*
 import freenet.pluginmanager.*
 import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.core.event.*
 import net.pterodactylus.sone.fcp.*
 import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.test.*
@@ -13,17 +15,20 @@ import net.pterodactylus.sone.web.*
 import net.pterodactylus.sone.web.notification.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
+import org.junit.experimental.categories.*
 import org.mockito.Mockito.*
+import java.io.*
+import java.util.concurrent.atomic.*
 import kotlin.test.*
 
 /**
  * Unit test for [SonePlugin].
  */
 @Dirty
+@Category(NotParallel::class)
 class SonePluginTest {
 
-       private var injector = mockInjector()
-       private val sonePlugin by lazy { SonePlugin { injector } }
+       private var sonePlugin = SonePlugin { injector }
        private val pluginRespirator = deepMock<PluginRespirator>()
        private val node = deepMock<Node>()
        private val clientCore = deepMock<NodeClientCore>()
@@ -71,11 +76,12 @@ class SonePluginTest {
                assertThat(injector.getInstance<NotificationHandler>(), notNullValue())
        }
 
-       private fun runSonePluginWithRealInjector(): Injector {
+       private fun runSonePluginWithRealInjector(injectorConsumer: (Injector) -> Unit = {}): Injector {
                lateinit var injector: Injector
-               val sonePlugin = SonePlugin {
+               sonePlugin = SonePlugin {
                        Guice.createInjector(*it).also {
                                injector = it
+                               injectorConsumer(it)
                        }
                }
                sonePlugin.setLanguage(ENGLISH)
@@ -91,27 +97,153 @@ class SonePluginTest {
        }
 
        @Test
-       fun `notification handler is being started`() {
+       fun `notification handler is being requested`() {
                sonePlugin.runPlugin(pluginRespirator)
-               val notificationHandler = injector.getInstance<NotificationHandler>()
-               verify(notificationHandler).start()
+               assertThat(getInjected(NotificationHandler::class.java), notNullValue())
        }
 
-}
+       @Test
+       fun `ticker shutdown is being requested`() {
+               sonePlugin.runPlugin(pluginRespirator)
+               assertThat(getInjected(TickerShutdown::class.java), notNullValue())
+       }
+
+       private class FirstStartListener(private val firstStartReceived: AtomicBoolean) {
+               @Subscribe
+               fun firstStart(@Suppress("UNUSED_PARAMETER") firstStart: FirstStart) {
+                       firstStartReceived.set(true)
+               }
+       }
 
-private fun mockInjector() = mock<Injector>().apply {
-       val injected = mutableMapOf<Pair<TypeLiteral<*>, Annotation?>, Any>()
-       fun mockValue(clazz: Class<*>) = false.takeIf { clazz.name == java.lang.Boolean::class.java.name } ?: mock(clazz)
-       whenever(getInstance(any<Key<*>>())).then {
-               injected.getOrPut((it.getArgument(0) as Key<*>).let { it.typeLiteral to it.annotation }) {
-                       it.getArgument<Key<*>>(0).typeLiteral.type.typeName.toClass().let(::mockValue)
+       @Test
+       fun `first-start event is sent to event bus when first start is true`() {
+               File("sone.properties").delete()
+               val firstStartReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(FirstStartListener(firstStartReceived))
                }
+               assertThat(firstStartReceived.get(), equalTo(true))
        }
-       whenever(getInstance(any<Class<*>>())).then {
-               injected.getOrPut(TypeLiteral.get(it.getArgument(0) as Class<*>) to null) {
-                       it.getArgument<Class<*>>(0).let(::mockValue)
+
+       @Test
+       fun `first-start event is not sent to event bus when first start is false`() {
+               File("sone.properties").deleteAfter {
+                       writeText("# empty")
+                       val firstStartReceived = AtomicBoolean()
+                       runSonePluginWithRealInjector {
+                               val eventBus = it.getInstance(EventBus::class.java)
+                               eventBus.register(FirstStartListener(firstStartReceived))
+                       }
+                       assertThat(firstStartReceived.get(), equalTo(false))
+               }
+       }
+
+       private class ConfigNotReadListener(private val configNotReadReceiver: AtomicBoolean) {
+               @Subscribe
+               fun configNotRead(@Suppress("UNUSED_PARAMETER") configNotRead: ConfigNotRead) {
+                       configNotReadReceiver.set(true)
                }
        }
+
+       @Test
+       fun `config-not-read event is sent to event bus when new config is true`() {
+               File("sone.properties").deleteAfter {
+                       writeText("Invalid")
+                       val configNotReadReceived = AtomicBoolean()
+                       runSonePluginWithRealInjector {
+                               val eventBus = it.getInstance(EventBus::class.java)
+                               eventBus.register(ConfigNotReadListener(configNotReadReceived))
+                       }
+                       assertThat(configNotReadReceived.get(), equalTo(true))
+               }
+       }
+
+       @Test
+       fun `config-not-read event is not sent to event bus when first start is true`() {
+               File("sone.properties").delete()
+               val configNotReadReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(ConfigNotReadListener(configNotReadReceived))
+               }
+               assertThat(configNotReadReceived.get(), equalTo(false))
+       }
+
+       @Test
+       fun `config-not-read event is not sent to event bus when new config is false`() {
+               File("sone.properties").deleteAfter {
+                       writeText("# comment")
+                       val configNotReadReceived = AtomicBoolean()
+                       runSonePluginWithRealInjector {
+                               val eventBus = it.getInstance(EventBus::class.java)
+                               eventBus.register(ConfigNotReadListener(configNotReadReceived))
+                       }
+                       assertThat(configNotReadReceived.get(), equalTo(false))
+               }
+       }
+
+       private class StartupListener(private val startupReceived: () -> Unit) {
+               @Subscribe
+               fun startup(@Suppress("UNUSED_PARAMETER") startup: Startup) {
+                       startupReceived()
+               }
+       }
+
+       @Test
+       fun `startup event is sent to event bus`() {
+               val startupReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(StartupListener { startupReceived.set(true) })
+               }
+               assertThat(startupReceived.get(), equalTo(true))
+       }
+
+       private class ShutdownListener(private val shutdownReceived: () -> Unit) {
+               @Subscribe
+               fun shutdown(@Suppress("UNUSED_PARAMETER") shutdown: Shutdown) {
+                       shutdownReceived()
+               }
+       }
+
+       @Test
+       fun `shutdown event is sent to event bus on terminate`() {
+               val shutdownReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(ShutdownListener { shutdownReceived.set(true) })
+               }
+               sonePlugin.terminate()
+               assertThat(shutdownReceived.get(), equalTo(true))
+       }
+
+       private fun <T> getInjected(clazz: Class<T>, annotation: Annotation? = null): T? =
+                       injected[TypeLiteral.get(clazz) to annotation] as? T
+
+       private val injected =
+                       mutableMapOf<Pair<TypeLiteral<*>, Annotation?>, Any>()
+
+       private val injector = mock<Injector>().apply {
+               fun mockValue(clazz: Class<*>) = false.takeIf { clazz.name == java.lang.Boolean::class.java.name } ?: mock(clazz)
+               whenever(getInstance(any<Key<*>>())).then {
+                       injected.getOrPut((it.getArgument(0) as Key<*>).let { it.typeLiteral to it.annotation }) {
+                               it.getArgument<Key<*>>(0).typeLiteral.type.typeName.toClass().let(::mockValue)
+                       }
+               }
+               whenever(getInstance(any<Class<*>>())).then {
+                       injected.getOrPut(TypeLiteral.get(it.getArgument(0) as Class<*>) to null) {
+                               it.getArgument<Class<*>>(0).let(::mockValue)
+                       }
+               }
+       }
+
 }
 
 private fun String.toClass(): Class<*> = SonePlugin::class.java.classLoader.loadClass(this)
+
+private fun File.deleteAfter(action: File.() -> Unit) = try {
+       action(this)
+} finally {
+       this.delete()
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt
new file mode 100644 (file)
index 0000000..c02b962
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Sone - TickerShutdownTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.main
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.test.*
+import org.mockito.Mockito.*
+import java.util.concurrent.*
+import kotlin.test.*
+
+/**
+ * Unit test for [TickerShutdown].
+ */
+@Suppress("UnstableApiUsage")
+class TickerShutdownTest {
+
+       private val eventBus = EventBus()
+       private val notificationTicker = mock<ScheduledExecutorService>()
+
+       init {
+               eventBus.register(TickerShutdown(notificationTicker))
+       }
+
+       @Test
+       fun `ticker is shutdown on shutdown`() {
+               eventBus.post(Shutdown())
+               verify(notificationTicker).shutdown()
+       }
+
+}
index c8f2417..b0bed5c 100644 (file)
@@ -2,6 +2,8 @@ package net.pterodactylus.sone.test
 
 import com.google.inject.*
 import com.google.inject.name.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
 import org.mockito.*
 import javax.inject.Provider
 import kotlin.reflect.*
@@ -17,6 +19,13 @@ inline fun <reified T : Any> Injector.getInstance(annotation: Annotation? = null
                ?.let { getInstance(Key.get(object : TypeLiteral<T>() {}, it)) }
                ?: getInstance(Key.get(object : TypeLiteral<T>() {}))
 
+
+inline fun <reified T : Any> Injector.verifySingletonInstance(annotation: Annotation? = null) {
+       val firstInstance = getInstance<T>(annotation)
+       val secondInstance = getInstance<T>(annotation)
+       assertThat(firstInstance, sameInstance(secondInstance))
+}
+
 fun <T : Any> supply(javaClass: Class<T>): Source<T> = object : Source<T> {
        override fun fromInstance(instance: T) = Module { it.bind(javaClass).toInstance(instance) }
        override fun byInstance(instance: T) = Module { it.bind(javaClass).toProvider(Provider<T> { instance }) }
index 7fc9428..85ed9e7 100644 (file)
@@ -5,8 +5,27 @@ import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.utils.*
 import net.pterodactylus.util.web.*
 import org.hamcrest.*
+import org.hamcrest.Matchers
 import org.hamcrest.Matchers.*
 
+/**
+ * Returns a [hamcrest matcher][Matcher] constructed from the given predicate.
+ */
+fun <T> matches(description: String? = null, predicate: (T) -> Boolean) = object : TypeSafeDiagnosingMatcher<T>() {
+
+       override fun matchesSafely(item: T, mismatchDescription: Description) =
+                       predicate(item).onFalse {
+                               mismatchDescription.appendValue(item).appendText(" did not match predicate")
+                       }
+
+       override fun describeTo(description: Description) {
+               description.appendText("matches predicate ").appendValue(predicate)
+       }
+
+}.let { matcher ->
+       description?.let { describedAs(description, matcher) } ?: matcher
+}
+
 fun hasHeader(name: String, value: String) = object : TypeSafeDiagnosingMatcher<Header>() {
        override fun matchesSafely(item: Header, mismatchDescription: Description) =
                        compare(item.name, { it.equals(name, ignoreCase = true) }) { mismatchDescription.appendText("name is ").appendValue(it) }
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt b/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt
new file mode 100644 (file)
index 0000000..3b2c716
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Sone - Mocks.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.test
+
+import com.google.common.base.*
+import freenet.crypt.*
+import freenet.keys.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.SoneOptions.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.text.*
+import net.pterodactylus.sone.utils.*
+
+val remoteSone1 = createRemoteSone()
+val remoteSone2 = createRemoteSone()
+
+val localSone1 = createLocalSone()
+val localSone2 = createLocalSone()
+
+fun createId() = InsertableClientSSK.createRandom(DummyRandomSource(), "").uri.routingKey.asFreenetBase64
+
+fun createLocalSone(id: String? = createId()) = object : IdOnlySone(id) {
+       private val options = DefaultSoneOptions()
+       override fun getOptions() = options
+       override fun isLocal() = true
+}
+fun createRemoteSone(id: String? = createId()) = IdOnlySone(id)
+
+fun createPost(text: String = "", sone: Sone = remoteSone1, known: Boolean = false): Post.EmptyPost {
+       return object : Post.EmptyPost("post-id") {
+               override fun getSone() = sone
+               override fun getText() = text
+               override fun isKnown() = known
+       }
+}
+
+fun emptyPostReply(text: String = "", post: Post? = createPost(), sone: Sone = remoteSone1, known: Boolean = false) = object : PostReply {
+       override val id = "reply-id"
+       override fun getSone() = sone
+       override fun getPostId() = post!!.id
+       override fun getPost(): Optional<Post> = Optional.fromNullable(post)
+       override fun getTime() = 1L
+       override fun getText() = text
+       override fun isKnown() = known
+       override fun setKnown(known: Boolean): PostReply = this
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/NotParallel.kt b/src/test/kotlin/net/pterodactylus/sone/test/NotParallel.kt
new file mode 100644 (file)
index 0000000..6e00fbf
--- /dev/null
@@ -0,0 +1,8 @@
+package net.pterodactylus.sone.test
+
+/**
+ * Marker class for a JUnit [org.junit.experimental.categories.Category], to
+ * mark tests that should not be run parallel to other tests.
+ */
+class NotParallel
+
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt b/src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt
new file mode 100644 (file)
index 0000000..f71e9f5
--- /dev/null
@@ -0,0 +1,21 @@
+package net.pterodactylus.sone.test
+
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.util.template.*
+import net.pterodactylus.util.web.*
+
+/**
+ * [Loaders] implementation for use in tests. Use [templates] to control what templates are
+ * returned by the [loadTemplate] method.
+ */
+class TestLoaders : Loaders {
+
+       val templates = mutableMapOf<String, Template>()
+
+       override fun loadTemplate(path: String) = templates[path] ?: Template()
+
+       override fun <REQ : Request> loadStaticPage(basePath: String, prefix: String, mimeType: String) = TestPage<REQ>()
+
+       override fun getTemplateProvider() = TemplateProvider { _, _ -> Template() }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt b/src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt
new file mode 100644 (file)
index 0000000..762f789
--- /dev/null
@@ -0,0 +1,14 @@
+package net.pterodactylus.sone.test
+
+import net.pterodactylus.util.web.*
+
+/**
+ * Dummy implementation of a [Page].
+ */
+class TestPage<REQ : Request> : Page<REQ> {
+
+       override fun getPath() = ""
+       override fun isPrefixPage() = false
+       override fun handleRequest(freenetRequest: REQ, response: Response) = response
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt
new file mode 100644 (file)
index 0000000..7208230
--- /dev/null
@@ -0,0 +1,263 @@
+/**
+ * Sone - SoneMentionDetectorTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.database.*
+import net.pterodactylus.sone.test.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneMentionDetector].
+ */
+@Suppress("UnstableApiUsage")
+class SoneMentionDetectorTest {
+
+       private val caughtExceptions = mutableListOf<Throwable>()
+       private val eventBus = EventBus { exception, _ -> caughtExceptions += exception }
+       private val soneProvider = TestSoneProvider()
+       private val postProvider = TestPostProvider()
+       private val soneTextParser = SoneTextParser(soneProvider, postProvider)
+       private val capturedFoundEvents = mutableListOf<MentionOfLocalSoneFoundEvent>()
+       private val capturedRemovedEvents = mutableListOf<MentionOfLocalSoneRemovedEvent>()
+       private val postReplyProvider = TestPostReplyProvider()
+
+       init {
+               eventBus.register(SoneMentionDetector(eventBus, soneTextParser, postReplyProvider))
+               eventBus.register(object : Any() {
+                       @Subscribe
+                       fun captureFoundEvent(mentionOfLocalSoneFoundEvent: MentionOfLocalSoneFoundEvent) {
+                               capturedFoundEvents += mentionOfLocalSoneFoundEvent
+                       }
+
+                       @Subscribe
+                       fun captureRemovedEvent(event: MentionOfLocalSoneRemovedEvent) {
+                               capturedRemovedEvents += event
+                       }
+               })
+       }
+
+       @Test
+       fun `detector does not emit event on post that does not contain any sones`() {
+               val post = createPost()
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does not emit event on post that does contain two remote sones`() {
+               val post = createPost("text mentions sone://${remoteSone1.id} and sone://${remoteSone2.id}.")
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector emits event on post that contains links to a remote and a local sone`() {
+               val post = createPost("text mentions sone://${localSone1.id} and sone://${remoteSone2.id}.")
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector emits one event on post that contains two links to the same local sone`() {
+               val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone1.id}.")
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector emits one event on post that contains links to two local sones`() {
+               val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone2.id}.")
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector does not emit event for post by local sone`() {
+               val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", localSone1)
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does not emit event for reply that contains no sones`() {
+               val reply = emptyPostReply()
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does not emit event for reply that contains two links to remote sones`() {
+               val reply = emptyPostReply("text mentions sone://${remoteSone1.id} and sone://${remoteSone2.id}.")
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector emits event on reply that contains links to a remote and a local sone`() {
+               val post = createPost()
+               val reply = emptyPostReply("text mentions sone://${remoteSone1.id} and sone://${localSone1.id}.", post)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector emits one event on reply that contains two links to the same local sone`() {
+               val post = createPost()
+               val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone1.id}.", post)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector emits one event on reply that contains two links to local sones`() {
+               val post = createPost()
+               val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", post)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post)))
+       }
+
+       @Test
+       fun `detector does not emit event for reply by local sone`() {
+               val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", sone = localSone1)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does not emit event for reply without post`() {
+               val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", post = null)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               assertThat(caughtExceptions, emptyIterable())
+               assertThat(capturedFoundEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does not emit removed event when a post without mention is removed`() {
+               val post = createPost()
+               eventBus.post(PostRemovedEvent(post))
+               assertThat(capturedRemovedEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does emit removed event when post with mention is removed`() {
+               val post = createPost("sone://${localSone1.id}")
+               eventBus.post(NewPostFoundEvent(post))
+               eventBus.post(PostRemovedEvent(post))
+               assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post)))
+       }
+
+       @Test
+       fun `detector does not emit removed event when a post without mention is marked as known`() {
+               val post = createPost()
+               eventBus.post(MarkPostKnownEvent(post))
+               assertThat(capturedRemovedEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does emit removed event when post with mention is marked as known`() {
+               val post = createPost("sone://${localSone1.id}")
+               eventBus.post(NewPostFoundEvent(post))
+               eventBus.post(MarkPostKnownEvent(post))
+               assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post)))
+       }
+
+       @Test
+       fun `detector does emit removed event when reply with mention is removed and no more mentions in that post exist`() {
+               val post = createPost()
+               val reply = emptyPostReply("sone://${localSone1.id}", post)
+               postReplyProvider.postReplies[post.id] = listOf(reply)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               eventBus.post(PostReplyRemovedEvent(reply))
+               assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post)))
+       }
+
+       @Test
+       fun `detector does not emit removed event when reply with mention is removed and post mentions local sone`() {
+               val post = createPost("sone://${localSone1.id}")
+               val reply = emptyPostReply("sone://${localSone1.id}", post)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               eventBus.post(PostReplyRemovedEvent(reply))
+               assertThat(capturedRemovedEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does emit removed event when reply with mention is removed and post mentions local sone but is known`() {
+               val post = createPost("sone://${localSone1.id}", known = true)
+               val reply = emptyPostReply("sone://${localSone1.id}", post)
+               eventBus.post(NewPostReplyFoundEvent(reply))
+               eventBus.post(PostReplyRemovedEvent(reply))
+               assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post)))
+       }
+
+       @Test
+       fun `detector does not emit removed event when reply with mention is removed and post has other replies with mentions`() {
+               val post = createPost()
+               val reply1 = emptyPostReply("sone://${localSone1.id}", post)
+               val reply2 = emptyPostReply("sone://${localSone1.id}", post)
+               postReplyProvider.postReplies[post.id] = listOf(reply1, reply2)
+               eventBus.post(NewPostReplyFoundEvent(reply1))
+               eventBus.post(PostReplyRemovedEvent(reply1))
+               assertThat(capturedRemovedEvents, emptyIterable())
+       }
+
+       @Test
+       fun `detector does emit removed event when reply with mention is removed and post has other replies with mentions which are known`() {
+               val post = createPost()
+               val reply1 = emptyPostReply("sone://${localSone1.id}", post)
+               val reply2 = emptyPostReply("sone://${localSone1.id}", post, known = true)
+               postReplyProvider.postReplies[post.id] = listOf(reply1, reply2)
+               eventBus.post(NewPostReplyFoundEvent(reply1))
+               eventBus.post(PostReplyRemovedEvent(reply1))
+               assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post)))
+       }
+
+}
+
+private class TestSoneProvider : SoneProvider {
+
+       override val sones: Collection<Sone> get() = remoteSones + localSones
+       override val localSones: Collection<Sone> get() = setOf(localSone1, localSone2)
+       override val remoteSones: Collection<Sone> get() = setOf(remoteSone1, remoteSone2)
+       override val soneLoader: (String) -> Sone? get() = this::getSone
+       override fun getSone(soneId: String): Sone? =
+                       localSones.firstOrNull { it.id == soneId } ?: remoteSones.firstOrNull { it.id == soneId }
+
+}
+
+private class TestPostProvider : PostProvider {
+
+       override fun getPost(postId: String): Post? = null
+       override fun getPosts(soneId: String): Collection<Post> = emptyList()
+       override fun getDirectedPosts(recipientId: String): Collection<Post> = emptyList()
+
+}
+
+private class TestPostReplyProvider : PostReplyProvider {
+
+       val replies = mutableMapOf<String, PostReply>()
+       val postReplies = mutableMapOf<String, List<PostReply>>()
+
+       override fun getPostReply(id: String) = replies[id]
+       override fun getReplies(postId: String) = postReplies[postId] ?: emptyList()
+
+}
index a1b6da7..bd81f08 100644 (file)
@@ -30,6 +30,26 @@ class BooleansTest {
        }
 
        @Test
+       fun `onTrue returns true on true`() {
+               assertThat(true.onTrue {}, equalTo(true))
+       }
+
+       @Test
+       fun `onTrue returns false on false`() {
+               assertThat(false.onTrue {}, equalTo(false))
+       }
+
+       @Test
+       fun `onTrue is not executed on false`() {
+               assertThat(false.onTrue { throw RuntimeException() }, equalTo(false))
+       }
+
+       @Test(expected = RuntimeException::class)
+       fun `onTrue is executed on true`() {
+               true.onTrue { throw RuntimeException() }
+       }
+
+       @Test
        fun `onFalse returns true on true`() {
                assertThat(true.onFalse {}, equalTo(true))
        }
index 739a0dd..2134661 100644 (file)
@@ -5,7 +5,6 @@ import freenet.clients.http.*
 import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.web.page.*
-import net.pterodactylus.util.web.*
 import org.junit.*
 import org.junit.rules.*
 import org.mockito.Mockito.*
@@ -36,7 +35,7 @@ class PageToadletRegistryTest {
                verify(pageMaker).addNavigationCategory("/Sone/index.html", "Navigation.Menu.Sone.Name", "Navigation.Menu.Sone.Tooltip", sonePlugin)
        }
 
-       private val page = TestPage()
+       private val page = TestPage<FreenetRequest>()
 
        @Test
        fun `adding a page without menuname will add it correctly`() {
@@ -147,10 +146,4 @@ class PageToadletRegistryTest {
                                whenever(this.menuName).thenReturn(menuName)
                        }
 
-       private class TestPage : Page<FreenetRequest> {
-               override fun getPath() = ""
-               override fun isPrefixPage() = false
-               override fun handleRequest(freenetRequest: FreenetRequest, response: Response) = response
-       }
-
 }
index c03d9f8..8840caf 100644 (file)
@@ -256,9 +256,7 @@ class WebInterfaceModuleTest {
 
        @Test
        fun `template context factory is created as singleton`() {
-           val factory1 = injector.getInstance<TemplateContextFactory>()
-           val factory2 = injector.getInstance<TemplateContextFactory>()
-               assertThat(factory1, sameInstance(factory2))
+               injector.verifySingletonInstance<TemplateContextFactory>()
        }
 
        @Test
@@ -280,26 +278,12 @@ class WebInterfaceModuleTest {
        @Test
        fun `page toadlet factory is created with correct prefix`() {
                val page = mock<Page<FreenetRequest>>()
-           assertThat(injector.getInstance<PageToadletFactory>().createPageToadlet(page).path(), startsWith("/Sone/"))
+               assertThat(injector.getInstance<PageToadletFactory>().createPageToadlet(page).path(), startsWith("/Sone/"))
        }
 
        @Test
        fun `notification manager is created as singleton`() {
-               val firstNotificationManager = injector.getInstance<NotificationManager>()
-               val secondNotificationManager = injector.getInstance<NotificationManager>()
-               assertThat(firstNotificationManager, sameInstance(secondNotificationManager))
-       }
-
-       @Test
-       fun `notification handler can be created`() {
-               assertThat(injector.getInstance<NotificationHandler>(), notNullValue())
-       }
-
-       @Test
-       fun `notification handler is created as singleton`() {
-               val firstNotificationHandler = injector.getInstance<NotificationHandler>()
-               val secondNotificationHandler = injector.getInstance<NotificationHandler>()
-               assertThat(firstNotificationHandler, sameInstance(secondNotificationHandler))
+               injector.verifySingletonInstance<NotificationManager>()
        }
 
 }
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt
new file mode 100644 (file)
index 0000000..cc02606
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Sone - ConfigNotReadHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [ConfigNotReadHandler].
+ */
+@Suppress("UnstableApiUsage")
+class ConfigNotReadHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification("", Template())
+
+       init {
+               eventBus.register(ConfigNotReadHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds notification to manager when config was not read`() {
+               eventBus.post(ConfigNotRead())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt
new file mode 100644 (file)
index 0000000..2bda6d1
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Sone - FirstStartHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [FirstStartHandler].
+ */
+@Suppress("UnstableApiUsage")
+class FirstStartHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification(Template())
+
+       init {
+               eventBus.register(FirstStartHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler can be created`() {
+               FirstStartHandler(notificationManager, notification)
+       }
+
+       @Test
+       fun `handler adds notification to manager on first start event`() {
+               eventBus.post(FirstStart())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt
new file mode 100644 (file)
index 0000000..3f570a9
--- /dev/null
@@ -0,0 +1,107 @@
+/**
+ * Sone - ImageInsertHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import freenet.keys.FreenetURI.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [ImageInsertHandler].
+ */
+@Suppress("UnstableApiUsage")
+class ImageInsertHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val imageInsertingNotification = ListNotification<Image>("", "", Template())
+       private val imageFailedNotification = ListNotification<Image>("", "", Template())
+       private val imageInsertedNotification = ListNotification<Image>("", "", Template())
+
+       init {
+               eventBus.register(ImageInsertHandler(notificationManager, imageInsertingNotification, imageFailedNotification, imageInsertedNotification))
+       }
+
+       @Test
+       fun `handler adds notification when image insert starts`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               assertThat(notificationManager.notifications, contains<Notification>(imageInsertingNotification))
+       }
+
+       @Test
+       fun `handler adds image to notification when image insert starts`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               assertThat(imageInsertingNotification.elements, contains(image))
+       }
+
+       @Test
+       fun `handler removes image from inserting notification when insert is aborted`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               eventBus.post(ImageInsertAbortedEvent(image))
+               assertThat(imageInsertingNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler removes image from inserting notification when insert fails`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               eventBus.post(ImageInsertFailedEvent(image, Throwable()))
+               assertThat(imageInsertingNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler adds image to insert-failed notification when insert fails`() {
+               eventBus.post(ImageInsertFailedEvent(image, Throwable()))
+               assertThat(imageFailedNotification.elements, contains(image))
+       }
+
+       @Test
+       fun `handler adds insert-failed notification to manager when insert fails`() {
+               eventBus.post(ImageInsertFailedEvent(image, Throwable()))
+               assertThat(notificationManager.notifications, contains<Notification>(imageFailedNotification))
+       }
+
+       @Test
+       fun `handler removes image from inserting notification when insert succeeds`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI))
+               assertThat(imageInsertingNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler adds image to inserted notification when insert succeeds`() {
+               eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI))
+               assertThat(imageInsertedNotification.elements, contains(image))
+       }
+
+       @Test
+       fun `handler adds inserted notification to manager when insert succeeds`() {
+               eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI))
+               assertThat(notificationManager.notifications, contains<Notification>(imageInsertedNotification))
+       }
+
+}
+
+private val image: Image = ImageImpl()
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt
new file mode 100644 (file)
index 0000000..1d97ae8
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ * Sone - NewLocalPostHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [LocalPostHandler].
+ */
+class LocalPostHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Post>("", "", Template())
+
+       init {
+               eventBus.register(LocalPostHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds post by local sone to notification`() {
+               eventBus.post(NewPostFoundEvent(localPost))
+               assertThat(notification.elements, contains<Post>(localPost))
+       }
+
+       @Test
+       fun `handler does not add post by remote sone to notification`() {
+               eventBus.post(NewPostFoundEvent(remotePost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not add notification to manager for post by remote sone`() {
+               eventBus.post(NewPostFoundEvent(remotePost))
+               assertThat(notificationManager.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `handler adds notification to manager`() {
+               eventBus.post(NewPostFoundEvent(localPost))
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler does not add notification during first start`() {
+               notificationManager.firstStart()
+               eventBus.post(NewPostFoundEvent(localPost))
+               assertThat(notificationManager.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `handler removes post from notification when post is removed`() {
+               notification.add(localPost)
+               notificationManager.addNotification(notification)
+               eventBus.post(PostRemovedEvent(localPost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not remove remote post from notification when post is removed`() {
+               notification.add(remotePost)
+               notificationManager.addNotification(notification)
+               eventBus.post(PostRemovedEvent(remotePost))
+               assertThat(notification.elements, contains(remotePost))
+       }
+
+       @Test
+       fun `handler removes post from notification when post is marked as known`() {
+               notification.add(localPost)
+               notificationManager.addNotification(notification)
+               eventBus.post(MarkPostKnownEvent(localPost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not remove remote post from notification when post is marked as known`() {
+               notification.add(remotePost)
+               notificationManager.addNotification(notification)
+               eventBus.post(MarkPostKnownEvent(remotePost))
+               assertThat(notification.elements, contains(remotePost))
+       }
+
+}
+
+private val localSone: Sone = object : IdOnlySone("local-sone") {
+       override fun isLocal() = true
+}
+private val localPost: Post = object : Post.EmptyPost("local-post") {
+       override fun getSone() = localSone
+}
+private val remoteSone: Sone = IdOnlySone("remote-sone")
+private val remotePost: Post = object : Post.EmptyPost("remote-post") {
+       override fun getSone() = remoteSone
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalReplyHandlerTest.kt
new file mode 100644 (file)
index 0000000..750d083
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * Sone - LocalReplyHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [LocalReplyHandler].
+ */
+class LocalReplyHandlerTest {
+
+       private val notification = ListNotification<PostReply>("", "", Template())
+       private val localReplyHandlerTester = NotificationHandlerTester { LocalReplyHandler(it, notification) }
+
+       @Test
+       fun `handler does not add reply to notification`() {
+               localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(remoteReply))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler does add local reply to notification`() {
+               localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(localReply))
+               assertThat(notification.elements, contains(localReply))
+       }
+
+       @Test
+       fun `handler adds notification to manager`() {
+               localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(localReply))
+               assertThat(localReplyHandlerTester.notifications, hasItem(notification))
+       }
+
+       @Test
+       fun `handler does not add notification to manager for remote reply`() {
+               localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(remoteReply))
+               assertThat(localReplyHandlerTester.notifications, not(hasItem(notification)))
+       }
+
+       @Test
+       fun `handler does not add notification to manager during first start`() {
+               localReplyHandlerTester.firstStart()
+               localReplyHandlerTester.sendEvent(NewPostReplyFoundEvent(localReply))
+               assertThat(localReplyHandlerTester.notifications, not(hasItem(notification)))
+       }
+
+       @Test
+       fun `handler removes reply from notification if reply is removed`() {
+               notification.add(localReply)
+               localReplyHandlerTester.sendEvent(PostReplyRemovedEvent(localReply))
+               assertThat(notification.elements, not(hasItem(localReply)))
+       }
+
+       @Test
+       fun `handler removes reply from notification if reply is marked as known`() {
+               notification.add(localReply)
+               localReplyHandlerTester.sendEvent(MarkPostReplyKnownEvent(localReply))
+               assertThat(notification.elements, not(hasItem(localReply)))
+       }
+
+}
+
+private val localReply = emptyPostReply(sone = localSone1)
+private val remoteReply = emptyPostReply()
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt
new file mode 100644 (file)
index 0000000..c2f8e41
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * Sone - MarkPostKnownDuringFirstStartHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.util.notify.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.function.*
+import kotlin.test.*
+
+/**
+ * Unit test for [MarkPostKnownDuringFirstStartHandler].
+ */
+@Suppress("UnstableApiUsage")
+class MarkPostKnownDuringFirstStartHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val markedPosts = mutableListOf<Post>()
+       private val handler = MarkPostKnownDuringFirstStartHandler(notificationManager, Consumer { markedPosts += it })
+
+       init {
+               eventBus.register(handler)
+       }
+
+       @Test
+       fun `post is not marked as known if not during first start`() {
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(markedPosts, emptyIterable())
+       }
+
+       @Test
+       fun `new post is marked as known during first start`() {
+               notificationManager.firstStart()
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(markedPosts, contains(post))
+       }
+
+}
+
+private val post: Post = Post.EmptyPost("post")
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostReplyKnownDuringFirstStartHandlerTest.kt
new file mode 100644 (file)
index 0000000..3cb463f
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Sone - MarkPostReplyKnownDuringFirstStartHandlerTest.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.test.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.function.*
+import kotlin.test.*
+
+/**
+ * Unit test for [MarkPostReplyKnownDuringFirstStartHandler].
+ */
+class MarkPostReplyKnownDuringFirstStartHandlerTest {
+
+       private val markedAsKnown = mutableListOf<PostReply>()
+       private val notificationTester = NotificationHandlerTester { MarkPostReplyKnownDuringFirstStartHandler(it, Consumer { markedAsKnown += it }) }
+       private val postReply = emptyPostReply()
+
+       @Test
+       fun `post reply is marked as known on new reply during first start`() {
+               notificationTester.firstStart()
+               notificationTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(markedAsKnown, contains(postReply))
+       }
+
+       @Test
+       fun `post reply is not marked as known on new reply if not during first start`() {
+               notificationTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(markedAsKnown, not(hasItem(postReply)))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt
new file mode 100644 (file)
index 0000000..805731d
--- /dev/null
@@ -0,0 +1,97 @@
+/**
+ * Sone - NewRemotePostHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.Post.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [NewRemotePostHandler].
+ */
+@Suppress("UnstableApiUsage")
+class NewRemotePostHandlerTest {
+
+       private val notification = ListNotification<Post>("", "", Template())
+       private val remotePostHandlerTest = NotificationHandlerTester { NewRemotePostHandler(it, notification) }
+
+       @Test
+       fun `handler adds remote post to new-post notification`() {
+               remotePostHandlerTest.sendEvent(NewPostFoundEvent(remotePost))
+               assertThat(notification.elements, contains(remotePost))
+       }
+
+       @Test
+       fun `handler does not add local post to new-post notification`() {
+               remotePostHandlerTest.sendEvent(NewPostFoundEvent(localPost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler adds notification for remote post to notification manager`() {
+               remotePostHandlerTest.sendEvent(NewPostFoundEvent(remotePost))
+               assertThat(remotePostHandlerTest.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler does not add notification for local post to notification manager`() {
+               remotePostHandlerTest.sendEvent(NewPostFoundEvent(localPost))
+               assertThat(remotePostHandlerTest.notifications, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not add notification to notification manager during first start`() {
+               remotePostHandlerTest.firstStart()
+               remotePostHandlerTest.sendEvent(NewPostFoundEvent(remotePost))
+               assertThat(remotePostHandlerTest.notifications, not(hasItem(notification)))
+       }
+
+       @Test
+       fun `handler removes post from notification if post is removed`() {
+               notification.add(remotePost)
+               remotePostHandlerTest.sendEvent(PostRemovedEvent(remotePost))
+               assertThat(notification.elements, not(hasItem(remotePost)))
+       }
+
+       @Test
+       fun `handler removes post from notification if post is marked as known`() {
+               notification.add(remotePost)
+               remotePostHandlerTest.sendEvent(MarkPostKnownEvent(remotePost))
+               assertThat(notification.elements, not(hasItem(remotePost)))
+       }
+
+}
+
+private val remoteSone: Sone = IdOnlySone("remote-sone")
+private val remotePost: Post = object : EmptyPost("remote-post") {
+       override fun getSone() = remoteSone
+}
+
+private val localSone: Sone = object : IdOnlySone("local-sone") {
+       override fun isLocal() = true
+}
+private val localPost: Post = object : EmptyPost("local-post") {
+       override fun getSone() = localSone
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt
new file mode 100644 (file)
index 0000000..acdcf2f
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Sone - NewSoneHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+class NewSoneHandlerTest {
+
+       @Suppress("UnstableApiUsage")
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Sone>("", "", Template())
+       private val handler = NewSoneHandler(notificationManager, notification)
+
+       init {
+               eventBus.register(handler)
+       }
+
+       @Test
+       fun `handler adds notification if new sone event is fired`() {
+               eventBus.post(NewSoneFoundEvent(sone))
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler adds sone to notification`() {
+               eventBus.post(NewSoneFoundEvent(sone))
+               assertThat(notification.elements, contains(sone))
+       }
+
+       @Test
+       fun `handler does not add notification on new sone event if first-start notification is present`() {
+               notificationManager.firstStart()
+               eventBus.post(NewSoneFoundEvent(sone))
+               assertThat(notificationManager.notifications, not(contains<Notification>(notification)))
+       }
+
+       @Test
+       fun `handler removes sone from notification if sone is marked as known`() {
+               notification.add(sone)
+               eventBus.post(MarkSoneKnownEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler removes sone from notification if sone is removed`() {
+               notification.add(sone)
+               eventBus.post(SoneRemovedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+}
+
+private val sone: Sone = IdOnlySone("sone-id")
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt
new file mode 100644 (file)
index 0000000..d37f60b
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Sone - NewVersionHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import net.pterodactylus.util.version.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [NewVersionHandler].
+ */
+@Suppress("UnstableApiUsage")
+class NewVersionHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification(Template())
+
+       init {
+               eventBus.register(NewVersionHandler(notificationManager, notification))
+               eventBus.post(UpdateFoundEvent(Version(1, 2, 3), 1000L, 2000L, true))
+       }
+
+       @Test
+       fun `new-version handler adds notification to manager on new version`() {
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler sets version in notification`() {
+               assertThat(notification.get("latestVersion"), equalTo<Any>(Version(1, 2, 3)))
+       }
+
+       @Test
+       fun `handler sets release time in notification`() {
+               assertThat(notification.get("releaseTime"), equalTo<Any>(1000L))
+       }
+
+       @Test
+       fun `handler sets edition in notification`() {
+               assertThat(notification.get("latestEdition"), equalTo<Any>(2000L))
+       }
+
+       @Test
+       fun `handler sets disruptive flag in notification`() {
+               assertThat(notification.get("disruptive"), equalTo<Any>(true))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt
new file mode 100644 (file)
index 0000000..40e8f4d
--- /dev/null
@@ -0,0 +1,611 @@
+/**
+ * Sone - NotificationHandlerModuleTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.inject.*
+import com.google.inject.Guice.*
+import com.google.inject.name.Names.*
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.Post.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.database.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.text.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.mockito.*
+import org.mockito.Mockito.*
+import java.util.concurrent.*
+import java.util.concurrent.TimeUnit.*
+import java.util.function.*
+import kotlin.test.*
+
+/**
+ * Unit test for [NotificationHandlerModule].
+ */
+class NotificationHandlerModuleTest {
+
+       private val core = mock<Core>()
+       private val webOfTrustConnector = mock<WebOfTrustConnector>()
+       private val ticker = mock<ScheduledExecutorService>()
+       private val notificationManager = NotificationManager()
+       private val loaders = TestLoaders()
+       private val injector: Injector = createInjector(
+                       Core::class.isProvidedBy(core),
+                       NotificationManager::class.isProvidedBy(notificationManager),
+                       Loaders::class.isProvidedBy(loaders),
+                       WebOfTrustConnector::class.isProvidedBy(webOfTrustConnector),
+                       ScheduledExecutorService::class.withNameIsProvidedBy(ticker, "notification"),
+                       SoneTextParser::class.isProvidedByMock(),
+                       PostReplyProvider::class.isProvidedByMock(),
+                       NotificationHandlerModule()
+       )
+
+       @Test
+       fun `notification handler is created as singleton`() {
+               injector.verifySingletonInstance<NotificationHandler>()
+       }
+
+       @Test
+       fun `mark-post-known-during-first-start handler is created as singleton`() {
+               injector.verifySingletonInstance<MarkPostKnownDuringFirstStartHandler>()
+       }
+
+       @Test
+       fun `mark-post-known-during-first-start handler is created with correct action`() {
+               notificationManager.firstStart()
+               val handler = injector.getInstance<MarkPostKnownDuringFirstStartHandler>()
+               val post = mock<Post>()
+               handler.newPostFound(NewPostFoundEvent(post))
+               verify(core).markPostKnown(post)
+       }
+
+       @Test
+       fun `mark-post-reply-known-during-first-start handler is created as singleton`() {
+               injector.verifySingletonInstance<MarkPostReplyKnownDuringFirstStartHandler>()
+       }
+
+       @Test
+       fun `mark-post-reply-known-during-first-start handler is created with correct action`() {
+               notificationManager.firstStart()
+               val handler = injector.getInstance<MarkPostReplyKnownDuringFirstStartHandler>()
+               val postReply = mock<PostReply>()
+               handler.newPostReply(NewPostReplyFoundEvent(postReply))
+               verify(core).markReplyKnown(postReply)
+       }
+
+       @Test
+       fun `sone-locked-on-startup handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneLockedOnStartupHandler>()
+       }
+
+       @Test
+       fun `module can create sone-locked-on-startup notification with correct id`() {
+               val notification = injector.getInstance<ListNotification<Sone>>(named("soneLockedOnStartup"))
+               assertThat(notification.id, equalTo("sone-locked-on-startup"))
+       }
+
+       @Test
+       fun `sone-locked-on-startup notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Sone>>(named("soneLockedOnStartup"))
+       }
+
+       @Test
+       fun `module can create sone-locked-on-startup notification with correct template and key`() {
+               loaders.templates += "/templates/notify/soneLockedOnStartupNotification.html" to "<% sones>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Sone>>(named("soneLockedOnStartup"))
+               val sone1 = IdOnlySone("sone1")
+               val sone2 = IdOnlySone("sone2")
+               notification.add(sone1)
+               notification.add(sone2)
+               assertThat(notification.render(), equalTo(listOf(sone1, sone2).toString()))
+       }
+
+       @Test
+       fun `sone-locked-on-startup notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLockedOnStartup")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `new-sone handler is created as singleton`() {
+               injector.verifySingletonInstance<NewSoneHandler>()
+       }
+
+       @Test
+       fun `new-sone notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("newSone")).id, equalTo("new-sone-notification"))
+       }
+
+       @Test
+       fun `new-sone notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newSoneNotification.html" to "<% sones>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Sone>>(named("newSone"))
+               val sones = listOf(IdOnlySone("sone1"), IdOnlySone("sone2"))
+               sones.forEach(notification::add)
+               assertThat(notification.render(), equalTo(sones.toString()))
+       }
+
+       @Test
+       fun `new-sone notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("newSone")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `new-remote-post handler is created as singleton`() {
+               injector.verifySingletonInstance<NewRemotePostHandler>()
+       }
+
+       @Test
+       fun `new-remote-post notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Post>>(named("newRemotePost"))
+       }
+
+       @Test
+       fun `new-remote-post notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("newRemotePost")).id, equalTo("new-post-notification"))
+       }
+
+       @Test
+       fun `new-remote-post notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("newRemotePost")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `new-remote-post notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newPostNotification.html" to "<% posts>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Post>>(named("newRemotePost"))
+               val posts = listOf(EmptyPost("post1"), EmptyPost("post2"))
+               posts.forEach(notification::add)
+               assertThat(notification.render(), equalTo(posts.toString()))
+       }
+
+       @Test
+       fun `remote-post handler is created as singleton`() {
+               injector.verifySingletonInstance<RemotePostReplyHandler>()
+       }
+
+       @Test
+       fun `new-remote-post-reply notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<PostReply>>(named("newRemotePostReply"))
+       }
+
+       @Test
+       fun `new-remote-post-reply notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<PostReply>>(named("newRemotePostReply")).id, equalTo("new-reply-notification"))
+       }
+
+       @Test
+       fun `new-remote-post-reply notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<PostReply>>(named("newRemotePostReply")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `new-remote-post-reply notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newReplyNotification.html" to "<% replies>".asTemplate()
+               val notification = injector.getInstance<ListNotification<PostReply>>(named("newRemotePostReply"))
+               val postReplies = listOf(emptyPostReply(), emptyPostReply())
+               postReplies.forEach(notification::add)
+               assertThat(notification.render(), equalTo(postReplies.toString()))
+       }
+
+       @Test
+       fun `sone-locked notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Sone>>(named("soneLocked"))
+       }
+
+       @Test
+       fun `sone-locked notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLocked")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `sone-locked notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLocked")).id, equalTo("sones-locked-notification"))
+       }
+
+       @Test
+       fun `sone-locked notification has correct key and template`() {
+               loaders.templates += "/templates/notify/lockedSonesNotification.html" to "<% sones>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Sone>>(named("soneLocked"))
+               val sones = listOf(IdOnlySone("sone1"), IdOnlySone("sone2"))
+               sones.forEach(notification::add)
+               assertThat(notification.render(), equalTo(sones.toString()))
+       }
+
+       @Test
+       fun `sone-locked handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneLockedHandler>()
+       }
+
+       @Test
+       fun `local-post notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("localPost")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `local-post notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("localPost")).id, equalTo("local-post-notification"))
+       }
+
+       @Test
+       fun `local-post notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newPostNotification.html" to "<% posts>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Post>>(named("localPost"))
+               val posts = listOf(EmptyPost("post1"), EmptyPost("post2"))
+               posts.forEach(notification::add)
+               assertThat(notification.render(), equalTo(posts.toString()))
+       }
+
+       @Test
+       fun `local-post notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Post>>(named("localPost"))
+       }
+
+       @Test
+       fun `local-post handler is created as singleton`() {
+               injector.verifySingletonInstance<LocalPostHandler>()
+       }
+
+       @Test
+       fun `local-reply notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<PostReply>>(named("localReply")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `local-reply notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<PostReply>>(named("localReply")).id, equalTo("local-reply-notification"))
+       }
+
+       @Test
+       fun `local-reply notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newReplyNotification.html" to "<% replies>".asTemplate()
+               val notification = injector.getInstance<ListNotification<PostReply>>(named("localReply"))
+               val replies = listOf(emptyPostReply("reply1"), emptyPostReply("reply2"))
+               replies.forEach(notification::add)
+               assertThat(notification.render(), equalTo(replies.toString()))
+       }
+
+       @Test
+       fun `local-reply notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<PostReply>>(named("localReply"))
+       }
+
+       @Test
+       fun `local-reply handler is created as singleton`() {
+               injector.verifySingletonInstance<LocalReplyHandler>()
+       }
+
+       @Test
+       fun `new-version notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("newVersion"))
+       }
+
+       @Test
+       fun `new-version notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("newVersion")).id, equalTo("new-version-notification"))
+       }
+
+       @Test
+       fun `new-version notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("newVersion")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `new-version notification loads correct template`() {
+               loaders.templates += "/templates/notify/newVersionNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("newVersion"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `new-version handler is created as singleton`() {
+               injector.verifySingletonInstance<NewVersionHandler>()
+       }
+
+       @Test
+       fun `inserting-image notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Image>>(named("imageInserting"))
+       }
+
+       @Test
+       fun `inserting-image notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserting")).id, equalTo("inserting-images-notification"))
+       }
+
+       @Test
+       fun `inserting-image notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserting")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `inserting-image notification loads correct template`() {
+               loaders.templates += "/templates/notify/inserting-images-notification.html" to "<% images>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Image>>(named("imageInserting"))
+               val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add)
+               assertThat(notification.render(), equalTo(images.toString()))
+       }
+
+       @Test
+       fun `inserting-image-failed notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Image>>(named("imageFailed"))
+       }
+
+       @Test
+       fun `inserting-image-failed notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageFailed")).id, equalTo("image-insert-failed-notification"))
+       }
+
+       @Test
+       fun `inserting-image-failed notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageFailed")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `inserting-image-failed notification loads correct template`() {
+               loaders.templates += "/templates/notify/image-insert-failed-notification.html" to "<% images>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Image>>(named("imageFailed"))
+               val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add)
+               assertThat(notification.render(), equalTo(images.toString()))
+       }
+
+       @Test
+       fun `inserted-image notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Image>>(named("imageInserted"))
+       }
+
+       @Test
+       fun `inserted-image notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserted")).id, equalTo("inserted-images-notification"))
+       }
+
+       @Test
+       fun `inserted-image notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserted")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `inserted-image notification loads correct template`() {
+               loaders.templates += "/templates/notify/inserted-images-notification.html" to "<% images>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Image>>(named("imageInserted"))
+               val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add)
+               assertThat(notification.render(), equalTo(images.toString()))
+       }
+
+       @Test
+       fun `image insert handler is created as singleton`() {
+               injector.verifySingletonInstance<ImageInsertHandler>()
+       }
+
+       @Test
+       fun `first-start notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("firstStart"))
+       }
+
+       @Test
+       fun `first-start notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("firstStart")).id, equalTo("first-start-notification"))
+       }
+
+       @Test
+       fun `first-start notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("firstStart")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `first-start notification loads correct template`() {
+               loaders.templates += "/templates/notify/firstStartNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("firstStart"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `first-start handler is created as singleton`() {
+               injector.verifySingletonInstance<FirstStartHandler>()
+       }
+
+       @Test
+       fun `config-not-read notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("configNotRead"))
+       }
+
+       @Test
+       fun `config-not-read notification has correct ID `() {
+               assertThat(injector.getInstance<TemplateNotification>(named("configNotRead")).id, equalTo("config-not-read-notification"))
+       }
+
+       @Test
+       fun `config-not-read notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("configNotRead")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `config-not-read notification loads correct template`() {
+               loaders.templates += "/templates/notify/configNotReadNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("configNotRead"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `config-not-read handler is created as singleton`() {
+               injector.verifySingletonInstance<ConfigNotReadHandler>()
+       }
+
+       @Test
+       fun `startup notification can be created`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("startup"))
+       }
+
+       @Test
+       fun `startup notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("startup")).id, equalTo("startup-notification"))
+       }
+
+       @Test
+       fun `startup notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("startup")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `startup notification loads correct template`() {
+               loaders.templates += "/templates/notify/startupNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("startup"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `startup handler is created as singleton`() {
+               injector.verifySingletonInstance<StartupHandler>()
+       }
+
+       @Test
+       fun `web-of-trust notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("webOfTrust"))
+       }
+
+       @Test
+       fun `web-of-trust notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("webOfTrust")).id, equalTo("wot-missing-notification"))
+       }
+
+       @Test
+       fun `web-of-trust notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("webOfTrust")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `web-of-trust notification loads correct template`() {
+               loaders.templates += "/templates/notify/wotMissingNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("webOfTrust"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `web-of-trust handler is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("webOfTrust"))
+       }
+
+       @Test
+       fun `web-of-trust reacher is created as singleton`() {
+               injector.verifySingletonInstance<Runnable>(named("webOfTrustReacher"))
+       }
+
+       @Test
+       fun `web-of-trust reacher access the wot connector`() {
+               injector.getInstance<Runnable>(named("webOfTrustReacher")).run()
+               verify(webOfTrustConnector).ping()
+       }
+
+       @Test
+       fun `web-of-trust reschedule is created as singleton`() {
+               injector.verifySingletonInstance<Consumer<Runnable>>(named("webOfTrustReschedule"))
+       }
+
+       @Test
+       fun `web-of-trust reschedule schedules at the correct delay`() {
+               val webOfTrustPinger = injector.getInstance<WebOfTrustPinger>()
+               injector.getInstance<Consumer<Runnable>>(named("webOfTrustReschedule"))(webOfTrustPinger)
+               verify(ticker).schedule(ArgumentMatchers.eq(webOfTrustPinger), ArgumentMatchers.eq(15L), ArgumentMatchers.eq(SECONDS))
+       }
+
+       @Test
+       fun `sone mention detector is created as singleton`() {
+               assertThat(injector.getInstance<SoneMentionDetector>(), notNullValue())
+       }
+
+       @Test
+       fun `sone-mentioned notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Post>>(named("soneMentioned"))
+       }
+
+       @Test
+       fun `sone-mentioned notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("soneMentioned")).id, equalTo("mention-notification"))
+       }
+
+       @Test
+       fun `sone-mentioned notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("soneMentioned")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `sone-mentioned notification loads correct template`() {
+               loaders.templates += "/templates/notify/mentionNotification.html" to "<% posts>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Post>>(named("soneMentioned"))
+               val posts = listOf(EmptyPost("1"), EmptyPost("2")).onEach(notification::add)
+               assertThat(notification.render(), equalTo(posts.toString()))
+       }
+
+       @Test
+       fun `sone-mentioned handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneMentionedHandler>()
+       }
+
+       @Test
+       fun `sone insert notification supplier is created as singleton`() {
+               injector.verifySingletonInstance<SoneInsertNotificationSupplier>()
+       }
+
+       @Test
+       fun `sone insert notification template is loaded correctly`() {
+               loaders.templates += "/templates/notify/soneInsertNotification.html" to "foo".asTemplate()
+               injector.getInstance<SoneInsertNotificationSupplier>()
+                               .invoke(createRemoteSone())
+                               .render()
+                               .let { assertThat(it, equalTo("foo")) }
+       }
+
+       @Test
+       fun `sone notification supplier returns different notifications for different sones`() {
+               val supplier = injector.getInstance<SoneInsertNotificationSupplier>()
+               listOf(createRemoteSone(), createRemoteSone(), createRemoteSone())
+                               .map(supplier)
+                               .distinct()
+                               .let { assertThat(it, hasSize(3)) }
+       }
+
+       @Test
+       fun `sone notification supplier caches notifications for a sone`() {
+               val supplier = injector.getInstance<SoneInsertNotificationSupplier>()
+               val sone = createRemoteSone()
+               listOf(sone, sone, sone)
+                               .map(supplier)
+                               .distinct()
+                               .let { assertThat(it, hasSize(1)) }
+       }
+
+       @Test
+       fun `sone notification supplier sets sone in notification template`() {
+               val supplier = injector.getInstance<SoneInsertNotificationSupplier>()
+               val sone = createRemoteSone()
+               val templateNotification = supplier(sone)
+               assertThat(templateNotification["insertSone"], sameInstance<Any>(sone))
+       }
+
+       @Test
+       fun `sone insert handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneInsertHandler>()
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTest.kt
deleted file mode 100644 (file)
index d145038..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * Sone - NotificationHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web.notification
-
-import com.google.common.eventbus.*
-import com.google.inject.*
-import com.google.inject.Guice.*
-import net.pterodactylus.sone.main.*
-import net.pterodactylus.sone.test.*
-import net.pterodactylus.util.notify.*
-import net.pterodactylus.util.template.*
-import net.pterodactylus.util.web.*
-import org.hamcrest.MatcherAssert.*
-import org.hamcrest.Matchers.*
-import kotlin.test.*
-
-/**
- * Unit test for [NotificationHandler].
- */
-class NotificationHandlerTest {
-
-       private val eventBus = TestEventBus()
-       private val loaders = TestLoaders()
-       private val notificationManager = NotificationManager()
-       private val handler = NotificationHandler(eventBus, loaders, notificationManager)
-
-       @Test
-       fun `notification handler can be created by guice`() {
-               val injector = createInjector(
-                               EventBus::class.isProvidedBy(eventBus),
-                               NotificationManager::class.isProvidedBy(notificationManager),
-                               Loaders::class.isProvidedBy(loaders)
-               )
-               assertThat(injector.getInstance<NotificationHandler>(), notNullValue())
-       }
-
-       @Test
-       fun `notification handler registers handler for sone-locked event`() {
-               handler.start()
-               assertThat(eventBus.registeredObjects.any { it.javaClass == SoneLockedOnStartupHandler::class.java }, equalTo(true))
-       }
-
-       @Test
-       fun `notification handler loads sone-locked notification template`() {
-               handler.start()
-               assertThat(loaders.requestedTemplatePaths.any { it == "/templates/notify/soneLockedOnStartupNotification.html" }, equalTo(true))
-       }
-
-}
-
-@Suppress("UnstableApiUsage")
-private class TestEventBus : EventBus() {
-       private val _registeredObjects = mutableListOf<Any>()
-       val registeredObjects: List<Any>
-               get() = _registeredObjects
-
-       override fun register(`object`: Any) {
-               super.register(`object`)
-               _registeredObjects += `object`
-       }
-
-}
-
-private class TestLoaders : Loaders {
-       val requestedTemplatePaths = mutableListOf<String>()
-
-       override fun loadTemplate(path: String) =
-                       Template().also { requestedTemplatePaths += path }
-
-       override fun <REQ : Request> loadStaticPage(basePath: String, prefix: String, mimeType: String) = object : Page<REQ> {
-
-               override fun getPath() = ""
-               override fun isPrefixPage() = false
-               override fun handleRequest(request: REQ, response: Response) = response
-
-       }
-
-       override fun getTemplateProvider() = TemplateProvider { _, _ -> Template() }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTester.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTester.kt
new file mode 100644 (file)
index 0000000..4d5627f
--- /dev/null
@@ -0,0 +1,58 @@
+/**
+ * Sone - NotificationHandlerTester.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.util.notify.*
+
+/**
+ * Helper for testing event handlers that deal with notifications. It contains
+ * a notification manager and an [event bus][EventBus] and automatically
+ * registers the created handler on the event bus.
+ *
+ * ```
+ * val notification = SomeNotification()
+ * val notificationTester = NotificationTester { SomeHandler(it, notification) }
+ *
+ * fun test() {
+ *     notificationTester.sendEvent(SomeEvent())
+ *     assertThat(notificationTester.elements, hasItem(notification))
+ * }
+ * ```
+ */
+@Suppress("UnstableApiUsage")
+class NotificationHandlerTester(createHandler: (NotificationManager) -> Any) {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+
+       /** Returns all notifications of the notification manager. */
+       val notifications: Set<Notification>
+               get() = notificationManager.notifications
+
+       init {
+               eventBus.register(createHandler(notificationManager))
+       }
+
+       /** Sends an event to the event bus. */
+       fun sendEvent(event: Any) = eventBus.post(event)
+
+       /** Sets the first-start notification on the notification manager. */
+       fun firstStart() = notificationManager.firstStart()
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/RemotePostReplyHandlerTest.kt
new file mode 100644 (file)
index 0000000..9c5361c
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Sone - RemotePostReplyHandler.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [RemotePostReplyHandler].
+ */
+class RemotePostReplyHandlerTest {
+
+       private val notification = ListNotification<PostReply>("", "", Template())
+       private val notificationHandlerTester = NotificationHandlerTester { RemotePostReplyHandler(it, notification) }
+       private val postReply = emptyPostReply()
+
+       @Test
+       fun `reply is added to notification on new reply`() {
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notification.elements, hasItem<PostReply>(postReply))
+       }
+
+       @Test
+       fun `notification is added to manager on new reply`() {
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notificationHandlerTester.notifications, hasItem<Notification>(notification))
+       }
+
+       @Test
+       fun `reply is not added to notification on new reply during first start`() {
+               notificationHandlerTester.firstStart()
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notification.elements, not(hasItem<PostReply>(postReply)))
+       }
+
+       @Test
+       fun `notification is not added to manager on new reply during first start`() {
+               notificationHandlerTester.firstStart()
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notificationHandlerTester.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `reply is not added to notification on new local reply`() {
+               val postReply = emptyPostReply(sone = localSone1)
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notification.elements, not(hasItem<PostReply>(postReply)))
+       }
+
+       @Test
+       fun `notification is not added to manager on new local reply`() {
+               val postReply = emptyPostReply(sone = localSone1)
+               notificationHandlerTester.sendEvent(NewPostReplyFoundEvent(postReply))
+               assertThat(notificationHandlerTester.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `reply is removed from notification when removed`() {
+               notification.add(postReply)
+               notificationHandlerTester.sendEvent(PostReplyRemovedEvent(postReply))
+               assertThat(notification.elements, not(hasItem<PostReply>(postReply)))
+       }
+
+       @Test
+       fun `reply is removed from notification when marked as known`() {
+               notification.add(postReply)
+               notificationHandlerTester.sendEvent(MarkPostReplyKnownEvent(postReply))
+               assertThat(notification.elements, not(hasItem<PostReply>(postReply)))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneInsertHandlerTest.kt
new file mode 100644 (file)
index 0000000..6a11a90
--- /dev/null
@@ -0,0 +1,112 @@
+/**
+ * Sone - SoneInsertHandlerTest.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneInsertHandler].
+ */
+class SoneInsertHandlerTest {
+
+       private val localSone = createLocalSone()
+       private val notification1 = TemplateNotification(Template())
+       private val notification2 = TemplateNotification(Template())
+       private val soneInsertHandlerTester = NotificationHandlerTester {
+               SoneInsertHandler(it) { sone ->
+                       if (sone == localSone) notification1 else notification2
+               }
+       }
+
+       @Test
+       fun `handler adds notification to manager when sone insert starts`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertingEvent(localSone))
+               assertThat(soneInsertHandlerTester.notifications, hasItem(notification1))
+       }
+
+       @Test
+       fun `handler sets sone status in notification when sone insert starts`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertingEvent(localSone))
+               assertThat(notification1.get("soneStatus"), equalTo<Any>("inserting"))
+       }
+
+       @Test
+       fun `handler does not add notification to manager if option is disabled`() {
+               localSone.options.isSoneInsertNotificationEnabled = false
+               soneInsertHandlerTester.sendEvent(SoneInsertingEvent(localSone))
+               assertThat(soneInsertHandlerTester.notifications, not(hasItem(notification1)))
+       }
+
+       @Test
+       fun `handler adds notification to manager when sone insert finishes`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, ""))
+               assertThat(soneInsertHandlerTester.notifications, hasItem(notification1))
+       }
+
+       @Test
+       fun `handler sets sone status in notification when sone insert finishes`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, ""))
+               assertThat(notification1.get("soneStatus"), equalTo<Any>("inserted"))
+       }
+
+       @Test
+       fun `handler sets insert duration in notification when sone insert finishes`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, ""))
+               assertThat(notification1.get("insertDuration"), equalTo<Any>(123L))
+       }
+
+       @Test
+       fun `handler does not add notification for finished insert to manager if option is disabled`() {
+               localSone.options.isSoneInsertNotificationEnabled = false
+               soneInsertHandlerTester.sendEvent(SoneInsertedEvent(localSone, 123456, ""))
+               assertThat(soneInsertHandlerTester.notifications, not(hasItem(notification1)))
+       }
+
+       @Test
+       fun `handler adds notification to manager when sone insert aborts`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertAbortedEvent(localSone, Exception()))
+               assertThat(soneInsertHandlerTester.notifications, hasItem(notification1))
+       }
+
+       @Test
+       fun `handler sets sone status in notification when sone insert aborts`() {
+               localSone.options.isSoneInsertNotificationEnabled = true
+               soneInsertHandlerTester.sendEvent(SoneInsertAbortedEvent(localSone, Exception()))
+               assertThat(notification1.get("soneStatus"), equalTo<Any>("insert-aborted"))
+       }
+
+       @Test
+       fun `handler does not add notification for aborted insert to manager if option is disabled`() {
+               localSone.options.isSoneInsertNotificationEnabled = false
+               soneInsertHandlerTester.sendEvent(SoneInsertAbortedEvent(localSone, Exception()))
+               assertThat(soneInsertHandlerTester.notifications, not(hasItem(notification1)))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt
new file mode 100644 (file)
index 0000000..f7c185a
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * Sone - SoneLockedHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneLockedHandler].
+ */
+@Suppress("UnstableApiUsage")
+class SoneLockedHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Sone>("", "", Template())
+       private val executor = TestScheduledThreadPoolExecutor()
+
+       init {
+               SoneLockedHandler(notificationManager, notification, executor).also(eventBus::register)
+       }
+
+       @AfterTest
+       fun shutdownExecutor() = executor.shutdown()
+
+       @Test
+       fun `notification is not added before the command is run`() {
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+       @Test
+       fun `sone is added to notification immediately`() {
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(notification.elements, contains(sone))
+       }
+
+       @Test
+       fun `notification is added to notification manager from command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               executor.scheduleds.single().command()
+               assertThat(notificationManager.notifications, contains<Any>(notification))
+       }
+
+       @Test
+       fun `command is registered with a delay of five minutes`() {
+               eventBus.post(SoneLockedEvent(sone))
+               with(executor.scheduleds.single()) {
+                       assertThat(timeUnit.toNanos(delay), equalTo(TimeUnit.MINUTES.toNanos(5)))
+               }
+       }
+
+       @Test
+       fun `unlocking sone after locking will cancel the future`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(executor.scheduleds.first().future.isCancelled, equalTo(true))
+       }
+
+       @Test
+       fun `unlocking sone after locking will remove the sone from the notification`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `unlocking sone after showing the notification will remove the sone from the notification`() {
+               eventBus.post(SoneLockedEvent(sone))
+               executor.scheduleds.single().command()
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `locking two sones will cancel the first command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(executor.scheduleds.first().future.isCancelled, equalTo(true))
+       }
+
+       @Test
+       fun `locking two sones will schedule a second command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(executor.scheduleds[1], notNullValue())
+       }
+
+}
+
+private val sone: Sone = IdOnlySone("sone")
index 0b9d1e1..36a8836 100644 (file)
@@ -19,10 +19,11 @@ package net.pterodactylus.sone.web.notification
 
 import com.google.common.eventbus.*
 import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
 import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.notify.*
-import net.pterodactylus.sone.utils.*
 import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import kotlin.test.*
@@ -35,29 +36,24 @@ class SoneLockedOnStartupHandlerTest {
        @Suppress("UnstableApiUsage")
        private val eventBus = EventBus()
        private val manager = NotificationManager()
-       private val notification by lazy { manager.notifications.single() as ListNotification<*> }
+       private val notification = ListNotification<Sone>("", "", Template())
 
        init {
-               SoneLockedOnStartupHandler(manager, template).also(eventBus::register)
-               eventBus.post(SoneLockedOnStartup(sone))
-       }
-
-       @Test
-       fun `notification has correct id`() {
-               assertThat(notification.id, equalTo("sone-locked-on-startup"))
+               SoneLockedOnStartupHandler(manager, notification).also(eventBus::register)
        }
 
        @Test
        fun `handler adds sone to notification when event is posted`() {
+               eventBus.post(SoneLockedOnStartup(sone))
                assertThat(notification.elements, contains<Any>(sone))
        }
 
        @Test
-       fun `handler creates notification with correct key`() {
-               assertThat(notification.render(), equalTo(listOf(sone).toString()))
+       fun `handler adds notification to manager`() {
+               eventBus.post(SoneLockedOnStartup(sone))
+               assertThat(manager.notifications, contains<Notification>(notification))
        }
 
 }
 
 private val sone = IdOnlySone("sone-id")
-private val template = "<% sones>".asTemplate()
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneMentionedHandlerTest.kt
new file mode 100644 (file)
index 0000000..4e91aa5
--- /dev/null
@@ -0,0 +1,87 @@
+/**
+ * Sone - SoneMentionedHandlerTest.kt - Copyright © 2020 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.Post.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneMentionedHandler].
+ */
+@Suppress("UnstableApiUsage")
+class SoneMentionedHandlerTest {
+
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Post>("", "", Template())
+       private val eventBus = EventBus()
+
+       init {
+               eventBus.register(SoneMentionedHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds notification to manager on event`() {
+               eventBus.post(MentionOfLocalSoneFoundEvent(post))
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler adds post to notification on event`() {
+               eventBus.post(MentionOfLocalSoneFoundEvent(post))
+               assertThat(notification.elements, contains<Post>(post))
+       }
+
+       @Test
+       fun `handler does not add notification during first start`() {
+               notificationManager.firstStart()
+               eventBus.post(MentionOfLocalSoneFoundEvent(post))
+               assertThat(notificationManager.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `handler does not add post to notification during first start`() {
+               notificationManager.firstStart()
+               eventBus.post(MentionOfLocalSoneFoundEvent(post))
+               assertThat(notification.elements, not(hasItem<Post>(post)))
+       }
+
+       @Test
+       fun `handler removes post from notification`() {
+               notification.add(post)
+               eventBus.post(MentionOfLocalSoneRemovedEvent(post))
+               assertThat(notification.elements, not(hasItem(post)))
+       }
+
+       @Test
+       fun `handler removes notification from manager`() {
+               notificationManager.addNotification(notification)
+               eventBus.post(MentionOfLocalSoneRemovedEvent(post))
+               assertThat(notificationManager.notifications, not(hasItem<Notification>(notification)))
+       }
+
+}
+
+private val post = EmptyPost("")
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt
new file mode 100644 (file)
index 0000000..bc2c8e6
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Sone - StartupHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.TimeUnit.*
+import kotlin.test.*
+
+/**
+ * Unit test for [StartupHandler].
+ */
+class StartupHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification("", Template())
+       private val executor = TestScheduledThreadPoolExecutor()
+
+       init {
+               eventBus.register(StartupHandler(notificationManager, notification, executor))
+       }
+
+       @AfterTest
+       fun shutdownExecutor() = executor.shutdown()
+
+       @Test
+       fun `handler adds notification to manager on startup`() {
+               eventBus.post(Startup())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler registers command on with 2-minute delay`() {
+               eventBus.post(Startup())
+               assertThat(with(executor.scheduleds.single()) { timeUnit.toNanos(delay) }, equalTo(MINUTES.toNanos(2)))
+       }
+
+       @Test
+       fun `registered command removes notification from manager`() {
+               eventBus.post(Startup())
+               executor.scheduleds.single().command()
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt
new file mode 100644 (file)
index 0000000..4009ea0
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * Sone - Testing.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import net.pterodactylus.util.notify.*
+import java.io.*
+import java.util.concurrent.*
+
+/** Information about a scheduled runnable. */
+data class Scheduled(val command: Runnable, val delay: Long, val timeUnit: TimeUnit, val future: ScheduledFuture<*>)
+
+/**
+ * [ScheduledThreadPoolExecutor] extension that stores parameters and return
+ * values for the [ScheduledThreadPoolExecutor.schedule] method.
+ */
+class TestScheduledThreadPoolExecutor : ScheduledThreadPoolExecutor(1) {
+
+       val scheduleds = mutableListOf<Scheduled>()
+
+       override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> =
+                       super.schedule(command, delay, unit)
+                                       .also { scheduleds += Scheduled(command, delay, unit, it) }
+
+}
+
+fun NotificationManager.firstStart() {
+       addNotification(object : AbstractNotification("first-start-notification") {
+               override fun render(writer: Writer?) = Unit
+       })
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt
new file mode 100644 (file)
index 0000000..ffd2536
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * Sone - WebOfTrustHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [WebOfTrustHandler].
+ */
+class WebOfTrustHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification("", Template())
+
+       init {
+               eventBus.register(WebOfTrustHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds notification if wot goes down`() {
+               eventBus.post(WebOfTrustDisappeared())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler removes notification if wot appears`() {
+               notificationManager.addNotification(notification)
+               eventBus.post(WebOfTrustAppeared())
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+}