From f183b6f47f50b20b72b7123d59933da9dbf38ad3 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Sun, 30 Dec 2007 22:02:16 +0000 Subject: [PATCH] - web: add general-purpose notification mechanism. Users can choose whether to get 1 email per notification, a daily "digest" email, or no email. (All notifications are shown on the Account page). Currently used for: - Friend requests and confirmations - Posts to subscribed threads - Private messages NOTE: To implement the "daily digest" feature, projects must add a periodic task for html/ops/notify.php to config.xml - web: have project_footer() generate links for Account Page and Message Boards as well as Home NOTE: projects that want this change will have to modify their own project.inc. svn path=/trunk/boinc/; revision=14447 --- bolt_checkin_notes.txt | 5 + checkin_notes | 43 ++++++ db/constraints.sql | 2 +- db/schema.sql | 7 + html/inc/boinc_db.inc | 4 +- html/inc/forum.inc | 27 +++- html/inc/forum_db.inc | 23 ++++ html/inc/forum_email.inc | 11 +- html/inc/friend.inc | 78 +++++++++++ html/inc/pm.inc | 59 ++++++-- html/inc/user.inc | 40 +++--- html/ops/db_update.php | 7 + html/ops/notify.php | 91 ++++++++++++ html/project.sample/project.inc | 9 +- html/user/edit_forum_preferences_action.php | 3 +- html/user/edit_forum_preferences_form.php | 35 +++-- html/user/forum_subscribe.php | 2 +- html/user/forum_thread.php | 2 + html/user/friend.php | 145 ++++++++++++-------- html/user/pm.php | 5 + tools/make_project | 5 + 21 files changed, 484 insertions(+), 119 deletions(-) create mode 100644 html/inc/friend.inc create mode 100755 html/ops/notify.php diff --git a/bolt_checkin_notes.txt b/bolt_checkin_notes.txt index 415a105bd4..9e1ba300a9 100644 --- a/bolt_checkin_notes.txt +++ b/bolt_checkin_notes.txt @@ -22,3 +22,8 @@ David Dec 19 2007 user/ bolt_course.php bolt_sched.php + +David Dec 27 2007 + - preliminary implementation of exercise_set + + inc/bolt.inc diff --git a/checkin_notes b/checkin_notes index 7358bbb83b..3a49d39610 100644 --- a/checkin_notes +++ b/checkin_notes @@ -12561,3 +12561,46 @@ David 27 Dec 2007 hosts_user.php tools/ upgrade + +David 30 Dec 2007 + - web: add general-purpose notification mechanism. + Users can choose whether to get 1 email per notification, + a daily "digest" email, or no email. + (All notifications are shown on the Account page). + Currently used for: + - Friend requests and confirmations + - Posts to subscribed threads + - Private messages + + NOTE: To implement the "daily digest" feature, projects must add + a periodic task for html/ops/notify.php to config.xml + - web: have project_footer() generate links for + Account Page and Message Boards as well as Home + NOTE: projects that want this change + will have to modify their own project.inc. + + db/ + constraints.sql + schema.sql + html/ + inc/ + boinc_db.inc + friend.inc (new) + forum.inc + forum_db.inc + forum_email.inc + pm.inc + user.inc + ops/ + db_update.php + notify.php + project.sample/ + project.inc + user/ + edit_forum_preferences_action.php + edit_forum_preferences_form.php + forum_subscribe.php + forum_thread.php + pm.php + tools/ + make_project diff --git a/db/constraints.sql b/db/constraints.sql index dc2be71e1e..84503b97da 100644 --- a/db/constraints.sql +++ b/db/constraints.sql @@ -122,4 +122,4 @@ alter table friend add unique friend_u (user_src, user_dest); alter table notify - add index notify_u (userid); + add unique notify_un (userid, type, opaque); diff --git a/db/schema.sql b/db/schema.sql index 76e4072437..f7382f5aee 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -398,8 +398,11 @@ create table subscriptions ( userid integer not null, threadid integer not null, notified_time integer not null default 0 + -- deprecated ) engine=InnoDB; +-- actually: prefs for all community features +-- create table forum_preferences ( userid integer not null default 0, signature varchar(254) not null default '', @@ -425,6 +428,10 @@ create table forum_preferences ( ignore_sticky_posts tinyint not null default 0, banished_until integer not null default 0, pm_notification tinyint not null default 0, + -- actually controls all notifications. + -- 0 = no email + -- 1 = email per event + -- 2 = digest email primary key (userid) ) engine=MyISAM; diff --git a/html/inc/boinc_db.inc b/html/inc/boinc_db.inc index 6a74a54ace..d3f9774c35 100644 --- a/html/inc/boinc_db.inc +++ b/html/inc/boinc_db.inc @@ -265,7 +265,9 @@ class BoincPrivateMessage { } static function insert($clause) { $db = BoincDb::get(); - return $db->insert('private_messages', $clause); + $ret = $db->insert('private_messages', $clause); + if (!$ret) return $ret; + return $db->insert_id(); } static function count($clause) { $db = BoincDb::get(); diff --git a/html/inc/forum.inc b/html/inc/forum.inc index a831751956..b350c691c3 100644 --- a/html/inc/forum.inc +++ b/html/inc/forum.inc @@ -710,6 +710,16 @@ function post_warning() { "; } +function notify_subscriber($thread, $user) { + BoincForumPrefs::lookup($user); + if ($user->prefs->pm_notification == 1) { + send_reply_notification_email($thread, $user); + } + $now = time(); + $type = NOTIFY_SUBSCRIBED_POST; + BoincNotify::replace("userid=$user->id, create_time=$now, type=$type, opaque=$thread->id"); +} + // Various functions for adding/hiding/unhiding stuff. // These take care of counts and timestamps. // Don't do these things directly - use these functions @@ -722,16 +732,13 @@ function create_post($content, $parent_id, $user, $forum, $thread, $signature) { $id = BoincPost::insert("(thread, user, timestamp, content, parent_post, signature) values ($thread->id, $user->id, $now, '$content', $parent_id, $sig)"); if (!$id) return null; - // send emails to subscribed users + // notify subscribed users // $subs = BoincSubscription::enum("threadid=$thread->id"); foreach ($subs as $sub) { if ($user->id == $sub->userid) continue; $user2 = BoincUser::lookup_id($sub->userid); - $visit_time = thread_last_visit($user2, $thread); - if ($visit_time > $sub->notified_time) { - send_reply_notification_email($thread, $user2); - } + notify_subscriber($thread, $user2); } $user->update("posts=posts+1"); $thread->update("replies=replies+1, timestamp=$now"); @@ -1060,5 +1067,15 @@ function is_forum_visible_to_user($forum, $user) { return true; } +function subscribed_post_email_line($notify) { + $thread = BoincThread::lookup_id($notify->opaque); + return "There are new posts in the thread '$thread->title'"; +} + +function subscribed_post_web_line($notify) { + $thread = BoincThread::lookup_id($notify->opaque); + return "New posts in the thread id>$thread->title"; +} + ?> diff --git a/html/inc/forum_db.inc b/html/inc/forum_db.inc index 643e886df3..f0143a8201 100644 --- a/html/inc/forum_db.inc +++ b/html/inc/forum_db.inc @@ -241,6 +241,15 @@ class BoincFriend { $db = BoincDb::get(); return $db->enum('friend', 'BoincFriend', $clause); } + static function delete($id1, $id2) { + $db = BoincDb::get(); + $db->delete_aux('friend', "user_src=$id1 and user_dest=$id2"); + $db->delete_aux('friend', "user_src=$id2 and user_dest=$id1"); + } + static function replace($clause) { + $db = BoincDb::get(); + return $db->replace('friend', $clause); + } } class BoincNotify { @@ -250,6 +259,10 @@ class BoincNotify { if (!$ret) return null; return $db->insert_id(); } + static function replace($clause) { + $db = BoincDb::get(); + return $db->replace('notify', $clause); + } static function enum($clause) { $db = BoincDb::get(); return $db->enum('notify', 'BoincNotify', $clause); @@ -262,9 +275,19 @@ class BoincNotify { $db = BoincDb::get(); return $db->delete($this, 'notify'); } + function delete_aux($clause) { + $db = BoincDb::get(); + $db->delete_aux('notify', $clause); + } + static function enum_general($query) { + $db = BoincDb::get(); + return $db->enum_general('BoincNotify', $query); + } } define ('NOTIFY_FRIEND_REQ', 1); define ('NOTIFY_FRIEND_ACCEPT', 2); +define ('NOTIFY_PM', 3); +define ('NOTIFY_SUBSCRIBED_POST', 4); ?> diff --git a/html/inc/forum_email.inc b/html/inc/forum_email.inc index bfd4d2bb8b..3aa0d7aae2 100644 --- a/html/inc/forum_email.inc +++ b/html/inc/forum_email.inc @@ -110,8 +110,15 @@ For further information and assistance with ".PROJECT." go to ".MASTER_URL; function send_reply_notification_email($thread, $user){ $title = PROJECT . ": A user has posted to '". stripslashes($thread->title) ."'"; $link = URL_BASE . "forum_thread.php?id=" . $thread->id; - $body = "Another " . PROJECT . " user has posted to the thread \"" . stripslashes($thread->title) . "\".\n" - ."To view the updated thread, visit the following URL:\n\n$link"; + $body = "Another " . PROJECT . " user has posted to the thread +\"" . stripslashes($thread->title) . "\".\n" + ."To view the updated thread, visit:\n$link + +-------------------------- +To change email preferences, visit: +".URL_BASE."edit_forum_preferences_form.php +Do not reply to this message. +"; return send_email($user, $title, $body); } diff --git a/html/inc/friend.inc b/html/inc/friend.inc new file mode 100644 index 0000000000..7cbc2d2a7e --- /dev/null +++ b/html/inc/friend.inc @@ -0,0 +1,78 @@ +opaque); + if (!$src_user) return ""; + return "$src_user->name has added you as a friend; please confirm"; +} + +function friend_notify_accept_email_line($notify) { + $src_user = BoincUser::lookup($notify->opaque); + if (!$src_user) return ""; + return "$src_user->name has confirmed you as a friend"; +} + +// The following two are what gets put in the Notification +// area of user's Account page +// +function friend_notify_req_web_line($notify) { + $user = BoincUser::lookup_id($notify->opaque); + return " + opaque>Friendship request from $user->name + "; +} + +function friend_notify_accept_web_line($notify) { + $user = BoincUser::lookup_id($notify->opaque); + return " + opaque>Friendship confirmation from $user->name + "; +} + +function send_friend_request_email($src_user, $dest_user, $msg) { + $message = " +$src_user->name has added you as a friend at ".PROJECT.". +"; + if (strlen($msg)) { + $message .= " +$src_user->name says: $msg +"; + } + + $message .= " +Please accept or decline by visiting +".URL_BASE."home.php + +-------------------------- +To change email preferences, visit: +".URL_BASE."edit_forum_preferences_form.php +Do not reply to this message. +" ; + send_email($dest_user, "[".PROJECT."] friend request", $message); +} + +function send_friend_accept_email($dest_user, $src_user, $msg) { + $message = " +$dest_user->name has confirmed you as a friend at ".PROJECT.". +"; + if (strlen($msg)) { + $message .= " +$dest_user->name says: $msg +"; + } + + $message .= " +Visit your Account page at +".URL_BASE."home.php + +-------------------------- +To change email preferences, visit: +".URL_BASE."edit_forum_preferences_form.php +Do not reply to this message. +" ; + send_email($src_user, "[".PROJECT."] friend confirmed", $message); +} + +?> diff --git a/html/inc/pm.inc b/html/inc/pm.inc index 51aaf7cc77..82c944192c 100644 --- a/html/inc/pm.inc +++ b/html/inc/pm.inc @@ -77,19 +77,10 @@ function pm_create_new($error = null) { exit(); } -function pm_send($to, $subject, $content) { - global $logged_in_user; - $userid = $to->id; - $senderid = $logged_in_user->id; - $sql_subject = mysql_real_escape_string($subject); - $sql_content = mysql_real_escape_string($content); - $to_user = BoincUser::lookup_id($userid); - BoincForumPrefs::lookup($to_user); - $send_email = false; - if ($to_user->prefs->pm_notification) $send_email = true; - BoincPrivateMessage::insert("(userid, senderid, date, subject, content) VALUES ($userid, $senderid, UNIX_TIMESTAMP(), '$sql_subject', '$sql_content')"); - if ($send_email) { - $message = " +function send_pm_notification_email( + $logged_in_user, $to_user, $subject, $content +) { + $message = " You have received a new private message at ".PROJECT.". From: $logged_in_user->name (ID $logged_in_user->id) @@ -101,12 +92,50 @@ $content To delete or respond to this message, visit: ".URL_BASE."pm.php -To disable email delivery of private messages, visit: +To change email preferences, visit: ".URL_BASE."edit_forum_preferences_form.php Do not reply to this message. " ; - send_email($to, "[".PROJECT."] private message", $message); + send_email($to_user, "[".PROJECT."] private message", $message); +} + +function pm_email_line($notify) { + $pm = BoincPrivateMessage::lookup_id($notify->opaque); + $from_user = BoincUser::lookup_id($pm->senderid); + return "$from_user->name sent you a private message; subject: '$pm->subject'"; +} + +function pm_web_line($notify) { + $pm = BoincPrivateMessage::lookup_id($notify->opaque); + $from_user = BoincUser::lookup_id($pm->senderid); + return "Private message from $from_user->name, subject: $pm->subject"; +} + +function pm_send($to_user, $subject, $content) { + global $logged_in_user; + $sql_subject = mysql_real_escape_string($subject); + $sql_content = mysql_real_escape_string($content); + $mid = BoincPrivateMessage::insert("(userid, senderid, date, subject, content) VALUES ($to_user->id, $logged_in_user->id, UNIX_TIMESTAMP(), '$sql_subject', '$sql_content')"); + if (!$mid) { + error_page("Couldn't create message"); } + // send email notification if needed + // + BoincForumPrefs::lookup($to_user); + switch ($to_user->prefs->pm_notification) { + case 0: + case 2: + break; + case 1: + send_pm_notification_email( + $logged_in_user, $to_user, $subject, $content + ); + break; + } + + // create notification in any case + // + BoincNotify::insert("(userid, create_time, type, opaque) values ($to_user->id, ".time().", ".NOTIFY_PM.", $mid)"); } function pm_count($userid, $duration) { diff --git a/html/inc/user.inc b/html/inc/user.inc index 40495c211f..59a47ea949 100644 --- a/html/inc/user.inc +++ b/html/inc/user.inc @@ -4,6 +4,7 @@ require_once("../inc/credit.inc"); require_once("../inc/email.inc"); require_once("../inc/util.inc"); require_once("../inc/team.inc"); +require_once("../inc/friend.inc"); require_once("../inc/forum_db.inc"); function parse_project($f) { @@ -150,20 +151,15 @@ function show_user_stats_private($user) { function notify_description($notify) { switch ($notify->type) { case NOTIFY_FRIEND_REQ: - $user = BoincUser::lookup_id($notify->opaque); - return " - opaque>Friend request from $user->name -
- "; - break; + return friend_notify_req_web_line($notify); case NOTIFY_FRIEND_ACCEPT: - $user = BoincUser::lookup_id($notify->opaque); - return " - opaque>$user->name confirmed as friend -
- "; - break; + return friend_notify_accept_web_line($notify); + case NOTIFY_PM: + return pm_web_line($notify); + case NOTIFY_SUBSCRIBED_POST: + return subscribed_post_web_line($notify); } + return "Unknown notification type: $notify->type"; } // show static user info (private) @@ -222,17 +218,18 @@ function show_user_info_private($user) { if (count($notifies)) { $x = ""; foreach ($notifies as $notify) { - $x .= notify_description($notify); + $x .= notify_description($notify)."
"; } row2("Notifications", $x); } $friends = BoincFriend::enum("user_src=$user->id and reciprocated=1"); if (count($friends)) { - $x = ""; + $x = null; foreach($friends as $friend) { + if ($x) $x .= " | "; $fuser = BoincUser::lookup_id($friend->user_dest); - $x .= " ". user_links($fuser); + $x .= user_links($fuser, true); } row2("Friends", $x); } @@ -305,10 +302,11 @@ function show_user_summary_public($user) { $friends = BoincFriend::enum("user_src=$user->id and reciprocated=1"); if (count($friends)) { - $x = ""; + $x = null; foreach($friends as $friend) { + if ($x) $x .= " | "; $fuser = BoincUser::lookup_id($friend->user_dest); - $x .= " ".user_links($fuser, true); + $x .= user_links($fuser, true); } row2("Friends", $x); } @@ -316,8 +314,12 @@ function show_user_summary_public($user) { if ($g_logged_in_user && $g_logged_in_user->id != $user->id) { row2("Contact", "id."\">Send private message"); $friend = BoincFriend::lookup($g_logged_in_user->id, $user->id); - if (!$friend) { - row2("Community", "id.">Add as friend"); + if ($friend && $friend->reciprocated) { + row2("This person is a friend", + "id>Cancel friendship" + ); + } else { + row2("Community", "id>Add as friend"); } } } diff --git a/html/ops/db_update.php b/html/ops/db_update.php index 2570f504b1..a8dd4fcea5 100755 --- a/html/ops/db_update.php +++ b/html/ops/db_update.php @@ -534,6 +534,13 @@ function update_12_18_2007() { "); } +function update_12_28_2007() { + do_query("alter table notify drop index notify_u"); + do_query("alter table notify + add unique notify_un (userid, type, opaque) + "); +} + // modify the following to call the function you want. // Make sure you do all needed functions, in order. // (Look at your DB structure using "explain" queries to see diff --git a/html/ops/notify.php b/html/ops/notify.php new file mode 100755 index 0000000000..a1f1f58140 --- /dev/null +++ b/html/ops/notify.php @@ -0,0 +1,91 @@ +#!/usr/bin/env php + +email_addr\n"; +} + +function send_notify_emails() { + $t = time() - (86400 + 3600); // 1-hour slop factor + $query = "select notify.* from DBNAME.notify, DBNAME.forum_preferences where forum_preferences.pm_notification=2 and notify.userid = forum_preferences.userid and notify.create_time > $t"; + + $notifies = BoincNotify::enum_general($query); + $userid = 0; + $message = ""; + $i = 1; + foreach ($notifies as $notify) { + if ($userid && $notify->userid != $userid) { + send_notify_email($userid, $message); + $message = ""; + $i = 1; + } + $userid = $notify->userid; + $message .= "$i) "; + switch ($notify->type) { + case NOTIFY_FRIEND_REQ: + $message .= friend_notify_req_email_line($notify); + break; + case NOTIFY_FRIEND_ACCEPT: + $message .= friend_notify_accept_email_line($notify); + break; + case NOTIFY_PM: + $message .= pm_email_line($notify); + break; + case NOTIFY_SUBSCRIBED_POST: + $message .= subscribed_post_email_line($notify); + break; + } + $message .= "\n"; + $i++; + } + if ($userid) { + send_notify_email($userid, $message); + } +} + +$t = time_str(time()); +echo "Starting at $t\n"; + +delete_old_notifies(); +send_notify_emails(); + +$t = time_str(time()); +echo "Ending at $t\n\n"; + +?> diff --git a/html/project.sample/project.inc b/html/project.sample/project.inc index cf495e6a3c..f6116d66fc 100644 --- a/html/project.sample/project.inc +++ b/html/project.sample/project.inc @@ -7,7 +7,7 @@ require_once("../inc/util.inc"); $master_url = parse_config(get_config(), ""); -define("PROJECT", "Test Project"); +define("PROJECT", "REPLACE WITH PROJECT NAME"); define("URL_BASE", $master_url); define("IMAGE_PATH", "../user_profile/images/"); define("IMAGE_URL", "user_profile/images/"); @@ -15,9 +15,10 @@ define("PROFILE_PATH", "../user_profile/"); define("PROFILE_URL", "user_profile/"); define("LANGUAGE_FILE", "languages.txt"); define("STYLESHEET", "white.css"); -define("COPYRIGHT_HOLDER", "Test Group"); +define("COPYRIGHT_HOLDER", "REPLACE WITH COPYRIGHT HOLDER"); define("SYS_ADMIN_EMAIL", "admin@$master_url"); -define("UOTD_ADMIN_EMAIL", "admin@$master_url"); # who gets user of the day pool running low e-mails? +define("UOTD_ADMIN_EMAIL", "admin@$master_url"); + // who gets user of the day pool running low e-mails? // Email addresses separated by pipe ( | ) that will receive user reported // offensive forum posts. @@ -37,7 +38,7 @@ function project_banner($title) { function project_footer($show_return, $show_date) { echo "

"; if ($show_return) { - echo "Return to ".PROJECT." main page
\n"; + echo "Home | My Account | Message Boards
\n"; } echo "

Copyright © ".date("Y ").COPYRIGHT_HOLDER."
\n"; if ($show_date) { diff --git a/html/user/edit_forum_preferences_action.php b/html/user/edit_forum_preferences_action.php index cc8a506cc2..3e6095d030 100644 --- a/html/user/edit_forum_preferences_action.php +++ b/html/user/edit_forum_preferences_action.php @@ -83,7 +83,6 @@ if ($avatar_type==0){ } } -// Update some simple prefs that are either on or off $images_as_links = ($_POST["forum_images_as_links"]!="")?1:0; $link_popup = ($_POST["forum_link_popup"]!="")?1:0; $hide_avatars = ($_POST["forum_hide_avatars"]!="")?1:0; @@ -91,7 +90,7 @@ $hide_signatures = ($_POST["forum_hide_signatures"]!="")?1:0; $jump_to_unread = ($_POST["forum_jump_to_unread"]!="")?1:0; $ignore_sticky_posts = ($_POST["forum_ignore_sticky_posts"]!="")?1:0; $no_signature_by_default = ($_POST["signature_by_default"]!="")?0:1; -$pm_notification = ($_POST["pm_notification"]!="")?1:0; +$pm_notification = post_int("pm_notification"); //$low_rating_threshold = post_int("forum_low_rating_threshold"); //$high_rating_threshold = post_int("forum_high_rating_threshold"); $signature = stripslashes($_POST["signature"]); diff --git a/html/user/edit_forum_preferences_form.php b/html/user/edit_forum_preferences_form.php index 91b06ec875..7b7f63d756 100644 --- a/html/user/edit_forum_preferences_form.php +++ b/html/user/edit_forum_preferences_form.php @@ -27,15 +27,31 @@ echo "