Please upgrade here. These earlier versions are no longer being updated and have security issues.
HackerOne users: Testing against this community violates our program's Terms of Service and will result in your bounty being denied.

Preventing notifications for 'secret' comments

I've recently started setting up vanilla for an alpha I'll be running. It's going quite well. I looked at several other forums with whose names I recognized, and Vanilla has beat them hands down so far. And, I really like the clean look. The other forums were way too complicated and distracting.

Users can post feedback to our web site from our mobile app. As the feedback comes in I start a new thread on the forum programmatically (using python's mechanize library). The description that starts the thread is suitable for sharing with the community. I then add a comment to the thread and mark it secret* so that only people with the role SecretViewer can see it. My team will use other secret comments to communicate internally as we investigate issues.

To add "secret"ness, I wrote a plugin inspired by Jonathan Pautsch's PostApproval. My plugin adds a new Yes/No column to Comments (default 'No'). It adds two new roles "SecretViewer" and "SecretManager". SecretManagers can check a box to mark their comments secret when they post them. They can also click "revealSecret" or "makeSecret" in the comment options of any comment. When a user without SecretViewer permission views a discussion, i skip the secret comments. (I'm not currently trying to prevent secret comments from affecting counts, especially since that's per user and at least some of the counts are stored in the db. I've also hidden the activity view so far since I haven't figured that part out yet and I think I can live without it.)

I would like to prevent email notifications from the system that mention the secret messages. Prior to working on notifications, I've been very pleased that I haven't had to change any code outside my new plugin. I haven't figured out the notification system yet, though (and my grasp of some of the other parts is weak too!).

I have played some with hacking CommentModel's Save2() and RecordAdvancedNotifications(). I can check the status of the comment with ($Comment['Secret'] == 'Yes'). I have written a userIsSecretViewer($UserID) method which selects from UserRole and Role. So...I think I'm getting close to knowing whether or not to send a notification to a given user about a particular comment.

But a few things disturb me about my approach so far. Here are the top two:

(1) I'm concerned I'll miss some notifications and leaking the secrets make them not very secret. I haven't found a really good choke point to be sure that I'll catch all notifications about all secret comments. By the time the lowest-level notification code sees the activities/stories they are pretty abstract and don't say much about what comments they affect. (That's a very reasonable design, so I'm not complaining!)

(2) I'm hacking at the core classes and I'd rather not do that. I'm hoping there's a hook I don't know about or don't understand yet. For instance, PostApproval uses CommentModel_BeforeCommentNotifications_Handler and i haven't figured that out yet.

if there's a document I've failed to read, please send me a pointer. :)

thanks in advance for your help,
ab

ps. i'm willing to share this plugin if you think someone else might find it useful.

Comments

  • Hmmm...that got really long. The quick summary is that I'd like a chance to prevent notifications on a per-comment, per-user basis.

    thanks,
    ab

    ps. I'm running Vanilla 2.0.18.8 with plugins FileUpload, Gravatar, Tagging, and WYSIWYG (CLEditor), and my plugin -- AbSecret.

  • @certainmagic said:
    ps. I'm running Vanilla 2.0.18.8 with plugins FileUpload, Gravatar, Tagging, and WYSIWYG (CLEditor), and my plugin -- AbSecret.

    That's the ticket! Try putting this handler in your plugin.

    ActivityModel_BeforeSendNotification_Handler($ActivityModel) {
      var_dump($ActivityModel->EventArguments);
    }
    

    This should point you in the right direction.

    Welcome to the community!

    Search first

    Check out the Documentation! We are always looking for new content and pull requests.

    Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

  • Thanks for the hint, @hgtonight. :)

    I've looked through EventArguments. I'm probably missing what you're trying to show me...

    The only linkage i see to the comment is buried in text fields, such as Body, Story, and Route. Route looks like the only one that might be reasonable to parse and, since I don't completely understand it, I'm loathe to count on it always being in a format I'll recognize.

    Even if I parse out the comment id, it looks like the Email will be sent no matter what happens in the event. (Unless I'm supposed to throw an exception?)

    Can you give me another hint, please? :)

    thanks,
    ab

  • This handler will let you modify the activity model. The event arguments are typically used to determine if you want to modify it.

    In this particular case, you only want to modify the model if your predetermined text exists in the comment.

    If you need more specifics, would you mind pasting in the result of var_dump($ActivityModel) like this ~~~ output of var_dump ~~~?

    Search first

    Check out the Documentation! We are always looking for new content and pull requests.

    Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

  • sorry that took so long. i had to learn about ob_start() and then clean up the output because it includes email addresses, passwords, etc.

    the output is really big, so i'm attaching it instead of inlining it.

    thanks,
    ab

    one.log 65.2K
  • edited April 2013

    btw, here's the code that fires that event (from ActivityModel's SendNotification in applications/dashboard/models/class.activitymodel.php):

    $Notification = array('ActivityID' => $ActivityID, 'User' => $User, 'Email' => $Email, 'Route' => $Activity->Route, 'Story' => $Story, 'Headline' => $ActivityHeadline, 'Activity' => $Activity); $this->EventArguments = $Notification; $this->FireEvent('BeforeSendNotification'); try { $Email->Send(); ....

    since it always calls $Email->Send() after firing that event, I don't understand how we would stop the email from being sent (unless we throw an exception).

  • just an idea..... taking off on @hgtonight's idea.

        ActivityModel_BeforeSendNotification_Handler($ActivityModel) {
        $Email->Message("");   
        $Notification = "";  
        $this->EventArguments = $Notification;
    
        }
    

    or some gyrations of it - haven't tested - but the gist is zero out the message and notification or change the sender email to a null mailbox.

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • Thanks, @peregrine. I can see where that would probably let us interfere with the email. :)

    Is it reasonable to try to identify these messages by looking at the route?

  • @certainmagic said:
    since it always calls $Email->Send() after firing that event, I don't understand how we would stop the email from being sent (unless we throw an exception).

    That's precisely the issue. The answer is "you don't stop it".

    When I developed the Post Scheduler plugin, one of the requirements was to prevent notifications from being sent when new Discussions were saved, so that Users would not receive them before the Post was due to be displayed. I tried multiple approaches, and the only one that worked for me was overriding the ActivityModel by adding a new event, which would be fired before Activities were saved to database.

    With that event in place, I implemented an event handler which processes the Activity before it's saved, and determines if it's associated to a Discussion. If it is, it adds the Discussion ID to the Activity fields, and saves it to the database. Finally, I implemented a handler for ActivityModel::AfterActivityQuery, where I alter the query and add the WHERE clauses that filter out Activities linked to Discussions that should not be displayed.

    The query is then used by the Notification class, which simply processes whatever it gets from it. Since the scheduled notifications are not even returned, they are not sent until they are due.

    All the above took me almost a week of work to figure out, it's not really intuitive. Also, it applies to Vanilla 2.0 only, version 2.1b1 required a fairly different approach altogether (although the result is the same, i.e. not retrieving Notifications that are not due).

  • edited April 2013

    Btw, i've got a version of CommentModel where I'm checking whether or not to skip based on the Comment object and the UserID.

    I'm in the process of changing it to fire a new SkipCommentNotification event which passes the Comment and the UserID and a skip flag in the EventArguments, so at least I won't have my 'secret'-ness in the core code.

    (Just got @businessdad's comment. not sure if he's suggesting a particular approach i could use or if he's pointing out that it's confusing and that i probably have to be invasive.)

  • peregrineperegrine MVP
    edited April 2013

    @certainmagic

    I hope you add your plugin to the add-ons in this community when you get it resolved. It sounds kind of interesting and would vastly help others in the community writing plugins when they see your working code.

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • hgtonighthgtonight MVP
    edited April 2013

    Hmmm... Looks like Gdn_Email only fires an event if it is passed an EventName. Bummer. I have to apologize, @certainmagic, for making you go through all the cleansing effort.

    Search first

    Check out the Documentation! We are always looking for new content and pull requests.

    Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

  • @peregrine said:
    certainmagic

    I hope you add your plugin to the add-ons in this community when you get it resolved.

    i'm willing to do that. i need to learn how though.

    ...and would vastly help others in the community writing plugins when they see your working code.

    hahahhaaaa!
    maybe in a "what not to do" sort of way.
    i've resorted to changing class.commentmodel.php. :(
    and there are a few other rough edges.

  • @certainmagic, there is a wiki article on prepping a plugin for upload to the addons section here. Be sure to add anything else you think is useful!

    Search first

    Check out the Documentation! We are always looking for new content and pull requests.

    Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

  • edited April 2013

    @certainmagic said:
    (Just got businessdad's comment. not sure if he's suggesting a particular approach i could use or if he's pointing out that it's confusing and that i probably have to be invasive.)

    Yes, it's a bit confusing. What I was trying to explain is that, of all approaches I tried, altering the ActivityModel was the only one that actually worked consistently.

    Implementation (Vanilla 2.0.x)

    The code below is extracted from my plugin. I tried to adapt it to your needs (obviously, with a bit of guesswork).

    Step 1 - Altering Activity Model
    Implement two methods in class.activitymodel.php:

         public function Insert($Fields) {
            $this->EventArguments['Fields'] = &$Fields;
            $this->FireEvent('BeforeActivityInsert');
    
            $ActivityID = parent::Insert($Fields);
    
            $this->EventArguments['Fields']['ActivityID'] = $ActivityID;
            $this->FireEvent('AfterActivityInsert');
    
            return $ActivityID;
         }
    
         public function Update($Fields, $Where = FALSE, $Limit = FALSE) {
            $this->EventArguments['Fields'] = &$Fields;
            $this->FireEvent('BeforeActivityUpdate');
    
            $UpdateResult = parent::Update($Fields, $Where, $Limit);
    
            $this->EventArguments['Fields']['UpdateResult'] = &$UpdateResult;
            $this->FireEvent('AfterActivityUpdate');
    
            return $UpdateResult;
         }
    

    This allows to intercept Activities before they are saved. Then implemented a handler in your plugin, to join the Notifications to the Comments to which they belong.

    Step 2 - Altering Activities before they are saved
    The following code goes in your plugin.

        public function ActivityModel_BeforeActivityInsert_Handler($Sender) {
            // Use the Route to find out if an Activity is linked to a Comment. In Vanilla 2.0,
            // Activity entries are not linked to other entities, and the route is the only way 
            // to associate them
            $Route = GetValue('Route', $Sender->EventArguments['Fields']);
    
            if(empty($Route)) {
                return null;
            }
    
            // Check if the Route is related to a Comment. If not, we don't need to
            // do anything.
            $RegExMatches = array();
            if(preg_match('/^\/discussion\/comment\/([0-9]+?)\//i', $Route, $RegExMatches) != 1) {
                return;
            }
    
            // Return the Comment ID, returned by capturing it in a RegEx group
            $CommentID = GetValue(1, $RegExMatches, -1);
    
            // Without a Comment Id, there's no point in proceeding
            if($CommentID <= 0) {
                return null;
            }
    
            // Retrieve the Comment details
            $CommentModel = new CommentModel();
            $Comment = $CommentModel->GetID($CommentID);
    
            // Add the CommentID to the Activity fields. Such field should be added 
            // during Plugin setup. The Activity Model will save it automatically
            $Sender->EventArguments['Fields']['CommentID'] = $Comment->CommentID;
        }
    

    Step 3 - Filtering out the notifications that should not be sent
    The following code goes in your plugin.

        public function ActivityModel_AfterActivityQuery_Handler($Sender) {
            // Only fetch Activities related to Comments having "CommentIsSecret" field
            // set to zero (i.e. not secret)
            $Sender->SQL
                ->LeftJoin('Comment c', 'c.CommentID = a.CommentID')
                ->Where('c.CommentIsSecret', 0);
        }
    
  • @certainmagic said:
    Thanks, businessdad. :)

    You're welcome. :)
    Just keep in mind that I put the above code together quickly, deriving it from my own, but I haven't tested or optimised it. It's meant to show you one way of achieving what you are looking for, you will probably have to adapt it.

  • @businessdad said:
    ... you will probably have to adapt it.

    yeah...that's why i'm going to stick with my current approach for now. (and that i need to start my alpha on my product in the next few days!)

    my approach is more direct because i don't have to run reverse engineer from the activity which comment and user its related to. unfortunately, i think mine is more invasive because i have to check in several places. yours is also better in that it's in the choke points, so as long as it can recognize the messages, it's probably less fragile. AND i suspect your approach is much more vanilla-like, and therefore better!

    i'm attaching a zip file with my updated version of class.commentmodel.php and my plugin. you can diff my class.commentmodel.php against the version in 2.0.18.8, or
    just search for the word "skipCommentNotification". please let me know if you see problems or things I can do better. :)

    thanks to all of you for your help. :)

  • edited April 2013

    I named the permissions badly in that zip.

    replace Plugins.Attachments.AbSecret.View with Plugins.AbSecret.View
    replace Plugins.Attachments.AbSecret.Manage with Plugins.AbSecret.Manage

    Btw, if you guys actually think this is useful and not too dangerous. I'll be happy to put a zip in the AddOns area. I just probably can't spend too much time maintaining it right now. :(

    ttfn,
    ab

  • Btw, if you guys actually think this is useful and not too dangerous. I'll be happy to put a zip in the AddOns area. I just probably can't spend too much time maintaining it right now. :(

    Seems like a good candidate to add to addons. I haven't tested it, but you have and it doesn't crash you server.

    or you can wait till you have more time.

    thanks for the plugin!

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

Sign In or Register to comment.