actorsDescr=actors.stream().sorted((a1, a2)->{
+ if(a1 instanceof User && a2 instanceof Group){
+ return -1;
+ }else if(a1 instanceof Group && a2 instanceof User){
+ return 1;
+ }else if(a1 instanceof User u1 && a2 instanceof User u2){
+ return Integer.compare(u1.id, u2.id);
+ }else if(a1 instanceof Group g1 && a2 instanceof Group g2){
+ return g1.eventStartTime.compareTo(g2.eventStartTime);
+ }
+ return 0;
+ }).map(a->new ActorWithDescription(a, getActorCalendarDescription(a, l, today, date, now, timeZone))).toList();
+ model.with("actors", actorsDescr);
+ if(isAjax(req)){
+ return new WebDeltaResponse(resp).box(l.get("events_for_date", Map.of("date", l.formatDay(date))), model.renderContentBlock(), null, true);
+ }
+ return model;
+ }
+
+ private static String getActorCalendarDescription(Actor a, Lang l, LocalDate today, LocalDate targetDate, Instant now, ZoneId timeZone){
+ if(a instanceof User u){
+ LocalDate birthday=u.birthDate.withYear(targetDate.getYear());
+ if(birthday.isBefore(targetDate))
+ birthday=birthday.plusYears(1);
+ int age=birthday.getYear()-u.birthDate.getYear();
+ String key;
+ if(targetDate.isBefore(today))
+ key="birthday_descr_past";
+ else if(targetDate.isAfter(today))
+ key="birthday_descr_future";
+ else
+ key="birthday_descr_today";
+ return l.get(key, Map.of("age", age));
+ }else if(a instanceof Group g){
+ return l.get(g.eventStartTime.isBefore(now) ? "event_descr_past" : "event_descr_future", Map.of("time", l.formatTime(g.eventStartTime, timeZone)));
+ }else{
+ throw new IllegalStateException();
+ }
+ }
+
+ public static Object inviteFriend(Request req, Response resp, Account self){
+ Group group=getGroup(req);
+ User user=context(req).getUsersController().getUserOrThrow(safeParseInt(req.queryParams("user")));
+ String msg=null;
+ try{
+ context(req).getGroupsController().inviteUserToGroup(self.user, user, group);
+ }catch(BadRequestException x){
+ msg=lang(req).get(x.getMessage());
+ }
+ if(msg==null)
+ msg=lang(req).get("invitation_sent");
+ if(isAjax(req)){
+ return new WebDeltaResponse(resp).setContent("frowActions"+user.id, ""+escapeHTML(msg)+"
");
+ }
+ return "";
+ }
+
+ public static Object groupInvitations(Request req, Response resp, Account self){
+ return invitations(req, resp, self, false);
+ }
+
+ public static Object eventInvitations(Request req, Response resp, Account self){
+ return invitations(req, resp, self, true);
+ }
+
+ private static Object invitations(Request req, Response resp, Account self, boolean events){
+ RenderedTemplateResponse model=new RenderedTemplateResponse("group_invites", req);
+ model.paginate(context(req).getGroupsController().getUserInvitations(self, events, offset(req), 25));
+ model.pageTitle(lang(req).get("group_invitations"));
+ model.with("events", events);
+ return model;
+ }
+
+ public static Object respondToInvite(Request req, Response resp, Account self){
+ Group group=getGroup(req);
+
+ boolean accept, tentative;
+ if(req.queryParams("accept")!=null){
+ accept=true;
+ tentative=false;
+ }else if(req.queryParams("tentativeAccept")!=null){
+ accept=true;
+ tentative=true;
+ }else if(req.queryParams("decline")!=null){
+ accept=false;
+ tentative=false;
+ }else{
+ throw new BadRequestException();
+ }
+
+ if(accept){
+ context(req).getGroupsController().joinGroup(group, self.user, tentative);
+ }else{
+ context(req).getGroupsController().declineInvitation(self.user, group);
+ }
+
+ if(isAjax(req)){
+ return new WebDeltaResponse(resp).setContent("groupInviteBtns"+group.id,
+ ""+lang(req).get(accept ? "group_invite_accepted" : "group_invite_declined")+"
");
+ }
+ resp.redirect(back(req));
+ return "";
+ }
+
+ public static Object confirmRemoveUser(Request req, Response resp, Account self){
+ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR);
+ User user=getUserOrThrow(req);
+ Lang l=Utils.lang(req);
+ String back=Utils.back(req);
+ return new RenderedTemplateResponse("generic_confirm", req).with("message", l.get("confirm_remove_user_X", Map.of("name", user.getFirstLastAndGender()))).with("formAction", "/groups/"+group.id+"/removeUser?id="+user.id+"&_redir="+URLEncoder.encode(back)).with("back", back);
+ }
+
+ public static Object removeUser(Request req, Response resp, Account self) throws SQLException{
+ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR);
+ User user=getUserOrThrow(req);
+ context(req).getGroupsController().removeUser(self.user, group, user);
+ if(isAjax(req)){
+ if(isMobile(req))
+ return new WebDeltaResponse(resp).refresh();
+ return new WebDeltaResponse(resp).setContent("groupMemberActions"+user.id, ""+lang(req).get("group_member_removed")+"");
+ }
+ resp.redirect(back(req));
+ return "";
+ }
+
+ public static Object editJoinRequests(Request req, Response resp, Account self){
+ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR);
+ RenderedTemplateResponse model=new RenderedTemplateResponse("group_edit_members", req);
+ model.with("summaryKey", "summary_group_X_join_requests").with("group", group).with("subtab", "requests");
+ PaginatedList list=context(req).getGroupsController().getJoinRequests(self.user, group, offset(req), 50);
+ model.paginate(list);
+ model.with("joinRequestCount", list.total);
+ String csrf=sessionInfo(req).csrfToken;
+ model.with("memberActions", List.of(
+ Map.of("href", "/groups/"+group.id+"/acceptJoinRequest?csrf="+csrf+"&id=", "title", lang(req).get("group_accept_join_request")),
+ Map.of("href", "/groups/"+group.id+"/rejectJoinRequest?csrf="+csrf+"&id=", "title", lang(req).get("group_reject_join_request"))
+ ));
+ model.pageTitle(group.name);
+ return model;
+ }
+
+ public static Object acceptJoinRequest(Request req, Response resp, Account self){
+ return respondToJoinRequest(req, resp, self, true);
+ }
+
+ public static Object rejectJoinRequest(Request req, Response resp, Account self){
+ return respondToJoinRequest(req, resp, self, false);
+ }
+
+ private static Object respondToJoinRequest(Request req, Response resp, Account self, boolean accept){
+ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR);
+ User user=getUserOrThrow(req);
+ if(accept)
+ context(req).getGroupsController().acceptJoinRequest(self.user, group, user);
+ else
+ context(req).getGroupsController().removeUser(self.user, group, user);
+ if(isAjax(req)){
+ if(isMobile(req))
+ return new WebDeltaResponse(resp).refresh();
+ return new WebDeltaResponse(resp)
+ .show("groupMemberActions"+user.id)
+ .hide("groupMemberProgress"+user.id)
+ .setContent("groupMemberActions"+user.id, ""+lang(req).get(accept ? "group_join_request_accepted" : "group_join_request_rejected")+"");
+ }
+ resp.redirect(back(req));
+ return "";
+ }
+
+ public static Object editInvitations(Request req, Response resp, Account self){
+ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR);
+ RenderedTemplateResponse model=new RenderedTemplateResponse("group_edit_members", req);
+ model.with("summaryKey", group.isEvent() ? "summary_event_X_invites" : "summary_group_X_invites").with("group", group).with("subtab", "invites");
+ PaginatedList list=context(req).getGroupsController().getGroupInvites(self.user, group, offset(req), 50);
+ model.paginate(list);
+ model.with("joinRequestCount", context(req).getGroupsController().getJoinRequestCount(self.user, group));
+ String csrf=sessionInfo(req).csrfToken;
+ model.with("memberActions", List.of(
+ Map.of("href", "/groups/"+group.id+"/cancelInvite?csrf="+csrf+"&id=", "title", lang(req).get("cancel_invitation"))
+ ));
+ model.pageTitle(group.name);
+ jsLangKey(req, "cancel");
+ return model;
+ }
+
+ public static Object editCancelInvitation(Request req, Response resp, Account self){
+ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR);
+ User user=getUserOrThrow(req);
+ context(req).getGroupsController().cancelInvitation(self.user, group, user);
+ if(isAjax(req)){
+ if(isMobile(req))
+ return new WebDeltaResponse(resp).refresh();
+ return new WebDeltaResponse(resp)
+ .show("groupMemberActions"+user.id)
+ .hide("groupMemberProgress"+user.id)
+ .setContent("groupMemberActions"+user.id, ""+lang(req).get("invitation_canceled")+"");
+ }
+ resp.redirect(back(req));
+ return "";
+ }
+
+ public static Object syncRelationshipsCollections(Request req, Response resp, Account self){
+ Group group=getGroup(req);
+ group.ensureRemote();
+ context(req).getActivityPubWorker().fetchActorRelationshipCollections(group);
+ Lang l=lang(req);
+ return new WebDeltaResponse(resp).messageBox(l.get("sync_members"), l.get("sync_started"), l.get("ok"));
+ }
+
+ public static Object syncProfile(Request req, Response resp, Account self){
+ Group group=getGroup(req);
+ group.ensureRemote();
+ context(req).getObjectLinkResolver().resolve(group.activityPubID, ForeignGroup.class, true, true, true);
+ return new WebDeltaResponse(resp).refresh();
+ }
+
+ public static Object syncContentCollections(Request req, Response resp, Account self){
+ Group group=getGroup(req);
+ group.ensureRemote();
+ context(req).getActivityPubWorker().fetchActorContentCollections(group);
+ Lang l=lang(req);
+ return new WebDeltaResponse(resp).messageBox(l.get("sync_content"), l.get("sync_started"), l.get("ok"));
+ }
}
diff --git a/src/main/java/smithereen/routes/NotificationsRoutes.java b/src/main/java/smithereen/routes/NotificationsRoutes.java
index f71aca79..dcc3fed3 100644
--- a/src/main/java/smithereen/routes/NotificationsRoutes.java
+++ b/src/main/java/smithereen/routes/NotificationsRoutes.java
@@ -3,10 +3,13 @@
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import smithereen.Utils;
import smithereen.data.Account;
+import smithereen.data.PaginatedList;
import smithereen.data.Post;
import smithereen.data.User;
import smithereen.data.notifications.Notification;
@@ -22,12 +25,12 @@
public class NotificationsRoutes{
public static Object notifications(Request req, Response resp, Account self) throws SQLException{
- int offset=Utils.parseIntOrDefault(req.queryParams("offset"), 0);
+ int offset=offset(req);
RenderedTemplateResponse model=new RenderedTemplateResponse("notifications", req);
int[] total={0};
- List notifications=NotificationsStorage.getNotifications(self.user.id, offset, total);
- model.with("title", lang(req).get("notifications")).with("notifications", notifications).with("offset", offset).with("total", total[0]);
- ArrayList needUsers=new ArrayList<>(), needPosts=new ArrayList<>();
+ List notifications=NotificationsStorage.getNotifications(self.user.id, offset, 50, total);
+ model.pageTitle(lang(req).get("notifications")).paginate(new PaginatedList(notifications, total[0], offset, 50));
+ HashSet needUsers=new HashSet<>(), needPosts=new HashSet<>();
for(Notification n:notifications){
needUsers.add(n.actorID);
@@ -39,17 +42,9 @@ public static Object notifications(Request req, Response resp, Account self) thr
}
}
- HashMap users=new HashMap<>();
- HashMap posts=new HashMap<>();
- // TODO get all users & posts in one database query
- for(Integer uid:needUsers){
- if(users.containsKey(uid))
- continue;
- users.put(uid, UserStorage.getById(uid));
- }
+ Map users=UserStorage.getById(needUsers);
+ Map posts=new HashMap<>();
for(Integer pid:needPosts){
- if(posts.containsKey(pid))
- continue;
posts.put(pid, PostStorage.getPostByID(pid, false));
}
diff --git a/src/main/java/smithereen/routes/PostRoutes.java b/src/main/java/smithereen/routes/PostRoutes.java
index d66d0220..d8c71f90 100644
--- a/src/main/java/smithereen/routes/PostRoutes.java
+++ b/src/main/java/smithereen/routes/PostRoutes.java
@@ -1,7 +1,5 @@
package smithereen.routes;
-import com.google.gson.JsonArray;
-
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@@ -10,10 +8,10 @@
import java.net.URI;
import java.net.URLEncoder;
import java.sql.SQLException;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -25,16 +23,12 @@
import smithereen.Config;
import smithereen.Utils;
-import smithereen.activitypub.ActivityPub;
import smithereen.activitypub.ActivityPubWorker;
-import smithereen.activitypub.objects.ActivityPubObject;
import smithereen.activitypub.objects.Actor;
-import smithereen.activitypub.objects.ForeignActor;
-import smithereen.controllers.WallController;
import smithereen.data.Account;
import smithereen.data.ForeignUser;
import smithereen.data.Group;
-import smithereen.data.ListAndTotal;
+import smithereen.data.PaginatedList;
import smithereen.data.Poll;
import smithereen.data.PollOption;
import smithereen.data.Post;
@@ -48,13 +42,11 @@
import smithereen.data.feed.NewsfeedEntry;
import smithereen.data.feed.PostNewsfeedEntry;
import smithereen.data.notifications.Notification;
-import smithereen.data.notifications.NotificationUtils;
import smithereen.exceptions.BadRequestException;
import smithereen.exceptions.ObjectNotFoundException;
import smithereen.exceptions.UserActionNotAllowedException;
import smithereen.storage.GroupStorage;
import smithereen.storage.LikeStorage;
-import smithereen.storage.MediaCache;
import smithereen.storage.MediaStorageUtils;
import smithereen.storage.NotificationsStorage;
import smithereen.storage.PostStorage;
@@ -101,7 +93,7 @@ public static Object createWallPost(Request req, Response resp, Account self, @N
if(timeLimit){
int seconds=parseIntOrDefault(req.queryParams("pollTimeLimitValue"), 0);
if(seconds>60)
- poll.endTime=new Date(System.currentTimeMillis()+(seconds*1000L));
+ poll.endTime=Instant.now().plusSeconds(seconds);
}
poll.options=pollOptions.stream().map(o->{
PollOption opt=new PollOption();
@@ -122,7 +114,7 @@ public static Object createWallPost(Request req, Response resp, Account self, @N
else
attachments=Collections.emptyList();
- Post post=context(req).getWallController().createWallPost(self.user, owner, replyTo, text, contentWarning, attachments, poll);
+ Post post=context(req).getWallController().createWallPost(self.user, self.id, owner, replyTo, text, contentWarning, attachments, poll);
SessionInfo sess=sessionInfo(req);
sess.postDraftAttachments.clear();
@@ -160,6 +152,7 @@ public static Object editPostForm(Request req, Response resp, Account self) thro
Post post=context(req).getWallController().getPostOrThrow(id);
if(!sessionInfo(req).permissions.canEditPost(post))
throw new UserActionNotAllowedException();
+ context(req).getPrivacyController().enforceObjectPrivacy(self.user, post);
RenderedTemplateResponse model;
if(isAjax(req)){
model=new RenderedTemplateResponse("wall_post_form", req);
@@ -202,7 +195,7 @@ public static Object editPost(Request req, Response resp, Account self) throws S
if(timeLimit){
int seconds=parseIntOrDefault(req.queryParams("pollTimeLimitValue"), 0);
if(seconds>60)
- poll.endTime=new Date(System.currentTimeMillis()+(seconds*1000L));
+ poll.endTime=Instant.now().plusSeconds(seconds);
else if(seconds==-1 && post.poll!=null)
poll.endTime=post.poll.endTime;
}
@@ -224,7 +217,7 @@ else if(seconds==-1 && post.poll!=null)
else
attachments=Collections.emptyList();
- Post post=context(req).getWallController().editPost(sessionInfo(req).permissions, id, text, contentWarning, attachments, poll);
+ Post post=context(req).getWallController().editPost(self.user, sessionInfo(req).permissions, id, text, contentWarning, attachments, poll);
if(isAjax(req)){
if(req.attribute("mobile")!=null)
return new WebDeltaResponse(resp).replaceLocation(post.getInternalURL().toString());
@@ -245,40 +238,17 @@ public static Object feed(Request req, Response resp, Account self) throws SQLEx
int offset=parseIntOrDefault(req.queryParams("offset"), 0);
int[] total={0};
List feed=PostStorage.getFeed(userID, startFromID, offset, total);
- HashSet postIDs=new HashSet<>();
- for(NewsfeedEntry e:feed){
- if(e instanceof PostNewsfeedEntry){
- PostNewsfeedEntry pe=(PostNewsfeedEntry) e;
- if(pe.post!=null){
- postIDs.add(pe.post.id);
- }else{
- System.err.println("No post: "+pe);
- }
- }
- }
- if(req.attribute("mobile")==null && !postIDs.isEmpty()){
- Map> allComments=PostStorage.getRepliesForFeed(postIDs);
- for(NewsfeedEntry e:feed){
- if(e instanceof PostNewsfeedEntry){
- PostNewsfeedEntry pe=(PostNewsfeedEntry) e;
- if(pe.post!=null){
- ListAndTotal comments=allComments.get(pe.post.id);
- if(comments!=null){
- pe.post.repliesObjects=comments.list;
- pe.post.totalTopLevelComments=comments.total;
- pe.post.getAllReplyIDs(postIDs);
- }
- }
- }
- }
+ List feedPosts=feed.stream().filter(e->e instanceof PostNewsfeedEntry pe && pe.post!=null).map(e->((PostNewsfeedEntry)e).post).collect(Collectors.toList());
+ if(req.attribute("mobile")==null && !feedPosts.isEmpty()){
+ context(req).getWallController().populateCommentPreviews(feedPosts);
}
- HashMap interactions=PostStorage.getPostInteractions(postIDs, self.user.id);
+ Map interactions=context(req).getWallController().getUserInteractions(feedPosts, self.user);
if(!feed.isEmpty() && startFromID==0)
startFromID=feed.get(0).id;
jsLangKey(req, "yes", "no", "delete_post", "delete_post_confirm", "delete", "post_form_cw", "post_form_cw_placeholder", "cancel", "attach_menu_photo", "attach_menu_cw", "attach_menu_poll", "max_file_size_exceeded", "max_attachment_count_exceeded", "remove_attachment");
jsLangKey(req, "create_poll_question", "create_poll_options", "create_poll_add_option", "create_poll_delete_option", "create_poll_multi_choice", "create_poll_anonymous", "create_poll_time_limit", "X_days", "X_hours");
return new RenderedTemplateResponse("feed", req).with("title", Utils.lang(req).get("feed")).with("feed", feed).with("postInteractions", interactions)
- .with("paginationURL", "/feed?startFrom="+startFromID+"&offset=").with("total", total[0]).with("offset", offset)
+ .with("paginationUrlPrefix", "/feed?startFrom="+startFromID+"&offset=").with("totalItems", total[0]).with("paginationOffset", offset).with("paginationPerPage", 25).with("paginationFirstPageUrl", "/feed")
.with("draftAttachments", Utils.sessionInfo(req).postDraftAttachments);
}
@@ -304,12 +274,15 @@ public static Object standalonePost(Request req, Response resp) throws SQLExcept
model.with("post", post);
model.with("isGroup", post.owner instanceof Group);
SessionInfo info=Utils.sessionInfo(req);
+ User self=null;
if(info!=null && info.account!=null){
model.with("draftAttachments", info.postDraftAttachments);
if(post.isGroupOwner() && post.getReplyLevel()==0){
model.with("groupAdminLevel", GroupStorage.getGroupMemberAdminLevel(((Group) post.owner).id, info.account.user.id));
}
+ self=info.account.user;
}
+ context(req).getPrivacyController().enforceObjectPrivacy(self, post);
if(post.replyKey.length>0){
model.with("prefilledPostText", post.user.getNameForReply()+", ");
}
@@ -390,14 +363,8 @@ public static Object confirmDelete(Request req, Response resp, Account self) thr
}
public static Object delete(Request req, Response resp, Account self) throws SQLException{
- int postID=Utils.parseIntOrDefault(req.params(":postID"), 0);
- if(postID==0){
- throw new ObjectNotFoundException("err_post_not_found");
- }
- Post post=PostStorage.getPostByID(postID, false);
- if(post==null){
- throw new ObjectNotFoundException("err_post_not_found");
- }
+ Post post=context(req).getWallController().getPostOrThrow(safeParseInt(req.params("postID")));
+ context(req).getPrivacyController().enforceObjectPrivacy(self.user, post);
if(!sessionInfo(req).permissions.canDeletePost(post)){
throw new UserActionNotAllowedException();
}
@@ -411,10 +378,10 @@ public static Object delete(Request req, Response resp, Account self) throws SQL
if(self.accessLevel.ordinal()>=Account.AccessLevel.MODERATOR.ordinal() && post.user.id!=self.id && !post.isGroupOwner() && post.owner.getLocalID()!=self.id && !(post.user instanceof ForeignUser)){
deleteActor=post.user;
}
- ActivityPubWorker.getInstance().sendDeletePostActivity(post, deleteActor);
+ context(req).getActivityPubWorker().sendDeletePostActivity(post, deleteActor);
if(isAjax(req)){
resp.type("application/json");
- return new WebDeltaResponse().remove("post"+postID);
+ return new WebDeltaResponse().remove("post"+post.id);
}
resp.redirect(Utils.back(req));
return "";
@@ -422,19 +389,15 @@ public static Object delete(Request req, Response resp, Account self) throws SQL
public static Object like(Request req, Response resp, Account self) throws SQLException{
req.attribute("noHistory", true);
- int postID=Utils.parseIntOrDefault(req.params(":postID"), 0);
- if(postID==0)
- throw new ObjectNotFoundException("err_post_not_found");
- Post post=PostStorage.getPostByID(postID, false);
- if(post==null)
- throw new ObjectNotFoundException("err_post_not_found");
+ Post post=context(req).getWallController().getPostOrThrow(safeParseInt(req.params("postID")));
+ context(req).getPrivacyController().enforceObjectPrivacy(self.user, post);
if(post.owner instanceof User)
ensureUserNotBlocked(self.user, (User) post.owner);
else
ensureUserNotBlocked(self.user, (Group) post.owner);
String back=Utils.back(req);
- int id=LikeStorage.setPostLiked(self.user.id, postID, true);
+ int id=LikeStorage.setPostLiked(self.user.id, post.id, true);
if(id==0) // Already liked
return "";
if(!(post.user instanceof ForeignUser) && post.user.id!=self.user.id){
@@ -445,12 +408,12 @@ public static Object like(Request req, Response resp, Account self) throws SQLEx
n.objectType=Notification.ObjectType.POST;
NotificationsStorage.putNotification(post.user.id, n);
}
- ActivityPubWorker.getInstance().sendLikeActivity(post, self.user, id);
+ context(req).getActivityPubWorker().sendLikeActivity(post, self.user, id);
if(isAjax(req)){
UserInteractions interactions=PostStorage.getPostInteractions(Collections.singletonList(post.id), self.user.id).get(post.id);
return new WebDeltaResponse(resp)
- .setContent("likeCounterPost"+postID, interactions.likeCount+"")
- .setAttribute("likeButtonPost"+postID, "href", post.getInternalURL()+"/unlike?csrf="+sessionInfo(req).csrfToken);
+ .setContent("likeCounterPost"+post.id, interactions.likeCount+"")
+ .setAttribute("likeButtonPost"+post.id, "href", post.getInternalURL()+"/unlike?csrf="+sessionInfo(req).csrfToken);
}
resp.redirect(back);
return "";
@@ -458,28 +421,24 @@ public static Object like(Request req, Response resp, Account self) throws SQLEx
public static Object unlike(Request req, Response resp, Account self) throws SQLException{
req.attribute("noHistory", true);
- int postID=Utils.parseIntOrDefault(req.params(":postID"), 0);
- if(postID==0)
- throw new ObjectNotFoundException("err_post_not_found");
- Post post=PostStorage.getPostByID(postID, false);
- if(post==null)
- throw new ObjectNotFoundException("err_post_not_found");
+ Post post=context(req).getWallController().getPostOrThrow(safeParseInt(req.params("postID")));
+ context(req).getPrivacyController().enforceObjectPrivacy(self.user, post);
String back=Utils.back(req);
- int id=LikeStorage.setPostLiked(self.user.id, postID, false);
+ int id=LikeStorage.setPostLiked(self.user.id, post.id, false);
if(id==0)
return "";
if(!(post.user instanceof ForeignUser) && post.user.id!=self.user.id){
- NotificationsStorage.deleteNotification(Notification.ObjectType.POST, postID, Notification.Type.LIKE, self.user.id);
+ NotificationsStorage.deleteNotification(Notification.ObjectType.POST, post.id, Notification.Type.LIKE, self.user.id);
}
- ActivityPubWorker.getInstance().sendUndoLikeActivity(post, self.user, id);
+ context(req).getActivityPubWorker().sendUndoLikeActivity(post, self.user, id);
if(isAjax(req)){
UserInteractions interactions=PostStorage.getPostInteractions(Collections.singletonList(post.id), self.user.id).get(post.id);
WebDeltaResponse b=new WebDeltaResponse(resp)
- .setContent("likeCounterPost"+postID, interactions.likeCount+"")
- .setAttribute("likeButtonPost"+postID, "href", post.getInternalURL()+"/like?csrf="+sessionInfo(req).csrfToken);
+ .setContent("likeCounterPost"+post.id, interactions.likeCount+"")
+ .setAttribute("likeButtonPost"+post.id, "href", post.getInternalURL()+"/like?csrf="+sessionInfo(req).csrfToken);
if(interactions.likeCount==0)
- b.hide("likeCounterPost"+postID);
+ b.hide("likeCounterPost"+post.id);
return b;
}
resp.redirect(back);
@@ -497,29 +456,26 @@ private static class LikePopoverResponse{
public static Object likePopover(Request req, Response resp) throws SQLException{
req.attribute("noHistory", true);
- int postID=Utils.parseIntOrDefault(req.params(":postID"), 0);
- if(postID==0)
- throw new ObjectNotFoundException("err_post_not_found");
- Post post=PostStorage.getPostByID(postID, false);
- if(post==null)
- throw new ObjectNotFoundException("err_post_not_found");
+ Post post=context(req).getWallController().getPostOrThrow(safeParseInt(req.params("postID")));
SessionInfo info=sessionInfo(req);
- int selfID=info!=null && info.account!=null ? info.account.user.id : 0;
- List ids=LikeStorage.getPostLikes(postID, selfID, 0, 6);
+ User self=info!=null && info.account!=null ? info.account.user : null;
+ int selfID=self!=null ? self.id : 0;
+ context(req).getPrivacyController().enforceObjectPrivacy(self, post);
+ List ids=LikeStorage.getPostLikes(post.id, selfID, 0, 6);
ArrayList users=new ArrayList<>();
for(int id:ids)
users.add(UserStorage.getById(id));
String _content=new RenderedTemplateResponse("like_popover", req).with("users", users).renderToString();
- UserInteractions interactions=PostStorage.getPostInteractions(Collections.singletonList(postID), selfID).get(postID);
+ UserInteractions interactions=PostStorage.getPostInteractions(Collections.singletonList(post.id), selfID).get(post.id);
WebDeltaResponse b=new WebDeltaResponse(resp)
- .setContent("likeCounterPost"+postID, interactions.likeCount+"");
+ .setContent("likeCounterPost"+post.id, interactions.likeCount+"");
if(info!=null && info.account!=null){
- b.setAttribute("likeButtonPost"+postID, "href", post.getInternalURL()+"/"+(interactions.isLiked ? "un" : "")+"like?csrf="+sessionInfo(req).csrfToken);
+ b.setAttribute("likeButtonPost"+post.id, "href", post.getInternalURL()+"/"+(interactions.isLiked ? "un" : "")+"like?csrf="+sessionInfo(req).csrfToken);
}
if(interactions.likeCount==0)
- b.hide("likeCounterPost"+postID);
+ b.hide("likeCounterPost"+post.id);
else
- b.show("likeCounterPost"+postID);
+ b.show("likeCounterPost"+post.id);
LikePopoverResponse o=new LikePopoverResponse();
o.content=_content;
@@ -527,19 +483,22 @@ public static Object likePopover(Request req, Response resp) throws SQLException
o.altTitle=selfID==0 ? null : lang(req).get("liked_by_X_people", Map.of("count", interactions.likeCount+(interactions.isLiked ? -1 : 1)));
o.actions=b.commands();
o.show=interactions.likeCount>0;
- o.fullURL="/posts/"+postID+"/likes";
+ o.fullURL="/posts/"+post.id+"/likes";
return gson.toJson(o);
}
- public static Object likeList(Request req, Response resp) throws SQLException{
+ public static Object likeList(Request req, Response resp){
+ SessionInfo info=Utils.sessionInfo(req);
+ @Nullable Account self=info!=null ? info.account : null;
int postID=Utils.parseIntOrDefault(req.params(":postID"), 0);
- Post post=getPostOrThrow(postID);
- int offset=parseIntOrDefault(req.queryParams("offset"), 0);
- List users=UserStorage.getByIdAsList(LikeStorage.getPostLikes(postID, 0, offset, 100));
- RenderedTemplateResponse model=new RenderedTemplateResponse(isAjax(req) ? "user_grid" : "content_wrap", req).with("users", users);
- UserInteractions interactions=PostStorage.getPostInteractions(Collections.singletonList(postID), 0).get(postID);
- model.with("pageOffset", offset).with("total", interactions.likeCount).with("paginationUrlPrefix", "/posts/"+postID+"/likes?fromPagination&offset=").with("emptyMessage", lang(req).get("likes_empty"));
- model.with("summary", lang(req).get("liked_by_X_people", Map.of("count", interactions.likeCount)));
+ Post post=context(req).getWallController().getPostOrThrow(postID);
+ context(req).getPrivacyController().enforceObjectPrivacy(self!=null ? self.user : null, post);
+ int offset=offset(req);
+ PaginatedList likes=context(req).getUserInteractionsController().getLikesForObject(post, self!=null ? self.user : null, offset, 100);
+ RenderedTemplateResponse model=new RenderedTemplateResponse(isAjax(req) ? "user_grid" : "content_wrap", req)
+ .paginate(likes, "/posts/"+postID+"/likes?fromPagination&offset=", null)
+ .with("emptyMessage", lang(req).get("likes_empty"))
+ .with("summary", lang(req).get("liked_by_X_people", Map.of("count", likes.total)));
if(isAjax(req)){
if(req.queryParams("fromPagination")==null)
return new WebDeltaResponse(resp).box(lang(req).get("likes_title"), model.renderToString(), "likesList", 474);
@@ -550,109 +509,71 @@ public static Object likeList(Request req, Response resp) throws SQLException{
return model;
}
- public static Object wallAll(Request req, Response resp) throws SQLException{
- return wall(req, resp, false);
+ public static Object userWallAll(Request req, Response resp){
+ User user=context(req).getUsersController().getUserOrThrow(safeParseInt(req.params(":id")));
+ return wall(req, resp, user, false);
}
- public static Object wallOwn(Request req, Response resp) throws SQLException{
- return wall(req, resp, true);
+ public static Object userWallOwn(Request req, Response resp){
+ User user=context(req).getUsersController().getUserOrThrow(safeParseInt(req.params(":id")));
+ return wall(req, resp, user, true);
}
- private static Object wall(Request req, Response resp, boolean ownOnly) throws SQLException{
- String username=req.params(":username");
- User user=UserStorage.getByUsername(username);
- Group group=null;
- if(user==null){
- group=GroupStorage.getByUsername(username);
- if(group==null){
- throw new ObjectNotFoundException("err_user_not_found");
- }else if(ownOnly){
- resp.redirect(Config.localURI("/"+username+"/wall").toString());
- return "";
- }
- }
+ public static Object groupWall(Request req, Response resp){
+ Group group=context(req).getGroupsController().getGroupOrThrow(safeParseInt(req.params(":id")));
+ return wall(req, resp, group, false);
+ }
+
+ private static Object wall(Request req, Response resp, Actor owner, boolean ownOnly){
SessionInfo info=Utils.sessionInfo(req);
@Nullable Account self=info!=null ? info.account : null;
+ if(owner instanceof Group group)
+ context(req).getPrivacyController().enforceUserAccessToGroupContent(self!=null ? self.user : null, group);
-
- int[] postCount={0};
- int offset=Utils.parseIntOrDefault(req.queryParams("offset"), 0);
- List wall=PostStorage.getWallPosts(user==null ? group.id : user.id, group!=null, 0, 0, offset, postCount, ownOnly);
- Set postIDs=wall.stream().map((Post p)->p.id).collect(Collectors.toSet());
-
+ int offset=offset(req);
+ PaginatedList wall=context(req).getWallController().getWallPosts(owner, ownOnly, offset, 20);
if(req.attribute("mobile")==null){
- Map> allComments=PostStorage.getRepliesForFeed(postIDs);
- for(Post post:wall){
- ListAndTotal comments=allComments.get(post.id);
- if(comments!=null){
- post.repliesObjects=comments.list;
- post.totalTopLevelComments=comments.total;
- post.getAllReplyIDs(postIDs);
- }
- }
+ context(req).getWallController().populateCommentPreviews(wall.list);
}
+ Map interactions=context(req).getWallController().getUserInteractions(wall.list, self!=null ? self.user : null);
- HashMap interactions=PostStorage.getPostInteractions(postIDs, self!=null ? self.user.id : 0);
RenderedTemplateResponse model=new RenderedTemplateResponse("wall_page", req)
- .with("posts", wall)
+ .paginate(wall)
.with("postInteractions", interactions)
- .with("owner", user!=null ? user : group)
- .with("isGroup", group!=null)
- .with("postCount", postCount[0])
- .with("pageOffset", offset)
+ .with("owner", owner)
+ .with("isGroup", owner instanceof Group)
.with("ownOnly", ownOnly)
- .with("paginationUrlPrefix", Config.localURI("/"+username+"/wall"+(ownOnly ? "/own" : "")))
.with("tab", ownOnly ? "own" : "all");
- if(user!=null){
- model.with("title", lang(req).get("wall_of_X", Map.of("name", user.getFirstAndGender())));
+ if(owner instanceof User user){
+ model.pageTitle(lang(req).get("wall_of_X", Map.of("name", user.getFirstAndGender())));
}else{
- model.with("title", lang(req).get("wall_of_group"));
+ model.pageTitle(lang(req).get("wall_of_group"));
}
return model;
}
- public static Object wallToWall(Request req, Response resp) throws SQLException{
- String username=req.params(":username");
- User user=UserStorage.getByUsername(username);
- if(user==null)
- throw new ObjectNotFoundException("err_user_not_found");
- String otherUsername=req.params(":other_username");
- User otherUser=UserStorage.getByUsername(otherUsername);
- if(otherUser==null)
- throw new ObjectNotFoundException("err_user_not_found");
+ public static Object wallToWall(Request req, Response resp){
+ User user=context(req).getUsersController().getUserOrThrow(safeParseInt(req.params(":id")));
+ User otherUser=context(req).getUsersController().getUserOrThrow(safeParseInt(req.params(":otherUserID")));
SessionInfo info=Utils.sessionInfo(req);
@Nullable Account self=info!=null ? info.account : null;
- int[] postCount={0};
- int offset=Utils.parseIntOrDefault(req.queryParams("offset"), 0);
- List wall=PostStorage.getWallToWall(user.id, otherUser.id, offset, postCount);
- Set postIDs=wall.stream().map((Post p)->p.id).collect(Collectors.toSet());
-
+ int offset=offset(req);
+ PaginatedList wall=context(req).getWallController().getWallToWallPosts(user, otherUser, offset, 20);
if(req.attribute("mobile")==null){
- Map> allComments=PostStorage.getRepliesForFeed(postIDs);
- for(Post post:wall){
- ListAndTotal comments=allComments.get(post.id);
- if(comments!=null){
- post.repliesObjects=comments.list;
- post.totalTopLevelComments=comments.total;
- post.getAllReplyIDs(postIDs);
- }
- }
+ context(req).getWallController().populateCommentPreviews(wall.list);
}
+ Map interactions=context(req).getWallController().getUserInteractions(wall.list, self!=null ? self.user : null);
- HashMap interactions=PostStorage.getPostInteractions(postIDs, self!=null ? self.user.id : 0);
return new RenderedTemplateResponse("wall_page", req)
- .with("posts", wall)
+ .paginate(wall)
.with("postInteractions", interactions)
.with("owner", user)
.with("otherUser", otherUser)
- .with("postCount", postCount[0])
- .with("pageOffset", offset)
- .with("paginationUrlPrefix", Config.localURI("/"+username+"/wall/with/"+otherUsername))
.with("tab", "wall2wall")
- .with("title", lang(req).get("wall_of_X", Map.of("name", user.getFirstAndGender())));
+ .pageTitle(lang(req).get("wall_of_X", Map.of("name", user.getFirstAndGender())));
}
public static Object ajaxCommentPreview(Request req, Response resp) throws SQLException{
@@ -660,6 +581,7 @@ public static Object ajaxCommentPreview(Request req, Response resp) throws SQLEx
@Nullable Account self=info!=null ? info.account : null;
Post post=getPostOrThrow(parseIntOrDefault(req.params(":postID"), 0));
+ context(req).getPrivacyController().enforceObjectPrivacy(self!=null ? self.user : null, post);
int maxID=parseIntOrDefault(req.queryParams("firstID"), 0);
if(maxID==0)
throw new BadRequestException();
@@ -686,7 +608,7 @@ public static Object ajaxCommentBranch(Request req, Response resp) throws SQLExc
@Nullable Account self=info!=null ? info.account : null;
Post post=getPostOrThrow(parseIntOrDefault(req.params(":postID"), 0));
-
+ context(req).getPrivacyController().enforceObjectPrivacy(self!=null ? self.user : null, post);
List comments=PostStorage.getReplies(post.getReplyKeyForReplies());
RenderedTemplateResponse model=new RenderedTemplateResponse("wall_reply_list", req);
model.with("comments", comments);
@@ -724,6 +646,7 @@ public static Object pollOptionVoters(Request req, Response resp) throws SQLExce
SessionInfo info=Utils.sessionInfo(req);
@Nullable Account self=info!=null ? info.account : null;
+ context(req).getPrivacyController().enforceObjectPrivacy(self!=null ? self.user : null, post);
List users=UserStorage.getByIdAsList(PostStorage.getPollOptionVoters(option.id, offset, 100));
RenderedTemplateResponse model=new RenderedTemplateResponse(isAjax(req) ? "user_grid" : "content_wrap", req).with("users", users);
@@ -759,6 +682,7 @@ public static Object pollOptionVotersPopover(Request req, Response resp) throws
SessionInfo info=Utils.sessionInfo(req);
@Nullable Account self=info!=null ? info.account : null;
+ context(req).getPrivacyController().enforceObjectPrivacy(self!=null ? self.user : null, post);
List users=UserStorage.getByIdAsList(PostStorage.getPollOptionVoters(option.id, 0, 6));
String _content=new RenderedTemplateResponse("like_popover", req).with("users", users).renderToString();
@@ -774,39 +698,16 @@ public static Object pollOptionVotersPopover(Request req, Response resp) throws
public static Object commentsFeed(Request req, Response resp, Account self) throws SQLException{
int offset=parseIntOrDefault(req.queryParams("offset"), 0);
- ListAndTotal feed=PostStorage.getCommentsFeed(self.user.id, offset, 25);
- HashSet postIDs=new HashSet<>();
- for(NewsfeedEntry e:feed.list){
- if(e instanceof PostNewsfeedEntry){
- PostNewsfeedEntry pe=(PostNewsfeedEntry) e;
- if(pe.post!=null){
- postIDs.add(pe.post.id);
- }else{
- System.err.println("No post: "+pe);
- }
- }
- }
- if(req.attribute("mobile")==null && !postIDs.isEmpty()){
- Map> allComments=PostStorage.getRepliesForFeed(postIDs);
- for(NewsfeedEntry e:feed.list){
- if(e instanceof PostNewsfeedEntry){
- PostNewsfeedEntry pe=(PostNewsfeedEntry) e;
- if(pe.post!=null){
- ListAndTotal comments=allComments.get(pe.post.id);
- if(comments!=null){
- pe.post.repliesObjects=comments.list;
- pe.post.totalTopLevelComments=comments.total;
- pe.post.getAllReplyIDs(postIDs);
- }
- }
- }
- }
+ PaginatedList feed=PostStorage.getCommentsFeed(self.user.id, offset, 25);
+ List feedPosts=feed.list.stream().filter(e->e instanceof PostNewsfeedEntry pe && pe.post!=null).map(e->((PostNewsfeedEntry)e).post).collect(Collectors.toList());
+ if(req.attribute("mobile")==null && !feedPosts.isEmpty()){
+ context(req).getWallController().populateCommentPreviews(feedPosts);
}
- HashMap interactions=PostStorage.getPostInteractions(postIDs, self.user.id);
+ Map interactions=context(req).getWallController().getUserInteractions(feedPosts, self.user);
jsLangKey(req, "yes", "no", "delete_post", "delete_post_confirm", "delete", "post_form_cw", "post_form_cw_placeholder", "cancel", "attach_menu_photo", "attach_menu_cw", "attach_menu_poll", "max_file_size_exceeded", "max_attachment_count_exceeded", "remove_attachment");
jsLangKey(req, "create_poll_question", "create_poll_options", "create_poll_add_option", "create_poll_delete_option", "create_poll_multi_choice", "create_poll_anonymous", "create_poll_time_limit", "X_days", "X_hours");
return new RenderedTemplateResponse("feed", req).with("title", Utils.lang(req).get("feed")).with("feed", feed.list).with("postInteractions", interactions)
- .with("paginationURL", "/feed/comments?offset=").with("total", feed.total).with("offset", offset).with("paginationFirstURL", "/feed/comments").with("tab", "comments")
+ .with("paginationUrlPrefix", "/feed/comments?offset=").with("totalItems", feed.total).with("paginationOffset", offset).with("paginationFirstPageUrl", "/feed/comments").with("tab", "comments").with("paginationPerPage", 25)
.with("draftAttachments", Utils.sessionInfo(req).postDraftAttachments);
}
}
diff --git a/src/main/java/smithereen/routes/ProfileRoutes.java b/src/main/java/smithereen/routes/ProfileRoutes.java
index 389b5fd0..f143bd8a 100644
--- a/src/main/java/smithereen/routes/ProfileRoutes.java
+++ b/src/main/java/smithereen/routes/ProfileRoutes.java
@@ -12,21 +12,17 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
import static smithereen.Utils.*;
import smithereen.Config;
import smithereen.Utils;
-import smithereen.activitypub.ActivityPubWorker;
import smithereen.activitypub.objects.PropertyValue;
import smithereen.data.Account;
import smithereen.data.ForeignUser;
-import smithereen.data.FriendRequest;
import smithereen.data.FriendshipStatus;
import smithereen.data.Group;
-import smithereen.data.ListAndTotal;
+import smithereen.data.PaginatedList;
import smithereen.data.Post;
import smithereen.data.SessionInfo;
import smithereen.data.SizedImage;
@@ -35,12 +31,12 @@
import smithereen.data.WebDeltaResponse;
import smithereen.data.feed.NewsfeedEntry;
import smithereen.data.notifications.Notification;
+import smithereen.exceptions.BadRequestException;
import smithereen.exceptions.ObjectNotFoundException;
import smithereen.lang.Lang;
import smithereen.storage.GroupStorage;
import smithereen.storage.NewsfeedStorage;
import smithereen.storage.NotificationsStorage;
-import smithereen.storage.PostStorage;
import smithereen.storage.UserStorage;
import smithereen.templates.RenderedTemplateResponse;
import spark.Request;
@@ -56,27 +52,20 @@ public static Object profile(Request req, Response resp) throws SQLException{
Lang l=lang(req);
if(user!=null){
boolean isSelf=self!=null && self.user.id==user.id;
- int[] postCount={0};
- int offset=Utils.parseIntOrDefault(req.queryParams("offset"), 0);
- List wall=PostStorage.getWallPosts(user.id, false, 0, 0, offset, postCount, false);
- RenderedTemplateResponse model=new RenderedTemplateResponse("profile", req).with("title", user.getFullName()).with("user", user).with("wall", wall).with("own", self!=null && self.user.id==user.id).with("postCount", postCount[0]);
- model.with("pageOffset", offset);
-
- Set postIDs=wall.stream().map((Post p)->p.id).collect(Collectors.toSet());
+ int offset=offset(req);
+ PaginatedList wall=context(req).getWallController().getWallPosts(user, false, offset, 20);
+ RenderedTemplateResponse model=new RenderedTemplateResponse("profile", req)
+ .pageTitle(user.getFullName())
+ .with("user", user)
+ .with("own", self!=null && self.user.id==user.id)
+ .with("postCount", wall.total)
+ .paginate(wall);
if(req.attribute("mobile")==null){
- Map> allComments=PostStorage.getRepliesForFeed(postIDs);
- for(Post post:wall){
- ListAndTotal comments=allComments.get(post.id);
- if(comments!=null){
- post.repliesObjects=comments.list;
- post.totalTopLevelComments=comments.total;
- post.getAllReplyIDs(postIDs);
- }
- }
+ context(req).getWallController().populateCommentPreviews(wall.list);
}
- HashMap interactions=PostStorage.getPostInteractions(postIDs, self!=null ? self.user.id : 0);
+ Map interactions=context(req).getWallController().getUserInteractions(wall.list, self!=null ? self.user : null);
model.with("postInteractions", interactions);
int[] friendCount={0};
@@ -93,7 +82,7 @@ public static Object profile(Request req, Response resp) throws SQLException{
ArrayList profileFields=new ArrayList<>();
if(user.birthDate!=null)
- profileFields.add(new PropertyValue(l.get("birth_date"), l.formatDay(user.birthDate.toLocalDate())));
+ profileFields.add(new PropertyValue(l.get("birth_date"), l.formatDay(user.birthDate)));
if(StringUtils.isNotEmpty(user.summary))
profileFields.add(new PropertyValue(l.get("profile_about"), user.summary));
if(user.attachment!=null)
@@ -145,7 +134,7 @@ public static Object profile(Request req, Response resp) throws SQLException{
meta.put("og:first_name", user.firstName);
if(StringUtils.isNotEmpty(user.lastName))
meta.put("og:last_name", user.lastName);
- String descr=l.get("X_friends", Map.of("count", friendCount[0]))+", "+l.get("X_posts", Map.of("count", postCount[0]));
+ String descr=l.get("X_friends", Map.of("count", friendCount[0]))+", "+l.get("X_posts", Map.of("count", wall.total));
if(StringUtils.isNotEmpty(user.summary))
descr+="\n"+Jsoup.clean(user.summary, Whitelist.none());
meta.put("og:description", descr);
@@ -172,7 +161,7 @@ else if(user.gender==User.Gender.FEMALE)
model.addNavBarItem(user.getFullName(), null, isSelf ? l.get("this_is_you") : null);
- model.with("groups", GroupStorage.getUserGroups(user.id));
+ model.with("groups", context(req).getGroupsController().getUserGroups(user, self!=null ? self.user : null, 0, 100).list);
jsLangKey(req, "yes", "no", "delete_post", "delete_post_confirm", "delete_reply", "delete_reply_confirm", "remove_friend", "cancel", "delete", "post_form_cw", "post_form_cw_placeholder", "attach_menu_photo", "attach_menu_cw", "attach_menu_poll", "max_file_size_exceeded", "max_attachment_count_exceeded", "remove_attachment");
jsLangKey(req, "create_poll_question", "create_poll_options", "create_poll_add_option", "create_poll_delete_option", "create_poll_multi_choice", "create_poll_anonymous", "create_poll_time_limit", "X_days", "X_hours");
return model;
@@ -196,33 +185,34 @@ public static Object confirmSendFriendRequest(Request req, Response resp, Accoun
ensureUserNotBlocked(self.user, user);
FriendshipStatus status=UserStorage.getFriendshipStatus(self.user.id, user.id);
Lang l=lang(req);
- if(status==FriendshipStatus.FOLLOWED_BY){
- if(isAjax(req) && verifyCSRF(req, resp)){
- UserStorage.followUser(self.user.id, user.id, !(user instanceof ForeignUser));
- if(user instanceof ForeignUser){
- ActivityPubWorker.getInstance().sendFollowActivity(self.user, (ForeignUser)user);
+ switch(status){
+ case FOLLOWED_BY:
+ if(isAjax(req) && verifyCSRF(req, resp)){
+ UserStorage.followUser(self.user.id, user.id, !(user instanceof ForeignUser), false);
+ if(user instanceof ForeignUser){
+ context(req).getActivityPubWorker().sendFollowUserActivity(self.user, (ForeignUser) user);
+ }
+ return new WebDeltaResponse(resp).refresh();
+ }else{
+ RenderedTemplateResponse model=new RenderedTemplateResponse("form_page", req);
+ model.with("targetUser", user);
+ model.with("contentTemplate", "send_friend_request").with("formAction", user.getProfileURL("doSendFriendRequest")).with("submitButton", l.get("add_friend"));
+ return model;
}
- return new WebDeltaResponse(resp).refresh();
- }else{
- RenderedTemplateResponse model=new RenderedTemplateResponse("form_page", req);
- model.with("targetUser", user);
- model.with("contentTemplate", "send_friend_request").with("formAction", user.getProfileURL("doSendFriendRequest")).with("submitButton", l.get("add_friend"));
- return model;
- }
- }else if(status==FriendshipStatus.NONE){
- if(user.supportsFriendRequests()){
- RenderedTemplateResponse model=new RenderedTemplateResponse("send_friend_request", req);
- model.with("targetUser", user);
- return wrapForm(req, resp, "send_friend_request", user.getProfileURL("doSendFriendRequest"), l.get("send_friend_req_title"), "add_friend", model);
- }else{
- return doSendFriendRequest(req, resp, self);
- }
- }else if(status==FriendshipStatus.FRIENDS){
- return wrapError(req, resp, "err_already_friends");
- }else if(status==FriendshipStatus.REQUEST_RECVD){
- return wrapError(req, resp, "err_have_incoming_friend_req");
- }else{ // REQ_SENT
- return wrapError(req, resp, "err_friend_req_already_sent");
+ case NONE:
+ if(user.supportsFriendRequests()){
+ RenderedTemplateResponse model=new RenderedTemplateResponse("send_friend_request", req);
+ model.with("targetUser", user);
+ return wrapForm(req, resp, "send_friend_request", user.getProfileURL("doSendFriendRequest"), l.get("send_friend_req_title"), "add_friend", model);
+ }else{
+ return doSendFriendRequest(req, resp, self);
+ }
+ case FRIENDS:
+ return wrapError(req, resp, "err_already_friends");
+ case REQUEST_RECVD:
+ return wrapError(req, resp, "err_have_incoming_friend_req");
+ default: // REQ_SENT
+ return wrapError(req, resp, "err_friend_req_already_sent");
}
}else{
throw new ObjectNotFoundException("err_user_not_found");
@@ -242,14 +232,14 @@ public static Object doSendFriendRequest(Request req, Response resp, Account sel
if(status==FriendshipStatus.NONE && user.supportsFriendRequests()){
UserStorage.putFriendRequest(self.user.id, user.id, req.queryParams("message"), !(user instanceof ForeignUser));
if(user instanceof ForeignUser){
- ActivityPubWorker.getInstance().sendFriendRequestActivity(self.user, (ForeignUser)user, req.queryParams("message"));
+ context(req).getActivityPubWorker().sendFriendRequestActivity(self.user, (ForeignUser)user, req.queryParams("message"));
}
}else{
- UserStorage.followUser(self.user.id, user.id, !(user instanceof ForeignUser));
+ UserStorage.followUser(self.user.id, user.id, !(user instanceof ForeignUser), false);
if(user instanceof ForeignUser){
- ActivityPubWorker.getInstance().sendFollowActivity(self.user, (ForeignUser)user);
+ context(req).getActivityPubWorker().sendFollowUserActivity(self.user, (ForeignUser)user);
}else{
- ActivityPubWorker.getInstance().sendAddToFriendsCollectionActivity(self.user, user);
+ context(req).getActivityPubWorker().sendAddToFriendsCollectionActivity(self.user, user);
}
}
if(isAjax(req)){
@@ -289,13 +279,30 @@ public static Object confirmRemoveFriend(Request req, Response resp, Account sel
private static Object friends(Request req, Response resp, User user, Account self) throws SQLException{
RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req);
- model.with("friendList", UserStorage.getFriendListForUser(user.id)).with("owner", user).with("tab", 0);
- model.with("title", lang(req).get("friends"));
+ model.with("owner", user);
+ model.pageTitle(lang(req).get("friends"));
+ model.paginate(context(req).getFriendsController().getFriends(user, offset(req), 100));
if(self!=null && user.id!=self.user.id){
int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id);
model.with("mutualCount", mutualCount);
}
model.with("tab", "friends");
+ @Nullable
+ String act=req.queryParams("act");
+ if("groupInvite".equals(act)){
+ int groupID=safeParseInt(req.queryParams("group"));
+ Group group=context(req).getGroupsController().getGroupOrThrow(groupID);
+ model.with("selectionMode", true);
+ model.with("customActions", List.of(
+ Map.of(
+ "href", "/groups/"+groupID+"/invite?csrf="+sessionInfo(req).csrfToken+"&user=",
+ "title", lang(req).get("send_invitation"),
+ "ajax", true
+ )
+ ));
+ model.addNavBarItem(group.name, group.getProfileURL()).addNavBarItem(lang(req).get("invite_friends_title"));
+ model.pageTitle(lang(req).get("invite_friends_title"));
+ }
jsLangKey(req, "remove_friend", "yes", "no");
return model;
}
@@ -316,8 +323,9 @@ public static Object mutualFriends(Request req, Response resp, Account self) thr
if(user.id==self.user.id)
throw new ObjectNotFoundException("err_user_not_found");
RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req);
- model.with("friendList", UserStorage.getMutualFriendListForUser(user.id, self.user.id)).with("owner", user).with("tab", 0);
- model.with("title", lang(req).get("friends"));
+ model.paginate(context(req).getFriendsController().getMutualFriends(user, self.user, offset(req), 100));
+ model.with("owner", user);
+ model.pageTitle(lang(req).get("friends"));
model.with("tab", "mutual");
int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id);
model.with("mutualCount", mutualCount);
@@ -328,96 +336,93 @@ public static Object mutualFriends(Request req, Response resp, Account self) thr
public static Object followers(Request req, Response resp) throws SQLException{
SessionInfo info=Utils.sessionInfo(req);
@Nullable Account self=info!=null ? info.account : null;
- String username=req.params(":username");
+ String _id=req.params(":id");
User user;
- if(username==null){
+ if(_id==null){
if(requireAccount(req, resp)){
- user=sessionInfo(req).account.user;
+ user=self.user;
}else{
return "";
}
}else{
- user=UserStorage.getByUsername(username);
+ user=context(req).getUsersController().getUserOrThrow(safeParseInt(_id));
}
- if(user!=null){
- RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req);
- model.with("title", lang(req).get("followers")).with("toolbarTitle", lang(req).get("friends"));
- model.with("friendList", UserStorage.getNonMutualFollowers(user.id, true, true)).with("owner", user).with("followers", true).with("tab", "followers");
- if(self!=null && user.id!=self.user.id){
- int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id);
- model.with("mutualCount", mutualCount);
- }
- return model;
+ RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req);
+ model.with("title", lang(req).get("followers")).with("toolbarTitle", lang(req).get("friends"));
+ model.paginate(context(req).getFriendsController().getFollowers(user, offset(req), 100));
+ model.with("owner", user).with("followers", true).with("tab", "followers");
+ if(self!=null && user.id!=self.user.id){
+ int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id);
+ model.with("mutualCount", mutualCount);
}
- throw new ObjectNotFoundException("err_user_not_found");
+ return model;
}
public static Object following(Request req, Response resp) throws SQLException{
SessionInfo info=Utils.sessionInfo(req);
@Nullable Account self=info!=null ? info.account : null;
- String username=req.params(":username");
+ String _id=req.params(":id");
User user;
- if(username==null){
+ if(_id==null){
if(requireAccount(req, resp)){
- user=sessionInfo(req).account.user;
+ user=self.user;
}else{
return "";
}
}else{
- user=UserStorage.getByUsername(username);
+ user=context(req).getUsersController().getUserOrThrow(safeParseInt(_id));
}
- if(user!=null){
- RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req);
- model.with("title", lang(req).get("following")).with("toolbarTitle", lang(req).get("friends"));
- model.with("friendList", UserStorage.getNonMutualFollowers(user.id, false, true)).with("owner", user).with("following", true).with("tab", "following");
- if(self!=null && user.id!=self.user.id){
- int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id);
- model.with("mutualCount", mutualCount);
- }
- jsLangKey(req, "unfollow", "yes", "no");
- return model;
+ RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req);
+ model.with("title", lang(req).get("following")).with("toolbarTitle", lang(req).get("friends"));
+ model.paginate(context(req).getFriendsController().getFollows(user, offset(req), 100));
+ model.with("owner", user).with("following", true).with("tab", "following");
+ if(self!=null && user.id!=self.user.id){
+ int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id);
+ model.with("mutualCount", mutualCount);
}
- throw new ObjectNotFoundException("err_user_not_found");
+ jsLangKey(req, "unfollow", "yes", "no");
+ return model;
}
public static Object incomingFriendRequests(Request req, Response resp, Account self) throws SQLException{
- List requests=UserStorage.getIncomingFriendRequestsForUser(self.user.id, 0, 100);
RenderedTemplateResponse model=new RenderedTemplateResponse("friend_requests", req);
- model.with("friendRequests", requests);
+ model.paginate(context(req).getFriendsController().getIncomingFriendRequests(self.user, offset(req), 20));
model.with("title", lang(req).get("friend_requests")).with("toolbarTitle", lang(req).get("friends")).with("owner", self.user);
return model;
}
public static Object respondToFriendRequest(Request req, Response resp, Account self) throws SQLException{
- String username=req.params(":username");
- User user=UserStorage.getByUsername(username);
- if(user!=null){
- if(req.queryParams("accept")!=null){
- if(user instanceof ForeignUser){
- UserStorage.acceptFriendRequest(self.user.id, user.id, false);
- ActivityPubWorker.getInstance().sendFollowActivity(self.user, (ForeignUser) user);
- }else{
- UserStorage.acceptFriendRequest(self.user.id, user.id, true);
- Notification n=new Notification();
- n.type=Notification.Type.FRIEND_REQ_ACCEPT;
- n.actorID=self.user.id;
- NotificationsStorage.putNotification(user.id, n);
- ActivityPubWorker.getInstance().sendAddToFriendsCollectionActivity(self.user, user);
- NewsfeedStorage.putEntry(user.id, self.user.id, NewsfeedEntry.Type.ADD_FRIEND, null);
- }
- NewsfeedStorage.putEntry(self.user.id, user.id, NewsfeedEntry.Type.ADD_FRIEND, null);
- }else if(req.queryParams("decline")!=null){
- UserStorage.deleteFriendRequest(self.user.id, user.id);
- if(user instanceof ForeignUser){
- ActivityPubWorker.getInstance().sendRejectFriendRequestActivity(self.user, (ForeignUser) user);
- }
+ User user=getUserOrThrow(req);
+ boolean accept;
+ if(req.queryParams("accept")!=null){
+ accept=true;
+ if(user instanceof ForeignUser){
+ UserStorage.acceptFriendRequest(self.user.id, user.id, false);
+ context(req).getActivityPubWorker().sendFollowUserActivity(self.user, (ForeignUser) user);
+ }else{
+ UserStorage.acceptFriendRequest(self.user.id, user.id, true);
+ Notification n=new Notification();
+ n.type=Notification.Type.FRIEND_REQ_ACCEPT;
+ n.actorID=self.user.id;
+ NotificationsStorage.putNotification(user.id, n);
+ context(req).getActivityPubWorker().sendAddToFriendsCollectionActivity(self.user, user);
+ NewsfeedStorage.putEntry(user.id, self.user.id, NewsfeedEntry.Type.ADD_FRIEND, null);
+ }
+ NewsfeedStorage.putEntry(self.user.id, user.id, NewsfeedEntry.Type.ADD_FRIEND, null);
+ }else if(req.queryParams("decline")!=null){
+ accept=false;
+ UserStorage.deleteFriendRequest(self.user.id, user.id);
+ if(user instanceof ForeignUser){
+ context(req).getActivityPubWorker().sendRejectFriendRequestActivity(self.user, (ForeignUser) user);
}
- if(isAjax(req))
- return new WebDeltaResponse(resp).refresh();
- resp.redirect(Utils.back(req));
}else{
- throw new ObjectNotFoundException("err_user_not_found");
+ throw new BadRequestException();
+ }
+ if(isAjax(req)){
+ return new WebDeltaResponse(resp).setContent("friendReqBtns"+user.id,
+ ""+lang(req).get(accept ? "friend_req_accepted" : "friend_req_declined")+"
");
}
+ resp.redirect(Utils.back(req));
return "";
}
@@ -429,10 +434,10 @@ public static Object doRemoveFriend(Request req, Response resp, Account self) th
if(status==FriendshipStatus.FRIENDS || status==FriendshipStatus.REQUEST_SENT || status==FriendshipStatus.FOLLOWING || status==FriendshipStatus.FOLLOW_REQUESTED){
UserStorage.unfriendUser(self.user.id, user.id);
if(user instanceof ForeignUser){
- ActivityPubWorker.getInstance().sendUnfriendActivity(self.user, user);
+ context(req).getActivityPubWorker().sendUnfriendActivity(self.user, user);
}
if(status==FriendshipStatus.FRIENDS){
- ActivityPubWorker.getInstance().sendRemoveFromFriendsCollectionActivity(self.user, user);
+ context(req).getActivityPubWorker().sendRemoveFromFriendsCollectionActivity(self.user, user);
NewsfeedStorage.deleteEntry(self.user.id, user.id, NewsfeedEntry.Type.ADD_FRIEND);
if(!(user instanceof ForeignUser)){
NewsfeedStorage.deleteEntry(user.id, self.user.id, NewsfeedEntry.Type.ADD_FRIEND);
@@ -474,9 +479,9 @@ public static Object blockUser(Request req, Response resp, Account self) throws
FriendshipStatus status=UserStorage.getFriendshipStatus(self.user.id, user.id);
UserStorage.blockUser(self.user.id, user.id);
if(user instanceof ForeignUser)
- ActivityPubWorker.getInstance().sendBlockActivity(self.user, (ForeignUser) user);
+ context(req).getActivityPubWorker().sendBlockActivity(self.user, (ForeignUser) user);
if(status==FriendshipStatus.FRIENDS){
- ActivityPubWorker.getInstance().sendRemoveFromFriendsCollectionActivity(self.user, user);
+ context(req).getActivityPubWorker().sendRemoveFromFriendsCollectionActivity(self.user, user);
NewsfeedStorage.deleteEntry(self.user.id, user.id, NewsfeedEntry.Type.ADD_FRIEND);
if(!(user instanceof ForeignUser)){
NewsfeedStorage.deleteEntry(user.id, self.user.id, NewsfeedEntry.Type.ADD_FRIEND);
@@ -492,7 +497,7 @@ public static Object unblockUser(Request req, Response resp, Account self) throw
User user=getUserOrThrow(req);
UserStorage.unblockUser(self.user.id, user.id);
if(user instanceof ForeignUser)
- ActivityPubWorker.getInstance().sendUndoBlockActivity(self.user, (ForeignUser) user);
+ context(req).getActivityPubWorker().sendUndoBlockActivity(self.user, (ForeignUser) user);
if(isAjax(req))
return new WebDeltaResponse(resp).refresh();
resp.redirect(back(req));
@@ -500,13 +505,31 @@ public static Object unblockUser(Request req, Response resp, Account self) throw
}
- private static User getUserOrThrow(Request req) throws SQLException{
+ private static User getUserOrThrow(Request req){
int id=parseIntOrDefault(req.params(":id"), 0);
- if(id==0)
- throw new ObjectNotFoundException("err_user_not_found");
- User user=UserStorage.getById(id);
- if(user==null)
- throw new ObjectNotFoundException("err_user_not_found");
- return user;
+ return context(req).getUsersController().getUserOrThrow(id);
+ }
+
+ public static Object syncRelationshipsCollections(Request req, Response resp, Account self){
+ User user=getUserOrThrow(req);
+ user.ensureRemote();
+ context(req).getActivityPubWorker().fetchActorRelationshipCollections(user);
+ Lang l=lang(req);
+ return new WebDeltaResponse(resp).messageBox(l.get("sync_friends_and_groups"), l.get("sync_started"), l.get("ok"));
+ }
+
+ public static Object syncProfile(Request req, Response resp, Account self){
+ User user=getUserOrThrow(req);
+ user.ensureRemote();
+ context(req).getObjectLinkResolver().resolve(user.activityPubID, ForeignUser.class, true, true, true);
+ return new WebDeltaResponse(resp).refresh();
+ }
+
+ public static Object syncContentCollections(Request req, Response resp, Account self){
+ User user=getUserOrThrow(req);
+ user.ensureRemote();
+ context(req).getActivityPubWorker().fetchActorContentCollections(user);
+ Lang l=lang(req);
+ return new WebDeltaResponse(resp).messageBox(l.get("sync_content"), l.get("sync_started"), l.get("ok"));
}
}
diff --git a/src/main/java/smithereen/routes/SettingsAdminRoutes.java b/src/main/java/smithereen/routes/SettingsAdminRoutes.java
index d0cfb4e6..de153b02 100644
--- a/src/main/java/smithereen/routes/SettingsAdminRoutes.java
+++ b/src/main/java/smithereen/routes/SettingsAdminRoutes.java
@@ -10,6 +10,7 @@
import smithereen.Mailer;
import smithereen.Utils;
import smithereen.data.Account;
+import smithereen.data.PaginatedList;
import smithereen.data.User;
import smithereen.data.WebDeltaResponse;
import smithereen.exceptions.ObjectNotFoundException;
@@ -78,10 +79,8 @@ public static Object users(Request req, Response resp, Account self) throws SQLE
Lang l=lang(req);
int offset=parseIntOrDefault(req.queryParams("offset"), 0);
List accounts=UserStorage.getAllAccounts(offset, 100);
- model.with("accounts", accounts);
+ model.paginate(new PaginatedList<>(accounts, UserStorage.getLocalUserCount(), offset, 100));
model.with("title", l.get("admin_users")+" | "+l.get("menu_admin")).with("toolbarTitle", l.get("menu_admin"));
- model.with("total", UserStorage.getLocalUserCount());
- model.with("pageOffset", offset);
model.with("wideOnDesktop", true);
jsLangKey(req, "cancel", "yes", "no");
return model;
diff --git a/src/main/java/smithereen/routes/SettingsRoutes.java b/src/main/java/smithereen/routes/SettingsRoutes.java
index f4bf6dc3..0ee07724 100644
--- a/src/main/java/smithereen/routes/SettingsRoutes.java
+++ b/src/main/java/smithereen/routes/SettingsRoutes.java
@@ -11,6 +11,8 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.SQLException;
+import java.time.LocalDate;
+import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -123,18 +125,12 @@ public static Object updateProfileGeneral(Request req, Response resp, Account se
if(_gender<0 || _gender>2)
_gender=0;
User.Gender gender=User.Gender.valueOf(_gender);
- java.sql.Date bdate=self.user.birthDate;
+ LocalDate bdate=self.user.birthDate;
String _bdate=req.queryParams("bdate");
if(_bdate!=null){
- String[] dateParts=_bdate.split("-");
- if(dateParts.length==3){
- int year=parseIntOrDefault(dateParts[0], 0);
- int month=parseIntOrDefault(dateParts[1], 0);
- int day=parseIntOrDefault(dateParts[2], 0);
- if(year>=1900 && year<9999 && month>0 && month<=12 && day>0 && day<=31){
- bdate=new java.sql.Date(year-1900, month-1, day);
- }
- }
+ try{
+ bdate=LocalDate.parse(_bdate);
+ }catch(DateTimeParseException ignore){}
}
String message;
if(first.length()<2){
@@ -146,7 +142,7 @@ public static Object updateProfileGeneral(Request req, Response resp, Account se
self.user=UserStorage.getById(self.user.id);
if(self.user==null)
throw new IllegalStateException("?!");
- ActivityPubWorker.getInstance().sendUpdateUserActivity(self.user);
+ context(req).getActivityPubWorker().sendUpdateUserActivity(self.user);
if(isAjax(req)){
return new WebDeltaResponse(resp).show("formMessage_profileEdit").setContent("formMessage_profileEdit", message);
}
@@ -247,7 +243,7 @@ public static Object updateProfilePicture(Request req, Response resp, Account se
}
UserStorage.updateProfilePicture(self.user, MediaStorageUtils.serializeAttachment(ava).toString());
self.user=UserStorage.getById(self.user.id);
- ActivityPubWorker.getInstance().sendUpdateUserActivity(self.user);
+ context(req).getActivityPubWorker().sendUpdateUserActivity(self.user);
}else{
if(group.icon!=null && !(group instanceof ForeignGroup)){
LocalImage li=(LocalImage) group.icon.get(0);
@@ -259,7 +255,7 @@ public static Object updateProfilePicture(Request req, Response resp, Account se
}
GroupStorage.updateProfilePicture(group, MediaStorageUtils.serializeAttachment(ava).toString());
group=GroupStorage.getById(group.id);
- ActivityPubWorker.getInstance().sendUpdateGroupActivity(group);
+ context(req).getActivityPubWorker().sendUpdateGroupActivity(group);
}
temp.delete();
}finally{
@@ -360,11 +356,11 @@ public static Object removeProfilePicture(Request req, Response resp, Account se
if(group!=null){
GroupStorage.updateProfilePicture(group, null);
group=GroupStorage.getById(groupID);
- ActivityPubWorker.getInstance().sendUpdateGroupActivity(group);
+ context(req).getActivityPubWorker().sendUpdateGroupActivity(group);
}else{
UserStorage.updateProfilePicture(self.user, null);
self.user=UserStorage.getById(self.user.id);
- ActivityPubWorker.getInstance().sendUpdateUserActivity(self.user);
+ context(req).getActivityPubWorker().sendUpdateUserActivity(self.user);
}
if(isAjax(req))
return new WebDeltaResponse(resp).refresh();
diff --git a/src/main/java/smithereen/routes/SystemRoutes.java b/src/main/java/smithereen/routes/SystemRoutes.java
index 23c863b6..5488e700 100644
--- a/src/main/java/smithereen/routes/SystemRoutes.java
+++ b/src/main/java/smithereen/routes/SystemRoutes.java
@@ -28,10 +28,8 @@
import smithereen.BuildInfo;
import smithereen.Config;
-import smithereen.ObjectLinkResolver;
import smithereen.Utils;
import smithereen.activitypub.ActivityPub;
-import smithereen.activitypub.ActivityPubWorker;
import smithereen.activitypub.objects.ActivityPubObject;
import smithereen.activitypub.objects.Actor;
import smithereen.activitypub.objects.Document;
@@ -68,21 +66,13 @@
import spark.Response;
import spark.utils.StringUtils;
-import static smithereen.Utils.USERNAME_DOMAIN_PATTERN;
-import static smithereen.Utils.back;
-import static smithereen.Utils.ensureUserNotBlocked;
-import static smithereen.Utils.isAjax;
-import static smithereen.Utils.isURL;
-import static smithereen.Utils.isUsernameAndDomain;
-import static smithereen.Utils.lang;
-import static smithereen.Utils.normalizeURLDomain;
-import static smithereen.Utils.parseIntOrDefault;
-import static smithereen.Utils.wrapError;
+import static smithereen.Utils.*;
public class SystemRoutes{
private static final Logger LOG=LoggerFactory.getLogger(SystemRoutes.class);
public static Object downloadExternalMedia(Request req, Response resp) throws SQLException{
+ requireQueryParams(req, "type", "format", "size");
MediaCache cache=MediaCache.getInstance();
String type=req.queryParams("type");
String mime;
@@ -195,19 +185,13 @@ public static Object downloadExternalMedia(Request req, Response resp) throws SQ
if(item==null){
if(itemType==MediaCache.ItemType.AVATAR && req.queryParams("retrying")==null){
if(user!=null){
- ActivityPubObject obj=ActivityPub.fetchRemoteObject(user.activityPubID);
- if(obj instanceof ForeignUser){
- ForeignUser updatedUser=(ForeignUser) obj;
- UserStorage.putOrUpdateForeignUser(updatedUser);
- resp.redirect(Config.localURI("/system/downloadExternalMedia?type=user_ava&user_id="+updatedUser.id+"&size="+sizeType.suffix()+"&format="+format.fileExtension()+"&retrying").toString());
- }
+ ForeignUser updatedUser=context(req).getObjectLinkResolver().resolve(user.activityPubID, ForeignUser.class, true, true, true);
+ resp.redirect(Config.localURI("/system/downloadExternalMedia?type=user_ava&user_id="+updatedUser.id+"&size="+sizeType.suffix()+"&format="+format.fileExtension()+"&retrying").toString());
+ return "";
}else if(group!=null){
- ActivityPubObject obj=ActivityPub.fetchRemoteObject(group.activityPubID);
- if(obj instanceof ForeignGroup){
- ForeignGroup updatedGroup=(ForeignGroup) obj;
- GroupStorage.putOrUpdateForeignGroup(updatedGroup);
- resp.redirect(Config.localURI("/system/downloadExternalMedia?type=group_ava&user_id="+updatedGroup.id+"&size="+sizeType.suffix()+"&format="+format.fileExtension()+"&retrying").toString());
- }
+ ForeignGroup updatedGroup=context(req).getObjectLinkResolver().resolve(group.activityPubID, ForeignGroup.class, true, true, true);
+ resp.redirect(Config.localURI("/system/downloadExternalMedia?type=group_ava&user_id="+updatedGroup.id+"&size="+sizeType.suffix()+"&format="+format.fileExtension()+"&retrying").toString());
+ return "";
}
}
resp.redirect(uri.toString());
@@ -342,7 +326,7 @@ public static Object quickSearch(Request req, Response resp, Account self) throw
query=normalizeURLDomain(query);
URI uri=URI.create(query);
try{
- ActivityPubObject obj=ObjectLinkResolver.resolve(uri, ActivityPubObject.class, false, false, false);
+ ActivityPubObject obj=context(req).getObjectLinkResolver().resolve(uri, ActivityPubObject.class, false, false, false);
if(obj instanceof User){
users=Collections.singletonList((User)obj);
}else if(obj instanceof Group){
@@ -353,7 +337,7 @@ public static Object quickSearch(Request req, Response resp, Account self) throw
}catch(ObjectNotFoundException x){
if(!Config.isLocal(uri)){
try{
- Actor actor=ObjectLinkResolver.resolve(uri, Actor.class, false, false, false);
+ Actor actor=context(req).getObjectLinkResolver().resolve(uri, Actor.class, false, false, false);
if(actor instanceof User){
users=Collections.singletonList((User)actor);
}else if(actor instanceof Group){
@@ -425,7 +409,7 @@ public static Object loadRemoteObject(Request req, Response resp, Account self)
}
}
try{
- obj=ObjectLinkResolver.resolve(uri, ActivityPubObject.class, true, false, false);
+ obj=context(req).getObjectLinkResolver().resolve(uri, ActivityPubObject.class, true, false, false);
}catch(UnsupportedRemoteObjectTypeException x){
LOG.debug("Unsupported remote object", x);
return new JsonObjectBuilder().add("error", lang(req).get("unsupported_remote_object_type")).build();
@@ -433,32 +417,29 @@ public static Object loadRemoteObject(Request req, Response resp, Account self)
LOG.debug("Remote object not found", x);
return new JsonObjectBuilder().add("error", lang(req).get("remote_object_not_found")).build();
}
- if(obj instanceof ForeignUser){
- ForeignUser user=(ForeignUser)obj;
- obj.storeDependencies();
+ if(obj instanceof ForeignUser user){
+ obj.storeDependencies(context(req));
UserStorage.putOrUpdateForeignUser(user);
return new JsonObjectBuilder().add("success", user.getProfileURL()).build();
- }else if(obj instanceof ForeignGroup){
- ForeignGroup group=(ForeignGroup)obj;
- obj.storeDependencies();
+ }else if(obj instanceof ForeignGroup group){
+ obj.storeDependencies(context(req));
GroupStorage.putOrUpdateForeignGroup(group);
return new JsonObjectBuilder().add("success", group.getProfileURL()).build();
- }else if(obj instanceof Post){
- Post post=(Post)obj;
+ }else if(obj instanceof Post post){
if(post.inReplyTo==null || post.id!=0){
- post.storeDependencies();
+ post.storeDependencies(context(req));
PostStorage.putForeignWallPost(post);
try{
- ActivityPubWorker.getInstance().fetchAllReplies(post).get(30, TimeUnit.SECONDS);
+ context(req).getActivityPubWorker().fetchAllReplies(post).get(30, TimeUnit.SECONDS);
}catch(Throwable x){
x.printStackTrace();
}
return new JsonObjectBuilder().add("success", Config.localURI("/posts/"+post.id).toString()).build();
}else{
- Future> future=ActivityPubWorker.getInstance().fetchReplyThread(post);
+ Future> future=context(req).getActivityPubWorker().fetchReplyThread(post);
try{
List posts=future.get(30, TimeUnit.SECONDS);
- ActivityPubWorker.getInstance().fetchAllReplies(posts.get(0)).get(30, TimeUnit.SECONDS);
+ context(req).getActivityPubWorker().fetchAllReplies(posts.get(0)).get(30, TimeUnit.SECONDS);
return new JsonObjectBuilder().add("success", Config.localURI("/posts/"+posts.get(0).id+"#comment"+post.id).toString()).build();
}catch(InterruptedException ignore){
}catch(ExecutionException e){
@@ -498,8 +479,9 @@ public static Object votePoll(Request req, Response resp, Account self) throws S
ensureUserNotBlocked(self.user, _owner);
owner=_owner;
}else{
- Group _owner=GroupStorage.getById(-poll.ownerID);
+ Group _owner=context(req).getGroupsController().getGroupOrThrow(-poll.ownerID);
ensureUserNotBlocked(self.user, _owner);
+ context(req).getPrivacyController().enforceUserAccessToGroupContent(self.user, _owner);
owner=_owner;
}
@@ -542,7 +524,13 @@ public static Object votePoll(Request req, Response resp, Account self) throws S
for(PollOption opt:options)
opt.addVotes(1);
- ActivityPubWorker.getInstance().sendPollVotes(self.user, poll, owner, options, voteIDs);
+ context(req).getActivityPubWorker().sendPollVotes(self.user, poll, owner, options, voteIDs);
+ int postID=PostStorage.getPostIdByPollId(id);
+ if(postID>0){
+ Post post=context(req).getWallController().getPostOrThrow(postID);
+ post.poll=poll; // So the last vote time is as it was before the vote
+ context(req).getWallController().sendUpdateQuestionIfNeeded(post);
+ }
if(isAjax(req)){
UserInteractions interactions=new UserInteractions();
diff --git a/src/main/java/smithereen/routes/WellKnownRoutes.java b/src/main/java/smithereen/routes/WellKnownRoutes.java
index baaa0e9a..34d764f6 100644
--- a/src/main/java/smithereen/routes/WellKnownRoutes.java
+++ b/src/main/java/smithereen/routes/WellKnownRoutes.java
@@ -33,7 +33,7 @@ public static Object webfinger(Request req, Response resp) throws SQLException{
wfr.subject="acct:"+username+"@"+Config.domain;
WebfingerResponse.Link selfLink=new WebfingerResponse.Link();
selfLink.rel="self";
- selfLink.type="application/activitypub+json";
+ selfLink.type="application/activity+json";
selfLink.href=Config.localURI("/activitypub/serviceActor");
wfr.links=List.of(selfLink);
return Utils.gson.toJson(wfr);
@@ -46,7 +46,7 @@ public static Object webfinger(Request req, Response resp) throws SQLException{
wfr.subject="acct:"+user.username+"@"+Config.domain;
WebfingerResponse.Link selfLink=new WebfingerResponse.Link();
selfLink.rel="self";
- selfLink.type="application/activitypub+json";
+ selfLink.type="application/activity+json";
selfLink.href=user.activityPubID;
WebfingerResponse.Link authLink=new WebfingerResponse.Link();
authLink.rel="http://ostatus.org/schema/1.0/subscribe";
@@ -62,7 +62,7 @@ public static Object webfinger(Request req, Response resp) throws SQLException{
wfr.subject="acct:"+group.username+"@"+Config.domain;
WebfingerResponse.Link selfLink=new WebfingerResponse.Link();
selfLink.rel="self";
- selfLink.type="application/activitypub+json";
+ selfLink.type="application/activity+json";
selfLink.href=group.activityPubID;
wfr.links=List.of(selfLink);
return Utils.gson.toJson(wfr);
diff --git a/src/main/java/smithereen/sparkext/ActivityPubCollectionPageResponse.java b/src/main/java/smithereen/sparkext/ActivityPubCollectionPageResponse.java
index 75f10c7e..bc8121c6 100644
--- a/src/main/java/smithereen/sparkext/ActivityPubCollectionPageResponse.java
+++ b/src/main/java/smithereen/sparkext/ActivityPubCollectionPageResponse.java
@@ -6,7 +6,7 @@
import smithereen.activitypub.objects.ActivityPubObject;
import smithereen.activitypub.objects.LinkOrObject;
-import smithereen.data.ListAndTotal;
+import smithereen.data.PaginatedList;
public class ActivityPubCollectionPageResponse{
public int totalItems;
@@ -27,11 +27,11 @@ public static ActivityPubCollectionPageResponse forLinks(List objects, int
return r;
}
- public static ActivityPubCollectionPageResponse forLinks(ListAndTotal lt){
+ public static ActivityPubCollectionPageResponse forLinks(PaginatedList lt){
return forLinks(lt.list, lt.total);
}
- public static ActivityPubCollectionPageResponse forObjects(ListAndTotal extends ActivityPubObject> lt){
+ public static ActivityPubCollectionPageResponse forObjects(PaginatedList extends ActivityPubObject> lt){
return forObjects(lt.list, lt.total);
}
diff --git a/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java b/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java
index c8c56b03..b06f916b 100644
--- a/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java
+++ b/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java
@@ -10,9 +10,10 @@
import smithereen.Config;
import smithereen.Utils;
+import smithereen.activitypub.objects.Actor;
public class DatabaseSchemaUpdater{
- public static final int SCHEMA_VERSION=16;
+ public static final int SCHEMA_VERSION=23;
private static final Logger LOG=LoggerFactory.getLogger(DatabaseSchemaUpdater.class);
public static void maybeUpdate() throws SQLException{
@@ -42,105 +43,105 @@ public static void maybeUpdate() throws SQLException{
private static void updateFromPrevious(int target) throws SQLException{
LOG.info("Updating database schema {} -> {}", Config.dbSchemaVersion, target);
Connection conn=DatabaseConnectionManager.getConnection();
- if(target==2){
- conn.createStatement().execute("ALTER TABLE wall_posts ADD (reply_count INTEGER UNSIGNED NOT NULL DEFAULT 0)");
- }else if(target==3){
- conn.createStatement().execute("ALTER TABLE users ADD middle_name VARCHAR(100) DEFAULT NULL AFTER lname, ADD maiden_name VARCHAR(100) DEFAULT NULL AFTER middle_name");
- }else if(target==4){
- conn.createStatement().execute("""
- CREATE TABLE `groups` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
- `name` varchar(200) NOT NULL DEFAULT '',
- `username` varchar(50) NOT NULL DEFAULT '',
- `domain` varchar(100) NOT NULL DEFAULT '',
- `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL,
- `ap_url` varchar(300) DEFAULT NULL,
- `ap_inbox` varchar(300) DEFAULT NULL,
- `ap_shared_inbox` varchar(300) DEFAULT NULL,
- `ap_outbox` varchar(300) DEFAULT NULL,
- `public_key` blob NOT NULL,
- `private_key` blob,
- `avatar` text,
- `about` text,
- `profile_fields` text,
- `event_start_time` timestamp NULL DEFAULT NULL,
- `event_end_time` timestamp NULL DEFAULT NULL,
- `type` tinyint(3) unsigned NOT NULL DEFAULT '0',
- `member_count` int(10) unsigned NOT NULL DEFAULT '0',
- `tentative_member_count` int(10) unsigned NOT NULL DEFAULT '0',
- `ap_followers` varchar(300) DEFAULT NULL,
- `ap_wall` varchar(300) DEFAULT NULL,
- `last_updated` timestamp NULL DEFAULT NULL,
- PRIMARY KEY (`id`),
- UNIQUE KEY `username` (`username`,`domain`),
- UNIQUE KEY `ap_id` (`ap_id`),
- KEY `type` (`type`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ switch(target){
+ case 2 -> conn.createStatement().execute("ALTER TABLE wall_posts ADD (reply_count INTEGER UNSIGNED NOT NULL DEFAULT 0)");
+ case 3 -> conn.createStatement().execute("ALTER TABLE users ADD middle_name VARCHAR(100) DEFAULT NULL AFTER lname, ADD maiden_name VARCHAR(100) DEFAULT NULL AFTER middle_name");
+ case 4 -> {
+ conn.createStatement().execute("""
+ CREATE TABLE `groups` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(200) NOT NULL DEFAULT '',
+ `username` varchar(50) NOT NULL DEFAULT '',
+ `domain` varchar(100) NOT NULL DEFAULT '',
+ `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL,
+ `ap_url` varchar(300) DEFAULT NULL,
+ `ap_inbox` varchar(300) DEFAULT NULL,
+ `ap_shared_inbox` varchar(300) DEFAULT NULL,
+ `ap_outbox` varchar(300) DEFAULT NULL,
+ `public_key` blob NOT NULL,
+ `private_key` blob,
+ `avatar` text,
+ `about` text,
+ `profile_fields` text,
+ `event_start_time` timestamp NULL DEFAULT NULL,
+ `event_end_time` timestamp NULL DEFAULT NULL,
+ `type` tinyint(3) unsigned NOT NULL DEFAULT '0',
+ `member_count` int(10) unsigned NOT NULL DEFAULT '0',
+ `tentative_member_count` int(10) unsigned NOT NULL DEFAULT '0',
+ `ap_followers` varchar(300) DEFAULT NULL,
+ `ap_wall` varchar(300) DEFAULT NULL,
+ `last_updated` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `username` (`username`,`domain`),
+ UNIQUE KEY `ap_id` (`ap_id`),
+ KEY `type` (`type`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
- conn.createStatement().execute("""
- CREATE TABLE `group_admins` (
- `user_id` int(11) unsigned NOT NULL,
- `group_id` int(11) unsigned NOT NULL,
- `level` int(11) unsigned NOT NULL,
- `title` varchar(300) DEFAULT NULL,
- `display_order` int(10) unsigned NOT NULL DEFAULT '0',
- KEY `user_id` (`user_id`),
- KEY `group_id` (`group_id`),
- CONSTRAINT `group_admins_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
- CONSTRAINT `group_admins_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ conn.createStatement().execute("""
+ CREATE TABLE `group_admins` (
+ `user_id` int(11) unsigned NOT NULL,
+ `group_id` int(11) unsigned NOT NULL,
+ `level` int(11) unsigned NOT NULL,
+ `title` varchar(300) DEFAULT NULL,
+ `display_order` int(10) unsigned NOT NULL DEFAULT '0',
+ KEY `user_id` (`user_id`),
+ KEY `group_id` (`group_id`),
+ CONSTRAINT `group_admins_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `group_admins_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
- conn.createStatement().execute("""
- CREATE TABLE `group_memberships` (
- `user_id` int(11) unsigned NOT NULL,
- `group_id` int(11) unsigned NOT NULL,
- `post_feed_visibility` tinyint(4) unsigned NOT NULL DEFAULT '0',
- `tentative` tinyint(1) NOT NULL DEFAULT '0',
- `accepted` tinyint(1) NOT NULL DEFAULT '1',
- UNIQUE KEY `user_id` (`user_id`,`group_id`),
- KEY `group_id` (`group_id`),
- CONSTRAINT `group_memberships_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
- CONSTRAINT `group_memberships_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ conn.createStatement().execute("""
+ CREATE TABLE `group_memberships` (
+ `user_id` int(11) unsigned NOT NULL,
+ `group_id` int(11) unsigned NOT NULL,
+ `post_feed_visibility` tinyint(4) unsigned NOT NULL DEFAULT '0',
+ `tentative` tinyint(1) NOT NULL DEFAULT '0',
+ `accepted` tinyint(1) NOT NULL DEFAULT '1',
+ UNIQUE KEY `user_id` (`user_id`,`group_id`),
+ KEY `group_id` (`group_id`),
+ CONSTRAINT `group_memberships_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `group_memberships_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
- conn.createStatement().execute("ALTER TABLE users ADD `ap_wall` varchar(300) DEFAULT NULL");
- }else if(target==5){
- conn.createStatement().execute("""
- CREATE TABLE `blocks_group_domain` (
- `owner_id` int(10) unsigned NOT NULL,
- `domain` varchar(100) CHARACTER SET ascii NOT NULL,
- UNIQUE KEY `owner_id` (`owner_id`,`domain`),
- KEY `domain` (`domain`),
- CONSTRAINT `blocks_group_domain_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
- conn.createStatement().execute("""
- CREATE TABLE `blocks_group_user` (
- `owner_id` int(10) unsigned NOT NULL,
- `user_id` int(10) unsigned NOT NULL,
- UNIQUE KEY `owner_id` (`owner_id`,`user_id`),
- KEY `user_id` (`user_id`),
- CONSTRAINT `blocks_group_user_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE,
- CONSTRAINT `blocks_group_user_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
- conn.createStatement().execute("""
- CREATE TABLE `blocks_user_domain` (
- `owner_id` int(10) unsigned NOT NULL,
- `domain` varchar(100) CHARACTER SET ascii NOT NULL,
- UNIQUE KEY `owner_id` (`owner_id`,`domain`),
- KEY `domain` (`domain`),
- CONSTRAINT `blocks_user_domain_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
- conn.createStatement().execute("""
- CREATE TABLE `blocks_user_user` (
- `owner_id` int(10) unsigned NOT NULL,
- `user_id` int(10) unsigned NOT NULL,
- UNIQUE KEY `owner_id` (`owner_id`,`user_id`),
- KEY `user_id` (`user_id`),
- CONSTRAINT `blocks_user_user_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
- CONSTRAINT `blocks_user_user_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
- }else if(target==6){
- conn.createStatement().execute("""
+ conn.createStatement().execute("ALTER TABLE users ADD `ap_wall` varchar(300) DEFAULT NULL");
+ }
+ case 5 -> {
+ conn.createStatement().execute("""
+ CREATE TABLE `blocks_group_domain` (
+ `owner_id` int(10) unsigned NOT NULL,
+ `domain` varchar(100) CHARACTER SET ascii NOT NULL,
+ UNIQUE KEY `owner_id` (`owner_id`,`domain`),
+ KEY `domain` (`domain`),
+ CONSTRAINT `blocks_group_domain_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ conn.createStatement().execute("""
+ CREATE TABLE `blocks_group_user` (
+ `owner_id` int(10) unsigned NOT NULL,
+ `user_id` int(10) unsigned NOT NULL,
+ UNIQUE KEY `owner_id` (`owner_id`,`user_id`),
+ KEY `user_id` (`user_id`),
+ CONSTRAINT `blocks_group_user_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `blocks_group_user_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ conn.createStatement().execute("""
+ CREATE TABLE `blocks_user_domain` (
+ `owner_id` int(10) unsigned NOT NULL,
+ `domain` varchar(100) CHARACTER SET ascii NOT NULL,
+ UNIQUE KEY `owner_id` (`owner_id`,`domain`),
+ KEY `domain` (`domain`),
+ CONSTRAINT `blocks_user_domain_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ conn.createStatement().execute("""
+ CREATE TABLE `blocks_user_user` (
+ `owner_id` int(10) unsigned NOT NULL,
+ `user_id` int(10) unsigned NOT NULL,
+ UNIQUE KEY `owner_id` (`owner_id`,`user_id`),
+ KEY `user_id` (`user_id`),
+ CONSTRAINT `blocks_user_user_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `blocks_user_user_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ }
+ case 6 -> conn.createStatement().execute("""
CREATE TABLE `email_codes` (
`code` binary(64) NOT NULL,
`account_id` int(10) unsigned DEFAULT NULL,
@@ -151,122 +152,120 @@ PRIMARY KEY (`code`),
KEY `account_id` (`account_id`),
CONSTRAINT `email_codes_ibfk_1` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
- }else if(target==7){
- conn.createStatement().execute("ALTER TABLE accounts ADD `last_active` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP");
- }else if(target==8){
- conn.createStatement().execute("ALTER TABLE accounts ADD `ban_info` text DEFAULT NULL");
- }else if(target==9){
- conn.createStatement().execute("ALTER TABLE users ADD `ap_friends` varchar(300) DEFAULT NULL, ADD `ap_groups` varchar(300) DEFAULT NULL");
- }else if(target==10){
- conn.createStatement().execute("ALTER TABLE likes ADD `ap_id` varchar(300) DEFAULT NULL");
- conn.createStatement().execute("UPDATE likes SET object_type=0");
- }else if(target==11){
- conn.createStatement().execute("""
- CREATE TABLE `qsearch_index` (
- `string` text NOT NULL,
- `user_id` int(10) unsigned DEFAULT NULL,
- `group_id` int(10) unsigned DEFAULT NULL,
- KEY `user_id` (`user_id`),
- KEY `group_id` (`group_id`),
- FULLTEXT KEY `string` (`string`),
- CONSTRAINT `qsearch_index_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
- CONSTRAINT `qsearch_index_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=ascii;""");
- try(ResultSet res=conn.createStatement().executeQuery("SELECT id, fname, lname, middle_name, maiden_name, username, domain FROM users")){
- res.beforeFirst();
- PreparedStatement stmt=conn.prepareStatement("INSERT INTO qsearch_index (string, user_id) VALUES (?, ?)");
- while(res.next()){
- int id=res.getInt("id");
- String fname=res.getString("fname");
- String lname=res.getString("lname");
- String mname=res.getString("middle_name");
- String mdname=res.getString("maiden_name");
- String uname=res.getString("username");
- String domain=res.getString("domain");
- StringBuilder sb=new StringBuilder(Utils.transliterate(fname));
- if(lname!=null){
- sb.append(' ');
- sb.append(Utils.transliterate(lname));
- }
- if(mname!=null){
- sb.append(' ');
- sb.append(Utils.transliterate(mname));
- }
- if(mdname!=null){
+ case 7 -> conn.createStatement().execute("ALTER TABLE accounts ADD `last_active` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP");
+ case 8 -> conn.createStatement().execute("ALTER TABLE accounts ADD `ban_info` text DEFAULT NULL");
+ case 9 -> conn.createStatement().execute("ALTER TABLE users ADD `ap_friends` varchar(300) DEFAULT NULL, ADD `ap_groups` varchar(300) DEFAULT NULL");
+ case 10 -> {
+ conn.createStatement().execute("ALTER TABLE likes ADD `ap_id` varchar(300) DEFAULT NULL");
+ conn.createStatement().execute("UPDATE likes SET object_type=0");
+ }
+ case 11 -> {
+ conn.createStatement().execute("""
+ CREATE TABLE `qsearch_index` (
+ `string` text NOT NULL,
+ `user_id` int(10) unsigned DEFAULT NULL,
+ `group_id` int(10) unsigned DEFAULT NULL,
+ KEY `user_id` (`user_id`),
+ KEY `group_id` (`group_id`),
+ FULLTEXT KEY `string` (`string`),
+ CONSTRAINT `qsearch_index_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `qsearch_index_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=ascii;""");
+ try(ResultSet res=conn.createStatement().executeQuery("SELECT id, fname, lname, middle_name, maiden_name, username, domain FROM users")){
+ res.beforeFirst();
+ PreparedStatement stmt=conn.prepareStatement("INSERT INTO qsearch_index (string, user_id) VALUES (?, ?)");
+ while(res.next()){
+ int id=res.getInt("id");
+ String fname=res.getString("fname");
+ String lname=res.getString("lname");
+ String mname=res.getString("middle_name");
+ String mdname=res.getString("maiden_name");
+ String uname=res.getString("username");
+ String domain=res.getString("domain");
+ StringBuilder sb=new StringBuilder(Utils.transliterate(fname));
+ if(lname!=null){
+ sb.append(' ');
+ sb.append(Utils.transliterate(lname));
+ }
+ if(mname!=null){
+ sb.append(' ');
+ sb.append(Utils.transliterate(mname));
+ }
+ if(mdname!=null){
+ sb.append(' ');
+ sb.append(Utils.transliterate(mdname));
+ }
sb.append(' ');
- sb.append(Utils.transliterate(mdname));
+ sb.append(uname);
+ if(domain!=null){
+ sb.append(' ');
+ sb.append(domain);
+ }
+ stmt.setString(1, sb.toString());
+ stmt.setInt(2, id);
+ stmt.execute();
}
- sb.append(' ');
- sb.append(uname);
- if(domain!=null){
- sb.append(' ');
- sb.append(domain);
+ }
+ try(ResultSet res=conn.createStatement().executeQuery("SELECT id, name, username, domain FROM groups")){
+ res.beforeFirst();
+ PreparedStatement stmt=conn.prepareStatement("INSERT INTO qsearch_index (string, group_id) VALUES (?, ?)");
+ while(res.next()){
+ String s=Utils.transliterate(res.getString("name"))+" "+res.getString("username");
+ String domain=res.getString("domain");
+ if(domain!=null)
+ s+=" "+domain;
+ stmt.setString(1, s);
+ stmt.setInt(2, res.getInt("id"));
+ stmt.execute();
}
- stmt.setString(1, sb.toString());
- stmt.setInt(2, id);
- stmt.execute();
}
}
- try(ResultSet res=conn.createStatement().executeQuery("SELECT id, name, username, domain FROM groups")){
- res.beforeFirst();
- PreparedStatement stmt=conn.prepareStatement("INSERT INTO qsearch_index (string, group_id) VALUES (?, ?)");
- while(res.next()){
- String s=Utils.transliterate(res.getString("name"))+" "+res.getString("username");
- String domain=res.getString("domain");
- if(domain!=null)
- s+=" "+domain;
- stmt.setString(1, s);
- stmt.setInt(2, res.getInt("id"));
- stmt.execute();
- }
+ case 12 -> conn.createStatement().execute("ALTER TABLE wall_posts ADD `ap_replies` varchar(300) DEFAULT NULL");
+ case 13 -> {
+ conn.createStatement().execute("""
+ CREATE TABLE `polls` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `owner_id` int(10) unsigned NOT NULL,
+ `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL,
+ `question` text,
+ `is_anonymous` tinyint(1) NOT NULL DEFAULT '0',
+ `is_multi_choice` tinyint(1) NOT NULL DEFAULT '0',
+ `end_time` timestamp NULL DEFAULT NULL,
+ `num_voted_users` int(10) unsigned NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `ap_id` (`ap_id`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ conn.createStatement().execute("""
+ CREATE TABLE `poll_options` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `poll_id` int(10) unsigned NOT NULL,
+ `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL,
+ `text` text NOT NULL,
+ `num_votes` int(10) unsigned NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `ap_id` (`ap_id`),
+ KEY `poll_id` (`poll_id`),
+ CONSTRAINT `poll_options_ibfk_1` FOREIGN KEY (`poll_id`) REFERENCES `polls` (`id`) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ conn.createStatement().execute("""
+ CREATE TABLE `poll_votes` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` int(11) unsigned NOT NULL,
+ `poll_id` int(10) unsigned NOT NULL,
+ `option_id` int(10) unsigned NOT NULL,
+ `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `ap_id` (`ap_id`),
+ KEY `user_id` (`user_id`),
+ KEY `poll_id` (`poll_id`),
+ KEY `option_id` (`option_id`),
+ CONSTRAINT `poll_votes_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
+ CONSTRAINT `poll_votes_ibfk_2` FOREIGN KEY (`poll_id`) REFERENCES `polls` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `poll_votes_ibfk_3` FOREIGN KEY (`option_id`) REFERENCES `poll_options` (`id`) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ conn.createStatement().execute("ALTER TABLE wall_posts ADD `poll_id` int(10) unsigned DEFAULT NULL");
}
- }else if(target==12){
- conn.createStatement().execute("ALTER TABLE wall_posts ADD `ap_replies` varchar(300) DEFAULT NULL");
- }else if(target==13){
- conn.createStatement().execute("""
- CREATE TABLE `polls` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
- `owner_id` int(10) unsigned NOT NULL,
- `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL,
- `question` text,
- `is_anonymous` tinyint(1) NOT NULL DEFAULT '0',
- `is_multi_choice` tinyint(1) NOT NULL DEFAULT '0',
- `end_time` timestamp NULL DEFAULT NULL,
- `num_voted_users` int(10) unsigned NOT NULL DEFAULT '0',
- PRIMARY KEY (`id`),
- UNIQUE KEY `ap_id` (`ap_id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;""");
- conn.createStatement().execute("""
- CREATE TABLE `poll_options` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
- `poll_id` int(10) unsigned NOT NULL,
- `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL,
- `text` text NOT NULL,
- `num_votes` int(10) unsigned NOT NULL DEFAULT '0',
- PRIMARY KEY (`id`),
- UNIQUE KEY `ap_id` (`ap_id`),
- KEY `poll_id` (`poll_id`),
- CONSTRAINT `poll_options_ibfk_1` FOREIGN KEY (`poll_id`) REFERENCES `polls` (`id`) ON DELETE CASCADE
- ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;""");
- conn.createStatement().execute("""
- CREATE TABLE `poll_votes` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
- `user_id` int(11) unsigned NOT NULL,
- `poll_id` int(10) unsigned NOT NULL,
- `option_id` int(10) unsigned NOT NULL,
- `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL,
- PRIMARY KEY (`id`),
- UNIQUE KEY `ap_id` (`ap_id`),
- KEY `user_id` (`user_id`),
- KEY `poll_id` (`poll_id`),
- KEY `option_id` (`option_id`),
- CONSTRAINT `poll_votes_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
- CONSTRAINT `poll_votes_ibfk_2` FOREIGN KEY (`poll_id`) REFERENCES `polls` (`id`) ON DELETE CASCADE,
- CONSTRAINT `poll_votes_ibfk_3` FOREIGN KEY (`option_id`) REFERENCES `poll_options` (`id`) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
- conn.createStatement().execute("ALTER TABLE wall_posts ADD `poll_id` int(10) unsigned DEFAULT NULL");
- }else if(target==14){
- conn.createStatement().execute("""
+ case 14 -> conn.createStatement().execute("""
CREATE TABLE `newsfeed_comments` (
`user_id` int(10) unsigned NOT NULL,
`object_type` int(10) unsigned NOT NULL,
@@ -277,10 +276,82 @@ PRIMARY KEY (`object_type`,`object_id`,`user_id`),
KEY `last_comment_time` (`last_comment_time`),
CONSTRAINT `newsfeed_comments_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
- }else if(target==15){
- conn.createStatement().execute("ALTER TABLE `wall_posts` ADD `federation_state` tinyint unsigned NOT NULL DEFAULT 0");
- }else if(target==16){
- conn.createStatement().execute("ALTER TABLE `wall_posts` ADD `source` text DEFAULT NULL, ADD `source_format` tinyint unsigned DEFAULT NULL");
+ case 15 -> conn.createStatement().execute("ALTER TABLE `wall_posts` ADD `federation_state` tinyint unsigned NOT NULL DEFAULT 0");
+ case 16 -> conn.createStatement().execute("ALTER TABLE `wall_posts` ADD `source` text DEFAULT NULL, ADD `source_format` tinyint unsigned DEFAULT NULL");
+ case 17 -> {
+ conn.createStatement().execute("ALTER TABLE `users` ADD `about_source` TEXT NULL AFTER `about`");
+ conn.createStatement().execute("ALTER TABLE `groups` ADD `about_source` TEXT NULL AFTER `about`");
+ conn.createStatement().execute("ALTER TABLE `friend_requests` ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST");
+ }
+ case 18 -> conn.createStatement().execute("ALTER TABLE `groups` ADD `flags` BIGINT UNSIGNED NOT NULL DEFAULT 0");
+ case 19 -> conn.createStatement().execute("""
+ CREATE TABLE `group_invites` (
+ `id` int unsigned NOT NULL AUTO_INCREMENT,
+ `inviter_id` int unsigned NOT NULL,
+ `invitee_id` int unsigned NOT NULL,
+ `group_id` int unsigned NOT NULL,
+ `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `is_event` tinyint(1) NOT NULL,
+ `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `ap_id` (`ap_id`),
+ KEY `inviter_id` (`inviter_id`),
+ KEY `invitee_id` (`invitee_id`),
+ KEY `group_id` (`group_id`),
+ KEY `is_event` (`is_event`),
+ CONSTRAINT `group_invites_ibfk_1` FOREIGN KEY (`inviter_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `group_invites_ibfk_2` FOREIGN KEY (`invitee_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `group_invites_ibfk_3` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""");
+ case 20 -> {
+ // Make room for a new column
+ conn.createStatement().execute("ALTER TABLE `users` DROP KEY `ap_outbox`");
+ conn.createStatement().execute("ALTER TABLE `users` CHANGE `ap_outbox` `ap_outbox` TEXT");
+ conn.createStatement().execute("ALTER TABLE `groups` CHANGE `ap_outbox` `ap_outbox` TEXT");
+ // Then add the new column
+ conn.createStatement().execute("ALTER TABLE `users` ADD `endpoints` json DEFAULT NULL");
+ conn.createStatement().execute("ALTER TABLE `groups` ADD `endpoints` json DEFAULT NULL");
+ PreparedStatement stmt=conn.prepareStatement("UPDATE `users` SET `endpoints`=? WHERE `id`=?");
+ try(ResultSet res=conn.createStatement().executeQuery("SELECT `id`,`ap_outbox`,`ap_followers`,`ap_following`,`ap_wall`,`ap_friends`,`ap_groups` FROM `users` WHERE `ap_id` IS NOT NULL")){
+ res.beforeFirst();
+ while(res.next()){
+ int id=res.getInt(1);
+ Actor.EndpointsStorageWrapper ep=new Actor.EndpointsStorageWrapper();
+ ep.outbox=res.getString(2);
+ ep.followers=res.getString(3);
+ ep.following=res.getString(4);
+ ep.wall=res.getString(5);
+ ep.friends=res.getString(6);
+ ep.groups=res.getString(7);
+ stmt.setString(1, Utils.gson.toJson(ep));
+ stmt.setInt(2, id);
+ stmt.execute();
+ }
+ }
+ stmt=conn.prepareStatement("UPDATE `groups` SET `endpoints`=? WHERE `id`=?");
+ try(ResultSet res=conn.createStatement().executeQuery("SELECT `id`,`ap_outbox`,`ap_followers`,`ap_wall` FROM `groups` WHERE `ap_id` IS NOT NULL")){
+ res.beforeFirst();
+ while(res.next()){
+ int id=res.getInt(1);
+ Actor.EndpointsStorageWrapper ep=new Actor.EndpointsStorageWrapper();
+ ep.outbox=res.getString(2);
+ ep.followers=res.getString(3);
+ ep.wall=res.getString(4);
+ stmt.setString(1, Utils.gson.toJson(ep));
+ stmt.setInt(2, id);
+ stmt.execute();
+ }
+ }
+ conn.createStatement().execute("ALTER TABLE `users` DROP `ap_outbox`, DROP `ap_followers`, DROP `ap_following`, DROP `ap_wall`, DROP `ap_friends`, DROP `ap_groups`");
+ conn.createStatement().execute("ALTER TABLE `groups` DROP `ap_outbox`, DROP `ap_followers`, DROP `ap_wall`");
+ conn.createStatement().execute("ALTER TABLE `groups` ADD `access_type` tinyint NOT NULL DEFAULT '0'");
+ }
+ case 21 -> conn.createStatement().execute("ALTER TABLE `group_memberships` ADD `time` timestamp DEFAULT CURRENT_TIMESTAMP()");
+ case 22 -> conn.createStatement().execute("ALTER TABLE `polls` CHANGE `owner_id` `owner_id` int NOT NULL");
+ case 23 -> {
+ conn.createStatement().execute("ALTER TABLE `polls` ADD `last_vote_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()");
+ conn.createStatement().execute("CREATE INDEX `poll_id` ON `wall_posts` (`poll_id`)");
+ }
}
}
}
diff --git a/src/main/java/smithereen/storage/DatabaseUtils.java b/src/main/java/smithereen/storage/DatabaseUtils.java
index c742b3c2..c435896b 100644
--- a/src/main/java/smithereen/storage/DatabaseUtils.java
+++ b/src/main/java/smithereen/storage/DatabaseUtils.java
@@ -1,9 +1,13 @@
package smithereen.storage;
import java.sql.Connection;
+import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;
@@ -38,6 +42,17 @@ public static int oneFieldToInt(final ResultSet res) throws SQLException{
}
}
+ public static T oneFieldToObject(final ResultSet res, Class type) throws SQLException{
+ try(res){
+ return res.first() ? res.getObject(1, type) : null;
+ }
+ }
+
+ public static F oneFieldToObject(ResultSet res, Class initialType, Function converter) throws SQLException{
+ T obj=oneFieldToObject(res, initialType);
+ return obj==null ? null : converter.apply(obj);
+ }
+
public static boolean runWithUniqueUsername(String username, DatabaseRunnable action) throws SQLException{
if(!Utils.isValidUsername(username))
return false;
@@ -83,6 +98,7 @@ public boolean tryAdvance(IntConsumer action){
action.accept(res.getInt(1));
return true;
}
+ res.close();
}catch(SQLException x){
throw new UncheckedSQLException(x);
}
@@ -95,6 +111,7 @@ public void forEachRemaining(IntConsumer action){
while(res.next()){
action.accept(res.getInt(1));
}
+ res.close();
}catch(SQLException x){
throw new UncheckedSQLException(x);
}
@@ -115,6 +132,8 @@ public boolean tryAdvance(Consumer super T> action){
if(res.next()){
action.accept(creator.deserialize(res));
return true;
+ }else{
+ res.close();
}
}catch(SQLException x){
throw new UncheckedSQLException(x);
@@ -128,6 +147,7 @@ public void forEachRemaining(Consumer super T> action){
while(res.next()){
action.accept(creator.deserialize(res));
}
+ res.close();
}catch(SQLException x){
throw new UncheckedSQLException(x);
}
@@ -138,6 +158,32 @@ public void forEachRemaining(Consumer super T> action){
}
}
+ public static Instant getInstant(ResultSet res, String name) throws SQLException{
+ Timestamp ts=res.getTimestamp(name);
+ return ts==null ? null : ts.toInstant();
+ }
+
+ public static LocalDate getLocalDate(ResultSet res, String name) throws SQLException{
+ Date date=res.getDate(name);
+ return date==null ? null : date.toLocalDate();
+ }
+
+ public static LocalDate getLocalDate(ResultSet res, int index) throws SQLException{
+ Date date=res.getDate(index);
+ return date==null ? null : date.toLocalDate();
+ }
+
+ public static void doWithTransaction(Connection conn, SQLRunnable r) throws SQLException{
+ boolean success=false;
+ try{
+ conn.createStatement().execute("START TRANSACTION");
+ r.run();
+ success=true;
+ }finally{
+ conn.createStatement().execute(success ? "COMMIT" : "ROLLBACK");
+ }
+ }
+
private static class UncheckedSQLException extends RuntimeException{
public UncheckedSQLException(SQLException cause){
super(cause);
@@ -148,4 +194,9 @@ public synchronized SQLException getCause(){
return (SQLException) super.getCause();
}
}
+
+ @FunctionalInterface
+ public interface SQLRunnable{
+ void run() throws SQLException;
+ }
}
diff --git a/src/main/java/smithereen/storage/GroupStorage.java b/src/main/java/smithereen/storage/GroupStorage.java
index 71594fdb..1cbd02e6 100644
--- a/src/main/java/smithereen/storage/GroupStorage.java
+++ b/src/main/java/smithereen/storage/GroupStorage.java
@@ -12,6 +12,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -19,20 +20,26 @@
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.function.Function;
import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import smithereen.Config;
import smithereen.LruCache;
import smithereen.Utils;
import smithereen.activitypub.ContextCollector;
+import smithereen.controllers.GroupsController;
import smithereen.data.ForeignGroup;
import smithereen.data.Group;
import smithereen.data.GroupAdmin;
-import smithereen.data.ListAndTotal;
+import smithereen.data.GroupInvitation;
+import smithereen.data.PaginatedList;
import smithereen.data.User;
+import smithereen.data.UserNotifications;
import spark.utils.StringUtils;
public class GroupStorage{
@@ -43,7 +50,7 @@ public class GroupStorage{
private static final Object adminUpdateLock=new Object();
- public static int createGroup(String name, String username, int userID) throws SQLException{
+ public static int createGroup(String name, String description, String descriptionSrc, int userID, boolean isEvent, Instant eventStart, Instant eventEnd) throws SQLException{
int id;
Connection conn=DatabaseConnectionManager.getConnection();
try{
@@ -53,15 +60,33 @@ public static int createGroup(String name, String username, int userID) throws S
kpg.initialize(2048);
KeyPair pair=kpg.generateKeyPair();
- PreparedStatement stmt=new SQLQueryBuilder(conn)
- .insertInto("groups")
- .value("name", name)
- .value("username", username)
- .value("public_key", pair.getPublic().getEncoded())
- .value("private_key", pair.getPrivate().getEncoded())
- .value("member_count", 1)
- .createStatement(Statement.RETURN_GENERATED_KEYS);
- id=DatabaseUtils.insertAndGetID(stmt);
+ PreparedStatement stmt;
+ String username;
+ synchronized(GroupStorage.class){
+ SQLQueryBuilder bldr=new SQLQueryBuilder(conn)
+ .insertInto("groups")
+ .value("name", name)
+ .value("username", "__tmp"+System.currentTimeMillis())
+ .value("public_key", pair.getPublic().getEncoded())
+ .value("private_key", pair.getPrivate().getEncoded())
+ .value("member_count", 1)
+ .value("about", description)
+ .value("about_source", descriptionSrc)
+ .value("event_start_time", eventStart)
+ .value("event_end_time", eventEnd)
+ .value("type", isEvent ? Group.Type.EVENT : Group.Type.GROUP);
+
+ stmt=bldr.createStatement(Statement.RETURN_GENERATED_KEYS);
+ id=DatabaseUtils.insertAndGetID(stmt);
+ username=(isEvent ? "event" : "club")+id;
+
+ new SQLQueryBuilder(conn)
+ .update("groups")
+ .value("username", username)
+ .where("id=?", id)
+ .createStatement()
+ .execute();
+ }
new SQLQueryBuilder(conn)
.insertInto("group_memberships")
@@ -119,11 +144,15 @@ public static synchronized void putOrUpdateForeignGroup(ForeignGroup group) thro
.value("ap_url", Objects.toString(group.url, null))
.value("ap_inbox", group.inbox.toString())
.value("ap_shared_inbox", Objects.toString(group.sharedInbox, null))
- .value("ap_outbox", Objects.toString(group.outbox, null))
.value("public_key", group.publicKey.getEncoded())
.value("avatar", group.hasAvatar() ? group.icon.get(0).asActivityPubObject(new JsonObject(), new ContextCollector()).toString() : null)
- .value("ap_followers", Objects.toString(group.followers, null))
- .value("ap_wall", Objects.toString(group.getWallURL(), null))
+ .value("event_start_time", group.eventStartTime)
+ .value("event_end_time", group.eventEndTime)
+ .value("type", group.type)
+ .value("flags", Utils.serializeEnumSet(group.capabilities, ForeignGroup.Capability.class))
+ .value("access_type", group.accessType)
+ .value("endpoints", group.serializeEndpoints())
+ .value("about", group.summary)
.valueExpr("last_updated", "CURRENT_TIMESTAMP()");
stmt=builder.createStatement(Statement.RETURN_GENERATED_KEYS);
@@ -145,7 +174,7 @@ public static synchronized void putOrUpdateForeignGroup(ForeignGroup group) thro
.createStatement()
.execute();
}
- putIntoCache(group);
+ removeFromCache(group);
synchronized(adminUpdateLock){
stmt=new SQLQueryBuilder(conn)
.selectFrom("group_admins")
@@ -311,30 +340,32 @@ public static Map getById(Collection _ids) throws SQLEx
}
}
- public static List getRandomMembersForProfile(int groupID) throws SQLException{
+ public static List getRandomMembersForProfile(int groupID, boolean tentative) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
- PreparedStatement stmt=conn.prepareStatement("SELECT user_id FROM group_memberships WHERE group_id=? ORDER BY RAND() LIMIT 6");
- stmt.setInt(1, groupID);
+ PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT user_id FROM group_memberships WHERE group_id=? AND tentative=? AND accepted=1 ORDER BY RAND() LIMIT 6", groupID, tentative);
try(ResultSet res=stmt.executeQuery()){
return UserStorage.getByIdAsList(DatabaseUtils.intResultSetToList(res));
}
}
- public static ListAndTotal getMembers(int groupID, int offset, int count) throws SQLException{
+ public static PaginatedList getMembers(int groupID, int offset, int count, @Nullable Boolean tentative) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
+ String _tentative=tentative==null ? "" : (" AND tentative="+(tentative ? '1' : '0'));
PreparedStatement stmt=new SQLQueryBuilder(conn)
.selectFrom("group_memberships")
.count()
- .where("group_id=? AND accepted=1", groupID)
+ .where("group_id=? AND accepted=1"+_tentative, groupID)
.createStatement();
int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery());
+ if(total==0)
+ return PaginatedList.emptyList(count);
stmt=new SQLQueryBuilder(conn)
.selectFrom("group_memberships")
- .where("group_id=? AND accepted=1", groupID)
+ .where("group_id=? AND accepted=1"+_tentative, groupID)
.limit(count, offset)
.createStatement();
try(ResultSet res=stmt.executeQuery()){
- return new ListAndTotal<>(UserStorage.getByIdAsList(DatabaseUtils.intResultSetToList(res)), total);
+ return new PaginatedList<>(UserStorage.getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count);
}
}
@@ -344,9 +375,13 @@ public static Group.MembershipState getUserMembershipState(int groupID, int user
stmt.setInt(1, groupID);
stmt.setInt(2, userID);
try(ResultSet res=stmt.executeQuery()){
- if(!res.first())
- return Group.MembershipState.NONE;
- return Group.MembershipState.MEMBER;
+ if(!res.first()){
+ stmt=new SQLQueryBuilder(conn).selectFrom("group_invites").count().where("group_id=? AND invitee_id=?", groupID, userID).createStatement();
+ return DatabaseUtils.oneFieldToInt(stmt.executeQuery())==0 ? Group.MembershipState.NONE : Group.MembershipState.INVITED;
+ }
+ if(!res.getBoolean("accepted"))
+ return Group.MembershipState.REQUESTED;
+ return res.getBoolean("tentative") ? Group.MembershipState.TENTATIVE_MEMBER : Group.MembershipState.MEMBER;
}
}
@@ -364,13 +399,17 @@ public static void joinGroup(Group group, int userID, boolean tentative, boolean
.createStatement()
.execute();
- String memberCountField=tentative ? "tentative_member_count" : "member_count";
- new SQLQueryBuilder(conn)
- .update("groups")
- .valueExpr(memberCountField, memberCountField+"+1")
- .where("id=?", group.id)
- .createStatement()
- .execute();
+ if(accepted){
+ String memberCountField=tentative ? "tentative_member_count" : "member_count";
+ new SQLQueryBuilder(conn)
+ .update("groups")
+ .valueExpr(memberCountField, memberCountField+"+1")
+ .where("id=?", group.id)
+ .createStatement()
+ .execute();
+ }
+
+ deleteInvitation(userID, group.id, group.isEvent());
removeFromCache(group);
@@ -380,24 +419,49 @@ public static void joinGroup(Group group, int userID, boolean tentative, boolean
}
}
- public static void leaveGroup(Group group, int userID, boolean tentative) throws SQLException{
+ public static void updateUserEventDecision(Group group, int userID, boolean tentative) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
- conn.createStatement().execute("START TRANSACTION");
- boolean success=false;
- try{
+ DatabaseUtils.doWithTransaction(conn, ()->{
new SQLQueryBuilder(conn)
- .deleteFrom("group_memberships")
+ .update("group_memberships")
.where("user_id=? AND group_id=?", userID, group.id)
+ .value("tentative", tentative)
.createStatement()
.execute();
- String memberCountField=tentative ? "tentative_member_count" : "member_count";
+ String memberCountFieldOld=tentative ? "member_count" : "tentative_member_count";
+ String memberCountFieldNew=tentative ? "tentative_member_count" : "member_count";
new SQLQueryBuilder(conn)
.update("groups")
- .valueExpr(memberCountField, memberCountField+"-1")
+ .valueExpr(memberCountFieldOld, memberCountFieldOld+"-1")
+ .valueExpr(memberCountFieldNew, memberCountFieldNew+"+1")
.where("id=?", group.id)
.createStatement()
.execute();
+ removeFromCache(group);
+ });
+ }
+
+ public static void leaveGroup(Group group, int userID, boolean tentative, boolean wasAccepted) throws SQLException{
+ Connection conn=DatabaseConnectionManager.getConnection();
+ conn.createStatement().execute("START TRANSACTION");
+ boolean success=false;
+ try{
+ new SQLQueryBuilder(conn)
+ .deleteFrom("group_memberships")
+ .where("user_id=? AND group_id=?", userID, group.id)
+ .createStatement()
+ .execute();
+
+ if(wasAccepted){
+ String memberCountField=tentative ? "tentative_member_count" : "member_count";
+ new SQLQueryBuilder(conn)
+ .update("groups")
+ .valueExpr(memberCountField, "GREATEST(0, CAST("+memberCountField+" AS SIGNED)-1)")
+ .where("id=?", group.id)
+ .createStatement()
+ .execute();
+ }
removeFromCache(group);
@@ -407,22 +471,51 @@ public static void leaveGroup(Group group, int userID, boolean tentative) throws
}
}
- public static List getUserGroups(int userID) throws SQLException{
- PreparedStatement stmt=new SQLQueryBuilder().selectFrom("group_memberships").columns("group_id").where("user_id=? AND accepted=1", userID).createStatement();
+ public static PaginatedList getUserGroups(int userID, int offset, int count, boolean includePrivate) throws SQLException{
+ Connection conn=DatabaseConnectionManager.getConnection();
+ String query="SELECT %s FROM group_memberships JOIN `groups` ON group_id=`groups`.id WHERE user_id=? AND accepted=1 AND `groups`.`type`=0";
+ if(!includePrivate){
+ query+=" AND `groups`.`access_type`<>2";
+ }
+ PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, String.format(Locale.US, query, "COUNT(*)"), userID);
+ int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery());
+ if(total==0)
+ return new PaginatedList<>(Collections.emptyList(), 0, 0, count);
+ query+=" ORDER BY group_id ASC LIMIT ? OFFSET ?";
+ stmt=SQLQueryBuilder.prepareStatement(conn, String.format(Locale.US, query, "group_id"), userID, count, offset);
+ try(ResultSet res=stmt.executeQuery()){
+ return new PaginatedList<>(getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count);
+ }
+ }
+
+ public static PaginatedList getUserEvents(int userID, GroupsController.EventsType type, int offset, int count) throws SQLException{
+ String query="SELECT %s FROM group_memberships JOIN `groups` ON group_id=`groups`.id WHERE user_id=? AND accepted=1 AND `groups`.`type`=1";
+ query+=switch(type){
+ case PAST -> " AND event_start_time<=CURRENT_TIMESTAMP()";
+ case FUTURE -> " AND event_start_time>CURRENT_TIMESTAMP()";
+ case ALL -> "";
+ };
+ Connection conn=DatabaseConnectionManager.getConnection();
+ PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, String.format(Locale.US, query, "COUNT(*)"), userID);
+ int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery());
+ if(total==0)
+ return PaginatedList.emptyList(count);
+ query+=" ORDER BY event_start_time "+(type==GroupsController.EventsType.PAST ? "DESC" : "ASC")+" LIMIT ? OFFSET ?";
+ stmt=SQLQueryBuilder.prepareStatement(conn, String.format(Locale.US, query, "group_id"), userID, count, offset);
try(ResultSet res=stmt.executeQuery()){
- return getByIdAsList(DatabaseUtils.intResultSetToList(res));
+ return new PaginatedList<>(getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count);
}
}
- public static ListAndTotal getUserGroupIDs(int userID, int offset, int count) throws SQLException{
+ public static PaginatedList getUserGroupIDs(int userID, int offset, int count) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
- PreparedStatement stmt=conn.prepareStatement("SELECT count(*) FROM group_memberships WHERE user_id=? AND accepted=1");
+ PreparedStatement stmt=conn.prepareStatement("SELECT count(*) FROM group_memberships JOIN `groups` ON group_id=`groups`.id WHERE user_id=? AND accepted=1 AND `groups`.`type`=0 AND `groups`.`access_type`<>2");
stmt.setInt(1, userID);
int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery());
if(total==0)
- return new ListAndTotal<>(Collections.emptyList(), 0);
+ return new PaginatedList<>(Collections.emptyList(), 0);
- stmt=conn.prepareStatement("SELECT group_id, ap_id FROM group_memberships JOIN groups ON group_id=id WHERE user_id=? AND accepted=1 LIMIT ? OFFSET ?");
+ stmt=conn.prepareStatement("SELECT group_id, ap_id FROM group_memberships JOIN groups ON group_id=id WHERE user_id=? AND accepted=1 AND `groups`.`type`=0 AND `groups`.`access_type`<>2 LIMIT ? OFFSET ?");
stmt.setInt(1, userID);
stmt.setInt(2, count);
stmt.setInt(3, offset);
@@ -433,27 +526,31 @@ public static ListAndTotal getUserGroupIDs(int userID, int offset, int coun
String apID=res.getString(2);
list.add(apID!=null ? URI.create(apID) : Config.localURI("/groups/"+res.getInt(1)));
}
- return new ListAndTotal<>(list, total);
+ return new PaginatedList<>(list, total);
}
}
- public static List getUserManagedGroups(int userID) throws SQLException{
- PreparedStatement stmt=new SQLQueryBuilder().selectFrom("group_admins").columns("group_id").where("user_id=?", userID).createStatement();
+ public static PaginatedList getUserManagedGroups(int userID, int offset, int count) throws SQLException{
+ Connection conn=DatabaseConnectionManager.getConnection();
+ PreparedStatement stmt=new SQLQueryBuilder(conn)
+ .selectFrom("group_admins")
+ .count()
+ .where("user_id=?", userID)
+ .createStatement();
+ int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery());
+ if(total==0)
+ return PaginatedList.emptyList(count);
+ stmt=new SQLQueryBuilder(conn).selectFrom("group_admins").columns("group_id").where("user_id=?", userID).createStatement();
try(ResultSet res=stmt.executeQuery()){
- return getByIdAsList(DatabaseUtils.intResultSetToList(res));
+ return new PaginatedList<>(getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count);
}
}
- public static List getGroupMemberURIs(int groupID, boolean tentative, int offset, int count, int[] total) throws SQLException{
+ public static PaginatedList getGroupMemberURIs(int groupID, boolean tentative, int offset, int count) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
PreparedStatement stmt;
- if(total!=null){
- stmt=new SQLQueryBuilder(conn).selectFrom("group_memberships").count().where("group_id=? AND accepted=1 AND tentative=?", groupID, tentative).createStatement();
- try(ResultSet res=stmt.executeQuery()){
- res.first();
- total[0]=res.getInt(1);
- }
- }
+ stmt=new SQLQueryBuilder(conn).selectFrom("group_memberships").count().where("group_id=? AND accepted=1 AND tentative=?", groupID, tentative).createStatement();
+ int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery());
if(count>0){
stmt=conn.prepareStatement("SELECT `ap_id`,`id` FROM `group_memberships` INNER JOIN `users` ON `users`.`id`=`user_id` WHERE `group_id`=? AND `accepted`=1 AND tentative=? LIMIT ? OFFSET ?");
stmt.setInt(1, groupID);
@@ -473,9 +570,9 @@ public static List getGroupMemberURIs(int groupID, boolean tentative, int o
}while(res.next());
}
}
- return list;
+ return new PaginatedList<>(list, total, offset, count);
}
- return Collections.emptyList();
+ return new PaginatedList<>(Collections.emptyList(), total, offset, count);
}
public static List getGroupMemberInboxes(int groupID) throws SQLException{
@@ -503,13 +600,36 @@ public static Group.AdminLevel getGroupMemberAdminLevel(int groupID, int userID)
}
}
- public static void setMemberAccepted(int groupID, int userID, boolean accepted) throws SQLException{
- new SQLQueryBuilder()
+ public static void setMemberAccepted(Group group, int userID, boolean accepted) throws SQLException{
+ int groupID=group.id;
+ Connection conn=DatabaseConnectionManager.getConnection();
+ PreparedStatement stmt=new SQLQueryBuilder(conn)
+ .selectFrom("group_memberships")
+ .columns("tentative")
+ .where("group_id=? AND user_id=? AND accepted=?", groupID, userID, !accepted)
+ .createStatement();
+ boolean tentative;
+ try(ResultSet res=stmt.executeQuery()){
+ if(!res.first())
+ return;
+ tentative=res.getBoolean(1);
+ }
+
+ new SQLQueryBuilder(conn)
.update("group_memberships")
.value("accepted", accepted)
.where("group_id=? AND user_id=?", groupID, userID)
.createStatement()
.execute();
+
+ String memberCountField=tentative ? "tentative_member_count" : "member_count";
+ new SQLQueryBuilder(conn)
+ .update("groups")
+ .valueExpr(memberCountField, "GREATEST(0, CAST("+memberCountField+" AS SIGNED)"+(accepted ? "+1" : "-1")+")")
+ .where("id=?", groupID)
+ .createStatement()
+ .execute();
+ removeFromCache(group);
}
public static List getGroupAdmins(int groupID) throws SQLException{
@@ -660,11 +780,16 @@ public static void updateProfilePicture(Group group, String serializedPic) throw
}
}
- public static void updateGroupGeneralInfo(Group group, String name, String about) throws SQLException{
+ public static void updateGroupGeneralInfo(Group group, String name, String username, String aboutSrc, String about, Instant eventStart, Instant eventEnd, Group.AccessType accessType) throws SQLException{
new SQLQueryBuilder()
.update("groups")
.value("name", name)
+ .value("username", username)
+ .value("about_source", aboutSrc)
.value("about", about)
+ .value("event_start_time", eventStart)
+ .value("event_end_time", eventEnd)
+ .value("access_type", accessType)
.where("id=?", group.id)
.createStatement()
.execute();
@@ -798,4 +923,263 @@ static String getQSearchStringForGroup(Group group){
s+=" "+group.domain;
return s;
}
+
+ public static List getUserEventsInTimeRange(int userID, Instant from, Instant to) throws SQLException{
+ Connection conn=DatabaseConnectionManager.getConnection();
+ PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT group_id, event_start_time FROM group_memberships JOIN `groups` ON `groups`.id=group_memberships.group_id WHERE user_id=? AND accepted=1 AND `groups`.type=1 AND event_start_time>=? AND event_start_time", userID, from, to);
+ return getByIdAsList(DatabaseUtils.intResultSetToList(stmt.executeQuery()));
+ }
+
+ public static IntStream getAllMembersAsStream(int groupID) throws SQLException{
+ PreparedStatement stmt=new SQLQueryBuilder()
+ .selectFrom("group_memberships")
+ .columns("user_id")
+ .where("accepted=1 AND group_id=?", groupID)
+ .createStatement();
+ return DatabaseUtils.intResultSetToStream(stmt.executeQuery());
+ }
+
+ public static int putInvitation(int groupID, int inviterID, int inviteeID, boolean isEvent, String apID) throws SQLException{
+ PreparedStatement stmt=new SQLQueryBuilder()
+ .insertInto("group_invites")
+ .value("group_id", groupID)
+ .value("inviter_id", inviterID)
+ .value("invitee_id", inviteeID)
+ .value("is_event", isEvent)
+ .value("ap_id", apID)
+ .createStatement(Statement.RETURN_GENERATED_KEYS);
+ stmt.execute();
+ return DatabaseUtils.oneFieldToInt(stmt.getGeneratedKeys());
+ }
+
+ public static List getUserInvitations(int userID, boolean isEvent, int offset, int count) throws SQLException{
+ PreparedStatement stmt=new SQLQueryBuilder()
+ .selectFrom("group_invites")
+ .columns("group_id", "inviter_id")
+ .where("invitee_id=? AND is_event=?", userID, isEvent)
+ .limit(count, offset)
+ .createStatement();
+ ArrayList ids=new ArrayList<>();
+ try(ResultSet res=stmt.executeQuery()){
+ while(res.next()){
+ ids.add(new IdPair(res.getInt(1), res.getInt(2)));
+ }
+ }
+ Set needGroups=ids.stream().map(IdPair::first).collect(Collectors.toSet());
+ Set needUsers=ids.stream().map(IdPair::second).collect(Collectors.toSet());
+ Map groups=getById(needGroups);
+ Map users=UserStorage.getById(needUsers);
+ // All groups and users must exist, this is taken care of by schema constraints
+ return ids.stream().map(i->new GroupInvitation(groups.get(i.first()), users.get(i.second()))).collect(Collectors.toList());
+ }
+
+ public static URI getInvitationApID(int userID, int groupID) throws SQLException{
+ PreparedStatement stmt=new SQLQueryBuilder()
+ .selectFrom("group_invites")
+ .columns("ap_id")
+ .where("invitee_id=? AND group_id=?", userID, groupID)
+ .createStatement();
+ return DatabaseUtils.oneFieldToObject(stmt.executeQuery(), String.class, URI::create);
+ }
+
+ public static int deleteInvitation(int userID, int groupID, boolean isEvent) throws SQLException{
+ Connection conn=DatabaseConnectionManager.getConnection();
+ PreparedStatement stmt=new SQLQueryBuilder(conn)
+ .selectFrom("group_invites")
+ .columns("id")
+ .where("invitee_id=? AND group_id=?", userID, groupID)
+ .createStatement();
+ int id=DatabaseUtils.oneFieldToInt(stmt.executeQuery());
+ if(id<1)
+ return id;
+ stmt=new SQLQueryBuilder(conn)
+ .deleteFrom("group_invites")
+ .where("id=?", id)
+ .createStatement();
+ int count=stmt.executeUpdate();
+ if(count>0){
+ UserNotifications notifications=NotificationsStorage.getNotificationsFromCache(userID);
+ if(notifications!=null){
+ if(isEvent)
+ notifications.incNewEventInvitationsCount(-count);
+ else
+ notifications.incNewGroupInvitationsCount(-count);
+ }
+ }
+ return id;
+ }
+
+ public static PaginatedList getGroupJoinRequests(int groupID, int offset, int count) throws SQLException{
+ int total=getJoinRequestCount(groupID);
+ if(total==0)
+ return PaginatedList.emptyList(count);
+ PreparedStatement stmt=new SQLQueryBuilder()
+ .selectFrom("group_memberships")
+ .columns("user_id")
+ .where("group_id=? AND accepted=0", groupID)
+ .orderBy("time DESC")
+ .limit(count, offset)
+ .createStatement();
+ return new PaginatedList<>(UserStorage.getByIdAsList(DatabaseUtils.intResultSetToList(stmt.executeQuery())), total, offset, count);
+ }
+
+ public static int getJoinRequestCount(int groupID) throws SQLException{
+ PreparedStatement stmt=new SQLQueryBuilder()
+ .selectFrom("group_memberships")
+ .count()
+ .where("group_id=? AND accepted=0", groupID)
+ .createStatement();
+ return DatabaseUtils.oneFieldToInt(stmt.executeQuery());
+ }
+
+ public static PaginatedList getGroupInvitations(int groupID, int offset, int count) throws SQLException{
+ Connection conn=DatabaseConnectionManager.getConnection();
+ PreparedStatement stmt=new SQLQueryBuilder(conn)
+ .selectFrom("group_invites")
+ .count()
+ .where("group_id=?", groupID)
+ .createStatement();
+ int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery());
+ if(total==0)
+ return PaginatedList.emptyList(count);
+ stmt=new SQLQueryBuilder(conn)
+ .selectFrom("group_invites")
+ .columns("invitee_id")
+ .where("group_id=?", groupID)
+ .orderBy("id DESC")
+ .limit(count, offset)
+ .createStatement();
+ return new PaginatedList<>(UserStorage.getByIdAsList(DatabaseUtils.intResultSetToList(stmt.executeQuery())), total, offset, count);
+ }
+
+ public static boolean areThereGroupMembersWithDomain(int groupID, String domain) throws SQLException{
+ Connection conn=DatabaseConnectionManager.getConnection();
+ PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT COUNT(*) FROM `group_memberships` JOIN `users` ON user_id=`users`.id WHERE group_id=? AND accepted=1 AND `users`.`domain`=?", groupID, domain);
+ return DatabaseUtils.oneFieldToInt(stmt.executeQuery())>0;
+ }
+
+ public static boolean areThereGroupInvitationsWithDomain(int groupID, String domain) throws SQLException{
+ Connection conn=DatabaseConnectionManager.getConnection();
+ PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT COUNT(*) FROM `group_invites` JOIN `users` ON invitee_id=`users`.id WHERE group_id=? AND `users`.`domain`=?", groupID, domain);
+ return DatabaseUtils.oneFieldToInt(stmt.executeQuery())>0;
+ }
+
+ public static Map getMembersByActivityPubIDs(Collection ids, int groupID, boolean tentative) throws SQLException{
+ if(ids.isEmpty())
+ return Map.of();
+ Connection conn=DatabaseConnectionManager.getConnection();
+ ArrayList localIDs=new ArrayList<>();
+ ArrayList remoteIDs=new ArrayList<>();
+ for(URI id:ids){
+ if(Config.isLocal(id)){
+ String path=id.getPath();
+ if(StringUtils.isEmpty(path))
+ continue;
+ String[] pathSegments=path.split("/");
+ if(pathSegments.length!=3 || !"users".equals(pathSegments[1])) // "", "users", id
+ continue;
+ int uid=Utils.safeParseInt(pathSegments[2]);
+ if(uid>0)
+ localIDs.add(uid);
+ }else{
+ remoteIDs.add(id.toString());
+ }
+ }
+ HashMap localIdToApIdMap=new HashMap<>();
+ if(!remoteIDs.isEmpty()){
+ try(ResultSet res=new SQLQueryBuilder(conn).selectFrom("users").columns("id", "ap_id").whereIn("ap_id", remoteIDs).execute()){
+ while(res.next()){
+ int localID=res.getInt(1);
+ localIDs.add(localID);
+ localIdToApIdMap.put(localID, URI.create(res.getString(2)));
+ }
+ }
+ }
+ if(localIDs.isEmpty())
+ return Map.of();
+ return new SQLQueryBuilder(conn)
+ .selectFrom("group_memberships")
+ .columns("user_id")
+ .whereIn("user_id", localIDs)
+ .andWhere("tentative=? AND accepted=1 AND group_id=?", tentative, groupID)
+ .executeAsStream(res->res.getInt(1))
+ .collect(Collectors.toMap(id->localIdToApIdMap.computeIfAbsent(id, GroupStorage::localUserURI), Function.identity()));
+ }
+
+ public static Map getUserGroupsByActivityPubIDs(Collection ids, int userID) throws SQLException{
+ if(ids.isEmpty())
+ return Map.of();
+ Connection conn=DatabaseConnectionManager.getConnection();
+ ArrayList localIDs=new ArrayList<>();
+ ArrayList remoteIDs=new ArrayList<>();
+ for(URI id:ids){
+ if(Config.isLocal(id)){
+ String path=id.getPath();
+ if(StringUtils.isEmpty(path))
+ continue;
+ String[] pathSegments=path.split("/");
+ if(pathSegments.length!=3 || !"groups".equals(pathSegments[1])) // "", "groups", id
+ continue;
+ int uid=Utils.safeParseInt(pathSegments[2]);
+ if(uid>0)
+ localIDs.add(uid);
+ }else{
+ remoteIDs.add(id.toString());
+ }
+ }
+ if(!localIDs.isEmpty()){
+ // Filter local IDs to avoid returning private groups
+ List filteredLocalIDs=new SQLQueryBuilder(conn)
+ .selectFrom("groups")
+ .columns("id")
+ .whereIn("id", localIDs)
+ .andWhere("access_type<>"+Group.AccessType.PRIVATE)
+ .executeAndGetIntList();
+ localIDs.clear();
+ localIDs.addAll(filteredLocalIDs);
+ }
+ HashMap localIdToApIdMap=new HashMap<>();
+ if(!remoteIDs.isEmpty()){
+ try(ResultSet res=new SQLQueryBuilder(conn).selectFrom("groups").columns("id", "ap_id").whereIn("ap_id", remoteIDs).andWhere("access_type<>"+Group.AccessType.PRIVATE.ordinal()).execute()){
+ while(res.next()){
+ int localID=res.getInt(1);
+ localIDs.add(localID);
+ localIdToApIdMap.put(localID, URI.create(res.getString(2)));
+ }
+ }
+ }
+ if(localIDs.isEmpty())
+ return Map.of();
+ return new SQLQueryBuilder(conn)
+ .selectFrom("group_memberships")
+ .columns("group_id")
+ .whereIn("group_id", localIDs)
+ .andWhere("accepted=1 AND user_id=?", userID)
+ .executeAsStream(res->res.getInt(1))
+ .collect(Collectors.toMap(id->localIdToApIdMap.computeIfAbsent(id, GroupStorage::localGroupURI), Function.identity()));
+ }
+
+ private static URI localUserURI(int id){
+ return Config.localURI("/users/"+id);
+ }
+
+ private static URI localGroupURI(int id){
+ return Config.localURI("/groups/"+id);
+ }
+
+ public static void setMemberCount(Group group, int count, boolean tentative) throws SQLException{
+ String field=tentative ? "tentative_member_count" : "member_count";
+ new SQLQueryBuilder()
+ .update("groups")
+ .value(field, count)
+ .where("id=?", group.id)
+ .executeNoResult();
+ removeFromCache(group);
+ }
+
+ public static int getLocalMembersCount(int groupID) throws SQLException{
+ Connection conn=DatabaseConnectionManager.getConnection();
+ PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT COUNT(*) FROM `group_memberships` JOIN `users` ON `user_id`=`users`.id WHERE group_id=? AND accepted=1 AND `users`.domain=''", groupID);
+ return DatabaseUtils.oneFieldToInt(stmt.executeQuery());
+ }
}
diff --git a/src/main/java/smithereen/storage/IdPair.java b/src/main/java/smithereen/storage/IdPair.java
new file mode 100644
index 00000000..efaeb7f7
--- /dev/null
+++ b/src/main/java/smithereen/storage/IdPair.java
@@ -0,0 +1,4 @@
+package smithereen.storage;
+
+record IdPair(int first, int second){
+}
diff --git a/src/main/java/smithereen/storage/LikeStorage.java b/src/main/java/smithereen/storage/LikeStorage.java
index 6c7e98c9..384774dc 100644
--- a/src/main/java/smithereen/storage/LikeStorage.java
+++ b/src/main/java/smithereen/storage/LikeStorage.java
@@ -14,7 +14,7 @@
import smithereen.activitypub.objects.LinkOrObject;
import smithereen.activitypub.objects.activities.Like;
import smithereen.data.ForeignUser;
-import smithereen.data.ListAndTotal;
+import smithereen.data.PaginatedList;
import smithereen.data.User;
public class LikeStorage{
@@ -59,7 +59,7 @@ private static int deleteLike(int userID, int objectID, Like.ObjectType type) th
}
}
- public static ListAndTotal getLikes(int objectID, URI objectApID, Like.ObjectType type, int offset, int count) throws SQLException{
+ public static PaginatedList getLikes(int objectID, URI objectApID, Like.ObjectType type, int offset, int count) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT likes.id AS like_id, user_id, likes.ap_id AS like_ap_id, users.ap_id AS user_ap_id FROM likes JOIN users ON users.id=likes.user_id WHERE object_id=? AND object_type=? ORDER BY likes.id ASC LIMIT ?,?",
objectID, type, offset, count);
@@ -86,7 +86,7 @@ public static ListAndTotal getLikes(int objectID, URI objectApID, Like.Obj
.count()
.where("object_id=? AND object_type=?", objectID, type.ordinal())
.createStatement();
- return new ListAndTotal<>(likes, DatabaseUtils.oneFieldToInt(stmt.executeQuery()));
+ return new PaginatedList<>(likes, DatabaseUtils.oneFieldToInt(stmt.executeQuery()));
}
public static List getPostLikes(int objectID, int selfID, int offset, int count) throws SQLException{
diff --git a/src/main/java/smithereen/storage/NotificationsStorage.java b/src/main/java/smithereen/storage/NotificationsStorage.java
index f92996a7..fe75e7b2 100644
--- a/src/main/java/smithereen/storage/NotificationsStorage.java
+++ b/src/main/java/smithereen/storage/NotificationsStorage.java
@@ -45,7 +45,7 @@ public static void putNotification(int owner, @NotNull Notification n) throws SQ
un.incNewNotificationsCount(1);
}
- public static List getNotifications(int owner, int offset, @Nullable int[] total) throws SQLException{
+ public static List getNotifications(int owner, int offset, int count, @Nullable int[] total) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
PreparedStatement stmt;
if(total!=null){
@@ -56,9 +56,10 @@ public static List getNotifications(int owner, int offset, @Nullab
total[0]=res.getInt(1);
}
}
- stmt=conn.prepareStatement("SELECT * FROM `notifications` WHERE `owner_id`=? ORDER BY `time` DESC LIMIT ?,50");
+ stmt=conn.prepareStatement("SELECT * FROM `notifications` WHERE `owner_id`=? ORDER BY `time` DESC LIMIT ?,?");
stmt.setInt(1, owner);
stmt.setInt(2, offset);
+ stmt.setInt(3, count);
try(ResultSet res=stmt.executeQuery()){
if(res.first()){
ArrayList notifications=new ArrayList<>();
@@ -68,7 +69,7 @@ public static List getNotifications(int owner, int offset, @Nullab
return notifications;
}
}
- return Collections.EMPTY_LIST;
+ return Collections.emptyList();
}
public static void deleteNotificationsForObject(@NotNull Notification.ObjectType type, int objID) throws SQLException{
@@ -139,6 +140,15 @@ public static synchronized UserNotifications getNotificationsForUser(int userID,
r.first();
res.incNewNotificationsCount(r.getInt(1));
}
+ stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT COUNT(*), is_event FROM group_invites WHERE invitee_id=? GROUP BY is_event", userID);
+ try(ResultSet r=stmt.executeQuery()){
+ while(r.next()){
+ if(r.getBoolean(2)) // event
+ res.incNewEventInvitationsCount(r.getInt(1));
+ else
+ res.incNewGroupInvitationsCount(r.getInt(1));
+ }
+ }
userNotificationsCache.put(userID, res);
return res;
}
diff --git a/src/main/java/smithereen/storage/PostStorage.java b/src/main/java/smithereen/storage/PostStorage.java
index 0c6ed78d..9007eef4 100644
--- a/src/main/java/smithereen/storage/PostStorage.java
+++ b/src/main/java/smithereen/storage/PostStorage.java
@@ -15,28 +15,29 @@
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
-import java.sql.Types;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
-import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import smithereen.Config;
import smithereen.activitypub.objects.activities.Like;
import smithereen.data.FederationState;
-import smithereen.data.ListAndTotal;
+import smithereen.data.PaginatedList;
import smithereen.data.Poll;
import smithereen.data.PollOption;
import smithereen.data.UriBuilder;
import smithereen.data.feed.AddFriendNewsfeedEntry;
+import smithereen.data.feed.JoinEventNewsfeedEntry;
import smithereen.data.feed.JoinGroupNewsfeedEntry;
import smithereen.exceptions.ObjectNotFoundException;
import smithereen.Utils;
@@ -60,7 +61,7 @@ public static int createWallPost(int userID, int ownerUserID, int ownerGroupID,
if(ownerUserID<=0 && ownerGroupID<=0)
throw new IllegalArgumentException("Need either ownerUserID or ownerGroupID");
- PreparedStatement stmt=new SQLQueryBuilder(conn)
+ int id=new SQLQueryBuilder(conn)
.insertInto("wall_posts")
.value("author_id", userID)
.value("owner_user_id", ownerUserID>0 ? ownerUserID : null)
@@ -73,34 +74,27 @@ public static int createWallPost(int userID, int ownerUserID, int ownerGroupID,
.value("poll_id", pollID>0 ? pollID : null)
.value("source", textSource)
.value("source_format", 0)
- .createStatement(Statement.RETURN_GENERATED_KEYS);
-
- stmt.execute();
- try(ResultSet keys=stmt.getGeneratedKeys()){
- keys.first();
- int id=keys.getInt(1);
- if(userID==ownerUserID && replyKey==null){
- new SQLQueryBuilder(conn)
- .insertInto("newsfeed")
- .value("type", NewsfeedEntry.Type.POST)
- .value("author_id", userID)
- .value("object_id", id)
- .createStatement()
- .execute();
- }
- if(replyKey!=null && replyKey.length>0){
- new SQLQueryBuilder(conn)
- .update("wall_posts")
- .valueExpr("reply_count", "reply_count+1")
- .whereIn("id", Arrays.stream(replyKey).boxed().collect(Collectors.toList()))
- .createStatement()
- .execute();
-
- SQLQueryBuilder.prepareStatement(conn, "INSERT INTO newsfeed_comments (user_id, object_type, object_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE object_id=object_id", userID, 0, replyKey[0]).execute();
- BackgroundTaskRunner.getInstance().submit(new UpdateCommentBookmarksRunnable(replyKey[0]));
- }
- return id;
+ .executeAndGetID();
+
+ if(userID==ownerUserID && replyKey==null){
+ new SQLQueryBuilder(conn)
+ .insertInto("newsfeed")
+ .value("type", NewsfeedEntry.Type.POST)
+ .value("author_id", userID)
+ .value("object_id", id)
+ .executeNoResult();
+ }
+ if(replyKey!=null && replyKey.length>0){
+ new SQLQueryBuilder(conn)
+ .update("wall_posts")
+ .valueExpr("reply_count", "reply_count+1")
+ .whereIn("id", Arrays.stream(replyKey).boxed().collect(Collectors.toList()))
+ .executeNoResult();
+
+ SQLQueryBuilder.prepareStatement(conn, "INSERT INTO newsfeed_comments (user_id, object_type, object_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE object_id=object_id", userID, 0, replyKey[0]).execute();
+ BackgroundTaskRunner.getInstance().submit(new UpdateCommentBookmarksRunnable(replyKey[0]));
}
+ return id;
}
public static void updateWallPost(int id, String text, String textSource, List mentionedUsers, String attachments, String contentWarning, int pollID) throws SQLException{
@@ -125,7 +119,7 @@ private static int putForeignPoll(Connection conn, int ownerID, URI activityPubI
.value("owner_id", ownerID)
.value("question", poll.question)
.value("is_anonymous", poll.anonymous)
- .value("end_time", poll.endTime!=null ? new Timestamp(poll.endTime.getTime()) : null)
+ .value("end_time", poll.endTime)
.value("is_multi_choice", poll.multipleChoice)
.value("num_voted_users",poll.numVoters)
.createStatement(Statement.RETURN_GENERATED_KEYS);
@@ -162,7 +156,7 @@ public static void putForeignWallPost(Post post) throws SQLException{
PreparedStatement stmt;
if(existing==null){
if(post.poll!=null){
- post.poll.id=putForeignPoll(conn, post.user.id, post.activityPubID, post.poll);
+ post.poll.id=putForeignPoll(conn, post.owner.getOwnerID(), post.activityPubID, post.poll);
}
stmt=new SQLQueryBuilder(conn)
@@ -176,7 +170,7 @@ public static void putForeignWallPost(Post post) throws SQLException{
.value("ap_url", post.url.toString())
.value("ap_id", post.activityPubID.toString())
.value("reply_key", Utils.serializeIntArray(post.replyKey))
- .value("created_at", new Timestamp(post.published.getTime()))
+ .value("created_at", post.published)
.value("mentions", post.mentionedUsers.isEmpty() ? null : Utils.serializeIntArray(post.mentionedUsers.stream().mapToInt(u->u.id).toArray()))
.value("ap_replies", Objects.toString(post.getRepliesURL(), null))
.value("poll_id", post.poll!=null ? post.poll.id : null)
@@ -225,17 +219,15 @@ public static void putForeignWallPost(Post post) throws SQLException{
new SQLQueryBuilder(conn)
.deleteFrom("polls")
.where("id=?", existing.poll.id)
- .createStatement()
- .execute();
- post.poll.id=putForeignPoll(conn, post.user.id, post.activityPubID, post.poll);
+ .executeNoResult();
+ post.poll.id=putForeignPoll(conn, post.owner.getOwnerID(), post.activityPubID, post.poll);
}else if(post.poll!=null){ // poll was added
- post.poll.id=putForeignPoll(conn, post.user.id, post.activityPubID, post.poll);
+ post.poll.id=putForeignPoll(conn, post.owner.getOwnerID(), post.activityPubID, post.poll);
}else if(existing.poll!=null){ // poll was removed
new SQLQueryBuilder(conn)
.deleteFrom("polls")
.where("id=?", existing.poll.id)
- .createStatement()
- .execute();
+ .executeNoResult();
}
stmt=new SQLQueryBuilder(conn)
.update("wall_posts")
@@ -255,17 +247,15 @@ public static void putForeignWallPost(Post post) throws SQLException{
.value("type", NewsfeedEntry.Type.POST)
.value("author_id", post.user.id)
.value("object_id", post.id)
- .value("time", new Timestamp(post.published.getTime()))
- .createStatement()
- .execute();
+ .value("time", post.published)
+ .executeNoResult();
}
if(post.getReplyLevel()>0){
new SQLQueryBuilder(conn)
.update("wall_posts")
.valueExpr("reply_count", "reply_count+1")
.whereIn("id", Arrays.stream(post.replyKey).boxed().collect(Collectors.toList()))
- .createStatement()
- .execute();
+ .executeNoResult();
BackgroundTaskRunner.getInstance().submit(new UpdateCommentBookmarksRunnable(post.replyKey[0]));
}
}else{
@@ -320,13 +310,20 @@ public static List getFeed(int userID, int startFromID, int offse
_entry.author=UserStorage.getById(res.getInt(3));
yield _entry;
}
- case JOIN_GROUP -> {
+ case JOIN_GROUP, CREATE_GROUP -> {
JoinGroupNewsfeedEntry _entry=new JoinGroupNewsfeedEntry();
_entry.objectID=res.getInt(2);
_entry.group=GroupStorage.getById(_entry.objectID);
_entry.author=UserStorage.getById(res.getInt(3));
yield _entry;
}
+ case JOIN_EVENT, CREATE_EVENT -> {
+ JoinEventNewsfeedEntry _entry=new JoinEventNewsfeedEntry();
+ _entry.objectID=res.getInt(2);
+ _entry.event=GroupStorage.getById(_entry.objectID);
+ _entry.author=UserStorage.getById(res.getInt(3));
+ yield _entry;
+ }
};
entry.type=type;
entry.id=res.getInt(4);
@@ -367,7 +364,7 @@ public static List getFeed(int userID, int startFromID, int offse
return posts;
}
- public static List getWallPosts(int ownerID, boolean isGroup, int minID, int maxID, int offset, int[] total, boolean ownOnly) throws SQLException{
+ public static List getWallPosts(int ownerID, boolean isGroup, int minID, int maxID, int offset, int count, int[] total, boolean ownOnly) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
PreparedStatement stmt;
String ownCondition=ownOnly ? " AND owner_user_id=author_id" : "";
@@ -381,13 +378,13 @@ public static List getWallPosts(int ownerID, boolean isGroup, int minID, i
}
}
if(minID>0){
- stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `"+ownerField+"`=? AND `id`>? AND `reply_key` IS NULL"+ownCondition+" ORDER BY created_at DESC LIMIT 25");
+ stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `"+ownerField+"`=? AND `id`>? AND `reply_key` IS NULL"+ownCondition+" ORDER BY created_at DESC LIMIT "+count);
stmt.setInt(2, minID);
}else if(maxID>0){
- stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `"+ownerField+"`=? AND `id`= AND `reply_key` IS NULL"+ownCondition+" ORDER BY created_at DESC LIMIT "+offset+",25");
+ stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `"+ownerField+"`=? AND `id`= AND `reply_key` IS NULL"+ownCondition+" ORDER BY created_at DESC LIMIT "+offset+","+count);
stmt.setInt(2, maxID);
}else{
- stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `"+ownerField+"`=? AND `reply_key` IS NULL"+ownCondition+" ORDER BY created_at DESC LIMIT "+offset+",25");
+ stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `"+ownerField+"`=? AND `reply_key` IS NULL"+ownCondition+" ORDER BY created_at DESC LIMIT "+offset+","+count);
}
stmt.setInt(1, ownerID);
ArrayList posts=new ArrayList<>();
@@ -404,38 +401,30 @@ public static List getWallPosts(int ownerID, boolean isGroup, int minID, i
public static List getWallPostActivityPubIDs(int ownerID, boolean isGroup, int offset, int count, int[] total) throws SQLException{
String ownerField=isGroup ? "owner_group_id" : "owner_user_id";
Connection conn=DatabaseConnectionManager.getConnection();
- PreparedStatement stmt=new SQLQueryBuilder(conn)
+ total[0]=new SQLQueryBuilder(conn)
.selectFrom("wall_posts")
.count()
.where(ownerField+"=? AND reply_key IS NULL", ownerID)
- .createStatement();
- try(ResultSet res=stmt.executeQuery()){
- res.first();
- total[0]=res.getInt(1);
- }
- stmt=new SQLQueryBuilder(conn)
+ .executeAndGetInt();
+
+ return new SQLQueryBuilder(conn)
.selectFrom("wall_posts")
.columns("id", "ap_id")
.where(ownerField+"=? AND reply_key IS NULL", ownerID)
.orderBy("id ASC")
.limit(count, offset)
- .createStatement();
- try(ResultSet res=stmt.executeQuery()){
- res.beforeFirst();
- List ids=new ArrayList<>();
- while(res.next()){
- String apID=res.getString(2);
- if(StringUtils.isNotEmpty(apID)){
- ids.add(URI.create(apID));
- }else{
- ids.add(UriBuilder.local().path("posts", res.getInt(1)+"").build());
- }
- }
- return ids;
- }
+ .executeAsStream(res->{
+ String apID=res.getString(2);
+ if(StringUtils.isNotEmpty(apID)){
+ return URI.create(apID);
+ }else{
+ return UriBuilder.local().path("posts", res.getInt(1)+"").build();
+ }
+ })
+ .toList();
}
- public static List getWallToWall(int userID, int otherUserID, int offset, int[] total) throws SQLException{
+ public static List getWallToWall(int userID, int otherUserID, int offset, int count, int[] total) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
PreparedStatement stmt;
if(total!=null){
@@ -449,7 +438,7 @@ public static List getWallToWall(int userID, int otherUserID, int offset,
total[0]=res.getInt(1);
}
}
- stmt=conn.prepareStatement("SELECT * FROM wall_posts WHERE ((owner_user_id=? AND author_id=?) OR (owner_user_id=? AND author_id=?)) AND `reply_key` IS NULL ORDER BY created_at DESC LIMIT "+offset+",25");
+ stmt=conn.prepareStatement("SELECT * FROM wall_posts WHERE ((owner_user_id=? AND author_id=?) OR (owner_user_id=? AND author_id=?)) AND `reply_key` IS NULL ORDER BY created_at DESC LIMIT "+offset+","+count);
stmt.setInt(1, userID);
stmt.setInt(2, otherUserID);
stmt.setInt(3, otherUserID);
@@ -535,15 +524,15 @@ public static void deletePost(int id) throws SQLException{
stmt=conn.prepareStatement("DELETE FROM `wall_posts` WHERE `id`=?");
stmt.setInt(1, id);
stmt.execute();
- stmt=conn.prepareStatement("DELETE FROM `newsfeed` WHERE (`type`=0 OR `type`=1) AND `object_id`=?");
- stmt.setInt(1, id);
- stmt.execute();
}else{
// (comments don't exist in the feed anyway)
stmt=conn.prepareStatement("UPDATE wall_posts SET author_id=NULL, owner_user_id=NULL, owner_group_id=NULL, text=NULL, attachments=NULL, content_warning=NULL, updated_at=NULL, mentions=NULL WHERE id=?");
stmt.setInt(1, id);
stmt.execute();
}
+ stmt=conn.prepareStatement("DELETE FROM `newsfeed` WHERE (`type`=0 OR `type`=1) AND `object_id`=?");
+ stmt.setInt(1, id);
+ stmt.execute();
if(post.getReplyLevel()>0){
conn.createStatement().execute("UPDATE wall_posts SET reply_count=GREATEST(1, reply_count)-1 WHERE id IN ("+Arrays.stream(post.replyKey).mapToObj(String::valueOf).collect(Collectors.joining(","))+")");
@@ -553,23 +542,23 @@ public static void deletePost(int id) throws SQLException{
}
}
- public static Map> getRepliesForFeed(Set postIDs) throws SQLException{
+ public static Map> getRepliesForFeed(Set postIDs) throws SQLException{
if(postIDs.isEmpty())
return Collections.emptyMap();
Connection conn=DatabaseConnectionManager.getConnection();
- PreparedStatement stmt=conn.prepareStatement(String.join(" UNION ALL ", Collections.nCopies(postIDs.size(), "(SELECT * FROM wall_posts WHERE reply_key=? ORDER BY id DESC LIMIT 3)")));
+ PreparedStatement stmt=conn.prepareStatement(String.join(" UNION ALL ", Collections.nCopies(postIDs.size(), "(SELECT * FROM wall_posts WHERE reply_key=? ORDER BY created_at DESC LIMIT 3)")));
int i=0;
for(int id:postIDs){
stmt.setBytes(i+1, Utils.serializeIntArray(new int[]{id}));
i++;
}
LOG.debug("{}", stmt);
- HashMap> map=new HashMap<>();
+ HashMap> map=new HashMap<>();
try(ResultSet res=stmt.executeQuery()){
res.afterLast();
while(res.previous()){
Post post=Post.fromResultSet(res);
- List posts=map.computeIfAbsent(post.getReplyChainElement(0), (k)->new ListAndTotal<>(new ArrayList<>(), 0)).list;
+ List posts=map.computeIfAbsent(post.getReplyChainElement(0), (k)->new PaginatedList<>(new ArrayList<>(), 0)).list;
posts.add(post);
}
}
@@ -591,7 +580,7 @@ public static Map> getRepliesForFeed(Set po
public static List getReplies(int[] prefix) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
- PreparedStatement stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `reply_key` LIKE BINARY bin_prefix(?) ESCAPE CHAR(255) ORDER BY `reply_key` ASC, `id` ASC LIMIT 100");
+ PreparedStatement stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `reply_key` LIKE BINARY bin_prefix(?) ESCAPE CHAR(255) ORDER BY `reply_key` ASC, `created_at` ASC LIMIT 100");
byte[] replyKey;
ByteArrayOutputStream b=new ByteArrayOutputStream(prefix.length*4);
try{
@@ -640,7 +629,7 @@ public static List getRepliesExact(int[] replyKey, int maxID, int limit, i
.allColumns()
.where("reply_key=? AND id", Utils.serializeIntArray(replyKey), maxID)
.limit(limit, 0)
- .orderBy("id ASC")
+ .orderBy("created_at ASC")
.createStatement();
try(ResultSet res=stmt.executeQuery()){
ArrayList posts=new ArrayList<>();
@@ -901,12 +890,12 @@ public static synchronized int[] voteInPoll(int userID, int pollID, int[] option
}
}
- stmt=new SQLQueryBuilder(conn)
+ new SQLQueryBuilder(conn)
.update("polls")
.valueExpr("num_voted_users", "num_voted_users+1")
+ .valueExpr("last_vote_time", "CURRENT_TIMESTAMP()")
.where("id=?", pollID)
- .createStatement();
- stmt.execute();
+ .executeNoResult();
return voteIDs;
}
@@ -956,35 +945,28 @@ public static synchronized int voteInPoll(int userID, int pollID, int optionID,
}
if(!userVoted){
- stmt=new SQLQueryBuilder(conn)
+ new SQLQueryBuilder(conn)
.update("polls")
.valueExpr("num_voted_users", "num_voted_users+1")
.where("id=?", pollID)
- .createStatement();
- stmt.execute();
+ .executeNoResult();
}
return rVoteID;
}
- public static int createPoll(int ownerID, @NotNull String question, @NotNull List options, boolean anonymous, boolean multiChoice, @Nullable Date endTime) throws SQLException{
+ public static int createPoll(int ownerID, @NotNull String question, @NotNull List options, boolean anonymous, boolean multiChoice, @Nullable Instant endTime) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
- PreparedStatement stmt=new SQLQueryBuilder(conn)
+ int pollID=new SQLQueryBuilder(conn)
.insertInto("polls")
.value("owner_id", ownerID)
.value("question", question)
.value("is_anonymous", anonymous)
.value("is_multi_choice", multiChoice)
- .value("end_time", endTime!=null ? new Timestamp(endTime.getTime()) : null)
- .createStatement(Statement.RETURN_GENERATED_KEYS);
- stmt.execute();
- int pollID;
- try(ResultSet res=stmt.getGeneratedKeys()){
- res.first();
- pollID=res.getInt(1);
- }
+ .value("end_time", endTime)
+ .executeAndGetID();
- stmt=new SQLQueryBuilder(conn)
+ PreparedStatement stmt=new SQLQueryBuilder(conn)
.insertInto("poll_options")
.value("text", "")
.value("poll_id", pollID)
@@ -1001,19 +983,17 @@ public static void deletePoll(int pollID) throws SQLException{
new SQLQueryBuilder()
.deleteFrom("polls")
.where("id=?", pollID)
- .createStatement()
- .execute();
+ .executeNoResult();
}
public static List getPollOptionVoters(int optionID, int offset, int count) throws SQLException{
- PreparedStatement stmt=new SQLQueryBuilder()
+ return new SQLQueryBuilder()
.selectFrom("poll_votes")
.columns("user_id")
.where("option_id=?", optionID)
.orderBy("id ASC")
.limit(count, offset)
- .createStatement();
- return DatabaseUtils.intResultSetToList(stmt.executeQuery());
+ .executeAndGetIntList();
}
public static List getPollOptionVotersApIDs(int optionID, int offset, int count) throws SQLException{
@@ -1039,7 +1019,7 @@ public static void setPostFederationState(int postID, FederationState state) thr
.execute();
}
- public static ListAndTotal getCommentsFeed(int userID, int offset, int count) throws SQLException{
+ public static PaginatedList getCommentsFeed(int userID, int offset, int count) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
int total;
PreparedStatement stmt=new SQLQueryBuilder(conn)
@@ -1054,7 +1034,7 @@ public static ListAndTotal getCommentsFeed(int userID, int offset
}
if(total==0)
- return new ListAndTotal<>(Collections.emptyList(), 0);
+ return new PaginatedList<>(Collections.emptyList(), 0);
stmt=new SQLQueryBuilder(conn)
.selectFrom("newsfeed_comments")
@@ -1082,7 +1062,7 @@ public static ListAndTotal getCommentsFeed(int userID, int offset
}
}
if(needPosts.isEmpty())
- return new ListAndTotal<>(entries, total);
+ return new PaginatedList<>(entries, total);
stmt=new SQLQueryBuilder(conn)
.selectFrom("wall_posts")
@@ -1091,73 +1071,117 @@ public static ListAndTotal getCommentsFeed(int userID, int offset
.createStatement();
Map posts;
- try(ResultSet res=stmt.executeQuery()){
- posts=DatabaseUtils.resultSetToObjectStream(res, Post::fromResultSet).collect(Collectors.toMap(post->post.id, Function.identity()));
- }
+ posts=DatabaseUtils.resultSetToObjectStream(stmt.executeQuery(), Post::fromResultSet).collect(Collectors.toMap(post->post.id, Function.identity()));
for(NewsfeedEntry entry : entries){
if(entry.type==NewsfeedEntry.Type.POST){
((PostNewsfeedEntry) entry).post=posts.get(entry.objectID);
}
}
- return new ListAndTotal<>(entries, total);
+ return new PaginatedList<>(entries.stream().filter(e->!(e instanceof PostNewsfeedEntry pe) || pe.post!=null).collect(Collectors.toList()), total, offset, count);
}
- private static class DeleteCommentBookmarksRunnable implements Runnable{
- private final int postID;
+ public static Map getPostLocalIDsByActivityPubIDs(Collection ids, int ownerUserID, int ownerGroupID) throws SQLException{
+ if(ids.isEmpty())
+ return Map.of();
- public DeleteCommentBookmarksRunnable(int postID){
- this.postID=postID;
- }
+ Connection conn=DatabaseConnectionManager.getConnection();
- @Override
- public void run(){
- try{
- new SQLQueryBuilder()
- .deleteFrom("newsfeed_comments")
- .where("object_type=? AND object_id=?", 0, postID)
- .createStatement()
- .execute();
- }catch(SQLException x){
- LOG.warn("Error deleting comment bookmarks for post {}", postID, x);
+ List localIDs=ids.stream().filter(Config::isLocal).map(u->{
+ String path=u.getPath();
+ if(StringUtils.isEmpty(path))
+ return 0;
+ String[] parts=path.split("/"); // "", "posts", id
+ return parts.length==3 && "posts".equals(parts[1]) ? Utils.safeParseInt(parts[2]) : 0;
+ }).filter(i->i>0).toList();
+ List remoteIDs=ids.stream().filter(Predicate.not(Config::isLocal)).map(Object::toString).toList();
+
+ Map result=new HashMap<>();
+ record IdPair(URI apID, int localID){}
+
+ if(!remoteIDs.isEmpty()){
+ SQLQueryBuilder builder=new SQLQueryBuilder(conn)
+ .selectFrom("wall_posts")
+ .columns("id", "ap_id")
+ .whereIn("ap_id", remoteIDs)
+ .andWhere("reply_key IS NULL");
+
+ if(ownerUserID>0 && ownerGroupID==0){
+ builder.andWhere("owner_user_id=?", ownerUserID).andWhere("owner_group_id IS NULL");
+ }else if(ownerGroupID>0 && ownerUserID==0){
+ builder.andWhere("owner_group_id=?", ownerGroupID).andWhere("owner_user_id IS NULL");
+ }else{
+ throw new IllegalArgumentException("either ownerUserID or ownerGroupID must be >0");
+ }
+ result.putAll(builder.executeAsStream(rs->new IdPair(URI.create(rs.getString(2)), rs.getInt(1))).collect(Collectors.toMap(IdPair::apID, IdPair::localID)));
+ }
+ if(!localIDs.isEmpty()){
+ SQLQueryBuilder builder=new SQLQueryBuilder(conn)
+ .selectFrom("wall_posts")
+ .columns("id")
+ .whereIn("id", localIDs)
+ .andWhere("reply_key IS NULL");
+
+ if(ownerUserID>0 && ownerGroupID==0){
+ builder.andWhere("owner_user_id=?", ownerUserID).andWhere("owner_group_id IS NULL");
+ }else if(ownerGroupID>0 && ownerUserID==0){
+ builder.andWhere("owner_group_id=?", ownerGroupID).andWhere("owner_user_id IS NULL");
+ }else{
+ throw new IllegalArgumentException("either ownerUserID or ownerGroupID must be >0");
}
+ result.putAll(builder.executeAsStream(rs->new IdPair(Config.localURI("/posts/"+rs.getInt(1)), rs.getInt(1))).collect(Collectors.toMap(IdPair::apID, IdPair::localID)));
}
+ return result;
}
- private static class UpdateCommentBookmarksRunnable implements Runnable{
- private final int postID;
-
- public UpdateCommentBookmarksRunnable(int postID){
- this.postID=postID;
- }
+ public static int getPostIdByPollId(int pollID) throws SQLException{
+ return new SQLQueryBuilder()
+ .selectFrom("wall_posts")
+ .columns("id")
+ .where("poll_id=?", pollID)
+ .executeAndGetInt();
+ }
+ private record DeleteCommentBookmarksRunnable(int postID) implements Runnable{
@Override
- public void run(){
- try{
- Connection conn=DatabaseConnectionManager.getConnection();
- PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT MAX(created_at) FROM wall_posts WHERE reply_key LIKE BINARY bin_prefix(?)", (Object) Utils.serializeIntArray(new int[]{postID}));
- Timestamp ts;
- try(ResultSet res=stmt.executeQuery()){
- res.first();
- ts=res.getTimestamp(1);
- }
- if(ts==null){
- new SQLQueryBuilder(conn)
+ public void run(){
+ try{
+ new SQLQueryBuilder()
.deleteFrom("newsfeed_comments")
.where("object_type=? AND object_id=?", 0, postID)
- .createStatement()
- .execute();
- }else{
- new SQLQueryBuilder(conn)
- .update("newsfeed_comments")
- .value("last_comment_time", ts)
- .where("object_type=? AND object_id=?", 0, postID)
- .createStatement()
- .execute();
+ .executeNoResult();
+ }catch(SQLException x){
+ LOG.warn("Error deleting comment bookmarks for post {}", postID, x);
+ }
+ }
+ }
+
+ private record UpdateCommentBookmarksRunnable(int postID) implements Runnable{
+ @Override
+ public void run(){
+ try{
+ Connection conn=DatabaseConnectionManager.getConnection();
+ PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT MAX(created_at) FROM wall_posts WHERE reply_key LIKE BINARY bin_prefix(?)", (Object) Utils.serializeIntArray(new int[]{postID}));
+ Timestamp ts;
+ try(ResultSet res=stmt.executeQuery()){
+ res.first();
+ ts=res.getTimestamp(1);
+ }
+ if(ts==null){
+ new SQLQueryBuilder(conn)
+ .deleteFrom("newsfeed_comments")
+ .where("object_type=? AND object_id=?", 0, postID)
+ .executeNoResult();
+ }else{
+ new SQLQueryBuilder(conn)
+ .update("newsfeed_comments")
+ .value("last_comment_time", ts)
+ .where("object_type=? AND object_id=?", 0, postID)
+ .executeNoResult();
+ }
+ }catch(SQLException x){
+ LOG.warn("Error updating comment bookmarks for post {}", postID, x);
}
- }catch(SQLException x){
- LOG.warn("Error updating comment bookmarks for post {}", postID, x);
}
}
- }
}
diff --git a/src/main/java/smithereen/storage/SQLQueryBuilder.java b/src/main/java/smithereen/storage/SQLQueryBuilder.java
index a1dfebef..49c7a17c 100644
--- a/src/main/java/smithereen/storage/SQLQueryBuilder.java
+++ b/src/main/java/smithereen/storage/SQLQueryBuilder.java
@@ -5,12 +5,19 @@
import java.sql.Connection;
import java.sql.PreparedStatement;
+import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.stream.Stream;
import smithereen.Config;
+import spark.utils.StringUtils;
public class SQLQueryBuilder{
private static final Logger LOG=LoggerFactory.getLogger(SQLQueryBuilder.class);
@@ -25,7 +32,7 @@ public class SQLQueryBuilder{
private boolean selectDistinct;
private List values;
private String condition;
- private Object[] conditionArgs;
+ private List