♻️ Move web of trust notification into handler
[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.HashMap;
25 import java.util.HashSet;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.TimeZone;
29 import java.util.UUID;
30 import java.util.concurrent.Executors;
31 import java.util.concurrent.ScheduledExecutorService;
32 import java.util.logging.Logger;
33 import javax.annotation.Nonnull;
34 import javax.annotation.Nullable;
35 import javax.inject.Named;
36
37 import net.pterodactylus.sone.core.Core;
38 import net.pterodactylus.sone.core.ElementLoader;
39 import net.pterodactylus.sone.core.event.*;
40 import net.pterodactylus.sone.data.Post;
41 import net.pterodactylus.sone.data.PostReply;
42 import net.pterodactylus.sone.data.Sone;
43 import net.pterodactylus.sone.freenet.L10nFilter;
44 import net.pterodactylus.sone.freenet.Translation;
45 import net.pterodactylus.sone.main.Loaders;
46 import net.pterodactylus.sone.main.PluginHomepage;
47 import net.pterodactylus.sone.main.PluginVersion;
48 import net.pterodactylus.sone.main.PluginYear;
49 import net.pterodactylus.sone.main.SonePlugin;
50 import net.pterodactylus.sone.notify.ListNotification;
51 import net.pterodactylus.sone.notify.ListNotificationFilter;
52 import net.pterodactylus.sone.notify.PostVisibilityFilter;
53 import net.pterodactylus.sone.notify.ReplyVisibilityFilter;
54 import net.pterodactylus.sone.template.LinkedElementRenderFilter;
55 import net.pterodactylus.sone.template.ParserFilter;
56 import net.pterodactylus.sone.template.RenderFilter;
57 import net.pterodactylus.sone.template.ShortenFilter;
58 import net.pterodactylus.sone.text.Part;
59 import net.pterodactylus.sone.text.SonePart;
60 import net.pterodactylus.sone.text.SoneTextParser;
61 import net.pterodactylus.sone.text.TimeTextConverter;
62 import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
63 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
64 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
65 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
66 import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
67 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
68 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
69 import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage;
70 import net.pterodactylus.sone.web.ajax.EditImageAjaxPage;
71 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
72 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
73 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
74 import net.pterodactylus.sone.web.ajax.GetLinkedElementAjaxPage;
75 import net.pterodactylus.sone.web.ajax.GetNotificationsAjaxPage;
76 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
77 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
78 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
79 import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
80 import net.pterodactylus.sone.web.ajax.GetTranslationAjaxPage;
81 import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
82 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
83 import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
84 import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
85 import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage;
86 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
87 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
88 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
89 import net.pterodactylus.sone.web.page.FreenetRequest;
90 import net.pterodactylus.sone.web.page.TemplateRenderer;
91 import net.pterodactylus.sone.web.pages.*;
92 import net.pterodactylus.util.notify.Notification;
93 import net.pterodactylus.util.notify.NotificationManager;
94 import net.pterodactylus.util.notify.TemplateNotification;
95 import net.pterodactylus.util.template.Template;
96 import net.pterodactylus.util.template.TemplateContextFactory;
97 import net.pterodactylus.util.web.RedirectPage;
98 import net.pterodactylus.util.web.TemplatePage;
99
100 import freenet.clients.http.SessionManager;
101 import freenet.clients.http.SessionManager.Session;
102 import freenet.clients.http.ToadletContext;
103
104 import com.codahale.metrics.*;
105 import com.google.common.base.Optional;
106 import com.google.common.collect.Collections2;
107 import com.google.common.collect.ImmutableSet;
108 import com.google.common.eventbus.Subscribe;
109 import com.google.inject.Inject;
110
111 /**
112  * Bundles functionality that a web interface of a Freenet plugin needs, e.g.
113  * references to l10n helpers.
114  */
115 public class WebInterface implements SessionProvider {
116
117         /** The logger. */
118         private static final Logger logger = getLogger(WebInterface.class.getName());
119
120         /** The loaders for templates, pages, and classpath providers. */
121         private final Loaders loaders;
122
123         /** The notification manager. */
124         private final NotificationManager notificationManager;
125
126         /** The Sone plugin. */
127         private final SonePlugin sonePlugin;
128
129         /** The form password. */
130         private final String formPassword;
131
132         /** The template context factory. */
133         private final TemplateContextFactory templateContextFactory;
134         private final TemplateRenderer templateRenderer;
135
136         /** The Sone text parser. */
137         private final SoneTextParser soneTextParser;
138
139         /** The parser filter. */
140         private final ParserFilter parserFilter;
141         private final ShortenFilter shortenFilter;
142         private final RenderFilter renderFilter;
143
144         private final ListNotificationFilter listNotificationFilter;
145         private final PostVisibilityFilter postVisibilityFilter;
146         private final ReplyVisibilityFilter replyVisibilityFilter;
147
148         private final ElementLoader elementLoader;
149         private final LinkedElementRenderFilter linkedElementRenderFilter;
150         private final TimeTextConverter timeTextConverter = new TimeTextConverter();
151         private final L10nFilter l10nFilter;
152
153         private final PageToadletRegistry pageToadletRegistry;
154         private final MetricRegistry metricRegistry;
155         private final Translation translation;
156
157         /** The “new post” notification. */
158         private final ListNotification<Post> newPostNotification;
159
160         /** The “new reply” notification. */
161         private final ListNotification<PostReply> newReplyNotification;
162
163         /** The invisible “local post” notification. */
164         private final ListNotification<Post> localPostNotification;
165
166         /** The invisible “local reply” notification. */
167         private final ListNotification<PostReply> localReplyNotification;
168
169         /** The “you have been mentioned” notification. */
170         private final ListNotification<Post> mentionNotification;
171
172         /** Notifications for sone inserts. */
173         private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<>();
174
175         /** Scheduled executor for time-based notifications. */
176         private final ScheduledExecutorService ticker = Executors.newScheduledThreadPool(1);
177
178         @Inject
179         public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter,
180                         PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter,
181                         ElementLoader elementLoader, TemplateContextFactory templateContextFactory,
182                         TemplateRenderer templateRenderer,
183                         ParserFilter parserFilter, ShortenFilter shortenFilter,
184                         RenderFilter renderFilter,
185                         LinkedElementRenderFilter linkedElementRenderFilter,
186                         PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter,
187                         NotificationManager notificationManager, @Named("newRemotePost") ListNotification<Post> newPostNotification,
188                         @Named("localPost") ListNotification<Post> localPostNotification) {
189                 this.sonePlugin = sonePlugin;
190                 this.loaders = loaders;
191                 this.listNotificationFilter = listNotificationFilter;
192                 this.postVisibilityFilter = postVisibilityFilter;
193                 this.replyVisibilityFilter = replyVisibilityFilter;
194                 this.elementLoader = elementLoader;
195                 this.templateRenderer = templateRenderer;
196                 this.parserFilter = parserFilter;
197                 this.shortenFilter = shortenFilter;
198                 this.renderFilter = renderFilter;
199                 this.linkedElementRenderFilter = linkedElementRenderFilter;
200                 this.pageToadletRegistry = pageToadletRegistry;
201                 this.metricRegistry = metricRegistry;
202                 this.l10nFilter = l10nFilter;
203                 this.translation = translation;
204                 this.notificationManager = notificationManager;
205                 this.newPostNotification = newPostNotification;
206                 this.localPostNotification = localPostNotification;
207                 formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
208                 soneTextParser = new SoneTextParser(getCore(), getCore());
209
210                 this.templateContextFactory = templateContextFactory;
211                 templateContextFactory.addTemplateObject("webInterface", this);
212                 templateContextFactory.addTemplateObject("formPassword", formPassword);
213
214                 /* create notifications. */
215                 Template newReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
216                 newReplyNotification = new ListNotification<>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
217
218                 Template localReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
219                 localReplyNotification = new ListNotification<>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
220
221                 Template mentionNotificationTemplate = loaders.loadTemplate("/templates/notify/mentionNotification.html");
222                 mentionNotification = new ListNotification<>("mention-notification", "posts", mentionNotificationTemplate, false);
223         }
224
225         //
226         // ACCESSORS
227         //
228
229         /**
230          * Returns the Sone core used by the Sone plugin.
231          *
232          * @return The Sone core
233          */
234         @Nonnull
235         public Core getCore() {
236                 return sonePlugin.core();
237         }
238
239         /**
240          * Returns the template context factory of the web interface.
241          *
242          * @return The template context factory
243          */
244         public TemplateContextFactory getTemplateContextFactory() {
245                 return templateContextFactory;
246         }
247
248         private Session getCurrentSessionWithoutCreation(ToadletContext toadletContenxt) {
249                 return getSessionManager().useSession(toadletContenxt);
250         }
251
252         private Session getOrCreateCurrentSession(ToadletContext toadletContenxt) {
253                 Session session = getCurrentSessionWithoutCreation(toadletContenxt);
254                 if (session == null) {
255                         session = getSessionManager().createSession(UUID.randomUUID().toString(), toadletContenxt);
256                 }
257                 return session;
258         }
259
260         public Sone getCurrentSoneCreatingSession(ToadletContext toadletContext) {
261                 Collection<Sone> localSones = getCore().getLocalSones();
262                 if (localSones.size() == 1) {
263                         return localSones.iterator().next();
264                 }
265                 return getCurrentSone(getOrCreateCurrentSession(toadletContext));
266         }
267
268         public Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) {
269                 Collection<Sone> localSones = getCore().getLocalSones();
270                 if (localSones.size() == 1) {
271                         return localSones.iterator().next();
272                 }
273                 return getCurrentSone(getCurrentSessionWithoutCreation(toadletContext));
274         }
275
276         /**
277          * Returns the currently logged in Sone.
278          *
279          * @param session
280          *            The session
281          * @return The currently logged in Sone, or {@code null} if no Sone is
282          *         currently logged in
283          */
284         private Sone getCurrentSone(Session session) {
285                 if (session == null) {
286                         return null;
287                 }
288                 String soneId = (String) session.getAttribute("Sone.CurrentSone");
289                 if (soneId == null) {
290                         return null;
291                 }
292                 return getCore().getLocalSone(soneId);
293         }
294
295         @Override
296         @Nullable
297         public Sone getCurrentSone(@Nonnull ToadletContext toadletContext, boolean createSession) {
298                 return createSession ? getCurrentSoneCreatingSession(toadletContext) : getCurrentSoneWithoutCreatingSession(toadletContext);
299         }
300
301         /**
302          * Sets the currently logged in Sone.
303          *
304          * @param toadletContext
305          *            The toadlet context
306          * @param sone
307          *            The Sone to set as currently logged in
308          */
309         @Override
310         public void setCurrentSone(@Nonnull ToadletContext toadletContext, @Nullable Sone sone) {
311                 Session session = getOrCreateCurrentSession(toadletContext);
312                 if (sone == null) {
313                         session.removeAttribute("Sone.CurrentSone");
314                 } else {
315                         session.setAttribute("Sone.CurrentSone", sone.getId());
316                 }
317         }
318
319         /**
320          * Returns the notification manager.
321          *
322          * @return The notification manager
323          */
324         public NotificationManager getNotifications() {
325                 return notificationManager;
326         }
327
328         @Nonnull
329         public Optional<Notification> getNotification(@Nonnull String notificationId) {
330                 return Optional.fromNullable(notificationManager.getNotification(notificationId));
331         }
332
333         @Nonnull
334         public Collection<Notification> getNotifications(@Nullable Sone currentSone) {
335                 return listNotificationFilter.filterNotifications(notificationManager.getNotifications(), currentSone);
336         }
337
338         public Translation getTranslation() {
339                 return translation;
340         }
341
342         /**
343          * Returns the session manager of the node.
344          *
345          * @return The node’s session manager
346          */
347         public SessionManager getSessionManager() {
348                 return sonePlugin.pluginRespirator().getSessionManager("Sone");
349         }
350
351         /**
352          * Returns the node’s form password.
353          *
354          * @return The form password
355          */
356         public String getFormPassword() {
357                 return formPassword;
358         }
359
360         @Nonnull
361         public Collection<Post> getNewPosts(@Nullable Sone currentSone) {
362                 Set<Post> allNewPosts = ImmutableSet.<Post> builder()
363                                 .addAll(newPostNotification.getElements())
364                                 .addAll(localPostNotification.getElements())
365                                 .build();
366                 return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet();
367         }
368
369         /**
370          * Returns the replies that have been announced as new in the
371          * {@link #newReplyNotification}.
372          *
373          * @return The new replies
374          */
375         public Set<PostReply> getNewReplies() {
376                 return ImmutableSet.<PostReply> builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build();
377         }
378
379         @Nonnull
380         public Collection<PostReply> getNewReplies(@Nullable Sone currentSone) {
381                 Set<PostReply> allNewReplies = ImmutableSet.<PostReply>builder()
382                                 .addAll(newReplyNotification.getElements())
383                                 .addAll(localReplyNotification.getElements())
384                                 .build();
385                 return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet();
386         }
387
388         //
389         // PRIVATE ACCESSORS
390         //
391
392         /**
393          * Returns whether the first start notification is currently displayed.
394          *
395          * @return {@code true} if the first-start notification is currently
396          *         displayed, {@code false} otherwise
397          */
398         private boolean hasFirstStartNotification() {
399                 return notificationManager.getNotification("first-start-notification") != null;
400         }
401
402         //
403         // ACTIONS
404         //
405
406         /**
407          * Starts the web interface and registers all toadlets.
408          */
409         public void start() {
410                 registerToadlets();
411         }
412
413         /**
414          * Stops the web interface and unregisters all toadlets.
415          */
416         public void stop() {
417                 pageToadletRegistry.unregisterToadlets();
418                 ticker.shutdownNow();
419         }
420
421         //
422         // PRIVATE METHODS
423         //
424
425         /**
426          * Register all toadlets.
427          */
428         private void registerToadlets() {
429                 Template postTemplate = loaders.loadTemplate("/templates/include/viewPost.html");
430                 Template replyTemplate = loaders.loadTemplate("/templates/include/viewReply.html");
431                 Template openSearchTemplate = loaders.loadTemplate("/templates/xml/OpenSearch.xml");
432
433                 pageToadletRegistry.addPage(new RedirectPage<FreenetRequest>("", "index.html"));
434                 pageToadletRegistry.addPage(new IndexPage(this, loaders, templateRenderer, postVisibilityFilter));
435                 pageToadletRegistry.addPage(new NewPage(this, loaders, templateRenderer));
436                 pageToadletRegistry.addPage(new CreateSonePage(this, loaders, templateRenderer));
437                 pageToadletRegistry.addPage(new KnownSonesPage(this, loaders, templateRenderer));
438                 pageToadletRegistry.addPage(new EditProfilePage(this, loaders, templateRenderer));
439                 pageToadletRegistry.addPage(new EditProfileFieldPage(this, loaders, templateRenderer));
440                 pageToadletRegistry.addPage(new DeleteProfileFieldPage(this, loaders, templateRenderer));
441                 pageToadletRegistry.addPage(new CreatePostPage(this, loaders, templateRenderer));
442                 pageToadletRegistry.addPage(new CreateReplyPage(this, loaders, templateRenderer));
443                 pageToadletRegistry.addPage(new ViewSonePage(this, loaders, templateRenderer));
444                 pageToadletRegistry.addPage(new ViewPostPage(this, loaders, templateRenderer));
445                 pageToadletRegistry.addPage(new LikePage(this, loaders, templateRenderer));
446                 pageToadletRegistry.addPage(new UnlikePage(this, loaders, templateRenderer));
447                 pageToadletRegistry.addPage(new DeletePostPage(this, loaders, templateRenderer));
448                 pageToadletRegistry.addPage(new DeleteReplyPage(this, loaders, templateRenderer));
449                 pageToadletRegistry.addPage(new LockSonePage(this, loaders, templateRenderer));
450                 pageToadletRegistry.addPage(new UnlockSonePage(this, loaders, templateRenderer));
451                 pageToadletRegistry.addPage(new FollowSonePage(this, loaders, templateRenderer));
452                 pageToadletRegistry.addPage(new UnfollowSonePage(this, loaders, templateRenderer));
453                 pageToadletRegistry.addPage(new ImageBrowserPage(this, loaders, templateRenderer));
454                 pageToadletRegistry.addPage(new CreateAlbumPage(this, loaders, templateRenderer));
455                 pageToadletRegistry.addPage(new EditAlbumPage(this, loaders, templateRenderer));
456                 pageToadletRegistry.addPage(new DeleteAlbumPage(this, loaders, templateRenderer));
457                 pageToadletRegistry.addPage(new UploadImagePage(this, loaders, templateRenderer));
458                 pageToadletRegistry.addPage(new EditImagePage(this, loaders, templateRenderer));
459                 pageToadletRegistry.addPage(new DeleteImagePage(this, loaders, templateRenderer));
460                 pageToadletRegistry.addPage(new MarkAsKnownPage(this, loaders, templateRenderer));
461                 pageToadletRegistry.addPage(new BookmarkPage(this, loaders, templateRenderer));
462                 pageToadletRegistry.addPage(new UnbookmarkPage(this, loaders, templateRenderer));
463                 pageToadletRegistry.addPage(new BookmarksPage(this, loaders, templateRenderer));
464                 pageToadletRegistry.addPage(new SearchPage(this, loaders, templateRenderer));
465                 pageToadletRegistry.addPage(new DeleteSonePage(this, loaders, templateRenderer));
466                 pageToadletRegistry.addPage(new LoginPage(this, loaders, templateRenderer));
467                 pageToadletRegistry.addPage(new LogoutPage(this, loaders, templateRenderer));
468                 pageToadletRegistry.addPage(new OptionsPage(this, loaders, templateRenderer));
469                 pageToadletRegistry.addPage(new RescuePage(this, loaders, templateRenderer));
470                 pageToadletRegistry.addPage(new AboutPage(this, loaders, templateRenderer, new PluginVersion(SonePlugin.getPluginVersion()), new PluginYear(sonePlugin.getYear()), new PluginHomepage(sonePlugin.getHomepage())));
471                 pageToadletRegistry.addPage(new InvalidPage(this, loaders, templateRenderer));
472                 pageToadletRegistry.addPage(new NoPermissionPage(this, loaders, templateRenderer));
473                 pageToadletRegistry.addPage(new EmptyImageTitlePage(this, loaders, templateRenderer));
474                 pageToadletRegistry.addPage(new EmptyAlbumTitlePage(this, loaders, templateRenderer));
475                 pageToadletRegistry.addPage(new DismissNotificationPage(this, loaders, templateRenderer));
476                 pageToadletRegistry.addPage(new DebugPage(this, loaders, templateRenderer));
477                 pageToadletRegistry.addDebugPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry));
478                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
479                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
480                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
481                 pageToadletRegistry.addPage(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate));
482                 pageToadletRegistry.addPage(new GetImagePage(this));
483                 pageToadletRegistry.addPage(new GetTranslationAjaxPage(this));
484                 pageToadletRegistry.addPage(new GetStatusAjaxPage(this, elementLoader, timeTextConverter, l10nFilter, TimeZone.getDefault()));
485                 pageToadletRegistry.addPage(new GetNotificationsAjaxPage(this));
486                 pageToadletRegistry.addPage(new DismissNotificationAjaxPage(this));
487                 pageToadletRegistry.addPage(new CreatePostAjaxPage(this));
488                 pageToadletRegistry.addPage(new CreateReplyAjaxPage(this));
489                 pageToadletRegistry.addPage(new GetReplyAjaxPage(this, replyTemplate));
490                 pageToadletRegistry.addPage(new GetPostAjaxPage(this, postTemplate));
491                 pageToadletRegistry.addPage(new GetLinkedElementAjaxPage(this, elementLoader, linkedElementRenderFilter));
492                 pageToadletRegistry.addPage(new GetTimesAjaxPage(this, timeTextConverter, l10nFilter, TimeZone.getDefault()));
493                 pageToadletRegistry.addPage(new MarkAsKnownAjaxPage(this));
494                 pageToadletRegistry.addPage(new DeletePostAjaxPage(this));
495                 pageToadletRegistry.addPage(new DeleteReplyAjaxPage(this));
496                 pageToadletRegistry.addPage(new LockSoneAjaxPage(this));
497                 pageToadletRegistry.addPage(new UnlockSoneAjaxPage(this));
498                 pageToadletRegistry.addPage(new FollowSoneAjaxPage(this));
499                 pageToadletRegistry.addPage(new UnfollowSoneAjaxPage(this));
500                 pageToadletRegistry.addPage(new EditAlbumAjaxPage(this));
501                 pageToadletRegistry.addPage(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter));
502                 pageToadletRegistry.addPage(new LikeAjaxPage(this));
503                 pageToadletRegistry.addPage(new UnlikeAjaxPage(this));
504                 pageToadletRegistry.addPage(new GetLikesAjaxPage(this));
505                 pageToadletRegistry.addPage(new BookmarkAjaxPage(this));
506                 pageToadletRegistry.addPage(new UnbookmarkAjaxPage(this));
507                 pageToadletRegistry.addPage(new EditProfileFieldAjaxPage(this));
508                 pageToadletRegistry.addPage(new DeleteProfileFieldAjaxPage(this));
509                 pageToadletRegistry.addPage(new MoveProfileFieldAjaxPage(this));
510
511                 pageToadletRegistry.registerToadlets();
512         }
513
514         /**
515          * Returns all {@link Sone#isLocal() local Sone}s that are referenced by
516          * {@link SonePart}s in the given text (after parsing it using
517          * {@link SoneTextParser}).
518          *
519          * @param text
520          *            The text to parse
521          * @return All mentioned local Sones
522          */
523         private Collection<Sone> getMentionedSones(String text) {
524                 /* we need no context to find mentioned Sones. */
525                 Set<Sone> mentionedSones = new HashSet<>();
526                 for (Part part : soneTextParser.parse(text, null)) {
527                         if (part instanceof SonePart) {
528                                 mentionedSones.add(((SonePart) part).getSone());
529                         }
530                 }
531                 return Collections2.filter(mentionedSones, Sone.LOCAL_SONE_FILTER);
532         }
533
534         /**
535          * Returns the Sone insert notification for the given Sone. If no
536          * notification for the given Sone exists, a new notification is created and
537          * cached.
538          *
539          * @param sone
540          *            The Sone to get the insert notification for
541          * @return The Sone insert notification
542          */
543         private TemplateNotification getSoneInsertNotification(Sone sone) {
544                 synchronized (soneInsertNotifications) {
545                         TemplateNotification templateNotification = soneInsertNotifications.get(sone);
546                         if (templateNotification == null) {
547                                 templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
548                                 templateNotification.set("insertSone", sone);
549                                 soneInsertNotifications.put(sone, templateNotification);
550                         }
551                         return templateNotification;
552                 }
553         }
554
555         private boolean localSoneMentionedInNewPostOrReply(Post post) {
556                 if (!post.getSone().isLocal()) {
557                         if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) {
558                                 return true;
559                         }
560                 }
561                 for (PostReply postReply : getCore().getReplies(post.getId())) {
562                         if (postReply.getSone().isLocal()) {
563                                 continue;
564                         }
565                         if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) {
566                                 return true;
567                         }
568                 }
569                 return false;
570         }
571
572         //
573         // EVENT HANDLERS
574         //
575
576         /**
577          * Notifies the web interface that a new {@link Post} was found.
578          *
579          * @param newPostFoundEvent
580          *            The event
581          */
582         @Subscribe
583         public void newPostFound(NewPostFoundEvent newPostFoundEvent) {
584                 Post post = newPostFoundEvent.getPost();
585                 boolean isLocal = post.getSone().isLocal();
586                 if (!hasFirstStartNotification()) {
587                         if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
588                                 mentionNotification.add(post);
589                                 notificationManager.addNotification(mentionNotification);
590                         }
591                 }
592         }
593
594         /**
595          * Notifies the web interface that a new {@link PostReply} was found.
596          *
597          * @param newPostReplyFoundEvent
598          *            The event
599          */
600         @Subscribe
601         public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) {
602                 PostReply reply = newPostReplyFoundEvent.getPostReply();
603                 boolean isLocal = reply.getSone().isLocal();
604                 if (isLocal) {
605                         localReplyNotification.add(reply);
606                 } else {
607                         newReplyNotification.add(reply);
608                 }
609                 if (!hasFirstStartNotification()) {
610                         notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
611                         if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
612                                 mentionNotification.add(reply.getPost().get());
613                                 notificationManager.addNotification(mentionNotification);
614                         }
615                 } else {
616                         getCore().markReplyKnown(reply);
617                 }
618         }
619
620         @Subscribe
621         public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
622                 removePost(markPostKnownEvent.getPost());
623         }
624
625         @Subscribe
626         public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) {
627                 removeReply(markPostReplyKnownEvent.getPostReply());
628         }
629
630         @Subscribe
631         public void postRemoved(PostRemovedEvent postRemovedEvent) {
632                 removePost(postRemovedEvent.getPost());
633         }
634
635         private void removePost(Post post) {
636                 newPostNotification.remove(post);
637                 if (!localSoneMentionedInNewPostOrReply(post)) {
638                         mentionNotification.remove(post);
639                 }
640         }
641
642         @Subscribe
643         public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) {
644                 removeReply(postReplyRemovedEvent.getPostReply());
645         }
646
647         private void removeReply(PostReply reply) {
648                 newReplyNotification.remove(reply);
649                 localReplyNotification.remove(reply);
650                 if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
651                         mentionNotification.remove(reply.getPost().get());
652                 }
653         }
654
655         /**
656          * Notifies the web interface that a {@link Sone} is being inserted.
657          *
658          * @param soneInsertingEvent
659          *            The event
660          */
661         @Subscribe
662         public void soneInserting(SoneInsertingEvent soneInsertingEvent) {
663                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.getSone());
664                 soneInsertNotification.set("soneStatus", "inserting");
665                 if (soneInsertingEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
666                         notificationManager.addNotification(soneInsertNotification);
667                 }
668         }
669
670         /**
671          * Notifies the web interface that a {@link Sone} was inserted.
672          *
673          * @param soneInsertedEvent
674          *            The event
675          */
676         @Subscribe
677         public void soneInserted(SoneInsertedEvent soneInsertedEvent) {
678                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.getSone());
679                 soneInsertNotification.set("soneStatus", "inserted");
680                 soneInsertNotification.set("insertDuration", soneInsertedEvent.getInsertDuration() / 1000);
681                 if (soneInsertedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
682                         notificationManager.addNotification(soneInsertNotification);
683                 }
684         }
685
686         /**
687          * Notifies the web interface that a {@link Sone} insert was aborted.
688          *
689          * @param soneInsertAbortedEvent
690          *            The event
691          */
692         @Subscribe
693         public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) {
694                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.getSone());
695                 soneInsertNotification.set("soneStatus", "insert-aborted");
696                 soneInsertNotification.set("insert-error", soneInsertAbortedEvent.getCause());
697                 if (soneInsertAbortedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
698                         notificationManager.addNotification(soneInsertNotification);
699                 }
700         }
701
702         @Subscribe
703         public void debugActivated(@Nonnull DebugActivatedEvent debugActivatedEvent) {
704                 pageToadletRegistry.activateDebugMode();
705         }
706
707 }