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