From: David ‘Bombe’ Roden Date: Tue, 28 Apr 2020 08:44:24 +0000 (+0200) Subject: 🔀 Merge branch 'release/v82' X-Git-Tag: v82^0 X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=commitdiff_plain;h=HEAD;hp=faf66247a34f64946990a985d2ea3003465969cb 🔀 Merge branch 'release/v82' --- diff --git a/build.gradle b/build.gradle index 3730ead..db5af2b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,13 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id 'org.jetbrains.kotlin.jvm' version '1.3.61' - id 'org.jetbrains.kotlin.plugin.noarg' version '1.3.61' + id 'org.jetbrains.kotlin.jvm' version '1.3.70' + id 'org.jetbrains.kotlin.plugin.noarg' version '1.3.70' id 'info.solidsoft.pitest' version '1.4.5' } group = 'net.pterodactylus' -version = '81' +version = '82' repositories { mavenCentral() @@ -22,6 +23,12 @@ tasks.withType(JavaCompile) { options.encoding = 'UTF-8' } +tasks.withType(KotlinCompile) { + kotlinOptions { + jvmTarget = "1.8" + } +} + configurations { provided { dependencies.all { dep -> @@ -30,7 +37,7 @@ configurations { } compile.extendsFrom provided } - + dependencies { provided group: 'org.freenetproject', name: 'fred', version: '0.7.5.1475' provided group: 'org.freenetproject', name: 'freenet-ext', version: '29' @@ -69,6 +76,7 @@ task notParallelTest(type: Test) { useJUnit { includeCategories 'net.pterodactylus.sone.test.NotParallel' } + dependsOn parallelTest } test { @@ -77,7 +85,7 @@ test { } task fatJar(type: Jar) { - archiveName = project.name.toLowerCase() + '-jar-with-dependencies.jar' + archiveFileName = project.name.toLowerCase() + '-jar-with-dependencies.jar' from { (configurations.runtime - configurations.provided).collect { it.isDirectory() ? it : zipTree(it) } } manifest { attributes('Plugin-Main-Class': 'net.pterodactylus.sone.main.SonePlugin') diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index 18588a1..da1b2f8 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -24,7 +24,7 @@ import static com.google.common.primitives.Longs.tryParse; import static java.lang.String.format; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; -import static net.pterodactylus.sone.data.AlbumsKt.getAllImages; +import static net.pterodactylus.sone.data.AlbumKt.getAllImages; import java.util.ArrayList; import java.util.Collection; @@ -62,6 +62,7 @@ import net.pterodactylus.sone.data.Profile.Field; import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.SoneStatus; +import net.pterodactylus.sone.data.SoneKt; import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent; import net.pterodactylus.sone.data.TemporaryImage; import net.pterodactylus.sone.database.AlbumBuilder; @@ -90,7 +91,6 @@ import net.pterodactylus.util.thread.NamedThreadFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; -import com.google.common.collect.FluentIterable; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; @@ -180,24 +180,10 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, private final MetricRegistry metricRegistry; private final Histogram configurationSaveTimeHistogram; - /** - * Creates a new core. - * - * @param configuration - * The configuration of the core - * @param freenetInterface - * The freenet interface - * @param identityManager - * The identity manager - * @param webOfTrustUpdater - * The WebOfTrust updater - * @param eventBus - * The event bus - * @param database - * The database - */ + private final SoneUriCreator soneUriCreator; + @Inject - public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database, MetricRegistry metricRegistry) { + public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database, MetricRegistry metricRegistry, SoneUriCreator soneUriCreator) { super("Sone Core"); this.configuration = configuration; this.freenetInterface = freenetInterface; @@ -209,6 +195,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, this.eventBus = eventBus; this.database = database; this.metricRegistry = metricRegistry; + this.soneUriCreator = soneUriCreator; preferences = new Preferences(eventBus); this.configurationSaveTimeHistogram = metricRegistry.histogram("configuration.save.duration", () -> new Histogram(new ExponentiallyDecayingReservoir(3000, 0))); } @@ -626,7 +613,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, sone.setLatestEdition(fromNullable(tryParse(property)).or(0L)); sone.setClient(new Client("Sone", SonePlugin.getPluginVersion())); sone.setKnown(true); - SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, metricRegistry, ownIdentity.getId()); + SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, metricRegistry, soneUriCreator, ownIdentity.getId()); soneInserter.insertionDelayChanged(new InsertionDelayChangedEvent(preferences.getInsertionDelay())); eventBus.register(soneInserter); synchronized (soneInserters) { @@ -810,9 +797,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, } for (PostReply postReply : soneComparison.getNewPostReplies()) { if (postReply.getSone().equals(newSone)) { - postReply.setKnown(true); + database.setPostReplyKnown(postReply); } else if (postReply.getTime() < database.getFollowingTime(newSone.getId())) { - postReply.setKnown(true); + database.setPostReplyKnown(postReply); } else if (!postReply.isKnown()) { events.add(new NewPostReplyFoundEvent(postReply)); } @@ -989,7 +976,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, post.setKnown(true); } for (PostReply reply : replies) { - reply.setKnown(true); + database.setPostReplyKnown(reply); } logger.info(String.format("Sone loaded successfully: %s", sone)); @@ -1131,7 +1118,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ public void markReplyKnown(PostReply reply) { boolean previouslyKnown = reply.isKnown(); - reply.setKnown(true); + database.setPostReplyKnown(reply); eventBus.post(new MarkPostReplyKnownEvent(reply)); if (!previouslyKnown) { touchConfiguration(); @@ -1405,7 +1392,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null); /* save albums. first, collect in a flat structure, top-level first. */ - List albums = FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList(); + List albums = SoneKt.getAllAlbums(sone); int albumCounter = 0; for (Album album : albums) { diff --git a/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java b/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java index a01a2bb..35ac6a1 100644 --- a/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java +++ b/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java @@ -91,6 +91,8 @@ public class FreenetInterface { /** The node to interact with. */ private final Node node; + private final SoneUriCreator soneUriCreator; + /** The high-level client to use for requests. */ private final HighLevelSimpleClient client; private final RequestClient requestClient = new RequestClientBuilder().realTime().build(); @@ -104,18 +106,11 @@ public class FreenetInterface { private final RequestClient imageInserts = new RequestClientBuilder().realTime().build(); private final RequestClient imageLoader = new RequestClientBuilder().realTime().build(); - /** - * Creates a new Freenet interface. - * - * @param eventBus - * The event bus - * @param node - * The node to interact with - */ @Inject - public FreenetInterface(EventBus eventBus, Node node) { + public FreenetInterface(EventBus eventBus, Node node, SoneUriCreator soneUriCreator) { this.eventBus = eventBus; this.node = node; + this.soneUriCreator = soneUriCreator; this.client = node.clientCore.makeClient(RequestStarter.INTERACTIVE_PRIORITY_CLASS, false, true); } @@ -291,9 +286,9 @@ public class FreenetInterface { } try { logger.log(Level.FINEST, String.format("Unsubscribing from USK for %s…", sone)); - node.clientCore.uskManager.unsubscribe(USK.create(sone.getRequestUri()), uskCallback); + node.clientCore.uskManager.unsubscribe(USK.create(soneUriCreator.getRequestUri(sone)), uskCallback); } catch (MalformedURLException mue1) { - logger.log(Level.FINE, String.format("Could not unsubscribe USK “%s”!", sone.getRequestUri()), mue1); + logger.log(Level.FINE, String.format("Could not unsubscribe USK “%s”!", soneUriCreator.getRequestUri(sone)), mue1); } } diff --git a/src/main/java/net/pterodactylus/sone/core/Options.java b/src/main/java/net/pterodactylus/sone/core/Options.java index 88e33a0..398374a 100644 --- a/src/main/java/net/pterodactylus/sone/core/Options.java +++ b/src/main/java/net/pterodactylus/sone/core/Options.java @@ -23,8 +23,6 @@ import java.util.Map; import net.pterodactylus.sone.utils.Option; -import com.google.common.base.Predicate; - /** * Stores various options that influence Sone’s behaviour. */ diff --git a/src/main/java/net/pterodactylus/sone/core/PreferenceChangedEvent.kt b/src/main/java/net/pterodactylus/sone/core/PreferenceChangedEvent.kt deleted file mode 100644 index 2ebb62d..0000000 --- a/src/main/java/net/pterodactylus/sone/core/PreferenceChangedEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.pterodactylus.sone.core - -data class PreferenceChangedEvent(val preferenceName: String, val newValue: Any) diff --git a/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt deleted file mode 100644 index 32c35cb..0000000 --- a/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt +++ /dev/null @@ -1,58 +0,0 @@ -package net.pterodactylus.sone.core - -import net.pterodactylus.sone.fcp.FcpInterface.* -import net.pterodactylus.util.config.* - -/** - * Loads preferences stored in a [Configuration] into a [Preferences] object. - */ -class PreferencesLoader(private val preferences: Preferences) { - - fun loadFrom(configuration: Configuration) { - loadInsertionDelay(configuration) - loadPostsPerPage(configuration) - loadImagesPerPage(configuration) - loadCharactersPerPost(configuration) - loadPostCutOffLength(configuration) - loadRequireFullAccess(configuration) - loadFcpInterfaceActive(configuration) - loadFcpFullAccessRequired(configuration) - } - - private fun loadInsertionDelay(configuration: Configuration) { - preferences.newInsertionDelay = configuration.getIntValue("Option/InsertionDelay").getValue(null) - } - - private fun loadPostsPerPage(configuration: Configuration) { - preferences.newPostsPerPage = configuration.getIntValue("Option/PostsPerPage").getValue(null) - } - - private fun loadImagesPerPage(configuration: Configuration) { - preferences.newImagesPerPage = configuration.getIntValue("Option/ImagesPerPage").getValue(null) - } - - private fun loadCharactersPerPost(configuration: Configuration) { - preferences.newCharactersPerPost = configuration.getIntValue("Option/CharactersPerPost").getValue(null) - } - - private fun loadPostCutOffLength(configuration: Configuration) { - try { - preferences.newPostCutOffLength = configuration.getIntValue("Option/PostCutOffLength").getValue(null) - } catch (iae1: IllegalArgumentException) { /* previous versions allowed -1, ignore and use default. */ - } - } - - private fun loadRequireFullAccess(configuration: Configuration) { - preferences.newRequireFullAccess = configuration.getBooleanValue("Option/RequireFullAccess").getValue(null) - } - - private fun loadFcpInterfaceActive(configuration: Configuration) { - preferences.newFcpInterfaceActive = configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null) - } - - private fun loadFcpFullAccessRequired(configuration: Configuration) { - val fullAccessRequiredInteger = configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null) - preferences.newFcpFullAccessRequired = fullAccessRequiredInteger?.let { FullAccessRequired.values()[it] } - } - -} diff --git a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java index 0538c8b..43a87fb 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java @@ -17,7 +17,6 @@ package net.pterodactylus.sone.core; -import static freenet.support.io.Closer.close; import static java.lang.String.format; import static java.lang.System.currentTimeMillis; import static java.util.concurrent.TimeUnit.DAYS; @@ -185,11 +184,8 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade private Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) { logger.finest(() -> format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone)); Bucket soneBucket = fetchResult.asBucket(); - InputStream soneInputStream = null; - try { - soneInputStream = soneBucket.getInputStream(); - Sone parsedSone = soneParser.parseSone(originalSone, - soneInputStream); + try (InputStream soneInputStream = soneBucket.getInputStream()) { + Sone parsedSone = soneParser.parseSone(originalSone, soneInputStream); if (parsedSone != null) { logger.finer(() -> format("Sone %s was successfully parsed.", parsedSone)); parsedSone.setLatestEdition(requestUri.getEdition()); @@ -198,8 +194,7 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade } catch (Exception e1) { logger.log(Level.WARNING, e1, () -> format("Could not parse Sone from %s!", requestUri)); } finally { - close(soneInputStream); - close(soneBucket); + soneBucket.free(); } return null; } diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java index 7ccb374..fcbe1b5 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java @@ -21,18 +21,16 @@ import static java.lang.String.format; import static java.lang.System.currentTimeMillis; import static java.util.concurrent.TimeUnit.*; import static java.util.logging.Logger.getLogger; -import static net.pterodactylus.sone.data.Album.NOT_EMPTY; +import static java.util.stream.Collectors.toList; +import static net.pterodactylus.sone.data.PostKt.newestPostFirst; +import static net.pterodactylus.sone.data.ReplyKt.newestReplyFirst; -import java.io.Closeable; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.StringWriter; +import java.io.*; import java.nio.charset.Charset; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -44,13 +42,11 @@ import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent; import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent; import net.pterodactylus.sone.core.event.SoneInsertedEvent; import net.pterodactylus.sone.core.event.SoneInsertingEvent; -import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.Reply; +import net.pterodactylus.sone.data.AlbumKt; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.SoneStatus; +import net.pterodactylus.sone.data.SoneKt; import net.pterodactylus.sone.main.SonePlugin; -import net.pterodactylus.util.io.Closer; import net.pterodactylus.util.service.AbstractService; import net.pterodactylus.util.template.HtmlFilter; import net.pterodactylus.util.template.ReflectionAccessor; @@ -62,7 +58,6 @@ import net.pterodactylus.util.template.TemplateParser; import net.pterodactylus.util.template.XmlFilter; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.FluentIterable; import com.google.common.collect.Ordering; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; @@ -106,6 +101,7 @@ public class SoneInserter extends AbstractService { private final FreenetInterface freenetInterface; private final SoneModificationDetector soneModificationDetector; + private final SoneUriCreator soneUriCreator; private final long delay; private final String soneId; private final Histogram soneInsertDurationHistogram; @@ -123,8 +119,8 @@ public class SoneInserter extends AbstractService { * @param soneId * The ID of the Sone to insert */ - public SoneInserter(final Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, final String soneId) { - this(core, eventBus, freenetInterface, metricRegistry, soneId, new SoneModificationDetector(new LockableFingerprintProvider() { + public SoneInserter(final Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, SoneUriCreator soneUriCreator, final String soneId) { + this(core, eventBus, freenetInterface, metricRegistry, soneUriCreator, soneId, new SoneModificationDetector(new LockableFingerprintProvider() { @Override public boolean isLocked() { Sone sone = core.getSone(soneId); @@ -146,13 +142,14 @@ public class SoneInserter extends AbstractService { } @VisibleForTesting - SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, String soneId, SoneModificationDetector soneModificationDetector, long delay) { + SoneInserter(Core core, EventBus eventBus, FreenetInterface freenetInterface, MetricRegistry metricRegistry, SoneUriCreator soneUriCreator, String soneId, SoneModificationDetector soneModificationDetector, long delay) { super("Sone Inserter for “" + soneId + "”", false); this.core = core; this.eventBus = eventBus; this.freenetInterface = freenetInterface; this.soneInsertDurationHistogram = metricRegistry.histogram("sone.insert.duration", () -> new Histogram(new ExponentiallyDecayingReservoir(3000, 0))); this.soneInsertErrorMeter = metricRegistry.meter("sone.insert.errors"); + this.soneUriCreator = soneUriCreator; this.soneId = soneId; this.soneModificationDetector = soneModificationDetector; this.delay = delay; @@ -237,7 +234,7 @@ public class SoneInserter extends AbstractService { long insertTime = currentTimeMillis(); eventBus.post(new SoneInsertingEvent(sone)); Stopwatch stopwatch = Stopwatch.createStarted(); - FreenetURI finalUri = freenetInterface.insertDirectory(sone.getInsertUri(), insertInformation.generateManifestEntries(), "index.html"); + FreenetURI finalUri = freenetInterface.insertDirectory(soneUriCreator.getInsertUri(sone), insertInformation.generateManifestEntries(), "index.html"); stopwatch.stop(); soneInsertDurationHistogram.update(stopwatch.elapsed(MICROSECONDS)); eventBus.post(new SoneInsertedEvent(sone, stopwatch.elapsed(MILLISECONDS), insertInformation.getFingerprint())); @@ -310,13 +307,12 @@ public class SoneInserter extends AbstractService { soneProperties.put("id", sone.getId()); soneProperties.put("name", sone.getName()); soneProperties.put("time", currentTimeMillis()); - soneProperties.put("requestUri", sone.getRequestUri()); soneProperties.put("profile", sone.getProfile()); - soneProperties.put("posts", Ordering.from(Post.NEWEST_FIRST).sortedCopy(sone.getPosts())); - soneProperties.put("replies", Ordering.from(Reply.TIME_COMPARATOR).reverse().sortedCopy(sone.getReplies())); + soneProperties.put("posts", Ordering.from(newestPostFirst()).sortedCopy(sone.getPosts())); + soneProperties.put("replies", Ordering.from(newestReplyFirst()).sortedCopy(sone.getReplies())); soneProperties.put("likedPostIds", new HashSet<>(sone.getLikedPostIds())); soneProperties.put("likedReplyIds", new HashSet<>(sone.getLikedReplyIds())); - soneProperties.put("albums", FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).filter(NOT_EMPTY).toList()); + soneProperties.put("albums", SoneKt.getAllAlbums(sone).stream().filter(AlbumKt.notEmpty()::invoke).collect(toList())); manifestCreator = new ManifestCreator(core, soneProperties); } @@ -377,19 +373,13 @@ public class SoneInserter extends AbstractService { } public ManifestElement createManifestElement(String name, String contentType, String templateName) { - InputStreamReader templateInputStreamReader = null; - InputStream templateInputStream = null; Template template; - try { - templateInputStream = getClass().getResourceAsStream(templateName); - templateInputStreamReader = new InputStreamReader(templateInputStream, utf8Charset); + try (InputStream templateInputStream = getClass().getResourceAsStream(templateName); + InputStreamReader templateInputStreamReader = new InputStreamReader(templateInputStream, utf8Charset)) { template = TemplateParser.parse(templateInputStreamReader); - } catch (TemplateException te1) { - logger.log(Level.SEVERE, String.format("Could not parse template “%s”!", templateName), te1); + } catch (IOException | TemplateException e1) { + logger.log(Level.SEVERE, String.format("Could not parse template “%s”!", templateName), e1); return null; - } finally { - Closer.close(templateInputStreamReader); - Closer.close(templateInputStream); } TemplateContext templateContext = templateContextFactory.createTemplateContext(); @@ -397,17 +387,14 @@ public class SoneInserter extends AbstractService { templateContext.set("currentSone", soneProperties); templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition()); templateContext.set("version", SonePlugin.getPluginVersion()); - StringWriter writer = new StringWriter(); - try { + try (StringWriter writer = new StringWriter()) { template.render(templateContext, writer); RandomAccessBucket bucket = new ArrayBucket(writer.toString().getBytes(Charsets.UTF_8)); buckets.add(bucket); return new ManifestElement(name, bucket, contentType, bucket.size()); - } catch (TemplateException te1) { - logger.log(Level.SEVERE, String.format("Could not render template “%s”!", templateName), te1); + } catch (IOException | TemplateException e1) { + logger.log(Level.SEVERE, String.format("Could not render template “%s”!", templateName), e1); return null; - } finally { - Closer.close(writer); } } diff --git a/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java b/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java index e5e20d9..793d332 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneModificationDetector.java @@ -1,7 +1,5 @@ package net.pterodactylus.sone.core; -import static com.google.common.base.Optional.absent; -import static com.google.common.base.Optional.of; import static com.google.common.base.Ticker.systemTicker; import static java.util.concurrent.TimeUnit.NANOSECONDS; @@ -11,7 +9,6 @@ import net.pterodactylus.sone.data.Sone; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; -import com.google.common.base.Optional; import com.google.common.base.Ticker; /** diff --git a/src/main/java/net/pterodactylus/sone/core/SoneUri.java b/src/main/java/net/pterodactylus/sone/core/SoneUri.java deleted file mode 100644 index a929950..0000000 --- a/src/main/java/net/pterodactylus/sone/core/SoneUri.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Sone - SoneUri.java - Copyright © 2013–2020 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.sone.core; - -import static java.util.logging.Logger.getLogger; - -import java.net.MalformedURLException; -import java.util.logging.Level; -import java.util.logging.Logger; - -import freenet.keys.FreenetURI; - -/** - * Helper class that creates {@link FreenetURI}s for Sone to insert to and - * request from. - */ -public class SoneUri { - - /** The logger. */ - private static final Logger logger = getLogger(SoneUri.class.getName()); - - /** - * Generate a Sone URI from the given URI. - * - * @param uri - * The URI to derive the Sone URI from - * @return The derived URI - */ - public static FreenetURI create(String uri) { - try { - return new FreenetURI(uri).setDocName("Sone").setMetaString(new String[0]); - } catch (MalformedURLException mue1) { - /* this should never happen. */ - logger.log(Level.WARNING, String.format("Could not create Sone URI from URI: %s", uri), mue1); - return null; - } - } - -} diff --git a/src/main/java/net/pterodactylus/sone/data/Album.java b/src/main/java/net/pterodactylus/sone/data/Album.java index 576c73a..bf8f3ec 100644 --- a/src/main/java/net/pterodactylus/sone/data/Album.java +++ b/src/main/java/net/pterodactylus/sone/data/Album.java @@ -17,78 +17,13 @@ package net.pterodactylus.sone.data; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; - -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import javax.annotation.Nonnull; - -import com.google.common.base.Function; -import com.google.common.base.Predicate; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableList; /** * Container for images that can also contain nested {@link Album}s. */ public interface Album extends Identified, Fingerprintable { - /** Function that flattens the given album and all albums beneath it. */ - Function> FLATTENER = new Function>() { - - @Override - @Nonnull - public List apply(Album album) { - if (album == null) { - return emptyList(); - } - List albums = new ArrayList<>(); - albums.add(album); - for (Album subAlbum : album.getAlbums()) { - albums.addAll(FluentIterable.from(ImmutableList.of(subAlbum)).transformAndConcat(FLATTENER).toList()); - } - return albums; - } - }; - - /** Function that transforms an album into the images it contains. */ - Function> IMAGES = new Function>() { - - @Override - @Nonnull - public List apply(Album album) { - return (album != null) ? album.getImages() : Collections.emptyList(); - } - }; - - /** - * Filter that removes all albums that do not have any images in any album - * below it. - */ - Predicate NOT_EMPTY = new Predicate() { - - @Override - public boolean apply(Album album) { - /* so, we flatten all albums below the given one and check whether at least one album… */ - return FluentIterable.from(asList(album)).transformAndConcat(FLATTENER).anyMatch(new Predicate() { - - @Override - public boolean apply(Album album) { - /* …contains any inserted images. */ - return !album.getImages().isEmpty() && FluentIterable.from(album.getImages()).allMatch(new Predicate() { - - @Override - public boolean apply(Image input) { - return input.isInserted(); - } - }); - } - }); - } - }; - /** * Returns the ID of this album. * diff --git a/src/main/java/net/pterodactylus/sone/data/Post.java b/src/main/java/net/pterodactylus/sone/data/Post.java index a4a794e..d4d34e6 100644 --- a/src/main/java/net/pterodactylus/sone/data/Post.java +++ b/src/main/java/net/pterodactylus/sone/data/Post.java @@ -19,10 +19,7 @@ package net.pterodactylus.sone.data; import static com.google.common.base.Optional.absent; -import java.util.Comparator; - import com.google.common.base.Optional; -import com.google.common.base.Predicate; /** * A post is a short message that a user writes in his Sone to let other users @@ -30,26 +27,6 @@ import com.google.common.base.Predicate; */ public interface Post extends Identified { - /** Comparator for posts, sorts descending by time. */ - public static final Comparator NEWEST_FIRST = new Comparator() { - - @Override - public int compare(Post leftPost, Post rightPost) { - return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, rightPost.getTime() - leftPost.getTime())); - } - - }; - - /** Filter for posts with timestamps from the future. */ - public static final Predicate FUTURE_POSTS_FILTER = new Predicate() { - - @Override - public boolean apply(Post post) { - return (post != null) && (post.getTime() <= System.currentTimeMillis()); - } - - }; - // // ACCESSORS // diff --git a/src/main/java/net/pterodactylus/sone/data/PostReply.java b/src/main/java/net/pterodactylus/sone/data/PostReply.java index f5ffea3..6db3876 100644 --- a/src/main/java/net/pterodactylus/sone/data/PostReply.java +++ b/src/main/java/net/pterodactylus/sone/data/PostReply.java @@ -18,7 +18,6 @@ package net.pterodactylus.sone.data; import com.google.common.base.Optional; -import com.google.common.base.Predicate; /** * A reply is like a {@link Post} but can never be posted on its own, it always @@ -27,18 +26,6 @@ import com.google.common.base.Predicate; public interface PostReply extends Reply { /** - * Filter that selects {@link PostReply}s that have a - * {@link Optional#isPresent() present} {@link #getPost() post}. - */ - public static final Predicate HAS_POST_FILTER = new Predicate() { - - @Override - public boolean apply(PostReply postReply) { - return (postReply != null) && postReply.getPost().isPresent(); - } - }; - - /** * Returns the ID of the post this reply refers to. * * @return The ID of the post this reply refers to diff --git a/src/main/java/net/pterodactylus/sone/data/Profile.java b/src/main/java/net/pterodactylus/sone/data/Profile.java index b87ef4f..34246cb 100644 --- a/src/main/java/net/pterodactylus/sone/data/Profile.java +++ b/src/main/java/net/pterodactylus/sone/data/Profile.java @@ -19,7 +19,6 @@ package net.pterodactylus.sone.data; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; import static java.nio.charset.StandardCharsets.UTF_8; import java.util.ArrayList; diff --git a/src/main/java/net/pterodactylus/sone/data/Reply.java b/src/main/java/net/pterodactylus/sone/data/Reply.java index 120575e..0cf2a52 100644 --- a/src/main/java/net/pterodactylus/sone/data/Reply.java +++ b/src/main/java/net/pterodactylus/sone/data/Reply.java @@ -17,10 +17,6 @@ package net.pterodactylus.sone.data; -import java.util.Comparator; - -import com.google.common.base.Predicate; - /** * Defines methods common for all replies. * @@ -29,32 +25,6 @@ import com.google.common.base.Predicate; */ public interface Reply> extends Identified { - /** Comparator that sorts replies ascending by time. */ - public static final Comparator> TIME_COMPARATOR = new Comparator>() { - - /** - * {@inheritDoc} - */ - @Override - public int compare(Reply leftReply, Reply rightReply) { - return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, leftReply.getTime() - rightReply.getTime())); - } - - }; - - /** Filter for replies with timestamps from the future. */ - public static final Predicate> FUTURE_REPLY_FILTER = new Predicate>() { - - /** - * {@inheritDoc} - */ - @Override - public boolean apply(Reply reply) { - return (reply != null) && (reply.getTime() <= System.currentTimeMillis()); - } - - }; - /** * Returns the ID of the reply. * @@ -90,13 +60,4 @@ public interface Reply> extends Identified { */ public boolean isKnown(); - /** - * Sets whether this reply is known. - * - * @param known - * {@code true} if this reply is known, {@code false} otherwise - * @return This reply - */ - public T setKnown(boolean known); - } diff --git a/src/main/java/net/pterodactylus/sone/data/Sone.java b/src/main/java/net/pterodactylus/sone/data/Sone.java index b43d025..2b0a2eb 100644 --- a/src/main/java/net/pterodactylus/sone/data/Sone.java +++ b/src/main/java/net/pterodactylus/sone/data/Sone.java @@ -17,14 +17,7 @@ package net.pterodactylus.sone.data; -import static com.google.common.collect.FluentIterable.from; -import static java.util.Arrays.asList; -import static net.pterodactylus.sone.data.Album.FLATTENER; -import static net.pterodactylus.sone.data.Album.IMAGES; - import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Set; @@ -32,15 +25,9 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import net.pterodactylus.sone.freenet.wot.Identity; -import net.pterodactylus.sone.freenet.wot.OwnIdentity; -import net.pterodactylus.sone.template.SoneAccessor; import freenet.keys.FreenetURI; -import com.google.common.base.Function; -import com.google.common.base.Predicate; -import com.google.common.primitives.Ints; - /** * A Sone defines everything about a user: her profile, her status updates, her * replies, her likes and dislikes, etc. @@ -65,101 +52,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable { downloading, } - /** comparator that sorts Sones by their nice name. */ - public static final Comparator NICE_NAME_COMPARATOR = new Comparator() { - - @Override - public int compare(Sone leftSone, Sone rightSone) { - int diff = SoneAccessor.getNiceName(leftSone).compareToIgnoreCase(SoneAccessor.getNiceName(rightSone)); - if (diff != 0) { - return diff; - } - return leftSone.getId().compareToIgnoreCase(rightSone.getId()); - } - - }; - - /** Comparator that sorts Sones by last activity (least recent active first). */ - public static final Comparator LAST_ACTIVITY_COMPARATOR = new Comparator() { - - @Override - public int compare(Sone firstSone, Sone secondSone) { - return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, secondSone.getTime() - firstSone.getTime())); - } - }; - - /** Comparator that sorts Sones by numbers of posts (descending). */ - public static final Comparator POST_COUNT_COMPARATOR = new Comparator() { - - /** - * {@inheritDoc} - */ - @Override - public int compare(Sone leftSone, Sone rightSone) { - return (leftSone.getPosts().size() != rightSone.getPosts().size()) ? (rightSone.getPosts().size() - leftSone.getPosts().size()) : (rightSone.getReplies().size() - leftSone.getReplies().size()); - } - }; - - /** Comparator that sorts Sones by number of images (descending). */ - public static final Comparator IMAGE_COUNT_COMPARATOR = new Comparator() { - - /** - * {@inheritDoc} - */ - @Override - public int compare(Sone leftSone, Sone rightSone) { - int rightSoneImageCount = from(asList(rightSone.getRootAlbum())).transformAndConcat(FLATTENER).transformAndConcat(IMAGES).size(); - int leftSoneImageCount = from(asList(leftSone.getRootAlbum())).transformAndConcat(FLATTENER).transformAndConcat(IMAGES).size(); - /* sort descending. */ - return Ints.compare(rightSoneImageCount, leftSoneImageCount); - } - }; - - /** Filter to remove Sones that have not been downloaded. */ - public static final Predicate EMPTY_SONE_FILTER = new Predicate() { - - @Override - public boolean apply(Sone sone) { - return (sone != null) && (sone.getTime() != 0); - } - }; - - /** Filter that matches all {@link Sone#isLocal() local Sones}. */ - public static final Predicate LOCAL_SONE_FILTER = new Predicate() { - - @Override - public boolean apply(Sone sone) { - return (sone != null) && (sone.getIdentity() instanceof OwnIdentity); - } - - }; - - /** Filter that matches Sones that have at least one album. */ - public static final Predicate HAS_ALBUM_FILTER = new Predicate() { - - @Override - public boolean apply(Sone sone) { - return (sone != null) && !sone.getRootAlbum().getAlbums().isEmpty(); - } - }; - - public static final Function> toAllAlbums = new Function>() { - @Override - public List apply(@Nullable Sone sone) { - return (sone == null) ? Collections.emptyList() : FLATTENER.apply( - sone.getRootAlbum()); - } - }; - - public static final Function> toAllImages = new Function>() { - @Override - public List apply(@Nullable Sone sone) { - return (sone == null) ? Collections.emptyList() : - from(FLATTENER.apply(sone.getRootAlbum())) - .transformAndConcat(IMAGES).toList(); - } - }; - /** * Returns the identity of this Sone. * @@ -192,14 +84,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable { FreenetURI getRequestUri(); /** - * Returns the insert URI of this Sone. - * - * @return The insert URI of this Sone - */ - @Nullable - FreenetURI getInsertUri(); - - /** * Returns the latest edition of this Sone. * * @return The latest edition of this Sone diff --git a/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java b/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java index e06e5a7..ddd96b9 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java @@ -53,11 +53,6 @@ public class IdOnlySone implements Sone { } @Override - public FreenetURI getInsertUri() { - return null; - } - - @Override public long getLatestEdition() { return 0; } diff --git a/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java b/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java index 1069550..f2f2ea6 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java @@ -21,6 +21,9 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.logging.Logger.getLogger; +import static net.pterodactylus.sone.data.PostKt.newestPostFirst; +import static net.pterodactylus.sone.data.ReplyKt.newestReplyFirst; +import static net.pterodactylus.sone.data.SoneKt.*; import java.net.MalformedURLException; import java.util.ArrayList; @@ -36,17 +39,16 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.AlbumKt; import net.pterodactylus.sone.data.Client; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.PostReply; import net.pterodactylus.sone.data.Profile; -import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.SoneOptions; import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions; import net.pterodactylus.sone.database.Database; import net.pterodactylus.sone.freenet.wot.Identity; -import net.pterodactylus.sone.freenet.wot.OwnIdentity; import freenet.keys.FreenetURI; @@ -191,26 +193,6 @@ public class SoneImpl implements Sone { } /** - * Returns the insert URI of this Sone. - * - * @return The insert URI of this Sone - */ - @Nullable - public FreenetURI getInsertUri() { - if (!isLocal()) { - return null; - } - try { - return new FreenetURI(((OwnIdentity) getIdentity()).getInsertUri()) - .setDocName("Sone") - .setMetaString(new String[0]) - .setSuggestedEdition(latestEdition); - } catch (MalformedURLException e) { - throw new IllegalStateException(format("Own identity %s's insert URI is incorrect.", getIdentity()), e); - } - } - - /** * Returns the latest edition of this Sone. * * @return The latest edition of this Sone @@ -384,7 +366,7 @@ public class SoneImpl implements Sone { synchronized (this) { sortedPosts = new ArrayList<>(posts); } - Collections.sort(sortedPosts, Post.NEWEST_FIRST); + sortedPosts.sort(newestPostFirst()); return sortedPosts; } @@ -644,7 +626,7 @@ public class SoneImpl implements Sone { hash.putString(")", UTF_8); List replies = new ArrayList<>(getReplies()); - Collections.sort(replies, Reply.TIME_COMPARATOR); + replies.sort(newestReplyFirst().reversed()); hash.putString("Replies(", UTF_8); for (PostReply reply : replies) { hash.putString("Reply(", UTF_8).putString(reply.getId(), UTF_8).putString(")", UTF_8); @@ -669,7 +651,7 @@ public class SoneImpl implements Sone { hash.putString("Albums(", UTF_8); for (Album album : rootAlbum.getAlbums()) { - if (!Album.NOT_EMPTY.apply(album)) { + if (!AlbumKt.notEmpty().invoke(album)) { continue; } hash.putString(album.getFingerprint(), UTF_8); @@ -686,7 +668,7 @@ public class SoneImpl implements Sone { /** {@inheritDoc} */ @Override public int compareTo(Sone sone) { - return NICE_NAME_COMPARATOR.compare(this, sone); + return niceNameComparator().compare(this, sone); } // diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt deleted file mode 100644 index 8722873..0000000 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.kt +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Sone - MemoryDatabase.kt - Copyright © 2013–2020 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.sone.database.memory - -import com.google.common.base.Preconditions.checkNotNull -import com.google.common.collect.HashMultimap -import com.google.common.collect.Multimap -import com.google.common.collect.TreeMultimap -import com.google.common.util.concurrent.* -import com.google.inject.Inject -import com.google.inject.Singleton -import net.pterodactylus.sone.data.Album -import net.pterodactylus.sone.data.Image -import net.pterodactylus.sone.data.Post -import net.pterodactylus.sone.data.PostReply -import net.pterodactylus.sone.data.Reply.TIME_COMPARATOR -import net.pterodactylus.sone.data.Sone -import net.pterodactylus.sone.data.Sone.toAllAlbums -import net.pterodactylus.sone.data.Sone.toAllImages -import net.pterodactylus.sone.data.impl.AlbumBuilderImpl -import net.pterodactylus.sone.data.impl.ImageBuilderImpl -import net.pterodactylus.sone.database.AlbumBuilder -import net.pterodactylus.sone.database.Database -import net.pterodactylus.sone.database.DatabaseException -import net.pterodactylus.sone.database.ImageBuilder -import net.pterodactylus.sone.database.PostBuilder -import net.pterodactylus.sone.database.PostDatabase -import net.pterodactylus.sone.database.PostReplyBuilder -import net.pterodactylus.sone.utils.* -import net.pterodactylus.util.config.Configuration -import net.pterodactylus.util.config.ConfigurationException -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.withLock - -/** - * Memory-based [PostDatabase] implementation. - */ -@Singleton -class MemoryDatabase @Inject constructor(private val configuration: Configuration) : AbstractService(), Database { - - private val lock = ReentrantReadWriteLock() - private val readLock by lazy { lock.readLock()!! } - private val writeLock by lazy { lock.writeLock()!! } - private val configurationLoader = ConfigurationLoader(configuration) - private val allSones = mutableMapOf() - private val allPosts = mutableMapOf() - private val sonePosts: Multimap = HashMultimap.create() - private val knownPosts = mutableSetOf() - private val allPostReplies = mutableMapOf() - private val sonePostReplies: Multimap = TreeMultimap.create(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, TIME_COMPARATOR) - private val knownPostReplies = mutableSetOf() - private val allAlbums = mutableMapOf() - private val soneAlbums: Multimap = HashMultimap.create() - private val allImages = mutableMapOf() - private val soneImages: Multimap = HashMultimap.create() - private val memoryBookmarkDatabase = MemoryBookmarkDatabase(this, configurationLoader) - private val memoryFriendDatabase = MemoryFriendDatabase(configurationLoader) - private val saveRateLimiter: RateLimiter = RateLimiter.create(1.0) - private val saveKnownPostsRateLimiter: RateLimiter = RateLimiter.create(1.0) - private val saveKnownPostRepliesRateLimiter: RateLimiter = RateLimiter.create(1.0) - - override val soneLoader get() = this::getSone - - override val sones get() = readLock.withLock { allSones.values.toSet() } - - override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) } - - override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) } - - override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts - - override fun save() { - if (saveRateLimiter.tryAcquire()) { - saveKnownPosts() - saveKnownPostReplies() - } - } - - override fun doStart() { - memoryBookmarkDatabase.start() - loadKnownPosts() - loadKnownPostReplies() - notifyStarted() - } - - override fun doStop() { - try { - memoryBookmarkDatabase.stop() - save() - notifyStopped() - } catch (de1: DatabaseException) { - notifyFailed(de1) - } - } - - override fun newSoneBuilder() = MemorySoneBuilder(this) - - override fun storeSone(sone: Sone) { - writeLock.withLock { - removeSone(sone) - - allSones[sone.id] = sone - sonePosts.putAll(sone.id, sone.posts) - for (post in sone.posts) { - allPosts[post.id] = post - } - sonePostReplies.putAll(sone.id, sone.replies) - for (postReply in sone.replies) { - allPostReplies[postReply.id] = postReply - } - soneAlbums.putAll(sone.id, toAllAlbums.apply(sone)!!) - for (album in toAllAlbums.apply(sone)!!) { - allAlbums[album.id] = album - } - soneImages.putAll(sone.id, toAllImages.apply(sone)!!) - for (image in toAllImages.apply(sone)!!) { - allImages[image.id] = image - } - } - } - - override fun removeSone(sone: Sone) { - writeLock.withLock { - allSones.remove(sone.id) - val removedPosts = sonePosts.removeAll(sone.id) - for (removedPost in removedPosts) { - allPosts.remove(removedPost.id) - } - val removedPostReplies = sonePostReplies.removeAll(sone.id) - for (removedPostReply in removedPostReplies) { - allPostReplies.remove(removedPostReply.id) - } - val removedAlbums = soneAlbums.removeAll(sone.id) - for (removedAlbum in removedAlbums) { - allAlbums.remove(removedAlbum.id) - } - val removedImages = soneImages.removeAll(sone.id) - for (removedImage in removedImages) { - allImages.remove(removedImage.id) - } - } - } - - override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] } - - override fun getFriends(localSone: Sone): Collection = - if (!localSone.isLocal) { - emptySet() - } else { - memoryFriendDatabase.getFriends(localSone.id) - } - - override fun isFriend(localSone: Sone, friendSoneId: String) = - if (!localSone.isLocal) { - false - } else { - memoryFriendDatabase.isFriend(localSone.id, friendSoneId) - } - - override fun addFriend(localSone: Sone, friendSoneId: String) { - if (!localSone.isLocal) { - return - } - memoryFriendDatabase.addFriend(localSone.id, friendSoneId) - } - - override fun removeFriend(localSone: Sone, friendSoneId: String) { - if (!localSone.isLocal) { - return - } - memoryFriendDatabase.removeFriend(localSone.id, friendSoneId) - } - - override fun getFollowingTime(friendSoneId: String) = - memoryFriendDatabase.getFollowingTime(friendSoneId) - - override fun getPost(postId: String) = - readLock.withLock { allPosts[postId] } - - override fun getPosts(soneId: String): Collection = - sonePosts[soneId].toSet() - - override fun getDirectedPosts(recipientId: String) = - readLock.withLock { - allPosts.values.filter { - it.recipientId.orNull() == recipientId - } - } - - override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this) - - override fun storePost(post: Post) { - checkNotNull(post, "post must not be null") - writeLock.withLock { - allPosts[post.id] = post - sonePosts[post.sone.id].add(post) - } - } - - override fun removePost(post: Post) { - checkNotNull(post, "post must not be null") - writeLock.withLock { - allPosts.remove(post.id) - sonePosts[post.sone.id].remove(post) - post.sone.removePost(post) - } - } - - override fun getPostReply(id: String) = readLock.withLock { allPostReplies[id] } - - override fun getReplies(postId: String) = - readLock.withLock { - allPostReplies.values - .filter { it.postId == postId } - .sortedWith(TIME_COMPARATOR) - } - - override fun newPostReplyBuilder(): PostReplyBuilder = - MemoryPostReplyBuilder(this, this) - - override fun storePostReply(postReply: PostReply) = - writeLock.withLock { - allPostReplies[postReply.id] = postReply - } - - override fun removePostReply(postReply: PostReply) = - writeLock.withLock { - allPostReplies.remove(postReply.id) - }.unit - - override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] } - - override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl() - - override fun storeAlbum(album: Album) = - writeLock.withLock { - allAlbums[album.id] = album - soneAlbums.put(album.sone.id, album) - }.unit - - override fun removeAlbum(album: Album) = - writeLock.withLock { - allAlbums.remove(album.id) - soneAlbums.remove(album.sone.id, album) - }.unit - - override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] } - - override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl() - - override fun storeImage(image: Image): Unit = - writeLock.withLock { - allImages[image.id] = image - soneImages.put(image.sone.id, image) - } - - override fun removeImage(image: Image): Unit = - writeLock.withLock { - allImages.remove(image.id) - soneImages.remove(image.sone.id, image) - } - - override fun bookmarkPost(post: Post) = - memoryBookmarkDatabase.bookmarkPost(post) - - override fun unbookmarkPost(post: Post) = - memoryBookmarkDatabase.unbookmarkPost(post) - - override fun isPostBookmarked(post: Post) = - memoryBookmarkDatabase.isPostBookmarked(post) - - protected fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts } - - fun setPostKnown(post: Post, known: Boolean): Unit = - writeLock.withLock { - if (known) - knownPosts.add(post.id) - else - knownPosts.remove(post.id) - saveKnownPosts() - } - - protected fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies } - - fun setPostReplyKnown(postReply: PostReply, known: Boolean): Unit = - writeLock.withLock { - if (known) - knownPostReplies.add(postReply.id) - else - knownPostReplies.remove(postReply.id) - saveKnownPostReplies() - } - - private fun loadKnownPosts() = - configurationLoader.loadKnownPosts() - .let { - writeLock.withLock { - knownPosts.clear() - knownPosts.addAll(it) - } - } - - private fun saveKnownPosts() = - saveKnownPostsRateLimiter.tryAcquire().ifTrue { - try { - readLock.withLock { - knownPosts.forEachIndexed { index, knownPostId -> - configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId - } - configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null - } - } catch (ce1: ConfigurationException) { - throw DatabaseException("Could not save database.", ce1) - } - } - - private fun loadKnownPostReplies(): Unit = - configurationLoader.loadKnownPostReplies().let { knownPostReplies -> - writeLock.withLock { - this.knownPostReplies.clear() - this.knownPostReplies.addAll(knownPostReplies) - } - } - - private fun saveKnownPostReplies() = - saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue { - try { - readLock.withLock { - knownPostReplies.forEachIndexed { index, knownPostReply -> - configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply - } - configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null - } - } catch (ce1: ConfigurationException) { - throw DatabaseException("Could not save database.", ce1) - } - } - -} diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReply.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReply.java index e4a8f30..5764622 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReply.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostReply.java @@ -124,15 +124,6 @@ class MemoryPostReply implements PostReply { return database.isPostReplyKnown(this); } - /** - * {@inheritDocs} - */ - @Override - public PostReply setKnown(boolean known) { - database.setPostReplyKnown(this, known); - return this; - } - // // POSTREPLY METHODS // @@ -177,4 +168,17 @@ class MemoryPostReply implements PostReply { return memoryPostReply.id.equals(id); } + @Override + public String toString() { + return "MemoryPostReply{" + + "database=" + database + + ", soneProvider=" + soneProvider + + ", id='" + id + '\'' + + ", soneId='" + soneId + '\'' + + ", time=" + time + + ", text='" + text + '\'' + + ", postId='" + postId + '\'' + + '}'; + } + } diff --git a/src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java b/src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java index 39a6a1e..3cb1d6b 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java @@ -17,8 +17,6 @@ package net.pterodactylus.sone.fcp; -import com.google.common.base.Optional; - import net.pterodactylus.sone.core.Core; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.Sone; diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java index 821c198..05c2349 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java @@ -32,6 +32,9 @@ import com.google.common.collect.Collections2; import freenet.support.SimpleFieldSet; +import static net.pterodactylus.sone.data.PostKt.newestPostFirst; +import static net.pterodactylus.sone.data.PostKt.noFuturePost; + /** * Implementation of an FCP interface for other clients or plugins to * communicate with Sone. @@ -67,10 +70,10 @@ public class GetPostFeedCommand extends AbstractSoneCommand { allPosts.addAll(friendSone.getPosts()); } allPosts.addAll(getCore().getDirectedPosts(sone.getId())); - allPosts = Collections2.filter(allPosts, Post.FUTURE_POSTS_FILTER); + allPosts = Collections2.filter(allPosts, noFuturePost()::invoke); List sortedPosts = new ArrayList<>(allPosts); - Collections.sort(sortedPosts, Post.NEWEST_FIRST); + sortedPosts.sort(newestPostFirst()); if (sortedPosts.size() < startPost) { return new Response("PostFeed", encodePosts(Collections. emptyList(), "Posts.", false)); diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java index 83167cd..eedf120 100644 --- a/src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java +++ b/src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java @@ -17,6 +17,7 @@ package net.pterodactylus.sone.fcp; +import static net.pterodactylus.sone.data.SoneKt.*; import static net.pterodactylus.sone.fcp.AbstractSoneCommandKt.encodeSones; import java.util.ArrayList; @@ -24,7 +25,8 @@ import java.util.Collections; import java.util.List; import net.pterodactylus.sone.core.Core; -import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.*; + import freenet.support.SimpleFieldSet; /** @@ -53,7 +55,7 @@ public class GetSonesCommand extends AbstractSoneCommand { if (sones.size() < startSone) { return new Response("Sones", encodeSones(Collections. emptyList(), "Sones.")); } - Collections.sort(sones, Sone.NICE_NAME_COMPARATOR); + sones.sort(niceNameComparator()); return new Response("Sones", encodeSones(sones.subList(startSone, (maxSones == -1) ? sones.size() : Math.min(startSone + maxSones, sones.size())), "Sones.")); } diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java index b8d4e6d..e6f4f62 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java @@ -17,13 +17,9 @@ package net.pterodactylus.sone.freenet.wot; -import java.util.Collection; -import java.util.Collections; import java.util.Map; import java.util.Set; -import com.google.common.base.Function; - /** * Interface for web of trust identities, defining all functions that can be * performed on an identity. An identity is only a container for identity data @@ -135,6 +131,8 @@ public interface Identity { */ public Identity removeProperty(String name); + Map getTrust(); + /** * Retrieves the trust that this identity receives from the given own * identity. If this identity is not in the own identity’s trust tree, a diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt b/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt deleted file mode 100644 index ffcafb3..0000000 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Sone - IdentityChangeDetector.kt - Copyright © 2013–2020 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.sone.freenet.wot - -/** - * Detects changes between two lists of [Identity]s. The detector can find - * added and removed identities, and for identities that exist in both list - * their contexts and properties are checked for added, removed, or (in case of - * properties) changed values. - */ -class IdentityChangeDetector(oldIdentities: Collection) { - - private val oldIdentities: Map = oldIdentities.associateBy { it.id } - var onNewIdentity: IdentityProcessor? = null - var onRemovedIdentity: IdentityProcessor? = null - var onChangedIdentity: IdentityProcessor? = null - var onUnchangedIdentity: IdentityProcessor? = null - - fun detectChanges(newIdentities: Collection) { - onRemovedIdentity.notify(oldIdentities.values.filter { it !in newIdentities }) - onNewIdentity.notify(newIdentities.filter { it !in oldIdentities.values }) - onChangedIdentity.notify(newIdentities.filter { it.id in oldIdentities }.filter { identityHasChanged(oldIdentities[it.id]!!, it) }) - onUnchangedIdentity.notify(newIdentities.filter { it.id in oldIdentities }.filterNot { identityHasChanged(oldIdentities[it.id]!!, it) }) - } - - private fun identityHasChanged(oldIdentity: Identity, newIdentity: Identity?) = - identityHasNewContexts(oldIdentity, newIdentity!!) - || identityHasRemovedContexts(oldIdentity, newIdentity) - || identityHasNewProperties(oldIdentity, newIdentity) - || identityHasRemovedProperties(oldIdentity, newIdentity) - || identityHasChangedProperties(oldIdentity, newIdentity) - - private fun identityHasNewContexts(oldIdentity: Identity, newIdentity: Identity) = - newIdentity.contexts.any { it !in oldIdentity.contexts } - - private fun identityHasRemovedContexts(oldIdentity: Identity, newIdentity: Identity) = - oldIdentity.contexts.any { it !in newIdentity.contexts } - - private fun identityHasNewProperties(oldIdentity: Identity, newIdentity: Identity) = - newIdentity.properties.keys.any { it !in oldIdentity.properties } - - private fun identityHasRemovedProperties(oldIdentity: Identity, newIdentity: Identity) = - oldIdentity.properties.keys.any { it !in newIdentity.properties } - - private fun identityHasChangedProperties(oldIdentity: Identity, newIdentity: Identity) = - oldIdentity.properties.entries.any { newIdentity.properties[it.key] != it.value } - -} - -typealias IdentityProcessor = (Identity) -> Unit - -private fun IdentityProcessor?.notify(identities: Iterable) = - this?.let { identities.forEach(this::invoke) } diff --git a/src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java b/src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java index 2a2bd8c..5072970 100644 --- a/src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java +++ b/src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java @@ -1,13 +1,10 @@ package net.pterodactylus.sone.main; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.UnsupportedEncodingException; +import java.io.*; + import javax.annotation.Nonnull; import net.pterodactylus.sone.web.WebInterface; -import net.pterodactylus.util.io.Closer; import net.pterodactylus.util.template.ClassPathTemplateProvider; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateProvider; @@ -25,17 +22,11 @@ public class DefaultLoaders implements Loaders { @Nonnull @Override public Template loadTemplate(@Nonnull String path) { - InputStream templateInputStream = null; - Reader reader = null; - try { - templateInputStream = getClass().getResourceAsStream(path); - reader = new InputStreamReader(templateInputStream, "UTF-8"); + try (InputStream templateInputStream = getClass().getResourceAsStream(path); + Reader reader = new InputStreamReader(templateInputStream, "UTF-8");) { return parse(reader); - } catch (UnsupportedEncodingException uee1) { + } catch (IOException ioe1) { throw new RuntimeException("UTF-8 not supported."); - } finally { - Closer.close(reader); - Closer.close(templateInputStream); } } diff --git a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java index 25c6dac..82ab065 100644 --- a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java +++ b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java @@ -42,7 +42,6 @@ import com.google.common.annotations.*; import com.google.common.eventbus.*; import com.google.common.cache.*; import com.google.inject.*; -import com.google.inject.Module; import com.google.inject.name.*; import kotlin.jvm.functions.*; @@ -97,7 +96,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr /** The current year at time of release. */ private static final int YEAR = 2020; private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/"; - private static final int LATEST_EDITION = 80; + private static final int LATEST_EDITION = 81; /** The logger. */ private static final Logger logger = getLogger(SonePlugin.class.getName()); diff --git a/src/main/java/net/pterodactylus/sone/main/SonePlugin.kt b/src/main/java/net/pterodactylus/sone/main/SonePlugin.kt deleted file mode 100644 index 5e0b2c1..0000000 --- a/src/main/java/net/pterodactylus/sone/main/SonePlugin.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.pterodactylus.sone.main - -data class PluginVersion(val version: String) - -data class PluginYear(val year: Int) - -data class PluginHomepage(val homepage: String) diff --git a/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java b/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java index f170701..f960b08 100644 --- a/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java @@ -17,12 +17,13 @@ package net.pterodactylus.sone.template; +import static net.pterodactylus.sone.data.SoneKt.*; + import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; -import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.*; import net.pterodactylus.util.template.Accessor; import net.pterodactylus.util.template.ReflectionAccessor; import net.pterodactylus.util.template.TemplateContext; @@ -52,7 +53,7 @@ public class CollectionAccessor extends ReflectionAccessor { } sones.add((Sone) sone); } - Collections.sort(sones, Sone.NICE_NAME_COMPARATOR); + sones.sort(niceNameComparator()); StringBuilder soneNames = new StringBuilder(); for (Sone sone : sones) { if (soneNames.length() > 0) { diff --git a/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java b/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java index 4bc6a5f..b072dc9 100644 --- a/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java @@ -17,11 +17,7 @@ package net.pterodactylus.sone.template; -import static com.google.common.collect.FluentIterable.from; -import static java.util.Arrays.asList; import static java.util.logging.Logger.getLogger; -import static net.pterodactylus.sone.data.Album.FLATTENER; -import static net.pterodactylus.sone.data.Album.IMAGES; import java.util.logging.Level; import java.util.logging.Logger; @@ -30,6 +26,7 @@ import net.pterodactylus.sone.core.Core; import net.pterodactylus.sone.data.Profile; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.SoneStatus; +import net.pterodactylus.sone.data.SoneKt; import net.pterodactylus.sone.freenet.wot.OwnIdentity; import net.pterodactylus.sone.freenet.wot.Trust; import net.pterodactylus.sone.text.TimeTextConverter; @@ -116,7 +113,7 @@ public class SoneAccessor extends ReflectionAccessor { } return trust; } else if (member.equals("allImages")) { - return from(asList(sone.getRootAlbum())).transformAndConcat(FLATTENER).transformAndConcat(IMAGES); + return SoneKt.getAllImages(sone); } else if (member.equals("albums")) { return sone.getRootAlbum().getAlbums(); } diff --git a/src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java b/src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java index d3a1557..598765a 100644 --- a/src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java +++ b/src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java @@ -18,7 +18,6 @@ package net.pterodactylus.sone.text; import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.web.page.FreenetRequest; /** * {@link ParserContext} implementation for the {@link SoneTextParser}. It diff --git a/src/main/java/net/pterodactylus/sone/utils/DefaultOption.java b/src/main/java/net/pterodactylus/sone/utils/DefaultOption.java deleted file mode 100644 index d9acaeb..0000000 --- a/src/main/java/net/pterodactylus/sone/utils/DefaultOption.java +++ /dev/null @@ -1,83 +0,0 @@ -package net.pterodactylus.sone.utils; - -import com.google.common.base.Predicate; - -/** - * Basic implementation of an {@link Option}. - * - * @param - * The type of the option - */ -public class DefaultOption implements Option { - - /** The default value. */ - private final T defaultValue; - - /** The current value. */ - private volatile T value; - - /** The validator. */ - private Predicate validator; - - /** - * Creates a new default option. - * - * @param defaultValue - * The default value of the option - */ - public DefaultOption(T defaultValue) { - this(defaultValue, null); - } - - /** - * Creates a new default option. - * - * @param defaultValue - * The default value of the option - * @param validator - * The validator for value validation (may be {@code null}) - */ - public DefaultOption(T defaultValue, Predicate validator) { - this.defaultValue = defaultValue; - this.validator = validator; - } - - /** - * {@inheritDoc} - */ - @Override - public T get() { - return (value != null) ? value : defaultValue; - } - - /** - * Returns the real value of the option. This will also return an unset - * value (usually {@code null})! - * - * @return The real value of the option - */ - @Override - public T getReal() { - return value; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean validate(T value) { - return (validator == null) || (value == null) || validator.apply(value); - } - - /** - * {@inheritDoc} - */ - @Override - public void set(T value) { - if ((value != null) && (validator != null) && (!validator.apply(value))) { - throw new IllegalArgumentException("New Value (" + value + ") could not be validated."); - } - this.value = value; - } - -} diff --git a/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java b/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java deleted file mode 100644 index 3a631cf..0000000 --- a/src/main/java/net/pterodactylus/sone/utils/IntegerRangePredicate.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Sone - IntegerRangePredicate.java - Copyright © 2013–2020 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.sone.utils; - -import com.google.common.base.Predicate; - -/** - * {@link Predicate} that verifies that an {@link Integer} value is not - * {@code null} and is between a lower and an upper bound. Both bounds are - * inclusive. - */ -public class IntegerRangePredicate implements Predicate { - - /** The lower bound. */ - private final int lowerBound; - - /** The upper bound. */ - private final int upperBound; - - /** - * Creates a new integer range predicate. - * - * @param lowerBound - * The lower bound - * @param upperBound - * The upper bound - */ - public IntegerRangePredicate(int lowerBound, int upperBound) { - this.lowerBound = lowerBound; - this.upperBound = upperBound; - } - - // - // PREDICATE METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public boolean apply(Integer value) { - return (value != null) && (value >= lowerBound) && (value <= upperBound); - } - - public static IntegerRangePredicate range(int lowerBound, int upperBound) { - return new IntegerRangePredicate(lowerBound, upperBound); - } - -} diff --git a/src/main/java/net/pterodactylus/sone/utils/NumberParsers.java b/src/main/java/net/pterodactylus/sone/utils/NumberParsers.java index 471fc26..d9091c0 100644 --- a/src/main/java/net/pterodactylus/sone/utils/NumberParsers.java +++ b/src/main/java/net/pterodactylus/sone/utils/NumberParsers.java @@ -1,6 +1,5 @@ package net.pterodactylus.sone.utils; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.google.common.primitives.Ints; diff --git a/src/main/java/net/pterodactylus/sone/web/AllPages.kt b/src/main/java/net/pterodactylus/sone/web/AllPages.kt deleted file mode 100644 index c538f0e..0000000 --- a/src/main/java/net/pterodactylus/sone/web/AllPages.kt +++ /dev/null @@ -1,52 +0,0 @@ -package net.pterodactylus.sone.web - -import net.pterodactylus.sone.web.pages.* -import javax.inject.Inject - -/** - * Container for all web pages. This uses field injection because there are way too many pages - * to sensibly use constructor injection. - */ -class AllPages { - - @Inject lateinit var aboutPage: AboutPage - @Inject lateinit var bookmarkPage: BookmarkPage - @Inject lateinit var bookmarksPage: BookmarksPage - @Inject lateinit var createAlbumPage: CreateAlbumPage - @Inject lateinit var createPostPage: CreatePostPage - @Inject lateinit var createReplyPage: CreateReplyPage - @Inject lateinit var createSonePage: CreateSonePage - @Inject lateinit var deleteAlbumPage: DeleteAlbumPage - @Inject lateinit var deleteImagePage: DeleteImagePage - @Inject lateinit var deletePostPage: DeletePostPage - @Inject lateinit var deleteProfileFieldPage: DeleteProfileFieldPage - @Inject lateinit var deleteReplyPage: DeleteReplyPage - @Inject lateinit var deleteSonePage: DeleteSonePage - @Inject lateinit var dismissNotificationPage: DismissNotificationPage - @Inject lateinit var editAlbumPage: EditAlbumPage - @Inject lateinit var editImagePage: EditImagePage - @Inject lateinit var editProfileFieldPage: EditProfileFieldPage - @Inject lateinit var editProfilePage: EditProfilePage - @Inject lateinit var followSonePage: FollowSonePage - @Inject lateinit var getImagePage: GetImagePage - @Inject lateinit var imageBrowserPage: ImageBrowserPage - @Inject lateinit var indexPage: IndexPage - @Inject lateinit var knownSonesPage: KnownSonesPage - @Inject lateinit var likePage: LikePage - @Inject lateinit var lockSonePage: LockSonePage - @Inject lateinit var loginPage: LoginPage - @Inject lateinit var logoutPage: LogoutPage - @Inject lateinit var markAsKnownPage: MarkAsKnownPage - @Inject lateinit var newPage: NewPage - @Inject lateinit var optionsPage: OptionsPage - @Inject lateinit var rescuePage: RescuePage - @Inject lateinit var searchPage: SearchPage - @Inject lateinit var unbookmarkPage: UnbookmarkPage - @Inject lateinit var unfollowSonePage: UnfollowSonePage - @Inject lateinit var unlikePage: UnlikePage - @Inject lateinit var unlockSonePage: UnlockSonePage - @Inject lateinit var uploadImagePage: UploadImagePage - @Inject lateinit var viewPostPage: ViewPostPage - @Inject lateinit var viewSonePage: ViewSonePage - -} diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index 5448054..40e5708 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -23,7 +23,6 @@ import static java.util.logging.Logger.getLogger; import java.util.Collection; import java.util.Set; import java.util.TimeZone; -import java.util.UUID; import java.util.logging.Logger; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -88,15 +87,12 @@ import net.pterodactylus.util.template.TemplateContextFactory; import net.pterodactylus.util.web.RedirectPage; import net.pterodactylus.util.web.TemplatePage; -import freenet.clients.http.SessionManager; -import freenet.clients.http.SessionManager.Session; -import freenet.clients.http.ToadletContext; - import com.codahale.metrics.*; import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; +import freenet.clients.http.ToadletContext; /** * Bundles functionality that a web interface of a Freenet plugin needs, e.g. @@ -140,6 +136,7 @@ public class WebInterface implements SessionProvider { private final PageToadletRegistry pageToadletRegistry; private final MetricRegistry metricRegistry; private final Translation translation; + private final SessionProvider sessionProvider; /** The “new post” notification. */ private final ListNotification newPostNotification; @@ -162,7 +159,8 @@ public class WebInterface implements SessionProvider { RenderFilter renderFilter, LinkedElementRenderFilter linkedElementRenderFilter, PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter, - NotificationManager notificationManager, @Named("newRemotePost") ListNotification newPostNotification, + NotificationManager notificationManager, SessionProvider sessionProvider, + @Named("newRemotePost") ListNotification newPostNotification, @Named("newRemotePostReply") ListNotification newReplyNotification, @Named("localPost") ListNotification localPostNotification, @Named("localReply") ListNotification localReplyNotification) { @@ -182,6 +180,7 @@ public class WebInterface implements SessionProvider { this.l10nFilter = l10nFilter; this.translation = translation; this.notificationManager = notificationManager; + this.sessionProvider = sessionProvider; this.newPostNotification = newPostNotification; this.newReplyNotification = newReplyNotification; this.localPostNotification = localPostNotification; @@ -216,75 +215,15 @@ public class WebInterface implements SessionProvider { return templateContextFactory; } - private Session getCurrentSessionWithoutCreation(ToadletContext toadletContenxt) { - return getSessionManager().useSession(toadletContenxt); - } - - private Session getOrCreateCurrentSession(ToadletContext toadletContenxt) { - Session session = getCurrentSessionWithoutCreation(toadletContenxt); - if (session == null) { - session = getSessionManager().createSession(UUID.randomUUID().toString(), toadletContenxt); - } - return session; - } - - public Sone getCurrentSoneCreatingSession(ToadletContext toadletContext) { - Collection localSones = getCore().getLocalSones(); - if (localSones.size() == 1) { - return localSones.iterator().next(); - } - return getCurrentSone(getOrCreateCurrentSession(toadletContext)); - } - - public Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) { - Collection localSones = getCore().getLocalSones(); - if (localSones.size() == 1) { - return localSones.iterator().next(); - } - return getCurrentSone(getCurrentSessionWithoutCreation(toadletContext)); - } - - /** - * Returns the currently logged in Sone. - * - * @param session - * The session - * @return The currently logged in Sone, or {@code null} if no Sone is - * currently logged in - */ - private Sone getCurrentSone(Session session) { - if (session == null) { - return null; - } - String soneId = (String) session.getAttribute("Sone.CurrentSone"); - if (soneId == null) { - return null; - } - return getCore().getLocalSone(soneId); - } - - @Override @Nullable - public Sone getCurrentSone(@Nonnull ToadletContext toadletContext, boolean createSession) { - return createSession ? getCurrentSoneCreatingSession(toadletContext) : getCurrentSoneWithoutCreatingSession(toadletContext); + @Override + public Sone getCurrentSone(@Nonnull ToadletContext toadletContext) { + return sessionProvider.getCurrentSone(toadletContext); } - /** - * Sets the currently logged in Sone. - * - * @param toadletContext - * The toadlet context - * @param sone - * The Sone to set as currently logged in - */ @Override public void setCurrentSone(@Nonnull ToadletContext toadletContext, @Nullable Sone sone) { - Session session = getOrCreateCurrentSession(toadletContext); - if (sone == null) { - session.removeAttribute("Sone.CurrentSone"); - } else { - session.setAttribute("Sone.CurrentSone", sone.getId()); - } + sessionProvider.setCurrentSone(toadletContext, sone); } /** @@ -311,15 +250,6 @@ public class WebInterface implements SessionProvider { } /** - * Returns the session manager of the node. - * - * @return The node’s session manager - */ - public SessionManager getSessionManager() { - return sonePlugin.pluginRespirator().getSessionManager("Sone"); - } - - /** * Returns the node’s form password. * * @return The form password diff --git a/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java b/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java deleted file mode 100644 index 45d6fff..0000000 --- a/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Sone - PageToadlet.java - Copyright © 2010–2020 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.sone.web.page; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.URI; - -import net.pterodactylus.sone.utils.AutoCloseableBucket; -import net.pterodactylus.util.web.Header; -import net.pterodactylus.util.web.Method; -import net.pterodactylus.util.web.Page; -import net.pterodactylus.util.web.Response; - -import freenet.client.HighLevelSimpleClient; -import freenet.clients.http.LinkEnabledCallback; -import freenet.clients.http.LinkFilterExceptedToadlet; -import freenet.clients.http.SessionManager; -import freenet.clients.http.Toadlet; -import freenet.clients.http.ToadletContext; -import freenet.clients.http.ToadletContextClosedException; -import freenet.support.MultiValueTable; -import freenet.support.api.HTTPRequest; - -/** - * {@link Toadlet} implementation that is wrapped around a {@link Page}. - */ -public class PageToadlet extends Toadlet implements LinkEnabledCallback, LinkFilterExceptedToadlet { - - private final SessionManager sessionManager; - - /** The name of the menu item. */ - private final String menuName; - - /** The page that handles processing. */ - private final Page page; - - /** The path prefix for the page. */ - private final String pathPrefix; - - /** - * Creates a new toadlet that hands off processing to a {@link Page}. - * - * @param highLevelSimpleClient - * The high-level simple client - * @param menuName - * The name of the menu item - * @param page - * The page to handle processing - * @param pathPrefix - * Prefix that is prepended to all {@link Page#getPath()} return - * values - */ - protected PageToadlet(HighLevelSimpleClient highLevelSimpleClient, SessionManager sessionManager, String menuName, Page page, String pathPrefix) { - super(highLevelSimpleClient); - this.sessionManager = sessionManager; - this.menuName = menuName; - this.page = page; - this.pathPrefix = pathPrefix; - } - - /** - * Returns the name to display in the menu. - * - * @return The name in the menu - */ - public String getMenuName() { - return menuName; - } - - /** - * {@inheritDoc} - */ - @Override - public String path() { - return pathPrefix + page.getPath(); - } - - /** - * Handles a HTTP GET request. - * - * @param uri - * The URI of the request - * @param httpRequest - * The HTTP request - * @param toadletContext - * The toadlet context - * @throws IOException - * if an I/O error occurs - * @throws ToadletContextClosedException - * if the toadlet context is closed - */ - public void handleMethodGET(URI uri, HTTPRequest httpRequest, ToadletContext toadletContext) throws IOException, ToadletContextClosedException { - handleRequest(new FreenetRequest(uri, Method.GET, httpRequest, toadletContext, sessionManager)); - } - - /** - * Handles a HTTP POST request. - * - * @param uri - * The URI of the request - * @param httpRequest - * The HTTP request - * @param toadletContext - * The toadlet context - * @throws IOException - * if an I/O error occurs - * @throws ToadletContextClosedException - * if the toadlet context is closed - */ - public void handleMethodPOST(URI uri, HTTPRequest httpRequest, ToadletContext toadletContext) throws IOException, ToadletContextClosedException { - handleRequest(new FreenetRequest(uri, Method.POST, httpRequest, toadletContext, sessionManager)); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return getClass().getName() + "[path=" + path() + ",page=" + page + "]"; - } - - /** - * Handles a HTTP request. - * - * @param pageRequest - * The request to handle - * @throws IOException - * if an I/O error occurs - * @throws ToadletContextClosedException - * if the toadlet context is closed - */ - private void handleRequest(FreenetRequest pageRequest) throws IOException, ToadletContextClosedException { - try (AutoCloseableBucket pageBucket = new AutoCloseableBucket(pageRequest.getToadletContext().getBucketFactory().makeBucket(-1)); - OutputStream pageBucketOutputStream = pageBucket.getBucket().getOutputStream()) { - Response pageResponse = page.handleRequest(pageRequest, new Response(pageBucketOutputStream)); - MultiValueTable headers = new MultiValueTable<>(); - if (pageResponse.getHeaders() != null) { - for (Header header : pageResponse.getHeaders()) { - for (String value : header) { - headers.put(header.getName(), value); - } - } - } - writeReply(pageRequest.getToadletContext(), pageResponse.getStatusCode(), pageResponse.getContentType(), pageResponse.getStatusText(), headers, pageBucket.getBucket()); - } - } - - /** - * {@inheritDoc} - */ - @Override - public boolean isEnabled(ToadletContext toadletContext) { - if (page instanceof LinkEnabledCallback) { - return ((LinkEnabledCallback) page).isEnabled(toadletContext); - } - return true; - } - - // - // LINKFILTEREXCEPTEDTOADLET METHODS - // - - /** - * {@inheritDoc} - */ - @Override - public boolean isLinkExcepted(URI link) { - return (page instanceof FreenetPage) && ((FreenetPage) page).isLinkExcepted(link); - } - -} diff --git a/src/main/kotlin/net/pterodactylus/sone/core/PreferenceChangedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/PreferenceChangedEvent.kt new file mode 100644 index 0000000..2ebb62d --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/PreferenceChangedEvent.kt @@ -0,0 +1,3 @@ +package net.pterodactylus.sone.core + +data class PreferenceChangedEvent(val preferenceName: String, val newValue: Any) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt b/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt index 05b3279..fe1e3c3 100644 --- a/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt +++ b/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt @@ -17,16 +17,19 @@ package net.pterodactylus.sone.core -import com.google.common.base.Predicates.* -import com.google.common.eventbus.* -import net.pterodactylus.sone.core.event.* -import net.pterodactylus.sone.fcp.FcpInterface.* -import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.* -import net.pterodactylus.sone.fcp.event.* -import net.pterodactylus.sone.utils.* -import net.pterodactylus.sone.utils.IntegerRangePredicate.* -import net.pterodactylus.util.config.* -import java.lang.Integer.* +import com.google.common.eventbus.EventBus +import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent +import net.pterodactylus.sone.core.event.StrictFilteringActivatedEvent +import net.pterodactylus.sone.core.event.StrictFilteringDeactivatedEvent +import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired +import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.ALWAYS +import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent +import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent +import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged +import net.pterodactylus.sone.utils.DefaultOption +import net.pterodactylus.util.config.Configuration +import net.pterodactylus.util.config.ConfigurationException +import java.lang.Integer.MAX_VALUE /** * Convenience interface for external classes that want to access the core’s @@ -34,7 +37,7 @@ import java.lang.Integer.* */ class Preferences(private val eventBus: EventBus) { - private val _insertionDelay = DefaultOption(60, range(0, MAX_VALUE)) + private val _insertionDelay = DefaultOption(60) { it in 0..MAX_VALUE } val insertionDelay: Int get() = _insertionDelay.get() var newInsertionDelay: Int? get() = unsupported @@ -44,7 +47,7 @@ class Preferences(private val eventBus: EventBus) { eventBus.post(PreferenceChangedEvent("InsertionDelay", insertionDelay)) } - private val _postsPerPage = DefaultOption(10, range(1, MAX_VALUE)) + private val _postsPerPage = DefaultOption(10) { it in 1..MAX_VALUE } val postsPerPage: Int get() = _postsPerPage.get() var newPostsPerPage: Int? get() = unsupported @@ -53,19 +56,19 @@ class Preferences(private val eventBus: EventBus) { eventBus.post(PreferenceChangedEvent("PostsPerPage", postsPerPage)) } - private val _imagesPerPage = DefaultOption(9, range(1, MAX_VALUE)) + private val _imagesPerPage = DefaultOption(9) { it in 1..MAX_VALUE } val imagesPerPage: Int get() = _imagesPerPage.get() var newImagesPerPage: Int? get() = unsupported - set (value: Int?) = _imagesPerPage.set(value) + set(value: Int?) = _imagesPerPage.set(value) - private val _charactersPerPost = DefaultOption(400, or(range(50, MAX_VALUE), equalTo(-1))) + private val _charactersPerPost = DefaultOption(400) { it == -1 || it in 50..MAX_VALUE } val charactersPerPost: Int get() = _charactersPerPost.get() var newCharactersPerPost: Int? get() = unsupported set(value) = _charactersPerPost.set(value) - private val _postCutOffLength = DefaultOption(200, range(50, MAX_VALUE)) + private val _postCutOffLength = DefaultOption(200) { it in 50..MAX_VALUE } val postCutOffLength: Int get() = _postCutOffLength.get() var newPostCutOffLength: Int? get() = unsupported @@ -98,6 +101,17 @@ class Preferences(private val eventBus: EventBus) { eventBus.post(FullAccessRequiredChanged(fcpFullAccessRequired)) } + private val _strictFiltering = DefaultOption(false) + val strictFiltering: Boolean get() = _strictFiltering.get() + var newStrictFiltering: Boolean? = false + set(value) { + _strictFiltering.set(value) + when (strictFiltering) { + true -> eventBus.post(StrictFilteringActivatedEvent()) + else -> eventBus.post(StrictFilteringDeactivatedEvent()) + } + } + @Throws(ConfigurationException::class) fun saveTo(configuration: Configuration) { configuration.getIntValue("Option/ConfigurationVersion").value = 0 @@ -109,6 +123,7 @@ class Preferences(private val eventBus: EventBus) { configuration.getBooleanValue("Option/RequireFullAccess").value = _requireFullAccess.real configuration.getBooleanValue("Option/ActivateFcpInterface").value = _fcpInterfaceActive.real configuration.getIntValue("Option/FcpFullAccessRequired").value = toInt(_fcpFullAccessRequired.real) + configuration.getBooleanValue("Option/StrictFiltering").value = _strictFiltering.real } private fun toInt(fullAccessRequired: FullAccessRequired?): Int? { diff --git a/src/main/kotlin/net/pterodactylus/sone/core/PreferencesLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/PreferencesLoader.kt new file mode 100644 index 0000000..62b60ae --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/PreferencesLoader.kt @@ -0,0 +1,63 @@ +package net.pterodactylus.sone.core + +import net.pterodactylus.sone.fcp.FcpInterface.* +import net.pterodactylus.util.config.* + +/** + * Loads preferences stored in a [Configuration] into a [Preferences] object. + */ +class PreferencesLoader(private val preferences: Preferences) { + + fun loadFrom(configuration: Configuration) { + loadInsertionDelay(configuration) + loadPostsPerPage(configuration) + loadImagesPerPage(configuration) + loadCharactersPerPost(configuration) + loadPostCutOffLength(configuration) + loadRequireFullAccess(configuration) + loadFcpInterfaceActive(configuration) + loadFcpFullAccessRequired(configuration) + loadStrictFiltering(configuration) + } + + private fun loadInsertionDelay(configuration: Configuration) { + preferences.newInsertionDelay = configuration.getIntValue("Option/InsertionDelay").getValue(null) + } + + private fun loadPostsPerPage(configuration: Configuration) { + preferences.newPostsPerPage = configuration.getIntValue("Option/PostsPerPage").getValue(null) + } + + private fun loadImagesPerPage(configuration: Configuration) { + preferences.newImagesPerPage = configuration.getIntValue("Option/ImagesPerPage").getValue(null) + } + + private fun loadCharactersPerPost(configuration: Configuration) { + preferences.newCharactersPerPost = configuration.getIntValue("Option/CharactersPerPost").getValue(null) + } + + private fun loadPostCutOffLength(configuration: Configuration) { + try { + preferences.newPostCutOffLength = configuration.getIntValue("Option/PostCutOffLength").getValue(null) + } catch (iae1: IllegalArgumentException) { /* previous versions allowed -1, ignore and use default. */ + } + } + + private fun loadRequireFullAccess(configuration: Configuration) { + preferences.newRequireFullAccess = configuration.getBooleanValue("Option/RequireFullAccess").getValue(null) + } + + private fun loadFcpInterfaceActive(configuration: Configuration) { + preferences.newFcpInterfaceActive = configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null) + } + + private fun loadFcpFullAccessRequired(configuration: Configuration) { + val fullAccessRequiredInteger = configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null) + preferences.newFcpFullAccessRequired = fullAccessRequiredInteger?.let { FullAccessRequired.values()[it] } + } + + private fun loadStrictFiltering(configuration: Configuration) { + preferences.newStrictFiltering = configuration.getBooleanValue("Option/StrictFiltering").getValue(null) + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/core/SoneUriCreator.kt b/src/main/kotlin/net/pterodactylus/sone/core/SoneUriCreator.kt new file mode 100644 index 0000000..183303a --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/SoneUriCreator.kt @@ -0,0 +1,26 @@ +package net.pterodactylus.sone.core + +import freenet.keys.FreenetURI +import net.pterodactylus.sone.data.Sone +import net.pterodactylus.sone.freenet.wot.OwnIdentity + +/** + * Injectable helper class that can create request and insert URIs for [Sones][Sone]. + */ +open class SoneUriCreator { + + fun getRequestUri(sone: Sone): FreenetURI = sone.identity.requestUri + .let(::FreenetURI) + .sonify(sone.latestEdition) + + open fun getInsertUri(sone: Sone): FreenetURI? = (sone.identity as? OwnIdentity)?.insertUri + ?.let(::FreenetURI) + ?.sonify(sone.latestEdition) + +} + +private fun FreenetURI.sonify(edition: Long): FreenetURI = + setKeyType("USK") + .setDocName("Sone") + .setMetaString(emptyArray()) + .setSuggestedEdition(edition) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt b/src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt index 28bac6d..54af30a 100644 --- a/src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt +++ b/src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt @@ -42,7 +42,7 @@ abstract class BasicUpdateSoneProcessor(private val database: Database, private .map { PostRemovedEvent(it) } .forEach(eventBus::post) newPostReplies - .onEach { postReply -> if (postReply.time <= sone.followingTime) postReply.isKnown = true } + .onEach { postReply -> if (postReply.time <= sone.followingTime) database.setPostReplyKnown(postReply) } .mapNotNull { postReply -> postReply.isKnown.ifFalse { NewPostReplyFoundEvent(postReply) } } .forEach(eventBus::post) removedPostReplies diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/StrictFilteringEvents.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/StrictFilteringEvents.kt new file mode 100644 index 0000000..ea12459 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/StrictFilteringEvents.kt @@ -0,0 +1,13 @@ +package net.pterodactylus.sone.core.event + +/** + * Event that signals that the “[strict filtering][net.pterodactylus.sone.core.Preferences.strictFiltering]” + * preference was activated. + */ +class StrictFilteringActivatedEvent + +/** + * Event that signals that the “[strict filtering][net.pterodactylus.sone.core.Preferences.strictFiltering]” + * preference was deactivated. + */ +class StrictFilteringDeactivatedEvent diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Album.kt b/src/main/kotlin/net/pterodactylus/sone/data/Album.kt new file mode 100644 index 0000000..991c0ec --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/data/Album.kt @@ -0,0 +1,38 @@ +/** + * Sone - Album.kt - Copyright © 2019–2020 David ‘Bombe’ Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.data + +/** Returns all images contained in this album and all its albums. */ +val Album.allImages: Collection + get() = + images + albums.flatMap(Album::allImages) + +/** + * Returns this album and all albums contained in this album (recursively). + * A child album is always listed after its parent. + */ +val Album.allAlbums: List + get() = + listOf(this) + albums.flatMap(Album::allAlbums) + +@get:JvmName("notEmpty") +val notEmpty: (Album) -> Boolean = { album -> + album.allImages.let { images -> + images.isNotEmpty() && images.any(Image::isInserted) + } +} diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Albums.kt b/src/main/kotlin/net/pterodactylus/sone/data/Albums.kt deleted file mode 100644 index 0c79a84..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/data/Albums.kt +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Sone - Albums.kt - Copyright © 2019–2020 David ‘Bombe’ Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.sone.data - -/** Returns all images contained in this album and all its albums. */ -val Album.allImages: Collection - get() = - images + albums.flatMap { it.allImages } diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Post.kt b/src/main/kotlin/net/pterodactylus/sone/data/Post.kt new file mode 100644 index 0000000..d87bd3c --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/data/Post.kt @@ -0,0 +1,16 @@ +package net.pterodactylus.sone.data + +import java.util.Comparator.comparing + +/** + * Predicate that returns whether a post is _not_ from the future, + * i.e. whether it should be visible now. + */ +@get:JvmName("noFuturePost") +val noFuturePost: (Post) -> Boolean = { it.time <= System.currentTimeMillis() } + +/** + * Comparator that orders posts by their time, newest posts first. + */ +@get:JvmName("newestPostFirst") +val newestPostFirst: Comparator = comparing(Post::getTime).reversed() diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Reply.kt b/src/main/kotlin/net/pterodactylus/sone/data/Reply.kt new file mode 100644 index 0000000..cfc940a --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/data/Reply.kt @@ -0,0 +1,34 @@ +/** + * Sone - Reply.kt - Copyright © 2020 David ‘Bombe’ Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.data + +import java.util.Comparator.comparing + +/** + * Comparator that orders replies by their time, newest replies first. + */ +@get:JvmName("newestReplyFirst") +val newestReplyFirst: Comparator> = + comparing(Reply<*>::getTime).reversed() + +/** + * Predicate that returns whether a reply is _not_ from the future, + * i.e. whether it should be visible now. + */ +val noFutureReply: (Reply<*>) -> Boolean = + { it.getTime() <= System.currentTimeMillis() } diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Sone.kt b/src/main/kotlin/net/pterodactylus/sone/data/Sone.kt new file mode 100644 index 0000000..34403a1 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/data/Sone.kt @@ -0,0 +1,61 @@ +/** + * Sone - Sone.kt - Copyright © 2020 David ‘Bombe’ Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.data + +import net.pterodactylus.sone.template.* +import java.util.Comparator.* + +private val caseInsensitiveCompare = { left: String, right: String -> left.compareTo(right, true) } + +/** + * Comparator that sorts Sones by their [nice name][SoneAccessor.getNiceName] + * and, failing that, by [ID][Sone.id]. + */ +@get:JvmName("niceNameComparator") // TODO: remove once Sone is 100% Kotlin +val niceNameComparator: Comparator = + comparing(SoneAccessor::getNiceName, caseInsensitiveCompare).thenComparing(Sone::id) + +/** + * Comparator that sorts Sones by their [last activity][Sone.getTime], least + * recently active Sones first. + */ +@get:JvmName("lastActivityComparator") // TODO: remove once Sone is 100% Kotlin +val lastActivityComparator: Comparator = + comparing(Sone::getTime).reversed() + +/** + * Comparator that sorts Sones by their [post count][Sone.getPosts] (most posts + * first) and, failing that, by their [reply count][Sone.getReplies] (most + * replies first). + */ +@get:JvmName("postCountComparator") // TODO: remove once Sone is 100% Kotlin +val postCountComparator: Comparator = + comparing { it.posts.size } + .thenComparing { it.replies.size } + .reversed() + +val imageCountComparator: Comparator = + comparing { it.rootAlbum.allImages.size }.reversed() + +val Sone.allAlbums: List + get() = + rootAlbum.albums.flatMap(Album::allAlbums) + +val Sone.allImages: Collection + get() = + rootAlbum.allImages diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyStore.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyStore.kt index 671ab53..859cbb0 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyStore.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyStore.kt @@ -26,5 +26,6 @@ interface PostReplyStore { fun storePostReply(postReply: PostReply) fun removePostReply(postReply: PostReply) + fun setPostReplyKnown(postReply: PostReply) } diff --git a/src/main/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabase.kt b/src/main/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabase.kt new file mode 100644 index 0000000..3b5a6a9 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabase.kt @@ -0,0 +1,353 @@ +/* + * Sone - MemoryDatabase.kt - Copyright © 2013–2020 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.database.memory + +import com.google.common.base.Preconditions.checkNotNull +import com.google.common.collect.HashMultimap +import com.google.common.collect.Multimap +import com.google.common.collect.TreeMultimap +import com.google.common.util.concurrent.AbstractService +import com.google.common.util.concurrent.RateLimiter +import com.google.inject.Inject +import com.google.inject.Singleton +import net.pterodactylus.sone.data.Album +import net.pterodactylus.sone.data.Image +import net.pterodactylus.sone.data.Post +import net.pterodactylus.sone.data.PostReply +import net.pterodactylus.sone.data.Sone +import net.pterodactylus.sone.data.allAlbums +import net.pterodactylus.sone.data.allImages +import net.pterodactylus.sone.data.impl.AlbumBuilderImpl +import net.pterodactylus.sone.data.impl.ImageBuilderImpl +import net.pterodactylus.sone.data.newestReplyFirst +import net.pterodactylus.sone.database.AlbumBuilder +import net.pterodactylus.sone.database.Database +import net.pterodactylus.sone.database.DatabaseException +import net.pterodactylus.sone.database.ImageBuilder +import net.pterodactylus.sone.database.PostBuilder +import net.pterodactylus.sone.database.PostDatabase +import net.pterodactylus.sone.database.PostReplyBuilder +import net.pterodactylus.sone.utils.ifTrue +import net.pterodactylus.sone.utils.unit +import net.pterodactylus.util.config.Configuration +import net.pterodactylus.util.config.ConfigurationException +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.withLock + +/** + * Memory-based [PostDatabase] implementation. + */ +@Singleton +class MemoryDatabase @Inject constructor(private val configuration: Configuration) : AbstractService(), Database { + + private val lock = ReentrantReadWriteLock() + private val readLock by lazy { lock.readLock()!! } + private val writeLock by lazy { lock.writeLock()!! } + private val configurationLoader = ConfigurationLoader(configuration) + private val allSones = mutableMapOf() + private val allPosts = mutableMapOf() + private val sonePosts: Multimap = HashMultimap.create() + private val knownPosts = mutableSetOf() + private val allPostReplies = mutableMapOf() + private val sonePostReplies: Multimap = TreeMultimap.create(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, newestReplyFirst) + private val knownPostReplies = mutableSetOf() + private val allAlbums = mutableMapOf() + private val soneAlbums: Multimap = HashMultimap.create() + private val allImages = mutableMapOf() + private val soneImages: Multimap = HashMultimap.create() + private val memoryBookmarkDatabase = MemoryBookmarkDatabase(this, configurationLoader) + private val memoryFriendDatabase = MemoryFriendDatabase(configurationLoader) + private val saveRateLimiter: RateLimiter = RateLimiter.create(1.0) + private val saveKnownPostsRateLimiter: RateLimiter = RateLimiter.create(1.0) + private val saveKnownPostRepliesRateLimiter: RateLimiter = RateLimiter.create(1.0) + + override val soneLoader get() = this::getSone + + override val sones get() = readLock.withLock { allSones.values.toSet() } + + override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) } + + override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) } + + override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts + + override fun save() { + if (saveRateLimiter.tryAcquire()) { + saveKnownPosts() + saveKnownPostReplies() + } + } + + override fun doStart() { + memoryBookmarkDatabase.start() + loadKnownPosts() + loadKnownPostReplies() + notifyStarted() + } + + override fun doStop() { + try { + memoryBookmarkDatabase.stop() + save() + notifyStopped() + } catch (de1: DatabaseException) { + notifyFailed(de1) + } + } + + override fun newSoneBuilder() = MemorySoneBuilder(this) + + override fun storeSone(sone: Sone) { + writeLock.withLock { + removeSone(sone) + + allSones[sone.id] = sone + sonePosts.putAll(sone.id, sone.posts) + for (post in sone.posts) { + allPosts[post.id] = post + } + sonePostReplies.putAll(sone.id, sone.replies) + for (postReply in sone.replies) { + allPostReplies[postReply.id] = postReply + } + sone.allAlbums.let { albums -> + soneAlbums.putAll(sone.id, albums) + albums.forEach { album -> allAlbums[album.id] = album } + } + sone.rootAlbum.allImages.let { images -> + soneImages.putAll(sone.id, images) + images.forEach { image -> allImages[image.id] = image } + } + } + } + + override fun removeSone(sone: Sone) { + writeLock.withLock { + allSones.remove(sone.id) + val removedPosts = sonePosts.removeAll(sone.id) + for (removedPost in removedPosts) { + allPosts.remove(removedPost.id) + } + val removedPostReplies = sonePostReplies.removeAll(sone.id) + for (removedPostReply in removedPostReplies) { + allPostReplies.remove(removedPostReply.id) + } + val removedAlbums = soneAlbums.removeAll(sone.id) + for (removedAlbum in removedAlbums) { + allAlbums.remove(removedAlbum.id) + } + val removedImages = soneImages.removeAll(sone.id) + for (removedImage in removedImages) { + allImages.remove(removedImage.id) + } + } + } + + override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] } + + override fun getFriends(localSone: Sone): Collection = + if (!localSone.isLocal) { + emptySet() + } else { + memoryFriendDatabase.getFriends(localSone.id) + } + + override fun isFriend(localSone: Sone, friendSoneId: String) = + if (!localSone.isLocal) { + false + } else { + memoryFriendDatabase.isFriend(localSone.id, friendSoneId) + } + + override fun addFriend(localSone: Sone, friendSoneId: String) { + if (!localSone.isLocal) { + return + } + memoryFriendDatabase.addFriend(localSone.id, friendSoneId) + } + + override fun removeFriend(localSone: Sone, friendSoneId: String) { + if (!localSone.isLocal) { + return + } + memoryFriendDatabase.removeFriend(localSone.id, friendSoneId) + } + + override fun getFollowingTime(friendSoneId: String) = + memoryFriendDatabase.getFollowingTime(friendSoneId) + + override fun getPost(postId: String) = + readLock.withLock { allPosts[postId] } + + override fun getPosts(soneId: String): Collection = + sonePosts[soneId].toSet() + + override fun getDirectedPosts(recipientId: String) = + readLock.withLock { + allPosts.values.filter { + it.recipientId.orNull() == recipientId + } + } + + override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this) + + override fun storePost(post: Post) { + checkNotNull(post, "post must not be null") + writeLock.withLock { + allPosts[post.id] = post + sonePosts[post.sone.id].add(post) + } + } + + override fun removePost(post: Post) { + checkNotNull(post, "post must not be null") + writeLock.withLock { + allPosts.remove(post.id) + sonePosts[post.sone.id].remove(post) + post.sone.removePost(post) + } + } + + override fun getPostReply(id: String) = readLock.withLock { allPostReplies[id] } + + override fun getReplies(postId: String) = + readLock.withLock { + allPostReplies.values + .filter { it.postId == postId } + .sortedWith(newestReplyFirst.reversed()) + } + + override fun newPostReplyBuilder(): PostReplyBuilder = + MemoryPostReplyBuilder(this, this) + + override fun storePostReply(postReply: PostReply) = + writeLock.withLock { + allPostReplies[postReply.id] = postReply + } + + override fun removePostReply(postReply: PostReply) = + writeLock.withLock { + allPostReplies.remove(postReply.id) + }.unit + + override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] } + + override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl() + + override fun storeAlbum(album: Album) = + writeLock.withLock { + allAlbums[album.id] = album + soneAlbums.put(album.sone.id, album) + }.unit + + override fun removeAlbum(album: Album) = + writeLock.withLock { + allAlbums.remove(album.id) + soneAlbums.remove(album.sone.id, album) + }.unit + + override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] } + + override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl() + + override fun storeImage(image: Image): Unit = + writeLock.withLock { + allImages[image.id] = image + soneImages.put(image.sone.id, image) + } + + override fun removeImage(image: Image): Unit = + writeLock.withLock { + allImages.remove(image.id) + soneImages.remove(image.sone.id, image) + } + + override fun bookmarkPost(post: Post) = + memoryBookmarkDatabase.bookmarkPost(post) + + override fun unbookmarkPost(post: Post) = + memoryBookmarkDatabase.unbookmarkPost(post) + + override fun isPostBookmarked(post: Post) = + memoryBookmarkDatabase.isPostBookmarked(post) + + protected fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts } + + fun setPostKnown(post: Post, known: Boolean): Unit = + writeLock.withLock { + if (known) + knownPosts.add(post.id) + else + knownPosts.remove(post.id) + saveKnownPosts() + } + + protected fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies } + + override fun setPostReplyKnown(postReply: PostReply): Unit = + writeLock.withLock { + knownPostReplies.add(postReply.id) + saveKnownPostReplies() + } + + private fun loadKnownPosts() = + configurationLoader.loadKnownPosts() + .let { + writeLock.withLock { + knownPosts.clear() + knownPosts.addAll(it) + } + } + + private fun saveKnownPosts() = + saveKnownPostsRateLimiter.tryAcquire().ifTrue { + try { + readLock.withLock { + knownPosts.forEachIndexed { index, knownPostId -> + configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId + } + configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null + } + } catch (ce1: ConfigurationException) { + throw DatabaseException("Could not save database.", ce1) + } + } + + private fun loadKnownPostReplies(): Unit = + configurationLoader.loadKnownPostReplies().let { knownPostReplies -> + writeLock.withLock { + this.knownPostReplies.clear() + this.knownPostReplies.addAll(knownPostReplies) + } + } + + private fun saveKnownPostReplies() = + saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue { + try { + readLock.withLock { + knownPostReplies.forEachIndexed { index, knownPostReply -> + configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply + } + configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null + } + } catch (ce1: ConfigurationException) { + throw DatabaseException("Could not save database.", ce1) + } + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/fcp/AbstractSoneCommand.kt b/src/main/kotlin/net/pterodactylus/sone/fcp/AbstractSoneCommand.kt index 08e0593..f884e34 100644 --- a/src/main/kotlin/net/pterodactylus/sone/fcp/AbstractSoneCommand.kt +++ b/src/main/kotlin/net/pterodactylus/sone/fcp/AbstractSoneCommand.kt @@ -44,12 +44,12 @@ abstract class AbstractSoneCommand val requiresWriteAccess: Boolean = false) : AbstractCommand() { @Throws(FcpException::class) - protected fun getSone(simpleFieldSet: SimpleFieldSet, parameterName: String, localOnly: Boolean): Sone = - getSone(simpleFieldSet, parameterName, localOnly, true).get() + protected fun SimpleFieldSet.getSone(parameterName: String, localOnly: Boolean): Sone = + getSone(parameterName, localOnly, true).get() @Throws(FcpException::class) - protected fun getSone(simpleFieldSet: SimpleFieldSet, parameterName: String, localOnly: Boolean, mandatory: Boolean): Optional { - val soneId = simpleFieldSet.get(parameterName) + protected fun SimpleFieldSet.getSone(parameterName: String, localOnly: Boolean, mandatory: Boolean): Optional { + val soneId = get(parameterName) .throwOnNullIf(mandatory) { FcpException("Could not load Sone ID from “$parameterName”.") } ?: return Optional.absent() val sone = core.getSone(soneId) @@ -60,9 +60,9 @@ abstract class AbstractSoneCommand } @Throws(FcpException::class) - protected fun getPost(simpleFieldSet: SimpleFieldSet, parameterName: String): Post { + protected fun SimpleFieldSet.getPost(parameterName: String): Post { try { - val postId = simpleFieldSet.getString(parameterName) + val postId = getString(parameterName) return core.getPost(postId) ?: throw FcpException("Could not load post from “$postId”.") } catch (fspe1: FSParseException) { @@ -71,9 +71,9 @@ abstract class AbstractSoneCommand } @Throws(FcpException::class) - protected fun getReply(simpleFieldSet: SimpleFieldSet, parameterName: String): PostReply { + protected fun SimpleFieldSet.getReply(parameterName: String): PostReply { try { - val replyId = simpleFieldSet.getString(parameterName) + val replyId = getString(parameterName) return core.getPostReply(replyId) ?: throw FcpException("Could not load reply from “$replyId”.") } catch (fspe1: FSParseException) { diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt index 6097916..88527b3 100644 --- a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/DefaultIdentity.kt @@ -17,7 +17,9 @@ package net.pterodactylus.sone.freenet.wot -import java.util.Collections.* +import java.util.Collections.synchronizedMap +import java.util.Collections.synchronizedSet +import kotlin.collections.set /** * A Web of Trust identity. @@ -77,6 +79,10 @@ open class DefaultIdentity(private val id: String, private val nickname: String? } } + override fun getTrust(): Map = synchronized(trustCache) { + trustCache.toMap() + } + override fun getTrust(ownIdentity: OwnIdentity) = synchronized(trustCache) { trustCache[ownIdentity] } diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt new file mode 100644 index 0000000..ffcafb3 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.kt @@ -0,0 +1,68 @@ +/* + * Sone - IdentityChangeDetector.kt - Copyright © 2013–2020 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.freenet.wot + +/** + * Detects changes between two lists of [Identity]s. The detector can find + * added and removed identities, and for identities that exist in both list + * their contexts and properties are checked for added, removed, or (in case of + * properties) changed values. + */ +class IdentityChangeDetector(oldIdentities: Collection) { + + private val oldIdentities: Map = oldIdentities.associateBy { it.id } + var onNewIdentity: IdentityProcessor? = null + var onRemovedIdentity: IdentityProcessor? = null + var onChangedIdentity: IdentityProcessor? = null + var onUnchangedIdentity: IdentityProcessor? = null + + fun detectChanges(newIdentities: Collection) { + onRemovedIdentity.notify(oldIdentities.values.filter { it !in newIdentities }) + onNewIdentity.notify(newIdentities.filter { it !in oldIdentities.values }) + onChangedIdentity.notify(newIdentities.filter { it.id in oldIdentities }.filter { identityHasChanged(oldIdentities[it.id]!!, it) }) + onUnchangedIdentity.notify(newIdentities.filter { it.id in oldIdentities }.filterNot { identityHasChanged(oldIdentities[it.id]!!, it) }) + } + + private fun identityHasChanged(oldIdentity: Identity, newIdentity: Identity?) = + identityHasNewContexts(oldIdentity, newIdentity!!) + || identityHasRemovedContexts(oldIdentity, newIdentity) + || identityHasNewProperties(oldIdentity, newIdentity) + || identityHasRemovedProperties(oldIdentity, newIdentity) + || identityHasChangedProperties(oldIdentity, newIdentity) + + private fun identityHasNewContexts(oldIdentity: Identity, newIdentity: Identity) = + newIdentity.contexts.any { it !in oldIdentity.contexts } + + private fun identityHasRemovedContexts(oldIdentity: Identity, newIdentity: Identity) = + oldIdentity.contexts.any { it !in newIdentity.contexts } + + private fun identityHasNewProperties(oldIdentity: Identity, newIdentity: Identity) = + newIdentity.properties.keys.any { it !in oldIdentity.properties } + + private fun identityHasRemovedProperties(oldIdentity: Identity, newIdentity: Identity) = + oldIdentity.properties.keys.any { it !in newIdentity.properties } + + private fun identityHasChangedProperties(oldIdentity: Identity, newIdentity: Identity) = + oldIdentity.properties.entries.any { newIdentity.properties[it.key] != it.value } + +} + +typealias IdentityProcessor = (Identity) -> Unit + +private fun IdentityProcessor?.notify(identities: Iterable) = + this?.let { identities.forEach(this::invoke) } diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt index 474ab57..f6e1d59 100644 --- a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityLoader.kt @@ -31,11 +31,16 @@ class IdentityLoader @Inject constructor(private val webOfTrustConnector: WebOfT private val logger: Logger = Logger.getLogger(IdentityLoader::class.java.name) @Throws(WebOfTrustException::class) - fun loadIdentities() = + fun loadTrustedIdentities() = time({ stopwatch, identities -> "Loaded ${identities.size} own identities in ${stopwatch.elapsed(MILLISECONDS) / 1000.0}s." }) { webOfTrustConnector.loadAllOwnIdentities() }.let(this::loadTrustedIdentitiesForOwnIdentities) + fun loadAllIdentities() = + time({ stopwatch, identities -> "Loaded ${identities.size} own identities in ${stopwatch.elapsed(MILLISECONDS) / 1000.0}s." }) { + webOfTrustConnector.loadAllOwnIdentities() + }.let(this::loadAllIdentitiesForOwnIdentities) + @Throws(PluginException::class) private fun loadTrustedIdentitiesForOwnIdentities(ownIdentities: Collection) = ownIdentities @@ -53,6 +58,22 @@ class IdentityLoader @Inject constructor(private val webOfTrustConnector: WebOfT } } + private fun loadAllIdentitiesForOwnIdentities(ownIdentities: Collection) = + ownIdentities + .also { logger.fine { "Getting trusted identities for ${it.size} own identities..." } } + .associateWith { ownIdentity -> + logger.fine { "Getting trusted identities for $ownIdentity..." } + if (ownIdentity.doesNotHaveCorrectContext()) { + logger.fine { "Skipping $ownIdentity because of incorrect context." } + emptySet() + } else { + logger.fine { "Loading trusted identities for $ownIdentity from WoT..." } + time({ stopwatch, identities -> "Loaded ${identities.size} identities for ${ownIdentity.nickname} in ${stopwatch.elapsed(MILLISECONDS) / 1000.0}s." }) { + webOfTrustConnector.loadAllIdentities(ownIdentity, context?.context) + } + } + } + private fun OwnIdentity.doesNotHaveCorrectContext() = context?.let { it.context !in contexts } ?: false diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt index 67e70e0..829affc 100644 --- a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/IdentityManagerImpl.kt @@ -17,12 +17,18 @@ package net.pterodactylus.sone.freenet.wot -import com.google.common.eventbus.* -import com.google.inject.* -import net.pterodactylus.util.service.* -import java.util.concurrent.TimeUnit.* -import java.util.logging.* -import java.util.logging.Logger.* +import com.google.common.eventbus.EventBus +import com.google.common.eventbus.Subscribe +import com.google.inject.Inject +import com.google.inject.Singleton +import net.pterodactylus.sone.core.event.StrictFilteringActivatedEvent +import net.pterodactylus.sone.core.event.StrictFilteringDeactivatedEvent +import net.pterodactylus.util.service.AbstractService +import java.util.concurrent.TimeUnit.SECONDS +import java.util.concurrent.atomic.AtomicBoolean +import java.util.logging.Level +import java.util.logging.Logger +import java.util.logging.Logger.getLogger /** * The identity manager takes care of loading and storing identities, their @@ -42,6 +48,7 @@ class IdentityManagerImpl @Inject constructor( ) : AbstractService("Sone Identity Manager", false), IdentityManager { private val currentOwnIdentities = mutableSetOf() + private val strictFiltering = AtomicBoolean(false) override val isConnected: Boolean get() = notThrowing { webOfTrustConnector.ping() } @@ -56,7 +63,7 @@ class IdentityManagerImpl @Inject constructor( while (!shouldStop()) { try { - val currentIdentities = identityLoader.loadIdentities() + val currentIdentities = identityLoader.loadAllIdentities().applyStrictFiltering() val identityChangeEventSender = IdentityChangeEventSender(eventBus, oldIdentities) identityChangeEventSender.detectChanges(currentIdentities) @@ -78,6 +85,38 @@ class IdentityManagerImpl @Inject constructor( } } + private fun Map>.applyStrictFiltering() = + if (strictFiltering.get()) { + val identitiesWithTrust = values.flatten() + .groupBy { it.id } + .mapValues { (_, identities) -> + identities.reduce { accIdentity, identity -> + identity.trust.forEach { (ownIdentity: OwnIdentity?, trust: Trust?) -> + accIdentity.setTrust(ownIdentity, trust) + } + accIdentity + } + } + + mapValues { (_, trustedIdentities) -> + trustedIdentities.filter { trustedIdentity -> + identitiesWithTrust[trustedIdentity.id]!!.trust.all { it.value.hasZeroOrPositiveTrust() } + } + } + } else { + this + } + + @Subscribe + fun strictFilteringActivated(event: StrictFilteringActivatedEvent) { + strictFiltering.set(true) + } + + @Subscribe + fun strictFilteringDeactivated(event: StrictFilteringDeactivatedEvent) { + strictFiltering.set(false) + } + } private val logger: Logger = getLogger(IdentityManagerImpl::class.java.name) @@ -89,3 +128,10 @@ private fun notThrowing(action: () -> Unit): Boolean = } catch (e: Exception) { false } + +private fun Trust.hasZeroOrPositiveTrust() = + if (explicit == null) { + implicit == null || implicit >= 0 + } else { + explicit >= 0 + } diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt index 2fad1d1..d829734 100644 --- a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/PluginWebOfTrustConnector.kt @@ -17,15 +17,17 @@ package net.pterodactylus.sone.freenet.wot -import com.google.inject.* -import freenet.support.* -import kotlinx.coroutines.* -import net.pterodactylus.sone.freenet.* -import net.pterodactylus.sone.freenet.plugin.* -import java.lang.String.* -import java.util.logging.* +import com.google.inject.Inject +import freenet.support.SimpleFieldSet +import kotlinx.coroutines.runBlocking +import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder +import net.pterodactylus.sone.freenet.plugin.PluginConnector +import net.pterodactylus.sone.freenet.plugin.PluginException +import net.pterodactylus.sone.freenet.plugin.PluginReply +import java.lang.String.format +import java.util.logging.Level import java.util.logging.Logger -import java.util.logging.Logger.* +import java.util.logging.Logger.getLogger /** * Connector for the Web of Trust plugin. @@ -46,6 +48,14 @@ class PluginWebOfTrustConnector @Inject constructor(private val pluginConnector: .fields .parseIdentities { parseTrustedIdentity(it, ownIdentity) } + override fun loadAllIdentities(ownIdentity: OwnIdentity, context: String?): Set = + performRequest(SimpleFieldSetBuilder().put("Message", "GetIdentitiesByScore").put("Truster", ownIdentity.id).put("Selection", "+").put("Context", context ?: "").put("WantTrustValues", "true").get()) + .fields + .parseIdentities { parseTrustedIdentity(it, ownIdentity) } + + performRequest(SimpleFieldSetBuilder().put("Message", "GetIdentitiesByScore").put("Truster", ownIdentity.id).put("Selection", "-").put("Context", context ?: "").put("WantTrustValues", "true").get()) + .fields + .parseIdentities { parseTrustedIdentity(it, ownIdentity) } + @Throws(PluginException::class) override fun addContext(ownIdentity: OwnIdentity, context: String) { performRequest(SimpleFieldSetBuilder().put("Message", "AddContext").put("Identity", ownIdentity.id).put("Context", context).get()) diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt index e31dce6..a407e2a 100644 --- a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt +++ b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.kt @@ -29,6 +29,16 @@ interface WebOfTrustConnector { fun loadTrustedIdentities(ownIdentity: OwnIdentity, context: String? = null): Set /** + * Loads all identities known to the given own identity that have the (optional) given context. + * + * @param ownIdentity The own identity + * @param context The context to filter, or `null` + * @return All trusted identities + * @throws PluginException if an error occured talking to the Web of Trust plugin + */ + fun loadAllIdentities(ownIdentity: OwnIdentity, context: String? = null): Set + + /** * Adds the given context to the given identity. * * @param ownIdentity The identity to add the context to diff --git a/src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt b/src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt index 749de0d..a95cf78 100644 --- a/src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt +++ b/src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt @@ -7,10 +7,13 @@ import com.google.inject.* import com.google.inject.matcher.* import com.google.inject.name.Names.* import com.google.inject.spi.* +import net.pterodactylus.sone.core.SoneUriCreator import net.pterodactylus.sone.database.* import net.pterodactylus.sone.database.memory.* import net.pterodactylus.sone.freenet.* import net.pterodactylus.sone.freenet.wot.* +import net.pterodactylus.sone.web.FreenetSessionProvider +import net.pterodactylus.sone.web.SessionProvider import net.pterodactylus.util.config.* import net.pterodactylus.util.config.ConfigurationException import net.pterodactylus.util.logging.* @@ -61,6 +64,8 @@ open class SoneModule(private val sonePlugin: SonePlugin, private val eventBus: bind(MetricRegistry::class.java).`in`(Singleton::class.java) bind(WebOfTrustConnector::class.java).to(PluginWebOfTrustConnector::class.java).`in`(Singleton::class.java) bind(TickerShutdown::class.java).`in`(Singleton::class.java) + bind(SoneUriCreator::class.java).`in`(Singleton::class.java) + bind(SessionProvider::class.java).to(FreenetSessionProvider::class.java).`in`(Singleton::class.java) bindListener(Matchers.any(), object : TypeListener { override fun hear(typeLiteral: TypeLiteral, typeEncounter: TypeEncounter) { diff --git a/src/main/kotlin/net/pterodactylus/sone/main/SonePlugin.kt b/src/main/kotlin/net/pterodactylus/sone/main/SonePlugin.kt new file mode 100644 index 0000000..5e0b2c1 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/main/SonePlugin.kt @@ -0,0 +1,7 @@ +package net.pterodactylus.sone.main + +data class PluginVersion(val version: String) + +data class PluginYear(val year: Int) + +data class PluginHomepage(val homepage: String) diff --git a/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt b/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt index ab7f6ba..42239aa 100644 --- a/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt +++ b/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt @@ -51,5 +51,5 @@ class PostAccessor(private val core: Core) : ReflectionAccessor() { } -private fun Core.getReplies(post: Post) = getReplies(post.id).filter { Reply.FUTURE_REPLY_FILTER.apply(it) } +private fun Core.getReplies(post: Post) = getReplies(post.id).filter(noFutureReply) private val TemplateContext?.currentSone: Sone? get() = this?.get("currentSone") as? Sone diff --git a/src/main/kotlin/net/pterodactylus/sone/text/SoneTextParser.kt b/src/main/kotlin/net/pterodactylus/sone/text/SoneTextParser.kt index b711ca8..24b04fa 100644 --- a/src/main/kotlin/net/pterodactylus/sone/text/SoneTextParser.kt +++ b/src/main/kotlin/net/pterodactylus/sone/text/SoneTextParser.kt @@ -1,7 +1,6 @@ package net.pterodactylus.sone.text import freenet.keys.* -import freenet.support.* import net.pterodactylus.sone.data.* import net.pterodactylus.sone.data.impl.* import net.pterodactylus.sone.database.* diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucket.kt b/src/main/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucket.kt deleted file mode 100644 index 58c181f..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucket.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.pterodactylus.sone.utils - -import freenet.support.api.Bucket - -class AutoCloseableBucket(val bucket: Bucket) : AutoCloseable { - - override fun close() { - bucket.free() - } - -} diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/DefaultOption.kt b/src/main/kotlin/net/pterodactylus/sone/utils/DefaultOption.kt new file mode 100644 index 0000000..fd4215f --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/utils/DefaultOption.kt @@ -0,0 +1,28 @@ +package net.pterodactylus.sone.utils + +/** + * Basic implementation of an [Option]. + * + * @param The type of the option + */ +class DefaultOption @JvmOverloads constructor( + private val defaultValue: T, + private val validator: ((T) -> Boolean)? = null +) : Option { + + @Volatile + private var value: T? = null + + override fun get() = value ?: defaultValue + + override fun getReal(): T? = value + + override fun validate(value: T?): Boolean = + value == null || validator?.invoke(value) ?: true + + override fun set(value: T?) { + require(validate(value)) { "New Value ($value) could not be validated." } + this.value = value + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/AllPages.kt b/src/main/kotlin/net/pterodactylus/sone/web/AllPages.kt new file mode 100644 index 0000000..c538f0e --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/AllPages.kt @@ -0,0 +1,52 @@ +package net.pterodactylus.sone.web + +import net.pterodactylus.sone.web.pages.* +import javax.inject.Inject + +/** + * Container for all web pages. This uses field injection because there are way too many pages + * to sensibly use constructor injection. + */ +class AllPages { + + @Inject lateinit var aboutPage: AboutPage + @Inject lateinit var bookmarkPage: BookmarkPage + @Inject lateinit var bookmarksPage: BookmarksPage + @Inject lateinit var createAlbumPage: CreateAlbumPage + @Inject lateinit var createPostPage: CreatePostPage + @Inject lateinit var createReplyPage: CreateReplyPage + @Inject lateinit var createSonePage: CreateSonePage + @Inject lateinit var deleteAlbumPage: DeleteAlbumPage + @Inject lateinit var deleteImagePage: DeleteImagePage + @Inject lateinit var deletePostPage: DeletePostPage + @Inject lateinit var deleteProfileFieldPage: DeleteProfileFieldPage + @Inject lateinit var deleteReplyPage: DeleteReplyPage + @Inject lateinit var deleteSonePage: DeleteSonePage + @Inject lateinit var dismissNotificationPage: DismissNotificationPage + @Inject lateinit var editAlbumPage: EditAlbumPage + @Inject lateinit var editImagePage: EditImagePage + @Inject lateinit var editProfileFieldPage: EditProfileFieldPage + @Inject lateinit var editProfilePage: EditProfilePage + @Inject lateinit var followSonePage: FollowSonePage + @Inject lateinit var getImagePage: GetImagePage + @Inject lateinit var imageBrowserPage: ImageBrowserPage + @Inject lateinit var indexPage: IndexPage + @Inject lateinit var knownSonesPage: KnownSonesPage + @Inject lateinit var likePage: LikePage + @Inject lateinit var lockSonePage: LockSonePage + @Inject lateinit var loginPage: LoginPage + @Inject lateinit var logoutPage: LogoutPage + @Inject lateinit var markAsKnownPage: MarkAsKnownPage + @Inject lateinit var newPage: NewPage + @Inject lateinit var optionsPage: OptionsPage + @Inject lateinit var rescuePage: RescuePage + @Inject lateinit var searchPage: SearchPage + @Inject lateinit var unbookmarkPage: UnbookmarkPage + @Inject lateinit var unfollowSonePage: UnfollowSonePage + @Inject lateinit var unlikePage: UnlikePage + @Inject lateinit var unlockSonePage: UnlockSonePage + @Inject lateinit var uploadImagePage: UploadImagePage + @Inject lateinit var viewPostPage: ViewPostPage + @Inject lateinit var viewSonePage: ViewSonePage + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/FreenetSessionProvider.kt b/src/main/kotlin/net/pterodactylus/sone/web/FreenetSessionProvider.kt new file mode 100644 index 0000000..8928f12 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/FreenetSessionProvider.kt @@ -0,0 +1,53 @@ +/** + * Sone - FreenetSessionProvider.kt - Copyright © 2020 David ‘Bombe’ Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.web + +import freenet.clients.http.SessionManager +import freenet.clients.http.ToadletContext +import net.pterodactylus.sone.data.Sone +import net.pterodactylus.sone.database.SoneProvider +import java.util.UUID +import javax.inject.Inject + +/** + * [SoneProvider] implementation based on Freenet’s [SessionManager]. + */ +class FreenetSessionProvider @Inject constructor(private val soneProvider: SoneProvider, private val sessionManager: SessionManager) : SessionProvider { + + override fun getCurrentSone(toadletContext: ToadletContext): Sone? = + soneProvider.localSones.singleOrNull() + ?: sessionManager.useSession(toadletContext) + ?.let { it.getAttribute("Sone.CurrentSone") as? String } + ?.let(soneProvider.soneLoader) + ?.takeIf { it.isLocal } + + override fun setCurrentSone(toadletContext: ToadletContext, sone: Sone?) { + if (sone == null) { + sessionManager.useSession(toadletContext) + ?.removeAttribute("Sone.CurrentSone") + } else { + sessionManager.getOrCreateSession(toadletContext) + ?.setAttribute("Sone.CurrentSone", sone.id) + } + } + + private fun SessionManager.getOrCreateSession(toadletContext: ToadletContext) = + useSession(toadletContext) + ?: createSession(UUID.randomUUID().toString(), toadletContext) + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/SessionProvider.kt b/src/main/kotlin/net/pterodactylus/sone/web/SessionProvider.kt index 463ddaa..93cd6af 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/SessionProvider.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/SessionProvider.kt @@ -8,7 +8,7 @@ import net.pterodactylus.sone.data.Sone */ interface SessionProvider { - fun getCurrentSone(toadletContext: ToadletContext, createSession: Boolean = true): Sone? + fun getCurrentSone(toadletContext: ToadletContext): Sone? fun setCurrentSone(toadletContext: ToadletContext, sone: Sone?) } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt index 66c8ed2..c60fe56 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt @@ -21,7 +21,7 @@ class GetNotificationsAjaxPage @Inject constructor(webInterface: WebInterface) : override val requiresLogin = false override fun createJsonObject(request: FreenetRequest) = - getCurrentSone(request.toadletContext, false).let { currentSone -> + getCurrentSone(request.toadletContext).let { currentSone -> webInterface.getNotifications(currentSone) .sortedBy(Notification::getCreatedTime) .let { notifications -> diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.kt index 75f3c3c..5d17262 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.kt @@ -35,7 +35,7 @@ class GetStatusAjaxPage(webInterface: WebInterface, private val elementLoader: E } override fun createJsonObject(request: FreenetRequest) = - getCurrentSone(request.toadletContext, false).let { currentSone -> + getCurrentSone(request.toadletContext).let { currentSone -> createSuccessJsonObject().apply { this["loggedIn"] = currentSone != null this["options"] = currentSone?.options?.toJsonOptions() ?: jsonObject {} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonPage.kt index 99c0828..356aca6 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonPage.kt @@ -31,8 +31,8 @@ abstract class JsonPage(protected val webInterface: WebInterface) : Page. + */ + +package net.pterodactylus.sone.web.page + +import freenet.client.HighLevelSimpleClient +import freenet.clients.http.LinkEnabledCallback +import freenet.clients.http.LinkFilterExceptedToadlet +import freenet.clients.http.Toadlet +import freenet.clients.http.ToadletContext +import freenet.support.MultiValueTable +import freenet.support.api.HTTPRequest +import net.pterodactylus.sone.utils.use +import net.pterodactylus.util.web.Method +import net.pterodactylus.util.web.Page +import net.pterodactylus.util.web.Response +import java.net.URI + +/** + * [Toadlet] implementation that is wrapped around a [Page]. + */ +class PageToadlet( + highLevelSimpleClient: HighLevelSimpleClient, + val menuName: String?, + private val page: Page, + private val pathPrefix: String +) : Toadlet(highLevelSimpleClient), LinkEnabledCallback, LinkFilterExceptedToadlet { + + override fun path() = pathPrefix + page.path + + override fun handleMethodGET(uri: URI, httpRequest: HTTPRequest, toadletContext: ToadletContext) = + handleRequest(FreenetRequest(uri, Method.GET, httpRequest, toadletContext)) + + fun handleMethodPOST(uri: URI?, httpRequest: HTTPRequest?, toadletContext: ToadletContext?) = + handleRequest(FreenetRequest(uri!!, Method.POST, httpRequest!!, toadletContext!!)) + + private fun handleRequest(pageRequest: FreenetRequest) { + pageRequest.toadletContext.bucketFactory.makeBucket(-1).use { pageBucket -> + pageBucket.outputStream.use { pageBucketOutputStream -> + val pageResponse = page.handleRequest(pageRequest, Response(pageBucketOutputStream)) + // according to the javadoc, headers is allowed to return null but that’s stupid and it doesn’t do that. + val headers = pageResponse.headers.fold(MultiValueTable()) { headers, header -> + headers.apply { + header.forEach { put(header.name, it) } + } + } + with(pageResponse) { + writeReply(pageRequest.toadletContext, statusCode, contentType, statusText, headers, pageBucket) + } + } + } + } + + override fun isEnabled(toadletContext: ToadletContext) = + if (page is LinkEnabledCallback) { + page.isEnabled(toadletContext) + } else + true + + override fun isLinkExcepted(link: URI) = + page is FreenetPage && page.isLinkExcepted(link) + + override fun toString() = "${javaClass.name}[path=${path()},page=$page]" + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/web/page/PageToadletFactory.kt b/src/main/kotlin/net/pterodactylus/sone/web/page/PageToadletFactory.kt index 4c671ad..61d8718 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/page/PageToadletFactory.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/page/PageToadletFactory.kt @@ -18,18 +18,16 @@ package net.pterodactylus.sone.web.page import freenet.client.* -import freenet.clients.http.* import net.pterodactylus.util.web.* import javax.inject.* class PageToadletFactory @Inject constructor( private val highLevelSimpleClient: HighLevelSimpleClient, - private val sessionManager: SessionManager, @Named("toadletPathPrefix") private val pathPrefix: String ) { @JvmOverloads fun createPageToadlet(page: Page, menuName: String? = null) = - PageToadlet(highLevelSimpleClient, sessionManager, menuName ?: page.menuName, page, pathPrefix) + PageToadlet(highLevelSimpleClient, menuName ?: page.menuName, page, pathPrefix) } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/page/SoneRequest.kt b/src/main/kotlin/net/pterodactylus/sone/web/page/SoneRequest.kt index 703f953..fcfc63d 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/page/SoneRequest.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/page/SoneRequest.kt @@ -7,10 +7,10 @@ import net.pterodactylus.sone.web.* import net.pterodactylus.util.web.* import java.net.* -class SoneRequest(uri: URI, method: Method, httpRequest: HTTPRequest, toadletContext: ToadletContext, sessionManager: SessionManager, +class SoneRequest(uri: URI, method: Method, httpRequest: HTTPRequest, toadletContext: ToadletContext, val core: Core, val webInterface: WebInterface -) : FreenetRequest(uri, method, httpRequest, toadletContext, sessionManager) +) : FreenetRequest(uri, method, httpRequest, toadletContext) fun FreenetRequest.toSoneRequest(core: Core, webInterface: WebInterface) = - SoneRequest(uri, method, httpRequest, toadletContext, sessionManager, core, webInterface) + SoneRequest(uri, method, httpRequest, toadletContext, core, webInterface) diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt index 0b690da..959788b 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt @@ -21,7 +21,7 @@ class CreateSonePage @Inject constructor(webInterface: WebInterface, loaders: Lo private val logger = Logger.getLogger(CreateSonePage::class.java.name) override fun handleRequest(soneRequest: SoneRequest, templateContext: TemplateContext) { - templateContext["sones"] = soneRequest.core.localSones.sortedWith(Sone.NICE_NAME_COMPARATOR) + templateContext["sones"] = soneRequest.core.localSones.sortedWith(niceNameComparator) templateContext["identitiesWithoutSone"] = soneRequest.core.identityManager.allOwnIdentities.filterNot { "Sone" in it.contexts }.sortedBy { "${it.nickname}@${it.id}".toLowerCase() } if (soneRequest.isPOST) { val identity = soneRequest.httpRequest.getPartAsStringFailsafe("identity", 43) diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPage.kt index 20219d7..a0ca0d7 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPage.kt @@ -29,9 +29,7 @@ class ImageBrowserPage @Inject constructor(webInterface: WebInterface, loaders: } else if (soneRequest.parameters["mode"] == "gallery") { templateContext["galleryRequested"] = true soneRequest.core.sones - .map(Sone::getRootAlbum) - .flatMap(Album::getAlbums) - .flatMap { Album.FLATTENER.apply(it)!! } + .flatMap(Sone::allAlbums) .filterNot(Album::isEmpty) .sortedBy(Album::getTitle) .also { albums -> diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPage.kt index ae0d7d1..01d1d22 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPage.kt @@ -28,10 +28,10 @@ class KnownSonesPage @Inject constructor(webInterface: WebInterface, loaders: Lo .filterNot { soneRequest.parameters["filter"] == "not-own" && it.isLocal } .sortedWith( when (soneRequest.parameters["sort"]) { - "images" -> Sone.IMAGE_COUNT_COMPARATOR - "name" -> Sone.NICE_NAME_COMPARATOR.reversed() - "posts" -> Sone.POST_COUNT_COMPARATOR - else -> Sone.LAST_ACTIVITY_COMPARATOR + "images" -> imageCountComparator + "name" -> niceNameComparator.reversed() + "posts" -> postCountComparator + else -> lastActivityComparator }.let { comparator -> when (soneRequest.parameters["order"]) { "asc" -> comparator.reversed() diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt index 5be34ac..6244325 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt @@ -26,7 +26,7 @@ class LoginPage @Inject constructor(webInterface: WebInterface, loaders: Loaders redirectTo(target) } } - templateContext["sones"] = soneRequest.core.localSones.sortedWith(Sone.NICE_NAME_COMPARATOR) + templateContext["sones"] = soneRequest.core.localSones.sortedWith(niceNameComparator) templateContext["identitiesWithoutSone"] = soneRequest.core.identityManager.allOwnIdentities.filterNot { "Sone" in it.contexts }.sortedBy { "${it.nickname}@${it.id}" } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt index 412258d..9465a6a 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt @@ -40,9 +40,11 @@ class OptionsPage @Inject constructor(webInterface: WebInterface, loaders: Loade } val fullAccessRequired = "require-full-access" in soneRequest.parameters val fcpInterfaceActive = "fcp-interface-active" in soneRequest.parameters + val strictFiltering = "strict-filtering" in soneRequest.parameters soneRequest.core.preferences.newRequireFullAccess = fullAccessRequired soneRequest.core.preferences.newFcpInterfaceActive = fcpInterfaceActive + soneRequest.core.preferences.newStrictFiltering = strictFiltering val postsPerPage = soneRequest.parameters["posts-per-page"]?.toIntOrNull() val charactersPerPost = soneRequest.parameters["characters-per-post"]?.toIntOrNull() @@ -82,6 +84,7 @@ class OptionsPage @Inject constructor(webInterface: WebInterface, loaders: Loade templateContext["require-full-access"] = preferences.requireFullAccess templateContext["post-cut-off-length"] = preferences.postCutOffLength templateContext["posts-per-page"] = preferences.postsPerPage + templateContext["strict-filtering"] = preferences.strictFiltering } } diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt index 19eba1c..9bbc3e5 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt @@ -58,7 +58,7 @@ class SearchPage(webInterface: WebInterface, loaders: Loaders, templateRenderer: val postPagination = cache.get(phrases) { soneRequest.core.sones .flatMap(Sone::getPosts) - .filter { Post.FUTURE_POSTS_FILTER.apply(it) } + .filter(noFuturePost) .scoreAndPaginate(phrases, soneRequest.core.preferences.postsPerPage) { it.allText(soneNameCache, soneRequest.core::getReplies) } }.apply { page = soneRequest.parameters["postPage"].emptyToNull?.toIntOrNull() ?: 0 } @@ -88,7 +88,7 @@ class SearchPage(webInterface: WebInterface, loaders: Loaders, templateRenderer: private fun Post.allText(soneNameCache: (Sone) -> String, getReplies: (String) -> Collection) = (text + recipient.orNull()?.let { " ${soneNameCache(it)}" } + getReplies(id) - .filter { PostReply.FUTURE_REPLY_FILTER.apply(it) } + .filter(noFutureReply) .map { "${soneNameCache(it.sone)} ${it.text}" }.joinToString(" ", " ")).toLowerCase() private fun Iterable.indicesFor(text: String, predicate: (Phrase) -> Boolean) = diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt index 9734a03..86131c3 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt @@ -29,7 +29,7 @@ open class SoneTemplatePage( protected val translation: Translation = webInterface.translation protected fun getCurrentSone(toadletContext: ToadletContext, createSession: Boolean = true) = - sessionProvider.getCurrentSone(toadletContext, createSession) + sessionProvider.getCurrentSone(toadletContext) protected fun setCurrentSone(toadletContext: ToadletContext, sone: Sone?) = sessionProvider.setCurrentSone(toadletContext, sone) @@ -91,7 +91,7 @@ open class SoneTemplatePage( private val String.urlEncode: String get() = URLEncoder.encode(this, "UTF-8") override fun isEnabled(toadletContext: ToadletContext) = - isEnabled(SoneRequest(toadletContext.uri, Method.GET, HTTPRequestImpl(toadletContext.uri, "GET"), toadletContext, webInterface.sessionManager, core, webInterface)) + isEnabled(SoneRequest(toadletContext.uri, Method.GET, HTTPRequestImpl(toadletContext.uri, "GET"), toadletContext, core, webInterface)) open fun isEnabled(soneRequest: SoneRequest) = when { requiresLogin && getCurrentSone(soneRequest.toadletContext) == null -> false diff --git a/src/main/resources/i18n/sone.de.properties b/src/main/resources/i18n/sone.de.properties index e17e995..cd42c45 100644 --- a/src/main/resources/i18n/sone.de.properties +++ b/src/main/resources/i18n/sone.de.properties @@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=FCP-Verbindungen nur von Page.Options.Option.FcpFullAccessRequired.Value.No=Nein Page.Options.Option.FcpFullAccessRequired.Value.Writing=Für Schreibzugriffe Page.Options.Option.FcpFullAccessRequired.Value.Always=Immer +Page.Options.Section.WebOfTrustOptions.Title=„Web of Trust“ Optionen +Page.Options.Option.StrictFiltering.Description=Identitäten strenger filtern. Wenn diese Option gewählt ist, werden Identitäten, die von mindestens einer Ihrer lokalen Identitäten einen negativen Vertrauenswert zugewiesen bekommen haben, komplett ignoriert; ansonsten werden Identitäten gezeigt, wenn sie von mindestens einer Ihrer Identitäten einen positiven Vertrauenswert zugewiesen bekommen. (Bitte beachten Sie, dass diese Einstellung ein paar Minuten braucht, um Wirkung zu zeigen!) Page.Options.Section.Cleaning.Title=Aufräumen Page.Options.Option.ClearOnNextRestart.Description=Setzt die Konfiguration des Sone-Plugins beim nächsten Start zurück. Vorsicht: {strong}Alle Informationen Ihrer Sones werden gelöscht{/strong}, also stellen Sie bitte sicher, dass Sie die notwendigen Sicherungen angefertigt haben! Damit diese Option aktiv wird, muss auch die folgende Option aktiviert werden. Page.Options.Option.ReallyClearOnNextRestart.Description=Diese Option muss auf „ja“ gestellt werden, wenn Sie wirklich {strong}wirklich{/strong} sämtliche Informationen des Sone-Plugins beim nächsten Start entfernen möchten. diff --git a/src/main/resources/i18n/sone.en.properties b/src/main/resources/i18n/sone.en.properties index 4376025..7c65dcf 100644 --- a/src/main/resources/i18n/sone.en.properties +++ b/src/main/resources/i18n/sone.en.properties @@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=Require FCP connection fro Page.Options.Option.FcpFullAccessRequired.Value.No=No Page.Options.Option.FcpFullAccessRequired.Value.Writing=For Write Access Page.Options.Option.FcpFullAccessRequired.Value.Always=Always +Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings +Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.) Page.Options.Section.Cleaning.Title=Clean Up Page.Options.Option.ClearOnNextRestart.Description=Resets the configuration of the Sone plugin at the next restart. Warning! {strong}This will destroy all of your Sones{/strong} so make sure you have backed up everyhing you still need! Also, you need to set the next option to true to actually do it. Page.Options.Option.ReallyClearOnNextRestart.Description=This option needs to be set to “yes” if you really, {strong}really{/strong} want to clear the plugin configuration on the next restart. diff --git a/src/main/resources/i18n/sone.es.properties b/src/main/resources/i18n/sone.es.properties index db20b73..bc90612 100644 --- a/src/main/resources/i18n/sone.es.properties +++ b/src/main/resources/i18n/sone.es.properties @@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=Requiere conexión FCP de Page.Options.Option.FcpFullAccessRequired.Value.No=No Page.Options.Option.FcpFullAccessRequired.Value.Writing=Para acceso de escritura Page.Options.Option.FcpFullAccessRequired.Value.Always=Siempre +Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings +Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.) Page.Options.Section.Cleaning.Title=Limpiar Page.Options.Option.ClearOnNextRestart.Description=Reinicia la configuración del plugin Sone en el siguiente reinicio.Cuidado! {strong}Esto destruirá todos tus Sone{/strong} de modo que asegurate de que has hecho una copia de seguridad de todo lo que necesitas! También tendrás que asignar cierto a la siguiente opción para hacerlo. Page.Options.Option.ReallyClearOnNextRestart.Description=Esta opción tiene que ser puesta en "yes" si realmente, {strong}realmente{/strong} quieres limpiar la configuración del plugin en el siguiente reinicio. diff --git a/src/main/resources/i18n/sone.fr.properties b/src/main/resources/i18n/sone.fr.properties index f4ff0d1..bf1eccd 100644 --- a/src/main/resources/i18n/sone.fr.properties +++ b/src/main/resources/i18n/sone.fr.properties @@ -26,8 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=Récupération Navigation.Menu.Sone.Item.Rescue.Tooltip=Récupération de votre Sone Navigation.Menu.Sone.Item.About.Name=A propos Navigation.Menu.Sone.Item.About.Tooltip=Informations à propos de Sone -Navigation.Menu.Sone.Item.Metrics.Name=Metrics -Navigation.Menu.Sone.Item.Metrics.Tooltip=Metrics collected by Sone +Navigation.Menu.Sone.Item.Metrics.Name=Métriques +Navigation.Menu.Sone.Item.Metrics.Tooltip=Métriques collectées par Sone Page.About.Title=A propos de - Sone Page.About.Page.Title=A propos @@ -74,6 +74,8 @@ Page.Options.Option.FcpFullAccessRequired.Description=Requière une connexion FC Page.Options.Option.FcpFullAccessRequired.Value.No=Non Page.Options.Option.FcpFullAccessRequired.Value.Writing=Pour accès à l'écriture Page.Options.Option.FcpFullAccessRequired.Value.Always=toujours +Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings +Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.) Page.Options.Section.Cleaning.Title=Nettoyer Page.Options.Option.ClearOnNextRestart.Description=Réinitialiser la configuration du plugin Sone au prochain redémarrage. Attention! {strong}Cela détruira tous vos Sones{/strong}. Soyez sûr d'avoir sauvegardé tout ce dont vous avez besoin! Vous devez également choisir "Oui" à l'option suivante pour procéder à la réinitialisation. Page.Options.Option.ReallyClearOnNextRestart.Description=Choisir "Oui" pour cette option si vous voulez vraiment{strong}vraiment{/strong} effacer la configuration au prochain redémarrage. diff --git a/src/main/resources/i18n/sone.it.properties b/src/main/resources/i18n/sone.it.properties new file mode 100644 index 0000000..d21adaa --- /dev/null +++ b/src/main/resources/i18n/sone.it.properties @@ -0,0 +1,468 @@ +Navigation.Menu.Sone.Name=Sone +Navigation.Menu.Sone.Tooltip=Freenet Social Network Plugin +Navigation.Menu.Sone.Item.Login.Name=Accedi +Navigation.Menu.Sone.Item.Login.Tooltip=Accedi al tuo "Sone" +Navigation.Menu.Sone.Item.Index.Name=Il tuo "Sone" +Navigation.Menu.Sone.Item.Index.Tooltip=Visualizza il tuo "Sone" +Navigation.Menu.Sone.Item.New.Name=Nuovi messaggi e Risposte +Navigation.Menu.Sone.Item.New.Tooltip=Visualizza nuovi messaggi e risposte +Navigation.Menu.Sone.Item.CreateSone.Name=Crea "Sone" +Navigation.Menu.Sone.Item.CreateSone.Tooltip=Crea un nuovo "Sone" +Navigation.Menu.Sone.Item.KnownSones.Name="Sone" conosciuti +Navigation.Menu.Sone.Item.KnownSones.Tooltip=Visualizza tutti i "Sone" conosciuti +Navigation.Menu.Sone.Item.Bookmarks.Name=Preferiti +Navigation.Menu.Sone.Item.Bookmarks.Tooltip=Visualizza messaggi preferiti +Navigation.Menu.Sone.Item.EditProfile.Name=Modifica Profilo +Navigation.Menu.Sone.Item.EditProfile.Tooltip=Modifica il profilo del tuo "Sone" +Navigation.Menu.Sone.Item.ImageBrowser.Name=Immagini +Navigation.Menu.Sone.Item.ImageBrowser.Tooltip=Gestisci le tue immagini +Navigation.Menu.Sone.Item.DeleteSone.Name=Cancella "Sone" +Navigation.Menu.Sone.Item.DeleteSone.Tooltip=Cancella il "Sone" corrente +Navigation.Menu.Sone.Item.Logout.Name=Esci +Navigation.Menu.Sone.Item.Logout.Tooltip=Disconnettiti dal "Sone" corrente +Navigation.Menu.Sone.Item.Options.Name=Opzioni +Navigation.Menu.Sone.Item.Options.Tooltip=Opzioni per il plugin "Sone" +Navigation.Menu.Sone.Item.Rescue.Name=Recupera +Navigation.Menu.Sone.Item.Rescue.Tooltip=Recupera "Sone" +Navigation.Menu.Sone.Item.About.Name=Informazioni +Navigation.Menu.Sone.Item.About.Tooltip=Informazioni su "Sone" +Navigation.Menu.Sone.Item.Metrics.Name=Metriche +Navigation.Menu.Sone.Item.Metrics.Tooltip=Metriche raccolte da "Sone" + +Page.About.Title=Informazioni su Sone +Page.About.Page.Title=Informazioni +Page.About.Flattr.Description=Se ti piace Sone e vuoi ringraziarmi con un premio, puoi usare il pulsante Flattr alla fine di ogni pagina. Flattr è un sistema di micro-pagamenti non anonimo che funziona come un barattolo delle mance e la quantità che ogni utente spende è limitata (minimo 2 € al mese). Maggiori informazioni possono essere trovate su {link}flattr.com{/link}. +Page.About.Homepage.Title=Homepage +Page.About.Homepage.Description=Puoi trovare maggiori informazioni e il codice sorgente di Sone sulla {link}homepage{/link}. +Page.About.License.Title=Licenza + +Page.Options.Title=Opzioni - "Sone" +Page.Options.Page.Title=Opzioni +Page.Options.Page.Description=Queste opzioni influenzano il comportamento del plugin "Sone" in esecuzione +Page.Options.Section.SoneSpecificOptions.Title=Opzioni specifiche di "Sone" +Page.Options.Section.SoneSpecificOptions.NotLoggedIn=Queste opzioni sono disponibili solo se sei {link}collegato{/link} +Page.Options.Section.SoneSpecificOptions.LoggedIn=Queste opzioni sono valide solo se ti sei identificato e sono valide solo per l'account con il quale ti sei identificato. +Page.Options.Option.AutoFollow.Description=Se un novo Sone viene trovato seguilo automaticamente. Da notare che verranno seguiti solo i Sone scoperti dopo l'attivazione di questa opzione! +Page.Options.Option.EnableSoneInsertNotifications.Description=Se abilitato, verrà visualizzato una notifica ogni volta che il tuo Sone sarà in fase di caricamento o sarà terminato il caricamento. +Page.Options.Option.ShowNotificationNewSones.Description=Visualizza notifiche per i nuovi "Sone". +Page.Options.Option.ShowNotificationNewPosts.Description=Visualizza notifiche per i nuovi messaggi. +Page.Options.Option.ShowNotificationNewReplies.Description=Visualizza notifiche per le nuove risposte. +Page.Options.Section.AvatarOptions.Title=Opzioni Avatar +Page.Options.Option.ShowAvatars.Description=Qui puoi disabilitare gli avatar customizzati, in base alla selezione fatta. Se un avatar è disabilitato verrà visualizzato l'avatar autogenerato. +Page.Options.Option.ShowAvatars.Never.Description=Non visualizzare mai gli avatar personalizzati. +Page.Options.Option.ShowAvatars.Followed.Description=Visualizza gli avatar solo per i "Sone" che segui. +Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Visualizza solo gli avatar per i Sone a cui hai assegnato un valore di trust maggiore di 0. +Page.Options.Option.ShowAvatars.Trusted.Description=Visualizza gli avatar solo per i "Sone" che hanno un livello di fiducia maggiore di 0. +Page.Options.Option.ShowAvatars.Always.Description=Visualizza sempre gli avatar personalizzati. Stai attento: alcuni avatar possono contenere immagini fastidiose od offensive. +Page.Options.Section.LoadLinkedImagesOptions.Title=Carica le immagini collegate +Page.Options.Option.LoadLinkedImages.Description="Sone" può cercare di caricare in automatico le immagini linkate nei messaggi e nelle risposte. Il caricamento avverrà sempre da Freenet, mai da internet! +Page.Options.Option.LoadLinkedImages.Never.Description=Non caricare mai le immagini linkate. +Page.Options.Option.LoadLinkedImages.Followed.Description=Carica le immagini linkate solo per i "Sone" che segui. +Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Visualizza solo le immagini linkate da Sone a cui hai assegnto un valore di trust maggiore di 0. +Page.Options.Option.LoadLinkedImages.Trusted.Description=Visualizza le immagini solo da Sone che hanno un livello di trust maggiore di 0. +Page.Options.Option.LoadLinkedImages.Always.Description=Carica sempre le immaigni. Attenzione: alcune immagini potrebbero essere offensive. +Page.Options.Section.RuntimeOptions.Title=Comportamento durante l'esecuzione +Page.Options.Option.InsertionDelay.Description=Il numero di secondi che il processo di caricamento del Sone aspetta, dopo una modifica, prima di iniziare il caricamento. +Page.Options.Option.PostsPerPage.Description=Il numero di messaggi da visualizzare su una pagina prima di visualizzare i controlli di paginazione. +Page.Options.Option.ImagesPerPage.Description=Il numero di immagini da visualizzare su una pagina prima di visualizzare i controlli di paginazione. +Page.Options.Option.CharactersPerPost.Description=The number of characters to display from a post before cutting it off and showing a link to expand it (-1 to disable). The actual length of the snippet is determined by the option below. +Page.Options.Option.PostCutOffLength.Description=The number of characters that are displayed if a post is deemed too long (see option above). Ignored if “number of characters to display” is disabled (set to -1). +Page.Options.Option.RequireFullAccess.Description=Whether to deny access to Sone to any host that has not been granted full access. +Page.Options.Section.FcpOptions.Title=Settaggi interfaccia FCP +Page.Options.Option.FcpInterfaceActive.Description=Activate the FCP interface to allow other plugins and remote clients to access your Sone plugin. +Page.Options.Option.FcpFullAccessRequired.Description=Require FCP connection from allowed hosts (see your {link}node’s configuration, section “FCP”{/link}) +Page.Options.Option.FcpFullAccessRequired.Value.No=No +Page.Options.Option.FcpFullAccessRequired.Value.Writing=Per l'accesso in scrittura +Page.Options.Option.FcpFullAccessRequired.Value.Always=Sempre +Page.Options.Section.WebOfTrustOptions.Title=Web of Trust Settings +Page.Options.Option.StrictFiltering.Description=Apply stricter filtering of identities. When this is selected, Sone will completely ignore identities that have a negative trust value for any of your local identities, otherwise they will be shown as long as at least one of your local identities has a positive trust value for them. (Note that it will take a couple of minutes for this setting to show effect.) +Page.Options.Section.Cleaning.Title=Pulisci +Page.Options.Option.ClearOnNextRestart.Description=Resets the configuration of the Sone plugin at the next restart. Warning! {strong}This will destroy all of your Sones{/strong} so make sure you have backed up everyhing you still need! Also, you need to set the next option to true to actually do it. +Page.Options.Option.ReallyClearOnNextRestart.Description=This option needs to be set to “yes” if you really, {strong}really{/strong} want to clear the plugin configuration on the next restart. +Page.Options.Warnings.ValueNotChanged=This option was not changed because the value you specified was not valid. +Page.Options.Button.Save=Salva + +Page.Login.Title=Login - Sone +Page.Login.Page.Title=Accedi +Page.Login.Label.SelectSone=Seleziona Sone: +Page.Login.Option.NoSone=Seleziona Sone... + +Page.Login.CreateSone.Title=Crea Sone + +Page.CreateSone.Title=Crea Sone - Sone + +Page.DeleteSone.Title=Cancella Sone - Sone +Page.DeleteSone.Page.Title=Cancellare il Sone "{sone}"? +Page.DeleteSone.Page.Description=This will not delete the Sone from Freenet (because that is impossible), it will merely disconnect your web of trust identity from Sone. +Page.DeleteSone.Button.Yes=Si, cancella. +Page.DeleteSone.Button.No=No, non cancellare. + +Page.Index.Title=Il tuo Sone - Sone +Page.Index.Label.Text=Testo del messaggio: +Page.Index.Label.Sender=Mittente: +Page.Index.Button.Post=Pubblica! +Page.Index.PostList.Title=Feed dei messaggi +Page.Index.PostList.Text.NoPostYet=Nessuno ha anocara scritto un messaggio ancora. Dovresti iniziare adesso! +Page.Index.PostList.Text.FollowSomeSones=Or maybe you are not following any Sones? Take a look at the list of {link}known Sones{/link} and follow whoever looks interesting! +Page.Index.PostList.Text.AutoFollowOption=You also have the option of automatically following newly discovered Sones. Take a look at the {link}options{/link} to activate the auto-follow feature! + +Page.New.Title=Nuovi messaggi e risposte - Sone +Page.New.Page.Title=Nuovi messaggi e risposte +Page.New.NothingNew=Al momento non c'è nulla di nuovo. + +Page.KnownSones.Title=Sone conosciuti - Sone +Page.KnownSones.Page.Title=Sone conosciuti +Page.KnownSones.Text.NoKnownSones=Al momento non ci sono Sone conosciuti che corrispondono al filtro. +Page.KnownSones.Label.Sort=Ordina: +Page.KnownSones.Label.FilterSones=Filtra i Sone: +Page.KnownSones.Sort.Field.Name=Nome +Page.KnownSones.Sort.Field.LastActivity=Ultima attività +Page.KnownSones.Sort.Field.Posts=Numero di messaggi +Page.KnownSones.Sort.Field.Images=Numero di immagini +Page.KnownSones.Sort.Order.Ascending=Ascendente +Page.KnownSones.Sort.Order.Descending=Discendente +Page.KnownSones.Filter.Followed=Visualizza solo i Sone seguiti +Page.KnownSones.Filter.NotFollowed=Nascondi i Sone seguiti +Page.KnownSones.Filter.New=Visualizza solo i nuovi Sone +Page.KnownSones.Filter.NotNew=Nascondi i nuovi Sone +Page.KnownSones.Filter.Own=Visualizza solo i Sone locali +Page.KnownSones.Filter.NotOwn=Visualizza solo i Sone remoti +Page.KnownSones.Button.Apply=Applica +Page.KnownSones.Button.FollowAllSones=Segui tutti i Sone su questa pagina +Page.KnownSones.Button.UnfollowAllSones=Smetti di seguire tutti i Sone su questa pagina + +Page.EditProfile.Title=Modifica profilo - Sone +Page.EditProfile.Page.Title=Modifica profilo +Page.EditProfile.Page.Description=Su questa pagina puoi inserire i dati del tuo profilo +Page.EditProfile.Page.Hint.Optionality=And remember, every single field of this profile is optional! You are not required to enter a single thing here! Also, everything you enter here will probably be stored in Freenet for a very long time! +Page.EditProfile.Label.FirstName=Nome: +Page.EditProfile.Label.MiddleName=Secondo nome(i): +Page.EditProfile.Label.LastName=Cognome: +Page.EditProfile.Birthday.Title=Compleanno: +Page.EditProfile.Birthday.Label.Day=Giorno: +Page.EditProfile.Birthday.Label.Month=Mese: +Page.EditProfile.Birthday.Label.Year=Anno: +Page.EditProfile.Avatar.Title=Avatar +Page.EditProfile.Avatar.Description=You can select one of your uploaded images to be shown as avatar. It should not be larger than 64×64 pixels because that is the largest size shown for other people (80×80 pixels is used for the page header). +Page.EditProfile.Avatar.Delete=Nessun avatar +Page.EditProfile.Fields.Title=Campi personalizzati +Page.EditProfile.Fields.Description=Here you can enter custom fields into your profile. These fields can contain anything you want and be as terse or as verbose as you wish. Just remember that when it comes to anonymity, sometimes less is more. +Page.EditProfile.Fields.Button.Edit=modifica +Page.EditProfile.Fields.Button.MoveUp=sposta sù +Page.EditProfile.Fields.Button.MoveDown=sposta giù +Page.EditProfile.Fields.Button.Delete=cancella +Page.EditProfile.Fields.Button.ReallyDelete=cancella davvero +Page.EditProfile.Fields.AddField.Title=Aggiungi campo +Page.EditProfile.Fields.AddField.Label.Name=Nome: +Page.EditProfile.Fields.AddField.Button.AddField=Aggiungi campo +Page.EditProfile.Button.Save=Salva profilo +Page.EditProfile.Error.DuplicateFieldName=Il nome “{fieldName}” esiste già. + +Page.EditProfileField.Title=Modifica campo del profilo - Sone +Page.EditProfileField.Page.Title=Modifica campo del profilo +Page.EditProfileField.Text=Inserisci un nome per questo campo del profilo. +Page.EditProfileField.Error.DuplicateFieldName=Il nome del campo inserito è già in uso. +Page.EditProfileField.Button.Save=Cambia +Page.EditProfileField.Button.Reset=Rimetti il vecchio nome +Page.EditProfileField.Button.Cancel=Non cambiare nome + +Page.DeleteProfileField.Title=Cancella il campo del profilo - Sone +Page.DeleteProfileField.Page.Title=Cancella il campo del profilo +Page.DeleteProfileField.Text=Vuoi davvero cancellare questo campo del profilo? +Page.DeleteProfileField.Button.Yes=Si, cancella +Page.DeleteProfileField.Button.No=No, non cancellare + +Page.CreatePost.Title=Scrivi un messaggio - Sone +Page.CreatePost.Page.Title=Scrivi un messaggio +Page.CreatePost.Label.Text=Testo del messaggio: +Page.CreatePost.Button.Post=Pubblica! +Page.CreatePost.Error.EmptyText=Non hai inserito alcun messaggio, che è un peccato. Dovresti provare a scrivere di più! + +Page.CreateReply.Title=Rispondi - Sone +Page.CreateReply.Page.Title=Riscpondi +Page.CreateReply.Error.EmptyText=Non hai inserito alcun messaggio, che è un peccato. Dovresti provare a scrivere di più! +Page.CreateReply.Label.Text=Testo della risposta: +Page.CreateReply.Button.Post=Pubblica risposta! + +Page.ViewSone.Title=Visualizza Sone - Sone +Page.ViewSone.Page.TitleWithoutSone=Visualizza Sone sconosciuto +Page.ViewSone.NoSone.Description=There is currently no known Sone with the ID {sone}. If you were looking for a specific Sone, make sure that it is visible in your web of trust: +Page.ViewSone.UnknownSone.Description=This Sone has not yet been retrieved. Please check back in a short time. +Page.ViewSone.UnknownSone.LinkToWebOfTrust=Even though the Sone is still unknown, its Web of Trust profile might already be available: +Page.ViewSone.WriteAMessage=You can write a message to this Sone here. Please note that everybody will be able to read this message! +Page.ViewSone.PostList.Title=Posts by {sone} +Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything. +Page.ViewSone.Profile.Title=Profilo +Page.ViewSone.Profile.Label.Name=Nome +Page.ViewSone.Profile.Label.Albums=Album +Page.ViewSone.Profile.Albums.Text.All=Tutti gli album +Page.ViewSone.Profile.Name.WoTLink=web of trust profile +Page.ViewSone.Replies.Title=Messaggi a cui {sone} ha risposto + +Page.ViewPost.Title=Visualizza messaggio - Sone +Page.ViewPost.Page.Title=Visualizza messaggi di {sone} +Page.ViewPost.Page.TitleUnknownSone=Visualizza messaggio sconosciuto +Page.ViewPost.Text.UnknownPost=This post has not yet been downloaded. + +Page.Like.Title=Like Post - Sone +Page.Unlike.Title=Unlike Post - Sone + +Page.DeletePost.Title=Cancella Sone - Sone +Page.DeletePost.Page.Title=Cancella Sone +Page.DeletePost.Text.PostWillBeGone=Deleting a post will remove it from your Sone. It will not remove it from Freenet because that is not possible. Older versions of your Sone will always include the deleted post. +Page.DeletePost.Button.Yes=Si, cancella. +Page.DeletePost.Button.No=No, non cancellare. + +Page.DeleteReply.Title=Cancella risposta - Sone +Page.DeleteReply.Page.Title=Cancella risposta +Page.DeleteReply.Text.PostWillBeGone=Deleting a reply will remove it from your Sone. It will not remove it from Freenet because that is not possible. Older versions of your Sone will always include the deleted reply. +Page.DeleteReply.Button.Yes=Si, cancella. +Page.DeleteReply.Button.No=No, do not delete. + +Page.LockSone.Title=Lock Sone - Sone + +Page.UnlockSone.Title=Unlock Sone - Sone + +Page.FollowSone.Title=Follow Sone - Sone + +Page.UnfollowSone.Title=Unfollow Sone - Sone + +Page.ImageBrowser.Title=Image Browser - Sone +Page.ImageBrowser.Album.Title=Album “{album}” +Page.ImageBrowser.Album.Error.NotFound.Text=The requested album could not be found. It is possible that it has not yet been downloaded, or that it has been deleted. +Page.ImageBrowser.Sone.Title=Albums of {sone} +Page.ImageBrowser.Sone.Error.NotFound.Text=The requested Sone could not be found. It is possible that it has not yet been downloaded. +Page.ImageBrowser.Header.Albums=Albums +Page.ImageBrowser.Header.Images=Images +Page.ImageBrowser.Link.All=All Sones +Page.ImageBrowser.CreateAlbum.Button.CreateAlbum=Create Album +Page.ImageBrowser.Album.Edit.Title=Edit Album +Page.ImageBrowser.Album.Delete.Title=Delete Album +Page.ImageBrowser.Album.Label.AlbumImage=Album Image: +Page.ImageBrowser.Album.Label.Title=Title: +Page.ImageBrowser.Album.Label.Description=Description: +Page.ImageBrowser.Album.AlbumImage.Choose=Choose Album Image… +Page.ImageBrowser.Album.Button.Save=Save Album +Page.ImageBrowser.Album.Button.Delete=Delete Album +Page.ImageBrowser.Image.Edit.Title=Edit Image +Page.ImageBrowser.Image.Title.Label=Title: +Page.ImageBrowser.Image.Description.Label=Description: +Page.ImageBrowser.Image.Button.MoveLeft=◀ +Page.ImageBrowser.Image.Button.Save=Save Image +Page.ImageBrowser.Image.Button.MoveRight=► +Page.ImageBrowser.Image.Delete.Title=Delete Image +Page.ImageBrowser.Image.Button.Delete=Delete Image + +Page.CreateAlbum.Title=Create Album - Sone +Page.CreateAlbum.Page.Title=Create Album +Page.CreateAlbum.Error.NameMissing=Sembra che ti sia dimenticato di inserire un nome per il nuovo album. + +Page.UploadImage.Title=Upload Image - Sone +Page.UploadImage.Error.InvalidImage=The image you were trying to upload could not be recognized. Please upload only JPEG (*.jpg or *.jpeg), or PNG (*.png) images. + +Page.EditImage.Title=Edit Image - Sone + +Page.DeleteImage.Title=Delete Image - Sone +Page.DeleteImage.Page.Title=Delete Image +Page.DeleteImage.Text.ImageWillBeGone=This will remove the image “{image}” from your album “{album}”. If it has already been inserted into Freenet it can not be removed from there forcefully. Do you want to delete the image? +Page.DeleteImage.Button.Yes=Si, cancella immagine. +Page.DeleteImage.Button.No=No, don’t delete image. + +Page.EditAlbum.Title=Edit Album - Sone + +Page.DeleteAlbum.Title=Delete Album - Sone +Page.DeleteAlbum.Page.Title=Delete Album +Page.DeleteAlbum.Text.AlbumWillBeGone=This will remove your album “{title}”. Do you really want to do that? +Page.DeleteAlbum.Button.Yes=Si, cancella album. +Page.DeleteAlbum.Button.No=No, don’t delete album. + +Page.MarkAsKnown.Title=Mark as Known - Sone + +Page.Bookmark.Title=Bookmark - Sone +Page.Unbookmark.Title=Remove Bookmark - Sone +Page.Bookmarks.Title=Bookmarks - Sone +Page.Bookmarks.Page.Title=Bookmarks +Page.Bookmarks.Text.NoBookmarks=You don’t have any bookmarks defined right now. You can bookmark posts by clicking the star below the post. +Page.Bookmarks.Text.PostsNotLoaded=Some of your bookmarked posts have not been shown because they could not be loaded. This can happen if you restarted Sone recently or if the originating Sone has deleted the post. If you are reasonable sure that these posts do not exist anymore, you can {link}unbookmark them{/link}. + +Page.Search.Title=Search - Sone +Page.Search.Page.Title=Search Results +Page.Search.Text.SoneHits=The following Sones match your search terms. +Page.Search.Text.PostHits=The following posts match your search terms. +Page.Search.Text.NoHits=No Sones or posts matched your search terms. + +Page.Rescue.Title=Rescue Sone - Sone +Page.Rescue.Page.Title=Rescue Sone “{0}” +Page.Rescue.Text.Description=The Rescue Mode lets you restore previous versions of your Sone. This can be necessary if your configuration was lost. +Page.Rescue.Text.Procedure=The Rescue Mode works by fetching the latest inserted edition of your Sone. If an edition was successfully fetched it will be loaded into your Sone, letting you control your posts, profile, and other settings (you could do that in a second browser tab or window). If the fetched edition is not the one you want to restore, instruct the Rescue Mode to fetch the next older edition below. +Page.Rescue.Text.Fetching=The Sone Rescuer is currently fetching edition {0} of your Sone. +Page.Rescue.Text.Fetched=The Sone Rescuer has downloaded edition {0} of your Sone. Please check your posts, replies, and profile. If you like what the current Sone contains, just unlock it. +Page.Rescue.Text.FetchedLast=The Sone rescuer has downloaded the last available edition. If it did not manage to restore your Sone you are probably out of luck now. +Page.Rescue.Text.NotFetched=The Sone Rescuer could not download edition {0} of your Sone. Please either try again with edition {0}, or try the next older edition. +Page.Rescue.Label.NextEdition=Next edition +Page.Rescue.Button.Fetch=Fetch edition + +Page.NoPermission.Title=Unauthorized Access - Sone +Page.NoPermission.Page.Title=Unauthorized Access +Page.NoPermission.Text.NoPermission=You tried to do something that you do not have sufficient authorization for. Please refrain from such actions in the future or we will be forced to take counter-measures! + +Page.EmptyImageTitle.Title=Title Must Not Be Empty - Sone +Page.EmptyImageTitle.Page.Title=Title Must Not Be Empty +Page.EmptyImageTitle.Text.EmptyImageTitle=You have to give your image a title. Please go back to the previous page and enter a title. + +Page.EmptyAlbumTitle.Title=Title Must Not Be Empty - Sone +Page.EmptyAlbumTitle.Page.Title=Title Must Not Be Empty +Page.EmptyAlbumTitle.Text.EmptyAlbumTitle=You have to give your album a title. Please go back to the previous page and enter a title. + +Page.DismissNotification.Title=Dismiss Notification - Sone + +Page.WotPluginMissing.Text.WotRequired=Because the Web of Trust is an integral part of Sone, the Web of Trust plugin has to be loaded in order to run Sone. +Page.WotPluginMissing.Text.LoadPlugin=Please load the Web of Trust plugin in the {link}plugin manager{/link}. + +Page.Logout.Title=Logout - Sone + +Page.Invalid.Title=Invalid Action Performed - Sone +Page.Invalid.Page.Title=Invalid Action Performed +Page.Invalid.Text=An invalid action was performed, or the action was valid but the parameters were not. Please go back to the {link}index page{/link} and try again. If the error persists you have probably found a bug. + +Page.Metrics.Title=Metrics +Page.Metrics.Page.Title=Metrics +Page.Metrics.SoneInsertDuration.Title=Sone Insert Duration +Page.Metrics.SoneParseDuration.Title=Sone Parse Duration +Page.Metrics.ConfigurationSaveDuration.Title=Configuration Save Duration + +View.Search.Button.Search=Search + +View.CreateSone.Text.WotIdentityRequired=To create a Sone you need an identity from the {link}Web of Trust plugin{/link}. +View.CreateSone.Select.Default=Select an identity +View.CreateSone.Text.NoIdentities=You do not have any Web of Trust identities. Please head over to the {link}Web of Trust plugin{/link} and create an identity. +View.CreateSone.Text.NoNonSoneIdentities=You do not have any Web of Trust identities that are not already a Sone. Use one of the remaining Web of Trust identities to create a new Sone or head over to the {link}Web of Trust plugin{/link} to create a new identity. +View.CreateSone.Button.Create=Create Sone +View.CreateSone.Text.Error.NoIdentity=You have not selected an identity. + +View.Sone.Label.LastUpdate=Last update: +View.Sone.Text.UnknownDate=unknown +View.Sone.Stats.Posts={0,number} {0,choice,0#posts|1#post|1