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