[Demo] Discord Bot: Reddit API Ask Feature

Disclaimer

This post was originally created in 2018 and PRAW/the Discord API work quite differently now. There’s no guarantee that any of this code will work anymore.

The Purpose

Some Discord bots have a feature where you can “chat” with the bot (i.e: Cleverbot integrations). I decided to take a spin on this concept and utilize the Reddit API to answer common questions. A user from the server asks a question using the b!ask command, then the bot gives a random answer from Reddit.

How It Works: A High-Level Overview

The method is simple, but tends to work well. When the bot receives input, take the user’s question and plug it into Reddit’s search function. From those results, choose a random top question/OP, then drill down and return a random top level comment.

What this does is select a relevant question, then returns a top-level comment which should be a direct answer to the question (and not a reply to another comment).

Problems and Challenges

The main problem is potential irrelevant answers. Reddit’s search can be unreliable at times, so an off-topic question post can return an irrelevant answer. Another issue is deleted comments, empty messages, and Reddit bots that aren’t answers such as auto-moderator. I did my best to clean the input, which works for the most part.

One thing I don’t have much control over is the contents of the actual answer. Considering that it’s Reddit.. the bot tends to return sarcastic answers. At the end of the day, that’s what makes this feature so interesting!

First Step: Grabbing the Answer Post

Here’s the code and its following helper functions to help me achieve this.

getCommentPost(arguments, recieved, subreddit) {
    let query = helpers.getSentence(arguments, 0);
    //search askreddit and return the result
    let result = search.media(query, subreddit);
    this.currentTries = 0;

    result.then( res => {
        let post = rhelpers.grabPost(res);

        if(post == undefined) {
            recieved.channel.send('Not sure..');
            return;
        }

        if(post.num_comments) {
            this.getComment(post.id, recieved, arguments, subreddit);
        }
        else {
            //no comments, try another post
            console.log('no comments');
            this.getCommentPost(arguments, recieved, subreddit);
        }
    });
},
/* 
 * from the search.js helpers file.
 * Use the PRAW library to search a subreddit
 * with the passed in argument
 */

media(command, subreddit) {
    return reddit.search({
        query: command,
        subreddit: subreddit,
        sort: 'relevance',
        syntax: 'lucene'
    });
},
/*
 * From the helpers.js file.
 * All this does is take the returned posts object and
 * returns a post with a random index
 */

grabPost(posts) {
    let postIndex = Helpers.generateRandom(posts.length);
    return posts.toJSON()[postIndex]; //choose a post by index
},

How This Bit Works..
When a command gets sent through, parse it, if it’s the b!ask command, run this function. The function grabs the posts based on a Reddit search, then selects a random post with the grabPost helper. If it’s undefined, nothing was returned, so inform the user.

If the post that was found passes the filters, return it back. If not, run the function again until it finds an appropriate post. Once it finds the right post.. we need to drill in and return a valid comment, which will be the final returned answer.

getComment(postID, recieved, arguments, subreddit) {
      //we have the posts ID, so just grab the submission
      reddit.getSubmission(postID)
        .comments
        .then( results => {
            //we can use the same helper to grab a random comment
            let comment = rhelpers.grabPost(results);
            let commentFilt = filters.commentFilter(comment.body);
            let commentLen = comment.body.length > 200 && comment.body.length < 10;

          if(this.currentTries > this.filters) {
              console.log('comment filterings not passed... trying different post');
              this.getCommentPost(arguments, recieved, subreddit);
          }

          if(!commentFilt || commentLen) {
              //not a good response, call again
              this.currentTries++;
              console.log('Comment was removed... trying again');
              this.getComment(postID, recieved);
              return;
          }

          //sanitize then send
          let response = this.sanatizeComment(comment.body);
          recieved.channel.send(response);
      })
  }
//Sanatize comment simply returns the characters
//that render oddly in the Discord chat, including extra spaces
sanatizeComment(comment) {
      let stripped = comment.trim().replace(/\s\s+/g, ' ');

      return stripped.split(' ').filter( word => {
          return !word.includes('&#');
      }).join(' ');
},

How this bit works…
Now that we have the post we want, use PRAW to get the post by ID, then go through each top-level comment. If a filter isn’t passed, call the function again until a comment that passes validation can be returned. Once we get the result, sanitize the comment with a helper function and return it.

Comments