diff --git a/VKAPI/Handlers/Newsfeed.php b/VKAPI/Handlers/Newsfeed.php index b3e3989a6..883d1f5ec 100644 --- a/VKAPI/Handlers/Newsfeed.php +++ b/VKAPI/Handlers/Newsfeed.php @@ -41,14 +41,7 @@ public function get(string $fields = "", string $start_from = "", int $start_tim }, iterator_to_array($subs)); $ids[] = $this->getUser()->getId(); - $posts = DatabaseConnection::i() - ->getContext() - ->table("posts") - ->select("id, created") - ->where("wall IN (?)", $ids) - ->where("deleted", 0) - ->where("suggested", 0) - ->where("archived", 0) + $posts = (new PostsRepo())->getFeedSelection($ids) ->where("created <= ?", $cursorTime) ->where("created < ? OR id < ?", $cursorTime, $cursorId) ->where("? <= created", empty($start_time) ? 0 : $start_time) @@ -86,29 +79,15 @@ public function getGlobal(string $fields = "", string $start_from = "", int $sta [$cursorTime, $cursorId] = $this->parseCursor($start_from); - $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)"; - $queryBase .= " WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND (`profiles`.`profile_type` = 0 OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0 AND `posts`.`archived` = 0"; - - if ($with_alien_wall_posts == 0) { - $queryBase .= " AND ((`posts`.`wall` < 0 AND (`posts`.`flags` & 128) > 0) OR (`posts`.`wall` > 0 AND `posts`.`wall` = `posts`.`owner`))"; - } - - if ($this->getUser()->getNsfwTolerance() === User::NSFW_INTOLERANT) { - $queryBase .= " AND `nsfw` = 0"; - } - - if ($return_banned == 0) { - $ignored_sources_ids = $this->getUser()->getIgnoredSources(0, OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50, true); - - if (sizeof($ignored_sources_ids) > 0) { - $imploded_ids = implode("', '", $ignored_sources_ids); - $queryBase .= " AND `posts`.`wall` NOT IN ('$imploded_ids')"; - } - } - $start_time = empty($start_time) ? 0 : $start_time; $end_time = empty($end_time) ? PHP_INT_MAX : $end_time; + $queryBase = (new PostsRepo())->getGlobalFeedQuery( + user: $this->getUser(), + with_alien_wall_posts: $with_alien_wall_posts === 1, + return_banned: $return_banned === 1 + ); + $cursorFilter = " AND (`posts`.`created` < {$cursorTime} OR (`posts`.`created` = {$cursorTime} AND `posts`.`id` < {$cursorId}))"; $posts = DatabaseConnection::i()->getConnection()->query( @@ -173,13 +152,7 @@ public function search(string $q = "", int $extended = 1, int $count = 30, int $ $postsRepo = new PostsRepo(); - $queryBase = DatabaseConnection::i()->getContext() - ->table("posts") - ->select("id, created") - ->where("content LIKE ?", "%{$q}%") - ->where("deleted", 0) - ->where("suggested", 0) - ->where("archived", 0) + $queryBase = $postsRepo->getSearchSelection($q)->select("id, created") ->where("created <= ?", $cursorTime) ->where("created < ? OR id < ?", $cursorTime, $cursorId) ->where("? <= created", $start_time) diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index 5747b1944..0fff05041 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -69,7 +69,11 @@ public function get(int $owner_id, string $domain = "", int $offset = 0, int $co $cnt = $posts->getOthersCountOnUserWall($owner_id); break; case "postponed": - $this->fail(42, "Postponed posts are not implemented."); + if (!$wallOnwer->canPlanPosts($this->getUser())) { + $this->fail(15, "Access denied"); + } + $iteratorv = $posts->getPlannedPostsFromWall($owner_id, 1, $count, $offset); + $cnt = $posts->getPlannedCountOnUserWall($owner_id); break; case "suggests": if ($owner_id < 0) { @@ -336,7 +340,7 @@ public function getById(string $posts, int $extended = 0, string $fields = "", U foreach ($psts as $pst) { $id = explode("_", $pst); - $post = (new PostsRepo())->getPostById(intval($id[0]), intval($id[1]), true); + $post = (new PostsRepo())->getPostById(intval($id[0]), intval($id[1]), true, true); if ($post && !$post->isDeleted()) { if (!$post->canBeViewedBy($user)) { @@ -347,6 +351,10 @@ public function getById(string $posts, int $extended = 0, string $fields = "", U continue; } + if ($post->isPlanned() && !$post->getOwner()->canPlanPosts($this->getUser())) { + continue; + } + $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); $attachments = []; $repost = []; // чел высрал семь сигарет 😳 помянем 🕯 @@ -571,7 +579,8 @@ public function post( int $explicit = 0, float $lat = null, float $long = null, - string $place_name = '' + string $place_name = '', + int $publish_date = 0 ): object { $this->requireUser(); $this->willExecuteWriteAction(); @@ -597,16 +606,26 @@ public function post( } if ($post_id > 0) { - if ($owner_id > 0) { - $this->fail(62, "Suggested posts available only at groups"); - } - - $post = (new PostsRepo())->getPostById($owner_id, $post_id, true); + $post = (new PostsRepo())->getPostById($owner_id, $post_id, true, true); if (!$post || $post->isDeleted()) { $this->fail(32, "Invald post"); } + if ($post->isPlanned()) { + if (!$wallOwner->canPlanPosts($this->getUser())) { + $this->fail(16, "Access denied"); + } + + $post->setCreated(time()); + $post->save(); + return (object) ["post_id" => $post->getVirtualId()]; + } + + if ($owner_id > 0) { + $this->fail(62, "Suggested posts available only at groups"); + } + if ($post->getSuggestionType() == 0) { $this->fail(20, "Post is not suggested"); } @@ -679,11 +698,27 @@ public function post( $this->fail(100, "Required parameter 'message' missing."); } + $time = time(); + $planned = false; + + if ($publish_date > 0) { + if (!$wallOwner->canPlanPosts($this->getUser())) { + $this->fail(7, "Access to planning posts denied"); + } + + if ($publish_date < time()) { + $this->fail(102, "publish_date must be in future"); + } + + $time = $publish_date; + $planned = true; + } + try { $post = new Post(); $post->setOwner($this->getUser()->getId()); $post->setWall($owner_id); - $post->setCreated(time()); + $post->setCreated($time); $post->setContent($message); $post->setFlags($flags); $post->setApi_Source_Name($this->getPlatform()); @@ -755,7 +790,7 @@ public function post( $post->attach($attachment); } - if ($owner_id > 0 && $owner_id !== $this->getUser()->getId()) { + if ($owner_id > 0 && $owner_id !== $this->getUser()->getId() && !$planned) { (new WallPostNotification($wallOwner, $post, $this->getUser()))->emit(); } @@ -1161,7 +1196,7 @@ public function delete(int $owner_id, int $post_id) $this->requireUser(); $this->willExecuteWriteAction(); - $post = (new PostsRepo())->getPostById($owner_id, $post_id, true); + $post = (new PostsRepo())->getPostById($owner_id, $post_id, true, true); if (!$post || $post->isDeleted()) { $this->fail(15, "Not found"); } @@ -1187,7 +1222,7 @@ public function delete(int $owner_id, int $post_id) } } - public function edit(int $owner_id, int $post_id, string $message = "", string $attachments = "", string $copyright = null, int $explicit = -1, int $from_group = 0, int $signed = 0) + public function edit(int $owner_id, int $post_id, string $message = "", string $attachments = "", string $copyright = null, int $explicit = -1, int $from_group = 0, int $signed = 0, int $publish_date = 0) { $this->requireUser(); $this->willExecuteWriteAction(); @@ -1205,7 +1240,7 @@ public function edit(int $owner_id, int $post_id, string $message = "", string $ $this->fail(-66, "Post will be empty, don't saving."); } - $post = (new PostsRepo())->getPostById($owner_id, $post_id, true); + $post = (new PostsRepo())->getPostById($owner_id, $post_id, true, true); if (!$post || $post->isDeleted()) { $this->fail(102, "Invalid post"); @@ -1249,6 +1284,23 @@ public function edit(int $owner_id, int $post_id, string $message = "", string $ $post->setFlags($flags); + if ($publish_date > 0 && $publish_date != $post->getCreated()) { + + if (!$wallOwner?->canPlanPosts($this->getUser())) { + $this->fail(7, "Access to planning posts denied"); + } + + if ($post->getCreated() < time()) { + $this->fail(102, "Post is already published"); + } + + if ($publish_date < time()) { + $this->fail(102, "Publish date must be in future"); + } + + $post->setCreated($publish_date); + } + $post->save(true); if ($attachments == 'remove' || sizeof($final_attachments) > 0) { diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 0eb2db4ab..946161443 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -655,4 +655,10 @@ public function getFinishDate(): ?DateTime { return !is_null($this->getRecord()->finish_date) ? new DateTime($this->getRecord()->finish_date) : null; } + + public function canPlanPosts(?User $user): bool + { + return $user && $this->canBeModifiedBy($user); + } + } diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php index 6b7a81951..779f5e8e5 100644 --- a/Web/Models/Entities/Post.php +++ b/Web/Models/Entities/Post.php @@ -183,6 +183,16 @@ public function setArchived(bool $archived): void } } + public function getCreated(): int + { + return $this->getRecord()->created; + } + + public function isPlanned(): bool + { + return $this->getRecord()->created > time(); + } + public function getOwnerPost(): int { return $this->getOwner(false)->getId(); @@ -308,6 +318,10 @@ public function canBePinnedBy(User $user = null): bool return false; } + if ($this->isPlanned()) { + return false; + } + if ($this->getTargetWall() < 0) { $club = (new Clubs())->get(abs($this->getTargetWall())); diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index d39f0faab..85fcc721e 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -1937,4 +1937,10 @@ public function resetEvents(array $list): void $this->stateChanges("events_refresh_time", time()); $this->save(); } + + public function canPlanPosts(?User $user): bool + { + return $this->getId() == $user?->getId(); + } + } diff --git a/Web/Models/Repositories/Posts.php b/Web/Models/Repositories/Posts.php index 9dcbcf57d..5fd790db5 100644 --- a/Web/Models/Repositories/Posts.php +++ b/Web/Models/Repositories/Posts.php @@ -28,6 +28,106 @@ private function toPost(?ActiveRow $ar): ?Post return is_null($ar) ? null : new Post($ar); } + private function getBaseSelection(): Selection + { + return (clone $this->posts)->where([ + "deleted" => false, + "suggested" => 0, + "archived" => false, + ])->where("created <= ?", time()); + } + + private function getWallSelection(int $user): Selection + { + return $this->getBaseSelection()->where([ + "wall" => $user, + ]); + } + + private function getOwnersWallSelection(int $user): Selection + { + $sel = $this->getWallSelection($user); + if ($user > 0) { + $sel->where("owner", $user); + } else { + $sel->where("flags !=", 0); + } + + return $sel; + } + + private function getOthersWallSelection(int $user): Selection + { + $sel = $this->getWallSelection($user); + if ($user > 0) { + $sel->where("owner !=", $user); + } else { + $sel->where("flags", 0); + } + + return $sel; + } + + private function getArchivedWallSelection(int $user, ?int $year = null): Selection + { + $sel = (clone $this->posts)->where([ + "wall" => $user, + "deleted" => false, + "suggested" => 0, + "archived" => true, + ]); + + return $this->applyYearFilter($sel, $year); + } + + private function getPlannedWallSelection(int $user): Selection + { + return (clone $this->posts)->where([ + "wall" => $user, + "deleted" => false, + "archived" => false, + ])->order("created ASC")->where("created > ?", time()); + } + + public function getFeedSelection(array $user_ids): Selection + { + return $this->getBaseSelection()->where("wall IN (?)", $user_ids); + } + + public function getSearchSelection(string $query): Selection + { + return $this->getBaseSelection()->where("content LIKE ?", "%{$query}%"); + } + + public function getGlobalFeedQuery( + User $user, + bool $with_alien_wall_posts = false, + bool $return_banned = false + ): string { + $time = time(); + + $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)"; + $queryBase .= " WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND ((`profiles`.`profile_type` = 0 AND `profiles`.`hide_global_feed` = 0) OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0 AND `posts`.`archived` = 0 and `posts`.`created` <= $time"; + + if (!$with_alien_wall_posts) { + $queryBase .= " AND ((`posts`.`wall` < 0 AND (`posts`.`flags` & 128) > 0) OR (`posts`.`wall` > 0 AND `posts`.`wall` = `posts`.`owner`))"; + } + + if ($user->getNsfwTolerance() === User::NSFW_INTOLERANT) { + $queryBase .= " AND `nsfw` = 0"; + } + + if (!$return_banned) { + $ignored_sources_ids = $user->getIgnoredSources(0, OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50, true); + + if (sizeof($ignored_sources_ids) > 0) { + $imploded_ids = implode("', '", $ignored_sources_ids); + $queryBase .= " AND `posts`.`wall` NOT IN ('$imploded_ids')"; + } + } + return $queryBase; + } + public function get(int $id): ?Post { return self::$cache[$id] ??= $this->toPost($this->posts->get($id)); @@ -45,6 +145,18 @@ public function getPinnedPost(int $user): ?Post return $this->toPost($post); } + private function listPosts(Selection $sel, int $perPage, int $offset, ?string $order = "created DESC"): \Traversable + { + if (!is_null($order)) { + $sel = $sel->order($order); + } + + $sel = $sel->limit($perPage, $offset); + foreach ($sel as $post) { + yield $post->id => new Post($post); + } + } + public function getPostsFromUsersWall(int $user, int $page = 1, ?int $perPage = null, ?int $offset = null): \Traversable { $perPage ??= OPENVK_DEFAULT_PER_PAGE; @@ -56,7 +168,7 @@ public function getPostsFromUsersWall(int $user, int $page = 1, ?int $perPage = if ($page === 1) { $perPage--; - yield $pinPost; + yield $pinPost->getId() => $pinPost; } else { $offset--; } @@ -65,17 +177,7 @@ public function getPostsFromUsersWall(int $user, int $page = 1, ?int $perPage = $offset--; } - $sel = $this->posts->where([ - "wall" => $user, - "pinned" => false, - "deleted" => false, - "suggested" => 0, - "archived" => false, - ])->order("created DESC")->limit($perPage, $offset); - - foreach ($sel as $post) { - yield new Post($post); - } + yield from $this->listPosts($this->getWallSelection($user)->where(["pinned" => false]), $perPage, $offset); } public function getOwnersPostsFromWall(int $user, int $page = 1, ?int $perPage = null, ?int $offset = null): \Traversable @@ -83,24 +185,7 @@ public function getOwnersPostsFromWall(int $user, int $page = 1, ?int $perPage = $perPage ??= OPENVK_DEFAULT_PER_PAGE; $offset ??= $perPage * ($page - 1); - $sel = $this->posts->where([ - "wall" => $user, - "deleted" => false, - "suggested" => 0, - "archived" => false, - ]); - - if ($user > 0) { - $sel->where("owner", $user); - } else { - $sel->where("flags !=", 0); - } - - $sel->order("created DESC")->limit($perPage, $offset); - - foreach ($sel as $post) { - yield new Post($post); - } + yield from $this->listPosts($this->getOwnersWallSelection($user), $perPage, $offset); } public function getOthersPostsFromWall(int $user, int $page = 1, ?int $perPage = null, ?int $offset = null): \Traversable @@ -108,21 +193,20 @@ public function getOthersPostsFromWall(int $user, int $page = 1, ?int $perPage = $perPage ??= OPENVK_DEFAULT_PER_PAGE; $offset ??= $perPage * ($page - 1); - $sel = $this->posts->where([ - "wall" => $user, - "deleted" => false, - "suggested" => 0, - "archived" => false, - ]); + yield from $this->listPosts($this->getOthersWallSelection($user), $perPage, $offset); + } - if ($user > 0) { - $sel->where("owner !=", $user); - } else { - $sel->where("flags", 0); - } + public function getPlannedPostsFromWall(int $user, int $page = 1, ?int $perPage = null, ?int $offset = null): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); - $sel->order("created DESC")->limit($perPage, $offset); + yield from $this->listPosts($this->getPlannedWallSelection($user), $perPage, $offset, null); + } + public function getAllPlannedPostsFromWall(int $user): \Traversable + { + $sel = $this->getPlannedWallSelection($user); foreach ($sel as $post) { yield new Post($post); } @@ -131,12 +215,9 @@ public function getOthersPostsFromWall(int $user, int $page = 1, ?int $perPage = public function getPostsByHashtag(string $hashtag, int $page = 1, ?int $perPage = null): \Traversable { $hashtag = "#$hashtag"; - $sel = $this->posts - ->where("MATCH (content) AGAINST (? IN BOOLEAN MODE)", "+$hashtag") - ->where("deleted", 0) - ->where("archived", 0) + $sel = $this->getBaseSelection() ->order("created DESC") - ->where("suggested", 0) + ->where("MATCH (content) AGAINST (? IN BOOLEAN MODE)", "+$hashtag") ->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); foreach ($sel as $post) { @@ -147,16 +228,12 @@ public function getPostsByHashtag(string $hashtag, int $page = 1, ?int $perPage public function getPostCountByHashtag(string $hashtag): int { $hashtag = "#$hashtag"; - $sel = $this->posts - ->where("content LIKE ?", "%$hashtag%") - ->where("deleted", 0) - ->where("archived", 0) - ->where("suggested", 0); + $sel = $this->getBaseSelection()->where("MATCH (content) AGAINST (? IN BOOLEAN MODE)", "+$hashtag"); return sizeof($sel); } - public function getPostById(int $wall, int $post, bool $forceSuggestion = false): ?Post + public function getPostById(int $wall, int $post, bool $forceSuggestion = false, bool $showPlanned = false): ?Post { $post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post]); @@ -164,6 +241,10 @@ public function getPostById(int $wall, int $post, bool $forceSuggestion = false) $post->where("suggested", 0); } + if (!$showPlanned) { + $post->where("created <= ?", time()); + } + $post = $post->fetch(); if (!is_null($post)) { @@ -176,8 +257,7 @@ public function getPostById(int $wall, int $post, bool $forceSuggestion = false) public function find(string $query = "", array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $query = "%$query%"; - $result = $this->posts->where("content LIKE ?", $query)->where("deleted", 0)->where("suggested", 0)->where("archived", 0); + $result = $this->getSearchSelection($query); $order_str = 'id'; switch ($order['type']) { @@ -245,25 +325,22 @@ public function find(string $query = "", array $params = [], array $order = ['ty public function getPostCountOnUserWall(int $user): int { - return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0, "archived" => 0])); + return sizeof($this->getWallSelection($user)); } public function getOwnersCountOnUserWall(int $user): int { - if ($user > 0) { - return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "archived" => 0, "owner" => $user])); - } else { - return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "archived" => 0, "suggested" => 0])->where("flags !=", 0)); - } + return sizeof($this->getOwnersWallSelection($user)); } public function getOthersCountOnUserWall(int $user): int { - if ($user > 0) { - return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "archived" => 0])->where("owner !=", $user)); - } else { - return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "archived" => 0, "suggested" => 0])->where("flags", 0)); - } + return sizeof($this->getOthersWallSelection($user)); + } + + public function getPlannedCountOnUserWall(int $user): int + { + return sizeof($this->getPlannedWallSelection($user)); } private function applyYearFilter(Selection $selection, ?int $year): Selection @@ -282,7 +359,7 @@ public function getPostYearsOnWall(int $user): array "wall" => $user, "deleted" => false, "suggested" => 0, - ]); + ])->where("created <= ?", time()); foreach ($posts as $post) { $years[(int) date("Y", $post->created)] = true; @@ -299,13 +376,7 @@ public function getArchivedPostsFromWall(int $user, int $page = 1, ?int $perPage $perPage ??= OPENVK_DEFAULT_PER_PAGE; $offset ??= $perPage * ($page - 1); - $sel = (clone $this->posts)->where([ - "wall" => $user, - "deleted" => false, - "suggested" => 0, - "archived" => true, - ]); - $this->applyYearFilter($sel, $year)->order("created DESC")->limit($perPage, $offset); + $sel = $this->getArchivedWallSelection($user, $year)->order("created DESC")->limit($perPage, $offset); foreach ($sel as $post) { yield new Post($post); @@ -314,9 +385,7 @@ public function getArchivedPostsFromWall(int $user, int $page = 1, ?int $perPage public function getArchivedCountOnUserWall(int $user, ?int $year = null): int { - $posts = (clone $this->posts)->where(["wall" => $user, "deleted" => 0, "archived" => 1, "suggested" => 0]); - - return sizeof($this->applyYearFilter($posts, $year)); + return sizeof($this->getArchivedWallSelection($user, $year)); } public function setArchivedOnWall(int $user, bool $archived, ?int $year = null): int @@ -325,7 +394,7 @@ public function setArchivedOnWall(int $user, bool $archived, ?int $year = null): "wall" => $user, "deleted" => false, "suggested" => 0, - ]); + ])->where("created <= ?", time()); $this->applyYearFilter($posts, $year); $changes = ["archived" => $archived]; diff --git a/Web/Presenters/InternalAPIPresenter.php b/Web/Presenters/InternalAPIPresenter.php index 4e01c2243..5478b6c8c 100644 --- a/Web/Presenters/InternalAPIPresenter.php +++ b/Web/Presenters/InternalAPIPresenter.php @@ -154,7 +154,7 @@ public function renderGetPostTemplate(int $owner_id, int $post_id) $type = $this->queryParam("type", false); if ($type == "post") { - $post = (new Posts())->getPostById($owner_id, $post_id, true); + $post = (new Posts())->getPostById($owner_id, $post_id, true, true); } else { $post = (new Comments())->get($post_id); } diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index cd1c6c7fe..4eb587487 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -7,7 +7,16 @@ use openvk\Web\Models\Exceptions\TooMuchOptionsException; use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User}; use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification, PostAcceptedNotification, NewSuggestedPostsNotification}; -use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos, Audios}; +use openvk\Web\Models\Repositories\{Posts, + Posts as PostsRepo, + Users, + Clubs, + Albums, + Notes, + Videos, + Comments, + Photos, + Audios}; use Chandler\Database\DatabaseConnection; use Nette\InvalidStateException as ISE; use Bhaktaraz\RSSGenerator\Item; @@ -72,14 +81,7 @@ private function setWithAlienWallPostsCookie(int $value): void public function renderWall(int $user, bool $embedded = false): void { - $owner = ($user < 0 ? (new Clubs()) : (new Users()))->get(abs($user)); - if (!$owner || $owner->isBanned() || !$owner->canBeViewedBy($this->user->identity)) { - $this->flashFail("err", tr("error"), tr("forbidden")); - } - - if ($user > 0 && $owner->isDeleted()) { - $this->flashFail("err", tr("error"), tr("forbidden")); - } + $owner = $this->getWallOwnerById($user); if (is_null($this->user->identity)) { $canPost = false; @@ -162,10 +164,31 @@ public function renderWall(int $user, bool $embedded = false): void "atTop" => false, ]; + $this->template->canPostPlanned = false; + $this->template->plannedCount = 0; + + if ($owner->canPlanPosts($this->user->identity)) { + $this->template->canPostPlanned = true; + $this->template->plannedCount = $this->posts->getPlannedCountOnUserWall($user); + } $this->logPostsViewed($this->template->posts, $user); } + public function renderWallPlanned(int $user): void + { + $owner = $this->getWallOwnerById($user); + + $posts = []; + if ($owner->canPlanPosts($this->user->identity)) { + $iterator = $this->posts->getAllPlannedPostsFromWall($user); + $posts = iterator_to_array($iterator); + } + + $this->template->posts = $posts; + $this->template->_template = "components/wall/planned.latte"; + } + public function renderWallEmbedded(int $user): void { $this->renderWall($user, true); @@ -232,15 +255,7 @@ public function renderFeed(): void $perPage = min((int) ($_GET["posts"] ?? OPENVK_DEFAULT_PER_PAGE), 50); $withAlienWallPosts = $this->getWithAlienWallPostsPreference(); - $posts = DatabaseConnection::i() - ->getContext() - ->table("posts") - ->select("id") - ->where("wall IN (?)", $ids) - ->where("deleted", 0) - ->where("suggested", 0) - ->where("archived", 0) - ->order("created DESC"); + $posts = $this->posts->getFeedSelection($ids)->order("created DESC")->select("id"); if ($withAlienWallPosts === 0) { $posts->where("(`posts`.`wall` < 0 AND (`posts`.`flags` & 128) > 0) OR (`posts`.`wall` > 0 AND `posts`.`wall` = `posts`.`owner`)"); @@ -268,26 +283,11 @@ public function renderGlobalFeed(): void $withAlienWallPosts = $this->getWithAlienWallPostsPreference(); - $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)"; - $queryBase .= " WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND ((`profiles`.`profile_type` = 0 AND `profiles`.`hide_global_feed` = 0) OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0 AND `posts`.`archived` = 0"; - - if ($withAlienWallPosts === 0) { - $queryBase .= " AND ((`posts`.`wall` < 0 AND (`posts`.`flags` & 128) > 0) OR (`posts`.`wall` > 0 AND `posts`.`wall` = `posts`.`owner`))"; - } - - if ($this->user->identity->getNsfwTolerance() === User::NSFW_INTOLERANT) { - $queryBase .= " AND `nsfw` = 0"; - } - - if (((int) $this->queryParam('return_banned')) == 0) { - $ignored_sources_ids = $this->user->identity->getIgnoredSources(0, OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50, true); - - if (sizeof($ignored_sources_ids) > 0) { - $imploded_ids = implode("', '", $ignored_sources_ids); - - $queryBase .= " AND `posts`.`wall` NOT IN ('$imploded_ids')"; - } - } + $queryBase = (new PostsRepo())->getGlobalFeedQuery( + user: $this->user->identity, + with_alien_wall_posts: (int) $withAlienWallPosts === 1, + return_banned: ((int) $this->queryParam('return_banned')) === 1 + ); $posts = DatabaseConnection::i()->getConnection()->query("SELECT `posts`.`id` " . $queryBase . " ORDER BY `created` DESC LIMIT " . $pPage . " OFFSET " . ($page - 1) * $pPage); $count = DatabaseConnection::i()->getConnection()->query("SELECT COUNT(*) " . $queryBase)->fetch()->{"COUNT(*)"}; @@ -421,6 +421,25 @@ public function renderMakePost(int $wall): void } } + $created_at = time(); + $planned = false; + + if (!is_null($this->postParam("publish_date"))) { + if (!$wallOwner->canPlanPosts($this->user->identity)) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + + $time = (int) $this->postParam("publish_date"); + $now = time(); + + if ($time < $now) { + $this->flashFail("err", tr("error"), tr("planned_time_is_in_past")); + } + + $created_at = $time; + $planned = true; + } + if (empty($this->postParam("text")) && sizeof($horizontal_attachments) < 1 && sizeof($vertical_attachments) < 1 && !$poll) { $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); } @@ -429,12 +448,12 @@ public function renderMakePost(int $wall): void $this->flashFail("err", tr("error"), tr("limit_exceed_exception")); } - $should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2; + $should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2 && !$planned; try { $post = new Post(); $post->setOwner($this->user->id); $post->setWall($wall); - $post->setCreated(time()); + $post->setCreated($created_at); $post->setContent($this->postParam("text")); $post->setAnonymous($anon); $post->setFlags($flags); @@ -481,7 +500,7 @@ public function renderMakePost(int $wall): void $post->attach($poll); } - if ($wall > 0 && $wall !== $this->user->identity->getId()) { + if ($wall > 0 && $wall !== $this->user->identity->getId() && !$planned) { $disturber = $this->user->identity; if ($anon) { $disturber = $post->getOwner(); @@ -495,7 +514,7 @@ public function renderMakePost(int $wall): void $excludeMentions[] = $wall; } - if (!$should_be_suggested) { + if (!$should_be_suggested && !$planned) { $mentions = iterator_to_array($post->resolveMentions($excludeMentions)); foreach ($mentions as $mentionee) { @@ -527,6 +546,10 @@ public function renderPost(int $wall, int $post_id): void $this->flashFail("err", tr("error"), tr("forbidden")); } + if ($post->isPlanned()) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + $this->logPostView($post, $wall); $this->template->post = $post; @@ -652,7 +675,7 @@ public function renderDelete(int $wall, int $post_id): void $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - $post = $this->posts->getPostById($wall, $post_id, true); + $post = $this->posts->getPostById($wall, $post_id, true, true); if (!$post) { $this->notFound(); } @@ -960,4 +983,19 @@ public function renderLikers(string $type, int $owner_id, int $item_id) $this->template->page = $page; $this->template->perPage = OPENVK_DEFAULT_PER_PAGE; } + + /* @return User|Club */ + private function getWallOwnerById(int $id) + { + $owner = ($id < 0 ? (new Clubs()) : (new Users()))->get(abs($id)); + + if (!$owner || $owner->isBanned() || !$owner->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + + if ($id > 0 && $owner->isDeleted()) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + return $owner; + } } diff --git a/Web/Presenters/templates/Wall/Wall.latte b/Web/Presenters/templates/Wall/Wall.latte index 948cc168b..fb9632e9b 100644 --- a/Web/Presenters/templates/Wall/Wall.latte +++ b/Web/Presenters/templates/Wall/Wall.latte @@ -4,7 +4,7 @@ {block header} {$oObj->getCanonicalName()} » - {_wall} + {_wall}
{tr("wall", $count)} @@ -43,7 +43,7 @@
- +
{_archive_banner_title} @@ -109,7 +109,7 @@
- +
{if sizeof($posts) > 0}
- + {include "../components/post.latte", post => $post, commentSection => true}
{include "../components/paginator.latte", conf => $paginatorConf} diff --git a/Web/Presenters/templates/components/post/microblogpost.latte b/Web/Presenters/templates/components/post/microblogpost.latte index b2533e92b..846a0e9e6 100644 --- a/Web/Presenters/templates/components/post/microblogpost.latte +++ b/Web/Presenters/templates/components/post/microblogpost.latte @@ -9,6 +9,8 @@ {var $canBeDeleted = $post->canBeDeletedBy($thisUser)} {var $canBeArchived = $post->canBeArchivedBy($thisUser ?? NULL)} {var $wallOwner = $post->getWallOwner()} +{var $isPlanned = $post->isPlanned()} + {if $post->isDeactivationMessage() && $post->getText()} {var $deac = "post_deact"} {else} @@ -21,7 +23,7 @@ {var $bigText = mb_strlen($post->getText()) > 520 || substr_count($post->getText(), "\n") + 1 > 10} - +