Commit 93c635b5 authored by Matthias Larisch's avatar Matthias Larisch

Implement forum rest endpoints + reactions

parent 9817e410
CREATE TABLE `fs_reaction` (
`target` VARCHAR(63) NOT NULL,
`time` DATETIME NOT NULL,
`foodsaver_id` INT NOT NULL,
`emoji` VARCHAR(63),
INDEX (`target`),
UNIQUE KEY `target-foodsaver-emoji` (`target`, `foodsaver_id`, `emoji`)
)
ENGINE = InnoDB;
<?php
namespace Foodsharing\Controller;
use Foodsharing\Lib\Session;
use Foodsharing\Modules\Region\ForumGateway;
use Foodsharing\Permissions\ForumPermissions;
use Foodsharing\Services\ForumService;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpKernel\Exception\HttpException;
class ForumRestController extends FOSRestController
{
private $session;
private $forumGateway;
private $forumPermissions;
private $forumService;
public function __construct(Session $session, ForumGateway $forumGateway, ForumPermissions $forumPermissions, ForumService $forumService)
{
$this->session = $session;
$this->forumGateway = $forumGateway;
$this->forumPermissions = $forumPermissions;
$this->forumService = $forumService;
}
private function normalizeThread($thread)
{
$res = [
'id' => $thread['id'],
'name' => $thread['name'],
'createdAt' => $thread['time'],
'sticky' => $thread['sticky'],
'active' => $thread['active'] ?? 1,
'lastPost' => [
'id' => $thread['last_post_id'],
],
'creator' => [
'id' => $thread['creator_id'],
]
];
if (isset($thread['post_time'])) {
$res['lastPost']['createdAt'] = $thread['post_time'];
$res['lastPost']['body'] = $thread['post_body'];
$res['lastPost']['author'] = [
'id' => $thread['foodsaver_id'],
'name' => $thread['foodsaver_name'],
'avatar' => '/images/130_q_' . $thread['foodsaver_photo'],
'sleepStatus' => $thread['sleep_status']
];
}
if (isset($thread['creator_name'])) {
$res['creator'] = [
'id' => $thread['creator_id'],
'name' => $thread['creator_name'],
'avatar' => '/images/130_q_' . $thread['creator_photo'],
'sleepStatus' => $thread['creator_sleep_status']
];
}
return $res;
}
private function normalizePost($post, $reactions)
{
return [
'id' => $post['id'],
'body' => $post['body'],
'createdAt' => $post['time'],
'author' => [
'id' => $post['author_id'],
'name' => $post['author_name'],
'avatar' => '/images/130_q_' . $post['author_photo'],
'sleep_status' => $post['author_sleep_status']
],
'reactions' => $reactions[$post['id']] ?? []
];
}
/**
* @param $forumId integer which forum to return threads for (maps to regions/groups)
* @param $subForumId integer each region/group as another namespace to separate different forums with the same base id (region/group id, here: forumId).
* So with any forumId, there is (currently) 2, possibly infinite, actual forums (list of threads)
* @Rest\Get("forum/{forumId}/{subForumId}", requirements={"forumId" = "\d+", "subForumId" = "\d"})
*/
public function listThreadsAction(int $forumId, int $subForumId)
{
if (!$this->forumPermissions->mayAccessForum($forumId, $subForumId)) {
throw new HttpException(403);
}
$threads = $this->forumGateway->listThreads($forumId, $subForumId, 0, 0, 1000);
$threads = array_map(function ($thread) { return $this->normalizeThread($thread); }, $threads);
$view = $this->view([
'data' => $threads
], 200);
return $this->handleView($view);
}
/**
* @param $threadId integer unique ID of thread to be requested
* @Rest\Get("forum/thread/{threadId}", requirements={"threadId" = "\d+"})
*/
public function getThreadAction($threadId)
{
if (!$this->forumPermissions->mayAccessThread($threadId)) {
throw new HttpException(403);
}
$thread = $this->forumGateway->getThread($threadId);
$posts = $this->forumGateway->listPosts($threadId);
$reactions = $this->forumService->getReactionsForThread($threadId);
$thread = $this->normalizeThread($thread);
$thread['posts'] = array_map(function ($post) use ($reactions) { return $this->normalizePost($post, $reactions); }, $posts);
$view = $this->view([
'data' => $thread
], 200);
return $this->handleView($view);
}
/**
* @Rest\Post("forum/post/{postId}/reaction/{emoji}", requirements={"postId" = "\d+", "emoji" = "\w+"})
*/
public function addReactionAction($postId, $emoji)
{
$threadId = $this->forumGateway->getThreadForPost($postId);
if (!$this->forumPermissions->mayAccessThread($threadId)) {
return new HttpException(403);
}
$this->forumService->addReaction($this->session->id(), $threadId, $postId, $emoji);
return $this->handleView($this->view());
}
/**
* @Rest\Delete("forum/post/{postId}/reaction/{emoji}", requirements={"postId" = "\d+", "emoji" = "\w+"})
*/
public function deleteReactionAction($postId, $emoji)
{
$threadId = $this->forumGateway->getThreadForPost($postId);
$this->forumService->removeReaction($this->session->id(), $threadId, $postId, $emoji);
return $this->handleView($this->view());
}
}
<?php
namespace Foodsharing\Modules\Reaction;
use Foodsharing\Modules\Core\BaseGateway;
use Foodsharing\Modules\Core\Database;
class ReactionGateway extends BaseGateway
{
public function __construct(Database $db)
{
parent::__construct($db);
}
/**
* returns all reactions for a given target.
* if isPrefix is true, target is evaluated as a prefix search, e.g. to get reactions on all sub objects of an object
* when target is composed like module-obj-subobj.
*/
public function getReactions($target, $isPrefix = false)
{
$q = '
SELECT
r.target,
r.emoji,
r.time,
r.foodsaver_id,
fs.name as foodsaver_name
FROM
fs_reaction r
LEFT JOIN
fs_foodsaver fs
ON
fs.id = r.foodsaver_id';
if ($isPrefix) {
$q .= '
WHERE r.target LIKE :target';
$target .= '%';
} else {
$q .= '
WHERE r.target = :target';
}
$res = $this->db->fetchAll($q, [
'target' => $target
]);
return $res;
}
public function addReaction($target, $fsId, $emoji): bool
{
$this->db->insert(
'fs_reaction',
[
'target' => $target,
'foodsaver_id' => $fsId,
'emoji' => $emoji,
'time' => $this->db->now()
]
);
return true;
}
public function removeReaction($target, $fsId, $emoji)
{
$this->db->delete(
'fs_reaction',
[
'target' => $target,
'foodsaver_id' => $fsId,
'emoji' => $emoji
]
);
}
}
......@@ -286,4 +286,29 @@ class ForumGateway extends BaseGateway
AND tp.id = :id
', ['id' => $post_id]);
}
public function getForumsForThread($threadId)
{
return $this->db->fetchAll('
SELECT
bt.bezirk_id AS forumId,
bt.bot_theme AS forumSubId
FROM
fs_bezirk_has_theme bt
WHERE bt.theme_id = :threadId
', ['threadId' => $threadId]);
}
public function getThreadForPost($postId)
{
return $this->db->fetchValue('
SELECT
theme_id AS threadId
FROM
fs_theme_post
WHERE
id = :postId
', ['postId' => $postId]);
}
}
......@@ -20,7 +20,7 @@ class ForumPermissions
$this->session = $session;
}
public function mayPostToRegion($regionId, $ambassadorForum)
public function mayPostToRegion($regionId, $ambassadorForum): bool
{
if ($this->session->isOrgaTeam()) {
return true;
......@@ -35,43 +35,51 @@ class ForumPermissions
return true;
}
public function mayPostToThread($threadId)
public function mayAccessForum($forumId, $forumSubId): bool
{
$threadStatus = $this->forumGateway->getBotThreadStatus($threadId);
if ($forumSubId !== 0 && $forumSubId !== 1) {
return false;
}
return $this->mayPostToRegion($threadStatus['bezirk_id'], $threadStatus['bot_theme']);
return $this->mayPostToRegion($forumId, $forumSubId);
}
public function mayAccessThread($threadId)
public function mayPostToThread($threadId): bool
{
return $this->mayPostToThread($threadId);
if ($this->session->isOrgaTeam()) {
return true;
}
$forums = $this->forumGateway->getForumsForThread($threadId);
foreach ($forums as $forum) {
if ($this->mayAccessForum($forum['forumId'], $forum['forumSubId'])) {
return true;
}
}
return false;
}
public function mayAccessAmbassadorBoard($regionId)
public function mayAccessThread($threadId): bool
{
return $this->mayPostToRegion($regionId, 1);
return $this->mayPostToThread($threadId);
}
public function mayAccessForum($forumId, $subForumId)
public function mayAccessAmbassadorBoard($regionId): bool
{
if ($subForumId !== 0 && $subForumId !== 1) {
return false;
}
return $this->mayPostToRegion($forumId, $subForumId);
return $this->mayPostToRegion($regionId, 1);
}
public function mayActivateThreads($regionId)
public function mayActivateThreads($regionId): bool
{
return $this->mayPostToRegion($regionId, 1);
}
public function mayChangeStickyness($regionId)
public function mayChangeStickyness($regionId): bool
{
return $this->mayPostToRegion($regionId, 1);
}
public function mayDeletePost($region, $post)
public function mayDeletePost($region, $post): bool
{
if ($this->session->isOrgaTeam()) {
return true;
......
......@@ -10,12 +10,14 @@ use Foodsharing\Modules\Bell\BellGateway;
use Foodsharing\Modules\Core\Model;
use Foodsharing\Modules\EmailTemplateAdmin\EmailTemplateGateway;
use Foodsharing\Modules\Foodsaver\FoodsaverGateway;
use Foodsharing\Modules\Reaction\ReactionGateway;
use Foodsharing\Modules\Region\ForumGateway;
use Foodsharing\Modules\Region\RegionGateway;
class ForumService
{
private $forumGateway;
private $reactionGateway;
private $regionGateway;
private $foodsaverGateway;
private $bellGateway;
......@@ -33,7 +35,8 @@ class ForumService
Func $func,
Session $session,
Model $model,
RegionGateway $regionGateway
RegionGateway $regionGateway,
ReactionGateway $reactionGateway
) {
$this->bellGateway = $bellGateway;
$this->emailTemplateGateway = $emailTemplateGateway;
......@@ -43,6 +46,7 @@ class ForumService
$this->session = $session;
$this->model = $model;
$this->regionGateway = $regionGateway;
$this->reactionGateway = $reactionGateway;
}
public function url($regionId, $ambassadorForum, $threadId = null, $postId = null)
......@@ -55,7 +59,7 @@ class ForumService
$url .= '&pid=' . $postId . '#post' . $postId;
}
return $url;
return $url;
}
public function notifyParticipantsViaBell($threadId, $authorId, $postId)
......@@ -275,4 +279,57 @@ class ForumService
$mail->send();
}
}
private function getReactionTarget($threadId, $postId = null)
{
$target = 'forum-' . $threadId . '-';
if ($postId) {
$target .= $postId;
}
return $target;
}
public function addReaction($fsId, $threadId, $postId, $emoji)
{
if (!$fsId || !$threadId || !$postId || !$emoji) {
throw new \InvalidArgumentException();
}
$this->reactionGateway->addReaction($this->getReactionTarget($threadId, $postId), $fsId, $emoji);
}
public function removeReaction($fsId, $threadId, $postId, $emoji)
{
if (!$fsId || !$threadId || !$postId || !$emoji) {
throw new \InvalidArgumentException();
}
$this->reactionGateway->removeReaction($this->getReactionTarget($threadId, $postId), $fsId, $emoji);
}
public function getReactionsForThread($threadId)
{
$target = $this->getReactionTarget($threadId);
$res = $this->reactionGateway->getReactions($target, true);
$reactions = [];
foreach ($res as $r) {
$id = explode($target, $r['target']);
if (count($id) !== 2) {
continue;
} else {
$postId = $id[1];
}
if (!isset($reactions[$postId])) {
$reactions[$postId] = [
'emoji' => $r['emoji'],
'users' => []
];
}
$reactions[$postId]['users'][] = [
'id' => $r['foodsaver_id'],
'name' => $r['foodsaver_name']
];
}
return $reactions;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment