avatarTalk to Reddit with Go ⛳

March 26th, 2020

I like Go and I think it’s a great language. I built memeinvestor_bot, which was one of the interactive reddit bots on the platform at the time. So you can imagine we used quite a bit of praw. About year and a half later, the python codebase became so disgusting and unmaintainable, where I decided to re-write the whole thing in Go. (The old "let’s rewrite everything" syndrome") Re-write would be too strong, re-build from scratch is what the goal was.

Surely, after a couple of seconds of DuckDuckGoing, I stumbled upon graw, one of the most popular Go Reddit Api Wrappers. I remember that I was just learning Go at a time and really just wanted to do something with it. Graw’s deal with announcer and other stuff confused me a bit. So I did what every software engineer does when it itches the wrong way. Make a new library!

I present to you, mira! It’s a really playful and straight-forward library. Mira aggressively uses the dot-notation (gorm style) and has the simplicity of praw. Golang’s great features as goroutines are used to implement the streaming functionality. Let’s dive straight into it!

Installing

mira can be installed like any other go package, just run

go get -u github.com/thecsw/mira

Introduction

mira requires you to authenticate via reddit by supplying reddit tokens. Other methods of authing should be implemented. Maybe you are the chosen one?

For authentication, take a look at mira’s readme, we will get to the cream of the crops and actual features.

Basic reddit

You can post submissions and submit/reply/edit/delete comments. You can also grab N number of submissions from any subreddit, paginate through them, and other exciting stuff! Below is some basic functionality example.

package main

import (
    "fmt"

    "github.com/thecsw/mira"
)

// Error checking is omitted for brevity
func main() {
    // Start mira
    r, err := mira.Init(mira.ReadCredsFromFile("login.conf"))
    // Make a submission
    post, err := r.Subreddit("mysubreddit").Submit("mytitle", "mytext")
    // Comment on our new submission
    comment, err := r.Submission(post.GetId()).Save("mycomment")
    // Reply to our own comment
    reply, err := r.Comment(comment.GetId()).Reply("myreply")
    // Delete the reply
    r.Comment(reply.GetId()).Delete()
    // Edit the first comment
    newComment, err := r.Comment(comment.GetId()).Edit("myedit")
    // Show the comment's body
    fmt.Println(newComment.GetBody())
}

Streaming

Streaming Reddit data is probably the most exciting part of using mira and usually the top reason of using any Reddit API wrappers. When you want to run a stream of submissions/comments/replies, mira will give you a channel where it will get populated as new streaming data becomes available.

Let’s take a quick example of how we can build a reminder bot from graw guide. This code is originally from the graw guide, the only diffence is that it’s written to use mira instead of graw. Example done to show how those two libraries differ.

package main

import (
    "strings"
    "time"
    "github.com/thecsw/mira"
)

func main() {
    r, err := mira.Init(mira.ReadCredsFromFile("login.conf"))
    if err != nil {
        panic(err)
    }
    c, err := r.Subreddit("bottesting").StreamSubmissions()
    if err != nil {
        panic(err)
    }
    for {
        post := <-c
        if !strings.Contains(post.GetBody(), "remind me of this bot") {
            continue
        }
        go func() {
            <-time.After(10 * time.Second)
            err = r.Redditor(post.GetAuthor()).Compose(
                fmt.Sprintf("Reminder: %s", post.GetTitle()),
                "You've been reminded!",
            )
            // check the error if you wish
        }()
    }
}
on concurrency

mira is concurrency safe! When you run r.Comment(...) or r.Redditor(...), it adds those values to its internal channel-based queue and dequeues them when requested. If two threads or routines try to dequeue at the same time or expect something, then the data can get mingled up a bit. Only call methods in a FIFO (First-In-First-Out) style. If comment was the first to be added, then a comment-related function must consume it.

Similar API is available for other Reddit entities and objects. For example, mira currently supports:

The names are very Java like and I hope they are intuitive

Extending mira

The library only supports around 15 endpoints. Reddit has well over 50-60. Mira exposes its caller Reddit.MiraRequest(httpMethod, endpoint, payload) and http request handler, so you can build your own mira callers and work with them!

Here is an example of how r.Comment(...).Reply(subject, text) is implemented:

NOTE
you can lookup checkType(...) in mira’s readme
func (c *Reddit) Reply(text string) (models.CommentWrap, error) {
    ret := &models.CommentWrap{}
    // Second return is type, which is "comment"
    name, _, err := c.checkType("comment")
    if err != nil {
        return *ret, err
    }
    target := RedditOauth + "/api/comment"
    ans, err := c.MiraRequest("POST", target, map[string]string{
        "text":     text,
        "thing_id": name,
        "api_type": "json",
    })
    json.Unmarshal(ans, ret)
    return *ret, err
}