Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send ical Notifications (Version 2) #623

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 293 additions & 0 deletions classes/task/send_ical_notifications.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
<?php
// This file is part of the Zoom plugin for Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Task: send_ical_notification
*
* @package mod_zoom
* @copyright 2024 OPENCOLLAB <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace mod_zoom\task;

defined('MOODLE_INTERNAL') || die();

require_once($CFG->dirroot.'/calendar/lib.php');
require_once($CFG->libdir.'/bennu/bennu.inc.php');
require_once($CFG->libdir.'/bennu/iCalendar_components.php');
jrchamp marked this conversation as resolved.
Show resolved Hide resolved
require_once($CFG->dirroot . '/mod/zoom/locallib.php');

/**
* Scheduled task to send ical notifications for zoom meetings that were scheduled within the last 30 minutes.
*/
class send_ical_notifications extends \core\task\scheduled_task {

Comment on lines +37 to +38
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the require_once() calls, collect all of the used classes.

use core\task\scheduled_task;
use core_availability\info_module;
class send_ical_notifications extends scheduled_task {

/**
* Execute the send ical notifications cron function.
*
* @return void nothing.
*/
public function execute() {
if (get_config('zoom', 'sendicalnotifications')) {
mtrace('[Zoom ical Notifications] Starting cron job.');
$zoomevents = $this->get_zoom_events_to_notify();
if ($zoomevents) {
foreach ($zoomevents as $zoomevent) {
mtrace('[Zoom ical Notifications] Checking to see if a zoom event with with ID ' .
$zoomevent->id . ' was notified before.');
$executiontime = $this->get_notification_executiontime($zoomevent->id);
// Only run if it hasn't run before.
if ($executiontime == 0) {
mtrace('[Zoom ical Notifications] Zoom event with ID ' .
$zoomevent->id . ' can be notified - not notified before.');
$this->send_zoom_ical_notifications($zoomevent);
// Set execution time for this cron job.
mtrace('[Zoom ical Notifications] Zoom event with ID ' . $zoomevent->id .
' was successfully notified - set execution time for log table.');
$this->set_notification_executiontime($zoomevent->id);
Comment on lines +50 to +61
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of log output here, even for items that were already processed. I don't love the use of the word "execution", it reminds me of trauma. Plus, "notification time" is accurate and clear without unnecessary words.

The "[Zoom ical Notifications] " prefix is not necessary for every line. At most, the start and end task messages are already providing that context.

}
}
} else {
mtrace('[Zoom ical Notifications] Found no zoom event records to process and notify ' .
'(created or modified within the last hour that was not notified before).');
}
mtrace('[Zoom ical Notifications] Cron job Completed.');
} else {
mtrace('[Zoom ical Notifications] The Admin Setting for the Send iCal Notification scheduled task ' .
'has not been enabled - will not run the cron job.');
}
}

/**
* Get zoom events created/modified in the last hour, but ignore the last 10 minutes. This allows
* the user to still make adjustments to the event before the ical invite is sent out.
* @return array
*/
private function get_zoom_events_to_notify() {
global $DB;

$sql = 'SELECT *
FROM {event}
WHERE modulename = :zoommodulename
AND eventtype = :zoomeventtype
AND timemodified >= (unix_timestamp() - (60 * 60))
AND timemodified <= (unix_timestamp() - (10 * 60))';

return $DB->get_records_sql($sql, ['zoommodulename' => 'zoom', 'zoomeventtype' => 'zoom']);
jrchamp marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Get the execution time (last successful ical notifications sent) for the related zoom event id.
* @param string $zoomeventid The zoom event id.
* @return string The timestamp of the last execution.
*/
private function get_notification_executiontime(string $zoomeventid) {
Comment on lines +100 to +103
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: Same concern about parameter types (should probably cast to `(int)``)

global $DB;

$executiontime = $DB->get_field('zoom_ical_notifications', 'executiontime', ['zoomeventid' => $zoomeventid]);
if (!$executiontime) {
$executiontime = 0;
}
return $executiontime;
}

/**
* Set the execution time (the current time) for successful ical notifications sent for the related zoom event id.
* @param string $zoomeventid The zoom event id.
*/
private function set_notification_executiontime(string $zoomeventid) {
Comment on lines +115 to +117
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: I would fully expect this to be an integer. Maybe it depends on the database and PHP versions, but that would mean this will break on some systems. Probably best to cast as (int) before passing to this function and update the parameter types.

global $DB;

$icalnotifojb = new \stdClass();
$icalnotifojb->zoomeventid = $zoomeventid;
$icalnotifojb->executiontime = time();

$DB->insert_record('zoom_ical_notifications', $icalnotifojb);
}

/**
* The zoom ical notification task.
* @param stdClass $zoomevent The zoom event record.
*/
private function send_zoom_ical_notifications($zoomevent) {
global $DB;

mtrace('[Zoom ical Notifications] Notifying Zoom event with ID ' . $zoomevent->id);

$users = $this->get_users_to_notify($zoomevent->instance, $zoomevent->courseid);

$zoom = $DB->get_record('zoom', ['id' => $zoomevent->instance], 'id,registration,join_url,meeting_id,webinar');

$filestorage = get_file_storage();

foreach ($users as $user) {
// Check if user has "Disable notifications" set.
if ($user->emailstop) {
continue;
}
Comment on lines +143 to +146
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: Is emailstop intended to stop all messages or only emails? Are we supposed to be doing this check, or is this supposed to be handled by the messaging system itself?


$ical = $this->create_ical_object($zoomevent, $zoom, $user);

$filerecord = [
'contextid' => \context_user::instance($user->id)->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => file_get_unused_draft_itemid(),
'filepath' => '/',
'filename' => clean_filename('icalexport.ics'),
Comment on lines +151 to +156
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: Determine if these is a concern about unique filenames. Do/should these files get cleaned up after use?

];

$serializedical = $ical->serialize();
if (!$serializedical || empty($serializedical)) {
mtrace('[Zoom ical Notifications] A problem occurred while trying to serialize the ical data for user ID ' .
$user->id . ' for zoom event ID ' . $zoomevent->id);
continue;
}

$icalfileattachment = $filestorage->create_file_from_string($filerecord, $serializedical);

$messagedata = new \core\message\message();
$messagedata->component = 'mod_zoom';
Comment on lines +168 to +169
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewer: Compare to core Moodle to see if this is the recommended pattern for initializing the object.

$messagedata->name = 'ical_notifications';
$messagedata->userfrom = \core_user::get_noreply_user();
$messagedata->userto = $user;
$messagedata->subject = $zoomevent->name;
$messagedata->fullmessage = $zoomevent->description;
$messagedata->fullmessageformat = FORMAT_HTML;
$messagedata->fullmessagehtml = $zoomevent->description;
$messagedata->smallmessage = $zoomevent->name . ' - ' . $zoomevent->description;
Comment on lines +173 to +177
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to tester: Do we need to apply filters like in #615 so the messages reflect the displayed values for the activities? Do the event names and descriptions already have the post-processed value or do they need to have the filter applied? There is a helper function for the activity name, but testing would be needed to determine if the description gets automatically passed through filters or not or if we need to supply the context.

$messagedata->notification = true;
$messagedata->attachment = $icalfileattachment;
$messagedata->attachname = $icalfileattachment->get_filename();

$emailsuccess = message_send($messagedata);

if ($emailsuccess) {
mtrace('[Zoom ical Notifications] Successfully emailed user ID ' . $user->id .
' for zoom event ID ' . $zoomevent->id);
} else {
mtrace('[Zoom ical Notifications] A problem occurred while emailing user ID ' . $user->id .
' for zoom event ID ' . $zoomevent->id);
}
}
}

/**
* Create the ical object.
* @param stdClass $zoomevent The zoom event record.
* @param stdClass $zoom The zoom record.
* @param stdClass $user The user object.
* @return \iCalendar
*/
private function create_ical_object($zoomevent, $zoom, $user) {
global $CFG, $SITE;

$ical = new \iCalendar();
$ical->add_property('method', 'PUBLISH');
$ical->add_property('prodid', '-//Moodle Pty Ltd//NONSGML Moodle Version ' . $CFG->version . '//EN');

$icalevent = zoom_helper_icalendar_event($zoomevent, $zoomevent->description);

if ($zoom->registration == ZOOM_REGISTRATION_OFF) {
$icalevent->add_property('location', $zoom->join_url);
} else {
$registrantjoinurl = zoom_get_registrant_join_url($user->email, $zoom->meeting_id, $zoom->webinar);
if ($registrantjoinurl) {
$icalevent->add_property('location', $registrantjoinurl);
} else {
$icalevent->add_property('location', $zoom->join_url);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of providing the stored join_url, it's probably better to send the user to the Moodle site's join URL. That way they won't circumvent the authentication/logging mechanisms or be left with an outdated/cached version of the URL.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrchamp Would it be a good idea if we maybe add a feature to the mod/zoom/view.php page to include a optional parameter like 'autojoin', so that the authenticated user is automatically forwarded to the zoom url?


$noreplyuser = \core_user::get_noreply_user();
$icalevent->add_property('organizer', 'mailto:' . $noreplyuser->email, ['cn' => $SITE->fullname]);
// Need to strip out the double quotations around the 'organizer' values - probably a bug in the core code.
$organizervalue = $icalevent->properties['ORGANIZER'][0]->value;
$icalevent->properties['ORGANIZER'][0]->value = substr($organizervalue, 1, -1);
$organizercnparam = $icalevent->properties['ORGANIZER'][0]->parameters['CN'];
$icalevent->properties['ORGANIZER'][0]->parameters['CN'] = substr($organizercnparam, 1, -1);

// Add the event to the iCal file.
$ical->add_component($icalevent);

return $ical;
}

/**
* Get an array of users in the format of userid=>user object.
* @param string $zoomid The zoom instance id.
* @param string $courseid The course id of the course in which the zoom event occurred.
* @return array An array of users.
*/
private function get_users_to_notify($zoomid, $courseid) {
global $DB;
$users = [];

$sql = 'SELECT distinct ue.userid
jrchamp marked this conversation as resolved.
Show resolved Hide resolved
FROM {zoom} z
JOIN {enrol} e
ON e.courseid = z.course
JOIN {user_enrolments} ue
ON ue.enrolid = e.id
jrchamp marked this conversation as resolved.
Show resolved Hide resolved
WHERE z.id = :zoom_id';

$zoomparticipantsids = $DB->get_records_sql($sql, ['zoom_id' => $zoomid]);
if ($zoomparticipantsids) {
foreach ($zoomparticipantsids as $zoomparticipantid) {
$users += [$zoomparticipantid->userid => \core_user::get_user($zoomparticipantid->userid)];
jrchamp marked this conversation as resolved.
Show resolved Hide resolved
}
}

if (count($users) > 0) {
jrchamp marked this conversation as resolved.
Show resolved Hide resolved
$users = $this->filter_users($zoomid, $courseid, $users);
}

return $users;
}

/**
* Filter the zoom users based on availability restrictions.
* @param string $zoomid The zoom instance id.
* @param string $courseid The course id of the course in which the zoom event occurred.
* @param array $users An array of users that potentially has access to the Zoom activity.
* @return array A filtered array of users.
*/
private function filter_users($zoomid, $courseid, $users) {
$modinfo = get_fast_modinfo($courseid);
$coursemodules = $modinfo->get_cms();
if ($coursemodules) {
foreach ($coursemodules as $coursemod) {
if ($coursemod->modname == 'zoom' && $coursemod->instance == $zoomid) {
$availinfo = new \core_availability\info_module($coursemod);
$users = $availinfo->filter_user_list($users);
break;
}
}
}
return $users;
}
jrchamp marked this conversation as resolved.
Show resolved Hide resolved

/**
* Returns the name of the task.
*
* @return string task name.
*/
public function get_name() {
return get_string('sendicalnotifications', 'mod_zoom');
}

}
11 changes: 11 additions & 0 deletions db/install.xml
Original file line number Diff line number Diff line change
Expand Up @@ -184,5 +184,16 @@
<KEY NAME="fk_breakoutroomid" TYPE="foreign" FIELDS="breakoutroomid" REFTABLE="zoom_meeting_breakout_rooms" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="zoom_ical_notifications" COMMENT="Identifies the zoom event for which ical notifications have been emailed.">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: Generate this using XMLDB and export it using XMLDB. Check for differences.

<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="zoomeventid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="executiontime" TYPE="int" LENGTH="12" NOTNULL="true" SEQUENCE="false" COMMENT="The time when the send ical notifications task was completed successfully."/>
jrchamp marked this conversation as resolved.
Show resolved Hide resolved
</FIELDS>
<KEYS>
<KEY NAME="id_primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_zoomeventid" TYPE="foreign" FIELDS="zoomeventid" REFTABLE="event" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
8 changes: 8 additions & 0 deletions db/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,12 @@

$messageproviders = [
'teacher_notification' => [],
// The ical notifications task messages.
'ical_notifications' => [
'defaults' => [
'popup' => MESSAGE_DISALLOWED,
'email' => MESSAGE_PERMITTED,
jrchamp marked this conversation as resolved.
Show resolved Hide resolved
'airnotifier' => MESSAGE_DISALLOWED,
],
],
];
9 changes: 9 additions & 0 deletions db/tasks.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,13 @@
'dayofweek' => '*',
'month' => '*',
],
[
'classname' => 'mod_zoom\task\send_ical_notifications',
'blocking' => 0,
'minute' => '*/5',
jrchamp marked this conversation as resolved.
Show resolved Hide resolved
'hour' => '*',
'day' => '*',
'dayofweek' => '*',
'month' => '*',
],
];
20 changes: 20 additions & 0 deletions db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -1002,5 +1002,25 @@ function xmldb_zoom_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2024072500, 'zoom');
}

if ($oldversion < 2024100300) {
// Conditionally create the Zoom iCal Notifications table.
$table = new xmldb_table('zoom_ical_notifications');

if (!$dbman->table_exists($table)) {

jrchamp marked this conversation as resolved.
Show resolved Hide resolved
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('zoomeventid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('executiontime', XMLDB_TYPE_INTEGER, '12', null, null, null, null);

$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('fk_zoomeventid', XMLDB_KEY_FOREIGN, ['zoomeventid'], 'event', ['id']);

$dbman->create_table($table);
}

// Zoom savepoint reached.
upgrade_mod_savepoint(true, 2024100300, 'zoom');
}

return true;
}
3 changes: 3 additions & 0 deletions lang/en/zoom.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
$string['meetingcapacitywarningheading'] = 'Meeting capacity warning:';
$string['meetingparticipantsdeleted'] = 'Meeting participant user data deleted.';
$string['meetingrecordingviewsdeleted'] = 'Meeting recording user view data deleted.';
$string['messageprovider:ical_notifications'] = 'Send iCal invitations for a newly created Zoom event to participants.';
$string['messageprovider:teacher_notification'] = 'Notify teachers about user grades (according to duration) in a Zoom session';
$string['modulename'] = 'Zoom meeting';
$string['modulename_help'] = 'Zoom is a video and web conferencing platform that gives authorized users the ability to host online meetings.';
Expand Down Expand Up @@ -394,6 +395,8 @@
$string['search:activity'] = 'Zoom - activity information';
$string['security'] = 'Security';
$string['selectionarea'] = 'No selection';
$string['sendicalnotifications'] = 'Send iCal Notifications';
$string['sendicalnotifications_help'] = "Enabling this option will allow iCal Notifications to be sent via the 'Send iCal Notification' scheduled task.";
jrchamp marked this conversation as resolved.
Show resolved Hide resolved
$string['sessions'] = 'Sessions';
$string['sessionsreport'] = 'Sessions report';
$string['sesskeyinvalid'] = 'Invalid session detected. Cannot proceed further.';
Expand Down
6 changes: 6 additions & 0 deletions settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,12 @@
);
$settings->add($offerdownloadical);

$sendicalnotifications = new admin_setting_configcheckbox('zoom/sendicalnotifications',
get_string('sendicalnotifications', 'mod_zoom'),
get_string('sendicalnotifications_help', 'mod_zoom'),
0, 1, 0);
Comment on lines +377 to +380
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Moodle strict parameter formatting.

Suggested change
$sendicalnotifications = new admin_setting_configcheckbox('zoom/sendicalnotifications',
get_string('sendicalnotifications', 'mod_zoom'),
get_string('sendicalnotifications_help', 'mod_zoom'),
0, 1, 0);
$sendicalnotifications = new admin_setting_configcheckbox(
'zoom/sendicalnotifications',
get_string('sendicalnotifications', 'mod_zoom'),
get_string('sendicalnotifications_help', 'mod_zoom'),
0,
1,
0
);

$settings->add($sendicalnotifications);

// Default Zoom settings.
$settings->add(new admin_setting_heading(
'zoom/defaultsettings',
Expand Down
Loading