♻️ Extract a handler for new posts during first start
[Sone.git] / src / main / java / net / pterodactylus / sone / web / WebInterface.java
1 /*
2  * Sone - WebInterface.java - Copyright © 2010–2019 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sone.web;
19
20 import static com.google.common.collect.FluentIterable.from;
21 import static java.util.logging.Logger.getLogger;
22
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.TimeZone;
30 import java.util.UUID;
31 import java.util.concurrent.Executors;
32 import java.util.concurrent.ScheduledExecutorService;
33 import java.util.concurrent.ScheduledFuture;
34 import java.util.concurrent.TimeUnit;
35 import java.util.logging.Logger;
36 import javax.annotation.Nonnull;
37 import javax.annotation.Nullable;
38
39 import net.pterodactylus.sone.core.Core;
40 import net.pterodactylus.sone.core.ElementLoader;
41 import net.pterodactylus.sone.core.event.*;
42 import net.pterodactylus.sone.data.Image;
43 import net.pterodactylus.sone.data.Post;
44 import net.pterodactylus.sone.data.PostReply;
45 import net.pterodactylus.sone.data.Sone;
46 import net.pterodactylus.sone.freenet.L10nFilter;
47 import net.pterodactylus.sone.freenet.Translation;
48 import net.pterodactylus.sone.main.Loaders;
49 import net.pterodactylus.sone.main.PluginHomepage;
50 import net.pterodactylus.sone.main.PluginVersion;
51 import net.pterodactylus.sone.main.PluginYear;
52 import net.pterodactylus.sone.main.SonePlugin;
53 import net.pterodactylus.sone.notify.ListNotification;
54 import net.pterodactylus.sone.notify.ListNotificationFilter;
55 import net.pterodactylus.sone.notify.PostVisibilityFilter;
56 import net.pterodactylus.sone.notify.ReplyVisibilityFilter;
57 import net.pterodactylus.sone.template.LinkedElementRenderFilter;
58 import net.pterodactylus.sone.template.ParserFilter;
59 import net.pterodactylus.sone.template.RenderFilter;
60 import net.pterodactylus.sone.template.ShortenFilter;
61 import net.pterodactylus.sone.text.Part;
62 import net.pterodactylus.sone.text.SonePart;
63 import net.pterodactylus.sone.text.SoneTextParser;
64 import net.pterodactylus.sone.text.TimeTextConverter;
65 import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
66 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
67 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
68 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
69 import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
70 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
71 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
72 import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage;
73 import net.pterodactylus.sone.web.ajax.EditImageAjaxPage;
74 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
75 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
76 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
77 import net.pterodactylus.sone.web.ajax.GetLinkedElementAjaxPage;
78 import net.pterodactylus.sone.web.ajax.GetNotificationsAjaxPage;
79 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
80 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
81 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
82 import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
83 import net.pterodactylus.sone.web.ajax.GetTranslationAjaxPage;
84 import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
85 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
86 import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
87 import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
88 import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage;
89 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
90 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
91 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
92 import net.pterodactylus.sone.web.page.FreenetRequest;
93 import net.pterodactylus.sone.web.page.TemplateRenderer;
94 import net.pterodactylus.sone.web.pages.*;
95 import net.pterodactylus.util.notify.Notification;
96 import net.pterodactylus.util.notify.NotificationManager;
97 import net.pterodactylus.util.notify.TemplateNotification;
98 import net.pterodactylus.util.template.Template;
99 import net.pterodactylus.util.template.TemplateContextFactory;
100 import net.pterodactylus.util.web.RedirectPage;
101 import net.pterodactylus.util.web.TemplatePage;
102
103 import freenet.clients.http.SessionManager;
104 import freenet.clients.http.SessionManager.Session;
105 import freenet.clients.http.ToadletContext;
106
107 import com.codahale.metrics.*;
108 import com.google.common.base.Optional;
109 import com.google.common.collect.Collections2;
110 import com.google.common.collect.ImmutableSet;
111 import com.google.common.eventbus.Subscribe;
112 import com.google.inject.Inject;
113
114 /**
115  * Bundles functionality that a web interface of a Freenet plugin needs, e.g.
116  * references to l10n helpers.
117  */
118 public class WebInterface implements SessionProvider {
119
120         /** The logger. */
121         private static final Logger logger = getLogger(WebInterface.class.getName());
122
123         /** The loaders for templates, pages, and classpath providers. */
124         private final Loaders loaders;
125
126         /** The notification manager. */
127         private final NotificationManager notificationManager;
128
129         /** The Sone plugin. */
130         private final SonePlugin sonePlugin;
131
132         /** The form password. */
133         private final String formPassword;
134
135         /** The template context factory. */
136         private final TemplateContextFactory templateContextFactory;
137         private final TemplateRenderer templateRenderer;
138
139         /** The Sone text parser. */
140         private final SoneTextParser soneTextParser;
141
142         /** The parser filter. */
143         private final ParserFilter parserFilter;
144         private final ShortenFilter shortenFilter;
145         private final RenderFilter renderFilter;
146
147         private final ListNotificationFilter listNotificationFilter;
148         private final PostVisibilityFilter postVisibilityFilter;
149         private final ReplyVisibilityFilter replyVisibilityFilter;
150
151         private final ElementLoader elementLoader;
152         private final LinkedElementRenderFilter linkedElementRenderFilter;
153         private final TimeTextConverter timeTextConverter = new TimeTextConverter();
154         private final L10nFilter l10nFilter;
155
156         private final PageToadletRegistry pageToadletRegistry;
157         private final MetricRegistry metricRegistry;
158         private final Translation translation;
159
160         /** The “new post” notification. */
161         private final ListNotification<Post> newPostNotification;
162
163         /** The “new reply” notification. */
164         private final ListNotification<PostReply> newReplyNotification;
165
166         /** The invisible “local post” notification. */
167         private final ListNotification<Post> localPostNotification;
168
169         /** The invisible “local reply” notification. */
170         private final ListNotification<PostReply> localReplyNotification;
171
172         /** The “you have been mentioned” notification. */
173         private final ListNotification<Post> mentionNotification;
174
175         /** Notifications for sone inserts. */
176         private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<>();
177
178         /** Sone locked notification ticker objects. */
179         private final Map<Sone, ScheduledFuture<?>> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, ScheduledFuture<?>>());
180
181         /** The “Sone locked” notification. */
182         private final ListNotification<Sone> lockedSonesNotification;
183
184         /** The “new version” notification. */
185         private final TemplateNotification newVersionNotification;
186
187         /** The “inserting images” notification. */
188         private final ListNotification<Image> insertingImagesNotification;
189
190         /** The “inserted images” notification. */
191         private final ListNotification<Image> insertedImagesNotification;
192
193         /** The “image insert failed” notification. */
194         private final ListNotification<Image> imageInsertFailedNotification;
195
196         /** Scheduled executor for time-based notifications. */
197         private final ScheduledExecutorService ticker = Executors.newScheduledThreadPool(1);
198
199         @Inject
200         public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter,
201                         PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter,
202                         ElementLoader elementLoader, TemplateContextFactory templateContextFactory,
203                         TemplateRenderer templateRenderer,
204                         ParserFilter parserFilter, ShortenFilter shortenFilter,
205                         RenderFilter renderFilter,
206                         LinkedElementRenderFilter linkedElementRenderFilter,
207                         PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter,
208                         NotificationManager notificationManager) {
209                 this.sonePlugin = sonePlugin;
210                 this.loaders = loaders;
211                 this.listNotificationFilter = listNotificationFilter;
212                 this.postVisibilityFilter = postVisibilityFilter;
213                 this.replyVisibilityFilter = replyVisibilityFilter;
214                 this.elementLoader = elementLoader;
215                 this.templateRenderer = templateRenderer;
216                 this.parserFilter = parserFilter;
217                 this.shortenFilter = shortenFilter;
218                 this.renderFilter = renderFilter;
219                 this.linkedElementRenderFilter = linkedElementRenderFilter;
220                 this.pageToadletRegistry = pageToadletRegistry;
221                 this.metricRegistry = metricRegistry;
222                 this.l10nFilter = l10nFilter;
223                 this.translation = translation;
224                 this.notificationManager = notificationManager;
225                 formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
226                 soneTextParser = new SoneTextParser(getCore(), getCore());
227
228                 this.templateContextFactory = templateContextFactory;
229                 templateContextFactory.addTemplateObject("webInterface", this);
230                 templateContextFactory.addTemplateObject("formPassword", formPassword);
231
232                 /* create notifications. */
233                 Template newPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
234                 newPostNotification = new ListNotification<>("new-post-notification", "posts", newPostNotificationTemplate, false);
235
236                 Template localPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
237                 localPostNotification = new ListNotification<>("local-post-notification", "posts", localPostNotificationTemplate, false);
238
239                 Template newReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
240                 newReplyNotification = new ListNotification<>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
241
242                 Template localReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
243                 localReplyNotification = new ListNotification<>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
244
245                 Template mentionNotificationTemplate = loaders.loadTemplate("/templates/notify/mentionNotification.html");
246                 mentionNotification = new ListNotification<>("mention-notification", "posts", mentionNotificationTemplate, false);
247
248                 Template lockedSonesTemplate = loaders.loadTemplate("/templates/notify/lockedSonesNotification.html");
249                 lockedSonesNotification = new ListNotification<>("sones-locked-notification", "sones", lockedSonesTemplate);
250
251                 Template newVersionTemplate = loaders.loadTemplate("/templates/notify/newVersionNotification.html");
252                 newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
253
254                 Template insertingImagesTemplate = loaders.loadTemplate("/templates/notify/inserting-images-notification.html");
255                 insertingImagesNotification = new ListNotification<>("inserting-images-notification", "images", insertingImagesTemplate);
256
257                 Template insertedImagesTemplate = loaders.loadTemplate("/templates/notify/inserted-images-notification.html");
258                 insertedImagesNotification = new ListNotification<>("inserted-images-notification", "images", insertedImagesTemplate);
259
260                 Template imageInsertFailedTemplate = loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html");
261                 imageInsertFailedNotification = new ListNotification<>("image-insert-failed-notification", "images", imageInsertFailedTemplate);
262         }
263
264         //
265         // ACCESSORS
266         //
267
268         /**
269          * Returns the Sone core used by the Sone plugin.
270          *
271          * @return The Sone core
272          */
273         @Nonnull
274         public Core getCore() {
275                 return sonePlugin.core();
276         }
277
278         /**
279          * Returns the template context factory of the web interface.
280          *
281          * @return The template context factory
282          */
283         public TemplateContextFactory getTemplateContextFactory() {
284                 return templateContextFactory;
285         }
286
287         private Session getCurrentSessionWithoutCreation(ToadletContext toadletContenxt) {
288                 return getSessionManager().useSession(toadletContenxt);
289         }
290
291         private Session getOrCreateCurrentSession(ToadletContext toadletContenxt) {
292                 Session session = getCurrentSessionWithoutCreation(toadletContenxt);
293                 if (session == null) {
294                         session = getSessionManager().createSession(UUID.randomUUID().toString(), toadletContenxt);
295                 }
296                 return session;
297         }
298
299         public Sone getCurrentSoneCreatingSession(ToadletContext toadletContext) {
300                 Collection<Sone> localSones = getCore().getLocalSones();
301                 if (localSones.size() == 1) {
302                         return localSones.iterator().next();
303                 }
304                 return getCurrentSone(getOrCreateCurrentSession(toadletContext));
305         }
306
307         public Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) {
308                 Collection<Sone> localSones = getCore().getLocalSones();
309                 if (localSones.size() == 1) {
310                         return localSones.iterator().next();
311                 }
312                 return getCurrentSone(getCurrentSessionWithoutCreation(toadletContext));
313         }
314
315         /**
316          * Returns the currently logged in Sone.
317          *
318          * @param session
319          *            The session
320          * @return The currently logged in Sone, or {@code null} if no Sone is
321          *         currently logged in
322          */
323         private Sone getCurrentSone(Session session) {
324                 if (session == null) {
325                         return null;
326                 }
327                 String soneId = (String) session.getAttribute("Sone.CurrentSone");
328                 if (soneId == null) {
329                         return null;
330                 }
331                 return getCore().getLocalSone(soneId);
332         }
333
334         @Override
335         @Nullable
336         public Sone getCurrentSone(@Nonnull ToadletContext toadletContext, boolean createSession) {
337                 return createSession ? getCurrentSoneCreatingSession(toadletContext) : getCurrentSoneWithoutCreatingSession(toadletContext);
338         }
339
340         /**
341          * Sets the currently logged in Sone.
342          *
343          * @param toadletContext
344          *            The toadlet context
345          * @param sone
346          *            The Sone to set as currently logged in
347          */
348         @Override
349         public void setCurrentSone(@Nonnull ToadletContext toadletContext, @Nullable Sone sone) {
350                 Session session = getOrCreateCurrentSession(toadletContext);
351                 if (sone == null) {
352                         session.removeAttribute("Sone.CurrentSone");
353                 } else {
354                         session.setAttribute("Sone.CurrentSone", sone.getId());
355                 }
356         }
357
358         /**
359          * Returns the notification manager.
360          *
361          * @return The notification manager
362          */
363         public NotificationManager getNotifications() {
364                 return notificationManager;
365         }
366
367         @Nonnull
368         public Optional<Notification> getNotification(@Nonnull String notificationId) {
369                 return Optional.fromNullable(notificationManager.getNotification(notificationId));
370         }
371
372         @Nonnull
373         public Collection<Notification> getNotifications(@Nullable Sone currentSone) {
374                 return listNotificationFilter.filterNotifications(notificationManager.getNotifications(), currentSone);
375         }
376
377         public Translation getTranslation() {
378                 return translation;
379         }
380
381         /**
382          * Returns the session manager of the node.
383          *
384          * @return The node’s session manager
385          */
386         public SessionManager getSessionManager() {
387                 return sonePlugin.pluginRespirator().getSessionManager("Sone");
388         }
389
390         /**
391          * Returns the node’s form password.
392          *
393          * @return The form password
394          */
395         public String getFormPassword() {
396                 return formPassword;
397         }
398
399         /**
400          * Returns the posts that have been announced as new in the
401          * {@link #newPostNotification}.
402          *
403          * @return The new posts
404          */
405         public Set<Post> getNewPosts() {
406                 return ImmutableSet.<Post> builder().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).build();
407         }
408
409         @Nonnull
410         public Collection<Post> getNewPosts(@Nullable Sone currentSone) {
411                 Set<Post> allNewPosts = ImmutableSet.<Post> builder()
412                                 .addAll(newPostNotification.getElements())
413                                 .addAll(localPostNotification.getElements())
414                                 .build();
415                 return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet();
416         }
417
418         /**
419          * Returns the replies that have been announced as new in the
420          * {@link #newReplyNotification}.
421          *
422          * @return The new replies
423          */
424         public Set<PostReply> getNewReplies() {
425                 return ImmutableSet.<PostReply> builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build();
426         }
427
428         @Nonnull
429         public Collection<PostReply> getNewReplies(@Nullable Sone currentSone) {
430                 Set<PostReply> allNewReplies = ImmutableSet.<PostReply>builder()
431                                 .addAll(newReplyNotification.getElements())
432                                 .addAll(localReplyNotification.getElements())
433                                 .build();
434                 return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet();
435         }
436
437         /**
438          * Sets whether the current start of the plugin is the first start. It is
439          * considered a first start if the configuration file does not exist.
440          *
441          * @param firstStart
442          *            {@code true} if no configuration file existed when Sone was
443          *            loaded, {@code false} otherwise
444          */
445         public void setFirstStart(boolean firstStart) {
446                 if (firstStart) {
447                         Template firstStartNotificationTemplate = loaders.loadTemplate("/templates/notify/firstStartNotification.html");
448                         Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate);
449                         notificationManager.addNotification(firstStartNotification);
450                 }
451         }
452
453         /**
454          * Sets whether Sone was started with a fresh configuration file.
455          *
456          * @param newConfig
457          *            {@code true} if Sone was started with a fresh configuration,
458          *            {@code false} if the existing configuration could be read
459          */
460         public void setNewConfig(boolean newConfig) {
461                 if (newConfig && !hasFirstStartNotification()) {
462                         Template configNotReadNotificationTemplate = loaders.loadTemplate("/templates/notify/configNotReadNotification.html");
463                         Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate);
464                         notificationManager.addNotification(configNotReadNotification);
465                 }
466         }
467
468         //
469         // PRIVATE ACCESSORS
470         //
471
472         /**
473          * Returns whether the first start notification is currently displayed.
474          *
475          * @return {@code true} if the first-start notification is currently
476          *         displayed, {@code false} otherwise
477          */
478         private boolean hasFirstStartNotification() {
479                 return notificationManager.getNotification("first-start-notification") != null;
480         }
481
482         //
483         // ACTIONS
484         //
485
486         /**
487          * Starts the web interface and registers all toadlets.
488          */
489         public void start() {
490                 registerToadlets();
491
492                 /* notification templates. */
493                 Template startupNotificationTemplate = loaders.loadTemplate("/templates/notify/startupNotification.html");
494
495                 final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate);
496                 notificationManager.addNotification(startupNotification);
497
498                 ticker.schedule(new Runnable() {
499
500                         @Override
501                         public void run() {
502                                 startupNotification.dismiss();
503                         }
504                 }, 2, TimeUnit.MINUTES);
505
506                 Template wotMissingNotificationTemplate = loaders.loadTemplate("/templates/notify/wotMissingNotification.html");
507                 final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate);
508                 ticker.scheduleAtFixedRate(new Runnable() {
509
510                         @Override
511                         @SuppressWarnings("synthetic-access")
512                         public void run() {
513                                 if (getCore().getIdentityManager().isConnected()) {
514                                         wotMissingNotification.dismiss();
515                                 } else {
516                                         notificationManager.addNotification(wotMissingNotification);
517                                 }
518                         }
519
520                 }, 15, 15, TimeUnit.SECONDS);
521         }
522
523         /**
524          * Stops the web interface and unregisters all toadlets.
525          */
526         public void stop() {
527                 pageToadletRegistry.unregisterToadlets();
528                 ticker.shutdownNow();
529         }
530
531         //
532         // PRIVATE METHODS
533         //
534
535         /**
536          * Register all toadlets.
537          */
538         private void registerToadlets() {
539                 Template postTemplate = loaders.loadTemplate("/templates/include/viewPost.html");
540                 Template replyTemplate = loaders.loadTemplate("/templates/include/viewReply.html");
541                 Template openSearchTemplate = loaders.loadTemplate("/templates/xml/OpenSearch.xml");
542
543                 pageToadletRegistry.addPage(new RedirectPage<FreenetRequest>("", "index.html"));
544                 pageToadletRegistry.addPage(new IndexPage(this, loaders, templateRenderer, postVisibilityFilter));
545                 pageToadletRegistry.addPage(new NewPage(this, loaders, templateRenderer));
546                 pageToadletRegistry.addPage(new CreateSonePage(this, loaders, templateRenderer));
547                 pageToadletRegistry.addPage(new KnownSonesPage(this, loaders, templateRenderer));
548                 pageToadletRegistry.addPage(new EditProfilePage(this, loaders, templateRenderer));
549                 pageToadletRegistry.addPage(new EditProfileFieldPage(this, loaders, templateRenderer));
550                 pageToadletRegistry.addPage(new DeleteProfileFieldPage(this, loaders, templateRenderer));
551                 pageToadletRegistry.addPage(new CreatePostPage(this, loaders, templateRenderer));
552                 pageToadletRegistry.addPage(new CreateReplyPage(this, loaders, templateRenderer));
553                 pageToadletRegistry.addPage(new ViewSonePage(this, loaders, templateRenderer));
554                 pageToadletRegistry.addPage(new ViewPostPage(this, loaders, templateRenderer));
555                 pageToadletRegistry.addPage(new LikePage(this, loaders, templateRenderer));
556                 pageToadletRegistry.addPage(new UnlikePage(this, loaders, templateRenderer));
557                 pageToadletRegistry.addPage(new DeletePostPage(this, loaders, templateRenderer));
558                 pageToadletRegistry.addPage(new DeleteReplyPage(this, loaders, templateRenderer));
559                 pageToadletRegistry.addPage(new LockSonePage(this, loaders, templateRenderer));
560                 pageToadletRegistry.addPage(new UnlockSonePage(this, loaders, templateRenderer));
561                 pageToadletRegistry.addPage(new FollowSonePage(this, loaders, templateRenderer));
562                 pageToadletRegistry.addPage(new UnfollowSonePage(this, loaders, templateRenderer));
563                 pageToadletRegistry.addPage(new ImageBrowserPage(this, loaders, templateRenderer));
564                 pageToadletRegistry.addPage(new CreateAlbumPage(this, loaders, templateRenderer));
565                 pageToadletRegistry.addPage(new EditAlbumPage(this, loaders, templateRenderer));
566                 pageToadletRegistry.addPage(new DeleteAlbumPage(this, loaders, templateRenderer));
567                 pageToadletRegistry.addPage(new UploadImagePage(this, loaders, templateRenderer));
568                 pageToadletRegistry.addPage(new EditImagePage(this, loaders, templateRenderer));
569                 pageToadletRegistry.addPage(new DeleteImagePage(this, loaders, templateRenderer));
570                 pageToadletRegistry.addPage(new MarkAsKnownPage(this, loaders, templateRenderer));
571                 pageToadletRegistry.addPage(new BookmarkPage(this, loaders, templateRenderer));
572                 pageToadletRegistry.addPage(new UnbookmarkPage(this, loaders, templateRenderer));
573                 pageToadletRegistry.addPage(new BookmarksPage(this, loaders, templateRenderer));
574                 pageToadletRegistry.addPage(new SearchPage(this, loaders, templateRenderer));
575                 pageToadletRegistry.addPage(new DeleteSonePage(this, loaders, templateRenderer));
576                 pageToadletRegistry.addPage(new LoginPage(this, loaders, templateRenderer));
577                 pageToadletRegistry.addPage(new LogoutPage(this, loaders, templateRenderer));
578                 pageToadletRegistry.addPage(new OptionsPage(this, loaders, templateRenderer));
579                 pageToadletRegistry.addPage(new RescuePage(this, loaders, templateRenderer));
580                 pageToadletRegistry.addPage(new AboutPage(this, loaders, templateRenderer, new PluginVersion(SonePlugin.getPluginVersion()), new PluginYear(sonePlugin.getYear()), new PluginHomepage(sonePlugin.getHomepage())));
581                 pageToadletRegistry.addPage(new InvalidPage(this, loaders, templateRenderer));
582                 pageToadletRegistry.addPage(new NoPermissionPage(this, loaders, templateRenderer));
583                 pageToadletRegistry.addPage(new EmptyImageTitlePage(this, loaders, templateRenderer));
584                 pageToadletRegistry.addPage(new EmptyAlbumTitlePage(this, loaders, templateRenderer));
585                 pageToadletRegistry.addPage(new DismissNotificationPage(this, loaders, templateRenderer));
586                 pageToadletRegistry.addPage(new DebugPage(this, loaders, templateRenderer));
587                 pageToadletRegistry.addDebugPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry));
588                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
589                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
590                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
591                 pageToadletRegistry.addPage(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate));
592                 pageToadletRegistry.addPage(new GetImagePage(this));
593                 pageToadletRegistry.addPage(new GetTranslationAjaxPage(this));
594                 pageToadletRegistry.addPage(new GetStatusAjaxPage(this, elementLoader, timeTextConverter, l10nFilter, TimeZone.getDefault()));
595                 pageToadletRegistry.addPage(new GetNotificationsAjaxPage(this));
596                 pageToadletRegistry.addPage(new DismissNotificationAjaxPage(this));
597                 pageToadletRegistry.addPage(new CreatePostAjaxPage(this));
598                 pageToadletRegistry.addPage(new CreateReplyAjaxPage(this));
599                 pageToadletRegistry.addPage(new GetReplyAjaxPage(this, replyTemplate));
600                 pageToadletRegistry.addPage(new GetPostAjaxPage(this, postTemplate));
601                 pageToadletRegistry.addPage(new GetLinkedElementAjaxPage(this, elementLoader, linkedElementRenderFilter));
602                 pageToadletRegistry.addPage(new GetTimesAjaxPage(this, timeTextConverter, l10nFilter, TimeZone.getDefault()));
603                 pageToadletRegistry.addPage(new MarkAsKnownAjaxPage(this));
604                 pageToadletRegistry.addPage(new DeletePostAjaxPage(this));
605                 pageToadletRegistry.addPage(new DeleteReplyAjaxPage(this));
606                 pageToadletRegistry.addPage(new LockSoneAjaxPage(this));
607                 pageToadletRegistry.addPage(new UnlockSoneAjaxPage(this));
608                 pageToadletRegistry.addPage(new FollowSoneAjaxPage(this));
609                 pageToadletRegistry.addPage(new UnfollowSoneAjaxPage(this));
610                 pageToadletRegistry.addPage(new EditAlbumAjaxPage(this));
611                 pageToadletRegistry.addPage(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter));
612                 pageToadletRegistry.addPage(new LikeAjaxPage(this));
613                 pageToadletRegistry.addPage(new UnlikeAjaxPage(this));
614                 pageToadletRegistry.addPage(new GetLikesAjaxPage(this));
615                 pageToadletRegistry.addPage(new BookmarkAjaxPage(this));
616                 pageToadletRegistry.addPage(new UnbookmarkAjaxPage(this));
617                 pageToadletRegistry.addPage(new EditProfileFieldAjaxPage(this));
618                 pageToadletRegistry.addPage(new DeleteProfileFieldAjaxPage(this));
619                 pageToadletRegistry.addPage(new MoveProfileFieldAjaxPage(this));
620
621                 pageToadletRegistry.registerToadlets();
622         }
623
624         /**
625          * Returns all {@link Sone#isLocal() local Sone}s that are referenced by
626          * {@link SonePart}s in the given text (after parsing it using
627          * {@link SoneTextParser}).
628          *
629          * @param text
630          *            The text to parse
631          * @return All mentioned local Sones
632          */
633         private Collection<Sone> getMentionedSones(String text) {
634                 /* we need no context to find mentioned Sones. */
635                 Set<Sone> mentionedSones = new HashSet<>();
636                 for (Part part : soneTextParser.parse(text, null)) {
637                         if (part instanceof SonePart) {
638                                 mentionedSones.add(((SonePart) part).getSone());
639                         }
640                 }
641                 return Collections2.filter(mentionedSones, Sone.LOCAL_SONE_FILTER);
642         }
643
644         /**
645          * Returns the Sone insert notification for the given Sone. If no
646          * notification for the given Sone exists, a new notification is created and
647          * cached.
648          *
649          * @param sone
650          *            The Sone to get the insert notification for
651          * @return The Sone insert notification
652          */
653         private TemplateNotification getSoneInsertNotification(Sone sone) {
654                 synchronized (soneInsertNotifications) {
655                         TemplateNotification templateNotification = soneInsertNotifications.get(sone);
656                         if (templateNotification == null) {
657                                 templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
658                                 templateNotification.set("insertSone", sone);
659                                 soneInsertNotifications.put(sone, templateNotification);
660                         }
661                         return templateNotification;
662                 }
663         }
664
665         private boolean localSoneMentionedInNewPostOrReply(Post post) {
666                 if (!post.getSone().isLocal()) {
667                         if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) {
668                                 return true;
669                         }
670                 }
671                 for (PostReply postReply : getCore().getReplies(post.getId())) {
672                         if (postReply.getSone().isLocal()) {
673                                 continue;
674                         }
675                         if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) {
676                                 return true;
677                         }
678                 }
679                 return false;
680         }
681
682         //
683         // EVENT HANDLERS
684         //
685
686         /**
687          * Notifies the web interface that a new {@link Post} was found.
688          *
689          * @param newPostFoundEvent
690          *            The event
691          */
692         @Subscribe
693         public void newPostFound(NewPostFoundEvent newPostFoundEvent) {
694                 Post post = newPostFoundEvent.getPost();
695                 boolean isLocal = post.getSone().isLocal();
696                 if (isLocal) {
697                         localPostNotification.add(post);
698                 } else {
699                         newPostNotification.add(post);
700                 }
701                 if (!hasFirstStartNotification()) {
702                         notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
703                         if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
704                                 mentionNotification.add(post);
705                                 notificationManager.addNotification(mentionNotification);
706                         }
707                 }
708         }
709
710         /**
711          * Notifies the web interface that a new {@link PostReply} was found.
712          *
713          * @param newPostReplyFoundEvent
714          *            The event
715          */
716         @Subscribe
717         public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) {
718                 PostReply reply = newPostReplyFoundEvent.getPostReply();
719                 boolean isLocal = reply.getSone().isLocal();
720                 if (isLocal) {
721                         localReplyNotification.add(reply);
722                 } else {
723                         newReplyNotification.add(reply);
724                 }
725                 if (!hasFirstStartNotification()) {
726                         notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
727                         if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
728                                 mentionNotification.add(reply.getPost().get());
729                                 notificationManager.addNotification(mentionNotification);
730                         }
731                 } else {
732                         getCore().markReplyKnown(reply);
733                 }
734         }
735
736         @Subscribe
737         public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
738                 removePost(markPostKnownEvent.getPost());
739         }
740
741         @Subscribe
742         public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) {
743                 removeReply(markPostReplyKnownEvent.getPostReply());
744         }
745
746         @Subscribe
747         public void postRemoved(PostRemovedEvent postRemovedEvent) {
748                 removePost(postRemovedEvent.getPost());
749         }
750
751         private void removePost(Post post) {
752                 newPostNotification.remove(post);
753                 localPostNotification.remove(post);
754                 if (!localSoneMentionedInNewPostOrReply(post)) {
755                         mentionNotification.remove(post);
756                 }
757         }
758
759         @Subscribe
760         public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) {
761                 removeReply(postReplyRemovedEvent.getPostReply());
762         }
763
764         private void removeReply(PostReply reply) {
765                 newReplyNotification.remove(reply);
766                 localReplyNotification.remove(reply);
767                 if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
768                         mentionNotification.remove(reply.getPost().get());
769                 }
770         }
771
772         /**
773          * Notifies the web interface that a Sone was locked.
774          *
775          * @param soneLockedEvent
776          *            The event
777          */
778         @Subscribe
779         public void soneLocked(SoneLockedEvent soneLockedEvent) {
780                 final Sone sone = soneLockedEvent.getSone();
781                 ScheduledFuture<?> tickerObject = ticker.schedule(new Runnable() {
782
783                         @Override
784                         @SuppressWarnings("synthetic-access")
785                         public void run() {
786                                 lockedSonesNotification.add(sone);
787                                 notificationManager.addNotification(lockedSonesNotification);
788                         }
789                 }, 5, TimeUnit.MINUTES);
790                 lockedSonesTickerObjects.put(sone, tickerObject);
791         }
792
793         /**
794          * Notifies the web interface that a Sone was unlocked.
795          *
796          * @param soneUnlockedEvent
797          *            The event
798          */
799         @Subscribe
800         public void soneUnlocked(SoneUnlockedEvent soneUnlockedEvent) {
801                 lockedSonesNotification.remove(soneUnlockedEvent.getSone());
802                 lockedSonesTickerObjects.remove(soneUnlockedEvent.getSone()).cancel(false);
803         }
804
805         /**
806          * Notifies the web interface that a {@link Sone} is being inserted.
807          *
808          * @param soneInsertingEvent
809          *            The event
810          */
811         @Subscribe
812         public void soneInserting(SoneInsertingEvent soneInsertingEvent) {
813                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.getSone());
814                 soneInsertNotification.set("soneStatus", "inserting");
815                 if (soneInsertingEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
816                         notificationManager.addNotification(soneInsertNotification);
817                 }
818         }
819
820         /**
821          * Notifies the web interface that a {@link Sone} was inserted.
822          *
823          * @param soneInsertedEvent
824          *            The event
825          */
826         @Subscribe
827         public void soneInserted(SoneInsertedEvent soneInsertedEvent) {
828                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.getSone());
829                 soneInsertNotification.set("soneStatus", "inserted");
830                 soneInsertNotification.set("insertDuration", soneInsertedEvent.getInsertDuration() / 1000);
831                 if (soneInsertedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
832                         notificationManager.addNotification(soneInsertNotification);
833                 }
834         }
835
836         /**
837          * Notifies the web interface that a {@link Sone} insert was aborted.
838          *
839          * @param soneInsertAbortedEvent
840          *            The event
841          */
842         @Subscribe
843         public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) {
844                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.getSone());
845                 soneInsertNotification.set("soneStatus", "insert-aborted");
846                 soneInsertNotification.set("insert-error", soneInsertAbortedEvent.getCause());
847                 if (soneInsertAbortedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
848                         notificationManager.addNotification(soneInsertNotification);
849                 }
850         }
851
852         /**
853          * Notifies the web interface that a new Sone version was found.
854          *
855          * @param updateFoundEvent
856          *            The event
857          */
858         @Subscribe
859         public void updateFound(UpdateFoundEvent updateFoundEvent) {
860                 newVersionNotification.set("latestVersion", updateFoundEvent.getVersion());
861                 newVersionNotification.set("latestEdition", updateFoundEvent.getLatestEdition());
862                 newVersionNotification.set("releaseTime", updateFoundEvent.getReleaseTime());
863                 newVersionNotification.set("disruptive", updateFoundEvent.isDisruptive());
864                 notificationManager.addNotification(newVersionNotification);
865         }
866
867         /**
868          * Notifies the web interface that an image insert was started
869          *
870          * @param imageInsertStartedEvent
871          *            The event
872          */
873         @Subscribe
874         public void imageInsertStarted(ImageInsertStartedEvent imageInsertStartedEvent) {
875                 insertingImagesNotification.add(imageInsertStartedEvent.getImage());
876                 notificationManager.addNotification(insertingImagesNotification);
877         }
878
879         /**
880          * Notifies the web interface that an {@link Image} insert was aborted.
881          *
882          * @param imageInsertAbortedEvent
883          *            The event
884          */
885         @Subscribe
886         public void imageInsertAborted(ImageInsertAbortedEvent imageInsertAbortedEvent) {
887                 insertingImagesNotification.remove(imageInsertAbortedEvent.getImage());
888         }
889
890         /**
891          * Notifies the web interface that an {@link Image} insert is finished.
892          *
893          * @param imageInsertFinishedEvent
894          *            The event
895          */
896         @Subscribe
897         public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
898                 insertingImagesNotification.remove(imageInsertFinishedEvent.getImage());
899                 insertedImagesNotification.add(imageInsertFinishedEvent.getImage());
900                 notificationManager.addNotification(insertedImagesNotification);
901         }
902
903         /**
904          * Notifies the web interface that an {@link Image} insert has failed.
905          *
906          * @param imageInsertFailedEvent
907          *            The event
908          */
909         @Subscribe
910         public void imageInsertFailed(ImageInsertFailedEvent imageInsertFailedEvent) {
911                 insertingImagesNotification.remove(imageInsertFailedEvent.getImage());
912                 imageInsertFailedNotification.add(imageInsertFailedEvent.getImage());
913                 notificationManager.addNotification(imageInsertFailedNotification);
914         }
915
916         @Subscribe
917         public void debugActivated(@Nonnull DebugActivatedEvent debugActivatedEvent) {
918                 pageToadletRegistry.activateDebugMode();
919         }
920
921 }