Add Messenger + Change Message entity to allow multiple receivers

pull/3924/head
Julio Montoya 4 years ago
parent f10e671c0e
commit 5b6d393693
  1. 7
      .env
  2. 92
      assets/vue/components/message/Form.vue
  3. 2
      assets/vue/main.js
  4. 16
      assets/vue/mixins/CreateMixin.js
  5. 64
      assets/vue/views/message/Create.vue
  6. 24
      assets/vue/views/message/List.vue
  7. 4
      assets/vue/views/message/Show.vue
  8. 12
      assets/vue/views/usergroup/List.vue
  9. 4
      assets/vue/views/usergroup/Show.vue
  10. 2
      assets/vue/views/userreluser/List.vue
  11. 7
      composer.json
  12. 7
      config/packages/api_platform.yaml
  13. 15
      config/packages/messenger.yaml
  14. 4
      config/packages/test/messenger.yaml
  15. 4
      public/main/inc/lib/api.lib.php
  16. 23
      public/main/inc/lib/message.lib.php
  17. 74
      src/CoreBundle/DataPersister/MessageDataPersister.php
  18. 8
      src/CoreBundle/DataPersister/UserRelUserDataPersister.php
  19. 40
      src/CoreBundle/DataProvider/Extension/MessageExtension.php
  20. 67
      src/CoreBundle/DataProvider/Extension/UserRelUserExtension.php
  21. 11
      src/CoreBundle/Entity/Listener/MessageListener.php
  22. 74
      src/CoreBundle/Entity/Message.php
  23. 17
      src/CoreBundle/Entity/User.php
  24. 55
      src/CoreBundle/MessageHandler/MessageHandler.php
  25. 70
      src/CoreBundle/Migrations/Schema/V200/Version20200821224242.php
  26. 9
      src/CoreBundle/Resources/config/services.yml
  27. 4
      src/CoreBundle/Resources/views/Mailer/Default/default.html.twig
  28. 10
      src/CoreBundle/Security/Authorization/Voter/MessageVoter.php
  29. 11
      src/CoreBundle/Serializer/UserToJsonNormalizer.php
  30. 36
      tests/CoreBundle/Repository/MessageRepositoryTest.php

@ -45,3 +45,10 @@ JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=your_secret_passphrase
###< lexik/jwt-authentication-bundle ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=doctrine://default
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
###< symfony/messenger ###

@ -0,0 +1,92 @@
<template>
<q-form>
<q-input
id="item_title"
v-model="item.title"
:placeholder="$t('Title')"
:error="v$.item.title.$error"
@input="v$.item.title.$touch()"
@blur="v$.item.title.$touch()"
:error-message="titleErrors"
/>
<slot></slot>
</q-form>
</template>
<script>
import has from 'lodash/has';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
export default {
name: 'MessageForm',
setup () {
return { v$: useVuelidate() }
},
props: {
values: {
type: Object,
required: true
},
errors: {
type: Object,
default: () => {}
},
initialValues: {
type: Object,
default: () => {}
},
},
data() {
return {
title: null,
parentResourceNodeId: null,
receivers: []
};
},
computed: {
item() {
return this.initialValues || this.values;
},
receiversErrors() {
const errors = [];
if (!this.v$.item.receivers.$dirty) return errors;
has(this.violations, 'receivers') && errors.push(this.violations.receivers);
if (this.v$.item.receivers.required) {
return this.$t('Field is required')
}
return errors;
},
titleErrors() {
const errors = [];
if (!this.v$.item.title.$dirty) return errors;
has(this.violations, 'title') && errors.push(this.violations.title);
if (this.v$.item.title.required) {
return this.$t('Field is required')
}
return errors;
},
violations() {
return this.errors || {};
}
},
validations: {
item: {
title: {
required,
},
receivers: {
required,
},
content: {
required,
},
}
}
};
</script>

@ -108,8 +108,6 @@ store.registerModule(
})
);
// Vuetify.
import '@mdi/font/css/materialdesignicons.css';
import 'vuetify/lib/styles/main.sass';

@ -1,6 +1,6 @@
import NotificationMixin from './NotificationMixin';
import { formatDateTime } from '../utils/dates';
import isEmpty from 'lodash/isEmpty';
export default {
mixins: [NotificationMixin],
methods: {
@ -37,15 +37,19 @@ export default {
onSendMessageForm() {
const createForm = this.$refs.createForm;
createForm.v$.$touch();
if (!createForm.v$.$invalid) {
let users = [];
createForm.v$.item.$model.receivers.forEach(user => {
// Send to inbox
createForm.v$.item.$model.userSender = '/api/users/' + this.currentUser.id;
createForm.v$.item.$model.userReceiver = user['@id'];
createForm.v$.item.$model.msgType = 1;
this.create(createForm.v$.item.$model);
users.push(user['@id']);
});
createForm.v$.item.$model.sender = '/api/users/' + this.currentUser.id;
createForm.v$.item.$model.receivers = users;
createForm.v$.item.$model.msgType = 1;
this.create(createForm.v$.item.$model);
}
},
resetForm() {

@ -3,27 +3,47 @@
<Toolbar
:handle-send="onSendMessageForm"
/>
<DocumentsForm
<MessageForm
ref="createForm"
:values="item"
:errors="violations"
>
<VueMultiselect
placeholder="To"
v-model="item.receivers"
:loading="isLoadingSelect"
:options="users"
:multiple="true"
:searchable="true"
:internal-search="false"
@search-change="asyncFind"
limit-text="3"
limit="3"
label="username"
track-by="id"
/>
<VueMultiselect
placeholder="To"
v-model="item.receivers"
:loading="isLoadingSelect"
:options="users"
:multiple="true"
:searchable="true"
:internal-search="false"
@search-change="asyncFind"
limit-text="3"
limit="3"
label="username"
track-by="id"
:allow-empty="false"
@input="v$.item.receivers.$touch()"
/>
<!-- @filter-abort="abortFilterFn"-->
<!-- <q-select-->
<!-- filled-->
<!-- v-model="item.receivers"-->
<!-- use-input-->
<!-- use-chips-->
<!-- :options="users"-->
<!-- input-debounce="0"-->
<!-- label="Lazy filter"-->
<!-- @filter="asyncFind"-->
<!-- style="width: 250px"-->
<!-- hint="With use-chips"-->
<!-- :error-message="receiversErrors"-->
<!-- />-->
<TinyEditor
<TinyEditor
v-model="item.content"
required
:init="{
@ -44,19 +64,20 @@
}
"
/>
</DocumentsForm>
</MessageForm>
<Loading :visible="isLoading" />
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<script>
import {mapActions, mapGetters, useStore} from 'vuex';
import { createHelpers } from 'vuex-map-fields';
import DocumentsForm from '../../components/documents/Form.vue';
import MessageForm from '../../components/message/Form.vue';
import Loading from '../../components/Loading.vue';
import Toolbar from '../../components/Toolbar.vue';
import CreateMixin from '../../mixins/CreateMixin';
import {ref} from "vue";
import isEmpty from "lodash/isEmpty";
import axios from "axios";
import {ENTRYPOINT} from "../../config/entrypoint";
import useVuelidate from "@vuelidate/core";
@ -75,12 +96,14 @@ export default {
components: {
Loading,
Toolbar,
DocumentsForm,
MessageForm,
VueMultiselect
},
setup () {
const users = ref([]);
const isLoadingSelect = ref(false);
const isTouched = ref(false);
function asyncFind (query) {
if (query.toString().length < 3) {
@ -102,6 +125,7 @@ export default {
});
}
return {v$: useVuelidate(), users, asyncFind, isLoadingSelect};
},
data() {

@ -127,10 +127,10 @@
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
<Column field="userSender" :header="$t('From')" :sortable="false">
<Column field="sender" :header="$t('From')" :sortable="false">
<template #body="slotProps">
<q-avatar size="40px">
<img :src="slotProps.data.userSender.illustrationUrl + '?w=80&h=80&fit=crop'" />
<img :src="slotProps.data.sender.illustrationUrl + '?w=80&h=80&fit=crop'" />
</q-avatar>
<a
@ -139,7 +139,7 @@
class="cursor-pointer"
:class="[ true === slotProps.data.read ? 'font-normal': 'font-semibold']"
>
{{ slotProps.data.userSender.username }}
{{ slotProps.data.sender.username }}
</a>
</template>
</Column>
@ -179,7 +179,7 @@
<!-- </template>-->
</Column>
<Column field="sendDate" :header="$t('Send date')" :sortable="true">
<Column field="sendDate" :header="$t('Send date')" :sortable="false">
<template #body="slotProps">
{{$luxonDateTime.fromISO(slotProps.data.sendDate).toRelative() }}
</template>
@ -289,13 +289,13 @@ export default {
filtersSent.value = {
msgType: 2,
userSender: user.id
sender: user.id
}
// inbox
filters.value = {
msgType: 1,
userReceiver: user.id
receivers: [user.id]
};
// Get user tags.
@ -312,7 +312,7 @@ export default {
title.value = 'Inbox';
filters.value = {
msgType: 1,
userReceiver: user.id,
receivers: [user.id],
};
store.dispatch('message/resetList');
store.dispatch('message/fetchAll', filters.value);
@ -322,7 +322,7 @@ export default {
title.value = 'Unread';
filters.value = {
msgType: 1,
userReceiver: user.id,
receivers: [user.id],
read: false
};
store.dispatch('message/resetList');
@ -333,7 +333,7 @@ export default {
title.value = 'Sent';
filters.value = {
msgType: 2,
userSender: user.id
sender: user.id
};
store.dispatch('message/resetList');
store.dispatch('message/fetchAll', filters.value);
@ -343,7 +343,7 @@ export default {
title.value = tag.tag;
filters.value = {
msgType: 1,
userReceiver: user.id,
receivers: [user.id],
tags: [tag]
};
store.dispatch('message/resetList');
@ -364,7 +364,7 @@ export default {
return {
columns: [
{ label: this.$i18n.t('Title'), field: 'title', name: 'title', sortable: true},
{ label: this.$i18n.t('Sender'), field: 'userSender', name: 'userSender', sortable: true},
{ label: this.$i18n.t('Sender'), field: 'sender', name: 'sender', sortable: true},
{ label: this.$i18n.t('Modified'), field: 'sendDate', name: 'updatedAt', sortable: true},
{ label: this.$i18n.t('Actions'), name: 'action', sortable: false}
],
@ -493,7 +493,7 @@ export default {
this.resetList = true;
},
reloadHandler() {
this.onUpdateOptions(this.options);
this.onUpdateOptions();
},
markAsUnReadMultiple(){
console.log('markAsUnReadMultiple');

@ -44,10 +44,10 @@
<p class="text-lg">
From:
<q-avatar size="32px">
<img :src="item['userSender']['illustrationUrl'] + '?w=80&h=80&fit=crop'" />
<img :src="item['sender']['illustrationUrl'] + '?w=80&h=80&fit=crop'" />
<!-- <q-icon name="person" ></q-icon>-->
</q-avatar>
{{ item['userSender']['username'] }}
{{ item['sender']['username'] }}
</p>
<p class="text-lg">

@ -127,10 +127,10 @@
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
<Column field="userSender" :header="$t('From')" :sortable="false">
<Column field="sender" :header="$t('From')" :sortable="false">
<template #body="slotProps">
<q-avatar size="40px">
<img :src="slotProps.data.userSender.illustrationUrl + '?w=80&h=80&fit=crop'" />
<img :src="slotProps.data.sender.illustrationUrl + '?w=80&h=80&fit=crop'" />
</q-avatar>
<a
@ -139,7 +139,7 @@
class="cursor-pointer"
:class="[ true === slotProps.data.read ? 'font-normal': 'font-semibold']"
>
{{ slotProps.data.userSender.username }}
{{ slotProps.data.sender.username }}
</a>
</template>
</Column>
@ -289,7 +289,7 @@ export default {
filtersSent.value = {
msgType: 2,
userSender: user.id
sender: user.id
}
// inbox
@ -333,7 +333,7 @@ export default {
title.value = 'Sent';
filters.value = {
msgType: 2,
userSender: user.id
sender: user.id
};
store.dispatch('message/resetList');
store.dispatch('message/fetchAll', filters.value);
@ -364,7 +364,7 @@ export default {
return {
columns: [
{ label: this.$i18n.t('Title'), field: 'title', name: 'title', sortable: true},
{ label: this.$i18n.t('Sender'), field: 'userSender', name: 'userSender', sortable: true},
{ label: this.$i18n.t('Sender'), field: 'sender', name: 'userSender', sortable: true},
{ label: this.$i18n.t('Modified'), field: 'sendDate', name: 'updatedAt', sortable: true},
{ label: this.$i18n.t('Actions'), name: 'action', sortable: false}
],

@ -44,10 +44,10 @@
<p class="text-lg">
From:
<q-avatar size="32px">
<img :src="item['userSender']['illustrationUrl'] + '?w=80&h=80&fit=crop'" />
<img :src="item['sender']['illustrationUrl'] + '?w=80&h=80&fit=crop'" />
<!-- <q-icon name="person" ></q-icon>-->
</q-avatar>
{{ item['userSender']['username'] }}
{{ item['sender']['username'] }}
</p>
<p class="text-lg">

@ -92,7 +92,7 @@
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
<Column field="userSender" :header="$t('User')" :sortable="false">
<Column field="sender" :header="$t('User')" :sortable="false">
<template #body="slotProps">
<q-avatar size="40px">
<img :src="slotProps.data.friend.illustrationUrl + '?w=80&h=80&fit=crop'" />

@ -118,6 +118,7 @@
"symfony/http-client": "^5.0",
"symfony/intl": "^5.0",
"symfony/mailer": "^5.0",
"symfony/messenger": "^5.0",
"symfony/mime": "^5.0",
"symfony/monolog-bundle": "^3.1",
"symfony/notifier": "^5.0",
@ -135,7 +136,7 @@
"symfony/string": "^5.0",
"symfony/templating": "^5.0",
"symfony/translation": "^5.0",
"symfony/twig-pack": "^1.0",
"symfony/twig-bundle": "^5.0",
"symfony/uid": "^5.0",
"symfony/validator": "^5.0",
"symfony/web-link": "^5.0",
@ -144,7 +145,11 @@
"symfonycasts/reset-password-bundle": "^1.8",
"szymach/c-pchart": "^3.0",
"tgalopin/html-sanitizer-bundle": "^1.3",
"twig/cssinliner-extra": "^3.3",
"twig/extra-bundle": "^2.12|^3.0",
"twig/inky-extra": "^3.3",
"twig/intl-extra": "^3.0",
"twig/twig": "^2.12|^3.0",
"vich/uploader-bundle": "^1.18",
"webit/eval-math": "^1.0",
"webonyx/graphql-php": "^14.8"

@ -31,6 +31,7 @@ api_platform:
enable_docs: true
enable_entrypoint: true
show_webby: false
messenger: true
defaults:
pagination_client_items_per_page: true
cache_headers:
@ -49,3 +50,9 @@ api_platform:
# mercure:
# hub_url: '%env(MERCURE_SUBSCRIBE_URL)%'
#doctrine:
# orm:
# filters:
# user_filter:
# class: Chamilo\CoreBundle\Filter\UserFilter

@ -0,0 +1,15 @@
framework:
messenger:
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
# failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
# async: '%env(MESSENGER_TRANSPORT_DSN)%'
sync_priority_high: 'sync://'
# failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
routing:
# Route your messages to the transports
'Chamilo\CoreBundle\Entity\Message': sync_priority_high

@ -0,0 +1,4 @@
framework:
messenger:
transports:
sync_priority_high: 'in-memory://'

@ -7072,6 +7072,10 @@ function api_mail_html(
}
try {
$bus = Container::getMessengerBus();
//$sendMessage = new \Chamilo\CoreBundle\Message\SendMessage();
//$bus->dispatch($sendMessage);
$message = new TemplatedEmail();
$message->subject($subject);

@ -101,13 +101,12 @@ class MessageManager
false,
false
);
$senderId = $message->getUserSender()->getId();
$senderInfo = api_get_user_info($senderId);
$sender = $message->getSender();
$html .= Display::panelCollapse(
$localTime.' '.$senderInfo['complete_name'].' '.$message->getTitle(),
$localTime.' '.UserManager::formatUserFullName($sender).' '.$message->getTitle(),
$message->getContent().'<br />'.$date.'<br />'.get_lang(
'Author'
).': '.$senderInfo['complete_name_with_message_link'],
).': '.$sender->getUsername(),
$tag,
null,
$tagAccordion,
@ -340,10 +339,9 @@ class MessageManager
}
$messageId = $editMessageId;
} else {
$message = new Message();
$message
->setUserSender($userSender)
->setUserReceiver($userRecipient)
$message = (new Message())
->setSender($userSender)
->addReceiver($userRecipient)
->setMsgType($status)
->setTitle($subject)
->setContent($content)
@ -574,7 +572,7 @@ class MessageManager
->setPath($fileName)
->setFilename($fileName)
->setComment($comment)
->setParent($message->getUserSender())
->setParent($message->getSender())
->setMessage($message)
;
@ -606,8 +604,11 @@ class MessageManager
if (null !== $fileToUpload) {
$em->persist($attachment);
$attachmentRepo->addFile($attachment, $fileToUpload);
$attachment->addUserLink($message->getUserSender());
$attachment->addUserLink($message->getUserReceiver());
$attachment->addUserLink($message->getSender());
$receivers = $message->getReceivers();
foreach ($receivers as $receiver) {
$attachment->addUserLink($receiver);
}
$em->flush();
return true;

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use ApiPlatform\Core\DataPersister\ResumableDataPersisterInterface;
use Chamilo\CoreBundle\Entity\Message;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Messenger\MessageBusInterface;
class MessageDataPersister implements ContextAwareDataPersisterInterface, ResumableDataPersisterInterface
{
private EntityManager $entityManager;
private ContextAwareDataPersisterInterface $decorated;
private MessageBusInterface $bus;
public function __construct(ContextAwareDataPersisterInterface $decorated, EntityManager $entityManager, MessageBusInterface $bus)
{
$this->decorated = $decorated;
$this->entityManager = $entityManager;
$this->bus = $bus;
}
public function supports($data, array $context = []): bool
{
return $this->decorated->supports($data, $context);
}
public function persist($data, array $context = [])
{
$result = $this->decorated->persist($data, $context);
if ($data instanceof Message && (
($context['collection_operation_name'] ?? null) === 'post' ||
($context['graphql_operation_name'] ?? null) === 'create'
//($context['item_operation_name'] ?? null) === 'put' // on update
)
) {
/*if (Message::MESSAGE_TYPE_INBOX === $result->getMsgType()) {
$messageSent = clone $result;
$messageSent
->setMsgType(Message::MESSAGE_TYPE_OUTBOX)
//->setRead(true)
;
$this->entityManager->persist($messageSent);
$this->entityManager->flush();
echo 'send11';
// Send message.
$this->bus->dispatch($data);
}*/
}
/*$this->entityManager->persist($data);
$this->entityManager->flush();*/
return $result;
}
public function remove($data, array $context = []): void
{
$this->entityManager->remove($data);
$this->entityManager->flush();
}
public function resumable(array $context = []): bool
{
return true;
}
}

@ -10,7 +10,7 @@ use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use Chamilo\CoreBundle\Entity\UserRelUser;
use Doctrine\ORM\EntityManager;
class UserRelUserDataPersister
class UserRelUserDataPersister implements ContextAwareDataPersisterInterface
{
private EntityManager $entityManager;
private ContextAwareDataPersisterInterface $decorated;
@ -29,13 +29,11 @@ class UserRelUserDataPersister
public function persist($data, array $context = [])
{
$result = $this->decorated->persist($data, $context);
if (
$data instanceof UserRelUser && (
if ($data instanceof UserRelUser && (
//($context['collection_operation_name'] ?? null) === 'post' ||
//($context['graphql_operation_name'] ?? null) === 'create'
($context['item_operation_name'] ?? null) === 'put' // on update
)
)
) {
if (UserRelUser::USER_RELATION_TYPE_FRIEND === $data->getRelationType()) {
//error_log((string)$data->getRelationType());

@ -10,6 +10,7 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInter
//use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Chamilo\CoreBundle\Entity\Message;
use Chamilo\CoreBundle\Entity\User;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
@ -48,7 +49,7 @@ final class MessageExtension implements QueryCollectionExtensionInterface //, Qu
//$this->addWhere($queryBuilder, $resourceClass);
}
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
private function addWhere(QueryBuilder $qb, string $resourceClass): void
{
if (Message::class !== $resourceClass) {
return;
@ -58,19 +59,36 @@ final class MessageExtension implements QueryCollectionExtensionInterface //, Qu
return;
}*/
/** @var User $user */
$user = $this->security->getUser();
$alias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere("
($alias.userSender = :current AND $alias.msgType = :outbox) OR
($alias.userReceiver = :current AND $alias.msgType = :inbox) OR
($alias.userReceiver = :current AND $alias.msgType = :invitation) OR
($alias.userReceiver = :current AND $alias.msgType = :promoted) OR
($alias.userReceiver = :current AND $alias.msgType = :wallPost) OR
($alias.userReceiver = :current AND $alias.msgType = :conversation)
$alias = $qb->getRootAliases()[0];
$qb->innerJoin("$alias.receivers", 'r');
/*$qb->andWhere(
$qb->expr()->orX(
$qb->andWhere(
$qb->expr()->eq("$alias.sender", $user->getId()),
$qb->expr()->eq("$alias.msgType", Message::MESSAGE_TYPE_OUTBOX)
),
$qb->andWhere(
$qb->expr()->in("r", $user->getId()),
$qb->expr()->eq("$alias.msgType", Message::MESSAGE_TYPE_INBOX)
)
),
);*/
$qb->andWhere("
($alias.sender = :current AND $alias.msgType = :outbox) OR
(r IN (:currentList) AND $alias.msgType = :inbox) OR
(r IN (:currentList) AND $alias.msgType = :invitation) OR
(r IN (:currentList) AND $alias.msgType = :promoted) OR
(r IN (:currentList) AND $alias.msgType = :wallPost) OR
(r IN (:currentList) AND $alias.msgType = :conversation)
");
$queryBuilder->setParameters([
$qb->setParameters([
'current' => $user,
'currentList' => [$user->getId()],
'inbox' => Message::MESSAGE_TYPE_INBOX,
'outbox' => Message::MESSAGE_TYPE_OUTBOX,
'invitation' => Message::MESSAGE_TYPE_INVITATION,

@ -0,0 +1,67 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\DataProvider\Extension;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
//use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Chamilo\CoreBundle\Entity\Message;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
final class UserRelUserExtension implements QueryCollectionExtensionInterface //, QueryItemExtensionInterface
{
private Security $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
{
//error_log('applyToItem');
//$this->addWhere($queryBuilder, $resourceClass);
}
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
if (Message::class !== $resourceClass) {
return;
}
/*if ($this->security->isGranted('ROLE_ADMIN')) {
return;
}*/
/*$user = $this->security->getUser();
$alias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere("
($alias.userSender = :current AND $alias.msgType = :outbox) OR
($alias.userReceiver = :current AND $alias.msgType = :inbox) OR
($alias.userReceiver = :current AND $alias.msgType = :invitation) OR
($alias.userReceiver = :current AND $alias.msgType = :promoted) OR
($alias.userReceiver = :current AND $alias.msgType = :wallPost) OR
($alias.userReceiver = :current AND $alias.msgType = :conversation)
");
$queryBuilder->setParameters([
'current' => $user,
'inbox' => Message::MESSAGE_TYPE_INBOX,
'outbox' => Message::MESSAGE_TYPE_OUTBOX,
'invitation' => Message::MESSAGE_TYPE_INVITATION,
'promoted' => Message::MESSAGE_TYPE_PROMOTED,
'wallPost' => Message::MESSAGE_TYPE_WALL,
'conversation' => Message::MESSAGE_STATUS_CONVERSATION,
]);*/
}
}

@ -9,9 +9,17 @@ namespace Chamilo\CoreBundle\Entity\Listener;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Message;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Messenger\MessageBusInterface;
class MessageListener
{
private MessageBusInterface $bus;
public function __construct(MessageBusInterface $bus)
{
$this->bus = $bus;
}
/**
* This code is executed when a new course is created.
*
@ -34,6 +42,9 @@ class MessageListener
;
$args->getEntityManager()->persist($messageSent);
$args->getEntityManager()->flush();
// Dispatch to the Messenger bus. Function MessageHandler.php will send the message.
$this->bus->dispatch($message);
}
}
}

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
@ -24,10 +25,6 @@ use Symfony\Component\Validator\Constraints as Assert;
*
* @ORM\Table(name="message", indexes={
* @ORM\Index(name="idx_message_user_sender", columns={"user_sender_id"}),
* @ORM\Index(name="idx_message_user_receiver", columns={"user_receiver_id"}),
* @ORM\Index(name="idx_message_user_sender_user_receiver", columns={"user_sender_id", "user_receiver_id"}),
* @ORM\Index(name="idx_message_user_receiver_type", columns={"user_receiver_id", "msg_type"}),
* @ORM\Index(name="idx_message_receiver_type_send_date", columns={"user_receiver_id", "msg_type", "send_date"}),
* @ORM\Index(name="idx_message_group", columns={"group_id"}),
* @ORM\Index(name="idx_message_type", columns={"msg_type"})
* })
@ -40,6 +37,10 @@ use Symfony\Component\Validator\Constraints as Assert;
'security' => "is_granted('ROLE_USER')", // the get collection is also filtered by MessageExtension.php
],
'post' => [
//'security' => "is_granted('ROLE_USER')",
/*'messenger' => true,
'output' => false,
'status' => 202,*/
'security_post_denormalize' => "is_granted('CREATE', object)",
// 'deserialize' => false,
// 'controller' => Create::class,
@ -88,10 +89,11 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiFilter(OrderFilter::class, properties: ['title', 'sendDate'])]
#[ApiFilter(SearchFilter::class, properties: [
'read' => 'exact',
'status' => 'exact',
'msgType' => 'exact',
'userSender' => 'exact',
'userReceiver' => 'exact',
'sender' => 'exact',
'tags' => 'exact',
'receivers' => 'exact',
])]
class Message
{
@ -118,6 +120,7 @@ class Message
* @ORM\Id
* @ORM\GeneratedValue()
*/
#[ApiProperty(identifier: true)]
#[Groups(['message:read'])]
protected int $id;
@ -127,14 +130,20 @@ class Message
*/
#[Assert\NotBlank]
#[Groups(['message:read', 'message:write'])]
protected User $userSender;
protected User $sender;
/**
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\User", inversedBy="receivedMessages")
* @ORM\JoinColumn(name="user_receiver_id", referencedColumnName="id", nullable=true)
* @var Collection<int, User>|User[]
*
* @ORM\ManyToMany(
* targetEntity="Chamilo\CoreBundle\Entity\User",
* inversedBy="receivedMessages",
* cascade={"persist"}
* )
* @ORM\JoinTable(name="message_rel_user")
*/
#[Groups(['message:read', 'message:write'])]
protected ?User $userReceiver = null;
protected array | null | Collection $receivers;
/**
* @ORM\Column(name="msg_type", type="smallint", nullable=false)
@ -257,12 +266,37 @@ class Message
$this->children = new ArrayCollection();
$this->tags = new ArrayCollection();
$this->likes = new ArrayCollection();
$this->receivers = new ArrayCollection();
$this->votes = 0;
$this->status = 0;
$this->read = false;
$this->starred = false;
}
/**
* @return null|Collection|User[]
*/
public function getReceivers()
{
return $this->receivers;
}
public function addReceiver(User $receiver): self
{
if (!$this->receivers->contains($receiver)) {
$this->receivers->add($receiver);
}
return $this;
}
public function setReceivers($receivers): self
{
$this->receivers = $receivers;
return $this;
}
/**
* @return Collection|MessageTag[]
*/
@ -289,28 +323,16 @@ class Message
return $this;
}
public function setUserSender(User $userSender): self
{
$this->userSender = $userSender;
return $this;
}
public function getUserSender(): User
{
return $this->userSender;
}
public function setUserReceiver(?User $userReceiver): self
public function setSender(User $sender): self
{
$this->userReceiver = $userReceiver;
$this->sender = $sender;
return $this;
}
public function getUserReceiver(): ?User
public function getSender(): User
{
return $this->userReceiver;
return $this->sender;
}
public function setMsgType(int $msgType): self

@ -157,10 +157,10 @@ class User implements UserInterface, EquatableInterface, ResourceInterface, Reso
protected ?string $apiToken = null;
/**
* @Assert\NotBlank()
* @ApiProperty(iri="http://schema.org/name")
* @ORM\Column(name="firstname", type="string", length=64, nullable=true)
*/
#[Assert\NotBlank]
#[Groups([
'user:read',
'user:write',
@ -226,11 +226,11 @@ class User implements UserInterface, EquatableInterface, ResourceInterface, Reso
/**
* @Groups({"user:read", "user:write", "user_json:read"})
* @Assert\NotBlank()
* @Assert\Email()
*
* @ORM\Column(name="email", type="string", length=100)
*/
#[Assert\NotBlank]
#[Assert\Email]
protected string $email;
/**
@ -239,10 +239,10 @@ class User implements UserInterface, EquatableInterface, ResourceInterface, Reso
protected bool $locked;
/**
* @Assert\NotBlank()
* @Groups({"user:read", "user:write"})
* @ORM\Column(name="enabled", type="boolean")
*/
#[Assert\NotBlank]
protected bool $enabled;
/**
@ -739,7 +739,7 @@ class User implements UserInterface, EquatableInterface, ResourceInterface, Reso
*
* @ORM\OneToMany(
* targetEntity="Message",
* mappedBy="userSender",
* mappedBy="sender",
* cascade={"persist", "remove"},
* orphanRemoval=true
* )
@ -749,11 +749,10 @@ class User implements UserInterface, EquatableInterface, ResourceInterface, Reso
/**
* @var Collection<int, Message>|Message[]
*
* @ORM\OneToMany(
* @ORM\ManyToMany(
* targetEntity="Chamilo\CoreBundle\Entity\Message",
* mappedBy="userReceiver",
* cascade={"persist", "remove"},
* orphanRemoval=true
* mappedBy="receivers",
* cascade={"persist", "remove"}
* )
*/
protected Collection $receivedMessages;

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\MessageHandler;
use Chamilo\CoreBundle\Entity\Message;
use Chamilo\CoreBundle\Repository\MessageRepository;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Mime\Address;
class MessageHandler implements MessageHandlerInterface
{
private Mailer $mailer;
private MessageRepository $repo;
public function __construct(Mailer $mailer, MessageRepository $repo)
{
$this->mailer = $mailer;
$this->repo = $repo;
}
public function __invoke(Message $message): void
{
if (Message::MESSAGE_TYPE_INBOX !== $message->getMsgType()) {
// Only send messages to the inbox.
return;
}
$email = (new TemplatedEmail())
->subject($message->getTitle())
->from(new Address($message->getSender()->getEmail(), $message->getSender()->getFirstname()))
->htmlTemplate('@ChamiloCore/Mailer/Default/default.html.twig')
->textTemplate('@ChamiloCore/Mailer/Default/default.text.twig')
;
foreach ($message->getReceivers() as $receiver) {
$address = new Address($receiver->getEmail(), $receiver->getFirstname());
$email->addBcc($address);
}
$params = [
'content' => $message->getContent(),
'automatic_email_text' => '',
'mail_header_style' => '',
'mail_content_style' => '',
'theme' => '',
];
$email->context($params);
$this->mailer->send($email);
}
}

@ -43,13 +43,18 @@ final class Version20200821224242 extends AbstractMigrationChamilo
}
$this->addSql('UPDATE message SET parent_id = NULL WHERE parent_id = 0');
$this->addSql('DELETE FROM message WHERE parent_id IS NOT NULL AND parent_id NOT IN (SELECT id FROM message)');
if (!$table->hasColumn('group_id')) {
$this->addSql('ALTER TABLE message CHANGE group_id group_id INT DEFAULT NULL');
}
$this->addSql('DELETE FROM message WHERE parent_id IS NOT NULL AND parent_id in (select id FROM message WHERE user_sender_id NOT IN (SELECT id FROM user))');
$this->addSql('DELETE FROM message WHERE parent_id IS NOT NULL AND parent_id in (select id FROM message WHERE user_receiver_id NOT IN (SELECT id FROM user))');
$this->addSql('DELETE FROM message WHERE user_sender_id NOT IN (SELECT id FROM user)');
$this->addSql('DELETE FROM message WHERE user_receiver_id IS NOT NULL AND user_receiver_id NOT IN (SELECT id FROM user)');
if (!$table->hasForeignKey('FK_B6BD307FFE54D947')) {
$this->addSql('ALTER TABLE message ADD CONSTRAINT FK_B6BD307FFE54D947 FOREIGN KEY (group_id) REFERENCES c_group_info (iid) ON DELETE CASCADE');
}
@ -61,24 +66,42 @@ final class Version20200821224242 extends AbstractMigrationChamilo
$this->addSql('CREATE INDEX IDX_B6BD307F727ACA70 ON message (parent_id)');
}
$this->addSql('DELETE FROM message WHERE user_sender_id IS NULL OR user_sender_id = 0');
$this->addSql('ALTER TABLE message CHANGE user_receiver_id user_receiver_id INT DEFAULT NULL');
$this->addSql('UPDATE message SET user_receiver_id = NULL WHERE user_receiver_id = 0');
$this->addSql('DELETE FROM message WHERE parent_id IS NOT NULL AND parent_id in (select id FROM message WHERE user_sender_id NOT IN (SELECT id FROM user))');
$this->addSql('DELETE FROM message WHERE parent_id IS NOT NULL AND parent_id in (select id FROM message WHERE user_receiver_id NOT IN (SELECT id FROM user))');
$this->addSql('DELETE FROM message WHERE user_sender_id NOT IN (SELECT id FROM user)');
$this->addSql('DELETE FROM message WHERE user_receiver_id IS NOT NULL AND user_receiver_id NOT IN (SELECT id FROM user)');
if ($table->hasForeignKey('FK_B6BD307F64482423')) {
$this->addSql('ALTER TABLE message DROP FOREIGN KEY FK_B6BD307F64482423');
}
if (false === $table->hasForeignKey('FK_B6BD307FF6C43E79')) {
if ($schema->hasTable('message_rel_user')) {
$this->addSql(
'ALTER TABLE message ADD CONSTRAINT FK_B6BD307FF6C43E79 FOREIGN KEY (user_sender_id) REFERENCES user (id)'
'CREATE TABLE message_rel_user (message_id BIGINT NOT NULL, user_id INT NOT NULL, INDEX IDX_24064D90537A1329 (message_id), INDEX IDX_24064D90A76ED395 (user_id), PRIMARY KEY(message_id, user_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC;'
);
$this->addSql(
'ALTER TABLE message_rel_user ADD CONSTRAINT FK_24064D90537A1329 FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE;'
);
$this->addSql(
'ALTER TABLE message_rel_user ADD CONSTRAINT FK_24064D90A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE;'
);
}
if (!$table->hasForeignKey('FK_B6BD307F64482423')) {
//$this->addSql('ALTER TABLE message CHANGE user_receiver_id user_receiver_id INT DEFAULT NULL');
$this->addSql('UPDATE message SET user_receiver_id = NULL WHERE user_receiver_id = 0');
$connection = $this->getEntityManager()->getConnection();
$result = $connection->executeQuery('SELECT * FROM message WHERE user_receiver_id IS NOT NULL');
$messages = $result->fetchAllAssociative();
if ($messages) {
foreach ($messages as $message) {
$messageId = $message['id'];
$receiverId = $message['user_receiver_id'];
$this->addSql("INSERT INTO message_rel_user (message_id, user_id) VALUES('$messageId', '$receiverId') ");
$this->addSql("UPDATE message SET user_receiver_id = NULL WHERE id = $messageId");
}
}
if (false === $table->hasForeignKey('FK_B6BD307FF6C43E79')) {
$this->addSql(
'ALTER TABLE message ADD CONSTRAINT FK_B6BD307F64482423 FOREIGN KEY (user_receiver_id) REFERENCES user (id)'
'ALTER TABLE message ADD CONSTRAINT FK_B6BD307FF6C43E79 FOREIGN KEY (user_sender_id) REFERENCES user (id)'
);
}
@ -94,18 +117,25 @@ final class Version20200821224242 extends AbstractMigrationChamilo
$this->addSql('DROP INDEX idx_message_status ON message');
}
if (!$table->hasIndex('idx_message_user_receiver_type')) {
$this->addSql('CREATE INDEX idx_message_user_receiver_type ON message (user_receiver_id, msg_type)');
if ($table->hasIndex('idx_message_user_receiver_type')) {
$this->addSql('DROP INDEX idx_message_user_receiver_type ON message');
}
if (!$table->hasIndex('idx_message_type')) {
$this->addSql('CREATE INDEX idx_message_type ON message (msg_type)');
if ($table->hasIndex('idx_message_receiver_type_send_date')) {
$this->addSql('DROP INDEX idx_message_receiver_type_send_date ON message');
}
if (!$table->hasIndex('idx_message_receiver_type_send_date')) {
$this->addSql(
'CREATE INDEX idx_message_receiver_type_send_date ON message (user_receiver_id, msg_type, send_date)'
);
if ($table->hasIndex('idx_message_user_receiver')) {
$this->addSql('DROP INDEX idx_message_user_receiver ON message');
}
if ($table->hasIndex('idx_message_user_sender_user_receiver')) {
$this->addSql('DROP INDEX idx_message_user_sender_user_receiver ON message');
}
//ALTER TABLE message DROP user_receiver_id;
if (!$table->hasIndex('idx_message_type')) {
$this->addSql('CREATE INDEX idx_message_type ON message (msg_type)');
}
//$this->addSql('ALTER TABLE message CHANGE msg_status msg_status SMALLINT NOT NULL;');

@ -19,12 +19,11 @@ services:
resource: '../../Controller'
tags: ['controller.service_arguments']
Chamilo\CoreBundle\DataPersister\UserRelUserDataPersister:
decorates: 'api_platform.doctrine.orm.data_persister'
Chamilo\CoreBundle\DataPersister\MessageDataPersister:
arguments: ['@api_platform.doctrine.orm.data_persister']
# Uncomment only if autoconfiguration is disabled
#arguments: ['@App\DataPersister\UserDataPersister.inner']
#tags: [ 'api_platform.data_persister' ]
Chamilo\CoreBundle\DataPersister\UserRelUserDataPersister:
arguments: ['@api_platform.doctrine.orm.data_persister']
# twig.extension.date:
# class: Twig_Extensions_Extension_Date

@ -12,8 +12,8 @@
"@type": "EmailMessage",
"description": "Chamilo Mail Notification",
"potentialAction": {
"@type": "ViewAction",
"target": "{{ link }}"
"@type": "ViewAction"
{# "target": "{{ link }}"#}
}
}
</script>

@ -69,24 +69,26 @@ class MessageVoter extends Voter
switch ($attribute) {
case self::CREATE:
if ($message->getUserSender() === $user) {
if ($message->getSender() === $user) {
return true;
}
break;
case self::VIEW:
if ($message->getUserReceiver() === $user) {
if ($message->getReceivers()->contains($user)) {
return true;
}
break;
case self::EDIT:
case self::DELETE:
if ($message->getUserReceiver() === $user && Message::MESSAGE_TYPE_INBOX === $message->getMsgType()) {
if ($message->getReceivers()->contains($user) &&
Message::MESSAGE_TYPE_INBOX === $message->getMsgType()
) {
return true;
}
if ($message->getUserSender() === $user && Message::MESSAGE_TYPE_OUTBOX === $message->getMsgType()) {
if ($message->getSender() === $user && Message::MESSAGE_TYPE_OUTBOX === $message->getMsgType()) {
return true;
}

@ -466,7 +466,7 @@ final class UserToJsonNormalizer
// Message
$criteria = [
'userSender' => $userId,
'sender' => $userId,
];
$result = $em->getRepository(Message::class)->findBy($criteria);
$messageList = [];
@ -474,13 +474,16 @@ final class UserToJsonNormalizer
foreach ($result as $item) {
$date = $item->getSendDate()->format($dateFormat);
$userName = '';
if ($item->getUserReceiver()) {
$userName = $item->getUserReceiver()->getUsername();
if ($item->getReceivers()) {
foreach ($item->getReceivers() as $receiver) {
$userName = ', '.$receiver->getUsername();
}
}
$list = [
'Title: '.$item->getTitle(),
'Sent date: '.$date,
'To user: '.$userName,
'To users: '.$userName,
'Type: '.$item->getMsgType(),
];
$messageList[] = implode(', ', $list);

@ -12,6 +12,7 @@ use Chamilo\CoreBundle\Repository\MessageRepository;
use Chamilo\CoreBundle\Repository\MessageTagRepository;
use Chamilo\Tests\AbstractApiTest;
use Chamilo\Tests\ChamiloTestTrait;
use Symfony\Component\Messenger\Transport\InMemoryTransport;
/**
* @covers \MessageRepository
@ -24,7 +25,7 @@ class MessageRepositoryTest extends AbstractApiTest
{
self::bootKernel();
$repo = self::getContainer()->get(MessageRepository::class);
$admin = $this->getUser('admin');
$testUser = $this->createUser('test');
$message =
@ -32,8 +33,8 @@ class MessageRepositoryTest extends AbstractApiTest
->setTitle('hello')
->setContent('content')
->setMsgType(Message::MESSAGE_TYPE_INBOX)
->setUserSender($this->getUser('admin'))
->setUserReceiver($testUser)
->setSender($admin)
->addReceiver($testUser)
;
$this->assertHasNoEntityViolations($message);
@ -107,8 +108,8 @@ class MessageRepositoryTest extends AbstractApiTest
'title' => 'hello',
'content' => 'content of hello',
'msgType' => Message::MESSAGE_TYPE_INBOX,
'userSender' => $fromUser->getIri(),
'userReceiver' => $toUser->getIri(),
'sender' => $fromUser->getIri(),
'receivers' => [$toUser->getIri()],
],
]
);
@ -116,6 +117,7 @@ class MessageRepositoryTest extends AbstractApiTest
$this->assertResponseIsSuccessful();
$this->assertResponseStatusCodeSame(201);
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains(
[
'@context' => '/api/contexts/Message',
@ -130,6 +132,18 @@ class MessageRepositoryTest extends AbstractApiTest
$this->assertSame(1, $repo->count(['msgType' => Message::MESSAGE_TYPE_INBOX]));
$this->assertSame(1, $repo->count(['msgType' => Message::MESSAGE_TYPE_OUTBOX]));
// The message was added in the queue.
/** @var InMemoryTransport $transport */
$transport = $this->getContainer()->get('messenger.transport.sync_priority_high');
$this->assertCount(1, $transport->getSent());
}
public function testCreateMessageWithApiAsOtherUser(): void
{
$fromUser = $this->createUser('from');
$toUser = $this->createUser('to');
$repo = self::getContainer()->get(MessageRepository::class);
// Try to send a message as another user.
$this->createUser('bad');
$tokenFromBadUser = $this->getUserToken(
@ -148,8 +162,8 @@ class MessageRepositoryTest extends AbstractApiTest
'title' => 'hello',
'content' => 'content of hello',
'msgType' => Message::MESSAGE_TYPE_INBOX,
'userSender' => $fromUser->getIri(),
'userReceiver' => $toUser->getIri(),
'sender' => $fromUser->getIri(),
'receivers' => [$toUser->getIri()],
],
]
);
@ -164,8 +178,8 @@ class MessageRepositoryTest extends AbstractApiTest
'title' => 'hello',
'content' => 'content of hello',
'msgType' => Message::MESSAGE_TYPE_INBOX,
'userSender' => $toUser->getIri(),
'userReceiver' => $fromUser->getIri(),
'sender' => $toUser->getIri(),
'receivers' => [$fromUser->getIri()],
],
]
);
@ -195,8 +209,8 @@ class MessageRepositoryTest extends AbstractApiTest
'title' => 'hello',
'content' => 'content of hello',
'msgType' => Message::MESSAGE_TYPE_INBOX,
'userSender' => $fromUser->getIri(),
'userReceiver' => $toUser->getIri(),
'sender' => $fromUser->getIri(),
'receivers' => [$toUser->getIri()],
],
]
);

Loading…
Cancel
Save