engagements.module

This is the primary module file for the engagements module.

File

modules/engagements/engagements.module
View source
  1. <?php
  2. /**
  3. * @file
  4. * This is the primary module file for the engagements module.
  5. */
  6. /**
  7. * Implement hook_menu
  8. */
  9. function engagements_menu()
  10. {
  11. $items = array();
  12. $items["engagements"] = array(
  13. "title" => t("Engagements"),
  14. "page_callback" => "engagements_display_main",
  15. "access_arguments" => array("can_view_engagements"),
  16. "tab_family" => "system",
  17. "page_settings" => array(
  18. "display_currently_advising" => TRUE,
  19. ),
  20. "weight" => 20,
  21. "type" => MENU_TYPE_TAB,
  22. );
  23. $items['engagements-track/%/%/pixel.gif'] = array(
  24. 'page_callback' => 'engagements_handle_tracking_pixel_request',
  25. 'page_arguments' => array(1, 2),
  26. 'access_callback' => TRUE,
  27. 'type' => MENU_TYPE_CALLBACK,
  28. );
  29. $items['engagements-handle-incoming-sms'] = array(
  30. 'page_callback' => 'engagements_handle_incoming_sms',
  31. 'access_callback' => TRUE,
  32. 'type' => MENU_TYPE_CALLBACK,
  33. );
  34. $items["admin/config/imap"] = array(
  35. "title" => "Engagements - IMAP settings",
  36. "description" => "Configure IMAP settings for Engagements",
  37. "page_callback" => "fp_render_form",
  38. "page_arguments" => array("engagements_imap_settings_form", "system_settings"),
  39. "access_arguments" => array("administer_engagements"),
  40. "page_settings" => array(
  41. "menu_icon" => fp_get_module_path('system') . "/icons/cog.png",
  42. "menu_links" => array(
  43. 0 => array(
  44. "text" => "Admin Console",
  45. "path" => "admin-tools/admin",
  46. "query" => "de_catalog_year=%DE_CATALOG_YEAR%",
  47. ),
  48. ),
  49. ),
  50. "type" => MENU_TYPE_NORMAL_ITEM,
  51. "tab_parent" => "admin-tools/admin",
  52. );
  53. $items["admin/config/sms"] = array(
  54. "title" => "Engagements - SMS settings",
  55. "description" => "Configure SMS settings for Engagements",
  56. "page_callback" => "fp_render_form",
  57. "page_arguments" => array("engagements_sms_settings_form", "system_settings"),
  58. "access_arguments" => array("administer_engagements"),
  59. "page_settings" => array(
  60. "menu_icon" => fp_get_module_path('system') . "/icons/cog.png",
  61. "menu_links" => array(
  62. 0 => array(
  63. "text" => "Admin Console",
  64. "path" => "admin-tools/admin",
  65. "query" => "de_catalog_year=%DE_CATALOG_YEAR%",
  66. ),
  67. ),
  68. ),
  69. "type" => MENU_TYPE_NORMAL_ITEM,
  70. "tab_parent" => "admin-tools/admin",
  71. );
  72. $items["admin-tools/mass-sms"] = array(
  73. "title" => "Send Mass Text Messages",
  74. "description" => "Send a text message to multiple recipients.",
  75. "page_callback" => "fp_render_form",
  76. "page_arguments" => array("engagements_mass_sms_form"),
  77. "access_arguments" => array("can_send_mass_sms"),
  78. "page_settings" => array(
  79. "menu_icon" => fp_get_module_path('system') . "/icons/transmit_go.png",
  80. "menu_links" => array(
  81. 0 => array(
  82. "text" => "Admin Tools",
  83. "path" => "admin-tools",
  84. "query" => "de_catalog_year=%DE_CATALOG_YEAR%",
  85. ),
  86. ),
  87. ),
  88. "type" => MENU_TYPE_NORMAL_ITEM,
  89. );
  90. $items["advisee-engagements"] = array(
  91. "title" => "Engagements Received%ENGAGEMENTS_ADVISEE_ALERTS_COUNT%",
  92. "page_callback" => "engagements_display_advisee_engagements_page",
  93. "access_arguments" => array('can_view_engagements'),
  94. "type" => MENU_TYPE_TAB,
  95. 'tab_family' => 'alerts',
  96. 'weight' => 15,
  97. "page_settings" => array(
  98. "menu_links" => array(
  99. 0 => array(
  100. "text" => "Dashboard",
  101. "path" => "main",
  102. ),
  103. ),
  104. ),
  105. );
  106. return $items;
  107. }
  108. /**
  109. * implements hook_menu_handle_replacement_pattern
  110. */
  111. function engagements_menu_handle_replacement_pattern($str) {
  112. if (strstr($str, "%ENGAGEMENTS_ADVISEE_ALERTS_COUNT%")) {
  113. // Get our count.
  114. $alert_counts = fp_get_alert_count_by_type();
  115. $c = intval($alert_counts['engagements']['engagement']['unread']);
  116. $x = "";
  117. if ($c > 0) {
  118. $x .= " ($c)";
  119. }
  120. $str = str_replace("%ENGAGEMENTS_ADVISEE_ALERTS_COUNT%", $x, $str);
  121. }
  122. return $str;
  123. }
  124. /**
  125. * Implements hook_get_count_for_alert_type
  126. *
  127. * Set and Return back "unread" (not in content_last_access)
  128. *
  129. */
  130. function engagements_get_alert_count_by_type($account = NULL) {
  131. global $user;
  132. if ($account === NULL) $account = $user;
  133. if ($account->id == 0) return FALSE;
  134. $rtn = array();
  135. $rtn['engagement']['total'] = 0;
  136. $rtn['engagement']['read'] = 0;
  137. $rtn['engagement']['unread'] = 0;
  138. $already_counted = array();
  139. // We need to know this user's list of advisees.
  140. $advisees = advise_get_advisees($account->cwid);
  141. $advisee_line = "";
  142. if ($advisees && count($advisees) > 0) {
  143. $advisees_list = "'" . join("','", $advisees) . "'";
  144. $advisee_line = " AND b.field__student_id IN (" . $advisees_list . ") ";
  145. /*
  146. $total_count = db_result(db_query("SELECT COUNT(*) as mycount
  147. FROM content__engagement b,
  148. content n
  149. WHERE n.type = ?
  150. AND b.vid = n.vid
  151. AND b.cid = n.cid
  152. AND n.published = 1
  153. AND n.delete_flag = 0
  154. AND field__direction = 'received'
  155. AND field__engagement_type IN ('email', 'txt_msg')
  156. AND (field__manual_entry IS NULL OR field__manual_entry != 'Y')
  157. $advisee_line ", array('engagement')));
  158. */
  159. $total_count = 0;
  160. $res = db_query("SELECT b.cid
  161. FROM content__engagement b,
  162. content n
  163. WHERE n.type = ?
  164. AND b.vid = n.vid
  165. AND b.cid = n.cid
  166. AND n.published = 1
  167. AND n.delete_flag = 0
  168. AND field__direction = 'received'
  169. AND field__engagement_type IN ('email', 'txt_msg')
  170. AND (field__manual_entry IS NULL OR field__manual_entry != 'Y')
  171. $advisee_line ", array('engagement'));
  172. while($cur = db_fetch_array($res)) {
  173. $cid = $cur['cid'];
  174. $already_counted[$cid] = $cid;
  175. $total_count++;
  176. }
  177. $read_count = db_result(db_query("SELECT COUNT(*) as mycount
  178. FROM content_last_access a,
  179. content__engagement b,
  180. content n
  181. WHERE n.type = ?
  182. AND n.published = 1
  183. AND b.vid = n.vid
  184. AND b.cid = n.cid
  185. AND n.delete_flag = 0
  186. AND n.cid = a.cid
  187. AND field__direction = 'received'
  188. AND field__engagement_type IN ('email', 'txt_msg')
  189. AND (field__manual_entry IS NULL OR field__manual_entry != 'Y')
  190. $advisee_line
  191. AND a.user_id = ?", array('engagement', $account->id)));
  192. $rtn['engagement']['total'] = intval($total_count);
  193. $rtn['engagement']['read'] = intval($read_count);
  194. $rtn['engagement']['unread'] = $total_count - $read_count;
  195. } // if advisees
  196. // We should ALSO get these students to show up when we're the recipient of a text number, regardless of if they are
  197. // considered our "advisee" or not.
  198. $numbers = engagements_get_user_notify_sms_receipt_values($account->id);
  199. if ($numbers && is_array($numbers) && count($numbers) > 0) {
  200. // Were there any txt messages sent to these number, which we have not read?
  201. $numbers_line = "";
  202. $numbers_list = "'" . join("','", $numbers) . "'";
  203. $numbers_line = " AND b.field__to_sms_phone IN (" . $numbers_list . ") ";
  204. // So, we should also search for results where we received at these numbers, regardless of if the user was our advisee or not.
  205. $total_count = 0;
  206. $res = db_query("SELECT b.cid
  207. FROM content__engagement b,
  208. content n
  209. WHERE n.type = ?
  210. AND b.vid = n.vid
  211. AND b.cid = n.cid
  212. AND n.published = 1
  213. AND n.delete_flag = 0
  214. AND field__direction = 'received'
  215. AND field__engagement_type IN ('email', 'txt_msg')
  216. AND (field__manual_entry IS NULL OR field__manual_entry != 'Y')
  217. $numbers_line ", array('engagement'));
  218. while($cur = db_fetch_array($res)) {
  219. $cid = $cur['cid'];
  220. if (!in_array($cid, $already_counted)) {
  221. $already_counted[$cid] = $cid;
  222. $total_count++;
  223. }
  224. }
  225. $read_count = db_result(db_query("SELECT COUNT(*) as mycount
  226. FROM content_last_access a,
  227. content__engagement b,
  228. content n
  229. WHERE n.type = ?
  230. AND n.published = 1
  231. AND b.vid = n.vid
  232. AND b.cid = n.cid
  233. AND n.delete_flag = 0
  234. AND n.cid = a.cid
  235. AND field__direction = 'received'
  236. AND field__engagement_type IN ('email', 'txt_msg')
  237. AND (field__manual_entry IS NULL OR field__manual_entry != 'Y')
  238. $numbers_line
  239. AND a.user_id = ?", array('engagement', $account->id)));
  240. $rtn['engagement']['total'] += intval($total_count);
  241. $rtn['engagement']['read'] += intval($read_count);
  242. $rtn['engagement']['unread'] += ($total_count - $read_count);
  243. } // if numbers
  244. if (count($rtn) == 0) return FALSE;
  245. return $rtn;
  246. } // engagements_get_alert_count_by_type
  247. function engagements_display_advisee_engagements_page() {
  248. global $user;
  249. $rtn = "";
  250. fp_set_title('');
  251. fp_add_css(fp_get_module_path('alerts') . '/css/style.css');
  252. $rtn .= "<p>" . t("This screen shows engagements/communications (emails and text messages) which advisees have sent, but not those which you or any other
  253. faculty/staff member has sent. To see the complete history of engagements with an advisee, visit the advisee's
  254. Engagements tab.") . "</p>";
  255. $student_ids = advise_get_advisees($user->cwid);
  256. $student_line = "''";
  257. if ($student_ids && count($student_ids) > 0) {
  258. $student_line = "'" . join("','", $student_ids) . "'";
  259. }
  260. if ($student_line) {
  261. $student_line = " field__student_id IN ($student_line) ";
  262. }
  263. // We also want to show messages sent to us which might not have been from an advisee, but we are a designated recipient anyway.
  264. $numbers = engagements_get_user_notify_sms_receipt_values($user->id);
  265. $numbers_line = "";
  266. $numbers_list = "''";
  267. if ($numbers && is_array($numbers) && count($numbers) > 0) {
  268. // Were there any txt messages sent to these number, which we have not read?
  269. $numbers_line = "";
  270. $numbers_list = "'" . join("','", $numbers) . "'";
  271. }
  272. $numbers_line = " field__to_sms_phone IN (" . $numbers_list . ") ";
  273. $table_headers = array();
  274. $table_headers[] = array("label" => "Actions");
  275. $table_headers[] = array("label" => "Student");
  276. $table_headers[] = array("label" => "Type");
  277. $table_headers[] = array("label" => "Details/Preview");
  278. $table_headers[] = array("label" => "Updated", "field" => "n.updated");
  279. $limit = 25;
  280. // Set our initial sort, if none is already set.
  281. theme_table_header_sortable_set_initial_sort('n.updated', 'DESC');
  282. $phones = engagements_get_from_phones();
  283. $rtn .= "<table border='0' class='advisees-alerts'>";
  284. // Draw our our table headers, with links....
  285. $rtn .= theme_table_header_sortable($table_headers);
  286. // Get our order by clause based on selected table header, if any.
  287. $order_by = theme_table_header_sortable_order_by($table_headers);
  288. // Now, we are going to search for alerts about these students, in the form of a pager query.
  289. // Query for alerts for this student. We will be using a pager_query, so we can display a complete history, if we wish.
  290. $res = pager_query("SELECT DISTINCT(a.cid) FROM content__engagement a, content n
  291. WHERE a.vid = n.vid
  292. AND a.cid = n.cid
  293. AND n.delete_flag = 0
  294. AND n.published = 1
  295. AND field__direction = 'received'
  296. AND ($student_line OR $numbers_line)
  297. AND field__engagement_type IN ('email', 'txt_msg')
  298. AND (field__manual_entry IS NULL OR field__manual_entry != 'Y')
  299. GROUP BY a.cid
  300. $order_by", $filter_params, $limit, 0, "SELECT COUNT(DISTINCT(a.cid)) FROM content__engagement a, content n
  301. WHERE a.vid = n.vid
  302. AND a.cid = n.cid
  303. AND n.delete_flag = 0
  304. AND n.published = 1
  305. AND field__direction = 'received'
  306. AND ($student_line OR $numbers_line)
  307. AND field__engagement_type IN ('email', 'txt_msg')
  308. AND (field__manual_entry IS NULL OR field__manual_entry != 'Y')
  309. GROUP BY a.cid
  310. $order_by");
  311. while ($cur = db_fetch_object($res)) {
  312. $cid = $cur->cid;
  313. $content = content_load($cid);
  314. $updated = format_date($content->updated, 'short');
  315. $student_id = $content->field__student_id['value'];
  316. $student_name = fp_get_student_name($student_id, TRUE);
  317. $extra_class = "";
  318. // If this content hasn't been read by this user, mark as "unread"
  319. if (!content_get_last_access($cid)) {
  320. $extra_class .= " unread";
  321. }
  322. $student_url = fp_url('student-profile', "current_student_id=$student_id");
  323. $view_url = fp_url("content/$cid", "window_mode=popup&content_tabs=false");
  324. $dtitle = t("View Engagement");
  325. $view_link = "<a href='javascript:fpOpenLargeIframeDialog(\"$view_url\",\"$dtitle\");' title='" . t("View") . "' class='action-link' ><i class='fa fa-eye'></i></a>";
  326. $student_profile_link = "<a href='$student_url' title='" . t("Student Profile") . "' class='action-link'><i class='fa fa-user'></i></a>";
  327. $disp_preview = "";
  328. if ($content->field__engagement_type['value'] == "txt_msg") {
  329. $msg = trim(filter_markup($content->field__engagement_msg['value'], 'plain'));
  330. if (strstr($msg, "~~")) {
  331. $temp = explode("~~", $msg);
  332. $msg = $temp[0];
  333. }
  334. $desc = "";
  335. $details = engagements_get_sms_from_history($content->field__external_msg_id['value']);
  336. if ($details) {
  337. $to = $details['to_number'];
  338. $pretty_to_number = engagements_convert_to_pretty_phone_number($to);
  339. $desc = @$phones['lines'][$details['to_number']]['description'];
  340. if (!$desc) $desc = @$phones['lines'][$details['from_number']]['description'];
  341. if ($desc) $desc = " / $desc";
  342. }
  343. $disp_preview = "$pretty_to_number$desc: $msg";
  344. } // txt_msg
  345. else if ($content->field__engagement_type['value'] == 'email') {
  346. $disp_preview = trim(filter_markup($content->field__engagement_msg['value'], 'plain'));
  347. }
  348. if (strlen($disp_preview) > 60) {
  349. $disp_preview = trim(substr($disp_preview, 0, 57)) . "...";
  350. }
  351. $disp_type = $content->field__engagement_type['display_value'];
  352. $rtn .= "
  353. <tr class='{$content->field__alert_status['value']} $extra_class'>
  354. <td class='actions'>$view_link $edit_link $remove_link $student_profile_link</td>
  355. ";
  356. $rtn .= "<td class='student'>$student_name</td>";
  357. $rtn .= "
  358. <td class='short-desc'><div class='short-desc-wrapper'>$disp_type</div></td>
  359. <td class='short-desc'><div class='short-desc-wrapper'>$disp_preview</div></td>
  360. <td class='updated'>$updated</td>
  361. </tr>
  362. ";
  363. } // while cur
  364. $rtn .= "</table>";
  365. $rtn .= theme_pager(array(t('« newest'), t('‹ newer'), '', t('older ›'), t('oldest »')));
  366. return $rtn;
  367. } // engagements_display_advisee_engagements_page
  368. function engagements_mass_sms_form()
  369. {
  370. global $user;
  371. $form = array();
  372. fp_add_css(fp_get_module_path('engagements') . '/css/style.css');
  373. $form['mark_top'] = array(
  374. 'value' => "<p>" . t("Use this form to send the same SMS (txt) message to multiple recipients at a time. <strong>Recipients must be either students
  375. or faculty/staff at your institution, with their mobile numbers already in the database</strong>. Your messages will
  376. be queued, and sent out approximately one per second.") . "</p>",
  377. );
  378. if (isset($_SESSION['mass_sms_batch_data'])) {
  379. $html = "";
  380. $html .= "<div class='mass-sms-batch-results'>";
  381. $message = $_SESSION['mass_sms_batch_data']['message'];
  382. if (strlen($message) > 160) $message = substr($message, 0, 160) . "...<em>" . t("trimmed") . "</em>";
  383. $message = nl2br($message);
  384. $last_timestamp = $_SESSION["mass_sms_batch_data"]["last_timestamp"];
  385. $last_batch_id = $_SESSION["mass_sms_batch_data"]["batch_id"];
  386. $html .= t("The last mass batch of txt messages were sent on %time.", array('%time' => format_date(convert_time($last_timestamp)))) . "<br><br>
  387. " . t("The message sent was:") . "
  388. <div class='mass-msg-sent-trunc'>$message</div>";
  389. $html .= "<p>" . t("You may download a CSV of results relating to this session for the next hour by clicking the following link (valid for 1 hour):") . "</p>";
  390. $html .= "<p>" . l(t("Download CSV"), "stats/download-csv-from-batch/$last_batch_id", "filename=" . urlencode("mass_sms_results__" . $last_timestamp), array('class' => 'button')) . "
  391. " . t("Note: Row 2 of this CSV shows the actual message that was sent. It is not repeated per-row, to save space.") . "</p>";
  392. $html .= "</div>";
  393. $form['mark_last_batch'] = array(
  394. 'type' => 'markup',
  395. 'value' => $html,
  396. );
  397. }
  398. $form['from_number'] = array(
  399. 'label' => t("Select which mass-text phone number this will be coming from:"),
  400. 'type' => 'select',
  401. 'options' => engagements_get_from_phones_for_fapi(FALSE, $user, TRUE),
  402. 'required' => TRUE,
  403. 'hide_please_select' => TRUE,
  404. );
  405. $form['message'] = array(
  406. 'type' => 'textarea',
  407. 'label' => t('Your message:'),
  408. 'maxlength' => 1600,
  409. 'required' => TRUE,
  410. 'description' => t("To insert emoji in Windows, press <span style='padding-left: 10px; padding-right: 10px; font-size: 1.3em;'><i class='fa fa-windows'></i> + .</span> <em>(Windows key + period)</em>
  411. <br><br>
  412. Keep in mind that pricing is based on 160-chars = 1 message segment.
  413. <br><b>Important:</b> Your message <u>will NOT</u> have any
  414. header attached. It would be a good idea to begin by stating what unit/department/school you are sending from.
  415. <br>&nbsp; &nbsp; Ex: &nbsp; <em>From the UFP President's Office: Welcome students to the new term...</em>
  416. <br><b>Important:</b> Your message <u>WILL automatically</u> have a message appended letting the user know they can reply with STOP to opt-out of receiving text messages."),
  417. );
  418. $form['recipients'] = array(
  419. 'type' => 'textarea',
  420. 'label' => t('Recipients:'),
  421. 'rows' => 20,
  422. 'required' => TRUE,
  423. 'description' => t("List the recipients you wish to send your message, one per line. You may enter a valid phone number, CWID, or email address.
  424. The mobile phone number that is on record for the user with the entered CWID or email address is the one which will be used. Invalid phone numbers
  425. or users missing phone numbers will be skipped, with a report of skipped users displayed after this routine is run.
  426. <br>Ex:
  427. <br> &nbsp; &nbsp; 100556
  428. <br> &nbsp; &nbsp; 100557
  429. <br> &nbsp; &nbsp; 100558
  430. <br> &nbsp; &nbsp; tom431@demo.edu
  431. <br> &nbsp; &nbsp; 555-123-4567 "),
  432. );
  433. $form['password'] = array(
  434. 'label' => t("Enter the Mass SMS password:"),
  435. 'type' => 'password',
  436. 'description' => t("To prevent accidental submissions, please enter the Mass SMS password. This is a word or phrase which is configured on the Engagements SMS settings
  437. screen. If you do not know this password, ask your system administrator."),
  438. );
  439. $form['submit_btn'] = array(
  440. 'type' => 'submit',
  441. 'value' => t("Submit"),
  442. 'spinner' => TRUE,
  443. 'suffix' => "<a href='javascript:history.go(-1);'>" . t("Cancel") . "</a>",
  444. );
  445. return $form;
  446. } // mass_sms_form
  447. function engagements_mass_sms_form_validate($form, $form_state)
  448. {
  449. $values = $form_state['values'];
  450. $pass = $values['password'];
  451. if ($pass !== variable_get('sms_mass_sms_password', 'changeMe')) {
  452. form_error('password', t("Sorry, but you did not enter the correct Mass SMS password. Please try again."));
  453. }
  454. }
  455. /**
  456. * Our submit function. Our main task is simply to set off a batch routine.
  457. */
  458. function engagements_mass_sms_form_submit($form, $form_state)
  459. {
  460. $values = $form_state['values'];
  461. $recipients = trim($values['recipients']);
  462. $message = trim($values['message']);
  463. $from_number = trim($values['from_number']);
  464. // Start a batch operation.
  465. // Okay, set up the batch....
  466. $batch = array(
  467. "operation" => array("engagements_mass_sms_perform_batch_operation", array($recipients, $message, $from_number)),
  468. "title" => t("Sending Mass Text Messages"),
  469. "progress_message" => "Attempting to send to recipient @current of @total",
  470. "display_percent" => TRUE,
  471. );
  472. // Set the batch...
  473. batch_set($batch);
  474. } // ... mass_sms_form_submit
  475. function engagements_mass_sms_perform_batch_operation(&$batch, $recipients, $message, $from_number)
  476. {
  477. // if this is our first time through, let's init our values.
  478. if (!isset($batch["results"]["total"])) {
  479. // Our first time through. Let's start up.
  480. $batch['recipients'] = array();
  481. $batch['not_found'] = array();
  482. $batch['rejected'] = array();
  483. $batch['date_sent_ts'] = time();
  484. $batch['date_sent'] = format_date(convert_time(time()));
  485. $batch['csv'] = "";
  486. $batch['message'] = $message;
  487. $safe_message = str_replace('"', "'", $message);
  488. $num_segments = ceil(strlen($message) / 160);
  489. // Create headers for the csv
  490. $batch['csv'] .= "From Number,To Entered Value,Recipient Mobile Number,Recipient Cwid,Recipient User Type,SMS Status,Message,Num Segments,MessageSid,Date Sent\n";
  491. // Second row states the message, so we don't have to repeat it each row.
  492. $batch['csv'] .= "000,Sending msg:,0,0,0,0,\"$safe_message\",$num_segments,0,0\n";
  493. $batch['csv'] .= "--,--,--,--,--,--,--,--,--,--\n";
  494. $lines = explode("\n", $recipients);
  495. $batch["results"] = array();
  496. $batch["results"]["total"] = count($lines);
  497. $batch["results"]["current"] = 0;
  498. $batch["results"]["finished"] = FALSE;
  499. } // if.... (this is our first time through)
  500. //////////////////////////////////////////////////////////////////////////////
  501. //////////////////////////////////////////////////////////////////////////////
  502. //////////////////////////////////////////////////////////////////////////////
  503. // Actually go through our lines of recipients, try to find their number, and send!
  504. ////////////////////////////////////////////
  505. $current = $batch["results"]["current"];
  506. $total = $batch["results"]["total"];
  507. $limit = 1; // How many numbers to process this time through
  508. $c = 0; // count of records.
  509. $lines = explode("\n", $recipients);
  510. for ($t = $current; $t < count($lines); $t++) {
  511. if ($c >= $limit) {
  512. break;
  513. }
  514. $line = trim($lines[$t]);
  515. if (!$line) {
  516. $c++;
  517. continue;
  518. }
  519. // Whatever this is, let's try to get a valid phone number out of it.
  520. $num = $cwid = $user_type = $user_name = "";
  521. $user_type = 'student';
  522. // First, we were given a CWID?
  523. $test = db_get_user_id_from_cwid($line, 'student');
  524. if (!$test) {
  525. $test = db_get_user_id_from_cwid($line, 'faculty');
  526. $user_type = 'faculty';
  527. }
  528. if (!$test) {
  529. $user_type = '';
  530. // Okay, it was NOT a CWID. Is this an email address?
  531. if (strstr($line, '@')) {
  532. $test = db_get_user_id_from_email($line, 'student');
  533. $user_type = 'student';
  534. if (!$test) {
  535. $test = db_get_user_id_from_email($line, 'faculty');
  536. $user_type = 'faculty';
  537. }
  538. }
  539. }
  540. if (!$test) {
  541. $user_type = '';
  542. // Okay, this was not an email address either. Perhaps this is simply a phone number all by itself?
  543. $num = engagements_convert_to_valid_phone_number($line);
  544. if (!$num) {
  545. // Okay, we are going to have to give up-- this was not a valid number, nor anything else we might try
  546. // to search for.
  547. $batch['not_found'][] = $line;
  548. // add to csv
  549. $batch['csv'] .= "\"$from_number\",\"$line\",\"\",\"\",\"\",\"user/number not found\",\"\",\"\",\"\",\"\"\n";
  550. $c++;
  551. continue;
  552. } else {
  553. // Figure out what user_id this phone number belongs to.
  554. // TODO: This bit of code means it can only be sent to current users, and not non-users. Perhaps that should be a setting?
  555. $test = db_result(db_query("SELECT user_id FROM user_attributes WHERE `name` = 'mobile_phone' AND `value` = ? LIMIT 1", array($num)));
  556. }
  557. }
  558. if ($test) {
  559. $user_type = 'student';
  560. // This means that we found a user_id. Let's get their mobile_phone number from their user attributes.
  561. $num = engagements_convert_to_valid_phone_number(user_get_attribute($test, 'mobile_phone', ''));
  562. $test_account = fp_load_user($test);
  563. if ($test_account && !$test_account->is_student) {
  564. $user_type = 'faculty';
  565. }
  566. // Let's also get their CWID.
  567. $cwid = db_get_cwid_from_user_id($test);
  568. // And let's get their name.
  569. $user_name = $test_account->l_name . ", " . $test_account->f_name;
  570. }
  571. if (!$num || $num == "") {
  572. // Okay, we are going to have to give up-- this was not a valid number
  573. $batch['not_found'][] = $line;
  574. $batch['csv'] .= "\"$from_number\",\"$line\",\"$cwid\",\"$user_type\",\"\",\"user has no valid mobile number\",\"\",\"\",\"\",\"\"\n";
  575. $c++;
  576. continue;
  577. }
  578. // If we made it here, then we have a valid recipient! Let's put it into an array called details.
  579. $details = array(
  580. 'num' => $num,
  581. 'user_id' => $test,
  582. 'cwid' => $cwid,
  583. 'user_type' => $user_type,
  584. 'user_name' => $user_name,
  585. 'line' => $line,
  586. );
  587. // Has the user opted-out of receiving our txt messages?
  588. if (!engagements_can_send_sms_to_number($num)) {
  589. $batch['opt_out'][] = $line;
  590. $batch['csv'] .= "\"$from_number\",\"$line\",\"$cwid\",\"$user_type\",\"\",\"user opted-out of receiving text\",\"\",\"\",\"\",\"\"\n";
  591. $c++;
  592. continue;
  593. }
  594. // Send our message to this number, record it as an engagement as well.
  595. $num_segments = ceil(strlen($message) / 160);
  596. $status = "sent";
  597. $external_msg_id = engagements_send_sms_to_number($num, $message, $details['cwid'], $from_number);
  598. if (!$external_msg_id) {
  599. $batch['rejected'][] = $num;
  600. $status = "rejected";
  601. } else {
  602. // We can create an engagement for this SMS. We will make use of our engagements_handle_incoming_sms function.
  603. $vars = array(
  604. 'MessageSid' => $external_msg_id,
  605. 'From' => $from_number,
  606. 'To' => $num,
  607. 'Body' => $message,
  608. 'NumMedia' => 0,
  609. 'NumSegements' => $num_segments,
  610. 'direction' => 'outbound-api',
  611. 'date_sent_ts' => $batch['date_sent_ts'],
  612. );
  613. engagements_handle_incoming_sms($vars);
  614. }
  615. $safe_message_sid = (string) $external_msg_id;
  616. $batch['csv'] .= "\"$from_number\",\"{$details['line']}\",\"$num\",\"{$details['cwid']}\",\"{$details['user_type']}\",\"$status\",\"\",\"$num_segments\",\"$safe_message_sid\",\"{$batch['date_sent']}\"\n";
  617. $c++;
  618. } // foreach
  619. // Update our $batch results variables
  620. $batch["results"]["current"] = $current + $c;
  621. if ($batch["results"]["current"] >= $total) {
  622. // We are finished!
  623. $batch["results"]["finished"] = TRUE;
  624. }
  625. // Add the information to the SESSION so we can call on it later.
  626. $_SESSION['mass_sms_batch_data'] = $batch;
  627. $_SESSION["mass_sms_batch_data"]["last_timestamp"] = time();
  628. } // . batch operation
  629. /**
  630. * Configure the imap settings used by Engagements
  631. */
  632. function engagements_imap_settings_form()
  633. {
  634. $form = array();
  635. $form['mark_top'] = array(
  636. 'value' => '<p>Configure the connection to your IMAP server here.</p>',
  637. );
  638. $form['imap_host'] = array(
  639. 'type' => 'textfield',
  640. 'label' => t('Host'),
  641. 'value' => variable_get('imap_host', ''),
  642. 'description' => 'Ex: mail.example.com',
  643. );
  644. $form['imap_port'] = array(
  645. 'type' => 'textfield',
  646. 'label' => t('Port'),
  647. 'value' => variable_get('imap_port', ''),
  648. 'size' => 10,
  649. 'description' => 'Ex: 143, 993',
  650. );
  651. $form['imap_secure'] = array(
  652. 'type' => 'select',
  653. 'label' => t('Security'),
  654. 'value' => variable_get('imap_secure', ''),
  655. 'options' => array('none' => 'none', 'tls' => 'TLS', 'ssl' => 'SSL'),
  656. 'description' => 'Ex: TLS',
  657. );
  658. $form['imap_username'] = array(
  659. 'type' => 'textfield',
  660. 'label' => t('Username'),
  661. 'value' => variable_get('imap_username', ''),
  662. 'description' => '',
  663. );
  664. $form['imap_password'] = array(
  665. 'type' => 'textfield',
  666. 'label' => t('Password'),
  667. 'value' => variable_get('imap_password', ''),
  668. 'description' => t('Note: The password will be stored within the database, in the Variables table.
  669. Make sure you take the necessary precautions to ensure the security of your database.'),
  670. );
  671. // TODO: folder names to look in?
  672. // TODO: other settings on deleting messages once read, etc?
  673. return $form;
  674. } // engagements_imap_settings_form
  675. /**
  676. * Configure the various SMS settings
  677. */
  678. function engagements_sms_settings_form()
  679. {
  680. $form = array();
  681. $form['mark_signalwire_top'] = array(
  682. 'value' => "<fieldset><legend>Settings for SignalWire</legend>
  683. <p>The following settings are for the service SignalWire. You must already have a SignalWire account
  684. set up to use.</p>",
  685. );
  686. $form['sms_project_id'] = array(
  687. 'label' => 'Project ID',
  688. 'type' => 'textfield',
  689. 'value' => variable_get('sms_project_id', ''),
  690. 'description' => t("This is the project id (from SignalWire)."),
  691. );
  692. $form['sms_auth_token'] = array(
  693. 'label' => 'Auth Token',
  694. 'type' => 'textfield',
  695. 'value' => variable_get('sms_auth_token', ''),
  696. 'description' => t("Given by the API settings in signalwire"),
  697. );
  698. $form['sms_space_url'] = array(
  699. 'label' => 'Space URL',
  700. 'type' => 'textfield',
  701. 'value' => variable_get('sms_space_url', ''),
  702. 'description' => t("The URL you use to access your region of signalwire. example.signalwire.com. No http:// or such."),
  703. );
  704. $form['sms_from_phone'] = array(
  705. 'label' => 'From Phone number(s):',
  706. 'type' => 'textarea',
  707. 'rows' => 3,
  708. 'value' => variable_get('sms_from_phone', '555-555-1234'),
  709. 'description' => t("These are the phone numbers from your SignalWire account.
  710. <br>Enter them one per line, with a tilda (~) between the number
  711. and a brief line description, like so:
  712. <br> &nbsp; &nbsp; 555-123-4567 ~ Brief Description ~ default
  713. <br> &nbsp; &nbsp; 555-222-1111 ~ Brief Description2
  714. <br>Notice that the 'default' designation lets FlightPath know what is the default number which standard notifications
  715. will be sent from. If no default is set, the first number will be used.
  716. <br><strong>Important:</strong> Do not send mass txt messages from these numbers unless your campaign is set to do so. These
  717. numbers will not appear as options to send mass txt messages."),
  718. );
  719. $form['sms_mass_phone'] = array(
  720. 'label' => 'Mass Text Phone number(s):',
  721. 'type' => 'textarea',
  722. 'rows' => 3,
  723. 'value' => variable_get('sms_mass_phone', '800-555-1234'),
  724. 'description' => t("These are phone numbers from your SignalWire account, specifically belonging to a campaign for marketting or otherwise
  725. mass text messages. At the time of this writing, toll-free numbers do not need to belong to such a campaign. These
  726. will be the only numbers which appear as options to send mass txt messages to."),
  727. );
  728. $form['sms_header'] = array(
  729. 'label' => 'SMS Header:',
  730. 'type' => 'textfield',
  731. 'value' => variable_get('sms_header', '(@initials) from @name:'),
  732. 'description' => t("All SMS text messages sent from FlightPath will begin with this header, in order to help the student
  733. understand the who the sender of the message is.
  734. <br>You may leave blank to disable (not recommended),
  735. or use the following replacement patterns:
  736. <ul>
  737. <li><b>@initials</b> - The sender's school initials. Ex: ULM or UD</li>
  738. <li><b>@school</b> - The sender's school name. Ex: University of Idaho (this is only applicable if you have the Schools module enabled.)</li>
  739. <li><b>@name</b> - The first and last name of the sending user OR for automated messages, it will simply say FlightPath (or whatever your system has been named).</li>
  740. <li><b>@line-desc</b> - This is the line description for the number the message is being sent from.</li>
  741. <li><b>@line-desc-trimmed</b> - This is a trimmed (to no more than 20 chars) of the line description for the number the message is being sent from.
  742. </ul>"),
  743. );
  744. $form['sms_mass_sms_password'] = array(
  745. 'label' => 'Mass SMS Password:',
  746. 'type' => 'textfield',
  747. 'value' => variable_get('sms_mass_sms_password', 'changeMe'),
  748. 'description' => t("Before a user may use the Send Mass Text Messages feature (to sent o many recipients), they must enter this password to protect against accidental submissions."),
  749. );
  750. $form['mark_signalwire_bottom'] = array(
  751. 'value' => "</fieldset><br><br>",
  752. );
  753. return $form;
  754. } // engagements_sms_settings_form
  755. function engagements_sms_replace_patterns($str, $from_mobile = "", $account = NULL)
  756. {
  757. global $user;
  758. if ($account == NULL) $account = $user;
  759. $school_id = 0;
  760. $school_name = "";
  761. if (function_exists('schools_menu')) {
  762. $school_id = schools_get_school_id_for_user($account->id);
  763. $school_name = schools_get_school_name_for_id($school_id);
  764. }
  765. $name = $account->f_name . " " . $account->l_name;
  766. $line_desc = $line_desc_trimmed = t("Num/Dept Description");
  767. $phones = engagements_get_from_phones();
  768. if ($from_mobile == "") $from_mobile = $phones['default']['num']; // use default if it was left blank
  769. if ($from_mobile) {
  770. $line_desc = @$phones['lines'][$from_mobile]['description'];
  771. $line_desc_trimmed = substr($line_desc, 0, 20);
  772. }
  773. $str = str_ireplace("@initials", variable_get_for_school('school_initials', '', $school_id), $str);
  774. $str = str_ireplace("@school", $school_name, $str);
  775. $str = str_ireplace("@name", $name, $str);
  776. $str = str_ireplace("@line-desc-trimmed", $line_desc_trimmed, $str);
  777. $str = str_ireplace("@line-desc", $line_desc, $str);
  778. return $str;
  779. } // ...sms_replace_patterns
  780. /**
  781. * Get the available "from phone" numbers in an organized array structure.
  782. */
  783. function engagements_get_from_phones($bool_mass_text_numbers = FALSE)
  784. {
  785. $rtn = array();
  786. $val = variable_get('sms_from_phone', '555-555-1234');
  787. if ($bool_mass_text_numbers) {
  788. $val = variable_get('sms_mass_phone', '800-555-1234');
  789. }
  790. $bool_default_set = FALSE;
  791. $first_num = "";
  792. $lines = explode("\n", $val);
  793. foreach ($lines as $line) {
  794. $line = trim($line);
  795. if ($line == "") continue;
  796. $temp = explode("~", $line);
  797. $num = engagements_convert_to_valid_phone_number(trim($temp[0]));
  798. $desc = @trim($temp[1]);
  799. $default = @trim(strtolower($temp[2]));
  800. if ($desc == '') $desc = $num;
  801. if ($first_num == '') $first_num = $num;
  802. $rtn['lines'][$num] = array(
  803. 'num' => $num,
  804. 'description' => $desc,
  805. 'default' => $default,
  806. );
  807. if ($default == 'default') {
  808. $rtn['default'] = $rtn['lines'][$num];
  809. $bool_default_set = TRUE;
  810. }
  811. }
  812. // Default not set, so use the first number.
  813. if (!$bool_default_set) {
  814. $rtn['default'] = $rtn['lines'][$first_num];
  815. }
  816. return $rtn;
  817. }
  818. function engagements_get_sms_from_history($message_sid)
  819. {
  820. $res = db_query("SELECT * FROM sms_history WHERE message_sid = ? ORDER BY mid DESC LIMIT 1", array($message_sid));
  821. $cur = db_fetch_array($res);
  822. if ($cur) return $cur;
  823. return FALSE;
  824. }
  825. /**
  826. * Implements hook_cron
  827. *
  828. * One of the things we want to do is check on all of our SMS messages.
  829. *
  830. * We will also want to check our imap messages
  831. *
  832. */
  833. function engagements_cron()
  834. {
  835. $last_sms_check = intval(variable_get('engagements_sms_get_all_last_check', 0));
  836. $last_imap_check = variable_get('engagements_imap_get_all_last_check', 0);
  837. // only check every X minutes at most. Maybe this is a setting?
  838. $sms_check_against = strtotime("NOW - 30 MINUTES");
  839. if ($sms_check_against >= $last_imap_check) {
  840. engagements_sms_get_all_messages(); // refreshes sms information for messages we've sent and received
  841. }
  842. // Check for imap messages in a similar way as the SMS messages.
  843. engagements_imap_get_all_received_messages();
  844. } // hook_cron
  845. /**
  846. * This catches incoming sms messages from POST, but can also be used by our "sms_get_all_messages" function, but it is also used by the
  847. * sms_get_all_messages to save/update information.
  848. *
  849. * TODO: Have a way to handle incoming calls, too, so we can charge correctly.
  850. *
  851. */
  852. function engagements_handle_incoming_sms($vars = array())
  853. {
  854. // If we did not supply a set of variables, use the POST superglobal
  855. if (count($vars) === 0) $vars = $_POST;
  856. watchdog("engagements_sms", "Received from vars (possibly POST): <pre>" . print_r($vars, TRUE) . '</pre>', array(), WATCHDOG_DEBUG);
  857. $message_sid = $vars['MessageSid'];
  858. $from_mobile = engagements_convert_to_valid_phone_number($vars['From']);
  859. $to_mobile = engagements_convert_to_valid_phone_number($vars['To']);
  860. $system_name = variable_get("system_name", "FlightPath");
  861. $body = $vars['Body'];
  862. $num_media = intval($vars['NumMedia']);
  863. $num_segments = intval($vars['NumSegments']);
  864. $price = 0; // We know this is an SMS, so this is the price per segment inbound (SignalWire does not charge us for basic SMS inbound).
  865. if ($num_media > 0) {
  866. $price = 0.01; // Contained at least one piece of media, so price increases. This is SignalWire's price.
  867. }
  868. $direction = "inbound";
  869. $fp_price = engagements_get_fp_price($price, $direction, $num_segments);
  870. $date_sent_ts = time(); // the time we received it.
  871. if (isset($vars['date_sent_ts'])) {
  872. $date_sent_ts = $vars['date_sent_ts'];
  873. }
  874. if (isset($vars['direction'])) {
  875. $direction = $vars['direction'];
  876. }
  877. $content = NULL;
  878. $link = $alert_link = "";
  879. // Figure out what user this came from/to!
  880. $from_cwid = $from_user_id = $to_user_id = "";
  881. $user_id = db_result(db_query("SELECT user_id FROM user_attributes WHERE `name` = 'mobile_phone' AND `value` = ?", array($from_mobile)));
  882. if ($user_id) {
  883. $from_cwid = db_get_cwid_from_user_id($user_id);
  884. $from_user_id = $user_id;
  885. }
  886. $to_cwid = "";
  887. $user_id = db_result(db_query("SELECT user_id FROM user_attributes WHERE `name` = 'mobile_phone' AND `value` = ?", array($to_mobile)));
  888. if ($user_id) {
  889. $to_cwid = db_get_cwid_from_user_id($user_id);
  890. $to_user_id = $user_id;
  891. }
  892. $dir = "received";
  893. $fac_cwid = $to_cwid;
  894. $stu_cwid = $from_cwid;
  895. if (strstr($direction, 'outbound')) {
  896. $dir = 'sent';
  897. $fac_cwid = $from_cwid;
  898. $stu_cwid = $to_cwid;
  899. }
  900. // Did we receive STOP?
  901. if ($dir == 'received') {
  902. if (trim(strtoupper($body)) === 'STOP') {
  903. engagements_handle_sms_stop($from_user_id, $from_cwid, $from_mobile, $to_mobile);
  904. }
  905. // UNSTOP or SUBSCRIBE?
  906. if (trim(strtoupper($body)) === 'UNSTOP' || trim(strtoupper($body)) === 'SUBSCRIBE') {
  907. engagements_handle_sms_unstop($from_user_id, $from_cwid, $from_mobile, $to_mobile);
  908. }
  909. // We do not return. Go ahead and let it create an engagement below, so the
  910. // advisor can see that this happened.
  911. }
  912. if (trim($stu_cwid) != "") {
  913. // create an engagement content node for this student!
  914. $content = new stdClass();
  915. $content->type = 'engagement';
  916. $content->cid = "new";
  917. $content->published = 1;
  918. $content->delete_flag = 0;
  919. $content->title = "Engagement: txt_msg re: student $stu_cwid"; // required
  920. $content->field__activity_datetime['value'] = date('Y-m-d H:i:s', $date_sent_ts);
  921. $content->field__faculty_id['value'] = $fac_cwid;
  922. $content->field__student_id['value'] = $stu_cwid;
  923. $content->field__engagement_type['value'] = 'txt_msg';
  924. $content->field__direction['value'] = $dir;
  925. $content->field__from_sms_phone['value'] = $from_mobile;
  926. $content->field__to_sms_phone['value'] = $to_mobile;
  927. content_save($content);
  928. $cid = $content->cid;
  929. $media_filenames = "";
  930. // Handle retreiving and saving media.
  931. if ($num_media > 0) { // Do we have any media?
  932. for ($t = 0; $t < $num_media; $t++) {
  933. $media_url = $vars["MediaUrl$t"];
  934. $media_type = $vars["MediaContentType$t"];
  935. $ext = engagements_get_file_extension_from_mime_type($media_type);
  936. if (!$ext || $ext == "html" || $ext == "htm") continue; // skip if its not a type we care about.
  937. // Create a new random filename to save it as.
  938. $filename = 'sms__' . $from_mobile . '_' . time() . '_' . mt_rand(1, 9999) . '.' . $ext;
  939. $tmp_name = sha1($filename . mt_rand(1,9999) . time() . mt_rand(1,9999));
  940. $file_contents = file_get_contents($media_url);
  941. // Save to our temporary location on the server.
  942. $test = file_put_contents(sys_get_temp_dir() . '/' . $tmp_name, $file_contents);
  943. if (!$test) {
  944. watchdog('engagements_sms', 'Unable to write media file at temp location: ' . sys_get_temp_dir(), array(), 'error');
  945. fpm('Unable to write media file at temp location:' . sys_get_temp_dir() . '. Permissions issue?');
  946. continue;
  947. }
  948. $file = array(
  949. 'name' => $filename,
  950. 'type' => $media_type,
  951. 'tmp_name' => $tmp_name,
  952. );
  953. // This function will handle encryption of files automatically.
  954. $fid = content_add_new_uploaded_file($file, $cid, FALSE);
  955. $media_filenames .= $filename . ',';
  956. } // for t (media)
  957. $media_filenames = rtrim($media_filenames, ',');
  958. } // if num media > 0
  959. // if we have media, add to the end of body.
  960. if ($media_filenames) {
  961. $body .= "~~MEDIA~~$media_filenames~~END_MEDIA~~";
  962. }
  963. $content->field__engagement_msg['value'] = $body;
  964. $content->field__external_msg_id['value'] = $message_sid;
  965. $content->field__visibility['value'] = 'public';
  966. content_save($content);
  967. } // We have a student to attach this to.
  968. if ($dir != "sent") { // meaning, we RECEIVED this message.
  969. $already_notified_users = array();
  970. $student_name = fp_get_student_name($stu_cwid, TRUE);
  971. $base_url = $GLOBALS['fp_system_settings']['base_url'];
  972. $link = $base_url . "/engagements?current_student_id=$stu_cwid";
  973. $phones = engagements_get_from_phones();
  974. $desc = "";
  975. $to = $to_mobile;
  976. $pretty_to_number = engagements_convert_to_pretty_phone_number($to);
  977. $desc = @$phones['lines'][$to]['description'];
  978. if (!$desc) $desc = @$phones['lines'][$from_mobile]['description'];
  979. if ($desc) $desc = " $desc";
  980. if (trim($stu_cwid) != "") {
  981. // Create a new "activity record" that the student has sent a txt message
  982. $acontent = new stdClass();
  983. $acontent->type = 'activity_record';
  984. $acontent->cid = "new";
  985. $acontent->published = 1;
  986. $acontent->delete_flag = 0;
  987. $acontent->title = t('Student sent a text message to ') . $pretty_to_number . " / " . $desc;
  988. $acontent->field__student_id['value'] = $stu_cwid;
  989. $acontent->field__activity_type['value'] = 'comment';
  990. content_save($acontent);
  991. // Notify the advisor(s).
  992. $tmsg = "$student_name has submitted a text message to $system_name
  993. (To: $pretty_to_number / $desc).
  994. <br><br>\n\n
  995. To view, visit the student's Engagements tab by logging in and following this link:<br>\n
  996. <a href='$link'>$link</a>";
  997. $advisors = advise_get_advisors_for_student($stu_cwid);
  998. foreach ($advisors as $c => $afaculty_id) {
  999. $account_user_id = db_get_user_id_from_cwid($afaculty_id, 'faculty');
  1000. if ($account_user_id) {
  1001. notify_send_notification_to_user($account_user_id, $tmsg, $content->cid, 'engagement');
  1002. $already_notified_users[] = $account_user_id;
  1003. }
  1004. } // foreach advisors
  1005. }
  1006. /*
  1007. * TODO: Only do this if we have a setting set?
  1008. * TODO: Note: don't send the full message, but instead send link to an alert that we create for the recipient? We'd have to do this in the foreach loop below.
  1009. *
  1010. if (trim($stu_cwid) == "") {
  1011. // Meaning, we couldn't find this student in our system!
  1012. // TODO: Create an alert for the recipient which does show the full txt message. The notification provides a link to the alert. We'd do this in
  1013. // TODO: the foreach loop below.
  1014. $tmsg .= "A text message has been received for $system_name. (To: $pretty_to_number / $desc).<br><br> No student could be found which is
  1015. associated with the sending number ($from_mobile).<br><br>Message body:<br>---------<br>
  1016. $body<br><br>Message SID:$message_sid.";
  1017. }
  1018. */
  1019. // Notify other users who might have been assigned to receive notifications
  1020. $to_be_notified = engagements_get_users_to_be_notified_for_sms_on_number($to_mobile);
  1021. foreach ($to_be_notified as $account_user_id) {
  1022. if (in_array($account_user_id, $already_notified_users)) continue;
  1023. if (trim($stu_cwid) == "") {
  1024. // Meaning, we couldn't find this student in our system!
  1025. // TODO: Create an alert for the recipient which does show the full txt message. The notification provides a link to the alert.
  1026. $alert = new stdClass();
  1027. $alert->type = 'alert';
  1028. $alert->cid = "new";
  1029. $alert->published = 1;
  1030. $alert->delete_flag = 0;
  1031. $alert->title = t('Unrecognized sender sent a text message to ') . $pretty_to_number . " / " . $desc;
  1032. $alert->field__alert_msg['value'] = $body;
  1033. $alert->field__alert_status['value'] = 'open';
  1034. $alert->field__visibility['value'] = 'faculty';
  1035. $alert->field__target_faculty_id['value'] = db_get_cwid_from_user_id($account_user_id);
  1036. $alert->field__student_id['value'] = "N/A";
  1037. $alert->field__exclude_advisor['value'] = 1;
  1038. content_save($alert);
  1039. $alert_link = $base_url . "/content/$alert->cid?content_crumbs=alerts";
  1040. $tmsg = "A text message has been received for $system_name. (To: $pretty_to_number / $desc).<br><br> No student could be found which is
  1041. associated with the sending number ($from_mobile).<br><br>Message SID: $message_sid.
  1042. <br><br>To view the full message, view the Alert by logging in and clicking here:
  1043. <a href='$alert_link'>$alert_link</a>";
  1044. notify_send_notification_to_user($account_user_id, $tmsg, $alert->cid, 'alert');
  1045. }
  1046. else {
  1047. // We DO have a student this came from. Let this user know normally.
  1048. $tmsg = "$student_name has submitted a text message to $system_name (To: $pretty_to_number / $desc).
  1049. <br><br>\n\n
  1050. To view, visit the student's Engagements tab by logging in and following this link:<br>\n
  1051. <a href='$link'>$link</a>";
  1052. notify_send_notification_to_user($account_user_id, $tmsg, $content->cid, 'engagement');
  1053. }
  1054. } //foreach to_be_notified
  1055. } // dir != sent (we received this text)
  1056. // Write our data to our sms_history table.
  1057. db_query(
  1058. "INSERT INTO sms_history (`message_sid`, sw_type, `body`, `from_number`, `to_number`, `sw_price`, `fp_price`, `from_cwid`, `to_cwid`, `updated`, `direction`,
  1059. `media_filenames`, `date_sent`, price_processed, num_segments, delivery_status, err_code, err_message, err_friendly_message)
  1060. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ",
  1061. array($message_sid, 'sms', $body, $from_mobile, $to_mobile, $price, $fp_price, $from_cwid, $to_cwid, time(), $direction,
  1062. $media_filenames, $date_sent_ts, 1, $num_segments, @$vars['display_status'], @$vars['err_code'], @$vars['err_message'], @$vars['err_friendly_message'])
  1063. );
  1064. } // engagements_handle_incoming_sms
  1065. /**
  1066. * This function is called by engagements_handle_incoming_sms, when we receive 'STOP' from the user.
  1067. * We must add them to our "sms_do_not_txt" table, and send a reply letting them know how to re-subscribe in the future.
  1068. */
  1069. function engagements_handle_sms_stop($user_id = 0, $cwid = '', $phone_number = '', $from_fp_phone_number = '') {
  1070. if ($user_id === 0) {
  1071. // Try to figure out what the user_id is based on the phone number.
  1072. $test = db_result(db_query("SELECT user_id FROM user_attributes WHERE name = 'mobile_phone' AND value = ?", array($from_fp_phone_number)));
  1073. $test = intval($test);
  1074. if ($test > 0) $user_id = $test;
  1075. }
  1076. $phone_number = engagements_convert_to_valid_phone_number($phone_number);
  1077. if (!$phone_number) {
  1078. // watchdog that there was an error because phone number not valid.
  1079. watchdog('engagements_sms', "User tried to opt-out of sms, but invalid phone number: $user_id, $cwid, $phone_number", array(), WATCHDOG_ERROR);
  1080. return FALSE;
  1081. }
  1082. if ($user_id) {
  1083. // Get current notification method for this user.
  1084. $prev_notification_method = user_get_setting($user_id, 'default_notification_method', 'email');
  1085. // Set their new defailt_notification_method to just email.
  1086. user_set_setting($user_id, 'default_notification_method', 'email');
  1087. // also set an opt-out setting value here, so the user can change it from within flightpath.
  1088. user_set_setting($user_id, "sms_opt_out__" . $phone_number, 'yes');
  1089. }
  1090. // Save everything to do_not_txt table. (First, delete anything already there for this phone_number, which is the most important to keep track of)
  1091. db_query('DELETE FROM sms_do_not_txt WHERE phone_number = ?', array($phone_number));
  1092. db_query("INSERT INTO sms_do_not_txt (user_id, cwid, phone_number, prev_notification_method, updated)
  1093. VALUES (?, ?, ?, ?, ?)", array($user_id, $cwid, $phone_number, $prev_notification_method, time()));
  1094. // Send final sms back to number telling them how to re-enable.
  1095. $msg = t("You have opted-out of receiving text messages from @flightpath. To re-subscribe, text UNSTOP or SUBSCRIBE.\n\nYou may also log into @flightpath and edit your notification settings.",
  1096. array("@flightpath" => variable_get("system_name", "FlightPath")));
  1097. engagements_send_sms_to_number($phone_number, $msg, '', $from_fp_phone_number, FALSE);
  1098. watchdog('engagements_sms', "User opted-out of sms: $user_id, $cwid, $phone_number");
  1099. } // engagements_handle_sms_stop
  1100. /**
  1101. * User opted-IN to receiving txt messages.
  1102. */
  1103. function engagements_handle_sms_unstop($user_id = 0, $cwid = '', $phone_number = '', $from_fp_phone_number = '') {
  1104. if ($user_id === 0) {
  1105. // Try to figure out what the user_id is based on the phone number.
  1106. $test = db_result(db_query("SELECT user_id FROM user_attributes WHERE name = 'mobile_phone' AND value = ?", array($from_fp_phone_number)));
  1107. $test = intval($test);
  1108. if ($test > 0) $user_id = $test;
  1109. }
  1110. $phone_number = engagements_convert_to_valid_phone_number($phone_number);
  1111. if (!$phone_number) {
  1112. // watchdog that there was an error because phone number not valid.
  1113. watchdog('engagements_sms', "User tried to opt-in to sms, but invalid phone number: $user_id, $cwid, $phone_number", array(), WATCHDOG_ERROR);
  1114. return FALSE;
  1115. }
  1116. if ($user_id) {
  1117. // Get current notification method for this user.
  1118. $notification_method = db_result(db_query("SELECT prev_notification_method FROM sms_do_not_txt WHERE phone_number = ?", array($phone_number)));
  1119. if ($notification_method) {
  1120. // Set their new defailt_notification_method to just email.
  1121. user_set_setting($user_id, 'default_notification_method', $notification_method);
  1122. }
  1123. // also set our opt-out setting value here, so the user can change it from within flightpath.
  1124. user_set_setting($user_id, "sms_opt_out__" . $phone_number, 'no');
  1125. }
  1126. // Save everything to do_not_txt table. (First, delete anything already there for this phone_number, which is the most important to keep track of)
  1127. db_query('DELETE FROM sms_do_not_txt WHERE phone_number = ?', array($phone_number));
  1128. // Send final sms back to number telling them how to re-enable.
  1129. $msg = t("You have opted-in to receiving text messages from @flightpath.\nReply STOP to opt-out.\n\nYou may also log into @flightpath and edit your notification settings.",
  1130. array("@flightpath" => variable_get("system_name", "FlightPath")));
  1131. engagements_send_sms_to_number($phone_number, $msg, '', $from_fp_phone_number, FALSE);
  1132. watchdog('engagements_sms', "User opted-out of sms: $user_id, $cwid, $phone_number");
  1133. }
  1134. function engagements_can_send_sms_to_number($phone_number) {
  1135. $phone_number = engagements_convert_to_valid_phone_number($phone_number);
  1136. if (!$phone_number) return FALSE;
  1137. $mid = db_result(db_query("SELECT mid FROM sms_do_not_txt WHERE phone_number = ?", array($phone_number)));
  1138. // We found an entry in this table, so NO, we cannot txt this number.
  1139. if ($mid) return FALSE;
  1140. // else
  1141. return TRUE;
  1142. }
  1143. /**
  1144. *
  1145. */
  1146. function engagements_get_signalwire_sms_error_codes_array() {
  1147. $rtn = array(
  1148. "10002" => "Trial account does not support this feature",
  1149. "11200" => "HTTP Retrieval Failure",
  1150. "11751" => "MMS -> Media exceeds mobile operator size limit",
  1151. "12100" => "Document Parse Failure",
  1152. "12300" => "Invalid Content-Type",
  1153. "13221" => "Dial -> Number: Invalid Method Value",
  1154. "15002" => "Call Progress: Queue Timeout",
  1155. "21210" => "'From' phone number not verified",
  1156. "21217" => "A call or SMS is placed to a destination which does not represent a valid phone number",
  1157. "21219" => "'To' phone number not verified",
  1158. "21601" => "Phone number is not a valid SMS-capable inbound phone number",
  1159. "21602" => "Message body is required",
  1160. "21603" => "The source 'From' phone number is required to send an SMS",
  1161. "21604" => "The destination 'To' phone number is required to send an SMS",
  1162. "21610" => "Attempt to send to unsubscribed recipient",
  1163. "21611" => "This 'From' number has exceeded the maximum number of queued messages",
  1164. "21617" => "The concatenated message body exceeds the 1600 character limit",
  1165. "21620" => "Invalid media URL(s)",
  1166. "21623" => "Number of media files exceeds allowed limit",
  1167. "30002" => "Account suspended",
  1168. "30003" => "Unreachable destination handset",
  1169. "30004" => "Message blocked or opted out",
  1170. "30005" => "Unknown destination handset",
  1171. "30006" => "Landline or unreachable carrier",
  1172. "30007" => "Message marked as spam",
  1173. "30008" => "Unknown error",
  1174. "30010" => "Message price exceeds max price",
  1175. "30022" => "Campaign Registry Throughput Limit Exceeded",
  1176. );
  1177. return $rtn;
  1178. }
  1179. /**
  1180. * Retrieve all messages, update the ones which don't have prices associated with them yet.
  1181. *
  1182. * We will also be getting voice calls & prices too for our log table.
  1183. */
  1184. function engagements_sms_get_all_messages()
  1185. {
  1186. require_once(fp_get_module_path('engagements', TRUE, FALSE) . '/lib/signalwire/vendor/autoload.php');
  1187. $vars = array();
  1188. $error_codes = engagements_get_signalwire_sms_error_codes_array();
  1189. // project id, auth tolen, space url
  1190. $project_id = variable_get('sms_project_id', '');
  1191. if ($project_id == "") {
  1192. return FALSE;
  1193. }
  1194. $auth_token = variable_get('sms_auth_token', '');
  1195. $space_url = variable_get('sms_space_url', '');
  1196. $client = new SignalWire\Rest\Client($project_id, $auth_token, array("signalwireSpaceUrl" => $space_url));
  1197. // Get all messages so we can update our table.
  1198. // We only care about messages sent TODAY, since we theoretically already got them yesterday
  1199. $check_since = date('Y-m-d', strtotime("NOW - 3 HOURS")); // We will get messages from 3 hours in the past and today,
  1200. // (in case it's close to ***midnight***)
  1201. // to make sure we don't miss anything. The idea being that we have a cron
  1202. // running frequently enough that this gives us lots of chances to retrieve information.
  1203. $messages = $client->messages->read(array('dateSentAfter' => $check_since));
  1204. foreach ($messages as $record) {
  1205. //fpm(ppm($record, TRUE));
  1206. // fpm($record->status);
  1207. watchdog('engagements_debug', "In engagements_sms_get_all_messages(). Reviewing message: <pre>" . print_r($record, TRUE) . "</pre>", array(), WATCHDOG_DEBUG);
  1208. $status = strtolower(trim($record->status));
  1209. $error_message = $record->errorMessage;
  1210. $error_code = intval($record->errorCode);
  1211. if ($error_code == 12100 || $error_code == 11200) {
  1212. // We don't care about these code, as they appear on EVERY message.
  1213. $error_code = NULL;
  1214. $status = NULL;
  1215. }
  1216. if ($error_code) {
  1217. $friendly_error_message = @$error_codes[$error_code];
  1218. }
  1219. $message_sid = $record->sid;
  1220. $vars['MessageSid'] = $message_sid;
  1221. // Should we update any details about this message? Price, errors, etc?
  1222. $test = engagements_get_sms_from_history($message_sid);
  1223. if ($test && (floatval($record->price) != floatval($test['sw_price']) || trim($error_code) != trim($test['err_code']))) {
  1224. // update prices
  1225. $price = $record->price;
  1226. //$fp_price = engagements_get_fp_price($price, $test['direction'], intval($test['num_segments']));
  1227. $fp_price = 0; // no longer using this field.
  1228. watchdog('engagements_debug', "In engagements_sms_get_all_messages(). Updating sms_history table.", array(), WATCHDOG_DEBUG);
  1229. db_query("UPDATE sms_history
  1230. SET sw_price = ?, fp_price = ?, price_processed = 1, delivery_status = ?, err_code = ?, err_message = ?, err_friendly_message = ?
  1231. WHERE mid = ?", array($price, $fp_price, $status, $error_code, $error_message, $friendly_error_message, $test['mid']));
  1232. // Meaning, we already have this one saved, and now that we've updated it, we can continue.
  1233. continue;
  1234. } // if test and we have things to update
  1235. else if ($test) {
  1236. // meaning, we already have this one saved. Skip it.
  1237. continue;
  1238. }
  1239. $from_mobile = engagements_convert_to_valid_phone_number($record->from);
  1240. $to_mobile = engagements_convert_to_valid_phone_number($record->to);
  1241. $body = $record->body;
  1242. $date_sent = $record->dateSent; // this is a Datetime object
  1243. $date_sent_ts = strtotime($date_sent->format("Y-m-d H:i:s"));
  1244. $direction = $record->direction;
  1245. $num_segments = intval($record->numSegments);
  1246. $vars['From'] = $from_mobile;
  1247. $vars['To'] = $to_mobile;
  1248. $vars['Body'] = $body;
  1249. $vars['date_sent_ts'] = $date_sent_ts;
  1250. $vars['direction'] = $direction;
  1251. $vars['NumSegements'] = $num_segments;
  1252. $vars['delivery_status'] = $status;
  1253. $vars['err_code'] = $error_code;
  1254. $vars['err_message'] = $error_message;
  1255. $vars['err_friendly_message'] = $friendly_error_message;
  1256. $vars['NumMedia'] = 0;
  1257. // Is there media?
  1258. if (intval($record->numMedia) > 0) {
  1259. $vars['NumMedia'] = intval($record->numMedia);
  1260. // List all media for this message.
  1261. $allmedia = $client->messages($message_sid)->media->read();
  1262. $c = 0;
  1263. foreach ($allmedia as $mitem) {
  1264. $media_sid = $mitem->sid;
  1265. $content_type = $mitem->contentType;
  1266. $vars['MediaContentType' . $c] = $content_type;
  1267. $vars['MediaUrl' . $c] = "https://$space_url/api/laml/2010-04-01/Accounts/$project_id/Messages/$message_sid/Media/$media_sid";
  1268. $c++;
  1269. } // foreach media
  1270. } // there was media!
  1271. watchdog('engagements_debug', "In engagements_sms_get_all_messages(). Sending to engagements_handle_incoming_sms: <pre>" . print_r($vars, TRUE) . "</pre>", array(), WATCHDOG_DEBUG);
  1272. // Save this message using our "handle_incoming_sms" function.
  1273. engagements_handle_incoming_sms($vars);
  1274. } // foreach messages
  1275. //////////////////////////////////////////////////////////////
  1276. $body = "";
  1277. // Now, we also want to get a list of all calls and their prices too.
  1278. $calls = $client->calls->read(array('startTimeAfter', $check_since));
  1279. foreach ($calls as $record) {
  1280. watchdog('engagements_debug', "In engagements_sms_get_all_messages(). Reviewing CALL: <pre>" . print_r($record, TRUE) . "</pre>", array(), WATCHDOG_DEBUG);
  1281. $sid = $record->sid;
  1282. // Do we already have this in our table? If so, skip.
  1283. $test = engagements_get_sms_from_history($sid);
  1284. if ($test) continue; // We've already recorded this, so skip.
  1285. $date_sent = $record->startTime; // this is a Datetime object
  1286. $date_sent_ts = strtotime($date_sent->format("Y-m-d H:i:s"));
  1287. $to_mobile = engagements_convert_to_valid_phone_number($record->to);
  1288. $from_mobile = engagements_convert_to_valid_phone_number($record->from);
  1289. // Figure out what user this came from/to!
  1290. $from_cwid = "";
  1291. $user_id = db_result(db_query("SELECT user_id FROM user_attributes WHERE `name` = 'mobile_phone' AND `value` = ?", array($from_mobile)));
  1292. if ($user_id) {
  1293. $from_cwid = db_get_cwid_from_user_id($user_id);
  1294. }
  1295. $to_cwid = "";
  1296. $user_id = db_result(db_query("SELECT user_id FROM user_attributes WHERE `name` = 'mobile_phone' AND `value` = ?", array($to_mobile)));
  1297. if ($user_id) {
  1298. $to_cwid = db_get_cwid_from_user_id($user_id);
  1299. }
  1300. $duration = $record->duration;
  1301. $direction = $record->direction;
  1302. $price = $record->price;
  1303. $fp_price = engagements_get_fp_price($price, $direction, 1, 'call');
  1304. // Write our data to our sms_history table.
  1305. db_query(
  1306. "INSERT INTO sms_history (`message_sid`, sw_type, `body`, `from_number`, `to_number`, `sw_price`, `fp_price`, `from_cwid`, `updated`, `direction`,
  1307. `media_filenames`, `date_sent`, price_processed, num_segments)
  1308. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ",
  1309. array($sid, 'call', $body, $from_mobile, $to_mobile, $price, $fp_price, $from_cwid, time(), $direction, '', $date_sent_ts, 1, $duration)
  1310. );
  1311. watchdog("engagements_sms", "Documented call to $to_mobile.", array(), WATCHDOG_DEBUG);
  1312. } //foreach calls as record
  1313. // Record the last time we called this function.
  1314. variable_set('engagements_sms_get_all_last_check', time());
  1315. } // function
  1316. /**
  1317. * figure out the price we will charge.
  1318. */
  1319. function engagements_get_fp_price($price, $direction, $num_segments = 1, $type = 'sms')
  1320. {
  1321. $rtn = 0;
  1322. if ($num_segments == 0) {
  1323. $num_segments = 1;
  1324. }
  1325. if (strstr($direction, 'outbound')) {
  1326. $rtn = 0.0033 * $num_segments;
  1327. }
  1328. if ($price == 0.01) {
  1329. $rtn = 0.015;
  1330. }
  1331. if ($type == 'call') {
  1332. $rtn = 0.005;
  1333. }
  1334. // Guess at 15% over, if we don't know.
  1335. $new_price = $price * 1.15; // generic; add 15%.
  1336. // Return the LARGER of the two prices.
  1337. if ($new_price > $rtn) {
  1338. return $new_price;
  1339. }
  1340. return $rtn;
  1341. }
  1342. // From: https://stackoverflow.com/questions/16511021/convert-mime-type-to-file-extension-php
  1343. /**
  1344. * This function will look at the mime type (aka content type) to figure out what the
  1345. * file extension should be. This is useful when retrieving txt message media.
  1346. *
  1347. * We will only have the ones we think students might actually try to txt to their teachers.
  1348. */
  1349. function engagements_get_file_extension_from_mime_type($mime)
  1350. {
  1351. $mime_map = array(
  1352. 'video/3gpp2' => '3g2',
  1353. 'video/3gp' => '3gp',
  1354. 'video/3gpp' => '3gp',
  1355. 'application/x-compressed' => '7zip',
  1356. 'audio/x-acc' => 'aac',
  1357. 'audio/ac3' => 'ac3',
  1358. 'application/postscript' => 'ai',
  1359. 'audio/x-aiff' => 'aif',
  1360. 'audio/aiff' => 'aif',
  1361. 'audio/x-au' => 'au',
  1362. 'video/x-msvideo' => 'avi',
  1363. 'video/msvideo' => 'avi',
  1364. 'video/avi' => 'avi',
  1365. 'application/x-troff-msvideo' => 'avi',
  1366. //'application/macbinary' => 'bin',
  1367. //'application/mac-binary' => 'bin',
  1368. //'application/x-binary' => 'bin',
  1369. //'application/x-macbinary' => 'bin',
  1370. 'image/bmp' => 'bmp',
  1371. 'image/x-bmp' => 'bmp',
  1372. 'image/x-bitmap' => 'bmp',
  1373. 'image/x-xbitmap' => 'bmp',
  1374. 'image/x-win-bitmap' => 'bmp',
  1375. 'image/x-windows-bmp' => 'bmp',
  1376. 'image/ms-bmp' => 'bmp',
  1377. 'image/x-ms-bmp' => 'bmp',
  1378. 'application/bmp' => 'bmp',
  1379. 'application/x-bmp' => 'bmp',
  1380. 'application/x-win-bitmap' => 'bmp',
  1381. //'application/cdr' => 'cdr',
  1382. //'application/coreldraw' => 'cdr',
  1383. //'application/x-cdr' => 'cdr',
  1384. //'application/x-coreldraw' => 'cdr',
  1385. //'image/cdr' => 'cdr',
  1386. //'image/x-cdr' => 'cdr',
  1387. //'zz-application/zz-winassoc-cdr' => 'cdr',
  1388. //'application/mac-compactpro' => 'cpt',
  1389. //'application/pkix-crl' => 'crl',
  1390. //'application/pkcs-crl' => 'crl',
  1391. //'application/x-x509-ca-cert' => 'crt',
  1392. //'application/pkix-cert' => 'crt',
  1393. 'text/css' => 'css',
  1394. 'text/x-comma-separated-values' => 'csv',
  1395. 'text/comma-separated-values' => 'csv',
  1396. 'application/vnd.msexcel' => 'csv',
  1397. 'application/x-director' => 'dcr',
  1398. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
  1399. 'application/vnd.oasis.opendocument.text' => 'odt',
  1400. 'application/x-dvi' => 'dvi',
  1401. 'message/rfc822' => 'eml',
  1402. //'application/x-msdownload' => 'exe',
  1403. 'video/x-f4v' => 'f4v',
  1404. 'audio/x-flac' => 'flac',
  1405. 'video/x-flv' => 'flv',
  1406. 'image/gif' => 'gif',
  1407. 'application/gpg-keys' => 'gpg',
  1408. 'application/x-gtar' => 'gtar',
  1409. 'application/x-gzip' => 'gzip',
  1410. 'application/mac-binhex40' => 'hqx',
  1411. 'application/mac-binhex' => 'hqx',
  1412. 'application/x-binhex40' => 'hqx',
  1413. 'application/x-mac-binhex40' => 'hqx',
  1414. 'text/html' => 'html',
  1415. 'image/x-icon' => 'ico',
  1416. 'image/x-ico' => 'ico',
  1417. 'image/vnd.microsoft.icon' => 'ico',
  1418. 'text/calendar' => 'ics',
  1419. 'application/java-archive' => 'jar',
  1420. 'application/x-java-application' => 'jar',
  1421. 'application/x-jar' => 'jar',
  1422. 'image/jp2' => 'jp2',
  1423. 'video/mj2' => 'jp2',
  1424. 'image/jpx' => 'jp2',
  1425. 'image/jpm' => 'jp2',
  1426. 'image/jpeg' => 'jpg',
  1427. 'image/pjpeg' => 'jpeg',
  1428. 'application/x-javascript' => 'js',
  1429. 'application/json' => 'json',
  1430. 'text/json' => 'json',
  1431. //'application/vnd.google-earth.kml+xml' => 'kml',
  1432. //'application/vnd.google-earth.kmz' => 'kmz',
  1433. 'text/x-log' => 'log',
  1434. 'audio/x-m4a' => 'm4a',
  1435. 'audio/mp4' => 'm4a',
  1436. 'application/vnd.mpegurl' => 'm4u',
  1437. 'audio/midi' => 'mid',
  1438. 'application/vnd.mif' => 'mif',
  1439. 'video/quicktime' => 'mov',
  1440. 'video/x-sgi-movie' => 'movie',
  1441. 'audio/mpeg' => 'mp3',
  1442. 'audio/mpg' => 'mp3',
  1443. 'audio/mpeg3' => 'mp3',
  1444. 'audio/mp3' => 'mp3',
  1445. 'video/mp4' => 'mp4',
  1446. 'video/mpeg' => 'mpeg',
  1447. 'application/oda' => 'oda',
  1448. 'audio/ogg' => 'ogg',
  1449. 'video/ogg' => 'ogg',
  1450. 'application/ogg' => 'ogg',
  1451. 'font/otf' => 'otf',
  1452. 'application/x-pkcs10' => 'p10',
  1453. 'application/pkcs10' => 'p10',
  1454. 'application/x-pkcs12' => 'p12',
  1455. 'application/x-pkcs7-signature' => 'p7a',
  1456. 'application/pkcs7-mime' => 'p7c',
  1457. 'application/x-pkcs7-mime' => 'p7c',
  1458. 'application/x-pkcs7-certreqresp' => 'p7r',
  1459. 'application/pkcs7-signature' => 'p7s',
  1460. 'application/pdf' => 'pdf',
  1461. 'application/octet-stream' => 'pdf',
  1462. 'application/x-x509-user-cert' => 'pem',
  1463. 'application/x-pem-file' => 'pem',
  1464. 'application/pgp' => 'pgp',
  1465. //'application/x-httpd-php' => 'php',
  1466. //'application/php' => 'php',
  1467. //'application/x-php' => 'php',
  1468. //'text/php' => 'php',
  1469. //'text/x-php' => 'php',
  1470. //'application/x-httpd-php-source' => 'php',
  1471. 'image/png' => 'png',
  1472. 'image/x-png' => 'png',
  1473. 'application/powerpoint' => 'ppt',
  1474. 'application/vnd.ms-powerpoint' => 'ppt',
  1475. 'application/vnd.ms-office' => 'ppt',
  1476. 'application/msword' => 'doc',
  1477. 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
  1478. 'application/x-photoshop' => 'psd',
  1479. 'image/vnd.adobe.photoshop' => 'psd',
  1480. 'audio/x-realaudio' => 'ra',
  1481. 'audio/x-pn-realaudio' => 'ram',
  1482. 'application/x-rar' => 'rar',
  1483. 'application/rar' => 'rar',
  1484. 'application/x-rar-compressed' => 'rar',
  1485. 'audio/x-pn-realaudio-plugin' => 'rpm',
  1486. 'application/x-pkcs7' => 'rsa',
  1487. 'text/rtf' => 'rtf',
  1488. 'text/richtext' => 'rtx',
  1489. 'video/vnd.rn-realvideo' => 'rv',
  1490. 'application/x-stuffit' => 'sit',
  1491. 'application/smil' => 'smil',
  1492. 'text/srt' => 'srt',
  1493. 'image/svg+xml' => 'svg',
  1494. 'application/x-shockwave-flash' => 'swf',
  1495. 'application/x-tar' => 'tar',
  1496. 'application/x-gzip-compressed' => 'tgz',
  1497. 'image/tiff' => 'tiff',
  1498. 'font/ttf' => 'ttf',
  1499. 'text/plain' => 'txt',
  1500. 'text/x-vcard' => 'vcf',
  1501. 'application/videolan' => 'vlc',
  1502. 'text/vtt' => 'vtt',
  1503. 'audio/x-wav' => 'wav',
  1504. 'audio/wave' => 'wav',
  1505. 'audio/wav' => 'wav',
  1506. 'application/wbxml' => 'wbxml',
  1507. 'video/webm' => 'webm',
  1508. 'image/webp' => 'webp',
  1509. 'audio/x-ms-wma' => 'wma',
  1510. 'application/wmlc' => 'wmlc',
  1511. 'video/x-ms-wmv' => 'wmv',
  1512. 'video/x-ms-asf' => 'wmv',
  1513. 'font/woff' => 'woff',
  1514. 'font/woff2' => 'woff2',
  1515. 'application/xhtml+xml' => 'xhtml',
  1516. 'application/excel' => 'xl',
  1517. 'application/msexcel' => 'xls',
  1518. 'application/x-msexcel' => 'xls',
  1519. 'application/x-ms-excel' => 'xls',
  1520. 'application/x-excel' => 'xls',
  1521. 'application/x-dos_ms_excel' => 'xls',
  1522. 'application/xls' => 'xls',
  1523. 'application/x-xls' => 'xls',
  1524. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
  1525. 'application/vnd.ms-excel' => 'xlsx',
  1526. 'application/xml' => 'xml',
  1527. 'text/xml' => 'xml',
  1528. 'text/xsl' => 'xsl',
  1529. 'application/xspf+xml' => 'xspf',
  1530. 'application/x-compress' => 'z',
  1531. 'application/x-zip' => 'zip',
  1532. 'application/zip' => 'zip',
  1533. 'application/x-zip-compressed' => 'zip',
  1534. 'application/s-compressed' => 'zip',
  1535. 'multipart/x-zip' => 'zip',
  1536. //'text/x-scriptzsh' => 'zsh',
  1537. );
  1538. return isset($mime_map[$mime]) ? $mime_map[$mime] : FALSE;
  1539. }
  1540. /**
  1541. * Connect to our imap server, download all received messages from students (or others).
  1542. * We will then delete them, so they don't get read twice.
  1543. */
  1544. function engagements_imap_get_all_received_messages()
  1545. {
  1546. $host = variable_get('imap_host', '');
  1547. $port = variable_get('imap_port', '');
  1548. $secure = variable_get('imap_secure', '');
  1549. $mailbox_server = "{" . "$host:$port/$secure" . "}";
  1550. $username = variable_get('imap_username', '');
  1551. $password = variable_get('imap_password', '');
  1552. if ($username == "") {
  1553. return FALSE;
  1554. }
  1555. watchdog('imap', "Attempting to connect to imap server.", array(), WATCHDOG_DEBUG);
  1556. $mbox = imap_open($mailbox_server, $username, $password);
  1557. if (!$mbox) {
  1558. watchdog('imap', "Could not connect to imap server!", array(), WATCHDOG_ERROR);
  1559. fp_add_message("Could not connect to IMAP server.", 'error', TRUE);
  1560. }
  1561. // We need to get the name of the "Trash" folder, which might contain a lot of weird extra stuff in the name.
  1562. // TODO: Maybe save in a cached field so we don't have to retrieve every time it runs? But, that gets cleared when we clear the cache.
  1563. $mailboxes = imap_list($mbox, $mailbox_server, "*");
  1564. $trash_box_name = "Trash";
  1565. foreach ($mailboxes as $boxname) {
  1566. if (stristr($boxname, "trash")) {
  1567. $trash_box_name = str_replace($mailbox_server, "", $boxname);
  1568. break;
  1569. }
  1570. }
  1571. $mbox_check = imap_check($mbox);
  1572. // Fetch an overview for all messages in INBOX, by going from 1 to the max number messages.
  1573. $result = imap_fetch_overview($mbox, "1:{$mbox_check->Nmsgs}", 0);
  1574. foreach ($result as $overview) {
  1575. $msgno = $overview->msgno;
  1576. $subject = $overview->subject;
  1577. $udate = $overview->udate; // udate is apparently already in UTC, which is what we want.
  1578. watchdog('imap', "Gettign message $msgno. Subject: $subject. Udate: $udate.", array(), WATCHDOG_DEBUG);
  1579. // We need to get our ONLY the email address from the From field, which
  1580. // might look like: Richard Peacock <richard.peacock@school.domain.co.com>
  1581. // We will use a regular expression to figure it out. From: https://stackoverflow.com/questions/33865113/extract-email-address-from-string-php
  1582. $ofrom = $overview->from;
  1583. preg_match_all("/[\._a-zA-Z0-9-]+@[\._a-zA-Z0-9-]+/i", $ofrom, $matches);
  1584. $from_email = $matches[0][0]; // Since this is a FROM email, we assume there's only one sender.
  1585. $body = _engagements_imap_get_body($mbox, $msgno);
  1586. watchdog('imap', "Found Body: $body", array(), WATCHDOG_DEBUG);
  1587. // match the from email to a student
  1588. $student_user_id = db_get_user_id_from_email($from_email, 'student');
  1589. if ($student_user_id) {
  1590. watchdog('imap', "Found from student $student_user_id, creating engagement content.", array(), WATCHDOG_DEBUG);
  1591. // create this as an engagement for this student.
  1592. $account = fp_load_user($student_user_id);
  1593. $student_cwid = $account->cwid;
  1594. $content = new stdClass();
  1595. $content->type = 'engagement';
  1596. $content->cid = "new";
  1597. $content->published = 1;
  1598. $content->delete_flag = 0;
  1599. $content->title = $subject; // required
  1600. $content->field__activity_datetime['value'] = date('Y-m-d H:i:s', $udate);
  1601. $content->field__student_id['value'] = $student_cwid;
  1602. $content->field__engagement_type['value'] = 'email';
  1603. $content->field__direction['value'] = 'received';
  1604. $content->field__engagement_msg['value'] = $body;
  1605. content_save($content);
  1606. $cid = $content->cid;
  1607. $attachments = _engagements_imap_get_attachments($mbox, $msgno, fp_get_machine_readable($from_email), $cid);
  1608. // save attachements along with the engagement, if there are any
  1609. if (count($attachments) > 0) {
  1610. $fid_line = "";
  1611. foreach ($attachments as $adetails) {
  1612. $fid = $adetails['fid'];
  1613. $fid_line .= $fid . ",";
  1614. }
  1615. $fid_line = rtrim($fid_line, ",");
  1616. $content->field__attachment['value'] = $fid_line;
  1617. }
  1618. $content->field__visibility['value'] = 'public';
  1619. content_save($content);
  1620. // if received, create an activity record.
  1621. // Create a new "activity record" that the student has sent an email message
  1622. $acontent = new stdClass();
  1623. $acontent->type = 'activity_record';
  1624. $acontent->cid = "new";
  1625. $acontent->published = 1;
  1626. $acontent->delete_flag = 0;
  1627. $acontent->title = t('Student replied to email message.');
  1628. $acontent->field__student_id['value'] = $student_cwid;
  1629. $acontent->field__activity_type['value'] = 'mail';
  1630. content_save($acontent);
  1631. // Notify the advisor(s).
  1632. // get list of all advisors for this student, and send notifications.
  1633. $advisors = advise_get_advisors_for_student($student_cwid);
  1634. foreach ($advisors as $c => $afaculty_id) {
  1635. $account_user_id = db_get_user_id_from_cwid($afaculty_id, 'faculty');
  1636. if ($account_user_id) {
  1637. $student_name = fp_get_student_name($student_cwid, TRUE);
  1638. $base_url = $GLOBALS['fp_system_settings']['base_url'];
  1639. $link = $base_url . "/engagements?current_student_id=$student_cwid";
  1640. $tmsg = "";
  1641. $tmsg .= "$student_name has submitted an email to FlightPath.<br><br>\n\n
  1642. To view, visit the student's Engagements tab by logging in and following this link:<br>\n
  1643. <a href='$link'>$link</a>";
  1644. notify_send_notification_to_user($account_user_id, $tmsg, $content->cid, 'engagement');
  1645. }
  1646. }
  1647. } // if student_user_id
  1648. // We are now finished with this email, so we can move it to our trash folder.
  1649. //imap_mail_move($mbox, $msgno, $trash_box_name);
  1650. imap_delete($mbox, $msgno);
  1651. } // foreach result as overview
  1652. // Delete all mail marked for deletion...
  1653. imap_expunge($mbox);
  1654. watchdog('imap', "Closing connection to imap server.", array(), WATCHDOG_DEBUG);
  1655. // Close connection, we're done.
  1656. imap_close($mbox);
  1657. // Record the last time we called this function.
  1658. variable_set('engagements_imap_get_all_last_check', time());
  1659. } // end of function
  1660. // From https://stackoverflow.com/questions/2649579/downloading-attachments-to-directory-with-imap-in-php-randomly-works
  1661. /**
  1662. * $inbox is the imap link. email_number is msg_no.
  1663. */
  1664. function _engagements_imap_get_attachments($inbox, $email_number, $from_email_machine_readable, $cid = 0)
  1665. {
  1666. /* get information specific to this email */
  1667. $overview = imap_fetch_overview($inbox, $email_number, 0);
  1668. $message = imap_fetchbody($inbox, $email_number, 2);
  1669. $structure = imap_fetchstructure($inbox, $email_number);
  1670. $attachments = array();
  1671. if (isset($structure->parts) && count($structure->parts)) {
  1672. for ($i = 0; $i < count($structure->parts); $i++) {
  1673. $attachments[$i] = array(
  1674. 'is_attachment' => false,
  1675. 'filename' => '',
  1676. 'name' => '',
  1677. 'subtype' => '',
  1678. 'attachment' => ''
  1679. );
  1680. if ($structure->parts[$i]->ifdparameters) {
  1681. foreach ($structure->parts[$i]->dparameters as $object) {
  1682. if (strtolower($object->attribute) == 'filename') {
  1683. $attachments[$i]['is_attachment'] = TRUE;
  1684. $attachments[$i]['filename'] = $object->value;
  1685. $attachments[$i]['subtype'] = $structure->parts[$i]->subtype;
  1686. }
  1687. }
  1688. }
  1689. if ($structure->parts[$i]->ifparameters) {
  1690. foreach ($structure->parts[$i]->parameters as $object) {
  1691. if (strtolower($object->attribute) == 'name') {
  1692. $attachments[$i]['is_attachment'] = true;
  1693. $attachments[$i]['name'] = $object->value;
  1694. $attachments[$i]['subtype'] = $structure->parts[$i]->subtype;
  1695. }
  1696. }
  1697. }
  1698. if ($attachments[$i]['is_attachment']) {
  1699. $attachments[$i]['attachment'] = imap_fetchbody($inbox, $email_number, $i + 1);
  1700. if ($structure->parts[$i]->encoding == 3) { // 3 = BASE64
  1701. $attachments[$i]['attachment'] = base64_decode($attachments[$i]['attachment']);
  1702. } elseif ($structure->parts[$i]->encoding == 4) { // 4 = QUOTED-PRINTABLE
  1703. $attachments[$i]['attachment'] = quoted_printable_decode($attachments[$i]['attachment']);
  1704. }
  1705. }
  1706. } // for($i = 0; $i < count($structure->parts); $i++)
  1707. } // if(isset($structure->parts) && count($structure->parts))
  1708. $rtn = array();
  1709. if (count($attachments) != 0) {
  1710. foreach ($attachments as $c => $at) {
  1711. if ($at['is_attachment'] == TRUE) {
  1712. // Create a new random filename to save it as.
  1713. $filename = 'email__' . $from_email_machine_readable . '_' . time() . '_' . mt_rand(1, 9999); // We will figure out the ext (from mimetype) later.
  1714. $tmp_name = sha1($filename . time() . mt_rand(1,99999) . mt_rand(1,99999));
  1715. // Save to our temporary location on the server.
  1716. $test = file_put_contents(sys_get_temp_dir() . '/' . $tmp_name, $at['attachment']);
  1717. if (!$test) {
  1718. watchdog('engagements_email', 'Unable to write media file at temp location: ' . sys_get_temp_dir(), array(), 'error');
  1719. fpm('Unable to write media file at temp location:' . sys_get_temp_dir() . '. Permissions issue?');
  1720. continue;
  1721. }
  1722. // Next, we want to rename it to have the proper file extension, as best we can tell.
  1723. $ext = strtolower($at['subtype']); // if we can't figure it out, use this.
  1724. // Attempt to figure out the mimetype....
  1725. $finfo = finfo_open(FILEINFO_MIME_TYPE);
  1726. $mimetype = finfo_file($finfo, sys_get_temp_dir() . '/' . $tmp_name);
  1727. $temp = explode(";", $mimetype);
  1728. $mimetype = strtolower(trim($temp[0]));
  1729. $new_ext = engagements_get_file_extension_from_mime_type($mimetype);
  1730. if ($new_ext) {
  1731. $ext = $new_ext;
  1732. }
  1733. $filename .= ".$ext"; // the filename doesn't have its extension already for whatever reason.
  1734. $file = array(
  1735. 'name' => $filename,
  1736. 'type' => $mimetype,
  1737. 'tmp_name' => $tmp_name,
  1738. );
  1739. $fid = content_add_new_uploaded_file($file, $cid, FALSE);
  1740. // TODO: Possibly rename filename based on if it is encrypted or not?
  1741. $rtn[] = array(
  1742. 'fid' => $fid,
  1743. 'filename' => $filename,
  1744. 'mimetime' => $mimetype,
  1745. 'ext' => $ext,
  1746. );
  1747. } // if is_attachment == TRUE
  1748. } // foreach
  1749. }
  1750. return $rtn;
  1751. } // ... imap_get_attachments
  1752. // From: https://stackoverflow.com/questions/4272551/extract-body-text-from-email-php
  1753. function _engagements_imap_get_body($imapLink, $msgno)
  1754. {
  1755. $obj_structure = imap_fetchstructure($imapLink, $msgno);
  1756. // Recherche de la section contenant le corps du message et extraction du contenu
  1757. $obj_section = $obj_structure;
  1758. //$text = imap_fetchbody($imapLink, $msgno, $section);
  1759. // So it turns out the $section for HTML can be 1.2, but it might also be just "2" in simple messages.
  1760. // Try to get HTML first....
  1761. $text = trim(imap_fetchbody($imapLink, $msgno, 1.2));
  1762. if ($text == "") {
  1763. $text = trim(imap_fetchbody($imapLink, $msgno, 2));
  1764. }
  1765. if ($text == "") {
  1766. $text = trim(imap_fetchbody($imapLink, $msgno, 1.1));
  1767. }
  1768. if ($text == "") {
  1769. $text = trim(imap_fetchbody($imapLink, $msgno, 1));
  1770. }
  1771. // Décodage éventuel
  1772. if ($obj_section->encoding == 3) {
  1773. $text = imap_base64($text);
  1774. } else if ($obj_section->encoding == 4) {
  1775. $text = imap_qprint($text);
  1776. }
  1777. // Encodage éventuel
  1778. foreach ($obj_section->parameters as $obj_param) {
  1779. if (($obj_param->attribute == "charset") && (mb_strtoupper($obj_param->value) != "UTF-8")) {
  1780. $text = utf8_encode($text);
  1781. break;
  1782. }
  1783. }
  1784. $text = filter_markup($text, 'basic');
  1785. // Sometimes, emails contain odd encoding. Ex: =C2=A0, which is apparently a non-breaking space. Let's convert just in case.
  1786. // From: https://stackoverflow.com/questions/12682208/parsing-email-body-with-7bit-content-transfer-encoding-php
  1787. // and: https://github.com/geerlingguy/Imap/blob/c4b54e52576bf71a93045d2a4581589eab9a00b6/JJG/Imap.php#L398
  1788. // Manually convert common encoded characters into their UTF-8 equivalents.
  1789. $characters = array(
  1790. '=20' => ' ', // space.
  1791. '=2C' => ',', // comma.
  1792. '=E2=80=99' => "'", // single quote.
  1793. '=C2=A0' => ' ', // non-breaking space.
  1794. '=0A' => "\r\n", // line break.
  1795. '=0D' => "\r\n", // carriage return.
  1796. '=A0' => ' ', // non-breaking space.
  1797. "=\r\n" => '', // joined line.
  1798. '=E2=80=A6' => '&hellip;', // ellipsis.
  1799. '=E2=80=A2' => '&bull;', // bullet.
  1800. '=E2=80=93' => '&ndash;', // en dash.
  1801. '=E2=80=94' => '&mdash;', // em dash.
  1802. );
  1803. // Loop through the encoded characters and replace any that are found.
  1804. foreach ($characters as $key => $value) {
  1805. $text = str_replace($key, $value, $text);
  1806. }
  1807. return $text;
  1808. }
  1809. /**
  1810. * The user has opened an email with a tracking pixel. We will now record that
  1811. * it was opened in our engagements_tracking table.
  1812. *
  1813. * We also need to actually render the pixel to the browser.
  1814. */
  1815. function engagements_handle_tracking_pixel_request($cid, $token)
  1816. {
  1817. // Record in database
  1818. // Fist, get the current count, if it exists.
  1819. $res = db_query("SELECT * FROM engagements_tracking WHERE cid = ? AND token = ?", array($cid, $token));
  1820. $cur = db_fetch_array($res);
  1821. if ($cur['cid'] != $cid || $cur['token'] != $token) {
  1822. // Means it was not generated, this might be a malicious attempt or an old token or something.
  1823. die;
  1824. }
  1825. $opens = intval($cur['opens']) + 1;
  1826. db_query('UPDATE engagements_tracking
  1827. SET opens = ?,
  1828. updated = ?
  1829. WHERE cid = ? AND token = ?', array($opens, time(), $cid, $token));
  1830. watchdog('engagements_track', "Tracking request for: $cid - $token");
  1831. // Create a new "activity record" that the student has viewed this email.
  1832. $email_content = content_load($cid);
  1833. // Create a new actvity_record.
  1834. $content = new stdClass();
  1835. $content->type = 'activity_record';
  1836. $content->cid = "new";
  1837. $content->published = 1;
  1838. $content->delete_flag = 0;
  1839. $content->title = t('Student opened email titled "@et".', array("@et" => $email_content->title));
  1840. $content->field__student_id['value'] = $email_content->field__student_id['value'];
  1841. $content->field__activity_type['value'] = 'mail';
  1842. content_save($content);
  1843. // Render pixel to browser and die. We will be using the 1x1.gif file in our assets folder.
  1844. // Output the image to browser
  1845. header('Content-Type: image/gif');
  1846. $im = imagecreatefromgif(fp_get_module_path('engagements', TRUE, FALSE) . '/assets/1x1.gif');
  1847. imagegif($im);
  1848. imagedestroy($im);
  1849. die;
  1850. } // engagements_handle_tracking_pixel_request
  1851. function engagements_perm()
  1852. {
  1853. $rtn = array(
  1854. 'administer_engagements' => array(
  1855. 'title' => 'Administer Engagements',
  1856. 'description' => t('The user can edit the system config settings for the Engagements module. Only give to admin users.'),
  1857. "admin_restricted" => TRUE, // means only appears for admin (user_id == 1)
  1858. ),
  1859. 'can_view_engagements' => array(
  1860. 'title' => 'Can view Engagements',
  1861. 'description' => t('The user may view engagements (only "everyone" by default)'),
  1862. ),
  1863. 'can_view_faculty_engagements' => array(
  1864. 'title' => 'View "Faculty/Staff" Engagements',
  1865. 'description' => t('The user is allowed to view engagements marked visible for "Faculty/Staff".'),
  1866. ),
  1867. 'can_send_email_engagements' => array(
  1868. 'title' => t('Can send email engagements to students'),
  1869. ),
  1870. 'can_log_engagements' => array(
  1871. 'title' => t('Can log engagements with students'),
  1872. ),
  1873. 'can_send_txt_engagements' => array(
  1874. 'title' => t('Can send txt message engagements to students'),
  1875. 'description' => t("<b>Important:</b> Be sure to also select the specific phone number/permissions below."),
  1876. ),
  1877. );
  1878. // set up other permissions for each SMS line we have.
  1879. $phones = engagements_get_from_phones();
  1880. foreach ($phones['lines'] as $num => $details) {
  1881. $pretty_num = engagements_convert_to_pretty_phone_number($num);
  1882. $desc = filter_markup($details['description'], "plain");
  1883. $desc = str_replace("'", "", $desc);
  1884. $desc = str_replace('"', "", $desc);
  1885. $rtn["can_send_sms_from_$num"] = array(
  1886. "title" => t("Can send txt messages from @pretty", array("@pretty" => $pretty_num)),
  1887. "description" => t("The user may send SMS (txt) messages from @pretty - %desc phone line.", array("@pretty" => $pretty_num, "%desc" => $desc)),
  1888. );
  1889. }
  1890. $rtn['can_send_mass_sms'] = array(
  1891. 'title' => t("Send mass txt messages"),
  1892. 'description' => t("The user may access and send mass SMS (txt) messages to lists of recipients from the designated mass text lines.
  1893. Only give to trusted users."),
  1894. );
  1895. return $rtn;
  1896. }
  1897. /**
  1898. * For use with the content module. We will register our custom content type(s)
  1899. * for use with this module.
  1900. */
  1901. function engagements_content_register_content_type()
  1902. {
  1903. $arr = array();
  1904. $arr['engagement'] = array(
  1905. 'title' => 'Engagement',
  1906. 'description' => 'This is a content type meant to track a students activities and communications in the system.',
  1907. 'settings' => array(
  1908. 'title' => array(
  1909. 'label' => t('Subject'),
  1910. 'weight' => 65,
  1911. ),
  1912. ),
  1913. );
  1914. // If we are in a popup (dialog)...
  1915. if (@$_GET['window_mode'] == 'popup') {
  1916. // We want to make sure we redirect to our handler URL, which will close the dialog.
  1917. $arr['engagement']['settings']['#redirect'] = array(
  1918. 'path' => 'content-dialog-handle-after-save',
  1919. 'query' => '',
  1920. );
  1921. }
  1922. $fields = array();
  1923. $fields['faculty_id'] = array(
  1924. 'type' => 'textfield',
  1925. 'label' => 'Faculty/Staff',
  1926. 'weight' => 5,
  1927. );
  1928. $fields['from_sms_phone'] = array(
  1929. 'type' => 'select',
  1930. 'label' => t('From Text Phone Line:'),
  1931. 'hide_please_select' => TRUE,
  1932. 'options' => array(),
  1933. 'weight' => 7,
  1934. );
  1935. $fields['student_id'] = array(
  1936. 'type' => 'textfield',
  1937. 'label' => 'Student',
  1938. 'weight' => 10,
  1939. );
  1940. $fields['activity_datetime'] = array(
  1941. 'type' => 'datetime-local',
  1942. 'label' => 'Date/Time',
  1943. 'value' => 'now',
  1944. 'format_date' => 'short',
  1945. 'weight' => 12,
  1946. );
  1947. $fields['engagement_type'] = array(
  1948. 'type' => 'select',
  1949. 'options' => array(
  1950. 'phone' => t('Phone Call'),
  1951. 'email' => t('Email'),
  1952. 'in_person' => t('In-Person Meeting'),
  1953. 'video_chat' => t('Video Chat'),
  1954. 'txt_msg' => t('Txt Message'),
  1955. 'social_media' => t('Social Media'),
  1956. 'other' => t('Other'),
  1957. ),
  1958. 'label' => 'Type',
  1959. 'required' => TRUE,
  1960. 'hide_please_select' => TRUE,
  1961. 'weight' => 40,
  1962. );
  1963. $fields['direction'] = array(
  1964. 'type' => 'select',
  1965. 'label' => t('Direction'),
  1966. 'required' => TRUE,
  1967. 'hide_please_select' => TRUE,
  1968. 'options' => array(
  1969. 'sent' => t("Sent"),
  1970. 'received' => t('Received'),
  1971. ),
  1972. 'weight' => 50,
  1973. );
  1974. $fields['phone_outcome'] = array(
  1975. 'type' => 'select',
  1976. 'label' => t('Phone Outcome'),
  1977. 'hide_please_select' => TRUE,
  1978. 'options' => array(
  1979. 'connected' => t("Connected"),
  1980. 'no_answer' => t('No Answer'),
  1981. 'voicemail' => t('Left Voicemail'),
  1982. 'busy' => t('Busy'),
  1983. 'wrong_number' => t('Wrong Number'),
  1984. ),
  1985. 'weight' => 60,
  1986. );
  1987. $fields['engagement_msg'] = array(
  1988. 'type' => 'textarea',
  1989. 'label' => t('Message'),
  1990. 'filter' => 'basic',
  1991. 'weight' => 70,
  1992. );
  1993. $fields['attachment'] = array(
  1994. 'type' => 'file',
  1995. 'label' => t('Attach File(s)'),
  1996. 'weight' => 80,
  1997. 'limit' => 999, // essentially, infinite.
  1998. );
  1999. $fields['visibility'] = array(
  2000. 'type' => 'radios',
  2001. 'label' => 'Visible to:',
  2002. 'options' => array('public' => 'Anyone (incl. student)', 'faculty' => 'Faculty/Staff only'),
  2003. 'weight' => 90,
  2004. );
  2005. $fields['manual_entry'] = array(
  2006. 'type' => 'hidden',
  2007. 'value' => '',
  2008. );
  2009. $fields['to_sms_phone'] = array(
  2010. 'type' => 'hidden',
  2011. 'value' => '',
  2012. );
  2013. $fields['external_msg_id'] = array(
  2014. 'type' => 'hidden',
  2015. 'value' => '',
  2016. );
  2017. $arr['engagement']['fields'] = $fields;
  2018. return $arr;
  2019. } // hook_content_register_content_type
  2020. /**
  2021. * Converts the string into a plain phone number, then tests to see if it is valid or not.
  2022. * RETURNS FALSE if not valid, otherwise, returns the converted phone number. This will be a valid
  2023. * number for use with our SMS service. (in the US anyway).
  2024. */
  2025. function engagements_convert_to_valid_phone_number($num)
  2026. {
  2027. // Remove any non-numeric characters from the num.
  2028. $num = preg_replace("/\D/", '', $num);
  2029. // The number should be 10 characters.
  2030. if (strlen($num) == 10) return $num;
  2031. if (strlen($num) == 11) {
  2032. // Maybe it starts with a "1", which is not necessary.
  2033. if (substr($num, 0, 1) == "1") {
  2034. return substr($num, 1, 10); // ditch the first character.
  2035. }
  2036. }
  2037. // Bad number, return false.
  2038. return FALSE;
  2039. }
  2040. // From: https://stackoverflow.com/questions/4708248/formatting-phone-numbers-in-php
  2041. function engagements_convert_to_pretty_phone_number($phoneNumber, $bool_convert_to_valid_first = TRUE)
  2042. {
  2043. if ($bool_convert_to_valid_first) {
  2044. $phoneNumber = engagements_convert_to_valid_phone_number($phoneNumber);
  2045. if (!$phoneNumber) return FALSE;
  2046. }
  2047. $phoneNumber = preg_replace('/[^0-9]/', '', $phoneNumber);
  2048. if (strlen($phoneNumber) > 10) {
  2049. $countryCode = substr($phoneNumber, 0, strlen($phoneNumber) - 10);
  2050. $areaCode = substr($phoneNumber, -10, 3);
  2051. $nextThree = substr($phoneNumber, -7, 3);
  2052. $lastFour = substr($phoneNumber, -4, 4);
  2053. $phoneNumber = '+' . $countryCode . ' (' . $areaCode . ') ' . $nextThree . '-' . $lastFour;
  2054. } else if (strlen($phoneNumber) == 10) {
  2055. $areaCode = substr($phoneNumber, 0, 3);
  2056. $nextThree = substr($phoneNumber, 3, 3);
  2057. $lastFour = substr($phoneNumber, 6, 4);
  2058. $phoneNumber = '(' . $areaCode . ') ' . $nextThree . '-' . $lastFour;
  2059. } else if (strlen($phoneNumber) == 7) {
  2060. $nextThree = substr($phoneNumber, 0, 3);
  2061. $lastFour = substr($phoneNumber, 3, 4);
  2062. $phoneNumber = $nextThree . '-' . $lastFour;
  2063. }
  2064. return $phoneNumber;
  2065. }
  2066. /**
  2067. * Implements hook_form_alter
  2068. *
  2069. * We want to make various modifications to our form, based on what we are trying to do to it. We may also
  2070. * want to add in some custom javascript as well.
  2071. *
  2072. */
  2073. function engagements_form_alter(&$form, $form_id)
  2074. {
  2075. global $user;
  2076. if ($form_id == 'content_edit_content_form') {
  2077. if (@$form['type']['value'] == 'engagement') {
  2078. $db = get_global_database_handler();
  2079. fp_add_js(fp_get_module_path("engagements") . "/js/engagements.js");
  2080. fp_add_css(fp_get_module_path("engagements") . "/css/style.css");
  2081. // If this is a NEW form, then check for values in the URL to auto-fill.
  2082. if ($form['cid']['value'] === 'new') {
  2083. if (!isset($_GET['engagement_type']) || $_GET['engagement_type'] == '' || $_GET['engagement_type'] == 'new') {
  2084. // This means we are logging a NEW engagement, and not trying to send a txt or email. We need to
  2085. // set our manual_entry value to something meaningful.
  2086. $form['manual_entry']['value'] = 'Y';
  2087. }
  2088. if (isset($_GET['engagement_type']) && $_GET['engagement_type'] != '' && $_GET['engagement_type'] != 'new') {
  2089. $form['engagement_type']['value'] = $_GET['engagement_type'];
  2090. // Since we are setting the type, we do not want to display it as an option.
  2091. $form['engagement_type']['attributes'] = array('class' => 'hidden');
  2092. // There are other fields we do not wish to display as well:
  2093. $form['direction']['attributes'] = array('class' => 'hidden');
  2094. $form['activity_datetime']['attributes'] = array('class' => 'hidden');
  2095. }
  2096. $initials = variable_get_for_school("school_initials", "DEMO", $user->school_id);
  2097. if (isset($_GET['faculty_id'])) {
  2098. $form['faculty_id']['value'] = $_GET['faculty_id'];
  2099. $form['faculty_id']['attributes'] = array('class' => 'hidden');
  2100. $form['mark_from'] = array(
  2101. 'type' => 'markup',
  2102. 'value' => "<div class='engagement-field-mark engagement-from'><label>" . t("From") . ":</label>
  2103. " . $db->get_faculty_name($form['faculty_id']['value']) . "
  2104. </div>",
  2105. 'weight' => $form['faculty_id']['weight'],
  2106. );
  2107. }
  2108. if (isset($_GET['student_id'])) {
  2109. $form['student_id']['value'] = $_GET['student_id'];
  2110. $initials = variable_get_for_school("school_initials", "DEMO", db_get_school_id_for_student_id($_GET['student_id']));
  2111. $form['student_id']['attributes'] = array('class' => 'hidden');
  2112. $extra_mark = "";
  2113. if ($form['engagement_type']['value'] == 'email') {
  2114. $extra_mark = fp_get_student_email($form['student_id']['value']);
  2115. // TODO: The EMAIL header may need to be a setting with replacement values
  2116. $form['engagement_msg']['prefix'] = t("Your message will automatically begin with <strong>(@initials) from @name:</strong>", array("@initials" => $initials, "@name" => $db->get_faculty_name($form['faculty_id']['value'])));
  2117. }
  2118. $student_user_id = db_get_user_id_from_cwid($_GET['student_id'], 'student');
  2119. $student_account = fp_load_user($student_user_id);
  2120. if ($form['engagement_type']['value'] == 'txt_msg') {
  2121. // Since this is a txt message, the textare has a max num of characters (1600)
  2122. $form['engagement_msg']['maxlength'] = 1600;
  2123. // Add in select list of the phone numbers we are allowed to select from.
  2124. $from_options = engagements_get_from_phones_for_fapi(FALSE, $user);
  2125. $form['from_sms_phone']['options'] = $from_options;
  2126. if (count($from_options) == 0) {
  2127. $form['from_sms_phone']['suffix'] .= "<h3>" . t("Note: You do
  2128. not have permission to send txt messages from any number.
  2129. See your system administrator for access.") . "</h3>";
  2130. $form['from_sms_phone']['attributes']['readonly'] = "readonly";
  2131. $form['from_sms_phone']['attributes']['class'] .= " readonly";
  2132. $form['from_sms_phone']['attributes']['disabled'] .= "disabled";
  2133. $form['engagement_msg']['attributes']['readonly'] = "readonly";
  2134. $form['engagement_msg']['attributes']['class'] .= " readonly";
  2135. $form['engagement_msg']['attributes']['disabled'] .= "disabled";
  2136. $form['submit_submit']['attributes']['disabled'] = 'disabled';
  2137. $form['submit_submit']['attributes']['class'] .= ' button-disabled';
  2138. }
  2139. $form['from_sms_phone']['required'] = TRUE;
  2140. // get student phone number
  2141. $extra_mark = "(NO MOBILE NUMBER AVAILABLE)";
  2142. $mobile_phone = user_get_attribute($student_user_id, "mobile_phone");
  2143. if ($mobile_phone) {
  2144. $extra_mark = engagements_convert_to_pretty_phone_number($mobile_phone, TRUE);
  2145. } else {
  2146. // Mobile phone was never found, so disable submit and message box.
  2147. $form['engagement_msg']['attributes']['readonly'] = 'readonly';
  2148. $form['engagement_msg']['attributes']['class'] .= ' readonly';
  2149. $form['submit_submit']['attributes']['disabled'] = 'disabled';
  2150. $form['submit_submit']['attributes']['class'] .= ' button-disabled';
  2151. $form['submit_submit']['suffix'] = "<strong>" . t('NOTE: Cannot send a text message as there is no mobile phone number for this recipient.') . "</strong>";
  2152. }
  2153. if (trim($mobile_phone) != "" && !engagements_can_send_sms_to_number($mobile_phone)) {
  2154. // Meaning, the user has opted-out! Disable fields and let the user know.
  2155. $form['engagement_msg']['attributes']['readonly'] = 'readonly';
  2156. $form['engagement_msg']['attributes']['class'] .= ' readonly';
  2157. $form['submit_submit']['attributes']['disabled'] = 'disabled';
  2158. $form['submit_submit']['attributes']['class'] .= ' button-disabled';
  2159. $form['submit_submit']['suffix'] = "<strong>" . t('NOTE: Cannot send a text message as this recipient has opted-out of receving text messages. Try email or calling instead.') . "</strong>";
  2160. }
  2161. // The header needs to be a setting with replacement values
  2162. $sms_header = engagements_sms_replace_patterns(variable_get('sms_header', '(@initials) from @name:'), "", $user);
  2163. $form['engagement_msg']['prefix'] = t("Your message will automatically begin with <strong>@header</strong>", array("@header" => $sms_header));
  2164. $form['engagement_msg']['description'] = t("To insert emoji in Windows, press <span style='padding-left: 10px; padding-right: 10px; font-size: 1.3em;'><i class='fa fa-windows'></i> + .</span> <em>(Windows key + period)</em>");
  2165. $form['attachment']['attributes'] = array('class' => 'hidden');
  2166. }
  2167. $form['mark_to'] = array(
  2168. 'type' => 'markup',
  2169. 'value' => "<div class='engagement-field-mark engagement-to'><label>" . t("To") . ":</label>
  2170. " . $db->get_student_name($form['student_id']['value'], TRUE) . "<span class='engagement-student-extra-mark'>$extra_mark</span>
  2171. </div>",
  2172. 'weight' => $form['student_id']['weight'],
  2173. );
  2174. }
  2175. // Always set the published = TRUE, and hide.
  2176. $form['published']['value'] = TRUE;
  2177. $form['published']['attributes'] = array('class' => 'hidden');
  2178. // Add a validate handler to form, so we can name sure email or mobile phone numbers are valid.
  2179. $form['#validate_handlers'][] = 'engagements_send_email_or_txt_form_validate';
  2180. // Add a submit handler to form, so we can send email or txt messages
  2181. $form['#submit_handlers'][] = 'engagements_send_email_or_txt_form_submit';
  2182. } // cid == new
  2183. if ($form['engagement_type']['value'] != 'txt_msg') {
  2184. // If this is anything OTHER than a txt_msg, we will show the tinymce editor for the Message
  2185. $form['engagement_msg']['type'] = 'textarea_editor';
  2186. // Also get rid of the sms number field.
  2187. $form['from_sms_phone'] = array(
  2188. 'type' => 'hidden',
  2189. 'value' => '',
  2190. 'required' => FALSE,
  2191. );
  2192. }
  2193. if ($form['engagement_type']['value'] != 'txt_msg' && $form['engagement_type']['value'] != 'email') {
  2194. // Set visibility to 'faculty' by default.
  2195. $form['visibility']['value'] = 'faculty';
  2196. } else {
  2197. $form['visibility']['value'] = 'public';
  2198. $form['visibility']['attributes'] = array('class' => 'hidden');
  2199. }
  2200. } // form type == engagement
  2201. } // content_edit_content_form
  2202. } // hook_form_alter
  2203. /**
  2204. * Get an array of numbers which the user should be notified of when they receive an SMS.
  2205. */
  2206. function engagements_get_user_notify_sms_receipt_values($user_id = NULL) {
  2207. global $user;
  2208. $rtn = array();
  2209. if ($user_id === NULL) {
  2210. $user_id = $user->id;
  2211. }
  2212. $options = engagements_get_from_phones_for_fapi(TRUE);
  2213. foreach ($options as $num => $val) {
  2214. $test = user_get_setting($user_id, "notify_sms_receipt__" . $num);
  2215. if (intval($test) === intval($num)) {
  2216. $rtn[$num] = $num;
  2217. }
  2218. }
  2219. return $rtn;
  2220. }
  2221. /**
  2222. * Returns an array of all the user who should receive notifications when
  2223. * we receive an SMS at a particular number.
  2224. */
  2225. function engagements_get_users_to_be_notified_for_sms_on_number($num) {
  2226. $rtn = array();
  2227. $res = db_query("SELECT user_id FROM user_settings
  2228. WHERE name = ?", array('notify_sms_receipt__' . $num));
  2229. while ($cur = db_fetch_array($res)) {
  2230. $rtn[$cur['user_id']] = intval($cur['user_id']);
  2231. }
  2232. return $rtn;
  2233. }
  2234. /**
  2235. * Returns back the phone lines available.
  2236. *
  2237. * If bool_return_all == FALSE, it means we will first check to make sure the user (specified by account) has permission
  2238. * to send sms for that number.
  2239. *
  2240. */
  2241. function engagements_get_from_phones_for_fapi($bool_return_all = TRUE, $account = NULL, $bool_only_mass_text_numbers = FALSE)
  2242. {
  2243. global $user;
  2244. $rtn = array();
  2245. if ($account == NULL) $account = $user;
  2246. $phones = engagements_get_from_phones($bool_only_mass_text_numbers);
  2247. $default = $phones['default']['num'];
  2248. $rtn[$default] = t("[Default] ") . engagements_convert_to_pretty_phone_number($default) . ' - ' . $phones['default']['description'];
  2249. foreach ($phones['lines'] as $num => $details) {
  2250. if (!isset($rtn[$details['num']])) {
  2251. $rtn[$details['num']] = engagements_convert_to_pretty_phone_number($details['num']) . " - " . $details['description'];
  2252. }
  2253. }
  2254. // if bool_return_all != TRUE, then make sure the account has permissing to send from each number. If not, remove it.
  2255. foreach ($rtn as $num => $details) {
  2256. if (!$bool_return_all) {
  2257. if (!$bool_only_mass_text_numbers && !user_has_permission("can_send_sms_from_$num", $account)) {
  2258. unset($rtn[$num]);
  2259. }
  2260. }
  2261. }
  2262. return $rtn;
  2263. } // engagements_get_from_phones_for_fapi()
  2264. function engagements_send_email_or_txt_form_validate($form, &$form_state)
  2265. {
  2266. $values = $form_state['values'];
  2267. ////////
  2268. // TODO: I am not sure I even need to get the cid here... delete it?
  2269. $cid = NULL;
  2270. if (isset($form_state['content_object']) && is_object($form_state['content_object'])) {
  2271. $cid = $form_state['content_object']->cid; // At this point, the content has already been saved, so we know its cid.
  2272. }
  2273. ////////
  2274. $engagement_type = $values['engagement_type'];
  2275. $manual_entry = @trim(strtoupper($values['manual_entry']));
  2276. if ($engagement_type != 'email' && $engagement_type != 'txt_msg') {
  2277. return; // we are not interested, harmlessly return.
  2278. }
  2279. $student_id = $values['student_id'];
  2280. $faculty_id = $values['faculty_id'];
  2281. $msg = $values['engagement_msg'];
  2282. $from_number = trim($values['from_sms_phone']);
  2283. $db = get_global_database_handler();
  2284. if ($engagement_type == 'email' && $manual_entry != 'Y') {
  2285. $student_email = fp_get_student_email($student_id);
  2286. if (!filter_var($student_email, FILTER_VALIDATE_EMAIL)) { // take advantage of pho built-in email validator
  2287. form_error('engagement_msg', t("Sorry, but the email provided for the student appears to be invalid."));
  2288. return;
  2289. }
  2290. } // type == 'email'
  2291. if ($engagement_type == 'txt_msg' && $manual_entry != 'Y') {
  2292. $student_user_id = db_get_user_id_from_cwid($student_id, 'student');
  2293. $temp = user_get_attribute($student_user_id, 'mobile_phone');
  2294. $mobile_phone = engagements_convert_to_valid_phone_number(trim($temp));
  2295. if (!$mobile_phone) {
  2296. form_error('engagement_msg', t('Sorry, but the mobile phone number provided for the student appears to be invalid.'));
  2297. return;
  2298. }
  2299. } // type == 'txt_msg'
  2300. } // engagements_send_email_or_txt_form_validate
  2301. function engagements_send_email_or_txt_form_submit($form, &$form_state)
  2302. {
  2303. global $user;
  2304. $values = $form_state['values'];
  2305. $cid = $form_state['content_object']->cid; // At this point, the content has already been saved, so we know its cid.
  2306. $content = $form_state['content_object'];
  2307. $engagement_type = $values['engagement_type'];
  2308. $manual_entry = @trim(strtoupper($values['manual_entry']));
  2309. if ($engagement_type != 'email' && $engagement_type != 'txt_msg') {
  2310. return; // we are not interested, harmlessly return.
  2311. }
  2312. $db = get_global_database_handler();
  2313. $student_id = $values['student_id'];
  2314. $faculty_id = $values['faculty_id'];
  2315. $faculty_name = $db->get_faculty_name($faculty_id);
  2316. $initials = variable_get("school_initials", "DEMO");
  2317. $initials = variable_get_for_school("school_initials", "DEMO", db_get_school_id_for_student_id($student_id));
  2318. // get the "from_number"
  2319. $from_number = $values['from_sms_phone'];
  2320. // this needs to be a setting with replacement values!
  2321. // Add header to msg...
  2322. $header = engagements_sms_replace_patterns(variable_get('sms_header', '(@initials) from @name:'), $from_number, $user);
  2323. $msg = $values['engagement_msg'];
  2324. $db = get_global_database_handler();
  2325. if ($engagement_type == 'email' && $manual_entry != 'Y') {
  2326. $student_email = fp_get_student_email($student_id);
  2327. $subject = $values['title'];
  2328. // Add our tracking pixel to the end of the email msg (but not what is saved to our database).
  2329. // We will use that to tell how many times the message gets opened.
  2330. $tracking_img_url = engagements_create_new_tracking_img_url($cid);
  2331. $msg .= "<br><br>----<br>You may respond directly to this email. It will be stored with FlightPath for $faculty_name to then review.";
  2332. $msg .= "\n\n<img src='$tracking_img_url'>";
  2333. // Were there any attachments with this content?
  2334. $attachments = array();
  2335. $attachment_csv = trim(@$content->field__attachment['value']);
  2336. if ($attachment_csv) {
  2337. $temp = explode(",", $attachment_csv);
  2338. foreach ($temp as $fid) {
  2339. if ($fid == "") continue;
  2340. $file = content_get_uploaded_file($fid);
  2341. if ($file) {
  2342. // Handle encrypted files
  2343. $fcont = file_get_contents($file['full_filename']); // the full_filename already contains the path to the file
  2344. if (intval($file['is_encrypted']) === 1) {
  2345. $fcont = encryption_decrypt($fcont);
  2346. }
  2347. $attachments[$file['original_filename']] = $fcont;
  2348. }
  2349. }
  2350. }
  2351. smtp_mail($student_email, $subject, $header . "<br><br>" . $msg, TRUE, $attachments, TRUE); // the TRUE sends as HTML.
  2352. fp_add_message(t("Email has been successfully sent."));
  2353. } // type == 'email'
  2354. $mobile_phone = '';
  2355. if ($engagement_type == 'txt_msg' && $manual_entry != 'Y') {
  2356. $student_user_id = db_get_user_id_from_cwid($student_id, 'student');
  2357. $temp = user_get_attribute($student_user_id, 'mobile_phone');
  2358. $mobile_phone = engagements_convert_to_valid_phone_number(trim($temp));
  2359. // Since we have passed validation, we will assume we have a valid phone number for this student.
  2360. // Send the message!
  2361. $external_msg_id = engagements_send_sms_to_number($mobile_phone, $header . "\n" . $msg, $student_id, $from_number);
  2362. if ($external_msg_id) {
  2363. fp_add_message(t("Text Message has been successfully sent."));
  2364. // save the external_msg_id with the content.
  2365. $content->field__external_msg_id['value'] = $external_msg_id;
  2366. $content->field__to_sms_phone['value'] = $mobile_phone;
  2367. $content->field__from_sms_phone['value'] = $from_number;
  2368. $content->log = "[AUTO] Updating external_msg_id";
  2369. content_save($content);
  2370. }
  2371. } // type == 'txt_msg'
  2372. if ($manual_entry != 'Y') {
  2373. watchdog("engagements", "sent $engagement_type to $student_id ($mobile_phone)");
  2374. }
  2375. else {
  2376. watchdog("engagements", "logged $engagement_type to $student_id");
  2377. }
  2378. watchdog("engagements", "$engagement_type message to $student_id: $header <br> $msg", array(), WATCHDOG_DEBUG);
  2379. } // ... send email or txt form submit
  2380. /**
  2381. * Actually send a text message. Will drupal_set_message() as an error if there is a problem, and return FALSE.
  2382. * Returns TRUE on success, no drupal_set_message is printed.
  2383. */
  2384. function engagements_send_sms_to_number($to_number, $body, $to_cwid = "", $from_number = "default", $bool_send_opt_out_message = TRUE)
  2385. {
  2386. $to_number = engagements_convert_to_valid_phone_number($to_number);
  2387. require_once(fp_get_module_path('engagements', TRUE, FALSE) . '/lib/signalwire/vendor/autoload.php');
  2388. //use SignalWire\Rest\Client;
  2389. // project id, auth tolen, space url
  2390. $project_id = variable_get('sms_project_id', '');
  2391. $auth_token = variable_get('sms_auth_token', '');
  2392. $space_url = variable_get('sms_space_url', '');
  2393. $from_phone = $from_number;
  2394. $phones = engagements_get_from_phones();
  2395. if ($from_phone == "default") {
  2396. $from_phone = engagements_convert_to_valid_phone_number($phones['default']['num']);
  2397. }
  2398. if ($bool_send_opt_out_message) {
  2399. // We must include a "text STOP to opt-out" type message to the bottom of every notification.
  2400. // TODO: May be a setting instead, on the signalwire settings page
  2401. $body .= "\n\n" . "Text STOP to opt-out of txt messages.";
  2402. }
  2403. // project id, auth tolen, space url
  2404. $client = new SignalWire\Rest\Client($project_id, $auth_token, array("signalwireSpaceUrl" => $space_url));
  2405. $external_msg_id = FALSE;
  2406. try {
  2407. $message = $client->messages
  2408. ->create(
  2409. "+1" . $to_number, // to
  2410. array(
  2411. "from" => "+1" . $from_phone, // my signalwire account phone number
  2412. "body" => $body
  2413. )
  2414. );
  2415. $external_msg_id = $message->sid;
  2416. // save message information to our sms_history table.
  2417. $record = $message;
  2418. $message_sid = $record->sid;
  2419. $from_mobile = engagements_convert_to_valid_phone_number($record->from);
  2420. $body = $record->body;
  2421. $num_segments = intval($record->numSegments);
  2422. $date_sent = $record->dateCreated; // Since we just created it, just dateCreated instead of dateSent.
  2423. $throw_away = print_r($date_sent, TRUE); // Not sure why, but I have to do this in order for the data field to populate.
  2424. //$date_sent_ts = strtotime($date_sent->date); // Was causing a mysql error all of a sudden. Just use current time.
  2425. $date_sent_ts = time();
  2426. // The price is not available right now, so we will set it...
  2427. $price = 0.0013 * $num_segments;
  2428. //$fp_price = engagements_get_fp_price($price);
  2429. $fp_price = 0.0033 * $num_segments;
  2430. $direction = $record->direction;
  2431. // Write our data to our sms_history table.
  2432. db_query(
  2433. "INSERT INTO sms_history (`message_sid`, `sw_type`, `body`, `from_number`, `to_number`, `sw_price`, `fp_price`, `to_cwid`, `updated`, `direction`, `media_filenames`, `date_sent`, price_processed, num_segments)
  2434. VALUES (?, 'sms', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ",
  2435. array($message_sid, $body, $from_mobile, $to_number, $price, $fp_price, $to_cwid, time(), $direction, '', $date_sent_ts, 0, $num_segments)
  2436. );
  2437. } catch (Exception $e) {
  2438. fp_add_message("An error has occured while trying to send a text message to <em>$to_number</em>. The
  2439. error has been logged. Please have the technical administrator investigate. Error message:" . $e->getMessage(), "error");
  2440. watchdog("engagements_sms", "An error has occured while trying to send a text message to <em>$to_number</em>. Error: " . $e->getMessage() . ". The complete
  2441. error is: <pre>" . print_r($e, true) . "</pre>", array(), WATCHDOG_ERROR);
  2442. return FALSE;
  2443. }
  2444. watchdog("engagements_sms", "SMS message sent successfully to $to_number. External_msg_id = " . $external_msg_id);
  2445. return $external_msg_id;
  2446. } // engagements_send_sms_to_number
  2447. /**
  2448. * Generates a URL to our tracking pixel, based on the cid included.
  2449. */
  2450. function engagements_create_new_tracking_img_url($cid)
  2451. {
  2452. $token = hash('sha512', ($cid . microtime() . mt_rand(9, 99999) . mt_rand(9, 99999)));
  2453. $url = $GLOBALS['fp_system_settings']['base_url'] . '/' . fp_url("engagements-track/$cid/$token/pixel.gif", '', FALSE);
  2454. // Write to database
  2455. db_query('INSERT INTO engagements_tracking (cid, token, opens, updated)
  2456. VALUES (?, ?, ?, ?)', array($cid, $token, 0, time()));
  2457. return $url;
  2458. }
  2459. /**
  2460. * displays the main Engagements tab, which shows the history of past engagements.
  2461. */
  2462. function engagements_display_main()
  2463. {
  2464. global $user, $current_student_id;
  2465. $faculty_id = 0;
  2466. if ($user->is_faculty) {
  2467. $faculty_id = $user->cwid;
  2468. }
  2469. fp_set_title('');
  2470. fp_add_css(fp_get_module_path("engagements") . "/css/style.css");
  2471. fp_add_js(fp_get_module_path("engagements") . "/js/engagements.js");
  2472. fp_add_js(fp_get_module_path("advise") . "/js/advise.js");
  2473. $types = content_get_types();
  2474. $icons = array(
  2475. 'phone' => 'fa-phone',
  2476. 'email' => 'fa-envelope',
  2477. 'txt_msg' => 'fa-comment',
  2478. 'note' => 'fa-file-text',
  2479. 'in_person' => 'fa-user',
  2480. 'social_media' => 'fa-cloud',
  2481. 'other' => 'fa-asterisk',
  2482. 'video_chat' => 'fa-video-camera',
  2483. 'reminder' => 'fa-thumb-tack',
  2484. );
  2485. $rtn = "";
  2486. $rtn .= "<div id='engagements-list-page'>";
  2487. $rtn .= "<div class='add-engagements-buttons'>";
  2488. if (user_has_permission('can_send_email_engagements')) {
  2489. $rtn .= "<a href='javascript:engagementsNewEngagementDialog(\"email\",\"New Email\",\"$current_student_id\",\"$faculty_id\");' class='button'><i class='fa fa-envelope-o'></i>&nbsp; New Email</a>";
  2490. }
  2491. if (user_has_permission('can_send_txt_engagements')) {
  2492. $rtn .= "<a href='javascript:engagementsNewEngagementDialog(\"txt_msg\",\"New Text Message\",\"$current_student_id\",\"$faculty_id\");' class='button'><i class='fa fa-comment-o'></i>&nbsp; New TXT</a>";
  2493. }
  2494. if (user_has_permission('can_log_engagements')) {
  2495. $rtn .= "<a href='javascript:engagementsNewEngagementDialog(\"\",\"Log New Engagement\",\"$current_student_id\",\"$faculty_id\");' class='button'><i class='fa fa-plus'></i>&nbsp; Log New Engagement</a>";
  2496. }
  2497. $rtn .= "</div> <!-- add-engagements-buttons -->
  2498. <div class='clear'></div>
  2499. ";
  2500. $rtn .= fp_render_section_title("Recent Engagements");
  2501. $rtn .= "<div class='engagements-list'>";
  2502. $res = pager_query("SELECT DISTINCT(a.cid) FROM content__engagement a, content n
  2503. WHERE field__student_id = ?
  2504. AND a.vid = n.vid
  2505. AND a.cid = n.cid
  2506. AND n.delete_flag = 0
  2507. AND n.published = 1
  2508. ORDER BY a.vid DESC, field__activity_datetime DESC", array($current_student_id), 5, 0, "SELECT COUNT(DISTINCT(a.cid)) FROM content__engagement a, content n
  2509. WHERE field__student_id = ?
  2510. AND a.vid = n.vid
  2511. AND a.cid = n.cid
  2512. AND n.delete_flag = 0
  2513. AND n.published = 1");
  2514. while ($cur = db_fetch_object($res)) {
  2515. $cid = $cur->cid;
  2516. $content = content_load($cid);
  2517. // is this "faculty" visibility? If so, do we have access to view?
  2518. if ($content->field__visibility['value'] == 'faculty' && !user_has_permission('can_view_faculty_engagements')) {
  2519. continue;
  2520. }
  2521. $visibility_icon = "";
  2522. if ($content->field__visibility['value'] == 'faculty') {
  2523. $visibility_icon = "<i class='fa fa-lock' title='Visibile to Faculty/Staff only'></i>";
  2524. }
  2525. $icon = $icons[$content->field__engagement_type['value']];
  2526. $type = $content->field__engagement_type['value'];
  2527. $direction = $content->field__direction['value'];
  2528. $msg = $content->field__engagement_msg['display_value'];
  2529. $manual_entry = $content->field__manual_entry['value'];
  2530. $external_msg_id = @trim($content->field__external_msg_id['value']);
  2531. $pre_description = "";
  2532. $sms_extra_details = $sms_delivery_details = "";
  2533. if ($type == 'txt_msg' && $external_msg_id) {
  2534. $phones = engagements_get_from_phones();
  2535. $details = engagements_get_sms_from_history($external_msg_id);
  2536. if ($details) {
  2537. $desc = @$phones['lines'][$details['to_number']]['description'];
  2538. if (!$desc) $desc = @$phones['lines'][$details['from_number']]['description'];
  2539. $sms_extra_details = "<span class='sms-extra-details'>";
  2540. $sms_extra_details .= t("(%desc)", array('%desc' => $desc));
  2541. $sms_extra_details .= "</span>";
  2542. if (isset($details['delivery_status']) && $details['delivery_status'] == 'undelivered') {
  2543. $moreinfo = "";
  2544. $moreinfo .= "<p>" . t("We're sorry, but this message was not delivered.
  2545. Usually this happens when the recipient's carrier (ex: AT&T, Verizon, etc) marks the message as 'spam' and refuses to deliver
  2546. it to the user. <strong>Unfortunately, this is something which is outside the control of @fp.</strong>
  2547. <br><br>
  2548. This may also be due to the user's phone being invalid, disconnected, or incapable of receiving SMS text messages.
  2549. <br><br>
  2550. Your IT/@fp administrator may be able to log into your
  2551. SMS vendor account for more information.
  2552. <br><br>
  2553. Note: the recipient <strong><u>did not receive this message</u></strong>. Please try again via email or another
  2554. communication route.", array("@fp" => variable_get("system_name", "FlightPath"))) . "</p>";
  2555. $moreinfo = base64_encode($moreinfo);
  2556. $sms_delivery_details .= "<div class='sms-undelivered-error'><i class='fa fa-warning'></i> " . t("Undelivered by phone carrier network - %msg", array("%msg" => @$details['err_friendly_message'])) . "
  2557. &nbsp; <a href='javascript:fp_alert(\"$moreinfo\",\"base64\");' title='" . t("More information") . "'><i class='fa fa-question-circle'></i></a></div>";
  2558. }
  2559. }
  2560. }
  2561. if ($manual_entry == 'Y') {
  2562. // This was a manually logged engagement.
  2563. $edit_link = "";
  2564. // Is the user allowed to EDIT this manual engagement?
  2565. if (content_user_access("edit", $content->cid)) {
  2566. $edit_link = "<a href='javascript:fpOpenLargeIframeDialog(\"" . fp_url("content/$cid/edit", "window_mode=popup&content_tabs=false") . "\",\"Edit Alert\");' title='Edit' class='action-link'><i class='fa fa-pencil'></i></a>";
  2567. }
  2568. $pre_description .= t("Logged:") . " $edit_link &nbsp; &nbsp;";
  2569. }
  2570. ///////////
  2571. // This section we can instead query the content_files table for this cid.
  2572. if ($type == 'txt_msg') {
  2573. $ttemp = array('','');
  2574. if (strstr($msg, "~~MEDIA~~") && strstr($msg, "~~END_MEDIA~~")) {
  2575. $temp = explode("~~MEDIA~~", $msg);
  2576. $msg = $temp[0]; // get rid of the media part of the msg.
  2577. $ttemp = explode("~~END_MEDIA~~", $temp[1]);
  2578. }
  2579. $res2 = db_query("SELECT * FROM content_files WHERE cid = ? ORDER BY cid", array($cid));
  2580. while ($cur2 = db_fetch_array($res2)) {
  2581. $fid = $cur2['fid'];
  2582. $file = content_get_uploaded_file($fid);
  2583. $img_tag = "<img src='{$file['url']}' style='max-width: 200px; height: auto;'>";
  2584. $msg .= "<div class='media-attached'><a href='{$file['url']}' target='_BLANK'>
  2585. $img_tag
  2586. </a></div>";
  2587. }
  2588. $msg .= $ttemp[1]; // in case there was anything AFTER ~~END_MEDIA~~
  2589. }
  2590. /*
  2591. // If the $msg contains ~~MEDIA~~ and ~~END_MEDIA~~ then we can assume there are filenames after it, which we assume to be images that got
  2592. // text messaged. Add them.
  2593. $files = explode(",", $temp[1]);
  2594. foreach ($files as $filename) {
  2595. $ttemp = explode("~~END_MEDIA~~", $filename);
  2596. $filename = $ttemp[0];
  2597. $path = base_path() . '/custom/files/content_uploads/' . $filename;
  2598. $img_tag = "<img src='$path' style='max-width: 200px; height: auto;'>";
  2599. $ext_temp = explode(".", $filename);
  2600. $ext = strtolower(@$ext_temp[count($ext_temp) - 1]);
  2601. $image_exts = array("gif", "tiff", "jpg", "jpeg", "png", "bmp");
  2602. if (!in_array($ext, $image_exts)) {
  2603. $mime_icon = content_get_fontawesome_icon_for_mimetype("", $ext);
  2604. $img_tag = "<div class='atached-file'><i class='fa $mime_icon'></i>" . " " . t("View %ext attachment in new window", array("%ext" => $ext)) . "</div>";
  2605. }
  2606. $msg .= "<div class='media-attached'><a href='$path' target='_BLANK'>
  2607. $img_tag
  2608. </a></div>" . $ttemp[1]; // add on the </p> at the end if its there.
  2609. }
  2610. } // contains ~~MEDIA~~
  2611. */
  2612. ////////////
  2613. $header_desc_extra = "";
  2614. if ($type == 'email' && $direction == 'sent' && $manual_entry != 'Y') {
  2615. //$header_desc_extra = "&nbsp; &nbsp; &nbsp; &nbsp; Opens: 1";
  2616. $num_opens = intval(db_result(db_query("SELECT opens FROM engagements_tracking WHERE cid = ?", array($content->cid))));
  2617. $header_desc_extra = "<span class='header-desc-extra'>Opens: $num_opens</span>";
  2618. }
  2619. $fid_csv = @trim($content->field__attachment['value']);
  2620. if ($fid_csv) {
  2621. // were there any attachments?
  2622. $attachment_icon = "";
  2623. if ($fid_csv) {
  2624. $attachment_icon = "<i class='fa fa-paperclip'></i> ";
  2625. $attachments = array();
  2626. $attachment_csv = trim(@$content->field__attachment['value']);
  2627. // TODO: An option here would be to just query the content_files table for this cid.
  2628. if ($attachment_csv) {
  2629. $temp = explode(",", $attachment_csv);
  2630. $msg .= "<label>" . t("Attached File(s):") . "</label><div class='attached-files'>";
  2631. foreach ($temp as $fid) {
  2632. if ($fid == "") continue;
  2633. $file = content_get_uploaded_file($fid);
  2634. if ($file) {
  2635. $mime_icon = content_get_fontawesome_icon_for_mimetype($file['mimetype'], $file['ext']);
  2636. $msg .= "<div class='attached-file'><a href='{$file['url']}'><i class='fa $mime_icon'></i> {$file['original_filename']}</a></div>";
  2637. }
  2638. }
  2639. $msg .= "</div>";
  2640. }
  2641. }
  2642. // We need a random id to assign to this email body.
  2643. $bodyid = "bid_" . md5(microtime() . mt_rand(9, 99999) . mt_rand(9, 9999));
  2644. // If the msg body is too long, let it be expandable.
  2645. $more_link = $less_link = "";
  2646. if (strlen($msg) > 500) {
  2647. $more_link = "<a href='javascript:engagementsExpandBody(\"$bodyid\");' class='more-body-link' id='more_$bodyid'>More</a>";
  2648. $less_link = "<a href='javascript:engagementsShrinkBody(\"$bodyid\");' class='less-body-link' id='less_$bodyid' style='display:none;'>Less</a>";
  2649. }
  2650. $msg = "<div class='email-subject'>{$attachment_icon}Subject: $content->title</div><div class='email-body' id='$bodyid'>$msg</div>$more_link$less_link";
  2651. }
  2652. $author_user_id = intval($content->user_id);
  2653. $author_account = fp_load_user($author_user_id);
  2654. $author_name = $author_account->f_name . " " . $author_account->l_name;
  2655. $act_feed_extra_class = "";
  2656. if ($content->field__from_sms_phone['value']) {
  2657. $act_feed_extra_class .= " activity-feed-from-sms-" . engagements_convert_to_valid_phone_number(fp_get_machine_readable($content->field__from_sms_phone['value']));
  2658. }
  2659. if ($content->field__to_sms_phone['value']) {
  2660. $act_feed_extra_class .= " activity-feed-to-sms-" . engagements_convert_to_valid_phone_number(fp_get_machine_readable($content->field__to_sms_phone['value']));
  2661. }
  2662. $rtn .= "<div class='activity-feed-teaser activity-feed-teaser-$direction activity-feed-teaser-type-$type $act_feed_extra_class'>
  2663. <div class='activity-header-content'>
  2664. <span class='header-icon'><i class='fa $icon'></i></span>
  2665. <span class='header-description'>
  2666. <span class='pre-description'>$pre_description</span>
  2667. {$content->field__engagement_type['display_value']}
  2668. </span>$sms_extra_details
  2669. <span class='header-phone-outcome'>: {$content->field__direction['display_value']} $header_desc_extra</span>
  2670. $sms_delivery_details
  2671. <span class='header-edit'></span>
  2672. <span class='header-date-time'>" . $content->field__activity_datetime['display_value'] . "</span>
  2673. <span class='header-author'>$author_name</span>
  2674. <span class='header-visibility'>$visibility_icon</span>
  2675. </div>
  2676. <div class='activity-contents'>
  2677. <div class='activity-comment'>
  2678. $msg
  2679. </div>
  2680. </div>
  2681. </div>
  2682. <div class='clear'></div>";
  2683. // mark this content as "read" since it appeared on the screen.
  2684. content_set_last_access($content->cid);
  2685. } // while cur
  2686. // Display the pager that was generated by the pager_query above!
  2687. $rtn .= theme_pager(array(t('« newest'), t('‹ newer'), '', t('older ›'), t('oldest »')));
  2688. $rtn .= "</div>"; // engagements-list
  2689. $rtn .= "</div>"; // engagements-list-page
  2690. // Let's set our breadcrumbs
  2691. $db = get_global_database_handler();
  2692. $crumbs = array();
  2693. $crumbs[] = array(
  2694. 'text' => 'Students',
  2695. 'path' => 'student-search',
  2696. );
  2697. $crumbs[] = array(
  2698. 'text' => $db->get_student_name($current_student_id) . " ({$current_student_id})",
  2699. 'path' => 'student-profile',
  2700. 'query' => "current_student_id={$current_student_id}",
  2701. );
  2702. fp_set_breadcrumbs($crumbs);
  2703. watchdog("engagements", "view $current_student_id");
  2704. return $rtn;
  2705. } // engagements_display_main

Functions

Namesort descending Description
engagements_can_send_sms_to_number
engagements_content_register_content_type For use with the content module. We will register our custom content type(s) for use with this module.
engagements_convert_to_pretty_phone_number
engagements_convert_to_valid_phone_number Converts the string into a plain phone number, then tests to see if it is valid or not. RETURNS FALSE if not valid, otherwise, returns the converted phone number. This will be a valid number for use with our SMS service. (in the US anyway).
engagements_create_new_tracking_img_url Generates a URL to our tracking pixel, based on the cid included.
engagements_cron Implements hook_cron
engagements_display_advisee_engagements_page
engagements_display_main displays the main Engagements tab, which shows the history of past engagements.
engagements_form_alter Implements hook_form_alter
engagements_get_alert_count_by_type Implements hook_get_count_for_alert_type
engagements_get_file_extension_from_mime_type This function will look at the mime type (aka content type) to figure out what the file extension should be. This is useful when retrieving txt message media.
engagements_get_fp_price figure out the price we will charge.
engagements_get_from_phones Get the available "from phone" numbers in an organized array structure.
engagements_get_from_phones_for_fapi Returns back the phone lines available.
engagements_get_signalwire_sms_error_codes_array
engagements_get_sms_from_history
engagements_get_users_to_be_notified_for_sms_on_number Returns an array of all the user who should receive notifications when we receive an SMS at a particular number.
engagements_get_user_notify_sms_receipt_values Get an array of numbers which the user should be notified of when they receive an SMS.
engagements_handle_incoming_sms This catches incoming sms messages from POST, but can also be used by our "sms_get_all_messages" function, but it is also used by the sms_get_all_messages to save/update information.
engagements_handle_sms_stop This function is called by engagements_handle_incoming_sms, when we receive 'STOP' from the user. We must add them to our "sms_do_not_txt" table, and send a reply letting them know how to re-subscribe in the future.
engagements_handle_sms_unstop User opted-IN to receiving txt messages.
engagements_handle_tracking_pixel_request The user has opened an email with a tracking pixel. We will now record that it was opened in our engagements_tracking table.
engagements_imap_get_all_received_messages Connect to our imap server, download all received messages from students (or others). We will then delete them, so they don't get read twice.
engagements_imap_settings_form Configure the imap settings used by Engagements
engagements_mass_sms_form
engagements_mass_sms_form_submit Our submit function. Our main task is simply to set off a batch routine.
engagements_mass_sms_form_validate
engagements_mass_sms_perform_batch_operation
engagements_menu Implement hook_menu
engagements_menu_handle_replacement_pattern implements hook_menu_handle_replacement_pattern
engagements_perm
engagements_send_email_or_txt_form_submit
engagements_send_email_or_txt_form_validate
engagements_send_sms_to_number Actually send a text message. Will drupal_set_message() as an error if there is a problem, and return FALSE. Returns TRUE on success, no drupal_set_message is printed.
engagements_sms_get_all_messages Retrieve all messages, update the ones which don't have prices associated with them yet.
engagements_sms_replace_patterns
engagements_sms_settings_form Configure the various SMS settings
_engagements_imap_get_attachments $inbox is the imap link. email_number is msg_no.
_engagements_imap_get_body