- + {$post->getPublicationTime()} ({_edited_short}){if $post->isPinned()}, {_pinned}{/if} +
diff --git a/ServiceAPI/Wall.php b/ServiceAPI/Wall.php index 787a998ef..06d7353ce 100644 --- a/ServiceAPI/Wall.php +++ b/ServiceAPI/Wall.php @@ -22,7 +22,10 @@ function getPost(int $id, callable $resolve, callable $reject): void { $post = $this->posts->get($id); if(!$post || $post->isDeleted()) - $reject("No post with id=$id"); + $reject(53, "No post with id=$id"); + + if($post->getSuggestionType() != 0) + $reject(25, "Can't get suggested post"); $res = (object) []; $res->id = $post->getId(); @@ -96,7 +99,7 @@ function getMyNotes(callable $resolve, callable $reject) $resolve($arr); } - + function getVideos(int $page = 1, callable $resolve, callable $reject) { $videos = $this->videos->getByUser($this->user, $page, 8); @@ -129,7 +132,7 @@ function searchVideos(int $page = 1, string $query, callable $resolve, callable ]; foreach($videos as $video) { - $res = json_decode(json_encode($video->toVkApiStruct()), true); + $res = json_decode(json_encode($video->toVkApiStruct($this->user)), true); $res["video"]["author_name"] = $video->getOwner()->getCanonicalName(); $arr["items"][] = $res; diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php index 007b68eab..5dc47316a 100644 --- a/VKAPI/Handlers/Groups.php +++ b/VKAPI/Handlers/Groups.php @@ -2,6 +2,7 @@ namespace openvk\VKAPI\Handlers; use openvk\Web\Models\Repositories\Clubs as ClubsRepo; use openvk\Web\Models\Repositories\Users as UsersRepo; +use openvk\Web\Models\Repositories\Posts as PostsRepo; use openvk\Web\Models\Entities\Club; final class Groups extends VKAPIRequestHandler @@ -80,6 +81,19 @@ function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count break; case "members_count": $rClubs[$i]->members_count = $usr->getFollowersCount(); + break; + case "can_suggest": + $rClubs[$i]->can_suggest = !$usr->canBeModifiedBy($this->getUser()) && $usr->getWallType() == 2; + break; + # unstandard feild + case "suggested_count": + if($usr->getWallType() != 2) { + $rClubs[$i]->suggested_count = NULL; + break; + } + + $rClubs[$i]->suggested_count = $usr->getSuggestedPostsCount($this->getUser()); + break; } } @@ -188,7 +202,19 @@ function getById(string $group_ids = "", string $group_id = "", string $fields = case "description": $response[$i]->description = $clb->getDescription(); break; - case "contacts": + case "can_suggest": + $response[$i]->can_suggest = !$clb->canBeModifiedBy($this->getUser()) && $clb->getWallType() == 2; + break; + # unstandard feild + case "suggested_count": + if($clb->getWallType() != 2) { + $response[$i]->suggested_count = NULL; + break; + } + + $response[$i]->suggested_count = $clb->getSuggestedPostsCount($this->getUser()); + break; + case "contacts": $contacts; $contactTmp = $clb->getManagers(1, true); @@ -288,7 +314,7 @@ function edit( string $description = NULL, string $screen_name = NULL, string $website = NULL, - int $wall = NULL, + int $wall = -1, int $topics = NULL, int $adminlist = NULL, int $topicsAboveWall = NULL, @@ -308,17 +334,26 @@ function edit( !empty($description) ? $club->setAbout($description) : NULL; !empty($screen_name) ? $club->setShortcode($screen_name) : NULL; !empty($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; - !empty($wall) ? $club->setWall($wall) : NULL; + + try { + $wall != -1 ? $club->setWall($wall) : NULL; + } catch(\Exception $e) { + $this->fail(50, "Invalid wall value"); + } + !empty($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL; !empty($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL; !empty($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL; !empty($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL; + in_array($audio, [0, 1]) ? $club->setEveryone_can_upload_audios($audio) : NULL; try { $club->save(); } catch(\TypeError $e) { - $this->fail(8, "Nothing changed"); + $this->fail(15, "Nothing changed"); + } catch(\Exception $e) { + $this->fail(18, "An unknown error occurred: maybe you set an incorrect value?"); } return 1; @@ -472,7 +507,7 @@ function getSettings(string $group_id) "title" => $club->getName(), "description" => $club->getDescription() != NULL ? $club->getDescription() : "", "address" => $club->getShortcode(), - "wall" => $club->canPost() == true ? 1 : 0, + "wall" => $club->getWallType(), # отличается от вкшных но да ладно "photos" => 1, "video" => 0, "audio" => $club->isEveryoneCanUploadAudios() ? 1 : 0, diff --git a/VKAPI/Handlers/Newsfeed.php b/VKAPI/Handlers/Newsfeed.php index d99924304..4833e7beb 100644 --- a/VKAPI/Handlers/Newsfeed.php +++ b/VKAPI/Handlers/Newsfeed.php @@ -51,7 +51,7 @@ function getGlobal(string $fields = "", int $start_from = 0, int $start_time = 0 { $this->requireUser(); - $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND `posts`.`deleted` = 0"; + $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0"; if($this->getUser()->getNsfwTolerance() === User::NSFW_INTOLERANT) $queryBase .= " AND `nsfw` = 0"; diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index fde87fcbe..b6082c616 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -1,7 +1,7 @@ requireUser(); @@ -29,7 +29,7 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 $items = []; $profiles = []; $groups = []; - $cnt = $posts->getPostCountOnUserWall($owner_id); + $cnt = 0; if ($owner_id > 0) $wallOnwer = (new UsersRepo)->get($owner_id); @@ -43,7 +43,47 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 if(!$wallOnwer) $this->fail(15, "Access denied: wall is disabled"); // Don't search for logic here pls - foreach($posts->getPostsFromUsersWall($owner_id, 1, $count, $offset) as $post) { + $iteratorv; + + switch($filter) { + case "all": + $iteratorv = $posts->getPostsFromUsersWall($owner_id, 1, $count, $offset); + $cnt = $posts->getPostCountOnUserWall($owner_id); + break; + case "owner": + $iteratorv = $posts->getOwnersPostsFromWall($owner_id, 1, $count, $offset); + $cnt = $posts->getOwnersCountOnUserWall($owner_id); + break; + case "others": + $iteratorv = $posts->getOthersPostsFromWall($owner_id, 1, $count, $offset); + $cnt = $posts->getOthersCountOnUserWall($owner_id); + break; + case "postponed": + $this->fail(42, "Postponed posts are not implemented."); + break; + case "suggests": + if($owner_id < 0) { + if($wallOnwer->getWallType() != 2) + $this->fail(125, "Group's wall type is open or closed"); + + if($wallOnwer->canBeModifiedBy($this->getUser())) { + $iteratorv = $posts->getSuggestedPosts($owner_id * -1, 1, $count, $offset); + $cnt = $posts->getSuggestedPostsCount($owner_id * -1); + } else { + $iteratorv = $posts->getSuggestedPostsByUser($owner_id * -1, $this->getUser()->getId(), 1, $count, $offset); + $cnt = $posts->getSuggestedPostsCountByUser($owner_id * -1, $this->getUser()->getId()); + } + } else { + $this->fail(528, "Suggested posts avaiable only at groups"); + } + + break; + default: + $this->fail(254, "Invalid filter"); + break; + } + + foreach($iteratorv as $post) { $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); $attachments = []; @@ -118,12 +158,23 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 ]; } + $postType = "post"; + $signerId = NULL; + if($post->getSuggestionType() != 0) + $postType = "suggest"; + + + if($post->isSigned()) { + $actualAuthor = $post->getOwner(false); + $signerId = $actualAuthor->getId(); + } + $items[] = (object)[ "id" => $post->getVirtualId(), "from_id" => $from_id, "owner_id" => $post->getTargetWall(), "date" => $post->getPublicationTime()->timestamp(), - "post_type" => "post", + "post_type" => $postType, "text" => $post->getText(false), "copy_history" => $repost, "can_edit" => $post->canBeEditedBy($this->getUser()), @@ -135,6 +186,7 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 "is_explicit" => $post->isExplicit(), "attachments" => $attachments, "post_source" => $post_source, + "signer_id" => $signerId, "comments" => (object)[ "count" => $post->getCommentsCount(), "can_post" => 1 @@ -156,6 +208,9 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 else $groups[] = $from_id * -1; + if($post->isSigned()) + $profiles[] = $post->getOwner(false)->getId(); + $attachments = NULL; # free attachments so it will not clone everythingg } @@ -298,12 +353,24 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us ]; } + # TODO: $post->getVkApiType() + $postType = "post"; + $signerId = NULL; + if($post->getSuggestionType() != 0) + $postType = "suggest"; + + + if($post->isSigned()) { + $actualAuthor = $post->getOwner(false); + $signerId = $actualAuthor->getId(); + } + $items[] = (object)[ "id" => $post->getVirtualId(), "from_id" => $from_id, "owner_id" => $post->getTargetWall(), "date" => $post->getPublicationTime()->timestamp(), - "post_type" => "post", + "post_type" => $postType, "text" => $post->getText(false), "copy_history" => $repost, "can_edit" => $post->canBeEditedBy($this->getUser()), @@ -314,6 +381,7 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us "is_pinned" => $post->isPinned(), "is_explicit" => $post->isExplicit(), "post_source" => $post_source, + "signer_id" => $signerId, "attachments" => $attachments, "comments" => (object)[ "count" => $post->getCommentsCount(), @@ -336,6 +404,9 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us else $groups[] = $from_id * -1; + if($post->isSigned()) + $profiles[] = $post->getOwner(false)->getId(); + $attachments = NULL; # free attachments so it will not clone everything $repost = NULL; # same } @@ -391,7 +462,7 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us ]; } - function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0, string $attachments = ""): object + function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0, string $attachments = "", int $post_id = 0): object { $this->requireUser(); $this->willExecuteWriteAction(); @@ -412,6 +483,46 @@ function post(string $owner_id, string $message = "", int $from_group = 0, int $ if($canPost == false) $this->fail(15, "Access denied"); + 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); + + if(!$post || $post->isDeleted()) + $this->fail(32, "Invald post"); + + if($post->getSuggestionType() == 0) + $this->fail(20, "Post is not suggested"); + + if($post->getSuggestionType() == 2) + $this->fail(16, "Post is declined"); + + if(!$post->canBePinnedBy($this->getUser())) + $this->fail(51, "Access denied"); + + $author = $post->getOwner(); + $flags = 0; + $flags |= 0b10000000; + + if($signed == 1) + $flags |= 0b01000000; + + $post->setSuggested(0); + $post->setCreated(time()); + $post->setFlags($flags); + + if(!empty($message) && iconv_strlen($message) > 0) + $post->setContent($message); + + $post->save(); + + if($author->getId() != $this->getUser()->getId()) + (new PostAcceptedNotification($author, $post, $post->getWallOwner()))->emit(); + + return (object)["post_id" => $post->getVirtualId()]; + } + $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; if($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) { $manager = $wallOwner->getManager($this->getUser()); @@ -440,6 +551,10 @@ function post(string $owner_id, string $message = "", int $from_group = 0, int $ $post->setContent($message); $post->setFlags($flags); $post->setApi_Source_Name($this->getPlatform()); + + if($owner_id < 0 && !$wallOwner->canBeModifiedBy($this->getUser()) && $wallOwner->getWallType() == 2) + $post->setSuggested(1); + $post->save(); } catch(\LogicException $ex) { $this->fail(100, "One of the parameters specified was missing or invalid"); @@ -528,6 +643,22 @@ function post(string $owner_id, string $message = "", int $from_group = 0, int $ if($wall > 0 && $wall !== $this->user->identity->getId()) (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); + if($owner_id < 0 && !$wallOwner->canBeModifiedBy($this->getUser()) && $wallOwner->getWallType() == 2) { + $suggsCount = (new PostsRepo)->getSuggestedPostsCount($wallOwner->getId()); + + if($suggsCount % 10 == 0) { + $managers = $wallOwner->getManagers(); + $owner = $wallOwner->getOwner(); + (new NewSuggestedPostsNotification($owner, $wallOwner))->emit(); + + foreach($managers as $manager) { + (new NewSuggestedPostsNotification($manager->getUser(), $wallOwner))->emit(); + } + } + + return (object)["post_id" => "on_view"]; + } + return (object)["post_id" => $post->getVirtualId()]; } @@ -830,7 +961,31 @@ function deleteComment(int $comment_id) { return 1; } + + function delete(int $owner_id, int $post_id) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $post = (new PostsRepo)->getPostById($owner_id, $post_id, true); + if(!$post || $post->isDeleted()) + $this->fail(583, "Invalid post"); + $wallOwner = $post->getWallOwner(); + + if($post->getTargetWall() < 0 && !$post->getWallOwner()->canBeModifiedBy($this->getUser()) && $post->getWallOwner()->getWallType() != 1 && $post->getSuggestionType() == 0) + $this->fail(12, "Access denied: you can't delete your accepted post."); + + if($post->getOwnerPost() == $this->getUser()->getId() || $post->getTargetWall() == $this->getUser()->getId() || $owner_id < 0 && $wallOwner->canBeModifiedBy($this->getUser())) { + $post->unwire(); + $post->delete(); + + return 1; + } else { + $this->fail(15, "Access denied"); + } + } + function edit(int $owner_id, int $post_id, string $message = "", string $attachments = "") { $this->requireUser(); $this->willExecuteWriteAction(); diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 319678f30..5324efb6c 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -3,7 +3,7 @@ use openvk\Web\Util\DateTime; use openvk\Web\Models\RowModel; use openvk\Web\Models\Entities\{User, Manager}; -use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Managers}; +use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Managers, Posts}; use Nette\Database\Table\{ActiveRow, GroupedSelection}; use Chandler\Database\DatabaseConnection as DB; use Chandler\Security\User as ChandlerUser; @@ -23,6 +23,10 @@ class Club extends RowModel const NOT_RELATED = 0; const SUBSCRIBED = 1; const REQUEST_SENT = 2; + + const WALL_CLOSED = 0; + const WALL_OPEN = 1; + const WALL_LIMITED = 2; function getId(): int { @@ -45,6 +49,11 @@ function getAvatarUrl(string $size = "miniscule"): string return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURLBySizeId($size); } + + function getWallType(): int + { + return $this->getRecord()->wall; + } function getAvatarLink(): string { @@ -182,6 +191,14 @@ function setShortCode(?string $code = NULL): ?bool $this->stateChanges("shortcode", $code); return true; } + + function setWall(int $type) + { + if($type > 2 || $type < 0) + throw new \LogicException("Invalid wall"); + + $this->stateChanges("wall", $type); + } function isSubscriptionAccepted(User $user): bool { @@ -291,6 +308,21 @@ function getFollowers(int $page = 1, int $perPage = 6, string $sort = "follower yield $rel; } } + + function getSuggestedPostsCount(User $user = NULL) + { + $count = 0; + + if(is_null($user)) + return NULL; + + if($this->canBeModifiedBy($user)) + $count = (new Posts)->getSuggestedPostsCount($this->getId()); + else + $count = (new Posts)->getSuggestedPostsCountByUser($this->getId(), $user->getId()); + + return $count; + } function getManagers(int $page = 1, bool $ignoreHidden = false): \Traversable { diff --git a/Web/Models/Entities/Notifications/NewSuggestedPostsNotification.php b/Web/Models/Entities/Notifications/NewSuggestedPostsNotification.php new file mode 100644 index 000000000..e1795b08a --- /dev/null +++ b/Web/Models/Entities/Notifications/NewSuggestedPostsNotification.php @@ -0,0 +1,13 @@ +hasEnded() && !$this->hasVoted($user); + return !$this->hasEnded() && !$this->hasVoted($user) && !is_null($this->getAttachedPost()) && $this->getAttachedPost()->getSuggestionType() == 0; } function vote(User $user, array $optionIds): void @@ -292,4 +292,17 @@ function save(?bool $log = false): void ]); } } + + function getAttachedPost() + { + $post = DatabaseConnection::i()->getContext()->table("attachments") + ->where( + ["attachable_type" => static::class, + "attachable_id" => $this->getId()])->fetch(); + + if(!is_null($post->target_id)) + return (new Posts)->get($post->target_id); + else + return NULL; + } } diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php index a8d444eb1..48a2191fd 100644 --- a/Web/Models/Entities/Post.php +++ b/Web/Models/Entities/Post.php @@ -207,6 +207,9 @@ function canBePinnedBy(User $user): bool function canBeDeletedBy(User $user): bool { + if($this->getTargetWall() < 0 && !$this->getWallOwner()->canBeModifiedBy($user) && $this->getWallOwner()->getWallType() != 1 && $this->getSuggestionType() == 0) + return false; + return $this->getOwnerPost() === $user->getId() || $this->canBePinnedBy($user); } @@ -245,6 +248,11 @@ function deletePost(): void $this->unwire(); $this->save(); } + + function getSuggestionType() + { + return $this->getRecord()->suggested; + } function toNotifApiStruct() { @@ -273,6 +281,12 @@ function canBeEditedBy(?User $user = NULL): bool if($this->getTargetWall() > 0) return $this->getPublicationTime()->timestamp() + WEEK > time() && $user->getId() == $this->getOwner(false)->getId(); + else { + if($this->isPostedOnBehalfOfGroup()) + return $this->getWallOwner()->canBeModifiedBy($user); + else + return $user->getId() == $this->getOwner(false)->getId(); + } return $user->getId() == $this->getOwner(false)->getId(); } diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php index a64786051..55e1adf74 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -33,7 +33,7 @@ function getOwner(bool $real = false): RowModel { $oid = (int) $this->getRecord()->owner; if(!$real && $this->isAnonymous()) - $oid = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]; + $oid = (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]; $oid = abs($oid); if($oid > 0) diff --git a/Web/Models/Repositories/Posts.php b/Web/Models/Repositories/Posts.php index c354f1525..89ee58eaa 100644 --- a/Web/Models/Repositories/Posts.php +++ b/Web/Models/Repositories/Posts.php @@ -58,14 +58,59 @@ function getPostsFromUsersWall(int $user, int $page = 1, ?int $perPage = NULL, ? } $sel = $this->posts->where([ - "wall" => $user, - "pinned" => false, - "deleted" => false, + "wall" => $user, + "pinned" => false, + "deleted" => false, + "suggested" => 0, ])->order("created DESC")->limit($perPage, $offset); foreach($sel as $post) yield new Post($post); } + + function getOwnersPostsFromWall(int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts->where([ + "wall" => $user, + "deleted" => false, + "suggested" => 0, + ]); + + 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); + } + + function getOthersPostsFromWall(int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts->where([ + "wall" => $user, + "deleted" => false, + "suggested" => 0, + ]); + + 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); + } function getPostsByHashtag(string $hashtag, int $page = 1, ?int $perPage = NULL): \Traversable { @@ -74,6 +119,7 @@ function getPostsByHashtag(string $hashtag, int $page = 1, ?int $perPage = NULL) ->where("MATCH (content) AGAINST (? IN BOOLEAN MODE)", "+$hashtag") ->where("deleted", 0) ->order("created DESC") + ->where("suggested", 0) ->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); foreach($sel as $post) @@ -85,14 +131,22 @@ function getPostCountByHashtag(string $hashtag): int $hashtag = "#$hashtag"; $sel = $this->posts ->where("content LIKE ?", "%$hashtag%") - ->where("deleted", 0); + ->where("deleted", 0) + ->where("suggested", 0); return sizeof($sel); } - function getPostById(int $wall, int $post): ?Post + function getPostById(int $wall, int $post, bool $forceSuggestion = false): ?Post { - $post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post])->fetch(); + $post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post]); + + if(!$forceSuggestion) { + $post->where("suggested", 0); + } + + $post = $post->fetch(); + if(!is_null($post)) return new Post($post); else @@ -112,7 +166,7 @@ function find(string $query = "", array $pars = [], string $sort = "id"): Util\E else $paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL; - $result = $this->posts->where("content LIKE ?", $query)->where("deleted", 0); + $result = $this->posts->where("content LIKE ?", $query)->where("deleted", 0)->where("suggested", 0); $nnparamsCount = sizeof($notNullParams); if($nnparamsCount > 0) { @@ -134,7 +188,66 @@ function find(string $query = "", array $pars = [], string $sort = "id"): Util\E function getPostCountOnUserWall(int $user): int { - return sizeof($this->posts->where(["wall" => $user, "deleted" => 0])); + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])); + } + + function getOwnersCountOnUserWall(int $user): int + { + if($user > 0) + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "owner" => $user])); + else + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])->where("flags !=", 0)); + } + + function getOthersCountOnUserWall(int $user): int + { + if($user > 0) + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0])->where("owner !=", $user)); + else + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])->where("flags", 0)); + } + + function getSuggestedPosts(int $club, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts + ->where("deleted", 0) + ->where("wall", $club * -1) + ->order("created DESC") + ->where("suggested", 1) + ->limit($perPage, $offset); + + foreach($sel as $post) + yield new Post($post); + } + + function getSuggestedPostsCount(int $club) + { + return sizeof($this->posts->where(["wall" => $club * -1, "deleted" => 0, "suggested" => 1])); + } + + function getSuggestedPostsByUser(int $club, int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts + ->where("deleted", 0) + ->where("wall", $club * -1) + ->where("owner", $user) + ->order("created DESC") + ->where("suggested", 1) + ->limit($perPage, $offset); + + foreach($sel as $post) + yield new Post($post); + } + + function getSuggestedPostsCountByUser(int $club, int $user): int + { + return sizeof($this->posts->where(["wall" => $club * -1, "deleted" => 0, "suggested" => 1, "owner" => $user])); } function getCount(): int diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index eb8f446c8..beeede139 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -3,7 +3,7 @@ use openvk\Web\Models\Entities\{Club, Photo, Post}; use Nette\InvalidStateException; use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; -use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios}; +use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios, Posts}; use Chandler\Security\Authenticator; final class GroupPresenter extends OpenVKPresenter @@ -35,6 +35,13 @@ function renderView(int $id): void $this->template->audiosCount = (new Audios)->getClubCollectionSize($club); } + if(!is_null($this->user->identity) && $club->getWallType() == 2) { + if(!$club->canBeModifiedBy($this->user->identity)) + $this->template->suggestedPostsCountByUser = (new Posts)->getSuggestedPostsCountByUser($club->getId(), $this->user->id); + else + $this->template->suggestedPostsCountByEveryone = (new Posts)->getSuggestedPostsCount($club->getId()); + } + $this->template->club = $club; } } @@ -216,7 +223,12 @@ function renderEdit(int $id): void $club->setName((empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) ? $club->getName() : $this->postParam("name")); $club->setAbout(empty($this->postParam("about")) ? NULL : $this->postParam("about")); - $club->setWall(empty($this->postParam("wall")) ? 0 : 1); + try { + $club->setWall(empty($this->postParam("wall")) ? 0 : (int)$this->postParam("wall")); + } catch(\Exception $e) { + $this->flashFail("err", tr("error"), tr("error_invalid_wall_value")); + } + $club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display")); $club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1); $club->setDisplay_Topics_Above_Wall(empty($this->postParam("display_topics_above_wall")) ? 0 : 1); @@ -414,4 +426,37 @@ function renderChangeOwner(int $id, int $newOwnerId): void $this->flashFail("succ", tr("information_-1"), tr("group_owner_setted", $newOwner->getCanonicalName(), $club->getName())); } + + function renderSuggested(int $id): void + { + $this->assertUserLoggedIn(); + + $club = $this->clubs->get($id); + if(!$club) + $this->notFound(); + else + $this->template->club = $club; + + if($club->getWallType() == 0) { + $this->flash("err", tr("error_suggestions"), tr("error_suggestions_closed")); + $this->redirect("/club".$club->getId()); + } + + if($club->getWallType() == 1) { + $this->flash("err", tr("error_suggestions"), tr("error_suggestions_open")); + $this->redirect("/club".$club->getId()); + } + + if(!$club->canBeModifiedBy($this->user->identity)) { + $this->template->posts = iterator_to_array((new Posts)->getSuggestedPostsByUser($club->getId(), $this->user->id, (int) ($this->queryParam("p") ?? 1))); + $this->template->count = (new Posts)->getSuggestedPostsCountByUser($club->getId(), $this->user->id); + $this->template->type = "my"; + } else { + $this->template->posts = iterator_to_array((new Posts)->getSuggestedPosts($club->getId(), (int) ($this->queryParam("p") ?? 1))); + $this->template->count = (new Posts)->getSuggestedPostsCount($club->getId()); + $this->template->type = "everyone"; + } + + $this->template->page = (int) ($this->queryParam("p") ?? 1); + } } diff --git a/Web/Presenters/InternalAPIPresenter.php b/Web/Presenters/InternalAPIPresenter.php index e2e6b50e9..dca5db042 100644 --- a/Web/Presenters/InternalAPIPresenter.php +++ b/Web/Presenters/InternalAPIPresenter.php @@ -104,7 +104,7 @@ function renderGetPhotosFromPost(int $owner_id, int $post_id) { } if($this->postParam("parentType", false) == "post") { - $post = (new Posts)->getPostById($owner_id, $post_id); + $post = (new Posts)->getPostById($owner_id, $post_id, true); } else { $post = (new Comments)->get($post_id); } diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index a01c82625..d0f17d567 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -2,7 +2,7 @@ namespace openvk\Web\Presenters; 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}; +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 Chandler\Database\DatabaseConnection; use Nette\InvalidStateException as ISE; @@ -66,11 +66,32 @@ function renderWall(int $user, bool $embedded = false): void $this->template->oObj = $owner; if($user < 0) $this->template->club = $owner; + + $iterator = NULL; + $count = 0; + $type = $this->queryParam("type") ?? "all"; + + switch($type) { + default: + case "all": + $iterator = $this->posts->getPostsFromUsersWall($user, (int) ($_GET["p"] ?? 1)); + $count = $this->posts->getPostCountOnUserWall($user); + break; + case "owners": + $iterator = $this->posts->getOwnersPostsFromWall($user, (int) ($_GET["p"] ?? 1)); + $count = $this->posts->getOwnersCountOnUserWall($user); + break; + case "others": + $iterator = $this->posts->getOthersPostsFromWall($user, (int) ($_GET["p"] ?? 1)); + $count = $this->posts->getOthersCountOnUserWall($user); + break; + } $this->template->owner = $user; $this->template->canPost = $canPost; - $this->template->count = $this->posts->getPostCountOnUserWall($user); - $this->template->posts = iterator_to_array($this->posts->getPostsFromUsersWall($user, (int) ($_GET["p"] ?? 1))); + $this->template->count = $count; + $this->template->type = $type; + $this->template->posts = iterator_to_array($iterator); $this->template->paginatorConf = (object) [ "count" => $this->template->count, "page" => (int) ($_GET["p"] ?? 1), @@ -150,6 +171,7 @@ function renderFeed(): void ->select("id") ->where("wall IN (?)", $ids) ->where("deleted", 0) + ->where("suggested", 0) ->order("created DESC"); $this->template->paginatorConf = (object) [ "count" => sizeof($posts), @@ -169,7 +191,7 @@ function renderGlobalFeed(): void $page = (int) ($_GET["p"] ?? 1); $pPage = min((int) ($_GET["posts"] ?? OPENVK_DEFAULT_PER_PAGE), 50); - $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND `posts`.`deleted` = 0"; + $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0"; if($this->user->identity->getNsfwTolerance() === User::NSFW_INTOLERANT) $queryBase .= " AND `nsfw` = 0"; @@ -343,6 +365,10 @@ function renderMakePost(int $wall): void $post->setAnonymous($anon); $post->setFlags($flags); $post->setNsfw($this->postParam("nsfw") === "on"); + + if($wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2) + $post->setSuggested(1); + $post->save(); } catch (\LengthException $ex) { $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big")); @@ -371,12 +397,32 @@ function renderMakePost(int $wall): void if($wall > 0) $excludeMentions[] = $wall; - $mentions = iterator_to_array($post->resolveMentions($excludeMentions)); - foreach($mentions as $mentionee) - if($mentionee instanceof User) - (new MentionNotification($mentionee, $post, $post->getOwner(), strip_tags($post->getText())))->emit(); + if($wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2) { + # Чтобы не было упоминаний из предложки + } else { + $mentions = iterator_to_array($post->resolveMentions($excludeMentions)); + + foreach($mentions as $mentionee) + if($mentionee instanceof User) + (new MentionNotification($mentionee, $post, $post->getOwner(), strip_tags($post->getText())))->emit(); + } - $this->redirect($wallOwner->getURL()); + if($wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2) { + $suggsCount = $this->posts->getSuggestedPostsCount($wallOwner->getId()); + + if($suggsCount % 10 == 0) { + $managers = $wallOwner->getManagers(); + $owner = $wallOwner->getOwner(); + (new NewSuggestedPostsNotification($owner, $wallOwner))->emit(); + + foreach($managers as $manager) + (new NewSuggestedPostsNotification($manager->getUser(), $wallOwner))->emit(); + } + + $this->redirect("/club".$wallOwner->getId()."/suggested"); + } else { + $this->redirect($wallOwner->getURL()); + } } function renderPost(int $wall, int $post_id): void @@ -487,7 +533,7 @@ function renderDelete(int $wall, int $post_id): void $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - $post = $this->posts->getPostById($wall, $post_id); + $post = $this->posts->getPostById($wall, $post_id, true); if(!$post) $this->notFound(); $user = $this->user->id; @@ -502,6 +548,9 @@ function renderDelete(int $wall, int $post_id): void else $canBeDeletedByOtherUser = false; if(!is_null($user)) { + if($post->getTargetWall() < 0 && !$post->getWallOwner()->canBeModifiedBy($this->user->identity) && $post->getWallOwner()->getWallType() != 1 && $post->getSuggestionType() == 0) + $this->flashFail("err", tr("failed_to_delete_post"), tr("error_deleting_suggested")); + if($post->getOwnerPost() == $user || $post->getTargetWall() == $user || $canBeDeletedByOtherUser) { $post->unwire(); $post->delete(); @@ -597,4 +646,93 @@ function renderEdit() "avatar" => $post->getOwner()->getAvatarUrl() ]]); } + + function renderAccept() { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + + if($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + exit("Ты дебил, это точка апи."); + } + + $id = $this->postParam("id"); + $sign = $this->postParam("sign") == 1; + $content = $this->postParam("new_content"); + + $post = (new Posts)->get((int)$id); + + if(!$post || $post->isDeleted()) + $this->flashFail("err", "Error", tr("error_accepting_invalid_post"), NULL, true); + + if($post->getSuggestionType() == 0) + $this->flashFail("err", "Error", tr("error_accepting_not_suggested_post"), NULL, true); + + if($post->getSuggestionType() == 2) + $this->flashFail("err", "Error", tr("error_accepting_declined_post"), NULL, true); + + if(!$post->canBePinnedBy($this->user->identity)) + $this->flashFail("err", "Error", "Can't accept this post.", NULL, true); + + $author = $post->getOwner(); + + $flags = 0; + $flags |= 0b10000000; + + if($sign) + $flags |= 0b01000000; + + $post->setSuggested(0); + $post->setCreated(time()); + $post->setApi_Source_Name(NULL); + $post->setFlags($flags); + + if(mb_strlen($content) > 0) + $post->setContent($content); + + $post->save(); + + if($author->getId() != $this->user->id) + (new PostAcceptedNotification($author, $post, $post->getWallOwner()))->emit(); + + $this->returnJson([ + "success" => true, + "id" => $post->getPrettyId(), + "new_count" => (new Posts)->getSuggestedPostsCount($post->getWallOwner()->getId()) + ]); + } + + function renderDecline() { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + + if($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + exit("Ты дебил, это метод апи."); + } + + $id = $this->postParam("id"); + $post = (new Posts)->get((int)$id); + + if(!$post || $post->isDeleted()) + $this->flashFail("err", "Error", tr("error_declining_invalid_post"), NULL, true); + + if($post->getSuggestionType() == 0) + $this->flashFail("err", "Error", tr("error_declining_not_suggested_post"), NULL, true); + + if($post->getSuggestionType() == 2) + $this->flashFail("err", "Error", tr("error_declining_declined_post"), NULL, true); + + if(!$post->canBePinnedBy($this->user->identity)) + $this->flashFail("err", "Error", "Can't decline this post.", NULL, true); + + $post->setSuggested(2); + $post->setDeleted(1); + $post->save(); + + $this->returnJson([ + "success" => true, + "new_count" => (new Posts)->getSuggestedPostsCount($post->getWallOwner()->getId()) + ]); + } } diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 8f1a35f03..d636ad421 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -233,7 +233,16 @@
`
+ }
+ ],
+ afterResponse: [
+ async (_request, _options, response) => {
+ let text = await response.text()
+ let parser = new DOMParser()
+ let body = parser.parseFromString(text, "text/html")
+
+ if(body.querySelectorAll(".post").length < 1) {
+ let url = new URL(location.href)
+ url.searchParams.set("p", url.searchParams.get("p") - 1)
+
+ if(url.searchParams.get("p") < 1) {
+ return 0;
+ }
+
+ history.pushState({}, "", url)
+
+ loadMoreSuggestedPosts()
+ }
+
+ body.querySelectorAll(".bsdn").forEach(bsdnInitElement)
+ document.getElementById("postz").innerHTML = body.getElementById("postz").innerHTML
+ }
+ ]
+ }
+ })
+}
+
+// нажатие на "x предложенных записей"
+$(document).on("click", ".sugglist a", (e) => {
+ e.preventDefault()
+
+ if(e.currentTarget.getAttribute("data-toogled") == null || e.currentTarget.getAttribute("data-toogled") == "false") {
+ e.currentTarget.setAttribute("data-toogled", "true")
+ document.getElementById("underHeader").style.display = "none"
+ document.querySelector(".insertThere").style.display = "block"
+ document.querySelector(".insertThere").classList.add("infContainer")
+ history.pushState({}, "", e.currentTarget.href)
+
+ // если ещё ничего не подгружалось
+ if(document.querySelector(".insertThere").innerHTML == "") {
+ ky(e.currentTarget.href, {
+ hooks: {
+ beforeRequest: [
+ (_request) => {
+ document.querySelector(".insertThere").insertAdjacentHTML("afterbegin", `
`)
+ }
+ ],
+ afterResponse: [
+ async (_request, _options, response) => {
+ let parser = new DOMParser
+ let result = parser.parseFromString(await response.text(), 'text/html').querySelector(".infContainer")
+
+ result.querySelectorAll(".bsdn").forEach(bsdnInitElement)
+ document.querySelector(".insertThere").innerHTML = result.innerHTML
+ }
+ ]
+ }
+ })
+ }
+ } else {
+ // переключение на нормальную стену
+ e.currentTarget.setAttribute("data-toogled", "false")
+ document.getElementById("underHeader").style.display = "block"
+ document.querySelector(".insertThere").style.display = "none"
+ document.querySelector(".insertThere").classList.remove("infContainer")
+ history.pushState({}, "", e.currentTarget.href.replace("/suggested", ""))
+ }
+})
+
+// нажатие на пагинатор у постов предложки
+$(document).on("click", "#postz .paginator a", (e) => {
+ e.preventDefault()
+
+ ky(e.currentTarget.href, {
+ hooks: {
+ beforeRequest: [
+ (_request) => {
+ if(document.querySelector(".sugglist") != null) {
+ document.querySelector(".sugglist").scrollIntoView({behavior: "smooth"})
+ } else {
+ document.querySelector(".infContainer").scrollIntoView({behavior: "smooth"})
+ }
+
+ setTimeout(() => {document.getElementById("postz").innerHTML = `
`}, 500)
+ }
+ ],
+ afterResponse: [
+ async (_request, _options, response) => {
+ let result = (new DOMParser).parseFromString(await response.text(), "text/html").querySelector(".infContainer")
+ result.querySelectorAll(".bsdn").forEach(bsdnInitElement)
+
+ document.getElementById("postz").innerHTML = result.innerHTML
+ history.pushState({}, "", e.currentTarget.href)
+ }
+ ]
+ }
+ })
+})
diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js
index 3fc96ce02..dc4cb47f2 100644
--- a/Web/static/js/al_wall.js
+++ b/Web/static/js/al_wall.js
@@ -445,7 +445,7 @@ tippy(".client_app", {
function addNote(textareaId, nid)
{
if(nid > 0) {
- note.value = nid
+ document.getElementById("note").value = nid
let noteObj = document.querySelector("#nd"+nid)
let nortd = document.querySelector("#post-buttons"+textareaId+" .post-has-note");
@@ -453,7 +453,7 @@ function addNote(textareaId, nid)
nortd.innerHTML = `${tr("note")} ${escapeHtml(noteObj.dataset.name)}`
} else {
- note.value = "none"
+ document.getElementById("note").value = "none"
let nortd = document.querySelector("#post-buttons"+textareaId+" .post-has-note");
nortd.style.display = "none"
@@ -481,7 +481,7 @@ async function attachNote(id)
${tr("select_or_create_new")}