openvk/Web/Presenters/WallPresenter.php
veselcraft 966850dc61
ActivityPub: Add post implementation
Tested on Pleroma
2022-02-13 12:49:03 +03:00

459 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Post, Photo, Video, Club, User};
use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums};
use Chandler\Database\DatabaseConnection;
use Nette\InvalidStateException as ISE;
use Bhaktaraz\RSSGenerator\Item;
use Bhaktaraz\RSSGenerator\Feed;
use Bhaktaraz\RSSGenerator\Channel;
final class WallPresenter extends OpenVKPresenter
{
private $posts;
function __construct(Posts $posts)
{
$this->posts = $posts;
parent::__construct();
}
private function logPostView(Post $post, int $wall): void
{
if(is_null($this->user))
return;
$this->logEvent("postView", [
"profile" => $this->user->identity->getId(),
"post" => $post->getId(),
"owner" => abs($wall),
"group" => $wall < 0,
"subscribed" => $wall < 0 ? $post->getOwner()->getSubscriptionStatus($this->user->identity) : false,
]);
}
private function logPostsViewed(array &$posts, int $wall): void
{
$x = array_values($posts); # clone array (otherwise Nette DB objects will become kinda gay)
foreach($x as $post)
$this->logPostView($post, $wall);
}
function renderWall(int $user, bool $embedded = false): void
{
if(false)
exit("Ошибка доступа: " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) {
$canPost = false;
} else if($user > 0) {
if(!$owner->isBanned())
$canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity);
else
$this->flashFail("err", tr("error"), "Ошибка доступа");
} else if($user < 0) {
if($owner->canBeModifiedBy($this->user->identity))
$canPost = true;
else
$canPost = $owner->canPost();
} else {
$canPost = false;
}
if ($embedded == true) $this->template->_template = "components/wall.xml";
$this->template->oObj = $owner;
$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->paginatorConf = (object) [
"count" => $this->template->count,
"page" => (int) ($_GET["p"] ?? 1),
"amount" => sizeof($this->template->posts),
"perPage" => OPENVK_DEFAULT_PER_PAGE,
];
$this->logPostsViewed($this->template->posts, $user);
}
function renderWallEmbedded(int $user): void
{
$this->renderWall($user, true);
}
function renderRSS(int $user): void
{
if(false)
exit("Ошибка доступа: " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) {
$canPost = false;
} else if($user > 0) {
if(!$owner->isBanned())
$canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity);
else
$this->flashFail("err", tr("error"), "Ошибка доступа");
} else if($user < 0) {
if($owner->canBeModifiedBy($this->user->identity))
$canPost = true;
else
$canPost = $owner->canPost();
} else {
$canPost = false;
}
$posts = iterator_to_array($this->posts->getPostsFromUsersWall($user));
$feed = new Feed();
$channel = new Channel();
$channel->title(OPENVK_ROOT_CONF['openvk']['appearance']['name'])->url(ovk_scheme(true) . $_SERVER["SERVER_NAME"])->appendTo($feed);
foreach($posts as $post) {
$item = new Item();
$item
->title($post->getOwner()->getCanonicalName())
->description($post->getText())
->url(ovk_scheme(true).$_SERVER["SERVER_NAME"]."/wall{$post->getPrettyId()}")
->pubDate($post->getPublicationTime()->timestamp())
->appendTo($channel);
}
header("Content-Type: application/rss+xml");
exit($feed);
}
function renderFeed(): void
{
$this->assertUserLoggedIn();
$id = $this->user->id;
$subs = DatabaseConnection::i()
->getContext()
->table("subscriptions")
->where("follower", $id);
$ids = array_map(function($rel) {
return $rel->target * ($rel->model === "openvk\Web\Models\Entities\User" ? 1 : -1);
}, iterator_to_array($subs));
$ids[] = $this->user->id;
$perPage = min((int) ($_GET["posts"] ?? OPENVK_DEFAULT_PER_PAGE), 50);
$posts = DatabaseConnection::i()
->getContext()
->table("posts")
->select("id")
->where("wall IN (?)", $ids)
->where("deleted", 0)
->order("created DESC");
$this->template->paginatorConf = (object) [
"count" => sizeof($posts),
"page" => (int) ($_GET["p"] ?? 1),
"amount" => sizeof($posts->page((int) ($_GET["p"] ?? 1), $perPage)),
"perPage" => $perPage,
];
$this->template->posts = [];
foreach($posts->page((int) ($_GET["p"] ?? 1), $perPage) as $post)
$this->template->posts[] = $this->posts->get($post->id);
}
function renderGlobalFeed(): void
{
$this->assertUserLoggedIn();
$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";
if($this->user->identity->getNsfwTolerance() === User::NSFW_INTOLERANT)
$queryBase .= " AND `nsfw` = 0";
$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(*)"};
$this->template->_template = "Wall/Feed.xml";
$this->template->globalFeed = true;
$this->template->paginatorConf = (object) [
"count" => $count,
"page" => (int) ($_GET["p"] ?? 1),
"amount" => sizeof($posts),
"perPage" => $pPage,
];
foreach($posts as $post)
$this->template->posts[] = $this->posts->get($post->id);
}
function renderHashtagFeed(string $hashtag): void
{
$hashtag = rawurldecode($hashtag);
$page = (int) ($_GET["p"] ?? 1);
$posts = $this->posts->getPostsByHashtag($hashtag, $page);
$count = $this->posts->getPostCountByHashtag($hashtag);
$this->template->hashtag = $hashtag;
$this->template->posts = $posts;
$this->template->paginatorConf = (object) [
"count" => 0,
"page" => $page,
"amount" => $count,
"perPage" => OPENVK_DEFAULT_PER_PAGE,
];
}
function renderMakePost(int $wall): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1))
?? $this->flashFail("err", "Не удалось опубликовать пост", "Такого пользователя не существует.");
if($wall > 0) {
if(!$wallOwner->isBanned())
$canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity);
else
$this->flashFail("err", "Ошибка доступа", "Вам нельзя писать на эту стену.");
} else if($wall < 0) {
if($wallOwner->canBeModifiedBy($this->user->identity))
$canPost = true;
else
$canPost = $wallOwner->canPost();
} else {
$canPost = false;
}
if(!$canPost)
$this->flashFail("err", "Ошибка доступа", "Вам нельзя писать на эту стену.");
$anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"];
if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) {
$manager = $wallOwner->getManager($this->user->identity);
if($manager)
$anon = $manager->isHidden();
elseif($this->user->identity->getId() === $wallOwner->getOwner()->getId())
$anon = $wallOwner->isOwnerHidden();
} else {
$anon = $anon && $this->postParam("anon") === "on";
}
$flags = 0;
if($this->postParam("as_group") === "on" && $wallOwner instanceof Club && $wallOwner->canBeModifiedBy($this->user->identity))
$flags |= 0b10000000;
if($this->postParam("force_sign") === "on")
$flags |= 0b01000000;
try {
$photo = NULL;
$video = NULL;
if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) {
$album = NULL;
if(!$anon && $wall > 0 && $wall === $this->user->id)
$album = (new Albums)->getUserWallAlbum($wallOwner);
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon);
}
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) {
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon);
}
} catch(\DomainException $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён.");
} catch(ISE $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён или слишком велик.");
}
if(empty($this->postParam("text")) && !$photo && !$video)
$this->flashFail("err", "Не удалось опубликовать пост", "Пост пустой или слишком большой.");
try {
$post = new Post;
$post->setOwner($this->user->id);
$post->setWall($wall);
$post->setCreated(time());
$post->setContent($this->postParam("text"));
$post->setAnonymous($anon);
$post->setFlags($flags);
$post->setNsfw($this->postParam("nsfw") === "on");
$post->save();
} catch (\LengthException $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Пост слишком большой.");
}
if(!is_null($photo))
$post->attach($photo);
if(!is_null($video))
$post->attach($video);
if($wall > 0 && $wall !== $this->user->identity->getId())
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
if($wall > 0)
$this->redirect("/id$wall", 2); #Will exit
$wall = $wall * -1;
$this->redirect("/club$wall", 2);
}
function renderPost(int $wall, int $post_id): void
{
$post = $this->posts->getPostById($wall, $post_id);
if(!$post || $post->isDeleted())
$this->notFound();
$this->logPostView($post, $wall);
if($this->isActivityPubClient()) {
$objPost = array(
"type" => "Note",
"id" => ovk_scheme(true) . $_SERVER['SERVER_NAME'] . "/wall" . $wall . "_" . $post_id,
"attributedTo" => $post->getOwner()->getFullURL(true),
"content" => $post->getText(),
"published" => $post->getPublicationTime()->format('%Y-%m-%dT%H:%M:%SZ'),
"url" => ovk_scheme(true) . $_SERVER['SERVER_NAME'] . "/wall" . $wall . "_" . $post_id,
"to" => [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc" => [
ovk_scheme(true) . $_SERVER['SERVER_NAME'] . "/friends" . $post->getOwner()->getId() . "?act=incoming"
],
"replies" => [
"type" => "Collection",
"id" => ovk_scheme(true) . $_SERVER['SERVER_NAME'] . "/wall" . $wall . "_" . $post_id . "#comments",
"first" => [
"type" => "CollectionPage",
"items" => [
],
"partOf" => ovk_scheme(true) . $_SERVER['SERVER_NAME'] . "/wall" . $wall . "_" . $post_id . "#comments",
"next" => ovk_scheme(true) . $_SERVER['SERVER_NAME'] . "/wall" . $wall . "_" . $post_id . "?p=2#comments"
]
],
"sensitive" => false,
"likes" => ovk_scheme(true) . $_SERVER['SERVER_NAME'] . "/wall" . $wall . "_" . $post_id . "/likes",
"@context" => [
"https://www.w3.org/ns/activitystreams",
(object) array(
"sensitive" => "as:sensitive"
)
]
);
$this->returnJson($objPost, CT_AP);
}
if ($post->getTargetWall() > 0) {
$this->template->wallOwner = (new Users)->get($post->getTargetWall());
$this->template->isWallOfGroup = false;
if($this->template->wallOwner->isBanned())
$this->flashFail("err", tr("error"), "Ошибка доступа");
} else {
$this->template->wallOwner = (new Clubs)->get(abs($post->getTargetWall()));
$this->template->isWallOfGroup = true;
}
$this->template->post = $post;
$this->template->cCount = $post->getCommentsCount();
$this->template->cPage = (int) ($_GET["p"] ?? 1);
$this->template->comments = iterator_to_array($post->getComments($this->template->cPage));
}
function renderLike(int $wall, int $post_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$this->assertNoCSRF();
$post = $this->posts->getPostById($wall, $post_id);
if(!$post || $post->isDeleted()) $this->notFound();
if(!is_null($this->user)) {
$post->toggleLike($this->user->identity);
}
$this->redirect(
"$_SERVER[HTTP_REFERER]#postGarter=" . $post->getId(),
static::REDIRECT_TEMPORARY
);
}
function renderShare(int $wall, int $post_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$this->assertNoCSRF();
$post = $this->posts->getPostById($wall, $post_id);
if(!$post || $post->isDeleted()) $this->notFound();
if(!is_null($this->user)) {
$nPost = new Post;
$nPost->setOwner($this->user->id);
$nPost->setWall($this->user->id);
$nPost->setContent($this->postParam("text"));
$nPost->save();
$nPost->attach($post);
if($post->getOwner(false)->getId() !== $this->user->identity->getId() && !($post->getOwner() instanceof Club))
(new RepostNotification($post->getOwner(false), $post, $this->user->identity))->emit();
};
$this->returnJson(["wall_owner" => $this->user->identity->getId()]);
}
function renderDelete(int $wall, int $post_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$post = $this->posts->getPostById($wall, $post_id);
if(!$post)
$this->notFound();
$user = $this->user->id;
$wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1))
?? $this->flashFail("err", "Не удалось удалить пост", "Такого пользователя не существует.");
if($wall < 0) $canBeDeletedByOtherUser = $wallOwner->canBeModifiedBy($this->user->identity);
else $canBeDeletedByOtherUser = false;
if(!is_null($user)) {
if($post->getOwnerPost() == $user || $post->getTargetWall() == $user || $canBeDeletedByOtherUser) {
$post->unwire();
$post->delete();
}
} else {
$this->flashFail("err", "Не удалось удалить пост", "Вы не вошли в аккаунт.");
}
$this->redirect($wall < 0 ? "/club".($wall*-1) : "/id".$wall, static::REDIRECT_TEMPORARY);
exit;
}
function renderPin(int $wall, int $post_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$post = $this->posts->getPostById($wall, $post_id);
if(!$post)
$this->notFound();
if(!$post->canBePinnedBy($this->user->identity))
$this->flashFail("err", "Ошибка доступа", "Вам нельзя закреплять этот пост.");
if(($this->queryParam("act") ?? "pin") === "pin") {
$post->pin();
} else {
$post->unpin();
}
// TODO localize message based on language and ?act=(un)pin
$this->flashFail("succ", "Операция успешна", "Операция успешна.");
}
}