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