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.

Writing a bot for my forum

Hey guys,

I'm trying to write a plugin that can create a new Discussion, and might also make new posts.
Now I've lifted this code from vanilla\applications\vanilla\settings\stub.php, and it works sort of.
It creates the discussion, adds a comment to it. I can browse to those discussions by browsing to http://localhost/vanilla/index.php?p=/discussion/3/discussie#latest, for example.

But somehow it breaks my forum, and if I browse to http://localhost/vanilla/index.php?p=/categories/general, it doesn't show the different threads, only this message:

Debug Trace
Info delivery method: XHTML
Info delivery type: ALL
Info syndication: NONE

Anyone know what's up with that?

The code I use to create a new discussion and comment:

$DiscussionModel=new DiscussionModel();
$DiscussionTitle = "BAM! You’ve got a sweet forum";
$DiscussionBody = "Default message";
$CommentBody = "Default message";
$SQL=Gdn::sql();
// Prep content meta data
$SystemUserID = Gdn::userModel()->GetSystemUserID();
$Now = Gdn_Format::toDateTime();
$CategoryID = val('CategoryID', CategoryModel::DefaultCategory());

// Insert first discussion & comment
$DiscussionID = $SQL->Options('Ignore', true)->insert('Discussion', array(
'Name' => t('StubDiscussionTitle', $DiscussionTitle),
'Body' => t('StubDiscussionBody', $DiscussionBody),
'Format' => 'Html',
'CategoryID' => $CategoryID,
'ForeignID' => 'stub',
'InsertUserID' => $SystemUserID,
'DateInserted' => $Now,
'DateLastComment' => $Now,
'LastCommentUserID' => $SystemUserID,
'CountComments' => 1
));
$CommentID = $SQL->insert('Comment', array(
'DiscussionID' => $DiscussionID,
'Body' => t('StubCommentBody', $CommentBody),
'Format' => 'Html',
'InsertUserID' => $SystemUserID,
'DateInserted' => $Now
));
$SQL->update('Discussion')
->set('LastCommentID', $CommentID)
->where('DiscussionID', $DiscussionID)
->put();
$DiscussionModel->UpdateDiscussionCount($CategoryID);
$DiscussionModel->save($DiscussionData);

«1

Comments

  • CaylusCaylus ✭✭
    edited August 2016

    Apparently it didn't have anything to do with my code, now I created an extra category everything works as expected.

    Yay!

    Ignore the $DiscussionModel->save($DiscussionData); at the end by the way, that was a remainder of another draft of a comment I was going to place on here.

  • R_JR_J Ex-Fanboy Munich Admin

    Do you know that there are already some bot plugins?

    https://vanillaforums.org/addon/bot-plugin
    https://github.com/vanilla/minion

    I think the last one wouldn't work out of the box with the open source version of Vanilla since it relies quite often on a closed source plugin. But it could be a source of inspiration.

    Instead of using SQL, I would advice to use the models own save methods:

    $userID = Gdn::userModel()->getSystemUserID();
    $categoryID = CategoryModel::defaultCategory()['CategoryID'];
    
    $discussion = array(
        'Name' => 'Hello From Robot',
        'Format' => c('Garden.InputFormatter'),
        'CategoryID' => $categoryID,
        'Body' => 'Call me HAL',
        'InsertUserID' => $userID,
        'DateInserted' => Gdn_Format::toDate()
    );
    $discussionModel = new DiscussionModel();
    $discussionID = $discussionModel->save($discussion);
    
    $comment = array(
        'DiscussionID' => $discussionID,
        'Format' => c('Garden.InputFormatter'),
        'Body' => 'Whatever',
        'InsertUserID' => $userID,
        'DateInserted' => Gdn_Format::toDate()
    );
    $commentModel = new CommentModel();
    $commentID = $commentModel->save($comment);
    $commentModel->save2($commentID, true);
    

    You should take a look at both classes (class.discussionmodel.php and class.commentmodel.php) to see more information on what happens "behind the scenes" when you call that method. Looking at Vanillas source code is very insightful.

  • LincLinc Detroit Admin

    I'm keen in particular to know what you think of using Bot if you choose to look into it.

  • @R_J,

    I tried using the models first. I wanted to write a bot that monitored a mailbox, and then post the information it received from certain senders to the forum. I wanted to use https://vanillaforums.org/addon/feeddiscussions-plugin as a basis for my own plugin. But I thought that I could put use the code to monitor the mailbox outside the plugin class, to excecute it every time the file was included instead of having to rely on handlers.

    That produced some weird categorymodel error I couldn't figure out (because apparently you're not supposed to do that), which is why I tried using SQL queries directly (because I was more confident I knew what was going on). When I figured out the weird error was due to my code being executed at a wrong time, I didn't think of switching it back from direct SQL to models again.

    But thanks for the snippet of code, I now at least have a working example of how it's supposed to be done!

    And thanks for the link to the two bots!

    @Linc, I'm afraid my bot will be less interactive than Bot seems to be judging from its source code. I'll keep you updated however if I decide to use it!

  • R_JR_J Ex-Fanboy Munich Admin

    Wrap your code in a public function pluginController_yourPluginName_create($sender) {}
    Use a config setting which stores the time of the last check.
    Let the first lines in your code be a check if now - that config setting is > 5 minutes
    Create a cron job that calls yourforum.com/plugin/yourpluginname periodically

  • Hey R_J, I was using Base_Render_Before(), since that seemed to fire every time the forum was loaded.

    How do you set config settings with Vanilla? I wrote my own code to store values, because I couldn't figure out how to store permanent variables with standard functions.

    Anyway, I'm getting a white screen. Any more helpful pointers would be very much appreciated :D

    function Base_Render_Before() {
        $this->startChecking();
    }
    

    function startChecking() {
    //Here I check wether it's been long enough to check again
    if (!$this->needsChecking()) {
    return;
    }
    $DiscussionModel = new DiscussionModel();

        $DiscussionID = $this->getValueFromDB("DiscussionID");
        $SystemUserID = Gdn::userModel()->GetSystemUserID();
        $Now = Gdn_Format::toDateTime();
        $CategoryID = val('CategoryID', CategoryModel::DefaultCategory());
    

    //If discussionID == false, there's no discussion yet so one should be created
    if (!$DiscussionID) {
    $discussion = array(
    'Name' => 'Hello From Robot',
    'Format' => c('Garden.InputFormatter'),
    'CategoryID' => $CategoryID,
    'Body' => 'Call me HAL',
    'InsertUserID' => $SystemUserID,
    'DateInserted' => Gdn_Format::toDate()
    );
    $DiscussionID = $DiscussionModel->save($discussion);
    $this->setValueInDB("DiscussionID", $DiscussionID);
    }
    //Username and password of my gmail account
    $username = "";
    $password = "";
    $mailbox = '{imap.gmail.com:993/ssl/novalidate-cert}INBOX';
    $mbox = imap_open($mailbox, $username, $password);
    $messageUID = $this->getValueFromDB('LastUIDRead');
    //If there haven't been messages been imported before start with the first
    $startIndex = $messageUID ? $messageUID : 1;
    $num_msgs = imap_num_msg($mbox);
    //Import a max of 10 messages each time to make use you don't use too much execution time
    for ($i = $startIndex; $i <= $num_msgs && $i < $startIndex + 10; $i++) {
    $commentBody = imap_body($mbox, $i);
    $commentHeader = imap_headerinfo($mbox, $i);
    //For testing purposes import even messages, real thing will import based on sender etc.
    if ($i % 2 === 0) {
    /* //If I comment this part out it works
    $comment = array(
    'DiscussionID' => $DiscussionID,
    'Body' => t('StubCommentBody', "[b]$i" . "[/b]
    This is the $i th message"),
    'Format' => 'Html',
    'InsertUserID' => $SystemUserID,
    'DateInserted' => Gdn_Format::toDate()
    );
    $commentModel = new CommentModel();
    $commentID = $commentModel->save($comment);
    $commentModel->save2($commentID, true);
    */
    }
    }
    //Set LastUIDRead to the last UID read
    $this->setValueInDB('LastUIDRead', imap_uid($mbox, $i));
    $DiscussionModel->UpdateDiscussionCount($CategoryID);
    imap_close($mbox);
    }

  • Okay, apparently as long as I limit it to a single comment getting posted per check it works.

    I've disabled spamcheck ($commentModel->SpamCheck=false;), is there another flood protection in place I've missed?

  • R_JR_J Ex-Fanboy Munich Admin

    You can post source code, if you enclose the code with a 3 times "~" in a single row like that: ~~~. That makes it easier to read your code.

    echo 'example';
    

    You can write to config like that saveToConfig($key, $value); and retrieve it like that $value = c($key);. Look at /library/core/functions.general.php to find some useful functions.

    There is a naming convention for $key. It should be "YourPluginsName.SomeInformativeName". if your plugin is simply called "bot", the config keys name could be e.g. "bot.TimestampLastChecked"

    @Caylus said:
    I've disabled spamcheck ($commentModel->SpamCheck=false;), is there another flood protection in place I've missed?

    That's a great finding! I would have messed with changing the config values temporarily to fool the spam check, but setting that property is way more elegant!

    I do not know anything about spam protection. I've done a quick search for "flood" in the source code but found nothing of interest...

    Would you mind sharing your setValueInDB and getValueFromDB? If you are not yet experienced with Vanilla there might be room for improvement.

  • R_JR_J Ex-Fanboy Munich Admin

    And you should think about making your mail credentials also config settings. That way you would be able to share your plugin without having to fear that you expose your mail accounts credentials.

  • Thanks R_J!

    Figured out the error: php execution time was exceeded >_> sorry for wasting your time on something that had nothing to do with the code! I've introduced a hard limit and now it works perfectly.


    This was the code for my getter and setter by the way, thanks for showing me the correct way:

        function getValueFromDB($name) {
            $result = Gdn::sql()->select('Value')
                    ->from('BotGlobalVar')
                    ->where('Name', $name)
                    ->get();
            $first_row = $result->firstRow();
            if (!$first_row) {
                return false;
            }
            return $first_row->Value;
        }
    
        function setValueInDB($name, $value) {
            if (getValueFromDB($name) === false) {
                Gdn::sql()->insert('BotGlobalVar', array(
                    'Name' => $name,
                    'Value' => $value
                ));
            } else {
                Gdn::sql()->update('BotGlobalVar')->set('Value', $value)->where('Name', $name)->put();
            }
        }
    
  • R_JR_J Ex-Fanboy Munich Admin

    I didn't understood they were your config functions replacements, now I see.

    Although it is not important anymore, here's one note on your setValueInDB: the sql class provides a replace($Table = '', $Set = null, $Where, $CheckExisting = false) {} method which does exactly what you do: check if a key exists, update if yes, insert if not. It just looks cleaner if you use it:

        function setValueInDB($name, $value) {
            Gdn::sql()->replace(
                'BotGlobalVar',
                ['Value' => $value],
                ['Name' => $name]
            );
        }
    
  • rbrahmsonrbrahmson "You may say I'm a dreamer / But I'm not the only one" NY ✭✭✭

    Hi @Caylus, could you post your entire plugin once it's finished?

  • @R_J Thanks! That's going to be useful for other plugins!

    @rbrahmson, sure! It's not really user friendly though, I couldn't figure out how to make a settings page.

    I do get a settings button, but when I click on it I go to http://localhost/vanilla/index.php?p=/settings/bot and get a page not found error (because I can't figure out in which directory to put bot.php).

    class Bot extends Gdn_Plugin {
    
        public function __construct() {
            parent::__construct();
        }
    
        function getParams() {
    
            $DiscussionID = c("Plugin.Bot.DiscussionID", false);
            $SystemUserID = Gdn::userModel()->GetSystemUserID();
            $Now = Gdn_Format::toDateTime();
            $CategoryID = val('CategoryID', CategoryModel::DefaultCategory());
            if (!$DiscussionID) {
    // Insert first discussion
                $DiscussionModel = new DiscussionModel();
                $discussion = array(
                    'Name' => 'Mail thread',
                    'Format' => c('Garden.InputFormatter'),
                    'CategoryID' => $CategoryID,
                    'Body' => 'Here are all the weekly mails',
                    'InsertUserID' => $SystemUserID,
                    'DateInserted' => $Now
                );
                $DiscussionID = $DiscussionModel->save($discussion);
                saveToConfig("Plugin.Bot.DiscussionID", $DiscussionID);
            }
            return array("DiscussionID" => $DiscussionID, "SystemUserID" => $SystemUserID, "Now" => $Now, "CategoryID" => $CategoryID);
        }
    
        function startChecking() {
            if (!$this->needsChecking()) {
                return;
            }
            $DiscussionModel = new DiscussionModel();
            $username = c("Plugin.Bot.Username", false);
            $password = c("Plugin.Bot.Password", false);
            if (!($username && $password)) {
                echo "Help!";
                return;
            }
            $mailbox = '{imap.gmail.com:993/ssl/novalidate-cert}INBOX';
            $mbox = imap_open($mailbox, $username, $password);
            $messageUID = c('Plugin.Bot.LastUIDRead', false);
            $startIndex = $messageUID ? $messageUID : 1;
            $time = time();
            $num_msgs = imap_num_msg($mbox);
            for ($i = $startIndex; $i <= $num_msgs && $i < $startIndex + 10; $i++) {
                if (time() - $time > 10) {
                    break;
                }
                $commentBody = imap_body($mbox, $i);
                $commentHeader = imap_headerinfo($mbox, $i);
                $this->EventArguments['body'] = $commentBody;
                $this->EventArguments['header'] = $commentHeader;
                $this->fireEvent("EmailRead");
            }
            saveToConfig("Plugin.Bot.LastUIDRead", imap_uid($mbox, $i));
            $DiscussionModel->UpdateDiscussionCount($this->getParams()['CategoryID']);
            imap_close($mbox);
        }
    
        function needsChecking() {
            $lastChecked = c('Plugin.Bot.LastChecked', false);
            if ($lastChecked) {
                if ($lastChecked + 3600 < time()) {
                    saveToConfig("Plugin.Bot.LastChecked", time());
                    return true;
                } else {
                    return false;
                }
            }
            SaveToConfig('Plugin.Bot.LastChecked', time());
            return true;
        }
    
        function Bot_EmailRead_handler($sender, $args) {
    //check if the email is from the correct email adress
            if(strpos($sender->EventArguments['header']->fromaddress,c('mailmaster',''))===false)
            {
                   return;
            }
            $params = $this->getParams();
            $commentModel = new CommentModel();
            $commentModel->SpamCheck = false;
            $comment = array(
                'DiscussionID' => $params['DiscussionID'],
                'Body' => t('StubCommentBody', "<b>" . $sender->EventArguments['header']->subject . "</b><br>".$sender->EventArguments['body']),
                'Format' => 'Html',
                'InsertUserID' => $params['SystemUserID'],
                'DateInserted' => $params['Now']
            );
            $commentID = $commentModel->save($comment);
            $commentModel->save2($commentID, true);
            $commentModel->Validation->Results(TRUE);
        }
    
        function Base_Render_After() {
            $this->startChecking();
        }
    
        /**
         *
         */
        public function setup() {
    
        }
    
    }
    
  • vrijvlindervrijvlinder Papillon-Sauvage MVP

    I do get a settings button, but when I click on it I go to http://localhost/vanilla/index.php?p=/settings/bot

    You might need to enable pretty urls

    I couldn't figure out how to make a settings page.

    Create a views folder for your settings page inside your plugin where you will add the forms to input the values or options.

    Look at my Copyright plugin for a simple input form and how to make the settings page. Or even the MagpieFeeds you liked…

  • RiverRiver MVP
    edited August 2016

    I do get a settings button, but when I click on it I go to http://localhost/vanilla/index.php?p=/settings/bot and get a page not found error (because I can't figure out in which directory to put bot.php).

    settingsurl is required

    depending on controller and how you set things up.

    'SettingsUrl' => '/plugin/yourpluginname',

    or

    'SettingsUrl' => '/dashboard/settings/tagging',

    you can add a settings page in views folder, some people like to use a settings.php in the views folder and some like to avoid it. personal preference.

    here is an example without a specific settings view

    https://vanillaforums.org/discussion/25253/simple-setting-screens-with-configurationmodule

    here is a file in views folder. (edit:just noticed after posting above comment has an example for a file in views folder)

    https://github.com/vanilla/addons/blob/master/plugins/Voting/views/settings.php

    https://github.com/vanilla/addons/blob/master/plugins/Voting/class.voting.plugin.php#L20
    https://github.com/vanilla/addons/blob/master/plugins/Voting/class.voting.plugin.php#L46

    Pragmatism is all I have to offer. Avoiding the sidelines and providing centerline pro-tips.

  • Thanks guys! Got it to work!

  • vrijvlindervrijvlinder Papillon-Sauvage MVP
    edited August 2016

    @Caylus said:
    Thanks guys! Got it to work!

    It is customary to post what you did exactly to fix or what was the problem. This way people other than you might get help too :)

    You might even earn the Solution Sayer Badge …

  • RiverRiver MVP
    edited August 2016

    @vrijvlinder said:

    @Caylus said:
    Thanks guys! Got it to work!

    It is customary to post what you did exactly to fix or what was the problem. This way people other than you might get help too :)

    You might even earn the Solution Sayer Badge …

    he could just add the plugin to the plugin folder (if you want to support) or zip it up here and add it as an attachment, then you are not as obliged to support.

    regarding your fix to feeddiscussions...

    under the plugin is even better - helps current users.

    https://vanillaforums.org/addon/feeddiscussions-plugin

    choose ask question. change category to feedback and post your change.

    then people can test and then a moderator or anyone else including you can add it to github.

    Pragmatism is all I have to offer. Avoiding the sidelines and providing centerline pro-tips.

  • vrijvlindervrijvlinder Papillon-Sauvage MVP

    Yea, but his fix was for the Magpie and learning how to make a settings page or not having enabled pretty url as the cause and solution can help give closure to this particular discussion.

  • oh :) thought it was feed discussions plugin.

    Pragmatism is all I have to offer. Avoiding the sidelines and providing centerline pro-tips.

Sign In or Register to comment.