Rewrote most of the comments backend.g

This commit is contained in:
flash 2023-07-15 23:58:17 +00:00
parent 6274f7f8d3
commit f24f811acc
22 changed files with 1012 additions and 888 deletions

View file

@ -1,12 +1,10 @@
<?php
namespace Misuzu;
use RuntimeException;
use Misuzu\AuditLog;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Comments\CommentsPost;
use Misuzu\Comments\CommentsPostNotFoundException;
use Misuzu\Comments\CommentsPostSaveFailedException;
use Misuzu\Comments\CommentsVote;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
@ -42,82 +40,97 @@ if($currentUserInfo->isSilenced()) {
return;
}
$comments = $msz->getComments();
$commentPerms = $currentUserInfo->commentPerms();
$commentId = (int)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$commentMode = filter_input(INPUT_GET, 'm');
$commentId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$commentMode = (string)filter_input(INPUT_GET, 'm');
$commentVote = (int)filter_input(INPUT_GET, 'v', FILTER_SANITIZE_NUMBER_INT);
if($commentId > 0)
if(!empty($commentId)) {
try {
$commentInfo2 = CommentsPost::byId($commentId);
} catch(CommentsPostNotFoundException $ex) {
$commentInfo = $comments->getPostById($commentId);
} catch(RuntimeException $ex) {
echo render_info('Post not found.', 404);
return;
}
$categoryInfo = $comments->getCategoryByPost($commentInfo);
}
if($commentMode !== 'create' && empty($commentInfo)) {
echo render_error(400);
return;
}
switch($commentMode) {
case 'pin':
case 'unpin':
if(!$commentPerms['can_pin'] && !$commentInfo2->isOwner($currentUserInfo)) {
if(!$commentPerms['can_pin'] && !$categoryInfo->isOwner($currentUserInfo)) {
echo render_info("You're not allowed to pin comments.", 403);
break;
}
if($commentInfo2->isDeleted()) {
if($commentInfo->isDeleted()) {
echo render_info("This comment doesn't exist!", 400);
break;
}
if($commentInfo2->hasParent()) {
if($commentInfo->isReply()) {
echo render_info("You can't pin replies!", 400);
break;
}
$isPinning = $commentMode === 'pin';
if($isPinning && $commentInfo2->isPinned()) {
echo render_info('This comment is already pinned.', 400);
break;
} elseif(!$isPinning && !$commentInfo2->isPinned()) {
echo render_info("This comment isn't pinned yet.", 400);
break;
if($isPinning) {
if($commentInfo->isPinned()) {
echo render_info('This comment is already pinned.', 400);
break;
}
$comments->pinPost($commentInfo);
} else {
if(!$commentInfo->isPinned()) {
echo render_info("This comment isn't pinned yet.", 400);
break;
}
$comments->unpinPost($commentInfo);
}
$commentInfo2->setPinned($isPinning);
$commentInfo2->save();
redirect($redirect . '#comment-' . $commentInfo2->getId());
redirect($redirect . '#comment-' . $commentInfo->getId());
break;
case 'vote':
if(!$commentPerms['can_vote'] && !$commentInfo2->isOwner($currentUserInfo)) {
if(!$commentPerms['can_vote'] && !$categoryInfo->isOwner($currentUserInfo)) {
echo render_info("You're not allowed to vote on comments.", 403);
break;
}
if($commentInfo2->isDeleted()) {
if($commentInfo->isDeleted()) {
echo render_info("This comment doesn't exist!", 400);
break;
}
if($commentVote > 0)
$commentInfo2->addPositiveVote($currentUserInfo);
$comments->addPostPositiveVote($commentInfo, $currentUserInfo);
elseif($commentVote < 0)
$commentInfo2->addNegativeVote($currentUserInfo);
$comments->addPostNegativeVote($commentInfo, $currentUserInfo);
else
$commentInfo2->removeVote($currentUserInfo);
$comments->removePostVote($commentInfo, $currentUserInfo);
redirect($redirect . '#comment-' . $commentInfo2->getId());
redirect($redirect . '#comment-' . $commentInfo->getId());
break;
case 'delete':
if(!$commentPerms['can_delete'] && !$commentInfo2->isOwner($currentUserInfo)) {
if(!$commentPerms['can_delete'] && !$categoryInfo->isOwner($currentUserInfo)) {
echo render_info("You're not allowed to delete comments.", 403);
break;
}
if($commentInfo2->isDeleted()) {
if($commentInfo->isDeleted()) {
echo render_info(
$commentPerms['can_delete_any'] ? 'This comment is already marked for deletion.' : "This comment doesn't exist.",
400
@ -125,7 +138,7 @@ switch($commentMode) {
break;
}
$isOwnComment = $commentInfo2->getUserId() === $currentUserInfo->getId();
$isOwnComment = $commentInfo->getUserId() === (string)$currentUserInfo->getId();
$isModAction = $commentPerms['can_delete_any'] && !$isOwnComment;
if(!$isModAction && !$isOwnComment) {
@ -133,17 +146,16 @@ switch($commentMode) {
break;
}
$commentInfo2->setDeleted(true);
$commentInfo2->save();
$comments->deletePost($commentInfo);
if($isModAction) {
AuditLog::create(AuditLog::COMMENT_ENTRY_DELETE_MOD, [
$commentInfo2->getId(),
$commentUserId = $commentInfo2->getUserId(),
($commentUserId < 1 ? '(Deleted User)' : $commentInfo2->getUser()->getUsername()),
$commentInfo->getId(),
$commentUserId = $commentInfo->getUserId(),
'<username>',
]);
} else {
AuditLog::create(AuditLog::COMMENT_ENTRY_DELETE, [$commentInfo2->getId()]);
AuditLog::create(AuditLog::COMMENT_ENTRY_DELETE, [$commentInfo->getId()]);
}
redirect($redirect);
@ -155,25 +167,24 @@ switch($commentMode) {
break;
}
if(!$commentInfo2->isDeleted()) {
if(!$commentInfo->isDeleted()) {
echo render_info("This comment isn't in a deleted state.", 400);
break;
}
$commentInfo2->setDeleted(false);
$commentInfo2->save();
$comments->restorePost($commentInfo);
AuditLog::create(AuditLog::COMMENT_ENTRY_RESTORE, [
$commentInfo2->getId(),
$commentUserId = $commentInfo2->getUserId(),
($commentUserId < 1 ? '(Deleted User)' : $commentInfo2->getUser()->getUsername()),
$commentInfo->getId(),
$commentUserId = $commentInfo->getUserId(),
'<username>',
]);
redirect($redirect . '#comment-' . $commentInfo2->getId());
redirect($redirect . '#comment-' . $commentInfo->getId());
break;
case 'create':
if(!$commentPerms['can_comment'] && !$commentInfo2->isOwner($currentUserInfo)) {
if(!$commentPerms['can_comment'] && !$categoryInfo->isOwner($currentUserInfo)) {
echo render_info("You're not allowed to post comments.", 403);
break;
}
@ -184,12 +195,11 @@ switch($commentMode) {
}
try {
$categoryInfo = CommentsCategory::byId(
isset($_POST['comment']['category']) && is_string($_POST['comment']['category'])
? (int)$_POST['comment']['category']
: 0
);
} catch(CommentsCategoryNotFoundException $ex) {
$categoryId = isset($_POST['comment']['category']) && is_string($_POST['comment']['category'])
? (int)$_POST['comment']['category']
: 0;
$categoryInfo = $comments->getCategoryById($categoryId);
} catch(RuntimeException $ex) {
echo render_info('This comment category doesn\'t exist.', 404);
break;
}
@ -199,21 +209,23 @@ switch($commentMode) {
break;
}
$commentText = !empty($_POST['comment']['text']) && is_string($_POST['comment']['text']) ? $_POST['comment']['text'] : '';
$commentReply = !empty($_POST['comment']['reply']) && is_string($_POST['comment']['reply']) ? (int)$_POST['comment']['reply'] : 0;
$commentLock = !empty($_POST['comment']['lock']) && $commentPerms['can_lock'];
$commentPin = !empty($_POST['comment']['pin']) && $commentPerms['can_pin'];
$commentText = !empty($_POST['comment']['text']) && is_string($_POST['comment']['text']) ? $_POST['comment']['text'] : '';
$commentReply = (string)(!empty($_POST['comment']['reply']) && is_string($_POST['comment']['reply']) ? (int)$_POST['comment']['reply'] : 0);
$commentLock = !empty($_POST['comment']['lock']) && $commentPerms['can_lock'];
$commentPin = !empty($_POST['comment']['pin']) && $commentPerms['can_pin'];
if($commentLock) {
$categoryInfo->setLocked(!$categoryInfo->isLocked());
$categoryInfo->save();
if($categoryInfo->isLocked())
$comments->unlockCategory($categoryInfo);
else
$comments->lockCategory($categoryInfo);
}
if(strlen($commentText) > 0) {
$commentText = preg_replace("/[\r\n]{2,}/", "\n", $commentText);
} else {
if($commentPerms['can_lock']) {
echo render_info('The action has been processed.');
echo render_info('The action has been processed.', 400);
} else {
echo render_info('Your comment is too short.', 400);
}
@ -227,34 +239,24 @@ switch($commentMode) {
if($commentReply > 0) {
try {
$parentCommentInfo = CommentsPost::byId($commentReply);
} catch(CommentsPostNotFoundException $ex) {
unset($parentCommentInfo);
}
$parentInfo = $comments->getPostById($commentReply);
} catch(RuntimeException $ex) {}
if(!isset($parentCommentInfo) || $parentCommentInfo->isDeleted()) {
if(!isset($parentInfo) || $parentInfo->isDeleted()) {
echo render_info('The comment you tried to reply to does not exist.', 404);
break;
}
}
$commentInfo2 = (new CommentsPost)
->setUser($currentUserInfo)
->setCategory($categoryInfo)
->setParsedText($commentText)
->setPinned($commentPin);
$commentInfo = $comments->createPost(
$categoryInfo,
$parentInfo ?? null,
$currentUserInfo,
$commentText,
$commentPin
);
if(isset($parentCommentInfo))
$commentInfo2->setParent($parentCommentInfo);
try {
$commentInfo2->save();
} catch(CommentsPostSaveFailedException $ex) {
echo render_info('Something went horribly wrong.', 500);
break;
}
redirect($redirect . '#comment-' . $commentInfo2->getId());
redirect($redirect . '#comment-' . $commentInfo->getId());
break;
default:

View file

@ -1,8 +1,8 @@
<?php
namespace Misuzu;
use RuntimeException;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Users\User;
require_once '../misuzu.php';
@ -15,6 +15,7 @@ if(!empty($searchQuery)) {
// this sure is an expansion
$news = $msz->getNews();
$comments = $msz->getComments();
$newsPosts = [];
$newsPostInfos = $news->getPostsBySearchQuery($searchQuery);
$newsUserInfos = [];
@ -41,11 +42,8 @@ if(!empty($searchQuery)) {
else
$newsCategoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo);
$commentsCount = 0;
if($postInfo->hasCommentsCategoryId())
try {
$commentsCount = CommentsCategory::byId($postInfo->getCommentsCategoryId())->getPostCount();
} catch(CommentsCategoryNotFoundException $ex) {}
$commentsCount = $postInfo->hasCommentsCategoryId()
? $comments->countPosts($postInfo->getCommentsCategoryId(), includeReplies: true) : 0;
$newsPosts[] = [
'post' => $postInfo,

524
src/Comments/Comments.php Normal file
View file

@ -0,0 +1,524 @@
<?php
namespace Misuzu\Comments;
use InvalidArgumentException;
use RuntimeException;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Misuzu\DbStatementCache;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Users\User;
class Comments {
private IDbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
public function countAllCategories(User|string|null $owner = null): int {
if($owner instanceof User)
$owner = (string)$owner->getId();
$hasOwner = $owner !== null;
$query = 'SELECT COUNT(*) FROM msz_comments_categories';
if($hasOwner)
$query .= ' WHERE owner_id = ?';
$stmt = $this->cache->get($query);
$stmt->addParameter(1, $owner);
$stmt->execute();
$count = 0;
$result = $stmt->getResult();
if($result->next())
$count = $result->getInteger(0);
return $count;
}
public function getCategories(
User|string|null $owner = null,
?Pagination $pagination = null
): array {
if($owner instanceof User)
$owner = (string)$owner->getId();
$hasOwner = $owner !== null;
$hasPagination = $pagination !== null;
$query = 'SELECT category_id, category_name, owner_id, UNIX_TIMESTAMP(category_created), UNIX_TIMESTAMP(category_locked), (SELECT COUNT(*) FROM msz_comments_posts AS cp WHERE cp.category_id = cc.category_id AND comment_deleted IS NULL) AS `category_comments` FROM msz_comments_categories AS cc';
if($hasOwner)
$query .= ' WHERE owner_id = ?';
$query .= ' ORDER BY category_id ASC'; // should order by date but no index on
if($hasPagination)
$query .= ' LIMIT ? RANGE ?';
$stmt = $this->cache->get($query);
$args = 0;
if($hasOwner)
$stmt->addParameter(++$args, $owner);
if($hasPagination) {
$stmt->addParameter(++$args, $pagination->getRange());
$stmt->addParameter(++$args, $pagination->getOffset());
}
$stmt->execute();
$result = $stmt->getResult();
$categories = [];
while($result->next())
$categories[] = new CommentsCategoryInfo($result);
return $categories;
}
public function getCategoryByName(string $name): CommentsCategoryInfo {
$stmt = $this->cache->get('SELECT category_id, category_name, owner_id, UNIX_TIMESTAMP(category_created), UNIX_TIMESTAMP(category_locked), (SELECT COUNT(*) FROM msz_comments_posts AS cp WHERE cp.category_id = cc.category_id AND comment_deleted IS NULL) AS `category_comments` FROM msz_comments_categories AS cc WHERE category_name = ?');
$stmt->addParameter(1, $name);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('No category with this name found.');
return new CommentsCategoryInfo($result);
}
public function getCategoryById(string $id): CommentsCategoryInfo {
$stmt = $this->cache->get('SELECT category_id, category_name, owner_id, UNIX_TIMESTAMP(category_created), UNIX_TIMESTAMP(category_locked), (SELECT COUNT(*) FROM msz_comments_posts AS cp WHERE cp.category_id = cc.category_id AND comment_deleted IS NULL) AS `category_comments` FROM msz_comments_categories AS cc WHERE category_id = ?');
$stmt->addParameter(1, $id);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('No category with this ID found.');
return new CommentsCategoryInfo($result);
}
public function getCategoryByPost(CommentsPostInfo|string $infoOrId): CommentsCategoryInfo {
$query = 'SELECT category_id, category_name, owner_id, UNIX_TIMESTAMP(category_created), UNIX_TIMESTAMP(category_locked), (SELECT COUNT(*) FROM msz_comments_posts AS cp WHERE cp.category_id = cc.category_id AND comment_deleted IS NULL) AS `category_comments` FROM msz_comments_categories AS cc WHERE category_id = ';
if($infoOrId instanceof CommentsPostInfo) {
$query .= '?';
$param = $infoOrId->getCategoryId();
} else {
$query .= '(SELECT category_id FROM msz_comments_posts WHERE comment_id = ?)';
$param = $infoOrId;
}
$stmt = $this->cache->get($query);
$stmt->addParameter(1, $param);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('No category belonging to this post found.');
return new CommentsCategoryInfo($result);
}
public function checkCategoryNameExists(string $name): bool {
$stmt = $this->cache->get('SELECT COUNT(*) FROM msz_comments_categories WHERE category_name = ?');
$stmt->addParameter(1, $name);
$stmt->execute();
$count = 0;
$result = $stmt->getResult();
if($result->next())
$count = $result->getInteger(0);
return $count > 0;
}
public function ensureCategory(string $name, User|string|null $owner = null): CommentsCategoryInfo {
if($this->checkCategoryNameExists($name))
return $this->getCategoryByName($name);
return $this->createCategory($name, $owner);
}
public function createCategory(string $name, User|string|null $owner = null): CommentsCategoryInfo {
if($owner instanceof User)
$owner = (string)$owner->getId();
$name = trim($name);
if(empty($name))
throw new InvalidArgumentException('$name may not be empty.');
$stmt = $this->cache->get('INSERT INTO msz_comments_categories (category_name, owner_id) VALUES (?, ?)');
$stmt->addParameter(1, $name);
$stmt->addParameter(2, $owner);
$stmt->execute();
return $this->getCategoryById((string)$this->dbConn->getLastInsertId());
}
public function deleteCategory(CommentsCategoryInfo|string $category): void {
if($category instanceof CommentsCategoryInfo)
$category = $category->getId();
$stmt = $this->cache->get('DELETE FROM msz_comments_categories WHERE category_id = ?');
$stmt->addParameter(1, $category);
$stmt->execute();
}
public function updateCategory(
CommentsCategoryInfo|string $category,
?string $name = null,
bool $updateOwner = false,
User|string|null $owner = null
): void {
if($category instanceof CommentsCategoryInfo)
$category = $category->getId();
if($owner instanceof User)
$owner = (string)$owner->getId();
if($name !== null) {
$name = trim($name);
if(empty($name))
throw new InvalidArgumentException('$name may not be empty.');
}
$stmt = $this->cache->get('UPDATE msz_comments_categories SET category_name = COALESCE(?, category_name), owner_id = IF(?, ?, owner_id) WHERE category_id = ?');
$stmt->addParameter(1, $name);
$stmt->addParameter(2, $updateOwner ? 1 : 0);
$stmt->addParameter(3, $owner ? 1 : 0);
$stmt->addParameter(4, $category);
$stmt->execute();
}
public function lockCategory(CommentsCategoryInfo|string $category): void {
if($category instanceof CommentsCategoryInfo)
$category = $category->getId();
$stmt = $this->cache->get('UPDATE msz_comments_categories SET category_locked = COALESCE(category_locked, NOW()) WHERE category_id = ?');
$stmt->addParameter(1, $category);
$stmt->execute();
}
public function unlockCategory(CommentsCategoryInfo|string $category): void {
if($category instanceof CommentsCategoryInfo)
$category = $category->getId();
$stmt = $this->cache->get('UPDATE msz_comments_categories SET category_locked = NULL WHERE category_id = ?');
$stmt->addParameter(1, $category);
$stmt->execute();
}
public function countPosts(
CommentsCategoryInfo|string|null $category = null,
CommentsPostInfo|string|null $parent = null,
bool $includeReplies = false,
bool $includeDeleted = false
): int {
if($category instanceof CommentsCategoryInfo)
$category = $category->getId();
if($parent instanceof CommentsPostInfo)
$parent = $parent->getId();
$hasCategory = $category !== null;
$hasParent = $parent !== null;
$args = 0;
$query = 'SELECT COUNT(*) FROM msz_comments_posts';
if($hasParent) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' comment_reply_to = ?';
} else {
if($hasCategory) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' category_id = ?';
}
if(!$includeReplies) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' comment_reply_to IS NULL';
}
}
if(!$includeDeleted) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' comment_deleted IS NULL';
}
$args = 0;
$stmt = $this->cache->get($query);
if($hasParent)
$stmt->addParameter(++$args, $parent);
elseif($hasCategory)
$stmt->addParameter(++$args, $category);
$stmt->execute();
$result = $stmt->getResult();
$count = 0;
if($result->next())
$count = $result->getInteger(0);
return $count;
}
public function getPosts(
CommentsCategoryInfo|string|null $category = null,
CommentsPostInfo|string|null $parent = null,
bool $includeReplies = false,
bool $includeDeleted = false,
bool $includeRepliesCount = false,
bool $includeVotesCount = false
): array {
if($category instanceof CommentsCategoryInfo)
$category = $category->getId();
if($parent instanceof CommentsPostInfo)
$parent = $parent->getId();
$hasCategory = $category !== null;
$hasParent = $parent !== null;
$args = 0;
$query = 'SELECT comment_id, category_id, user_id, comment_reply_to, comment_text, UNIX_TIMESTAMP(comment_created), UNIX_TIMESTAMP(comment_pinned), UNIX_TIMESTAMP(comment_edited), UNIX_TIMESTAMP(comment_deleted)';
if($includeRepliesCount)
$query .= ', (SELECT COUNT(*) FROM msz_comments_posts AS ccr WHERE ccr.comment_reply_to = cpp.comment_id AND comment_deleted IS NULL) AS `comment_replies`';
if($includeVotesCount) {
$query .= ', (SELECT COUNT(*) FROM msz_comments_votes AS cvc WHERE cvc.comment_id = cpp.comment_id) AS `comment_votes_total`';
$query .= ', (SELECT COUNT(*) FROM msz_comments_votes AS cvc WHERE cvc.comment_id = cpp.comment_id AND comment_vote > 0) AS `comment_votes_positive`';
$query .= ', (SELECT COUNT(*) FROM msz_comments_votes AS cvc WHERE cvc.comment_id = cpp.comment_id AND comment_vote < 0) AS `comment_votes_negative`';
}
$query .= ' FROM msz_comments_posts AS cpp';
if($hasParent) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' comment_reply_to = ?';
} else {
if($hasCategory) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' category_id = ?';
}
if(!$includeReplies) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' comment_reply_to IS NULL';
}
}
if(!$includeDeleted) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' comment_deleted IS NULL';
}
// this should probably not be implicit like this
if($hasParent)
$query .= ' ORDER BY comment_deleted ASC, comment_pinned DESC, comment_created ASC';
elseif($hasCategory)
$query .= ' ORDER BY comment_deleted ASC, comment_pinned DESC, comment_created DESC';
else
$query .= ' ORDER BY comment_created DESC';
$args = 0;
$stmt = $this->cache->get($query);
if($hasParent)
$stmt->addParameter(++$args, $parent);
elseif($hasCategory)
$stmt->addParameter(++$args, $category);
$stmt->execute();
$posts = [];
$result = $stmt->getResult();
while($result->next())
$posts[] = new CommentsPostInfo($result, $includeRepliesCount, $includeVotesCount);
return $posts;
}
public function getPostById(
string $postId,
bool $includeRepliesCount = false,
bool $includeVotesCount = false
): CommentsPostInfo {
$query = 'SELECT comment_id, category_id, user_id, comment_reply_to, comment_text, UNIX_TIMESTAMP(comment_created), UNIX_TIMESTAMP(comment_pinned), UNIX_TIMESTAMP(comment_edited), UNIX_TIMESTAMP(comment_deleted)';
if($includeRepliesCount)
$query .= ', (SELECT COUNT(*) FROM msz_comments_posts AS ccr WHERE ccr.comment_reply_to = cpp.comment_id AND comment_deleted IS NULL) AS `comment_replies`';
if($includeVotesCount) {
$query .= ', (SELECT COUNT(*) FROM msz_comments_votes AS cvc WHERE cvc.comment_id = cpp.comment_id) AS `comment_votes_total`';
$query .= ', (SELECT COUNT(*) FROM msz_comments_votes AS cvc WHERE cvc.comment_id = cpp.comment_id AND comment_vote > 0) AS `comment_votes_positive`';
$query .= ', (SELECT COUNT(*) FROM msz_comments_votes AS cvc WHERE cvc.comment_id = cpp.comment_id AND comment_vote < 0) AS `comment_votes_negative`';
}
$query .= ' FROM msz_comments_posts AS cpp WHERE comment_id = ?';
$stmt = $this->cache->get($query);
$stmt->addParameter(1, $postId);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('No comment with that ID exists.');
return new CommentsPostInfo($result, $includeRepliesCount, $includeVotesCount);
}
public function createPost(
CommentsCategoryInfo|string|null $category,
CommentsPostInfo|string|null $parent,
User|string|null $user,
string $body,
bool $pin = false
): CommentsPostInfo {
if($category instanceof CommentsCategoryInfo)
$category = $category->getId();
if($parent instanceof CommentsPostInfo) {
if($category === null)
$category = $parent->getCategoryId();
elseif($category !== $parent->getCategoryId())
throw new InvalidArgumentException('$parent belongs to a different category than where this post is attempted to be created.');
$parent = $parent->getId();
}
if($category === null)
throw new InvalidArgumentException('$category is null; at least a $category or $parent must be specified.');
if($user instanceof User)
$user = (string)$user->getId();
if(empty(trim($body)))
throw new InvalidArgumentException('$body may not be empty.');
$stmt = $this->cache->get('INSERT INTO msz_comments_posts (category_id, user_id, comment_reply_to, comment_text, comment_pinned) VALUES (?, ?, ?, ?, IF(?, NOW(), NULL))');
$stmt->addParameter(1, $category);
$stmt->addParameter(2, $user);
$stmt->addParameter(3, $parent);
$stmt->addParameter(4, $body);
$stmt->addParameter(5, $pin ? 1 : 0);
$stmt->execute();
return $this->getPostById((string)$this->dbConn->getLastInsertId());
}
public function deletePost(CommentsPostInfo|string $infoOrId): void {
if($infoOrId instanceof CommentsPostInfo)
$infoOrId = $infoOrId->getId();
$stmt = $this->cache->get('UPDATE msz_comments_posts SET comment_deleted = COALESCE(comment_deleted, NOW()) WHERE comment_id = ?');
$stmt->addParameter(1, $infoOrId);
$stmt->execute();
}
public function nukePost(CommentsPostInfo|string $infoOrId): void {
if($infoOrId instanceof CommentsPostInfo)
$infoOrId = $infoOrId->getId();
$stmt = $this->cache->get('DELETE FROM msz_comments_posts WHERE comment_id = ?');
$stmt->addParameter(1, $infoOrId);
$stmt->execute();
}
public function restorePost(CommentsPostInfo|string $infoOrId): void {
if($infoOrId instanceof CommentsPostInfo)
$infoOrId = $infoOrId->getId();
$stmt = $this->cache->get('UPDATE msz_comments_posts SET comment_deleted = NULL WHERE comment_id = ?');
$stmt->addParameter(1, $infoOrId);
$stmt->execute();
}
public function editPost(CommentsPostInfo|string $infoOrId, string $body): void {
if($infoOrId instanceof CommentsPostInfo)
$infoOrId = $infoOrId->getId();
if(empty(trim($body)))
throw new InvalidArgumentException('$body may not be empty.');
$stmt = $this->cache->get('UPDATE msz_comments_posts SET comment_text = ?, comment_edited = NOW() WHERE comment_id = ?');
$stmt->addParameter(1, $body);
$stmt->addParameter(2, $infoOrId);
$stmt->execute();
}
public function pinPost(CommentsPostInfo|string $infoOrId): void {
if($infoOrId instanceof CommentsPostInfo)
$infoOrId = $infoOrId->getId();
$stmt = $this->cache->get('UPDATE msz_comments_posts SET comment_pinned = COALESCE(comment_pinned, NOW()) WHERE comment_id = ?');
$stmt->addParameter(1, $infoOrId);
$stmt->execute();
}
public function unpinPost(CommentsPostInfo|string $infoOrId): void {
if($infoOrId instanceof CommentsPostInfo)
$infoOrId = $infoOrId->getId();
$stmt = $this->cache->get('UPDATE msz_comments_posts SET comment_pinned = NULL WHERE comment_id = ?');
$stmt->addParameter(1, $infoOrId);
$stmt->execute();
}
public function getPostVote(
CommentsPostInfo|string $post,
User|string|null $user
): CommentsPostVoteInfo {
if($post instanceof CommentsPostInfo)
$post = $post->getId();
if($user instanceof User)
$user = (string)$user->getId();
// SUM() here makes it so a result row is always returned, albeit with just NULLs
$stmt = $this->cache->get('SELECT comment_id, user_id, SUM(comment_vote) FROM msz_comments_votes WHERE comment_id = ? AND user_id = ?');
$stmt->addParameter(1, $post);
$stmt->addParameter(2, $user);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('Failed to fetch vote info.');
return new CommentsPostVoteInfo($result);
}
public function addPostVote(
CommentsPostInfo|string $post,
User|string $user,
int $weight
): void {
if($weight === 0)
return;
if($post instanceof CommentsPostInfo)
$post = $post->getId();
if($user instanceof User)
$user = (string)$user->getId();
$stmt = $this->cache->get('REPLACE INTO msz_comments_votes (comment_id, user_id, comment_vote) VALUES (?, ?, ?)');
$stmt->addParameter(1, $post);
$stmt->addParameter(2, $user);
$stmt->addParameter(3, $weight);
$stmt->execute();
}
public function addPostPositiveVote(CommentsPostInfo|string $post, User|string $user): void {
$this->addPostVote($post, $user, 1);
}
public function addPostNegativeVote(CommentsPostInfo|string $post, User|string $user): void {
$this->addPostVote($post, $user, -1);
}
public function removePostVote(
CommentsPostInfo|string $post,
User|string $user
): void {
if($post instanceof CommentsPostInfo)
$post = $post->getId();
if($user instanceof User)
$user = (string)$user->getId();
$stmt = $this->cache->get('DELETE FROM msz_comments_votes WHERE comment_id = ? AND user_id = ?');
$stmt->addParameter(1, $post);
$stmt->addParameter(2, $user);
$stmt->execute();
}
}

View file

@ -1,167 +0,0 @@
<?php
namespace Misuzu\Comments;
use Misuzu\DB;
use Misuzu\Memoizer;
use Misuzu\Pagination;
use Misuzu\Users\User;
class CommentsCategoryException extends CommentsException {};
class CommentsCategoryNotFoundException extends CommentsCategoryException {};
class CommentsCategory {
// Database fields
private $category_id = -1;
private $category_name = '';
private $owner_id = null;
private $category_created = null;
private $category_locked = null;
private $postCount = -1;
private $owner = null;
public const TABLE = 'comments_categories';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`category_id`, %1$s.`category_name`, %1$s.`owner_id`'
. ', UNIX_TIMESTAMP(%1$s.`category_created`) AS `category_created`'
. ', UNIX_TIMESTAMP(%1$s.`category_locked`) AS `category_locked`';
public function __construct(?string $name = null) {
if($name !== null)
$this->setName($name);
}
public function getId(): int {
return $this->category_id < 1 ? -1 : $this->category_id;
}
public function getName(): string {
return $this->category_name;
}
public function setName(string $name): self {
$this->category_name = $name;
return $this;
}
public function getOwnerId(): int {
return $this->owner_id < 1 ? -1 : $this->owner_id;
}
public function hasOwner(): bool {
return $this->owner_id !== null;
}
public function getOwner(): User {
if($this->owner === null && ($ownerId = $this->getOwnerId()) >= 1)
$this->owner = User::byId($ownerId);
return $this->owner;
}
public function isOwner(User $user): bool {
return $this->hasOwner() && $user->getId() === $this->getOwnerId();
}
public function getCreatedTime(): int {
return $this->category_created === null ? -1 : $this->category_created;
}
public function getLockedTime(): int {
return $this->category_locked === null ? -1 : $this->category_locked;
}
public function isLocked(): bool {
return $this->getLockedTime() >= 0;
}
public function setLocked(bool $locked): self {
if($locked !== $this->isLocked())
$this->category_locked = $locked ? time() : null;
return $this;
}
// Purely cosmetic, do not use for anything other than displaying
public function getPostCount(): int {
if($this->postCount < 0)
$this->postCount = (int)DB::prepare('
SELECT COUNT(`comment_id`)
FROM `msz_comments_posts`
WHERE `category_id` = :cat_id
AND `comment_deleted` IS NULL
')->bind('cat_id', $this->getId())->fetchColumn();
return $this->postCount;
}
public function save(): void {
$isInsert = $this->getId() < 1;
if($isInsert) {
$query = 'INSERT INTO `%1$s%2$s` (`category_name`, `category_locked`) VALUES'
. ' (:name, :locked)';
} else {
$query = 'UPDATE `%1$s%2$s` SET `category_name` = :name, `category_locked` = FROM_UNIXTIME(:locked)'
. ' WHERE `category_id` = :category';
}
$saveCategory = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
->bind('name', $this->category_name)
->bind('locked', $this->category_locked);
if($isInsert) {
$this->category_id = $saveCategory->executeGetId();
$this->category_created = time();
} else {
$saveCategory->bind('category', $this->getId())
->execute();
}
}
public function posts(?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $rootOnly = true, bool $includeDeleted = true): array {
return CommentsPost::byCategory($this, $voteUser, $includeVotes, $pagination, $rootOnly, $includeDeleted);
}
public function votes(?User $user = null, bool $rootOnly = true, ?Pagination $pagination = null): array {
return CommentsVote::byCategory($this, $user, $rootOnly, $pagination);
}
private static function memoizer() {
static $memoizer = null;
if($memoizer === null)
$memoizer = new Memoizer;
return $memoizer;
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $categoryId): self {
return self::memoizer()->find($categoryId, function() use ($categoryId) {
$cat = DB::prepare(self::byQueryBase() . ' WHERE `category_id` = :cat_id')
->bind('cat_id', $categoryId)
->fetchObject(self::class);
if(!$cat)
throw new CommentsCategoryNotFoundException;
return $cat;
});
}
public static function byName(string $categoryName): self {
return self::memoizer()->find(function($category) use ($categoryName) {
return $category->getName() === $categoryName;
}, function() use ($categoryName) {
$cat = DB::prepare(self::byQueryBase() . ' WHERE `category_name` = :name')
->bind('name', $categoryName)
->fetchObject(self::class);
if(!$cat)
throw new CommentsCategoryNotFoundException;
return $cat;
});
}
public static function all(?Pagination $pagination = null): array {
$catsQuery = self::byQueryBase()
. ' ORDER BY `category_id` ASC';
if($pagination !== null)
$catsQuery .= ' LIMIT :range OFFSET :offset';
$getCats = DB::prepare($catsQuery);
if($pagination !== null)
$getCats->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getCats->fetchObjects(self::class);
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Misuzu\Comments;
use Index\DateTime;
use Index\Data\IDbResult;
use Misuzu\Users\User;
class CommentsCategoryInfo {
private string $id;
private string $name;
private ?string $ownerId;
private int $created;
private ?int $locked;
private int $comments;
public function __construct(IDbResult $result) {
$this->id = (string)$result->getInteger(0);
$this->name = $result->getString(1);
$this->ownerId = $result->isNull(2) ? null : (string)$result->getInteger(2);
$this->created = $result->getInteger(3);
$this->locked = $result->isNull(4) ? null : $result->getInteger(4);
$this->comments = $result->getInteger(5);
}
public function getId(): string {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function hasOwnerId(): bool {
return $this->ownerId !== null;
}
public function getOwnerId(): ?string {
return $this->ownerId;
}
public function isOwner(User|string $user): bool {
if($this->ownerId === null)
return false;
if($user instanceof User)
$user = (string)$user->getId();
return $user === $this->ownerId;
}
public function getCreatedTime(): int {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
}
public function getLockedTime(): ?int {
return $this->locked;
}
public function getLockedAt(): ?DateTime {
return $this->locked === null ? null : DateTime::fromUnixTimeSeconds($this->locked);
}
public function isLocked(): bool {
return $this->locked !== null;
}
public function getCommentsCount(): int {
return $this->comments;
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Misuzu\Comments;
use stdClass;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class CommentsEx {
private Comments $comments;
private array $userInfos;
public function __construct(Comments $comments, array $userInfos = []) {
$this->comments = $comments;
$this->userInfos = $userInfos;
}
public function getCommentsForLayout(CommentsCategoryInfo|string $category): object {
$info = new stdClass;
if(is_string($category))
$category = $this->comments->ensureCategory($category);
$info->user = User::getCurrent();
$info->category = $category;
$info->posts = [];
$root = $this->comments->getPosts($category, includeRepliesCount: true, includeVotesCount: true, includeDeleted: true);
foreach($root as $postInfo)
$info->posts[] = $this->decorateComment($postInfo);
return $info;
}
public function decorateComment(CommentsPostInfo $postInfo): object {
if($postInfo->hasUserId()) {
$userId = $postInfo->getUserId();
if(array_key_exists($userId, $this->userInfos)) {
$userInfo = $this->userInfos[$userId];
} else {
try {
$userInfo = User::byId($userId);
} catch(UserNotFoundException $ex) {
$userInfo = null;
}
$this->userInfos[$userId] = $userInfo;
}
} else $userInfo = null;
$info = new stdClass;
$info->post = $postInfo;
$info->user = $userInfo;
$info->vote = $this->comments->getPostVote($postInfo, $userInfo);
$info->replies = [];
$root = $this->comments->getPosts(parent: $postInfo, includeRepliesCount: true, includeVotesCount: true, includeDeleted: true);
foreach($root as $childInfo)
$info->replies[] = $this->decorateComment($childInfo);
return $info;
}
}

View file

@ -1,6 +0,0 @@
<?php
namespace Misuzu\Comments;
use RuntimeException;
class CommentsException extends RuntimeException {}

View file

@ -1,326 +0,0 @@
<?php
namespace Misuzu\Comments;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class CommentsPostException extends CommentsException {}
class CommentsPostNotFoundException extends CommentsPostException {}
class CommentsPostHasNoParentException extends CommentsPostException {}
class CommentsPostSaveFailedException extends CommentsPostException {}
class CommentsPost {
// Database fields
private $comment_id = -1;
private $category_id = -1;
private $user_id = null;
private $comment_reply_to = null;
private $comment_text = '';
private $comment_created = null;
private $comment_pinned = null;
private $comment_edited = null;
private $comment_deleted = null;
// Virtual fields
private $comment_likes = -1;
private $comment_dislikes = -1;
private $user_vote = null;
private $category = null;
private $user = null;
private $userLookedUp = false;
private $parentPost = null;
public const TABLE = 'comments_posts';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`comment_id`, %1$s.`category_id`, %1$s.`user_id`, %1$s.`comment_reply_to`, %1$s.`comment_text`'
. ', UNIX_TIMESTAMP(%1$s.`comment_created`) AS `comment_created`'
. ', UNIX_TIMESTAMP(%1$s.`comment_pinned`) AS `comment_pinned`'
. ', UNIX_TIMESTAMP(%1$s.`comment_edited`) AS `comment_edited`'
. ', UNIX_TIMESTAMP(%1$s.`comment_deleted`) AS `comment_deleted`';
private const LIKE_VOTE_SELECT = '(SELECT COUNT(`comment_id`) FROM `' . DB::PREFIX . CommentsVote::TABLE . '` WHERE `comment_id` = %1$s.`comment_id` AND `comment_vote` = ' . CommentsVote::LIKE . ') AS `comment_likes`';
private const DISLIKE_VOTE_SELECT = '(SELECT COUNT(`comment_id`) FROM `' . DB::PREFIX . CommentsVote::TABLE . '` WHERE `comment_id` = %1$s.`comment_id` AND `comment_vote` = ' . CommentsVote::DISLIKE . ') AS `comment_dislikes`';
private const USER_VOTE_SELECT = '(SELECT `comment_vote` FROM `' . DB::PREFIX . CommentsVote::TABLE . '` WHERE `comment_id` = %1$s.`comment_id` AND `user_id` = :user) AS `user_vote`';
public function getId(): int {
return $this->comment_id < 1 ? -1 : $this->comment_id;
}
public function getCategoryId(): int {
return $this->category_id < 1 ? -1 : $this->category_id;
}
public function setCategoryId(int $categoryId): self {
$this->category_id = $categoryId;
$this->category = null;
return $this;
}
public function getCategory(): CommentsCategory {
if($this->category === null)
$this->category = CommentsCategory::byId($this->getCategoryId());
return $this->category;
}
public function setCategory(CommentsCategory $category): self {
$this->category_id = $category->getId();
$this->category = null;
return $this;
}
public function getUserId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
public function setUserId(int $userId): self {
$this->user_id = $userId < 1 ? null : $userId;
$this->userLookedUp = false;
$this->user = null;
return $this;
}
public function getUser(): ?User {
if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
$this->userLookedUp = true;
try {
$this->user = User::byId($userId);
} catch(UserNotFoundException $ex) {}
}
return $this->user;
}
public function setUser(?User $user): self {
$this->user_id = $user === null ? null : $user->getId();
$this->userLookedUp = true;
$this->user = $user;
return $this;
}
public function getParentId(): int {
return $this->comment_reply_to < 1 ? -1 : $this->comment_reply_to;
}
public function setParentId(int $parentId): self {
$this->comment_reply_to = $parentId < 1 ? null : $parentId;
$this->parentPost = null;
return $this;
}
public function hasParent(): bool {
return $this->getParentId() > 0;
}
public function getParent(): CommentsPost {
if(!$this->hasParent())
throw new CommentsPostHasNoParentException;
if($this->parentPost === null)
$this->parentPost = CommentsPost::byId($this->getParentId());
return $this->parentPost;
}
public function setParent(?CommentsPost $parent): self {
$this->comment_reply_to = $parent === null ? null : $parent->getId();
$this->parentPost = $parent;
return $this;
}
public function getText(): string {
return $this->comment_text;
}
public function setText(string $text): self {
$this->comment_text = $text;
return $this;
}
public function getParsedText(): string {
return CommentsParser::parseForDisplay($this->getText());
}
public function setParsedText(string $text): self {
return $this->setText(CommentsParser::parseForStorage($text));
}
public function getCreatedTime(): int {
return $this->comment_created === null ? -1 : $this->comment_created;
}
public function getPinnedTime(): int {
return $this->comment_pinned === null ? -1 : $this->comment_pinned;
}
public function isPinned(): bool {
return $this->getPinnedTime() >= 0;
}
public function setPinned(bool $pinned): self {
if($this->isPinned() !== $pinned)
$this->comment_pinned = $pinned ? time() : null;
return $this;
}
public function getEditedTime(): int {
return $this->comment_edited === null ? -1 : $this->comment_edited;
}
public function isEdited(): bool {
return $this->getEditedTime() >= 0;
}
public function getDeletedTime(): int {
return $this->comment_deleted === null ? -1 : $this->comment_deleted;
}
public function isDeleted(): bool {
return $this->getDeletedTime() >= 0;
}
public function setDeleted(bool $deleted): self {
if($this->isDeleted() !== $deleted)
$this->comment_deleted = $deleted ? time() : null;
return $this;
}
public function getLikes(): int {
return $this->comment_likes;
}
public function getDislikes(): int {
return $this->comment_dislikes;
}
public function hasUserVote(): bool {
return $this->user_vote !== null;
}
public function getUserVote(): int {
return $this->user_vote ?? 0;
}
public function save(): void {
$isInsert = $this->getId() < 1;
if($isInsert) {
$query = 'INSERT INTO `%1$s%2$s` (`category_id`, `user_id`, `comment_reply_to`, `comment_text`'
. ', `comment_pinned`, `comment_deleted`) VALUES'
. ' (:category, :user, :parent, :text, FROM_UNIXTIME(:pinned), FROM_UNIXTIME(:deleted))';
} else {
$query = 'UPDATE `%1$s%2$s` SET `category_id` = :category, `user_id` = :user, `comment_reply_to` = :parent'
. ', `comment_text` = :text, `comment_pinned` = FROM_UNIXTIME(:pinned), `comment_deleted` = FROM_UNIXTIME(:deleted)'
. ' WHERE `comment_id` = :post';
}
$savePost = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
->bind('category', $this->category_id)
->bind('user', $this->user_id)
->bind('parent', $this->comment_reply_to)
->bind('text', $this->comment_text)
->bind('pinned', $this->comment_pinned)
->bind('deleted', $this->comment_deleted);
if($isInsert) {
$this->comment_id = $savePost->executeGetId();
if($this->comment_id < 1)
throw new CommentsPostSaveFailedException;
$this->comment_created = time();
} else {
$this->comment_edited = time();
$savePost->bind('post', $this->getId());
if(!$savePost->execute())
throw new CommentsPostSaveFailedException;
}
}
public function nuke(): void {
$replies = $this->replies(null, true);
foreach($replies as $reply)
$reply->nuke();
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `comment_id` = :comment')
->bind('comment_id', $this->getId())
->execute();
}
public function replies(?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $includeDeleted = true): array {
return CommentsPost::byParent($this, $voteUser, $includeVotes, $pagination, $includeDeleted);
}
public function votes(): CommentsVoteCount {
return CommentsVote::countByPost($this);
}
public function childVotes(?User $user = null, ?Pagination $pagination = null): array {
return CommentsVote::byParent($this, $user, $pagination);
}
public function addPositiveVote(User $user): void {
CommentsVote::create($this, $user, CommentsVote::LIKE);
}
public function addNegativeVote(User $user): void {
CommentsVote::create($this, $user, CommentsVote::DISLIKE);
}
public function removeVote(User $user): void {
CommentsVote::delete($this, $user);
}
public function getVoteFromUser(User $user): CommentsVote {
return CommentsVote::byExact($this, $user);
}
private static function byQueryBase(bool $includeVotes = true, bool $includeUserVote = false): string {
$select = self::SELECT;
if($includeVotes)
$select .= ', ' . self::LIKE_VOTE_SELECT
. ', ' . self::DISLIKE_VOTE_SELECT;
if($includeUserVote)
$select .= ', ' . self::USER_VOTE_SELECT;
return sprintf(self::QUERY_SELECT, sprintf($select, self::TABLE));
}
public static function byId(int $postId): self {
$getPost = DB::prepare(self::byQueryBase() . ' WHERE `comment_id` = :post_id');
$getPost->bind('post_id', $postId);
$post = $getPost->fetchObject(self::class);
if(!$post)
throw new CommentsPostNotFoundException;
return $post;
}
public static function byCategory(CommentsCategory $category, ?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $rootOnly = true, bool $includeDeleted = true): array {
$postsQuery = self::byQueryBase($includeVotes, $voteUser !== null)
. ' WHERE `category_id` = :category'
. (!$rootOnly ? '' : ' AND `comment_reply_to` IS NULL')
. ($includeDeleted ? '' : ' AND `comment_deleted` IS NULL')
. ' ORDER BY `comment_deleted` ASC, `comment_pinned` DESC, `comment_id` DESC';
if($pagination !== null)
$postsQuery .= ' LIMIT :range OFFSET :offset';
$getPosts = DB::prepare($postsQuery)
->bind('category', $category->getId());
if($voteUser !== null)
$getPosts->bind('user', $voteUser->getId());
if($pagination !== null)
$getPosts->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getPosts->fetchObjects(self::class);
}
public static function byParent(CommentsPost $parent, ?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $includeDeleted = true): array {
$postsQuery = self::byQueryBase($includeVotes, $voteUser !== null)
. ' WHERE `comment_reply_to` = :parent'
. ($includeDeleted ? '' : ' AND `comment_deleted` IS NULL')
. ' ORDER BY `comment_deleted` ASC, `comment_pinned` DESC, `comment_id` ASC';
if($pagination !== null)
$postsQuery .= ' LIMIT :range OFFSET :offset';
$getPosts = DB::prepare($postsQuery)
->bind('parent', $parent->getId());
if($voteUser !== null)
$getPosts->bind('user', $voteUser->getId());
if($pagination !== null)
$getPosts->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getPosts->fetchObjects(self::class);
}
public static function all(?Pagination $pagination = null, bool $rootOnly = true, bool $includeDeleted = false): array {
$postsQuery = self::byQueryBase()
. ' WHERE 1' // this is disgusting
. (!$rootOnly ? '' : ' AND `comment_reply_to` IS NULL')
. ($includeDeleted ? '' : ' AND `comment_deleted` IS NULL')
. ' ORDER BY `comment_id` DESC';
if($pagination !== null)
$postsQuery .= ' LIMIT :range OFFSET :offset';
$getPosts = DB::prepare($postsQuery);
if($pagination !== null)
$getPosts->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getPosts->fetchObjects(self::class);
}
}

View file

@ -0,0 +1,146 @@
<?php
namespace Misuzu\Comments;
use Index\DateTime;
use Index\Data\IDbResult;
class CommentsPostInfo {
private string $id;
private string $categoryId;
private ?string $userId;
private ?string $replyingTo;
private string $body;
private int $created;
private ?int $pinned;
private ?int $updated;
private ?int $deleted;
private int $replies;
private int $votesTotal;
private int $votesPositive;
private int $votesNegative;
public function __construct(
IDbResult $result,
bool $includeRepliesCount = false,
bool $includeVotesCount = false
) {
$args = 0;
$this->id = (string)$result->getInteger($args);
$this->categoryId = (string)$result->getInteger(++$args);
$this->userId = $result->isNull(++$args) ? null : (string)$result->getInteger($args);
$this->replyingTo = $result->isNull(++$args) ? null : (string)$result->getInteger($args);
$this->body = $result->getString(++$args);
$this->created = $result->getInteger(++$args);
$this->pinned = $result->isNull(++$args) ? null : $result->getInteger($args);
$this->updated = $result->isNull(++$args) ? null : $result->getInteger($args);
$this->deleted = $result->isNull(++$args) ? null : $result->getInteger($args);
$this->replies = $includeRepliesCount ? $result->getInteger(++$args) : 0;
if($includeVotesCount) {
$this->votesTotal = $result->getInteger(++$args);
$this->votesPositive = $result->getInteger(++$args);
$this->votesNegative = $result->getInteger(++$args);
} else {
$this->votesTotal = 0;
$this->votesPositive = 0;
$this->votesNegative = 0;
}
}
public function getId(): string {
return $this->id;
}
public function getCategoryId(): string {
return $this->categoryId;
}
public function hasUserId(): bool {
return $this->userId !== null;
}
public function getUserId(): ?string {
return $this->userId;
}
public function isReply(): bool {
return $this->replyingTo !== null;
}
public function getReplyingTo(): ?string {
return $this->replyingTo;
}
public function getBody(): string {
return $this->body;
}
public function getCreatedTime(): int {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
}
public function getPinnedTime(): ?int {
return $this->pinned;
}
public function getPinnedAt(): DateTime {
return $this->pinned === null ? null : DateTime::fromUnixTimeSeconds($this->pinned);
}
public function isPinned(): bool {
return $this->pinned !== null;
}
public function getUpdatedTime(): ?int {
return $this->updated;
}
public function getUpdatedAt(): DateTime {
return $this->updated === null ? null : DateTime::fromUnixTimeSeconds($this->updated);
}
public function isEdited(): bool {
return $this->updated !== null;
}
public function getDeletedTime(): ?int {
return $this->deleted;
}
public function getDeletedAt(): DateTime {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted);
}
public function isDeleted(): bool {
return $this->deleted !== null;
}
public function hasRepliesCount(): bool {
return $this->replies > 0;
}
public function getRepliesCount(): int {
return $this->replies;
}
public function hasVotesCount(): bool {
return $this->votesTotal > 0;
}
public function getVotesTotal(): int {
return $this->votesTotal;
}
public function getVotesPositive(): int {
return $this->votesPositive;
}
public function getVotesNegative(): int {
return $this->votesNegative;
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Misuzu\Comments;
use Index\Data\IDbResult;
class CommentsPostVoteInfo {
private string $commentId;
private string $userId;
private int $weight;
public function __construct(IDbResult $result) {
$this->commentId = (string)$result->getInteger(0);
$this->userId = (string)$result->getInteger(1);
$this->weight = $result->getInteger(2);
}
public function getCommentId(): string {
return $this->commentId;
}
public function getUserId(): string {
return $this->userId;
}
public function getWeight(): int {
return $this->weight;
}
}

View file

@ -1,228 +0,0 @@
<?php
namespace Misuzu\Comments;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Users\User;
class CommentsVoteException extends CommentsException {}
class CommentsVoteCountFailedException extends CommentsVoteException {}
class CommentsVoteCreateFailedException extends CommentsVoteException {}
class CommentsVoteCount {
private $comment_id = -1;
private $likes = 0;
private $dislikes = 0;
private $total = 0;
public function getPostId(): int {
return $this->comment_id < 1 ? -1 : $this->comment_id;
}
public function getLikes(): int {
return $this->likes;
}
public function getDislikes(): int {
return $this->dislikes;
}
public function getTotal(): int {
return $this->total;
}
}
class CommentsVote {
// Database fields
private $comment_id = -1;
private $user_id = -1;
private $comment_vote = 0;
private $comment = null;
private $user = null;
public const LIKE = 1;
public const NONE = 0;
public const DISLIKE = -1;
public const TABLE = 'comments_votes';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`comment_id`, %1$s.`user_id`, %1$s.`comment_vote`';
private const QUERY_COUNT = 'SELECT %3$d AS `comment_id`'
. ', (SELECT COUNT(`comment_id`) FROM `%1$s%2$s` WHERE %6$s) AS `total`'
. ', (SELECT COUNT(`comment_id`) FROM `%1$s%2$s` WHERE %6$s AND `comment_vote` = %4$d) AS `likes`'
. ', (SELECT COUNT(`comment_id`) FROM `%1$s%2$s` WHERE %6$s AND `comment_vote` = %5$d) AS `dislikes`';
public function getPostId(): int {
return $this->comment_id < 1 ? -1 : $this->comment_id;
}
public function getPost(): CommentsPost {
if($this->comment === null)
$this->comment = CommentsPost::byId($this->comment_id);
return $this->comment;
}
public function getUserId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
public function getUser(): User {
if($this->user === null)
$this->user = User::byId($this->user_id);
return $this->user;
}
public function getVote(): int {
return $this->comment_vote;
}
public static function create(CommentsPost $post, User $user, int $vote, bool $return = false): ?self {
$createVote = DB::prepare('
REPLACE INTO `msz_comments_votes`
(`comment_id`, `user_id`, `comment_vote`)
VALUES
(:post, :user, :vote)
') ->bind('post', $post->getId())
->bind('user', $user->getId())
->bind('vote', $vote);
if(!$createVote->execute())
throw new CommentsVoteCreateFailedException;
if(!$return)
return null;
return self::byExact($post, $user);
}
public static function delete(CommentsPost $post, User $user): void {
DB::prepare('DELETE FROM `msz_comments_votes` WHERE `comment_id` = :post AND `user_id` = :user')
->bind('post', $post->getId())
->bind('user', $user->getId())
->execute();
}
private static function countQueryBase(int $id, string $condition = '1'): string {
return sprintf(self::QUERY_COUNT, DB::PREFIX, self::TABLE, $id, self::LIKE, self::DISLIKE, $condition);
}
public static function countByPost(CommentsPost $post): CommentsVoteCount {
$count = DB::prepare(self::countQueryBase($post->getId(), sprintf('`comment_id` = %d', $post->getId())))
->fetchObject(CommentsVoteCount::class);
if(!$count)
throw new CommentsVoteCountFailedException;
return $count;
}
private static function fake(CommentsPost $post, User $user, int $vote): CommentsVote {
$fake = new CommentsVote;
$fake->comment_id = $post->getId();
$fake->comment = $post;
$fake->user_id = $user->getId();
$fake->user = $user;
$fake->comment_vote = $vote;
return $fake;
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byExact(CommentsPost $post, User $user): self {
$vote = DB::prepare(self::byQueryBase() . ' WHERE `comment_id` = :post_id AND `user_id` = :user_id')
->bind('post_id', $post->getId())
->bind('user_id', $user->getId())
->fetchObject(self::class);
if(!$vote)
return self::fake($post, $user, self::NONE);
return $vote;
}
public static function byPost(CommentsPost $post, ?User $user = null, ?Pagination $pagination = null): array {
$votesQuery = self::byQueryBase()
. ' WHERE `comment_id` = :post'
. ($user === null ? '' : ' AND `user_id` = :user');
if($pagination !== null)
$votesQuery .= ' LIMIT :range OFFSET :offset';
$getVotes = DB::prepare($votesQuery)
->bind('post', $post->getId());
if($user !== null)
$getVotes->bind('user', $user->getId());
if($pagination !== null)
$getVotes->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getVotes->fetchObjects(self::class);
}
public static function byUser(User $user, ?Pagination $pagination = null): array {
$votesQuery = self::byQueryBase()
. ' WHERE `user_id` = :user';
if($pagination !== null)
$votesQuery .= ' LIMIT :range OFFSET :offset';
$getVotes = DB::prepare($votesQuery)
->bind('user', $user->getId());
if($pagination !== null)
$getVotes->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getVotes->fetchObjects(self::class);
}
public static function byCategory(CommentsCategory $category, ?User $user = null, bool $rootOnly = true, ?Pagination $pagination = null): array {
$votesQuery = self::byQueryBase()
. ' WHERE `comment_id` IN'
. ' (SELECT `comment_id` FROM `' . DB::PREFIX . CommentsPost::TABLE . '` WHERE `category_id` = :category'
. (!$rootOnly ? '' : ' AND `comment_reply_to` IS NULL')
. ')'
. ($user === null ? '' : ' AND `user_id` = :user');
if($pagination !== null)
$votesQuery .= ' LIMIT :range OFFSET :offset';
$getVotes = DB::prepare($votesQuery)
->bind('category', $category->getId());
if($user !== null)
$getVotes->bind('user', $user->getId());
if($pagination !== null)
$getVotes->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getVotes->fetchObjects(self::class);
}
public static function byParent(CommentsPost $parent, ?User $user = null, ?Pagination $pagination = null): array {
$votesQuery = self::byQueryBase()
. ' WHERE `comment_id` IN'
. ' (SELECT `comment_id` FROM `' . DB::PREFIX . CommentsPost::TABLE . '` WHERE `comment_reply_to` = :parent)'
. ($user === null ? '' : ' AND `user_id` = :user');
if($pagination !== null)
$votesQuery .= ' LIMIT :range OFFSET :offset';
$getVotes = DB::prepare($votesQuery)
->bind('parent', $parent->getId());
if($user !== null)
$getVotes->bind('user', $user->getId());
if($pagination !== null)
$getVotes->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getVotes->fetchObjects(self::class);
}
public static function all(?Pagination $pagination = null): array {
$votesQuery = self::byQueryBase();
if($pagination !== null)
$votesQuery .= ' LIMIT :range OFFSET :offset';
$getVotes = DB::prepare($votesQuery);
if($pagination !== null)
$getVotes->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getVotes->fetchObjects(self::class);
}
}

View file

@ -12,7 +12,7 @@ class Database {
}
public function queries(): int {
return (int)$this->query('SHOW SESSION STATUS LIKE "Questions"')->fetchColumn(1);
return ((int)$this->query('SHOW SESSION STATUS LIKE "Questions"')->fetchColumn(1));
}
public function exec(string $stmt): int {

View file

@ -7,8 +7,7 @@ use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Comments\CommentsEx;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
@ -17,6 +16,8 @@ use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class ChangelogHandler extends Handler {
private array $userInfos = [];
public function index($response, $request) {
$filterDate = (string)$request->getParam('date');
$filterUser = (int)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
@ -60,13 +61,12 @@ class ChangelogHandler extends Handler {
return 404;
$changes = [];
$userInfos = [];
foreach($changeInfos as $changeInfo) {
$userId = $changeInfo->getUserId();
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
if(array_key_exists($userId, $this->userInfos)) {
$userInfo = $this->userInfos[$userId];
} else {
try {
$userInfo = User::byId($userId);
@ -74,7 +74,7 @@ class ChangelogHandler extends Handler {
$userInfo = null;
}
$userInfos[$userId] = $userInfo;
$this->userInfos[$userId] = $userInfo;
}
$changes[] = [
@ -89,20 +89,13 @@ class ChangelogHandler extends Handler {
'changelog_user' => $filterUser,
'changelog_tags' => $filterTags,
'changelog_pagination' => $pagination,
'comments_user' => User::getCurrent(),
'comments_category' => empty($filterDate) ? null : self::getCommentsCategory($changeInfos[0]->getCommentsCategoryName()),
'comments_info' => empty($filterDate) ? null : $this->getCommentsInfo($changeInfos[0]->getCommentsCategoryName()),
]));
}
private static function getCommentsCategory(string $categoryName): CommentsCategory {
try {
$category = CommentsCategory::byName($categoryName);
} catch(CommentsCategoryNotFoundException $ex) {
$category = new CommentsCategory($categoryName);
$category->save();
}
return $category;
private function getCommentsInfo(string $categoryName): object {
$comments = new CommentsEx($this->context->getComments(), $this->userInfos);
return $comments->getCommentsForLayout($categoryName);
}
public function change($response, $request, string $changeId) {
@ -121,8 +114,7 @@ class ChangelogHandler extends Handler {
$response->setContent(Template::renderRaw('changelog.change', [
'change_info' => $changeInfo,
'change_user_info' => $userInfo,
'comments_user' => User::getCurrent(),
'comments_category' => self::getCommentsCategory($changeInfo->getCommentsCategoryName()),
'comments_info' => $this->getCommentsInfo($changeInfo->getCommentsCategoryName()),
]));
}

View file

@ -1,13 +1,13 @@
<?php
namespace Misuzu\Http\Handlers;
use RuntimeException;
use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
use Misuzu\Users\UserNotFoundException;
@ -107,6 +107,7 @@ final class HomeHandler extends Handler {
public function home($response, $request): void {
$news = $this->context->getNews();
$comments = $this->context->getComments();
$featuredNews = [];
$userInfos = [];
$categoryInfos = [];
@ -136,11 +137,8 @@ final class HomeHandler extends Handler {
else
$categoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo);
$commentsCount = 0;
if($postInfo->hasCommentsCategoryId())
try {
$commentsCount = CommentsCategory::byId($postInfo->getCommentsCategoryId())->getPostCount();
} catch(CommentsCategoryNotFoundException $ex) {}
$commentsCount = $postInfo->hasCommentsCategoryId()
? $comments->countPosts($postInfo->getCommentsCategoryId(), includeReplies: true) : 0;
$featuredNews[] = [
'post' => $postInfo,

View file

@ -7,7 +7,7 @@ use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Comments\CommentsEx;
use Misuzu\Config\IConfig;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
@ -21,6 +21,7 @@ use Misuzu\Users\UserNotFoundException;
final class NewsHandler extends Handler {
private function fetchPostInfo(array $postInfos, array $categoryInfos = []): array {
$news = $this->context->getNews();
$comments = $this->context->getComments();
$posts = [];
$userInfos = [];
@ -45,11 +46,8 @@ final class NewsHandler extends Handler {
else
$categoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo);
$commentsCount = 0;
if($postInfo->hasCommentsCategoryId())
try {
$commentsCount = CommentsCategory::byId($postInfo->getCommentsCategoryId())->getPostCount();
} catch(CommentsCategoryNotFoundException $ex) {}
$commentsCount = $postInfo->hasCommentsCategoryId()
? $comments->countPosts($postInfo->getCommentsCategoryId(), includeReplies: true) : 0;
$posts[] = [
'post' => $postInfo,
@ -116,6 +114,7 @@ final class NewsHandler extends Handler {
public function viewPost($response, $request, string $postId) {
$news = $this->context->getNews();
$comments = $this->context->getComments();
try {
$postInfo = $news->getPostById($postId);
@ -128,17 +127,13 @@ final class NewsHandler extends Handler {
$categoryInfo = $news->getCategoryByPost($postInfo);
$comments = $this->context->getComments();
if($postInfo->hasCommentsCategoryId()) {
$commentsCategory = CommentsCategory::byId($postInfo->getCommentsCategoryId());
$commentsCategory = $comments->getCategoryById($postInfo->getCommentsCategoryId());
} else {
$commentsCategoryName = $postInfo->getCommentsCategoryName();
try {
$commentsCategory = CommentsCategory::byName($commentsCategoryName);
} catch(CommentsCategoryNotFoundException $ex) {
$commentsCategory = new CommentsCategory($commentsCategoryName);
$commentsCategory->save();
$news->updatePostCommentCategory($postInfo, $commentsCategory);
}
$commentsCategory = $comments->ensureCategory($postInfo->getCommentsCategoryName());
$news->updatePostCommentCategory($postInfo, $commentsCategory);
}
$userInfo = null;
@ -147,12 +142,13 @@ final class NewsHandler extends Handler {
$userInfo = User::byId($postInfo->getUserId());
} catch(UserNotFoundException $ex) {}
$comments = new CommentsEx($comments);
$response->setContent(Template::renderRaw('news.post', [
'post_info' => $postInfo,
'post_category_info' => $categoryInfo,
'post_user_info' => $userInfo,
'comments_info' => $commentsCategory,
'comments_user' => User::getCurrent(),
'comments_info' => $comments->getCommentsForLayout($commentsCategory),
]));
}

View file

@ -3,6 +3,7 @@ namespace Misuzu;
use Misuzu\Template;
use Misuzu\Changelog\Changelog;
use Misuzu\Comments\Comments;
use Misuzu\Config\IConfig;
use Misuzu\Emoticons\Emotes;
use Misuzu\News\News;
@ -27,6 +28,7 @@ class MisuzuContext {
private Emotes $emotes;
private Changelog $changelog;
private News $news;
private Comments $comments;
public function __construct(IDbConnection $dbConn, IConfig $config) {
$this->dbConn = $dbConn;
@ -35,6 +37,7 @@ class MisuzuContext {
$this->emotes = new Emotes($this->dbConn);
$this->changelog = new Changelog($this->dbConn);
$this->news = new News($this->dbConn);
$this->comments = new Comments($this->dbConn);
}
public function getDbConn(): IDbConnection {
@ -78,6 +81,10 @@ class MisuzuContext {
return $this->news;
}
public function getComments(): Comments {
return $this->comments;
}
public function setUpHttp(bool $legacy = false): void {
$this->router = new HttpFx;
$this->router->use('/', function($response) {

View file

@ -8,7 +8,7 @@ use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Misuzu\DbStatementCache;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryInfo;
use Misuzu\Users\User;
class News {
@ -465,17 +465,16 @@ class News {
public function updatePostCommentCategory(
NewsPostInfo|string $postInfo,
CommentsCategory|string $commentsCategory
CommentsCategoryInfo|string $commentsCategory
): void {
if($postInfo instanceof NewsPostInfo)
$postInfo = $postInfo->getId();
if($commentsCategory instanceof CommentsCategory)
$commentsCategory = (string)$commentsCategory->getId();
if($commentsCategory instanceof CommentsCategoryInfo)
$commentsCategory = $commentsCategory->getId();
// "post_updated = post_updated" is an Attempt at making this not bump post_updated ON UPDATE
$stmt = $this->cache->get('UPDATE msz_news_posts SET comment_section_id = ?, post_updated = post_updated WHERE post_id = ?');
$stmt->addParameter(1, $postInfo);
$stmt->addParameter(2, $commentsCategory);
$stmt = $this->cache->get('UPDATE msz_news_posts SET comment_section_id = ? WHERE post_id = ?');
$stmt->addParameter(1, $commentsCategory);
$stmt->addParameter(2, $postInfo);
$stmt->execute();
}
}

View file

@ -5,10 +5,11 @@ use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\Environment as TwigEnvironment;
use Misuzu\Parsers\Parser;
use Misuzu\MisuzuContext;
use Index\ByteFormat;
use Index\Environment;
use Misuzu\MisuzuContext;
use Misuzu\Comments\CommentsParser;
use Misuzu\Parsers\Parser;
final class TwigMisuzu extends AbstractExtension {
private MisuzuContext $ctx;
@ -22,6 +23,7 @@ final class TwigMisuzu extends AbstractExtension {
new TwigFilter('html_colour', 'html_colour'),
new TwigFilter('country_name', 'get_country_name'),
new TwigFilter('parse_text', fn(string $text, int $parser): string => Parser::instance($parser)->parseText($text)),
new TwigFilter('parse_comment', fn(string $text): string => CommentsParser::parseForDisplay($text)),
new TwigFilter('perms_check', 'perms_check'),
new TwigFilter('time_diff', [$this, 'timeDiff'], ['needs_environment' => true]),
];

View file

@ -43,9 +43,27 @@
{% macro comments_entry(comment, indent, category, user) %}
{% from 'macros.twig' import avatar %}
{% from '_layout/input.twig' import input_checkbox_raw %}
{% set hide_details = comment.userId < 1 or comment.deleted and not user.commentPerms.can_delete_any|default(false) %}
{% if user.commentPerms.can_delete_any|default(false) or (not comment.deleted or comment.replies(user)|length > 0) %}
{% set replies = comment.replies %}
{% set poster = comment.user|default(null) %}
{% if comment.post is defined %}
{% set userVote = comment.vote.weight %}
{% set comment = comment.post %}
{% set body = comment.body %}
{% set likes = comment.votesPositive %}
{% set dislikes = comment.votesNegative %}
{% set isReply = comment.isReply %}
{% else %}
{% set body = comment.text %}
{% set userVote = comment.userVote %}
{% set likes = comment.likes %}
{% set dislikes = comment.dislikes %}
{% set isReply = comment.hasParent %}
{% endif %}
{% set hide_details = poster is null or comment.deleted and not user.commentPerms.can_delete_any|default(false) %}
{% if user.commentPerms.can_delete_any|default(false) or (not comment.deleted or replies|length > 0) %}
<div class="comment{% if comment.deleted %} comment--deleted{% endif %}" id="comment-{{ comment.id }}">
<div class="comment__container">
{% if hide_details %}
@ -53,16 +71,16 @@
{{ avatar(0, indent > 1 ? 40 : 50) }}
</div>
{% else %}
<a class="comment__avatar" href="{{ url('user-profile', {'user':comment.user.id}) }}">
{{ avatar(comment.user.id, indent > 1 ? 40 : 50, comment.user.username) }}
<a class="comment__avatar" href="{{ url('user-profile', {'user': poster.id}) }}">
{{ avatar(poster.id, indent > 1 ? 40 : 50, poster.username) }}
</a>
{% endif %}
<div class="comment__content">
<div class="comment__info">
{% if not hide_details %}
<a class="comment__user comment__user--link"
href="{{ url('user-profile', {'user':comment.user.id}) }}"
style="--user-colour: {{ comment.user.colour}}">{{ comment.user.username }}</a>
href="{{ url('user-profile', {'user': poster.id}) }}"
style="--user-colour: {{ poster.colour}}">{{ poster.username }}</a>
{% endif %}
<a class="comment__link" href="#comment-{{ comment.id }}">
<time class="comment__date"
@ -84,39 +102,39 @@
{% endif %}
</div>
<div class="comment__text">
{{ hide_details ? '(deleted)' : comment.parsedText|raw }}
{{ hide_details ? '(deleted)' : body|parse_comment|raw }}
</div>
<div class="comment__actions">
{% if not comment.deleted and user is not null %}
{% if user.commentPerms.can_vote|default(false) %}
{% set like_vote_state = comment.userVote > 0 ? 0 : 1 %}
{% set dislike_vote_state = comment.userVote < 0 ? 0 : -1 %}
{% set like_vote_state = userVote > 0 ? 0 : 1 %}
{% set dislike_vote_state = userVote < 0 ? 0 : -1 %}
<a class="comment__action comment__action--link comment__action--vote comment__action--like{% if comment.userVote > 0 %} comment__action--voted{% endif %}" data-comment-id="{{ comment.id }}" data-comment-vote="{{ like_vote_state }}"
<a class="comment__action comment__action--link comment__action--vote comment__action--like{% if userVote > 0 %} comment__action--voted{% endif %}" data-comment-id="{{ comment.id }}" data-comment-vote="{{ like_vote_state }}"
href="{{ url('comment-vote', {'comment':comment.id,'vote':like_vote_state}) }}">
Like
{% if comment.likes > 0 %}
({{ comment.likes|number_format }})
{% if likes > 0 %}
({{ likes|number_format }})
{% endif %}
</a>
<a class="comment__action comment__action--link comment__action--vote comment__action--dislike{% if comment.userVote < 0 %} comment__action--voted{% endif %}" data-comment-id="{{ comment.id }}" data-comment-vote="{{ dislike_vote_state }}"
<a class="comment__action comment__action--link comment__action--vote comment__action--dislike{% if userVote < 0 %} comment__action--voted{% endif %}" data-comment-id="{{ comment.id }}" data-comment-vote="{{ dislike_vote_state }}"
href="{{ url('comment-vote', {'comment':comment.id,'vote':dislike_vote_state}) }}">
Dislike
{% if comment.dislikes > 0 %}
({{ comment.dislikes|number_format }})
{% if dislikes > 0 %}
({{ dislikes|number_format }})
{% endif %}
</a>
{% endif %}
{% if user.commentPerms.can_comment|default(false) %}
<label class="comment__action comment__action--link" for="comment-reply-toggle-{{ comment.id }}">Reply</label>
{% endif %}
{% if user.commentPerms.can_delete_any|default(false) or (comment.user.id|default(0) == user.id and user.commentPerms.can_delete|default(false)) %}
{% if user.commentPerms.can_delete_any|default(false) or (poster.id|default(0) == user.id and user.commentPerms.can_delete|default(false)) %}
<a class="comment__action comment__action--link comment__action--hide comment__action--delete" data-comment-id="{{ comment.id }}" href="{{ url('comment-delete', {'comment':comment.id}) }}">Delete</a>
{% endif %}
{# if user is not null %}
<a class="comment__action comment__action--link comment__action--hide" href="#">Report</a>
{% endif #}
{% if not comment.hasParent and user.commentPerms.can_pin|default(false) %}
{% if not isReply and user.commentPerms.can_pin|default(false) %}
<a class="comment__action comment__action--link comment__action--hide comment__action--pin" data-comment-id="{{ comment.id }}" data-comment-pinned="{{ comment.pinned ? '1' : '0' }}" href="{{ url('comment-' ~ (comment.pinned ? 'unpin' : 'pin'), {'comment':comment.id}) }}">{{ comment.pinned ? 'Unpin' : 'Pin' }}</a>
{% endif %}
{% elseif user.commentPerms.can_delete_any|default(false) %}
@ -132,8 +150,8 @@
{{ input_checkbox_raw('', false, 'comment__reply-toggle', '', false, {'id':'comment-reply-toggle-' ~ comment.id}) }}
{{ comments_input(category, user, comment) }}
{% endif %}
{% if comment.replies|length > 0 %}
{% for reply in comment.replies %}
{% if replies|length > 0 %}
{% for reply in replies %}
{{ comments_entry(reply, indent + 1, category, user) }}
{% endfor %}
{% endif %}
@ -143,6 +161,14 @@
{% endmacro %}
{% macro comments_section(category, user) %}
{% if category.category is defined %}
{% set user = category.user %}
{% set posts = category.posts %}
{% set category = category.category %}
{% else %}
{% set posts = category.posts %}
{% endif %}
<div class="comments" id="comments">
<div class="comments__input">
{% if user|default(null) is null %}
@ -180,9 +206,9 @@
</noscript>#}
<div class="comments__listing">
{% if category.posts|length > 0 %}
{% if posts|length > 0 %}
{% from _self import comments_entry %}
{% for comment in category.posts(user) %}
{% for comment in posts %}
{{ comments_entry(comment, 1, category, user) }}
{% endfor %}
{% else %}

View file

@ -69,6 +69,6 @@
<div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments for ' ~ change_info.date) }}
{{ comments_section(comments_category, comments_user) }}
{{ comments_section(comments_info) }}
</div>
{% endblock %}

View file

@ -58,7 +58,7 @@
{% if is_date %}
<div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments') }}
{{ comments_section(comments_category, comments_user) }}
{{ comments_section(comments_info) }}
</div>
{% endif %}
{% endblock %}

View file

@ -13,7 +13,7 @@
{% if comments_info is defined %}
<div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments') }}
{{ comments_section(comments_info, comments_user) }}
{{ comments_section(comments_info) }}
</div>
{% endif %}
{% endblock %}