X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsone%2Fcore%2FCore.java;h=108b05c712b40f0ec033c40dadf6028a7d232f18;hp=a59c96c364c3446eb95c08a890521a9cdc16778f;hb=c03a4ced3dd1562f4387edb449680a6fd8b9a746;hpb=b4b45833977634634093407da37eb508000aeb56 diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index a59c96c..108b05c 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -38,11 +38,14 @@ import net.pterodactylus.sone.data.Album; import net.pterodactylus.sone.data.Client; import net.pterodactylus.sone.data.Image; 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.TemporaryImage; import net.pterodactylus.sone.data.Profile.Field; +import net.pterodactylus.sone.data.Sone.ShowCustomAvatars; +import net.pterodactylus.sone.data.Sone.SoneStatus; import net.pterodactylus.sone.fcp.FcpInterface; import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired; import net.pterodactylus.sone.freenet.wot.Identity; @@ -72,26 +75,6 @@ import freenet.keys.FreenetURI; */ public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener, ImageInsertListener { - /** - * Enumeration for the possible states of a {@link Sone}. - * - * @author David ‘Bombe’ Roden - */ - public enum SoneStatus { - - /** The Sone is unknown, i.e. not yet downloaded. */ - unknown, - - /** The Sone is idle, i.e. not being downloaded or inserted. */ - idle, - - /** The Sone is currently being inserted. */ - inserting, - - /** The Sone is currently being downloaded. */ - downloading, - } - /** The logger. */ private static final Logger logger = Logging.getLogger(Core.class); @@ -131,9 +114,8 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis /** The FCP interface. */ private volatile FcpInterface fcpInterface; - /** The Sones’ statuses. */ - /* synchronize access on itself. */ - private final Map soneStatuses = new HashMap(); + /** The times Sones were followed. */ + private final Map soneFollowingTimes = new HashMap(); /** Locked local Sones. */ /* synchronize on itself. */ @@ -155,28 +137,17 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis /* synchronize access on this on itself. */ private Map remoteSones = new HashMap(); - /** All new Sones. */ - private Set newSones = new HashSet(); - /** All known Sones. */ - /* synchronize access on {@link #newSones}. */ private Set knownSones = new HashSet(); /** All posts. */ private Map posts = new HashMap(); - /** All new posts. */ - private Set newPosts = new HashSet(); - /** All known posts. */ - /* synchronize access on {@link #newPosts}. */ private Set knownPosts = new HashSet(); /** All replies. */ - private Map replies = new HashMap(); - - /** All new replies. */ - private Set newReplies = new HashSet(); + private Map replies = new HashMap(); /** All known replies. */ private Set knownReplies = new HashSet(); @@ -301,33 +272,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } /** - * Returns the status of the given Sone. - * - * @param sone - * The Sone to get the status for - * @return The status of the Sone - */ - public SoneStatus getSoneStatus(Sone sone) { - synchronized (soneStatuses) { - return soneStatuses.get(sone); - } - } - - /** - * Sets the status of the given Sone. - * - * @param sone - * The Sone to set the status of - * @param soneStatus - * The status to set - */ - public void setSoneStatus(Sone sone, SoneStatus soneStatus) { - synchronized (soneStatuses) { - soneStatuses.put(sone, soneStatus); - } - } - - /** * Returns the Sone rescuer for the given local Sone. * * @param sone @@ -483,7 +427,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis if ((sone == null) && create) { sone = new Sone(id); localSones.put(id, sone); - setSoneStatus(sone, SoneStatus.unknown); } return sone; } @@ -505,17 +448,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * * @param id * The ID of the remote Sone to get - * @return The Sone with the given ID - */ - public Sone getRemoteSone(String id) { - return getRemoteSone(id, true); - } - - /** - * Returns the remote Sone with the given ID. - * - * @param id - * The ID of the remote Sone to get * @param create * {@code true} to always create a Sone, {@code false} to return * {@code null} if no Sone with the given ID exists @@ -524,10 +456,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis public Sone getRemoteSone(String id, boolean create) { synchronized (remoteSones) { Sone sone = remoteSones.get(id); - if ((sone == null) && create) { + if ((sone == null) && create && (id != null) && (id.length() == 43)) { sone = new Sone(id); remoteSones.put(id, sone); - setSoneStatus(sone, SoneStatus.unknown); } return sone; } @@ -562,19 +493,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } /** - * Returns whether the Sone with the given ID is a new Sone. - * - * @param soneId - * The ID of the sone to check for - * @return {@code true} if the given Sone is new, false otherwise - */ - public boolean isNewSone(String soneId) { - synchronized (newSones) { - return !knownSones.contains(soneId) && newSones.contains(soneId); - } - } - - /** * Returns whether the given Sone has been modified. * * @param sone @@ -587,6 +505,23 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } /** + * Returns the time when the given was first followed by any local Sone. + * + * @param sone + * The Sone to get the time for + * @return The time (in milliseconds since Jan 1, 1970) the Sone has first + * been followed, or {@link Long#MAX_VALUE} + */ + public long getSoneFollowingTime(Sone sone) { + synchronized (soneFollowingTimes) { + if (soneFollowingTimes.containsKey(sone)) { + return soneFollowingTimes.get(sone); + } + return Long.MAX_VALUE; + } + } + + /** * Returns whether the target Sone is trusted by the origin Sone. * * @param origin @@ -634,20 +569,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } /** - * Returns whether the given post ID is new. - * - * @param postId - * The post ID - * @return {@code true} if the post is considered to be new, {@code false} - * otherwise - */ - public boolean isNewPost(String postId) { - synchronized (newPosts) { - return !knownPosts.contains(postId) && newPosts.contains(postId); - } - } - - /** * Returns all posts that have the given Sone as recipient. * * @see Post#getRecipient() @@ -676,7 +597,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * The ID of the reply to get * @return The reply */ - public Reply getReply(String replyId) { + public PostReply getReply(String replyId) { return getReply(replyId, true); } @@ -692,11 +613,11 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * to return {@code null} if no reply can be found * @return The reply, or {@code null} if there is no such reply */ - public Reply getReply(String replyId, boolean create) { + public PostReply getReply(String replyId, boolean create) { synchronized (replies) { - Reply reply = replies.get(replyId); + PostReply reply = replies.get(replyId); if (create && (reply == null)) { - reply = new Reply(replyId); + reply = new PostReply(replyId); replies.put(replyId, reply); } return reply; @@ -710,11 +631,11 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * The post to get all replies for * @return All replies for the given post */ - public List getReplies(Post post) { + public List getReplies(Post post) { Set sones = getSones(); - List replies = new ArrayList(); + List replies = new ArrayList(); for (Sone sone : sones) { - for (Reply reply : sone.getReplies()) { + for (PostReply reply : sone.getReplies()) { if (reply.getPost().equals(post)) { replies.add(reply); } @@ -725,20 +646,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } /** - * Returns whether the reply with the given ID is new. - * - * @param replyId - * The ID of the reply to check - * @return {@code true} if the reply is considered to be new, {@code false} - * otherwise - */ - public boolean isNewReply(String replyId) { - synchronized (newReplies) { - return !knownReplies.contains(replyId) && newReplies.contains(replyId); - } - } - - /** * Returns all Sones that have liked the given post. * * @param post @@ -762,7 +669,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * The reply to get the liking Sones for * @return The Sones that like the given reply */ - public Set getLikes(Reply reply) { + public Set getLikes(PostReply reply) { Set sones = new HashSet(); for (Sone sone : getSones()) { if (sone.getLikedReplyIds().contains(reply.getId())) { @@ -935,29 +842,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } /** - * Adds a local Sone from the given ID which has to be the ID of an own - * identity. - * - * @param id - * The ID of an own identity to add a Sone for - * @return The added (or already existing) Sone - */ - public Sone addLocalSone(String id) { - synchronized (localSones) { - if (localSones.containsKey(id)) { - logger.log(Level.FINE, "Tried to add known local Sone: %s", id); - return localSones.get(id); - } - OwnIdentity ownIdentity = identityManager.getOwnIdentity(id); - if (ownIdentity == null) { - logger.log(Level.INFO, "Invalid Sone ID: %s", id); - return null; - } - return addLocalSone(ownIdentity); - } - } - - /** * Adds a local Sone from the given own identity. * * @param ownIdentity @@ -979,12 +863,13 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0)); sone.setClient(new Client("Sone", SonePlugin.VERSION.toString())); + sone.setKnown(true); /* TODO - load posts ’n stuff */ localSones.put(ownIdentity.getId(), sone); final SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone); soneInserter.addSoneInsertListener(this); soneInserters.put(sone, soneInserter); - setSoneStatus(sone, SoneStatus.idle); + sone.setStatus(SoneStatus.idle); loadSone(sone); soneInserter.start(); return sone; @@ -1007,7 +892,13 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } Sone sone = addLocalSone(ownIdentity); sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption(false)); - sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"); + sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption(false)); + sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption(true)); + sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption(true)); + sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption(true)); + sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption(ShowCustomAvatars.NEVER)); + + followSone(sone, getSone("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI")); touchConfiguration(); return sone; } @@ -1025,30 +916,25 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis return null; } synchronized (remoteSones) { - final Sone sone = getRemoteSone(identity.getId()).setIdentity(identity); + final Sone sone = getRemoteSone(identity.getId(), true).setIdentity(identity); boolean newSone = sone.getRequestUri() == null; sone.setRequestUri(getSoneUri(identity.getRequestUri())); sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0)); if (newSone) { - synchronized (newSones) { + synchronized (knownSones) { newSone = !knownSones.contains(sone.getId()); - if (newSone) { - newSones.add(sone.getId()); - } } + sone.setKnown(!newSone); if (newSone) { coreListenerManager.fireNewSoneFound(sone); for (Sone localSone : getLocalSones()) { if (localSone.getOptions().getBooleanOption("AutoFollow").get()) { - localSone.addFriend(sone.getId()); - touchConfiguration(); + followSone(localSone, sone); } } } } - remoteSones.put(identity.getId(), sone); soneDownloader.addSone(sone); - setSoneStatus(sone, SoneStatus.unknown); soneDownloaders.execute(new Runnable() { @Override @@ -1063,6 +949,95 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } /** + * Lets the given local Sone follow the Sone with the given ID. + * + * @param sone + * The local Sone that should follow another Sone + * @param soneId + * The ID of the Sone to follow + */ + public void followSone(Sone sone, String soneId) { + Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check(); + Sone followedSone = getSone(soneId, true); + if (followedSone == null) { + logger.log(Level.INFO, String.format("Ignored Sone with invalid ID: %s", soneId)); + return; + } + followSone(sone, getSone(soneId)); + } + + /** + * Lets the given local Sone follow the other given Sone. If the given Sone + * was not followed by any local Sone before, this will mark all elements of + * the followed Sone as read that have been created before the current + * moment. + * + * @param sone + * The local Sone that should follow the other Sone + * @param followedSone + * The Sone that should be followed + */ + public void followSone(Sone sone, Sone followedSone) { + Validation.begin().isNotNull("Sone", sone).isNotNull("Followed Sone", followedSone).check(); + sone.addFriend(followedSone.getId()); + synchronized (soneFollowingTimes) { + if (!soneFollowingTimes.containsKey(followedSone)) { + long now = System.currentTimeMillis(); + soneFollowingTimes.put(followedSone, now); + for (Post post : followedSone.getPosts()) { + if (post.getTime() < now) { + markPostKnown(post); + } + } + for (PostReply reply : followedSone.getReplies()) { + if (reply.getTime() < now) { + markReplyKnown(reply); + } + } + } + } + touchConfiguration(); + } + + /** + * Lets the given local Sone unfollow the Sone with the given ID. + * + * @param sone + * The local Sone that should unfollow another Sone + * @param soneId + * The ID of the Sone being unfollowed + */ + public void unfollowSone(Sone sone, String soneId) { + Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check(); + unfollowSone(sone, getSone(soneId, false)); + } + + /** + * Lets the given local Sone unfollow the other given Sone. If the given + * local Sone is the last local Sone that followed the given Sone, its + * following time will be removed. + * + * @param sone + * The local Sone that should unfollow another Sone + * @param unfollowedSone + * The Sone being unfollowed + */ + public void unfollowSone(Sone sone, Sone unfollowedSone) { + Validation.begin().isNotNull("Sone", sone).isNotNull("Unfollowed Sone", unfollowedSone).check(); + sone.removeFriend(unfollowedSone.getId()); + boolean unfollowedSoneStillFollowed = false; + for (Sone localSone : getLocalSones()) { + unfollowedSoneStillFollowed |= localSone.hasFriend(unfollowedSone.getId()); + } + if (!unfollowedSoneStillFollowed) { + synchronized (soneFollowingTimes) { + soneFollowingTimes.remove(unfollowedSone); + } + } + touchConfiguration(); + } + + /** * Retrieves the trust relationship from the origin to the target. If the * trust relationship can not be retrieved, {@code null} is returned. * @@ -1191,12 +1166,16 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } } List storedPosts = storedSone.getPosts(); - synchronized (newPosts) { + synchronized (knownPosts) { for (Post post : sone.getPosts()) { - post.setSone(storedSone); - if (!storedPosts.contains(post) && !knownPosts.contains(post.getId())) { - newPosts.add(post.getId()); - coreListenerManager.fireNewPostFound(post); + post.setSone(storedSone).setKnown(knownPosts.contains(post.getId())); + if (!storedPosts.contains(post)) { + if (post.getTime() < getSoneFollowingTime(sone)) { + knownPosts.add(post.getId()); + } else if (!knownPosts.contains(post.getId())) { + sone.setKnown(false); + coreListenerManager.fireNewPostFound(post); + } } posts.put(post.getId(), post); } @@ -1204,25 +1183,45 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } synchronized (replies) { if (!soneRescueMode) { - for (Reply reply : storedSone.getReplies()) { + for (PostReply reply : storedSone.getReplies()) { replies.remove(reply.getId()); if (!sone.getReplies().contains(reply)) { coreListenerManager.fireReplyRemoved(reply); } } } - Set storedReplies = storedSone.getReplies(); - synchronized (newReplies) { - for (Reply reply : sone.getReplies()) { - reply.setSone(storedSone); - if (!storedReplies.contains(reply) && !knownReplies.contains(reply.getId())) { - newReplies.add(reply.getId()); - coreListenerManager.fireNewReplyFound(reply); + Set storedReplies = storedSone.getReplies(); + synchronized (knownReplies) { + for (PostReply reply : sone.getReplies()) { + reply.setSone(storedSone).setKnown(knownReplies.contains(reply.getId())); + if (!storedReplies.contains(reply)) { + if (reply.getTime() < getSoneFollowingTime(sone)) { + knownReplies.add(reply.getId()); + } else if (!knownReplies.contains(reply.getId())) { + reply.setKnown(false); + coreListenerManager.fireNewReplyFound(reply); + } } replies.put(reply.getId(), reply); } } } + synchronized (albums) { + synchronized (images) { + for (Album album : storedSone.getAlbums()) { + albums.remove(album.getId()); + for (Image image : album.getImages()) { + images.remove(image.getId()); + } + } + for (Album album : sone.getAlbums()) { + albums.put(album.getId(), album); + for (Image image : album.getImages()) { + images.put(image.getId(), image); + } + } + } + } synchronized (storedSone) { if (!soneRescueMode || (sone.getTime() > storedSone.getTime())) { storedSone.setTime(sone.getTime()); @@ -1233,7 +1232,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis for (Post post : sone.getPosts()) { storedSone.addPost(post); } - for (Reply reply : sone.getReplies()) { + for (PostReply reply : sone.getReplies()) { storedSone.addReply(reply); } for (String likedPostId : sone.getLikedPostIds()) { @@ -1242,6 +1241,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis for (String likedReplyId : sone.getLikedReplyIds()) { storedSone.addLikedReplyId(likedReplyId); } + for (Album album : sone.getAlbums()) { + storedSone.addAlbum(album); + } } else { storedSone.setPosts(sone.getPosts()); storedSone.setReplies(sone.getReplies()); @@ -1298,12 +1300,13 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * The Sone to mark as known */ public void markSoneKnown(Sone sone) { - synchronized (newSones) { - if (newSones.remove(sone.getId())) { + if (!sone.isKnown()) { + sone.setKnown(true); + synchronized (knownSones) { knownSones.add(sone.getId()); - coreListenerManager.fireMarkSoneKnown(sone); - touchConfiguration(); } + coreListenerManager.fireMarkSoneKnown(sone); + touchConfiguration(); } } @@ -1323,6 +1326,10 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis /* initialize options. */ sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption(false)); sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption(false)); + sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption(true)); + sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption(true)); + sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption(true)); + sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption(ShowCustomAvatars.NEVER)); /* load Sone. */ String sonePrefix = "Sone/" + sone.getId(); @@ -1334,7 +1341,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis String lastInsertFingerprint = configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").getValue(""); /* load profile. */ - Profile profile = new Profile(); + Profile profile = new Profile(sone); profile.setFirstName(configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null)); profile.setMiddleName(configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null)); profile.setLastName(configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null)); @@ -1376,7 +1383,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } /* load replies. */ - Set replies = new HashSet(); + Set replies = new HashSet(); while (true) { String replyPrefix = sonePrefix + "/Replies/" + replies.size(); String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null); @@ -1435,11 +1442,12 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis String albumTitle = configuration.getStringValue(albumPrefix + "/Title").getValue(null); String albumDescription = configuration.getStringValue(albumPrefix + "/Description").getValue(null); String albumParentId = configuration.getStringValue(albumPrefix + "/Parent").getValue(null); + String albumImageId = configuration.getStringValue(albumPrefix + "/AlbumImage").getValue(null); if ((albumTitle == null) || (albumDescription == null)) { logger.log(Level.WARNING, "Invalid album found, aborting load!"); return; } - Album album = getAlbum(albumId).setSone(sone).setTitle(albumTitle).setDescription(albumDescription); + Album album = getAlbum(albumId).setSone(sone).setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId); if (albumParentId != null) { Album parentAlbum = getAlbum(albumParentId, false); if (parentAlbum == null) { @@ -1481,9 +1489,19 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis album.addImage(image); } + /* load avatar. */ + String avatarId = configuration.getStringValue(sonePrefix + "/Profile/Avatar").getValue(null); + if (avatarId != null) { + profile.setAvatar(getImage(avatarId, false)); + } + /* load options. */ sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null)); sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null)); + sone.getOptions().getBooleanOption("ShowNotification/NewSones").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null)); + sone.getOptions().getBooleanOption("ShowNotification/NewPosts").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null)); + sone.getOptions().getBooleanOption("ShowNotification/NewReplies").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null)); + sone.getOptions(). getEnumOption("ShowCustomAvatars").set(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name()))); /* if we’re still here, Sone was loaded successfully. */ synchronized (sone) { @@ -1493,22 +1511,24 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis sone.setReplies(replies); sone.setLikePostIds(likedPostIds); sone.setLikeReplyIds(likedReplyIds); - sone.setFriends(friends); + for (String friendId : friends) { + followSone(sone, friendId); + } sone.setAlbums(topLevelAlbums); soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint); } - synchronized (newSones) { + synchronized (knownSones) { for (String friend : friends) { knownSones.add(friend); } } - synchronized (newPosts) { + synchronized (knownPosts) { for (Post post : posts) { knownPosts.add(post.getId()); } } - synchronized (newReplies) { - for (Reply reply : replies) { + synchronized (knownReplies) { + for (PostReply reply : replies) { knownReplies.add(reply.getId()); } } @@ -1584,10 +1604,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis synchronized (posts) { posts.put(post.getId(), post); } - synchronized (newPosts) { - newPosts.add(post.getId()); - coreListenerManager.fireNewPostFound(post); - } + coreListenerManager.fireNewPostFound(post); sone.addPost(post); touchConfiguration(); localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() { @@ -1619,10 +1636,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis posts.remove(post.getId()); } coreListenerManager.firePostRemoved(post); - synchronized (newPosts) { - markPostKnown(post); - knownPosts.remove(post.getId()); - } + markPostKnown(post); touchConfiguration(); } @@ -1634,9 +1648,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * The post to mark as known */ public void markPostKnown(Post post) { - synchronized (newPosts) { - if (newPosts.remove(post.getId())) { - knownPosts.add(post.getId()); + post.setKnown(true); + synchronized (knownPosts) { + if (knownPosts.add(post.getId())) { coreListenerManager.fireMarkPostKnown(post); touchConfiguration(); } @@ -1698,7 +1712,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * The text of the reply * @return The created reply */ - public Reply createReply(Sone sone, Post post, String text) { + public PostReply createReply(Sone sone, Post post, String text) { return createReply(sone, post, System.currentTimeMillis(), text); } @@ -1715,17 +1729,16 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * The text of the reply * @return The created reply */ - public Reply createReply(Sone sone, Post post, long time, String text) { + public PostReply createReply(Sone sone, Post post, long time, String text) { if (!isLocalSone(sone)) { logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone); return null; } - final Reply reply = new Reply(sone, post, System.currentTimeMillis(), text); + final PostReply reply = new PostReply(sone, post, System.currentTimeMillis(), text); synchronized (replies) { replies.put(reply.getId(), reply); } - synchronized (newReplies) { - newReplies.add(reply.getId()); + synchronized (knownReplies) { coreListenerManager.fireNewReplyFound(reply); } sone.addReply(reply); @@ -1749,7 +1762,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * @param reply * The reply to delete */ - public void deleteReply(Reply reply) { + public void deleteReply(PostReply reply) { Sone sone = reply.getSone(); if (!isLocalSone(sone)) { logger.log(Level.FINE, "Tried to delete non-local reply: %s", reply); @@ -1758,7 +1771,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis synchronized (replies) { replies.remove(reply.getId()); } - synchronized (newReplies) { + synchronized (knownReplies) { markReplyKnown(reply); knownReplies.remove(reply.getId()); } @@ -1773,10 +1786,10 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * @param reply * The reply to mark as known */ - public void markReplyKnown(Reply reply) { - synchronized (newReplies) { - if (newReplies.remove(reply.getId())) { - knownReplies.add(reply.getId()); + public void markReplyKnown(PostReply reply) { + reply.setKnown(true); + synchronized (knownReplies) { + if (knownReplies.add(reply.getId())) { coreListenerManager.fireMarkReplyKnown(reply); touchConfiguration(); } @@ -2008,8 +2021,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis logger.log(Level.INFO, "Saving Sone: %s", sone); try { - ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition())); - /* save Sone into configuration. */ String sonePrefix = "Sone/" + sone.getId(); configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime()); @@ -2023,6 +2034,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay()); configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth()); configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear()); + configuration.getStringValue(sonePrefix + "/Profile/Avatar").setValue(profile.getAvatar()); /* save profile fields. */ int fieldCounter = 0; @@ -2046,7 +2058,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis /* save replies. */ int replyCounter = 0; - for (Reply reply : sone.getReplies()) { + for (PostReply reply : sone.getReplies()) { String replyPrefix = sonePrefix + "/Replies/" + replyCounter++; configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId()); configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId()); @@ -2077,7 +2089,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null); /* save albums. first, collect in a flat structure, top-level first. */ - List albums = Sone.flattenAlbums(sone.getAlbums()); + List albums = sone.getAllAlbums(); int albumCounter = 0; for (Album album : albums) { @@ -2086,6 +2098,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle()); configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription()); configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent() == null ? null : album.getParent().getId()); + configuration.getStringValue(albumPrefix + "/AlbumImage").setValue(album.getAlbumImage() == null ? null : album.getAlbumImage().getId()); } configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null); @@ -2111,9 +2124,16 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis /* save options. */ configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal()); + configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewSones").getReal()); + configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewPosts").getReal()); + configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewReplies").getReal()); configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal()); + configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions(). getEnumOption("ShowCustomAvatars").get().name()); configuration.save(); + + ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition())); + logger.log(Level.INFO, "Sone %s saved.", sone); } catch (ConfigurationException ce1) { logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1); @@ -2140,6 +2160,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal()); configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal()); configuration.getIntValue("Option/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal()); + configuration.getIntValue("Option/PostCutOffLength").setValue(options.getIntegerOption("PostCutOffLength").getReal()); configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal()); configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal()); configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal()); @@ -2152,16 +2173,27 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis /* save known Sones. */ int soneCounter = 0; - synchronized (newSones) { + synchronized (knownSones) { for (String knownSoneId : knownSones) { configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").setValue(knownSoneId); } configuration.getStringValue("KnownSone/" + soneCounter + "/ID").setValue(null); } + /* save Sone following times. */ + soneCounter = 0; + synchronized (soneFollowingTimes) { + for (Entry soneFollowingTime : soneFollowingTimes.entrySet()) { + configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(soneFollowingTime.getKey().getId()); + configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").setValue(soneFollowingTime.getValue()); + ++soneCounter; + } + configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(null); + } + /* save known posts. */ int postCounter = 0; - synchronized (newPosts) { + synchronized (knownPosts) { for (String knownPostId : knownPosts) { configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId); } @@ -2170,7 +2202,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis /* save known replies. */ int replyCounter = 0; - synchronized (newReplies) { + synchronized (knownReplies) { for (String knownReplyId : knownReplies) { configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId); } @@ -2213,7 +2245,8 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis })); options.addIntegerOption("PostsPerPage", new DefaultOption(10, new IntegerRangeValidator(1, Integer.MAX_VALUE))); - options.addIntegerOption("CharactersPerPost", new DefaultOption(200, new OrValidator(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator(-1)))); + options.addIntegerOption("CharactersPerPost", new DefaultOption(400, new OrValidator(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator(-1)))); + options.addIntegerOption("PostCutOffLength", new DefaultOption(200, new OrValidator(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator(-1)))); options.addBooleanOption("RequireFullAccess", new DefaultOption(false)); options.addIntegerOption("PositiveTrust", new DefaultOption(75, new IntegerRangeValidator(0, 100))); options.addIntegerOption("NegativeTrust", new DefaultOption(-25, new IntegerRangeValidator(-100, 100))); @@ -2253,6 +2286,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis loadConfigurationValue("InsertionDelay"); loadConfigurationValue("PostsPerPage"); loadConfigurationValue("CharactersPerPost"); + loadConfigurationValue("PostCutOffLength"); options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null)); loadConfigurationValue("PositiveTrust"); loadConfigurationValue("NegativeTrust"); @@ -2268,11 +2302,30 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis if (knownSoneId == null) { break; } - synchronized (newSones) { + synchronized (knownSones) { knownSones.add(knownSoneId); } } + /* load Sone following times. */ + soneCounter = 0; + while (true) { + String soneId = configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").getValue(null); + if (soneId == null) { + break; + } + long time = configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").getValue(Long.MAX_VALUE); + Sone followedSone = getSone(soneId); + if (followedSone == null) { + logger.log(Level.WARNING, String.format("Ignoring Sone with invalid ID: %s", soneId)); + } else { + synchronized (soneFollowingTimes) { + soneFollowingTimes.put(getSone(soneId), time); + } + } + ++soneCounter; + } + /* load known posts. */ int postCounter = 0; while (true) { @@ -2280,7 +2333,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis if (knownPostId == null) { break; } - synchronized (newPosts) { + synchronized (knownPosts) { knownPosts.add(knownPostId); } } @@ -2292,7 +2345,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis if (knownReplyId == null) { break; } - synchronized (newReplies) { + synchronized (knownReplies) { knownReplies.add(knownReplyId); } } @@ -2388,7 +2441,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis @Override @SuppressWarnings("synthetic-access") public void run() { - Sone sone = getRemoteSone(identity.getId()); + Sone sone = getRemoteSone(identity.getId(), false); sone.setIdentity(identity); sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition())); soneDownloader.addSone(sone); @@ -2422,19 +2475,17 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis return; } synchronized (posts) { - synchronized (newPosts) { + synchronized (knownPosts) { for (Post post : sone.getPosts()) { posts.remove(post.getId()); - newPosts.remove(post.getId()); coreListenerManager.firePostRemoved(post); } } } synchronized (replies) { - synchronized (newReplies) { - for (Reply reply : sone.getReplies()) { + synchronized (knownReplies) { + for (PostReply reply : sone.getReplies()) { replies.remove(reply.getId()); - newReplies.remove(reply.getId()); coreListenerManager.fireReplyRemoved(reply); } } @@ -2442,10 +2493,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis synchronized (remoteSones) { remoteSones.remove(identity.getId()); } - synchronized (newSones) { - newSones.remove(identity.getId()); - coreListenerManager.fireSoneRemoved(sone); - } + coreListenerManager.fireSoneRemoved(sone); } // @@ -2566,8 +2614,8 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis * * @param insertionDelay * The insertion delay to validate - * @return {@code true} if the given insertion delay was valid, {@code - * false} otherwise + * @return {@code true} if the given insertion delay was valid, + * {@code false} otherwise */ public boolean validateInsertionDelay(Integer insertionDelay) { return options.getIntegerOption("InsertionDelay").validate(insertionDelay); @@ -2655,6 +2703,39 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis } /** + * Returns the number of characters the shortened post should have. + * + * @return The number of characters of the snippet + */ + public int getPostCutOffLength() { + return options.getIntegerOption("PostCutOffLength").get(); + } + + /** + * Validates the number of characters after which to cut off the post. + * + * @param postCutOffLength + * The number of characters of the snippet + * @return {@code true} if the number of characters of the snippet is + * valid, {@code false} otherwise + */ + public boolean validatePostCutOffLength(Integer postCutOffLength) { + return options.getIntegerOption("PostCutOffLength").validate(postCutOffLength); + } + + /** + * Sets the number of characters the shortened post should have. + * + * @param postCutOffLength + * The number of characters of the snippet + * @return This preferences + */ + public Preferences setPostCutOffLength(Integer postCutOffLength) { + options.getIntegerOption("PostCutOffLength").set(postCutOffLength); + return this; + } + + /** * Returns whether Sone requires full access to be even visible. * * @return {@code true} if Sone requires full access, {@code false}