-
Notifications
You must be signed in to change notification settings - Fork 108
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
base: main
Are you sure you want to change the base?
Changes from all commits
7d91031
c5a07e3
f6a8f7f
3dca4b7
3b80e0e
c918384
708b379
3d39c40
079efb4
7fd8e0d
e7d6de2
29cfee1
c99249a
1c70ce7
46ce080
acc7ce8
bac9230
4ac56e6
31ae11c
4713350
8a098fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
<?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'); | ||
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 { | ||
|
||
/** | ||
* 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 >= :onehourago | ||
AND timemodified <= :tenminutesago'; | ||
|
||
return $DB->get_records_sql($sql, [ | ||
'zoommodulename' => 'zoom', | ||
'zoomeventtype' => 'zoom', | ||
'onehourago' => time() - (60 * 60), | ||
'tenminutesago' => time() - (60 * 10), | ||
]); | ||
} | ||
|
||
/** | ||
* 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed! |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could add some unique identifier to the filename, although I don't think it's absolutely necessary since a new .ics file is created for each user. Definitely wouldn't hurt to do some file cleanup after the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like file_get_unused_draft_itemid gives us a unique identifier, so that shouldn't be an issue. I don't know the internal notification system well enough to know what happens if the notification is sent as an internal message within Moodle (as opposed to an email) and thus if it is safe for us to clean up the draft file (or even if the draft files are auto-cleaned by Moodle). |
||
]; | ||
|
||
$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'; | ||
jrchamp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
||
$cm = get_fast_modinfo($zoomevent->courseid, $user->id)->instances['zoom'][$zoomevent->instance]; | ||
|
||
$zoomurl = new \moodle_url('/mod/zoom/view.php', ['id' => $cm->id]); | ||
|
||
if ($zoom->registration == ZOOM_REGISTRATION_OFF) { | ||
$icalevent->add_property('location', $zoomurl->out(false)); | ||
} 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', $zoomurl->out(false)); | ||
} | ||
} | ||
|
||
$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) { | ||
$cm = get_fast_modinfo($courseid)->instances['zoom'][$zoomid]; | ||
$users = get_users_by_capability($cm->context, 'mod/zoom:view'); | ||
|
||
if (empty($users)) { | ||
return []; | ||
} | ||
|
||
$info = new \core_availability\info_module($cm); | ||
return $info->filter_user_list($users); | ||
} | ||
|
||
/** | ||
* Returns the name of the task. | ||
* | ||
* @return string task name. | ||
*/ | ||
public function get_name() { | ||
return get_string('sendicalnotifications', 'mod_zoom'); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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."> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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-unique" FIELDS="zoomeventid" REFTABLE="event" REFFIELDS="id"/> | ||
</KEYS> | ||
</TABLE> | ||
</TABLES> | ||
</XMLDB> |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Moodle strict parameter formatting.
Suggested change
|
||||||||||||||||||||||||||
$settings->add($sendicalnotifications); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Default Zoom settings. | ||||||||||||||||||||||||||
$settings->add(new admin_setting_heading( | ||||||||||||||||||||||||||
'zoom/defaultsettings', | ||||||||||||||||||||||||||
|
There was a problem hiding this comment.
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.