Skip to content

Commit

Permalink
MDL-64715 message: add support for self conversations
Browse files Browse the repository at this point in the history
Added new MESSAGE_CONVERSATION_TYPE_SELF type for self-conversations
and upgraded legacy self-conversations to the new type, removing
repeated members in the message_conversation_members table.
Besides, from now, a self-conversation will be created by default for
all the existing users.

All the self-conversations have been also starred and a default message
will be displayed always to explain how to use them.
  • Loading branch information
sarjona committed Apr 15, 2019
1 parent fcd7f0f commit 734b198
Show file tree
Hide file tree
Showing 33 changed files with 1,450 additions and 282 deletions.
4 changes: 4 additions & 0 deletions lang/en/message.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@
$string['defaults'] = 'Defaults';
$string['deleteallconfirm'] = "Are you sure you would like to delete this entire conversation? This will not delete it for other conversation participants.";
$string['deleteallmessages'] = "Delete all messages";
$string['deleteallselfconfirm'] = "Are you sure you would like to delete this entire personal conversation?";
$string['deleteconversation'] = "Delete conversation";
$string['deleteselectedmessages'] = 'Delete selected messages';
$string['deleteselectedmessagesconfirm'] = 'Are you sure you would like to delete the selected messages? This will not delete them for other conversation participants.';
$string['deleteselectedmessagesconfirmselfconversation'] = 'Are you sure you would like to delete the selected personal messages?';
$string['disableall'] = 'Disable notifications';
$string['disabled'] = 'Messaging is disabled on this site';
$string['disallowed'] = 'Disallowed';
Expand Down Expand Up @@ -211,6 +213,8 @@
$string['seeall'] = 'See all';
$string['selectmessagestodelete'] = 'Select messages to delete';
$string['selectnotificationtoview'] = 'Select from the list of notifications on the side to view more details';
$string['selfconversation'] = 'Personal space';
$string['selfconversationdefaultmessage'] = 'Save draft messages, links, notes etc. to access later.';
$string['send'] = 'Send';
$string['sender'] = '{$a}:';
$string['sendingvia'] = 'Sending "{$a->provider}" via "{$a->processor}"';
Expand Down
6 changes: 6 additions & 0 deletions lib/classes/message/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ public static function send_message_to_conversation(message $eventdata, \stdClas
// Get conversation type and name. We'll use this to determine which message subject to generate, depending on type.
$conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type, name');

// For now Self conversations are not processed because users are aware of the messages sent by themselves, so we
// can return early.
if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF) {
return $savemessage->id;
}

// We treat individual conversations the same as any direct message with 'userfrom' and 'userto' specified.
// We know the other user, so set the 'userto' field so that the event code will get access to this field.
// If this was a legacy caller (eventdata->userto is set), then use that instead, as we want to use the fields specified
Expand Down
9 changes: 9 additions & 0 deletions lib/db/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,15 @@
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
'ajax' => true
),
'core_message_get_self_conversation' => array(
'classname' => 'core_message_external',
'methodname' => 'get_self_conversation',
'classpath' => 'message/externallib.php',
'description' => 'Retrieve a self-conversation for a user',
'type' => 'read',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
'ajax' => true
),
'core_message_get_messages' => array(
'classname' => 'core_message_external',
'methodname' => 'get_messages',
Expand Down
184 changes: 184 additions & 0 deletions lib/db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -2988,5 +2988,189 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2019041000.02);
}

if ($oldversion < 2019041300.01) {
// STEP 1. For the existing and migrated self-conversations, set the type to the new MESSAGE_CONVERSATION_TYPE_SELF, update
// the convhash and star them.
$sql = "SELECT mcm.conversationid, mcm.userid, MAX(mcm.id) as maxid
FROM {message_conversation_members} mcm
GROUP BY mcm.conversationid, mcm.userid
HAVING COUNT(*) > 1";
$selfconversationsrs = $DB->get_recordset_sql($sql);
$maxids = [];
foreach ($selfconversationsrs as $selfconversation) {
$DB->update_record('message_conversations',
['id' => $selfconversation->conversationid,
'type' => \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
'convhash' => \core_message\helper::get_conversation_hash([$selfconversation->userid])
]
);

// Star the existing self-conversation.
$favouriterecord = new \stdClass();
$favouriterecord->component = 'core_message';
$favouriterecord->itemtype = 'message_conversations';
$favouriterecord->itemid = $selfconversation->conversationid;
$userctx = \context_user::instance($selfconversation->userid);
$favouriterecord->contextid = $userctx->id;
$favouriterecord->userid = $selfconversation->userid;
$favouriterecord->timecreated = time();
$favouriterecord->timemodified = $favouriterecord->timecreated;

$DB->insert_record('favourite', $favouriterecord);

// Set the self-conversation member with maxid to remove it later.
$maxids[] = $selfconversation->maxid;
}
$selfconversationsrs->close();

// Remove the repeated member with the higher id for all the existing self-conversations.
if (!empty($maxids)) {
list($insql, $inparams) = $DB->get_in_or_equal($maxids);
$DB->delete_records_select('message_conversation_members', "id $insql", $inparams);
}

// STEP 2. Migrate existing self-conversation relying on old message tables, setting the type to the new
// MESSAGE_CONVERSATION_TYPE_SELF and the convhash to the proper one. Star them also.

// On the messaging legacy tables, self-conversations are only present in the 'message_read' table, so we don't need to
// check the content in the 'message' table.
$select = 'useridfrom = useridto AND notification = 0';
$legacyselfmessagesrs = $DB->get_recordset_select('message_read', $select);
foreach ($legacyselfmessagesrs as $message) {
// Get the self-conversation or create and star it if doesn't exist.
$conditions = [
'type' => \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
'convhash' => \core_message\helper::get_conversation_hash([$message->useridfrom])
];
$selfconversation = $DB->get_record('message_conversations', $conditions);
if (empty($selfconversation)) {
// Create the self-conversation.
$selfconversation = new \stdClass();
$selfconversation->type = \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF;
$selfconversation->convhash = \core_message\helper::get_conversation_hash([$message->useridfrom]);
$selfconversation->enabled = 1;
$selfconversation->timecreated = time();
$selfconversation->timemodified = $selfconversation->timecreated;

$selfconversation->id = $DB->insert_record('message_conversations', $selfconversation);

// Add user to this self-conversation.
$member = new \stdClass();
$member->conversationid = $selfconversation->id;
$member->userid = $message->useridfrom;
$member->timecreated = time();

$member->id = $DB->insert_record('message_conversation_members', $member);

// Star the self-conversation.
$favouriterecord = new \stdClass();
$favouriterecord->component = 'core_message';
$favouriterecord->itemtype = 'message_conversations';
$favouriterecord->itemid = $selfconversation->id;
$userctx = \context_user::instance($message->useridfrom);
$favouriterecord->contextid = $userctx->id;
$favouriterecord->userid = $message->useridfrom;
$favouriterecord->timecreated = time();
$favouriterecord->timemodified = $favouriterecord->timecreated;

$DB->insert_record('favourite', $favouriterecord);
}

// Create the object we will be inserting into the database.
$tabledata = new \stdClass();
$tabledata->useridfrom = $message->useridfrom;
$tabledata->conversationid = $selfconversation->id;
$tabledata->subject = $message->subject;
$tabledata->fullmessage = $message->fullmessage;
$tabledata->fullmessageformat = $message->fullmessageformat ?? FORMAT_MOODLE;
$tabledata->fullmessagehtml = $message->fullmessagehtml;
$tabledata->smallmessage = $message->smallmessage;
$tabledata->timecreated = $message->timecreated;

$messageid = $DB->insert_record('messages', $tabledata);

// Check if we need to mark this message as deleted (self-conversations add this information on the
// timeuserfromdeleted field.
if ($message->timeuserfromdeleted) {
$mua = new \stdClass();
$mua->userid = $message->useridfrom;
$mua->messageid = $messageid;
$mua->action = \core_message\api::MESSAGE_ACTION_DELETED;
$mua->timecreated = $message->timeuserfromdeleted;

$DB->insert_record('message_user_actions', $mua);
}

// Mark this message as read.
$mua = new \stdClass();
$mua->userid = $message->useridto;
$mua->messageid = $messageid;
$mua->action = \core_message\api::MESSAGE_ACTION_READ;
$mua->timecreated = $message->timeread;

$DB->insert_record('message_user_actions', $mua);
}
$legacyselfmessagesrs->close();

// We can now delete the records from legacy table because the self-conversations have been migrated from the legacy tables.
$DB->delete_records_select('message_read', $select);

// STEP 3. For existing users without self-conversations, create and star it.

// Get all the users without a self-conversation.
$sql = "SELECT u.id
FROM {user} u
WHERE u.id NOT IN (SELECT mcm.userid
FROM {message_conversation_members} mcm
INNER JOIN mdl_message_conversations mc
ON mc.id = mcm.conversationid AND mc.type = ?
)";
$useridsrs = $DB->get_recordset_sql($sql, [\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF]);
// Create the self-conversation for all these users.
foreach ($useridsrs as $user) {
$conditions = [
'type' => \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
'convhash' => \core_message\helper::get_conversation_hash([$user->id])
];
$selfconversation = $DB->get_record('message_conversations', $conditions);
if (empty($selfconversation)) {
// Create the self-conversation.
$selfconversation = new \stdClass();
$selfconversation->type = \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF;
$selfconversation->convhash = \core_message\helper::get_conversation_hash([$user->id]);
$selfconversation->enabled = 1;
$selfconversation->timecreated = time();
$selfconversation->timemodified = $selfconversation->timecreated;

$selfconversation->id = $DB->insert_record('message_conversations', $selfconversation);

// Add user to this self-conversation.
$member = new \stdClass();
$member->conversationid = $selfconversation->id;
$member->userid = $user->id;
$member->timecreated = time();

$member->id = $DB->insert_record('message_conversation_members', $member);

// Star the self-conversation.
$favouriterecord = new \stdClass();
$favouriterecord->component = 'core_message';
$favouriterecord->itemtype = 'message_conversations';
$favouriterecord->itemid = $selfconversation->id;
$userctx = \context_user::instance($user->id);
$favouriterecord->contextid = $userctx->id;
$favouriterecord->userid = $user->id;
$favouriterecord->timecreated = time();
$favouriterecord->timemodified = $favouriterecord->timecreated;

$DB->insert_record('favourite', $favouriterecord);
}
}
$useridsrs->close();

// Main savepoint reached.
upgrade_main_savepoint(true, 2019041300.01);
}

return true;
}
32 changes: 22 additions & 10 deletions lib/messagelib.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,30 @@ function message_send(\core\message\message $eventdata) {
return false;
}

if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id,
$eventdata->userto->id])) {
$conversation = \core_message\api::create_conversation(
\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
[
$eventdata->userfrom->id,
$eventdata->userto->id
]
);
if ($eventdata->userfrom->id == $eventdata->userto->id) {
// It's a self conversation.
$conversation = \core_message\api::get_self_conversation($eventdata->userfrom->id);
if (empty($conversation)) {
$conversation = \core_message\api::create_conversation(
\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
[$eventdata->userfrom->id]
);
}
} else {
if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id,
$eventdata->userto->id])) {
// It's a private conversation between users.
$conversation = \core_message\api::create_conversation(
\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
[
$eventdata->userfrom->id,
$eventdata->userto->id
]
);
}
}
// We either have found a conversation, or created one.
$conversationid = $conversationid ? $conversationid : $conversation->id;
$conversationid = !empty($conversationid) ? $conversationid : $conversation->id;
$eventdata->convid = $conversationid;
}

Expand Down
53 changes: 53 additions & 0 deletions lib/tests/messagelib_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,59 @@ public function test_message_send_to_conversation_individual() {
$sink->clear();
}

/**
* Tests calling message_send() with $eventdata representing a message to a self-conversation.
*
* This test will verify:
* - that the 'messages' record is created.
* - that the processors is not called (for now self-conversations are not processed).
* - the a single event will be generated - 'message_sent'
*
* Note: We won't redirect/capture messages in this test because doing so causes message_send() to return early, before
* processors and events code is called. We need to test this code here, as we generally redirect messages elsewhere and we
* need to be sure this is covered.
*/
public function test_message_send_to_self_conversation() {
global $DB;
$this->preventResetByRollback();
$this->resetAfterTest();

// Create some users and a conversation between them.
$user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
set_config('allowedemaildomains', 'example.com');
$conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
[$user1->id]);

// Generate the message.
$message = new \core\message\message();
$message->courseid = 1;
$message->component = 'moodle';
$message->name = 'instantmessage';
$message->userfrom = $user1;
$message->convid = $conversation->id;
$message->subject = 'message subject 1';
$message->fullmessage = 'message body';
$message->fullmessageformat = FORMAT_MARKDOWN;
$message->fullmessagehtml = '<p>message body</p>';
$message->smallmessage = 'small message';
$message->notification = '0';

// Content specific to the email processor.
$content = array('*' => array('header' => ' test ', 'footer' => ' test '));
$message->set_additional_content('email', $content);

// Ensure we're going to hit the email processor for this user.
$DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user1);

// Now, send a message and verify the message processors are empty (self-conversations are not processed for now).
$sink = $this->redirectEmails();
$messageid = message_send($message);
$emails = $sink->get_messages();
$this->assertCount(0, $emails);
$sink->clear();
}

/**
* Tests calling message_send() with $eventdata representing a message to an group conversation.
*
Expand Down
8 changes: 8 additions & 0 deletions lib/upgrade.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ attribute on forms to avoid collisions in forms loaded in AJAX requests.
in this category. To work with list of courses use API methods in core_course_category and also 'course' form element.
* It is possible to pass additional conditions to get_courses_search();
core_course_category::search_courses() now allows to search only among courses with completion enabled.
* A new conversation type has been created for self-conversations. During the upgrading process:
- Firstly, the existing self-conversations will be starred and migrated to the new type, removing the duplicated members in the
message_conversation_members table.
- Secondly, the legacy self conversations will be migrated from the legacy 'message_read' table. They will be created using the
new conversation type and will be favourited.
- Finally, the self-conversations for all remaining users without them will be created and starred.
Besides, from now, a self-conversation will be created and starred by default to all the new users (even when $CFG->messaging
is disabled).

=== 3.6 ===

Expand Down
2 changes: 1 addition & 1 deletion message/amd/build/message_drawer_view_conversation.min.js

Large diffs are not rendered by default.

Loading

0 comments on commit 734b198

Please sign in to comment.