engagements.module

  1. 7.x modules/engagements/engagements.module
  2. 6.x modules/engagements/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.
  838. // TODO: Maybe this is a setting?
  839. $sms_check_against = strtotime("NOW - 30 MINUTES");
  840. if ($sms_check_against >= $last_sms_check) {
  841. engagements_sms_get_all_messages(); // refreshes sms information for messages we've sent and received
  842. }
  843. // Check for imap messages in a similar way as the SMS messages.
  844. engagements_imap_get_all_received_messages();
  845. } // hook_cron
  846. /**
  847. * 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
  848. * sms_get_all_messages to save/update information.
  849. *
  850. * TODO: Have a way to handle incoming calls, too, so we can charge correctly.
  851. *
  852. */
  853. function engagements_handle_incoming_sms($vars = array())
  854. {
  855. // If we did not supply a set of variables, use the POST superglobal
  856. if (count($vars) === 0) $vars = $_POST;
  857. watchdog("engagements_sms", "Received from vars (possibly POST): <pre>" . print_r($vars, TRUE) . '</pre>', array(), WATCHDOG_DEBUG);
  858. $message_sid = $vars['MessageSid'];
  859. $from_mobile = engagements_convert_to_valid_phone_number($vars['From']);
  860. $to_mobile = engagements_convert_to_valid_phone_number($vars['To']);
  861. $system_name = variable_get("system_name", "FlightPath");
  862. $body = $vars['Body'];
  863. $num_media = intval($vars['NumMedia']);
  864. $num_segments = intval($vars['NumSegments']);
  865. $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).
  866. if ($num_media > 0) {
  867. $price = 0.01; // Contained at least one piece of media, so price increases. This is SignalWire's price.
  868. }
  869. $direction = "inbound";
  870. $fp_price = engagements_get_fp_price($price, $direction, $num_segments);
  871. $date_sent_ts = time(); // the time we received it.
  872. if (isset($vars['date_sent_ts'])) {
  873. $date_sent_ts = $vars['date_sent_ts'];
  874. }
  875. if (isset($vars['direction'])) {
  876. $direction = $vars['direction'];
  877. }
  878. $content = NULL;
  879. $link = $alert_link = "";
  880. // Figure out what user this came from/to!
  881. $from_cwid = $from_user_id = $to_user_id = "";
  882. $user_id = db_result(db_query("SELECT user_id FROM user_attributes WHERE `name` = 'mobile_phone' AND `value` = ?", array($from_mobile)));
  883. if ($user_id) {
  884. $from_cwid = db_get_cwid_from_user_id($user_id);
  885. $from_user_id = $user_id;
  886. }
  887. $to_cwid = "";
  888. $user_id = db_result(db_query("SELECT user_id FROM user_attributes WHERE `name` = 'mobile_phone' AND `value` = ?", array($to_mobile)));
  889. if ($user_id) {
  890. $to_cwid = db_get_cwid_from_user_id($user_id);
  891. $to_user_id = $user_id;
  892. }
  893. $dir = "received";
  894. $fac_cwid = $to_cwid;
  895. $stu_cwid = $from_cwid;
  896. if (strstr($direction, 'outbound')) {
  897. $dir = 'sent';
  898. $fac_cwid = $from_cwid;
  899. $stu_cwid = $to_cwid;
  900. }
  901. // Did we receive STOP?
  902. if ($dir == 'received') {
  903. if (trim(strtoupper($body)) === 'STOP') {
  904. engagements_handle_sms_stop($from_user_id, $from_cwid, $from_mobile, $to_mobile);
  905. }
  906. // UNSTOP or SUBSCRIBE?
  907. if (trim(strtoupper($body)) === 'UNSTOP' || trim(strtoupper($body)) === 'SUBSCRIBE') {
  908. engagements_handle_sms_unstop($from_user_id, $from_cwid, $from_mobile, $to_mobile);
  909. }
  910. // We do not return. Go ahead and let it create an engagement below, so the
  911. // advisor can see that this happened.
  912. }
  913. if (trim($stu_cwid) != "") {
  914. // create an engagement content node for this student!
  915. $content = new stdClass();
  916. $content->type = 'engagement';
  917. $content->cid = "new";
  918. $content->published = 1;
  919. $content->delete_flag = 0;
  920. $content->title = "Engagement: txt_msg re: student $stu_cwid"; // required
  921. $content->field__activity_datetime['value'] = date('Y-m-d H:i:s', $date_sent_ts);
  922. $content->field__faculty_id['value'] = $fac_cwid;
  923. $content->field__student_id['value'] = $stu_cwid;
  924. $content->field__engagement_type['value'] = 'txt_msg';
  925. $content->field__direction['value'] = $dir;
  926. $content->field__from_sms_phone['value'] = $from_mobile;
  927. $content->field__to_sms_phone['value'] = $to_mobile;
  928. content_save($content);
  929. $cid = $content->cid;
  930. $media_filenames = "";
  931. // Handle retreiving and saving media.
  932. if ($num_media > 0) { // Do we have any media?
  933. for ($t = 0; $t < $num_media; $t++) {
  934. $media_url = $vars["MediaUrl$t"];
  935. $media_type = $vars["MediaContentType$t"];
  936. $ext = engagements_get_file_extension_from_mime_type($media_type);
  937. if (!$ext || $ext == "html" || $ext == "htm") continue; // skip if its not a type we care about.
  938. // Create a new random filename to save it as.
  939. $filename = 'sms__' . $from_mobile . '_' . time() . '_' . mt_rand(1, 9999) . '.' . $ext;
  940. $tmp_name = sha1($filename . mt_rand(1,9999) . time() . mt_rand(1,9999));
  941. $file_contents = file_get_contents($media_url);
  942. // Save to our temporary location on the server.
  943. $test = file_put_contents(sys_get_temp_dir() . '/' . $tmp_name, $file_contents);
  944. if (!$test) {
  945. watchdog('engagements_sms', 'Unable to write media file at temp location: ' . sys_get_temp_dir(), array(), 'error');
  946. fpm('Unable to write media file at temp location:' . sys_get_temp_dir() . '. Permissions issue?');
  947. continue;
  948. }
  949. $file = array(
  950. 'name' => $filename,
  951. 'type' => $media_type,
  952. 'tmp_name' => $tmp_name,
  953. );
  954. // This function will handle encryption of files automatically.
  955. $fid = content_add_new_uploaded_file($file, $cid, FALSE);
  956. $media_filenames .= $filename . ',';
  957. } // for t (media)
  958. $media_filenames = rtrim($media_filenames, ',');
  959. } // if num media > 0
  960. // if we have media, add to the end of body.
  961. if ($media_filenames) {
  962. $body .= "~~MEDIA~~$media_filenames~~END_MEDIA~~";
  963. }
  964. $content->field__engagement_msg['value'] = $body;
  965. $content->field__external_msg_id['value'] = $message_sid;
  966. $content->field__visibility['value'] = 'public';
  967. content_save($content);
  968. } // We have a student to attach this to.
  969. if ($dir != "sent") { // meaning, we RECEIVED this message.
  970. $already_notified_users = array();
  971. $student_name = fp_get_student_name($stu_cwid, TRUE);
  972. $base_url = $GLOBALS['fp_system_settings']['base_url'];
  973. $link = $base_url . "/engagements?current_student_id=$stu_cwid";
  974. $phones = engagements_get_from_phones();
  975. $desc = "";
  976. $to = $to_mobile;
  977. $pretty_to_number = engagements_convert_to_pretty_phone_number($to);
  978. $desc = @$phones['lines'][$to]['description'];
  979. if (!$desc) $desc = @$phones['lines'][$from_mobile]['description'];
  980. if ($desc) $desc = " $desc";
  981. if (trim($stu_cwid) != "") {
  982. // Create a new "activity record" that the student has sent a txt message
  983. $acontent = new stdClass();
  984. $acontent->type = 'activity_record';
  985. $acontent->cid = "new";
  986. $acontent->published = 1;
  987. $acontent->delete_flag = 0;
  988. $acontent->title = t('Student sent a text message to ') . $pretty_to_number . " / " . $desc;
  989. $acontent->field__student_id['value'] = $stu_cwid;
  990. $acontent->field__activity_type['value'] = 'comment';
  991. content_save($acontent);
  992. // Notify the advisor(s).
  993. $tmsg = "$student_name has submitted a text message to $system_name
  994. (To: $pretty_to_number / $desc).
  995. <br><br>\n\n
  996. To view, visit the student's Engagements tab by logging in and following this link:<br>\n
  997. <a href='$link'>$link</a>";
  998. $advisors = advise_get_advisors_for_student($stu_cwid);
  999. foreach ($advisors as $c => $afaculty_id) {
  1000. $account_user_id = db_get_user_id_from_cwid($afaculty_id, 'faculty');
  1001. if ($account_user_id) {
  1002. notify_send_notification_to_user($account_user_id, $tmsg, $content->cid, 'engagement');
  1003. $already_notified_users[] = $account_user_id;
  1004. }
  1005. } // foreach advisors
  1006. }
  1007. /*
  1008. * TODO: Only do this if we have a setting set?
  1009. * 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.
  1010. *
  1011. if (trim($stu_cwid) == "") {
  1012. // Meaning, we couldn't find this student in our system!
  1013. // 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
  1014. // TODO: the foreach loop below.
  1015. $tmsg .= "A text message has been received for $system_name. (To: $pretty_to_number / $desc).<br><br> No student could be found which is
  1016. associated with the sending number ($from_mobile).<br><br>Message body:<br>---------<br>
  1017. $body<br><br>Message SID:$message_sid.";
  1018. }
  1019. */
  1020. // Notify other users who might have been assigned to receive notifications
  1021. $to_be_notified = engagements_get_users_to_be_notified_for_sms_on_number($to_mobile);
  1022. foreach ($to_be_notified as $account_user_id) {
  1023. if (in_array($account_user_id, $already_notified_users)) continue;
  1024. if (trim($stu_cwid) == "") {
  1025. // Meaning, we couldn't find this student in our system!
  1026. // TODO: Create an alert for the recipient which does show the full txt message. The notification provides a link to the alert.
  1027. $alert = new stdClass();
  1028. $alert->type = 'alert';
  1029. $alert->cid = "new";
  1030. $alert->published = 1;
  1031. $alert->delete_flag = 0;
  1032. $alert->title = t('Unrecognized sender sent a text message to ') . $pretty_to_number . " / " . $desc;
  1033. $alert->field__alert_msg['value'] = $body;
  1034. $alert->field__alert_status['value'] = 'open';
  1035. $alert->field__visibility['value'] = 'faculty';
  1036. $alert->field__target_faculty_id['value'] = db_get_cwid_from_user_id($account_user_id);
  1037. $alert->field__student_id['value'] = "N/A";
  1038. $alert->field__exclude_advisor['value'] = 1;
  1039. content_save($alert);
  1040. $alert_link = $base_url . "/content/$alert->cid?content_crumbs=alerts";
  1041. $tmsg = "A text message has been received for $system_name. (To: $pretty_to_number / $desc).<br><br> No student could be found which is
  1042. associated with the sending number ($from_mobile).<br><br>Message SID: $message_sid.
  1043. <br><br>To view the full message, view the Alert by logging in and clicking here:
  1044. <a href='$alert_link'>$alert_link</a>";
  1045. notify_send_notification_to_user($account_user_id, $tmsg, $alert->cid, 'alert');
  1046. }
  1047. else {
  1048. // We DO have a student this came from. Let this user know normally.
  1049. $tmsg = "$student_name has submitted a text message to $system_name (To: $pretty_to_number / $desc).
  1050. <br><br>\n\n
  1051. To view, visit the student's Engagements tab by logging in and following this link:<br>\n
  1052. <a href='$link'>$link</a>";
  1053. notify_send_notification_to_user($account_user_id, $tmsg, $content->cid, 'engagement');
  1054. }
  1055. } //foreach to_be_notified
  1056. } // dir != sent (we received this text)
  1057. // Write our data to our sms_history table.
  1058. db_query(
  1059. "INSERT INTO sms_history (`message_sid`, sw_type, `body`, `from_number`, `to_number`, `sw_price`, `fp_price`, `from_cwid`, `to_cwid`, `updated`, `direction`,
  1060. `media_filenames`, `date_sent`, price_processed, num_segments, delivery_status, err_code, err_message, err_friendly_message)
  1061. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ",
  1062. array($message_sid, 'sms', $body, $from_mobile, $to_mobile, $price, $fp_price, $from_cwid, $to_cwid, time(), $direction,
  1063. $media_filenames, $date_sent_ts, 1, $num_segments, @$vars['display_status'], @$vars['err_code'], @$vars['err_message'], @$vars['err_friendly_message'])
  1064. );
  1065. } // engagements_handle_incoming_sms
  1066. /**
  1067. * This function is called by engagements_handle_incoming_sms, when we receive 'STOP' from the user.
  1068. * 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.
  1069. */
  1070. function engagements_handle_sms_stop($user_id = 0, $cwid = '', $phone_number = '', $from_fp_phone_number = '') {
  1071. if ($user_id === 0) {
  1072. // Try to figure out what the user_id is based on the phone number.
  1073. $test = db_result(db_query("SELECT user_id FROM user_attributes WHERE name = 'mobile_phone' AND value = ?", array($from_fp_phone_number)));
  1074. $test = intval($test);
  1075. if ($test > 0) $user_id = $test;
  1076. }
  1077. $phone_number = engagements_convert_to_valid_phone_number($phone_number);
  1078. if (!$phone_number) {
  1079. // watchdog that there was an error because phone number not valid.
  1080. watchdog('engagements_sms', "User tried to opt-out of sms, but invalid phone number: $user_id, $cwid, $phone_number", array(), WATCHDOG_ERROR);
  1081. return FALSE;
  1082. }
  1083. if ($user_id) {
  1084. // Get current notification method for this user.
  1085. $prev_notification_method = user_get_setting($user_id, 'default_notification_method', 'email');
  1086. // Set their new defailt_notification_method to just email.
  1087. user_set_setting($user_id, 'default_notification_method', 'email');
  1088. // also set an opt-out setting value here, so the user can change it from within flightpath.
  1089. user_set_setting($user_id, "sms_opt_out__" . $phone_number, 'yes');
  1090. }
  1091. // 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)
  1092. db_query('DELETE FROM sms_do_not_txt WHERE phone_number = ?', array($phone_number));
  1093. db_query("INSERT INTO sms_do_not_txt (user_id, cwid, phone_number, prev_notification_method, updated)
  1094. VALUES (?, ?, ?, ?, ?)", array($user_id, $cwid, $phone_number, $prev_notification_method, time()));
  1095. // Send final sms back to number telling them how to re-enable.
  1096. $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.",
  1097. array("@flightpath" => variable_get("system_name", "FlightPath")));
  1098. engagements_send_sms_to_number($phone_number, $msg, '', $from_fp_phone_number, FALSE);
  1099. watchdog('engagements_sms', "User opted-out of sms: $user_id, $cwid, $phone_number");
  1100. } // engagements_handle_sms_stop
  1101. /**
  1102. * User opted-IN to receiving txt messages.
  1103. */
  1104. function engagements_handle_sms_unstop($user_id = 0, $cwid = '', $phone_number = '', $from_fp_phone_number = '') {
  1105. if ($user_id === 0) {
  1106. // Try to figure out what the user_id is based on the phone number.
  1107. $test = db_result(db_query("SELECT user_id FROM user_attributes WHERE name = 'mobile_phone' AND value = ?", array($from_fp_phone_number)));
  1108. $test = intval($test);
  1109. if ($test > 0) $user_id = $test;
  1110. }
  1111. $phone_number = engagements_convert_to_valid_phone_number($phone_number);
  1112. if (!$phone_number) {
  1113. // watchdog that there was an error because phone number not valid.
  1114. watchdog('engagements_sms', "User tried to opt-in to sms, but invalid phone number: $user_id, $cwid, $phone_number", array(), WATCHDOG_ERROR);
  1115. return FALSE;
  1116. }
  1117. if ($user_id) {
  1118. // Get current notification method for this user.
  1119. $notification_method = db_result(db_query("SELECT prev_notification_method FROM sms_do_not_txt WHERE phone_number = ?", array($phone_number)));
  1120. if ($notification_method) {
  1121. // Set their new defailt_notification_method to just email.
  1122. user_set_setting($user_id, 'default_notification_method', $notification_method);
  1123. }
  1124. // also set our opt-out setting value here, so the user can change it from within flightpath.
  1125. user_set_setting($user_id, "sms_opt_out__" . $phone_number, 'no');
  1126. }
  1127. // 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)
  1128. db_query('DELETE FROM sms_do_not_txt WHERE phone_number = ?', array($phone_number));
  1129. // Send final sms back to number telling them how to re-enable.
  1130. $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.",
  1131. array("@flightpath" => variable_get("system_name", "FlightPath")));
  1132. engagements_send_sms_to_number($phone_number, $msg, '', $from_fp_phone_number, FALSE);
  1133. watchdog('engagements_sms', "User opted-out of sms: $user_id, $cwid, $phone_number");
  1134. }
  1135. function engagements_can_send_sms_to_number($phone_number) {
  1136. $phone_number = engagements_convert_to_valid_phone_number($phone_number);
  1137. if (!$phone_number) return FALSE;
  1138. $mid = db_result(db_query("SELECT mid FROM sms_do_not_txt WHERE phone_number = ?", array($phone_number)));
  1139. // We found an entry in this table, so NO, we cannot txt this number.
  1140. if ($mid) return FALSE;
  1141. // else
  1142. return TRUE;
  1143. }
  1144. /**
  1145. *
  1146. */
  1147. function engagements_get_signalwire_sms_error_codes_array() {
  1148. $rtn = array(
  1149. "10002" => "Trial account does not support this feature",
  1150. "11200" => "HTTP Retrieval Failure",
  1151. "11751" => "MMS -> Media exceeds mobile operator size limit",
  1152. "12100" => "Document Parse Failure",
  1153. "12300" => "Invalid Content-Type",
  1154. "13221" => "Dial -> Number: Invalid Method Value",
  1155. "15002" => "Call Progress: Queue Timeout",
  1156. "21210" => "'From' phone number not verified",
  1157. "21217" => "A call or SMS is placed to a destination which does not represent a valid phone number",
  1158. "21219" => "'To' phone number not verified",
  1159. "21601" => "Phone number is not a valid SMS-capable inbound phone number",
  1160. "21602" => "Message body is required",
  1161. "21603" => "The source 'From' phone number is required to send an SMS",
  1162. "21604" => "The destination 'To' phone number is required to send an SMS",
  1163. "21610" => "Attempt to send to unsubscribed recipient",
  1164. "21611" => "This 'From' number has exceeded the maximum number of queued messages",
  1165. "21617" => "The concatenated message body exceeds the 1600 character limit",
  1166. "21620" => "Invalid media URL(s)",
  1167. "21623" => "Number of media files exceeds allowed limit",
  1168. "30002" => "Account suspended",
  1169. "30003" => "Unreachable destination handset",
  1170. "30004" => "Message blocked or opted out",
  1171. "30005" => "Unknown destination handset",
  1172. "30006" => "Landline or unreachable carrier",
  1173. "30007" => "Message marked as spam",
  1174. "30008" => "Unknown error",
  1175. "30010" => "Message price exceeds max price",
  1176. "30022" => "Campaign Registry Throughput Limit Exceeded",
  1177. );
  1178. return $rtn;
  1179. }
  1180. /**
  1181. * Retrieve all messages, update the ones which don't have prices associated with them yet.
  1182. *
  1183. * We will also be getting voice calls & prices too for our log table.
  1184. */
  1185. function engagements_sms_get_all_messages()
  1186. {
  1187. require_once(fp_get_module_path('engagements', TRUE, FALSE) . '/lib/signalwire/vendor/autoload.php');
  1188. $vars = array();
  1189. $error_codes = engagements_get_signalwire_sms_error_codes_array();
  1190. // project id, auth tolen, space url
  1191. $project_id = variable_get('sms_project_id', '');
  1192. if ($project_id == "") {
  1193. return FALSE;
  1194. }
  1195. $auth_token = variable_get('sms_auth_token', '');
  1196. $space_url = variable_get('sms_space_url', '');
  1197. $client = new SignalWire\Rest\Client($project_id, $auth_token, array("signalwireSpaceUrl" => $space_url));
  1198. // Get all messages so we can update our table.
  1199. // We only care about messages sent TODAY, since we theoretically already got them yesterday
  1200. $check_since = date('Y-m-d', strtotime("NOW - 3 HOURS")); // We will get messages from 3 hours in the past and today,
  1201. // (in case it's close to ***midnight***)
  1202. // to make sure we don't miss anything. The idea being that we have a cron
  1203. // running frequently enough that this gives us lots of chances to retrieve information.
  1204. $messages = $client->messages->read(array('dateSentAfter' => $check_since));
  1205. foreach ($messages as $record) {
  1206. //fpm(ppm($record, TRUE));
  1207. // fpm($record->status);
  1208. watchdog('engagements_debug', "In engagements_sms_get_all_messages(). Reviewing message: <pre>" . print_r($record, TRUE) . "</pre>", array(), WATCHDOG_DEBUG);
  1209. $status = strtolower(trim($record->status));
  1210. $error_message = $record->errorMessage;
  1211. $error_code = intval($record->errorCode);
  1212. if ($error_code == 12100 || $error_code == 11200) {
  1213. // We don't care about these code, as they appear on EVERY message.
  1214. $error_code = NULL;
  1215. $status = NULL;
  1216. }
  1217. if ($error_code) {
  1218. $friendly_error_message = @$error_codes[$error_code];
  1219. }
  1220. $message_sid = $record->sid;
  1221. $vars['MessageSid'] = $message_sid;
  1222. // Should we update any details about this message? Price, errors, etc?
  1223. $test = engagements_get_sms_from_history($message_sid);
  1224. if ($test && (floatval($record->price) != floatval($test['sw_price']) || trim($error_code) != trim($test['err_code']))) {
  1225. // update prices
  1226. $price = $record->price;
  1227. //$fp_price = engagements_get_fp_price($price, $test['direction'], intval($test['num_segments']));
  1228. $fp_price = 0; // no longer using this field.
  1229. watchdog('engagements_debug', "In engagements_sms_get_all_messages(). Updating sms_history table.", array(), WATCHDOG_DEBUG);
  1230. db_query("UPDATE sms_history
  1231. SET sw_price = ?, fp_price = ?, price_processed = 1, delivery_status = ?, err_code = ?, err_message = ?, err_friendly_message = ?
  1232. WHERE mid = ?", array($price, $fp_price, $status, $error_code, $error_message, $friendly_error_message, $test['mid']));
  1233. // Meaning, we already have this one saved, and now that we've updated it, we can continue.
  1234. continue;
  1235. } // if test and we have things to update
  1236. else if ($test) {
  1237. // meaning, we already have this one saved. Skip it.
  1238. continue;
  1239. }
  1240. $from_mobile = engagements_convert_to_valid_phone_number($record->from);
  1241. $to_mobile = engagements_convert_to_valid_phone_number($record->to);
  1242. $body = $record->body;
  1243. $date_sent = $record->dateSent; // this is a Datetime object
  1244. $date_sent_ts = strtotime($date_sent->format("Y-m-d H:i:s"));
  1245. $direction = $record->direction;
  1246. $num_segments = intval($record->numSegments);
  1247. $vars['From'] = $from_mobile;
  1248. $vars['To'] = $to_mobile;
  1249. $vars['Body'] = $body;
  1250. $vars['date_sent_ts'] = $date_sent_ts;
  1251. $vars['direction'] = $direction;
  1252. $vars['NumSegements'] = $num_segments;
  1253. $vars['delivery_status'] = $status;
  1254. $vars['err_code'] = $error_code;
  1255. $vars['err_message'] = $error_message;
  1256. $vars['err_friendly_message'] = $friendly_error_message;
  1257. $vars['NumMedia'] = 0;
  1258. // Is there media?
  1259. if (intval($record->numMedia) > 0) {
  1260. $vars['NumMedia'] = intval($record->numMedia);
  1261. // List all media for this message.
  1262. $allmedia = $client->messages($message_sid)->media->read();
  1263. $c = 0;
  1264. foreach ($allmedia as $mitem) {
  1265. $media_sid = $mitem->sid;
  1266. $content_type = $mitem->contentType;
  1267. $vars['MediaContentType' . $c] = $content_type;
  1268. $vars['MediaUrl' . $c] = "https://$space_url/api/laml/2010-04-01/Accounts/$project_id/Messages/$message_sid/Media/$media_sid";
  1269. $c++;
  1270. } // foreach media
  1271. } // there was media!
  1272. watchdog('engagements_debug', "In engagements_sms_get_all_messages(). Sending to engagements_handle_incoming_sms: <pre>" . print_r($vars, TRUE) . "</pre>", array(), WATCHDOG_DEBUG);
  1273. // Save this message using our "handle_incoming_sms" function.
  1274. engagements_handle_incoming_sms($vars);
  1275. } // foreach messages
  1276. //////////////////////////////////////////////////////////////
  1277. $body = "";
  1278. // Now, we also want to get a list of all calls and their prices too.
  1279. $calls = $client->calls->read(array('startTimeAfter', $check_since));
  1280. foreach ($calls as $record) {
  1281. watchdog('engagements_debug', "In engagements_sms_get_all_messages(). Reviewing CALL: <pre>" . print_r($record, TRUE) . "</pre>", array(), WATCHDOG_DEBUG);
  1282. $sid = $record->sid;
  1283. // Do we already have this in our table? If so, skip.
  1284. $test = engagements_get_sms_from_history($sid);
  1285. if ($test) continue; // We've already recorded this, so skip.
  1286. $date_sent = $record->startTime; // this is a Datetime object
  1287. $date_sent_ts = strtotime($date_sent->format("Y-m-d H:i:s"));
  1288. $to_mobile = engagements_convert_to_valid_phone_number($record->to);
  1289. $from_mobile = engagements_convert_to_valid_phone_number($record->from);
  1290. // Figure out what user this came from/to!
  1291. $from_cwid = "";
  1292. $user_id = db_result(db_query("SELECT user_id FROM user_attributes WHERE `name` = 'mobile_phone' AND `value` = ?", array($from_mobile)));
  1293. if ($user_id) {
  1294. $from_cwid = db_get_cwid_from_user_id($user_id);
  1295. }
  1296. $to_cwid = "";
  1297. $user_id = db_result(db_query("SELECT user_id FROM user_attributes WHERE `name` = 'mobile_phone' AND `value` = ?", array($to_mobile)));
  1298. if ($user_id) {
  1299. $to_cwid = db_get_cwid_from_user_id($user_id);
  1300. }
  1301. $duration = $record->duration;
  1302. $direction = $record->direction;
  1303. $price = $record->price;
  1304. $fp_price = engagements_get_fp_price($price, $direction, 1, 'call');
  1305. // Write our data to our sms_history table.
  1306. db_query(
  1307. "INSERT INTO sms_history (`message_sid`, sw_type, `body`, `from_number`, `to_number`, `sw_price`, `fp_price`, `from_cwid`, `updated`, `direction`,
  1308. `media_filenames`, `date_sent`, price_processed, num_segments)
  1309. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ",
  1310. array($sid, 'call', $body, $from_mobile, $to_mobile, $price, $fp_price, $from_cwid, time(), $direction, '', $date_sent_ts, 1, $duration)
  1311. );
  1312. watchdog("engagements_sms", "Documented call to $to_mobile.", array(), WATCHDOG_DEBUG);
  1313. } //foreach calls as record
  1314. // Record the last time we called this function.
  1315. variable_set('engagements_sms_get_all_last_check', time());
  1316. } // function
  1317. /**
  1318. * figure out the price we will charge.
  1319. */
  1320. function engagements_get_fp_price($price, $direction, $num_segments = 1, $type = 'sms')
  1321. {
  1322. $rtn = 0;
  1323. if ($num_segments == 0) {
  1324. $num_segments = 1;
  1325. }
  1326. if (strstr($direction, 'outbound')) {
  1327. $rtn = 0.0033 * $num_segments;
  1328. }
  1329. if ($price == 0.01) {
  1330. $rtn = 0.015;
  1331. }
  1332. if ($type == 'call') {
  1333. $rtn = 0.005;
  1334. }
  1335. // Guess at 15% over, if we don't know.
  1336. $new_price = $price * 1.15; // generic; add 15%.
  1337. // Return the LARGER of the two prices.
  1338. if ($new_price > $rtn) {
  1339. return $new_price;
  1340. }
  1341. return $rtn;
  1342. }
  1343. // From: https://stackoverflow.com/questions/16511021/convert-mime-type-to-file-extension-php
  1344. /**
  1345. * This function will look at the mime type (aka content type) to figure out what the
  1346. * file extension should be. This is useful when retrieving txt message media.
  1347. *
  1348. * We will only have the ones we think students might actually try to txt to their teachers.
  1349. */
  1350. function engagements_get_file_extension_from_mime_type($mime)
  1351. {
  1352. $mime_map = array(
  1353. 'video/3gpp2' => '3g2',
  1354. 'video/3gp' => '3gp',
  1355. 'video/3gpp' => '3gp',
  1356. 'application/x-compressed' => '7zip',
  1357. 'audio/x-acc' => 'aac',
  1358. 'audio/ac3' => 'ac3',
  1359. 'application/postscript' => 'ai',
  1360. 'audio/x-aiff' => 'aif',
  1361. 'audio/aiff' => 'aif',
  1362. 'audio/x-au' => 'au',
  1363. 'video/x-msvideo' => 'avi',
  1364. 'video/msvideo' => 'avi',
  1365. 'video/avi' => 'avi',
  1366. 'application/x-troff-msvideo' => 'avi',
  1367. //'application/macbinary' => 'bin',
  1368. //'application/mac-binary' => 'bin',
  1369. //'application/x-binary' => 'bin',
  1370. //'application/x-macbinary' => 'bin',
  1371. 'image/bmp' => 'bmp',
  1372. 'image/x-bmp' => 'bmp',
  1373. 'image/x-bitmap' => 'bmp',
  1374. 'image/x-xbitmap' => 'bmp',
  1375. 'image/x-win-bitmap' => 'bmp',
  1376. 'image/x-windows-bmp' => 'bmp',
  1377. 'image/ms-bmp' => 'bmp',
  1378. 'image/x-ms-bmp' => 'bmp',
  1379. 'application/bmp' => 'bmp',
  1380. 'application/x-bmp' => 'bmp',
  1381. 'application/x-win-bitmap' => 'bmp',
  1382. //'application/cdr' => 'cdr',
  1383. //'application/coreldraw' => 'cdr',
  1384. //'application/x-cdr' => 'cdr',
  1385. //'application/x-coreldraw' => 'cdr',
  1386. //'image/cdr' => 'cdr',
  1387. //'image/x-cdr' => 'cdr',
  1388. //'zz-application/zz-winassoc-cdr' => 'cdr',
  1389. //'application/mac-compactpro' => 'cpt',
  1390. //'application/pkix-crl' => 'crl',
  1391. //'application/pkcs-crl' => 'crl',
  1392. //'application/x-x509-ca-cert' => 'crt',
  1393. //'application/pkix-cert' => 'crt',
  1394. 'text/css' => 'css',
  1395. 'text/x-comma-separated-values' => 'csv',
  1396. 'text/comma-separated-values' => 'csv',
  1397. 'application/vnd.msexcel' => 'csv',
  1398. 'application/x-director' => 'dcr',
  1399. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
  1400. 'application/vnd.oasis.opendocument.text' => 'odt',
  1401. 'application/x-dvi' => 'dvi',
  1402. 'message/rfc822' => 'eml',
  1403. //'application/x-msdownload' => 'exe',
  1404. 'video/x-f4v' => 'f4v',
  1405. 'audio/x-flac' => 'flac',
  1406. 'video/x-flv' => 'flv',
  1407. 'image/gif' => 'gif',
  1408. 'application/gpg-keys' => 'gpg',
  1409. 'application/x-gtar' => 'gtar',
  1410. 'application/x-gzip' => 'gzip',
  1411. 'application/mac-binhex40' => 'hqx',
  1412. 'application/mac-binhex' => 'hqx',
  1413. 'application/x-binhex40' => 'hqx',
  1414. 'application/x-mac-binhex40' => 'hqx',
  1415. 'text/html' => 'html',
  1416. 'image/x-icon' => 'ico',
  1417. 'image/x-ico' => 'ico',
  1418. 'image/vnd.microsoft.icon' => 'ico',
  1419. 'text/calendar' => 'ics',
  1420. 'application/java-archive' => 'jar',
  1421. 'application/x-java-application' => 'jar',
  1422. 'application/x-jar' => 'jar',
  1423. 'image/jp2' => 'jp2',
  1424. 'video/mj2' => 'jp2',
  1425. 'image/jpx' => 'jp2',
  1426. 'image/jpm' => 'jp2',
  1427. 'image/jpeg' => 'jpg',
  1428. 'image/pjpeg' => 'jpeg',
  1429. 'application/x-javascript' => 'js',
  1430. 'application/json' => 'json',
  1431. 'text/json' => 'json',
  1432. //'application/vnd.google-earth.kml+xml' => 'kml',
  1433. //'application/vnd.google-earth.kmz' => 'kmz',
  1434. 'text/x-log' => 'log',
  1435. 'audio/x-m4a' => 'm4a',
  1436. 'audio/mp4' => 'm4a',
  1437. 'application/vnd.mpegurl' => 'm4u',
  1438. 'audio/midi' => 'mid',
  1439. 'application/vnd.mif' => 'mif',
  1440. 'video/quicktime' => 'mov',
  1441. 'video/x-sgi-movie' => 'movie',
  1442. 'audio/mpeg' => 'mp3',
  1443. 'audio/mpg' => 'mp3',
  1444. 'audio/mpeg3' => 'mp3',
  1445. 'audio/mp3' => 'mp3',
  1446. 'video/mp4' => 'mp4',
  1447. 'video/mpeg' => 'mpeg',
  1448. 'application/oda' => 'oda',
  1449. 'audio/ogg' => 'ogg',
  1450. 'video/ogg' => 'ogg',
  1451. 'application/ogg' => 'ogg',
  1452. 'font/otf' => 'otf',
  1453. 'application/x-pkcs10' => 'p10',
  1454. 'application/pkcs10' => 'p10',
  1455. 'application/x-pkcs12' => 'p12',
  1456. 'application/x-pkcs7-signature' => 'p7a',
  1457. 'application/pkcs7-mime' => 'p7c',
  1458. 'application/x-pkcs7-mime' => 'p7c',
  1459. 'application/x-pkcs7-certreqresp' => 'p7r',
  1460. 'application/pkcs7-signature' => 'p7s',
  1461. 'application/pdf' => 'pdf',
  1462. 'application/octet-stream' => 'pdf',
  1463. 'application/x-x509-user-cert' => 'pem',
  1464. 'application/x-pem-file' => 'pem',
  1465. 'application/pgp' => 'pgp',
  1466. //'application/x-httpd-php' => 'php',
  1467. //'application/php' => 'php',
  1468. //'application/x-php' => 'php',
  1469. //'text/php' => 'php',
  1470. //'text/x-php' => 'php',
  1471. //'application/x-httpd-php-source' => 'php',
  1472. 'image/png' => 'png',
  1473. 'image/x-png' => 'png',
  1474. 'application/powerpoint' => 'ppt',
  1475. 'application/vnd.ms-powerpoint' => 'ppt',
  1476. 'application/vnd.ms-office' => 'ppt',
  1477. 'application/msword' => 'doc',
  1478. 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
  1479. 'application/x-photoshop' => 'psd',
  1480. 'image/vnd.adobe.photoshop' => 'psd',
  1481. 'audio/x-realaudio' => 'ra',
  1482. 'audio/x-pn-realaudio' => 'ram',
  1483. 'application/x-rar' => 'rar',
  1484. 'application/rar' => 'rar',
  1485. 'application/x-rar-compressed' => 'rar',
  1486. 'audio/x-pn-realaudio-plugin' => 'rpm',
  1487. 'application/x-pkcs7' => 'rsa',
  1488. 'text/rtf' => 'rtf',
  1489. 'text/richtext' => 'rtx',
  1490. 'video/vnd.rn-realvideo' => 'rv',
  1491. 'application/x-stuffit' => 'sit',
  1492. 'application/smil' => 'smil',
  1493. 'text/srt' => 'srt',
  1494. 'image/svg+xml' => 'svg',
  1495. 'application/x-shockwave-flash' => 'swf',
  1496. 'application/x-tar' => 'tar',
  1497. 'application/x-gzip-compressed' => 'tgz',
  1498. 'image/tiff' => 'tiff',
  1499. 'font/ttf' => 'ttf',
  1500. 'text/plain' => 'txt',
  1501. 'text/x-vcard' => 'vcf',
  1502. 'application/videolan' => 'vlc',
  1503. 'text/vtt' => 'vtt',
  1504. 'audio/x-wav' => 'wav',
  1505. 'audio/wave' => 'wav',
  1506. 'audio/wav' => 'wav',
  1507. 'application/wbxml' => 'wbxml',
  1508. 'video/webm' => 'webm',
  1509. 'image/webp' => 'webp',
  1510. 'audio/x-ms-wma' => 'wma',
  1511. 'application/wmlc' => 'wmlc',
  1512. 'video/x-ms-wmv' => 'wmv',
  1513. 'video/x-ms-asf' => 'wmv',
  1514. 'font/woff' => 'woff',
  1515. 'font/woff2' => 'woff2',
  1516. 'application/xhtml+xml' => 'xhtml',
  1517. 'application/excel' => 'xl',
  1518. 'application/msexcel' => 'xls',
  1519. 'application/x-msexcel' => 'xls',
  1520. 'application/x-ms-excel' => 'xls',
  1521. 'application/x-excel' => 'xls',
  1522. 'application/x-dos_ms_excel' => 'xls',
  1523. 'application/xls' => 'xls',
  1524. 'application/x-xls' => 'xls',
  1525. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
  1526. 'application/vnd.ms-excel' => 'xlsx',
  1527. 'application/xml' => 'xml',
  1528. 'text/xml' => 'xml',
  1529. 'text/xsl' => 'xsl',
  1530. 'application/xspf+xml' => 'xspf',
  1531. 'application/x-compress' => 'z',
  1532. 'application/x-zip' => 'zip',
  1533. 'application/zip' => 'zip',
  1534. 'application/x-zip-compressed' => 'zip',
  1535. 'application/s-compressed' => 'zip',
  1536. 'multipart/x-zip' => 'zip',
  1537. //'text/x-scriptzsh' => 'zsh',
  1538. );
  1539. return isset($mime_map[$mime]) ? $mime_map[$mime] : FALSE;
  1540. }
  1541. /**
  1542. * Connect to our imap server, download all received messages from students (or others).
  1543. * We will then delete them, so they don't get read twice.
  1544. */
  1545. function engagements_imap_get_all_received_messages()
  1546. {
  1547. $host = variable_get('imap_host', '');
  1548. $port = variable_get('imap_port', '');
  1549. $secure = variable_get('imap_secure', '');
  1550. $mailbox_server = "{" . "$host:$port/$secure" . "}";
  1551. $username = variable_get('imap_username', '');
  1552. $password = variable_get('imap_password', '');
  1553. if ($username == "") {
  1554. return FALSE;
  1555. }
  1556. watchdog('imap', "Attempting to connect to imap server.", array(), WATCHDOG_DEBUG);
  1557. $mbox = imap_open($mailbox_server, $username, $password);
  1558. if (!$mbox) {
  1559. watchdog('imap', "Could not connect to imap server!", array(), WATCHDOG_ERROR);
  1560. fp_add_message("Could not connect to IMAP server.", 'error', TRUE);
  1561. }
  1562. // We need to get the name of the "Trash" folder, which might contain a lot of weird extra stuff in the name.
  1563. // 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.
  1564. $mailboxes = imap_list($mbox, $mailbox_server, "*");
  1565. $trash_box_name = "Trash";
  1566. foreach ($mailboxes as $boxname) {
  1567. if (stristr($boxname, "trash")) {
  1568. $trash_box_name = str_replace($mailbox_server, "", $boxname);
  1569. break;
  1570. }
  1571. }
  1572. $mbox_check = imap_check($mbox);
  1573. // Fetch an overview for all messages in INBOX, by going from 1 to the max number messages.
  1574. $result = imap_fetch_overview($mbox, "1:{$mbox_check->Nmsgs}", 0);
  1575. foreach ($result as $overview) {
  1576. $msgno = $overview->msgno;
  1577. $subject = $overview->subject;
  1578. $udate = $overview->udate; // udate is apparently already in UTC, which is what we want.
  1579. watchdog('imap', "Gettign message $msgno. Subject: $subject. Udate: $udate.", array(), WATCHDOG_DEBUG);
  1580. // We need to get our ONLY the email address from the From field, which
  1581. // might look like: Richard Peacock <richard.peacock@school.domain.co.com>
  1582. // We will use a regular expression to figure it out. From: https://stackoverflow.com/questions/33865113/extract-email-address-from-string-php
  1583. $ofrom = $overview->from;
  1584. preg_match_all("/[\._a-zA-Z0-9-]+@[\._a-zA-Z0-9-]+/i", $ofrom, $matches);
  1585. $from_email = $matches[0][0]; // Since this is a FROM email, we assume there's only one sender.
  1586. $body = _engagements_imap_get_body($mbox, $msgno);
  1587. watchdog('imap', "Found Body: $body", array(), WATCHDOG_DEBUG);
  1588. // match the from email to a student
  1589. $student_user_id = db_get_user_id_from_email($from_email, 'student');
  1590. if ($student_user_id) {
  1591. watchdog('imap', "Found from student $student_user_id, creating engagement content.", array(), WATCHDOG_DEBUG);
  1592. // create this as an engagement for this student.
  1593. $account = fp_load_user($student_user_id);
  1594. $student_cwid = $account->cwid;
  1595. $content = new stdClass();
  1596. $content->type = 'engagement';
  1597. $content->cid = "new";
  1598. $content->published = 1;
  1599. $content->delete_flag = 0;
  1600. $content->title = $subject; // required
  1601. $content->field__activity_datetime['value'] = date('Y-m-d H:i:s', $udate);
  1602. $content->field__student_id['value'] = $student_cwid;
  1603. $content->field__engagement_type['value'] = 'email';
  1604. $content->field__direction['value'] = 'received';
  1605. $content->field__engagement_msg['value'] = $body;
  1606. content_save($content);
  1607. $cid = $content->cid;
  1608. $attachments = _engagements_imap_get_attachments($mbox, $msgno, fp_get_machine_readable($from_email), $cid);
  1609. // save attachements along with the engagement, if there are any
  1610. if (count($attachments) > 0) {
  1611. $fid_line = "";
  1612. foreach ($attachments as $adetails) {
  1613. $fid = $adetails['fid'];
  1614. $fid_line .= $fid . ",";
  1615. }
  1616. $fid_line = rtrim($fid_line, ",");
  1617. $content->field__attachment['value'] = $fid_line;
  1618. }
  1619. $content->field__visibility['value'] = 'public';
  1620. content_save($content);
  1621. // if received, create an activity record.
  1622. // Create a new "activity record" that the student has sent an email message
  1623. $acontent = new stdClass();
  1624. $acontent->type = 'activity_record';
  1625. $acontent->cid = "new";
  1626. $acontent->published = 1;
  1627. $acontent->delete_flag = 0;
  1628. $acontent->title = t('Student replied to email message.');
  1629. $acontent->field__student_id['value'] = $student_cwid;
  1630. $acontent->field__activity_type['value'] = 'mail';
  1631. content_save($acontent);
  1632. // Notify the advisor(s).
  1633. // get list of all advisors for this student, and send notifications.
  1634. $advisors = advise_get_advisors_for_student($student_cwid);
  1635. foreach ($advisors as $c => $afaculty_id) {
  1636. $account_user_id = db_get_user_id_from_cwid($afaculty_id, 'faculty');
  1637. if ($account_user_id) {
  1638. $student_name = fp_get_student_name($student_cwid, TRUE);
  1639. $base_url = $GLOBALS['fp_system_settings']['base_url'];
  1640. $link = $base_url . "/engagements?current_student_id=$student_cwid";
  1641. $tmsg = "";
  1642. $tmsg .= "$student_name has submitted an email to FlightPath.<br><br>\n\n
  1643. To view, visit the student's Engagements tab by logging in and following this link:<br>\n
  1644. <a href='$link'>$link</a>";
  1645. notify_send_notification_to_user($account_user_id, $tmsg, $content->cid, 'engagement');
  1646. }
  1647. }
  1648. } // if student_user_id
  1649. // We are now finished with this email, so we can move it to our trash folder.
  1650. //imap_mail_move($mbox, $msgno, $trash_box_name);
  1651. imap_delete($mbox, $msgno);
  1652. } // foreach result as overview
  1653. // Delete all mail marked for deletion...
  1654. imap_expunge($mbox);
  1655. watchdog('imap', "Closing connection to imap server.", array(), WATCHDOG_DEBUG);
  1656. // Close connection, we're done.
  1657. imap_close($mbox);
  1658. // Record the last time we called this function.
  1659. variable_set('engagements_imap_get_all_last_check', time());
  1660. } // end of function
  1661. // From https://stackoverflow.com/questions/2649579/downloading-attachments-to-directory-with-imap-in-php-randomly-works
  1662. /**
  1663. * $inbox is the imap link. email_number is msg_no.
  1664. */
  1665. function _engagements_imap_get_attachments($inbox, $email_number, $from_email_machine_readable, $cid = 0)
  1666. {
  1667. /* get information specific to this email */
  1668. $overview = imap_fetch_overview($inbox, $email_number, 0);
  1669. $message = imap_fetchbody($inbox, $email_number, 2);
  1670. $structure = imap_fetchstructure($inbox, $email_number);
  1671. $attachments = array();
  1672. if (isset($structure->parts) && count($structure->parts)) {
  1673. for ($i = 0; $i < count($structure->parts); $i++) {
  1674. $attachments[$i] = array(
  1675. 'is_attachment' => false,
  1676. 'filename' => '',
  1677. 'name' => '',
  1678. 'subtype' => '',
  1679. 'attachment' => ''
  1680. );
  1681. if ($structure->parts[$i]->ifdparameters) {
  1682. foreach ($structure->parts[$i]->dparameters as $object) {
  1683. if (strtolower($object->attribute) == 'filename') {
  1684. $attachments[$i]['is_attachment'] = TRUE;
  1685. $attachments[$i]['filename'] = $object->value;
  1686. $attachments[$i]['subtype'] = $structure->parts[$i]->subtype;
  1687. }
  1688. }
  1689. }
  1690. if ($structure->parts[$i]->ifparameters) {
  1691. foreach ($structure->parts[$i]->parameters as $object) {
  1692. if (strtolower($object->attribute) == 'name') {
  1693. $attachments[$i]['is_attachment'] = true;
  1694. $attachments[$i]['name'] = $object->value;
  1695. $attachments[$i]['subtype'] = $structure->parts[$i]->subtype;
  1696. }
  1697. }
  1698. }
  1699. if ($attachments[$i]['is_attachment']) {
  1700. $attachments[$i]['attachment'] = imap_fetchbody($inbox, $email_number, $i + 1);
  1701. if ($structure->parts[$i]->encoding == 3) { // 3 = BASE64
  1702. $attachments[$i]['attachment'] = base64_decode($attachments[$i]['attachment']);
  1703. } elseif ($structure->parts[$i]->encoding == 4) { // 4 = QUOTED-PRINTABLE
  1704. $attachments[$i]['attachment'] = quoted_printable_decode($attachments[$i]['attachment']);
  1705. }
  1706. }
  1707. } // for($i = 0; $i < count($structure->parts); $i++)
  1708. } // if(isset($structure->parts) && count($structure->parts))
  1709. $rtn = array();
  1710. if (count($attachments) != 0) {
  1711. foreach ($attachments as $c => $at) {
  1712. if ($at['is_attachment'] == TRUE) {
  1713. // Create a new random filename to save it as.
  1714. $filename = 'email__' . $from_email_machine_readable . '_' . time() . '_' . mt_rand(1, 9999); // We will figure out the ext (from mimetype) later.
  1715. $tmp_name = sha1($filename . time() . mt_rand(1,99999) . mt_rand(1,99999));
  1716. // Save to our temporary location on the server.
  1717. $test = file_put_contents(sys_get_temp_dir() . '/' . $tmp_name, $at['attachment']);
  1718. if (!$test) {
  1719. watchdog('engagements_email', 'Unable to write media file at temp location: ' . sys_get_temp_dir(), array(), 'error');
  1720. fpm('Unable to write media file at temp location:' . sys_get_temp_dir() . '. Permissions issue?');
  1721. continue;
  1722. }
  1723. // Next, we want to rename it to have the proper file extension, as best we can tell.
  1724. $ext = strtolower($at['subtype']); // if we can't figure it out, use this.
  1725. // Attempt to figure out the mimetype....
  1726. $finfo = finfo_open(FILEINFO_MIME_TYPE);
  1727. $mimetype = finfo_file($finfo, sys_get_temp_dir() . '/' . $tmp_name);
  1728. $temp = explode(";", $mimetype);
  1729. $mimetype = strtolower(trim($temp[0]));
  1730. $new_ext = engagements_get_file_extension_from_mime_type($mimetype);
  1731. if ($new_ext) {
  1732. $ext = $new_ext;
  1733. }
  1734. $filename .= ".$ext"; // the filename doesn't have its extension already for whatever reason.
  1735. $file = array(
  1736. 'name' => $filename,
  1737. 'type' => $mimetype,
  1738. 'tmp_name' => $tmp_name,
  1739. );
  1740. $fid = content_add_new_uploaded_file($file, $cid, FALSE);
  1741. // TODO: Possibly rename filename based on if it is encrypted or not?
  1742. $rtn[] = array(
  1743. 'fid' => $fid,
  1744. 'filename' => $filename,
  1745. 'mimetime' => $mimetype,
  1746. 'ext' => $ext,
  1747. );
  1748. } // if is_attachment == TRUE
  1749. } // foreach
  1750. }
  1751. return $rtn;
  1752. } // ... imap_get_attachments
  1753. // From: https://stackoverflow.com/questions/4272551/extract-body-text-from-email-php
  1754. function _engagements_imap_get_body($imapLink, $msgno)
  1755. {
  1756. $obj_structure = imap_fetchstructure($imapLink, $msgno);
  1757. // Recherche de la section contenant le corps du message et extraction du contenu
  1758. $obj_section = $obj_structure;
  1759. //$text = imap_fetchbody($imapLink, $msgno, $section);
  1760. // So it turns out the $section for HTML can be 1.2, but it might also be just "2" in simple messages.
  1761. // Try to get HTML first....
  1762. $text = trim(imap_fetchbody($imapLink, $msgno, 1.2));
  1763. if ($text == "") {
  1764. $text = trim(imap_fetchbody($imapLink, $msgno, 2));
  1765. }
  1766. if ($text == "") {
  1767. $text = trim(imap_fetchbody($imapLink, $msgno, 1.1));
  1768. }
  1769. if ($text == "") {
  1770. $text = trim(imap_fetchbody($imapLink, $msgno, 1));
  1771. }
  1772. // Décodage éventuel
  1773. if ($obj_section->encoding == 3) {
  1774. $text = imap_base64($text);
  1775. } else if ($obj_section->encoding == 4) {
  1776. $text = imap_qprint($text);
  1777. }
  1778. // Encodage éventuel
  1779. foreach ($obj_section->parameters as $obj_param) {
  1780. if (($obj_param->attribute == "charset") && (mb_strtoupper($obj_param->value) != "UTF-8")) {
  1781. $text = utf8_encode($text);
  1782. break;
  1783. }
  1784. }
  1785. $text = filter_markup($text, 'basic');
  1786. // Sometimes, emails contain odd encoding. Ex: =C2=A0, which is apparently a non-breaking space. Let's convert just in case.
  1787. // From: https://stackoverflow.com/questions/12682208/parsing-email-body-with-7bit-content-transfer-encoding-php
  1788. // and: https://github.com/geerlingguy/Imap/blob/c4b54e52576bf71a93045d2a4581589eab9a00b6/JJG/Imap.php#L398
  1789. // Manually convert common encoded characters into their UTF-8 equivalents.
  1790. $characters = array(
  1791. '=20' => ' ', // space.
  1792. '=2C' => ',', // comma.
  1793. '=E2=80=99' => "'", // single quote.
  1794. '=C2=A0' => ' ', // non-breaking space.
  1795. '=0A' => "\r\n", // line break.
  1796. '=0D' => "\r\n", // carriage return.
  1797. '=A0' => ' ', // non-breaking space.
  1798. "=\r\n" => '', // joined line.
  1799. '=E2=80=A6' => '&hellip;', // ellipsis.
  1800. '=E2=80=A2' => '&bull;', // bullet.
  1801. '=E2=80=93' => '&ndash;', // en dash.
  1802. '=E2=80=94' => '&mdash;', // em dash.
  1803. );
  1804. // Loop through the encoded characters and replace any that are found.
  1805. foreach ($characters as $key => $value) {
  1806. $text = str_replace($key, $value, $text);
  1807. }
  1808. return $text;
  1809. }
  1810. /**
  1811. * The user has opened an email with a tracking pixel. We will now record that
  1812. * it was opened in our engagements_tracking table.
  1813. *
  1814. * We also need to actually render the pixel to the browser.
  1815. */
  1816. function engagements_handle_tracking_pixel_request($cid, $token)
  1817. {
  1818. // Record in database
  1819. // Fist, get the current count, if it exists.
  1820. $res = db_query("SELECT * FROM engagements_tracking WHERE cid = ? AND token = ?", array($cid, $token));
  1821. $cur = db_fetch_array($res);
  1822. if ($cur['cid'] != $cid || $cur['token'] != $token) {
  1823. // Means it was not generated, this might be a malicious attempt or an old token or something.
  1824. die;
  1825. }
  1826. $opens = intval($cur['opens']) + 1;
  1827. db_query('UPDATE engagements_tracking
  1828. SET opens = ?,
  1829. updated = ?
  1830. WHERE cid = ? AND token = ?', array($opens, time(), $cid, $token));
  1831. watchdog('engagements_track', "Tracking request for: $cid - $token");
  1832. // Create a new "activity record" that the student has viewed this email.
  1833. $email_content = content_load($cid);
  1834. // Create a new actvity_record.
  1835. $content = new stdClass();
  1836. $content->type = 'activity_record';
  1837. $content->cid = "new";
  1838. $content->published = 1;
  1839. $content->delete_flag = 0;
  1840. $content->title = t('Student opened email titled "@et".', array("@et" => $email_content->title));
  1841. $content->field__student_id['value'] = $email_content->field__student_id['value'];
  1842. $content->field__activity_type['value'] = 'mail';
  1843. content_save($content);
  1844. // Render pixel to browser and die. We will be using the 1x1.gif file in our assets folder.
  1845. // Output the image to browser
  1846. header('Content-Type: image/gif');
  1847. $im = imagecreatefromgif(fp_get_module_path('engagements', TRUE, FALSE) . '/assets/1x1.gif');
  1848. imagegif($im);
  1849. imagedestroy($im);
  1850. die;
  1851. } // engagements_handle_tracking_pixel_request
  1852. function engagements_perm()
  1853. {
  1854. $rtn = array(
  1855. 'administer_engagements' => array(
  1856. 'title' => 'Administer Engagements',
  1857. 'description' => t('The user can edit the system config settings for the Engagements module. Only give to admin users.'),
  1858. "admin_restricted" => TRUE, // means only appears for admin (user_id == 1)
  1859. ),
  1860. 'can_view_engagements' => array(
  1861. 'title' => 'Can view Engagements',
  1862. 'description' => t('The user may view engagements (only "everyone" by default)'),
  1863. ),
  1864. 'can_view_faculty_engagements' => array(
  1865. 'title' => 'View "Faculty/Staff" Engagements',
  1866. 'description' => t('The user is allowed to view engagements marked visible for "Faculty/Staff".'),
  1867. ),
  1868. 'can_send_email_engagements' => array(
  1869. 'title' => t('Can send email engagements to students'),
  1870. ),
  1871. 'can_log_engagements' => array(
  1872. 'title' => t('Can log engagements with students'),
  1873. ),
  1874. 'can_send_txt_engagements' => array(
  1875. 'title' => t('Can send txt message engagements to students'),
  1876. 'description' => t("<b>Important:</b> Be sure to also select the specific phone number/permissions below."),
  1877. ),
  1878. );
  1879. // set up other permissions for each SMS line we have.
  1880. $phones = engagements_get_from_phones();
  1881. foreach ($phones['lines'] as $num => $details) {
  1882. $pretty_num = engagements_convert_to_pretty_phone_number($num);
  1883. $desc = filter_markup($details['description'], "plain");
  1884. $desc = str_replace("'", "", $desc);
  1885. $desc = str_replace('"', "", $desc);
  1886. $rtn["can_send_sms_from_$num"] = array(
  1887. "title" => t("Can send txt messages from @pretty", array("@pretty" => $pretty_num)),
  1888. "description" => t("The user may send SMS (txt) messages from @pretty - %desc phone line.", array("@pretty" => $pretty_num, "%desc" => $desc)),
  1889. );
  1890. }
  1891. $rtn['can_send_mass_sms'] = array(
  1892. 'title' => t("Send mass txt messages"),
  1893. 'description' => t("The user may access and send mass SMS (txt) messages to lists of recipients from the designated mass text lines.
  1894. Only give to trusted users."),
  1895. );
  1896. return $rtn;
  1897. }
  1898. /**
  1899. * For use with the content module. We will register our custom content type(s)
  1900. * for use with this module.
  1901. */
  1902. function engagements_content_register_content_type()
  1903. {
  1904. $arr = array();
  1905. $arr['engagement'] = array(
  1906. 'title' => 'Engagement',
  1907. 'description' => 'This is a content type meant to track a students activities and communications in the system.',
  1908. 'settings' => array(
  1909. 'title' => array(
  1910. 'label' => t('Subject'),
  1911. 'weight' => 65,
  1912. ),
  1913. ),
  1914. );
  1915. // If we are in a popup (dialog)...
  1916. if (@$_GET['window_mode'] == 'popup') {
  1917. // We want to make sure we redirect to our handler URL, which will close the dialog.
  1918. $arr['engagement']['settings']['#redirect'] = array(
  1919. 'path' => 'content-dialog-handle-after-save',
  1920. 'query' => '',
  1921. );
  1922. }
  1923. $fields = array();
  1924. $fields['faculty_id'] = array(
  1925. 'type' => 'textfield',
  1926. 'label' => 'Faculty/Staff',
  1927. 'weight' => 5,
  1928. );
  1929. $fields['from_sms_phone'] = array(
  1930. 'type' => 'select',
  1931. 'label' => t('From Text Phone Line:'),
  1932. 'hide_please_select' => TRUE,
  1933. 'options' => array(),
  1934. 'weight' => 7,
  1935. );
  1936. $fields['student_id'] = array(
  1937. 'type' => 'textfield',
  1938. 'label' => 'Student',
  1939. 'weight' => 10,
  1940. );
  1941. $fields['activity_datetime'] = array(
  1942. 'type' => 'datetime-local',
  1943. 'label' => 'Date/Time',
  1944. 'value' => 'now',
  1945. 'format_date' => 'short',
  1946. 'weight' => 12,
  1947. );
  1948. $fields['engagement_type'] = array(
  1949. 'type' => 'select',
  1950. 'options' => array(
  1951. 'phone' => t('Phone Call'),
  1952. 'email' => t('Email'),
  1953. 'in_person' => t('In-Person Meeting'),
  1954. 'video_chat' => t('Video Chat'),
  1955. 'txt_msg' => t('Txt Message'),
  1956. 'social_media' => t('Social Media'),
  1957. 'other' => t('Other'),
  1958. ),
  1959. 'label' => 'Type',
  1960. 'required' => TRUE,
  1961. 'hide_please_select' => TRUE,
  1962. 'weight' => 40,
  1963. );
  1964. $fields['direction'] = array(
  1965. 'type' => 'select',
  1966. 'label' => t('Direction'),
  1967. 'required' => TRUE,
  1968. 'hide_please_select' => TRUE,
  1969. 'options' => array(
  1970. 'sent' => t("Sent"),
  1971. 'received' => t('Received'),
  1972. ),
  1973. 'weight' => 50,
  1974. );
  1975. $fields['phone_outcome'] = array(
  1976. 'type' => 'select',
  1977. 'label' => t('Phone Outcome'),
  1978. 'hide_please_select' => TRUE,
  1979. 'options' => array(
  1980. 'connected' => t("Connected"),
  1981. 'no_answer' => t('No Answer'),
  1982. 'voicemail' => t('Left Voicemail'),
  1983. 'busy' => t('Busy'),
  1984. 'wrong_number' => t('Wrong Number'),
  1985. ),
  1986. 'weight' => 60,
  1987. );
  1988. $fields['engagement_msg'] = array(
  1989. 'type' => 'textarea',
  1990. 'label' => t('Message'),
  1991. 'filter' => 'basic',
  1992. 'weight' => 70,
  1993. );
  1994. $fields['attachment'] = array(
  1995. 'type' => 'file',
  1996. 'label' => t('Attach File(s)'),
  1997. 'weight' => 80,
  1998. 'limit' => 999, // essentially, infinite.
  1999. );
  2000. $fields['visibility'] = array(
  2001. 'type' => 'radios',
  2002. 'label' => 'Visible to:',
  2003. 'options' => array('public' => 'Anyone (incl. student)', 'faculty' => 'Faculty/Staff only'),
  2004. 'weight' => 90,
  2005. );
  2006. $fields['manual_entry'] = array(
  2007. 'type' => 'hidden',
  2008. 'value' => '',
  2009. );
  2010. $fields['to_sms_phone'] = array(
  2011. 'type' => 'hidden',
  2012. 'value' => '',
  2013. );
  2014. $fields['external_msg_id'] = array(
  2015. 'type' => 'hidden',
  2016. 'value' => '',
  2017. );
  2018. $arr['engagement']['fields'] = $fields;
  2019. return $arr;
  2020. } // hook_content_register_content_type
  2021. /**
  2022. * Converts the string into a plain phone number, then tests to see if it is valid or not.
  2023. * RETURNS FALSE if not valid, otherwise, returns the converted phone number. This will be a valid
  2024. * number for use with our SMS service. (in the US anyway).
  2025. */
  2026. function engagements_convert_to_valid_phone_number($num)
  2027. {
  2028. // Remove any non-numeric characters from the num.
  2029. $num = preg_replace("/\D/", '', $num);
  2030. // The number should be 10 characters.
  2031. if (strlen($num) == 10) return $num;
  2032. if (strlen($num) == 11) {
  2033. // Maybe it starts with a "1", which is not necessary.
  2034. if (substr($num, 0, 1) == "1") {
  2035. return substr($num, 1, 10); // ditch the first character.
  2036. }
  2037. }
  2038. // Bad number, return false.
  2039. return FALSE;
  2040. }
  2041. // From: https://stackoverflow.com/questions/4708248/formatting-phone-numbers-in-php
  2042. function engagements_convert_to_pretty_phone_number($phoneNumber, $bool_convert_to_valid_first = TRUE)
  2043. {
  2044. if ($bool_convert_to_valid_first) {
  2045. $phoneNumber = engagements_convert_to_valid_phone_number($phoneNumber);
  2046. if (!$phoneNumber) return FALSE;
  2047. }
  2048. $phoneNumber = preg_replace('/[^0-9]/', '', $phoneNumber);
  2049. if (strlen($phoneNumber) > 10) {
  2050. $countryCode = substr($phoneNumber, 0, strlen($phoneNumber) - 10);
  2051. $areaCode = substr($phoneNumber, -10, 3);
  2052. $nextThree = substr($phoneNumber, -7, 3);
  2053. $lastFour = substr($phoneNumber, -4, 4);
  2054. $phoneNumber = '+' . $countryCode . ' (' . $areaCode . ') ' . $nextThree . '-' . $lastFour;
  2055. } else if (strlen($phoneNumber) == 10) {
  2056. $areaCode = substr($phoneNumber, 0, 3);
  2057. $nextThree = substr($phoneNumber, 3, 3);
  2058. $lastFour = substr($phoneNumber, 6, 4);
  2059. $phoneNumber = '(' . $areaCode . ') ' . $nextThree . '-' . $lastFour;
  2060. } else if (strlen($phoneNumber) == 7) {
  2061. $nextThree = substr($phoneNumber, 0, 3);
  2062. $lastFour = substr($phoneNumber, 3, 4);
  2063. $phoneNumber = $nextThree . '-' . $lastFour;
  2064. }
  2065. return $phoneNumber;
  2066. }
  2067. /**
  2068. * Implements hook_form_alter
  2069. *
  2070. * We want to make various modifications to our form, based on what we are trying to do to it. We may also
  2071. * want to add in some custom javascript as well.
  2072. *
  2073. */
  2074. function engagements_form_alter(&$form, $form_id)
  2075. {
  2076. global $user;
  2077. if ($form_id == 'content_edit_content_form') {
  2078. if (@$form['type']['value'] == 'engagement') {
  2079. $db = get_global_database_handler();
  2080. fp_add_js(fp_get_module_path("engagements") . "/js/engagements.js");
  2081. fp_add_css(fp_get_module_path("engagements") . "/css/style.css");
  2082. // If this is a NEW form, then check for values in the URL to auto-fill.
  2083. if ($form['cid']['value'] === 'new') {
  2084. if (!isset($_GET['engagement_type']) || $_GET['engagement_type'] == '' || $_GET['engagement_type'] == 'new') {
  2085. // This means we are logging a NEW engagement, and not trying to send a txt or email. We need to
  2086. // set our manual_entry value to something meaningful.
  2087. $form['manual_entry']['value'] = 'Y';
  2088. }
  2089. if (isset($_GET['engagement_type']) && $_GET['engagement_type'] != '' && $_GET['engagement_type'] != 'new') {
  2090. $form['engagement_type']['value'] = $_GET['engagement_type'];
  2091. // Since we are setting the type, we do not want to display it as an option.
  2092. $form['engagement_type']['attributes'] = array('class' => 'hidden');
  2093. // There are other fields we do not wish to display as well:
  2094. $form['direction']['attributes'] = array('class' => 'hidden');
  2095. $form['activity_datetime']['attributes'] = array('class' => 'hidden');
  2096. }
  2097. $initials = variable_get_for_school("school_initials", "DEMO", $user->school_id);
  2098. if (isset($_GET['faculty_id'])) {
  2099. $form['faculty_id']['value'] = $_GET['faculty_id'];
  2100. $form['faculty_id']['attributes'] = array('class' => 'hidden');
  2101. $form['mark_from'] = array(
  2102. 'type' => 'markup',
  2103. 'value' => "<div class='engagement-field-mark engagement-from'><label>" . t("From") . ":</label>
  2104. " . $db->get_faculty_name($form['faculty_id']['value']) . "
  2105. </div>",
  2106. 'weight' => $form['faculty_id']['weight'],
  2107. );
  2108. }
  2109. if (isset($_GET['student_id'])) {
  2110. $form['student_id']['value'] = $_GET['student_id'];
  2111. $initials = variable_get_for_school("school_initials", "DEMO", db_get_school_id_for_student_id($_GET['student_id']));
  2112. $form['student_id']['attributes'] = array('class' => 'hidden');
  2113. $extra_mark = "";
  2114. if ($form['engagement_type']['value'] == 'email') {
  2115. $extra_mark = fp_get_student_email($form['student_id']['value']);
  2116. // TODO: The EMAIL header may need to be a setting with replacement values
  2117. $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'])));
  2118. }
  2119. $student_user_id = db_get_user_id_from_cwid($_GET['student_id'], 'student');
  2120. $student_account = fp_load_user($student_user_id);
  2121. if ($form['engagement_type']['value'] == 'txt_msg') {
  2122. // Since this is a txt message, the textare has a max num of characters (1600)
  2123. $form['engagement_msg']['maxlength'] = 1600;
  2124. // Add in select list of the phone numbers we are allowed to select from.
  2125. $from_options = engagements_get_from_phones_for_fapi(FALSE, $user);
  2126. $form['from_sms_phone']['options'] = $from_options;
  2127. if (count($from_options) == 0) {
  2128. $form['from_sms_phone']['suffix'] .= "<h3>" . t("Note: You do
  2129. not have permission to send txt messages from any number.
  2130. See your system administrator for access.") . "</h3>";
  2131. $form['from_sms_phone']['attributes']['readonly'] = "readonly";
  2132. $form['from_sms_phone']['attributes']['class'] .= " readonly";
  2133. $form['from_sms_phone']['attributes']['disabled'] .= "disabled";
  2134. $form['engagement_msg']['attributes']['readonly'] = "readonly";
  2135. $form['engagement_msg']['attributes']['class'] .= " readonly";
  2136. $form['engagement_msg']['attributes']['disabled'] .= "disabled";
  2137. $form['submit_submit']['attributes']['disabled'] = 'disabled';
  2138. $form['submit_submit']['attributes']['class'] .= ' button-disabled';
  2139. }
  2140. $form['from_sms_phone']['required'] = TRUE;
  2141. // get student phone number
  2142. $extra_mark = "(NO MOBILE NUMBER AVAILABLE)";
  2143. $mobile_phone = user_get_attribute($student_user_id, "mobile_phone");
  2144. if ($mobile_phone) {
  2145. $extra_mark = engagements_convert_to_pretty_phone_number($mobile_phone, TRUE);
  2146. } else {
  2147. // Mobile phone was never found, so disable submit and message box.
  2148. $form['engagement_msg']['attributes']['readonly'] = 'readonly';
  2149. $form['engagement_msg']['attributes']['class'] .= ' readonly';
  2150. $form['submit_submit']['attributes']['disabled'] = 'disabled';
  2151. $form['submit_submit']['attributes']['class'] .= ' button-disabled';
  2152. $form['submit_submit']['suffix'] = "<strong>" . t('NOTE: Cannot send a text message as there is no mobile phone number for this recipient.') . "</strong>";
  2153. }
  2154. if (trim($mobile_phone) != "" && !engagements_can_send_sms_to_number($mobile_phone)) {
  2155. // Meaning, the user has opted-out! Disable fields and let the user know.
  2156. $form['engagement_msg']['attributes']['readonly'] = 'readonly';
  2157. $form['engagement_msg']['attributes']['class'] .= ' readonly';
  2158. $form['submit_submit']['attributes']['disabled'] = 'disabled';
  2159. $form['submit_submit']['attributes']['class'] .= ' button-disabled';
  2160. $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>";
  2161. }
  2162. // The header needs to be a setting with replacement values
  2163. $sms_header = engagements_sms_replace_patterns(variable_get('sms_header', '(@initials) from @name:'), "", $user);
  2164. $form['engagement_msg']['prefix'] = t("Your message will automatically begin with <strong>@header</strong>", array("@header" => $sms_header));
  2165. $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>");
  2166. $form['attachment']['attributes'] = array('class' => 'hidden');
  2167. }
  2168. $form['mark_to'] = array(
  2169. 'type' => 'markup',
  2170. 'value' => "<div class='engagement-field-mark engagement-to'><label>" . t("To") . ":</label>
  2171. " . $db->get_student_name($form['student_id']['value'], TRUE) . "<span class='engagement-student-extra-mark'>$extra_mark</span>
  2172. </div>",
  2173. 'weight' => $form['student_id']['weight'],
  2174. );
  2175. }
  2176. // Always set the published = TRUE, and hide.
  2177. $form['published']['value'] = TRUE;
  2178. $form['published']['attributes'] = array('class' => 'hidden');
  2179. // Add a validate handler to form, so we can name sure email or mobile phone numbers are valid.
  2180. $form['#validate_handlers'][] = 'engagements_send_email_or_txt_form_validate';
  2181. // Add a submit handler to form, so we can send email or txt messages
  2182. $form['#submit_handlers'][] = 'engagements_send_email_or_txt_form_submit';
  2183. } // cid == new
  2184. if ($form['engagement_type']['value'] != 'txt_msg') {
  2185. // If this is anything OTHER than a txt_msg, we will show the tinymce editor for the Message
  2186. $form['engagement_msg']['type'] = 'textarea_editor';
  2187. // Also get rid of the sms number field.
  2188. $form['from_sms_phone'] = array(
  2189. 'type' => 'hidden',
  2190. 'value' => '',
  2191. 'required' => FALSE,
  2192. );
  2193. }
  2194. if ($form['engagement_type']['value'] != 'txt_msg' && $form['engagement_type']['value'] != 'email') {
  2195. // Set visibility to 'faculty' by default.
  2196. $form['visibility']['value'] = 'faculty';
  2197. } else {
  2198. $form['visibility']['value'] = 'public';
  2199. $form['visibility']['attributes'] = array('class' => 'hidden');
  2200. }
  2201. } // form type == engagement
  2202. } // content_edit_content_form
  2203. } // hook_form_alter
  2204. /**
  2205. * Get an array of numbers which the user should be notified of when they receive an SMS.
  2206. */
  2207. function engagements_get_user_notify_sms_receipt_values($user_id = NULL) {
  2208. global $user;
  2209. $rtn = array();
  2210. if ($user_id === NULL) {
  2211. $user_id = $user->id;
  2212. }
  2213. $options = engagements_get_from_phones_for_fapi(TRUE);
  2214. foreach ($options as $num => $val) {
  2215. $test = user_get_setting($user_id, "notify_sms_receipt__" . $num);
  2216. if (intval($test) === intval($num)) {
  2217. $rtn[$num] = $num;
  2218. }
  2219. }
  2220. return $rtn;
  2221. }
  2222. /**
  2223. * Returns an array of all the user who should receive notifications when
  2224. * we receive an SMS at a particular number.
  2225. */
  2226. function engagements_get_users_to_be_notified_for_sms_on_number($num) {
  2227. $rtn = array();
  2228. $res = db_query("SELECT user_id FROM user_settings
  2229. WHERE name = ?", array('notify_sms_receipt__' . $num));
  2230. while ($cur = db_fetch_array($res)) {
  2231. $rtn[$cur['user_id']] = intval($cur['user_id']);
  2232. }
  2233. return $rtn;
  2234. }
  2235. /**
  2236. * Returns back the phone lines available.
  2237. *
  2238. * If bool_return_all == FALSE, it means we will first check to make sure the user (specified by account) has permission
  2239. * to send sms for that number.
  2240. *
  2241. */
  2242. function engagements_get_from_phones_for_fapi($bool_return_all = TRUE, $account = NULL, $bool_only_mass_text_numbers = FALSE)
  2243. {
  2244. global $user;
  2245. $rtn = array();
  2246. if ($account == NULL) $account = $user;
  2247. $phones = engagements_get_from_phones($bool_only_mass_text_numbers);
  2248. $default = $phones['default']['num'];
  2249. $rtn[$default] = t("[Default] ") . engagements_convert_to_pretty_phone_number($default) . ' - ' . $phones['default']['description'];
  2250. foreach ($phones['lines'] as $num => $details) {
  2251. if (!isset($rtn[$details['num']])) {
  2252. $rtn[$details['num']] = engagements_convert_to_pretty_phone_number($details['num']) . " - " . $details['description'];
  2253. }
  2254. }
  2255. // if bool_return_all != TRUE, then make sure the account has permissing to send from each number. If not, remove it.
  2256. foreach ($rtn as $num => $details) {
  2257. if (!$bool_return_all) {
  2258. if (!$bool_only_mass_text_numbers && !user_has_permission("can_send_sms_from_$num", $account)) {
  2259. unset($rtn[$num]);
  2260. }
  2261. }
  2262. }
  2263. return $rtn;
  2264. } // engagements_get_from_phones_for_fapi()
  2265. function engagements_send_email_or_txt_form_validate($form, &$form_state)
  2266. {
  2267. $values = $form_state['values'];
  2268. ////////
  2269. // TODO: I am not sure I even need to get the cid here... delete it?
  2270. $cid = NULL;
  2271. if (isset($form_state['content_object']) && is_object($form_state['content_object'])) {
  2272. $cid = $form_state['content_object']->cid; // At this point, the content has already been saved, so we know its cid.
  2273. }
  2274. ////////
  2275. $engagement_type = $values['engagement_type'];
  2276. $manual_entry = @trim(strtoupper($values['manual_entry']));
  2277. if ($engagement_type != 'email' && $engagement_type != 'txt_msg') {
  2278. return; // we are not interested, harmlessly return.
  2279. }
  2280. $student_id = $values['student_id'];
  2281. $faculty_id = $values['faculty_id'];
  2282. $msg = $values['engagement_msg'];
  2283. $from_number = trim($values['from_sms_phone']);
  2284. $db = get_global_database_handler();
  2285. if ($engagement_type == 'email' && $manual_entry != 'Y') {
  2286. $student_email = fp_get_student_email($student_id);
  2287. if (!filter_var($student_email, FILTER_VALIDATE_EMAIL)) { // take advantage of pho built-in email validator
  2288. form_error('engagement_msg', t("Sorry, but the email provided for the student appears to be invalid."));
  2289. return;
  2290. }
  2291. } // type == 'email'
  2292. if ($engagement_type == 'txt_msg' && $manual_entry != 'Y') {
  2293. $student_user_id = db_get_user_id_from_cwid($student_id, 'student');
  2294. $temp = user_get_attribute($student_user_id, 'mobile_phone');
  2295. $mobile_phone = engagements_convert_to_valid_phone_number(trim($temp));
  2296. if (!$mobile_phone) {
  2297. form_error('engagement_msg', t('Sorry, but the mobile phone number provided for the student appears to be invalid.'));
  2298. return;
  2299. }
  2300. } // type == 'txt_msg'
  2301. } // engagements_send_email_or_txt_form_validate
  2302. function engagements_send_email_or_txt_form_submit($form, &$form_state)
  2303. {
  2304. global $user;
  2305. $values = $form_state['values'];
  2306. $cid = $form_state['content_object']->cid; // At this point, the content has already been saved, so we know its cid.
  2307. $content = $form_state['content_object'];
  2308. $engagement_type = $values['engagement_type'];
  2309. $manual_entry = @trim(strtoupper($values['manual_entry']));
  2310. if ($engagement_type != 'email' && $engagement_type != 'txt_msg') {
  2311. return; // we are not interested, harmlessly return.
  2312. }
  2313. $db = get_global_database_handler();
  2314. $student_id = $values['student_id'];
  2315. $faculty_id = $values['faculty_id'];
  2316. $faculty_name = $db->get_faculty_name($faculty_id);
  2317. $initials = variable_get("school_initials", "DEMO");
  2318. $initials = variable_get_for_school("school_initials", "DEMO", db_get_school_id_for_student_id($student_id));
  2319. // get the "from_number"
  2320. $from_number = $values['from_sms_phone'];
  2321. // this needs to be a setting with replacement values!
  2322. // Add header to msg...
  2323. $header = engagements_sms_replace_patterns(variable_get('sms_header', '(@initials) from @name:'), $from_number, $user);
  2324. $msg = $values['engagement_msg'];
  2325. $db = get_global_database_handler();
  2326. if ($engagement_type == 'email' && $manual_entry != 'Y') {
  2327. $student_email = fp_get_student_email($student_id);
  2328. $subject = $values['title'];
  2329. // Add our tracking pixel to the end of the email msg (but not what is saved to our database).
  2330. // We will use that to tell how many times the message gets opened.
  2331. $tracking_img_url = engagements_create_new_tracking_img_url($cid);
  2332. $msg .= "<br><br>----<br>You may respond directly to this email. It will be stored with FlightPath for $faculty_name to then review.";
  2333. $msg .= "\n\n<img src='$tracking_img_url'>";
  2334. // Were there any attachments with this content?
  2335. $attachments = array();
  2336. $attachment_csv = trim(@$content->field__attachment['value']);
  2337. if ($attachment_csv) {
  2338. $temp = explode(",", $attachment_csv);
  2339. foreach ($temp as $fid) {
  2340. if ($fid == "") continue;
  2341. $file = content_get_uploaded_file($fid);
  2342. if ($file) {
  2343. // Handle encrypted files
  2344. $fcont = file_get_contents($file['full_filename']); // the full_filename already contains the path to the file
  2345. if (intval($file['is_encrypted']) === 1) {
  2346. $fcont = encryption_decrypt($fcont);
  2347. }
  2348. $attachments[$file['original_filename']] = $fcont;
  2349. }
  2350. }
  2351. }
  2352. smtp_mail($student_email, $subject, $header . "<br><br>" . $msg, TRUE, $attachments, TRUE); // the TRUE sends as HTML.
  2353. fp_add_message(t("Email has been successfully sent."));
  2354. } // type == 'email'
  2355. $mobile_phone = '';
  2356. if ($engagement_type == 'txt_msg' && $manual_entry != 'Y') {
  2357. $student_user_id = db_get_user_id_from_cwid($student_id, 'student');
  2358. $temp = user_get_attribute($student_user_id, 'mobile_phone');
  2359. $mobile_phone = engagements_convert_to_valid_phone_number(trim($temp));
  2360. // Since we have passed validation, we will assume we have a valid phone number for this student.
  2361. // Send the message!
  2362. $external_msg_id = engagements_send_sms_to_number($mobile_phone, $header . "\n" . $msg, $student_id, $from_number);
  2363. if ($external_msg_id) {
  2364. fp_add_message(t("Text Message has been successfully sent."));
  2365. // save the external_msg_id with the content.
  2366. $content->field__external_msg_id['value'] = $external_msg_id;
  2367. $content->field__to_sms_phone['value'] = $mobile_phone;
  2368. $content->field__from_sms_phone['value'] = $from_number;
  2369. $content->log = "[AUTO] Updating external_msg_id";
  2370. content_save($content);
  2371. }
  2372. } // type == 'txt_msg'
  2373. if ($manual_entry != 'Y') {
  2374. watchdog("engagements", "sent $engagement_type to $student_id ($mobile_phone)");
  2375. }
  2376. else {
  2377. watchdog("engagements", "logged $engagement_type to $student_id");
  2378. }
  2379. watchdog("engagements", "$engagement_type message to $student_id: $header <br> $msg", array(), WATCHDOG_DEBUG);
  2380. } // ... send email or txt form submit
  2381. /**
  2382. * Actually send a text message. Will drupal_set_message() as an error if there is a problem, and return FALSE.
  2383. * Returns TRUE on success, no drupal_set_message is printed.
  2384. */
  2385. function engagements_send_sms_to_number($to_number, $body, $to_cwid = "", $from_number = "default", $bool_send_opt_out_message = TRUE)
  2386. {
  2387. $to_number = engagements_convert_to_valid_phone_number($to_number);
  2388. require_once(fp_get_module_path('engagements', TRUE, FALSE) . '/lib/signalwire/vendor/autoload.php');
  2389. //use SignalWire\Rest\Client;
  2390. // project id, auth tolen, space url
  2391. $project_id = variable_get('sms_project_id', '');
  2392. $auth_token = variable_get('sms_auth_token', '');
  2393. $space_url = variable_get('sms_space_url', '');
  2394. $from_phone = $from_number;
  2395. $phones = engagements_get_from_phones();
  2396. if ($from_phone == "default") {
  2397. $from_phone = engagements_convert_to_valid_phone_number($phones['default']['num']);
  2398. }
  2399. if ($bool_send_opt_out_message) {
  2400. // We must include a "text STOP to opt-out" type message to the bottom of every notification.
  2401. // TODO: May be a setting instead, on the signalwire settings page
  2402. $body .= "\n\n" . "Text STOP to opt-out of txt messages.";
  2403. }
  2404. // project id, auth tolen, space url
  2405. $client = new SignalWire\Rest\Client($project_id, $auth_token, array("signalwireSpaceUrl" => $space_url));
  2406. $external_msg_id = FALSE;
  2407. try {
  2408. $message = $client->messages
  2409. ->create(
  2410. "+1" . $to_number, // to
  2411. array(
  2412. "from" => "+1" . $from_phone, // my signalwire account phone number
  2413. "body" => $body
  2414. )
  2415. );
  2416. $external_msg_id = $message->sid;
  2417. // save message information to our sms_history table.
  2418. $record = $message;
  2419. $message_sid = $record->sid;
  2420. $from_mobile = engagements_convert_to_valid_phone_number($record->from);
  2421. $body = $record->body;
  2422. $num_segments = intval($record->numSegments);
  2423. $date_sent = $record->dateCreated; // Since we just created it, just dateCreated instead of dateSent.
  2424. $throw_away = print_r($date_sent, TRUE); // Not sure why, but I have to do this in order for the data field to populate.
  2425. //$date_sent_ts = strtotime($date_sent->date); // Was causing a mysql error all of a sudden. Just use current time.
  2426. $date_sent_ts = time();
  2427. // The price is not available right now, so we will set it...
  2428. $price = 0.0013 * $num_segments;
  2429. //$fp_price = engagements_get_fp_price($price);
  2430. $fp_price = 0.0033 * $num_segments;
  2431. $direction = $record->direction;
  2432. // Write our data to our sms_history table.
  2433. db_query(
  2434. "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)
  2435. VALUES (?, 'sms', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ",
  2436. array($message_sid, $body, $from_mobile, $to_number, $price, $fp_price, $to_cwid, time(), $direction, '', $date_sent_ts, 0, $num_segments)
  2437. );
  2438. } catch (Exception $e) {
  2439. fp_add_message("An error has occured while trying to send a text message to <em>$to_number</em>. The
  2440. error has been logged. Please have the technical administrator investigate. Error message:" . $e->getMessage(), "error");
  2441. watchdog("engagements_sms", "An error has occured while trying to send a text message to <em>$to_number</em>. Error: " . $e->getMessage() . ". The complete
  2442. error is: <pre>" . print_r($e, true) . "</pre>", array(), WATCHDOG_ERROR);
  2443. return FALSE;
  2444. }
  2445. watchdog("engagements_sms", "SMS message sent successfully to $to_number. External_msg_id = " . $external_msg_id);
  2446. return $external_msg_id;
  2447. } // engagements_send_sms_to_number
  2448. /**
  2449. * Generates a URL to our tracking pixel, based on the cid included.
  2450. */
  2451. function engagements_create_new_tracking_img_url($cid)
  2452. {
  2453. $token = hash('sha512', ($cid . microtime() . mt_rand(9, 99999) . mt_rand(9, 99999)));
  2454. $url = $GLOBALS['fp_system_settings']['base_url'] . '/' . fp_url("engagements-track/$cid/$token/pixel.gif", '', FALSE);
  2455. // Write to database
  2456. db_query('INSERT INTO engagements_tracking (cid, token, opens, updated)
  2457. VALUES (?, ?, ?, ?)', array($cid, $token, 0, time()));
  2458. return $url;
  2459. }
  2460. /**
  2461. * displays the main Engagements tab, which shows the history of past engagements.
  2462. */
  2463. function engagements_display_main()
  2464. {
  2465. global $user, $current_student_id;
  2466. $faculty_id = 0;
  2467. if ($user->is_faculty) {
  2468. $faculty_id = $user->cwid;
  2469. }
  2470. fp_set_title('');
  2471. fp_add_css(fp_get_module_path("engagements") . "/css/style.css");
  2472. fp_add_js(fp_get_module_path("engagements") . "/js/engagements.js");
  2473. fp_add_js(fp_get_module_path("advise") . "/js/advise.js");
  2474. $types = content_get_types();
  2475. $icons = array(
  2476. 'phone' => 'fa-phone',
  2477. 'email' => 'fa-envelope',
  2478. 'txt_msg' => 'fa-comment',
  2479. 'note' => 'fa-file-text',
  2480. 'in_person' => 'fa-user',
  2481. 'social_media' => 'fa-cloud',
  2482. 'other' => 'fa-asterisk',
  2483. 'video_chat' => 'fa-video-camera',
  2484. 'reminder' => 'fa-thumb-tack',
  2485. );
  2486. $rtn = "";
  2487. $rtn .= "<div id='engagements-list-page'>";
  2488. $rtn .= "<div class='add-engagements-buttons'>";
  2489. if (user_has_permission('can_send_email_engagements')) {
  2490. $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>";
  2491. }
  2492. if (user_has_permission('can_send_txt_engagements')) {
  2493. $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>";
  2494. }
  2495. if (user_has_permission('can_log_engagements')) {
  2496. $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>";
  2497. }
  2498. $rtn .= "</div> <!-- add-engagements-buttons -->
  2499. <div class='clear'></div>
  2500. ";
  2501. $rtn .= fp_render_section_title("Recent Engagements");
  2502. $rtn .= "<div class='engagements-list'>";
  2503. $res = pager_query("SELECT DISTINCT(a.cid) FROM content__engagement a, content n
  2504. WHERE field__student_id = ?
  2505. AND a.vid = n.vid
  2506. AND a.cid = n.cid
  2507. AND n.delete_flag = 0
  2508. AND n.published = 1
  2509. 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
  2510. WHERE field__student_id = ?
  2511. AND a.vid = n.vid
  2512. AND a.cid = n.cid
  2513. AND n.delete_flag = 0
  2514. AND n.published = 1");
  2515. while ($cur = db_fetch_object($res)) {
  2516. $cid = $cur->cid;
  2517. $content = content_load($cid);
  2518. // is this "faculty" visibility? If so, do we have access to view?
  2519. if ($content->field__visibility['value'] == 'faculty' && !user_has_permission('can_view_faculty_engagements')) {
  2520. continue;
  2521. }
  2522. $visibility_icon = "";
  2523. if ($content->field__visibility['value'] == 'faculty') {
  2524. $visibility_icon = "<i class='fa fa-lock' title='Visibile to Faculty/Staff only'></i>";
  2525. }
  2526. $icon = $icons[$content->field__engagement_type['value']];
  2527. $type = $content->field__engagement_type['value'];
  2528. $direction = $content->field__direction['value'];
  2529. $msg = $content->field__engagement_msg['display_value'];
  2530. $manual_entry = $content->field__manual_entry['value'];
  2531. $external_msg_id = @trim($content->field__external_msg_id['value']);
  2532. $pre_description = "";
  2533. $sms_extra_details = $sms_delivery_details = "";
  2534. if ($type == 'txt_msg' && $external_msg_id) {
  2535. $phones = engagements_get_from_phones();
  2536. $details = engagements_get_sms_from_history($external_msg_id);
  2537. if ($details) {
  2538. $desc = @$phones['lines'][$details['to_number']]['description'];
  2539. if (!$desc) $desc = @$phones['lines'][$details['from_number']]['description'];
  2540. $sms_extra_details = "<span class='sms-extra-details'>";
  2541. $sms_extra_details .= t("(%desc)", array('%desc' => $desc));
  2542. $sms_extra_details .= "</span>";
  2543. if (isset($details['delivery_status']) && $details['delivery_status'] == 'undelivered') {
  2544. $moreinfo = "";
  2545. $moreinfo .= "<p>" . t("We're sorry, but this message was not delivered.
  2546. Usually this happens when the recipient's carrier (ex: AT&T, Verizon, etc) marks the message as 'spam' and refuses to deliver
  2547. it to the user. <strong>Unfortunately, this is something which is outside the control of @fp.</strong>
  2548. <br><br>
  2549. This may also be due to the user's phone being invalid, disconnected, or incapable of receiving SMS text messages.
  2550. <br><br>
  2551. Your IT/@fp administrator may be able to log into your
  2552. SMS vendor account for more information.
  2553. <br><br>
  2554. Note: the recipient <strong><u>did not receive this message</u></strong>. Please try again via email or another
  2555. communication route.", array("@fp" => variable_get("system_name", "FlightPath"))) . "</p>";
  2556. $moreinfo = base64_encode($moreinfo);
  2557. $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'])) . "
  2558. &nbsp; <a href='javascript:fp_alert(\"$moreinfo\",\"base64\");' title='" . t("More information") . "'><i class='fa fa-question-circle'></i></a></div>";
  2559. }
  2560. }
  2561. }
  2562. if ($manual_entry == 'Y') {
  2563. // This was a manually logged engagement.
  2564. $edit_link = "";
  2565. // Is the user allowed to EDIT this manual engagement?
  2566. if (content_user_access("edit", $content->cid)) {
  2567. $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>";
  2568. }
  2569. $pre_description .= t("Logged:") . " $edit_link &nbsp; &nbsp;";
  2570. }
  2571. ///////////
  2572. // This section we can instead query the content_files table for this cid.
  2573. if ($type == 'txt_msg') {
  2574. $ttemp = array('','');
  2575. if (strstr($msg, "~~MEDIA~~") && strstr($msg, "~~END_MEDIA~~")) {
  2576. $temp = explode("~~MEDIA~~", $msg);
  2577. $msg = $temp[0]; // get rid of the media part of the msg.
  2578. $ttemp = explode("~~END_MEDIA~~", $temp[1]);
  2579. }
  2580. $res2 = db_query("SELECT * FROM content_files WHERE cid = ? ORDER BY cid", array($cid));
  2581. while ($cur2 = db_fetch_array($res2)) {
  2582. $fid = $cur2['fid'];
  2583. $file = content_get_uploaded_file($fid);
  2584. $img_tag = "<img src='{$file['url']}' style='max-width: 200px; height: auto;'>";
  2585. $msg .= "<div class='media-attached'><a href='{$file['url']}' target='_BLANK'>
  2586. $img_tag
  2587. </a></div>";
  2588. }
  2589. $msg .= $ttemp[1]; // in case there was anything AFTER ~~END_MEDIA~~
  2590. }
  2591. /*
  2592. // 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
  2593. // text messaged. Add them.
  2594. $files = explode(",", $temp[1]);
  2595. foreach ($files as $filename) {
  2596. $ttemp = explode("~~END_MEDIA~~", $filename);
  2597. $filename = $ttemp[0];
  2598. $path = base_path() . '/custom/files/content_uploads/' . $filename;
  2599. $img_tag = "<img src='$path' style='max-width: 200px; height: auto;'>";
  2600. $ext_temp = explode(".", $filename);
  2601. $ext = strtolower(@$ext_temp[count($ext_temp) - 1]);
  2602. $image_exts = array("gif", "tiff", "jpg", "jpeg", "png", "bmp");
  2603. if (!in_array($ext, $image_exts)) {
  2604. $mime_icon = content_get_fontawesome_icon_for_mimetype("", $ext);
  2605. $img_tag = "<div class='atached-file'><i class='fa $mime_icon'></i>" . " " . t("View %ext attachment in new window", array("%ext" => $ext)) . "</div>";
  2606. }
  2607. $msg .= "<div class='media-attached'><a href='$path' target='_BLANK'>
  2608. $img_tag
  2609. </a></div>" . $ttemp[1]; // add on the </p> at the end if its there.
  2610. }
  2611. } // contains ~~MEDIA~~
  2612. */
  2613. ////////////
  2614. $header_desc_extra = "";
  2615. if ($type == 'email' && $direction == 'sent' && $manual_entry != 'Y') {
  2616. //$header_desc_extra = "&nbsp; &nbsp; &nbsp; &nbsp; Opens: 1";
  2617. $num_opens = intval(db_result(db_query("SELECT opens FROM engagements_tracking WHERE cid = ?", array($content->cid))));
  2618. $header_desc_extra = "<span class='header-desc-extra'>Opens: $num_opens</span>";
  2619. }
  2620. $fid_csv = @trim($content->field__attachment['value']);
  2621. if ($fid_csv) {
  2622. // were there any attachments?
  2623. $attachment_icon = "";
  2624. if ($fid_csv) {
  2625. $attachment_icon = "<i class='fa fa-paperclip'></i> ";
  2626. $attachments = array();
  2627. $attachment_csv = trim(@$content->field__attachment['value']);
  2628. // TODO: An option here would be to just query the content_files table for this cid.
  2629. if ($attachment_csv) {
  2630. $temp = explode(",", $attachment_csv);
  2631. $msg .= "<label>" . t("Attached File(s):") . "</label><div class='attached-files'>";
  2632. foreach ($temp as $fid) {
  2633. if ($fid == "") continue;
  2634. $file = content_get_uploaded_file($fid);
  2635. if ($file) {
  2636. $mime_icon = content_get_fontawesome_icon_for_mimetype($file['mimetype'], $file['ext']);
  2637. $msg .= "<div class='attached-file'><a href='{$file['url']}'><i class='fa $mime_icon'></i> {$file['original_filename']}</a></div>";
  2638. }
  2639. }
  2640. $msg .= "</div>";
  2641. }
  2642. }
  2643. // We need a random id to assign to this email body.
  2644. $bodyid = "bid_" . md5(microtime() . mt_rand(9, 99999) . mt_rand(9, 9999));
  2645. // If the msg body is too long, let it be expandable.
  2646. $more_link = $less_link = "";
  2647. if (strlen($msg) > 500) {
  2648. $more_link = "<a href='javascript:engagementsExpandBody(\"$bodyid\");' class='more-body-link' id='more_$bodyid'>More</a>";
  2649. $less_link = "<a href='javascript:engagementsShrinkBody(\"$bodyid\");' class='less-body-link' id='less_$bodyid' style='display:none;'>Less</a>";
  2650. }
  2651. $msg = "<div class='email-subject'>{$attachment_icon}Subject: $content->title</div><div class='email-body' id='$bodyid'>$msg</div>$more_link$less_link";
  2652. }
  2653. $author_user_id = intval($content->user_id);
  2654. $author_account = fp_load_user($author_user_id);
  2655. $author_name = $author_account->f_name . " " . $author_account->l_name;
  2656. $act_feed_extra_class = "";
  2657. if ($content->field__from_sms_phone['value']) {
  2658. $act_feed_extra_class .= " activity-feed-from-sms-" . engagements_convert_to_valid_phone_number(fp_get_machine_readable($content->field__from_sms_phone['value']));
  2659. }
  2660. if ($content->field__to_sms_phone['value']) {
  2661. $act_feed_extra_class .= " activity-feed-to-sms-" . engagements_convert_to_valid_phone_number(fp_get_machine_readable($content->field__to_sms_phone['value']));
  2662. }
  2663. $rtn .= "<div class='activity-feed-teaser activity-feed-teaser-$direction activity-feed-teaser-type-$type $act_feed_extra_class'>
  2664. <div class='activity-header-content'>
  2665. <span class='header-icon'><i class='fa $icon'></i></span>
  2666. <span class='header-description'>
  2667. <span class='pre-description'>$pre_description</span>
  2668. {$content->field__engagement_type['display_value']}
  2669. </span>$sms_extra_details
  2670. <span class='header-phone-outcome'>: {$content->field__direction['display_value']} $header_desc_extra</span>
  2671. $sms_delivery_details
  2672. <span class='header-edit'></span>
  2673. <span class='header-date-time'>" . $content->field__activity_datetime['display_value'] . "</span>
  2674. <span class='header-author'>$author_name</span>
  2675. <span class='header-visibility'>$visibility_icon</span>
  2676. </div>
  2677. <div class='activity-contents'>
  2678. <div class='activity-comment'>
  2679. $msg
  2680. </div>
  2681. </div>
  2682. </div>
  2683. <div class='clear'></div>";
  2684. // mark this content as "read" since it appeared on the screen.
  2685. content_set_last_access($content->cid);
  2686. } // while cur
  2687. // Display the pager that was generated by the pager_query above!
  2688. $rtn .= theme_pager(array(t('« newest'), t('‹ newer'), '', t('older ›'), t('oldest »')));
  2689. $rtn .= "</div>"; // engagements-list
  2690. $rtn .= "</div>"; // engagements-list-page
  2691. // Let's set our breadcrumbs
  2692. $db = get_global_database_handler();
  2693. $crumbs = array();
  2694. $crumbs[] = array(
  2695. 'text' => 'Students',
  2696. 'path' => 'student-search',
  2697. );
  2698. $crumbs[] = array(
  2699. 'text' => $db->get_student_name($current_student_id) . " ({$current_student_id})",
  2700. 'path' => 'student-profile',
  2701. 'query' => "current_student_id={$current_student_id}",
  2702. );
  2703. fp_set_breadcrumbs($crumbs);
  2704. watchdog("engagements", "view $current_student_id");
  2705. return $rtn;
  2706. } // 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