Skip to content

Instantly share code, notes, and snippets.

@aggrolite
Last active January 6, 2022 13:44
Show Gist options
  • Save aggrolite/160d5be23adb9e597553 to your computer and use it in GitHub Desktop.
Save aggrolite/160d5be23adb9e597553 to your computer and use it in GitHub Desktop.
Writing a reddit bot with Go and OAuth2

Writing a reddit bot with Go and OAuth2

snoo

Preface

I'm using the geddit package to authenticate and perform API requests.

Specifically, I'm making heavy use of changes I've proposed here which live on my fork here.

If you'd like to play with my fork, run go get -u github.com/aggrolite/geddit or add my account as a new remote within the geddit repository.

Quick Start

Fetching Submissions

Let's start with an example of using OAuth2 to fetch posts from a specific subreddit. We will print the title, score, and URL of each post matching the domain youtube.com.

You'll need to first create a OAuthSession type so that API requests can be made, which should be done through the NewOAuthSession constructor.

NewOAuthSession is the constructor which takes your app's info, including:

  • Your client ID
  • Your client secret
  • A descriptive user agent string
  • The redirect URL (only used for implicit grant authorization)
package main

import (
    "fmt"
    "log"

    "github.com/jzelinskie/geddit"
)

func main() {
    o, err := geddit.NewOAuthSession(
        "client_id",
        "client_secret",
        "Testing Geddit Bot with OAuth v0.1 by u/aggrolite - see source @ github.com/aggrolite/geddit/master",
        "",
    )
    if err != nil {
        log.Fatal(err)
    }

    // Login using our personal reddit account.
    err = o.LoginAuth("my_username", "my_password")
    if err != nil {
        log.Fatal(err)
    }

    // We can pass options to the query if desired (blank for now).
    opts := geddit.ListingOptions{}

    // Fetch posts from r/videos, sorted by Hot.
    posts, err := o.SubredditSubmissions("videos", geddit.HotSubmissions, opts)
    if err != nil {
        log.Fatal(err)
    }

    // Print the title and URL of each post that has a youtube.com domain.
    for _, p := range posts {
        if p.Domain == "youtube.com" {
            fmt.Printf("%s (%d) - %s\n", p.Title, p.Score, p.URL)
        }
    }
}

One important thing to notice is that we've created and set a new OAuth token using o.AuthLogin().

Without this function call, our API requests will not work.

Saving submissions

Now let's take it a step further and save each post linking to youtube.com.

Simply adjust the loop:

// Save each post linking to youtube.com.
for _, p := range posts {
    if p.Domain == "youtube.com" {
        // Save under category name "videos".
        err = o.Save(p, "videos")
        if err != nil {
            // Log any error, but keep going.
            log.Printf("Error! Problem saving submission: %v", err)
        }
    }
}

To verify the saves were applied, take a look at your account's upvote tab at reddit.com/user/username/saved.

...Or use the API wrapper:

// Fetch Submission types upvoted by our account.
l, err := o.SavedLinks("my_username", geddit.ListingOptions{})
if err != nil {
    log.Fatal(err)
}
for _, s := range l {
    fmt.Printf("%s - %s\n", s.Subreddit, s.URL)
}

The output should look something like this:

videos - https://www.youtube.com/watch?v=s8QY48bTcVs
videos - https://www.youtube.com/watch?v=LmIrtGIPfKk
videos - https://www.youtube.com/watch?v=6blOgs6r7MM
videos - https://www.youtube.com/watch?v=BFe8TBhLeQQ&feature
videos - https://www.youtube.com/watch?v=ri9-5-YT-Ag
videos - https://www.youtube.com/watch?v=T5BdemwSq0g
videos - https://www.youtube.com/watch?v=AloYcxp6XAk
videos - https://www.youtube.com/watch?v=O5bTbVbe4e4
videos - https://www.youtube.com/watch?v=1B6oiLNyKKI
videos - https://www.youtube.com/watch?v=yJO5lj8nRjU
videos - https://www.youtube.com/watch?v=4PN5JJDh78I
videos - https://www.youtube.com/watch?v=KjkzHGRR2LQ
videos - https://www.youtube.com/watch?v=F-ZskaqBshs
...

Crossposting submissions

Crossposting, or X-Posting, is where reddit users post content on a subreddit that originated from another subreddit.

What if we want our bot to do X-Posting for us?

For the sake of this walk-through, let's imagine we've created the subreddit r/dogsandcats. While r/dogpictures caters to dog lovers, and r/cats caters to cat lover, our new subreddit accepts both animals.

So it might make sense to monitor new posts from similar subreddits and copy them over to r/dogsandcats -- at least until we attract new users.

To complete this task, we might take the following steps:

  1. Fetch recent submissions from r/dogpictures
  2. Fetch recent submissions from r/cats
  3. Copy submissions to r/dogsandcats

However, we should be careful not to submit links too fast. It may be best to make use of geddit's client-side throttle feature to avoid any interruption.

It may also be nice to alter the title of each submission, revealing where the link was originally found. Reddit users typically include some bit at the beginning or end of the post's title, such as [xpost from r/cats].

Maybe our task now looks something like this:

  1. Fetch recent submissions from r/dogpictures
  2. Fetch recent submissions from r/cats
  3. Detect subreddit or each submission, and alter title text
  4. Copy submissions to r/dogsandcats with throttle enabled.
  5. Repeat steps 1-4 as needed.

There are other things to keep in mind, like duplicate posts, but this should be enough to get started.

Here's some code:

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/jzelinskie/geddit"
)

func main() {
    o, err := geddit.NewOAuthSession(
        "client_id",
        "client_secret",
        "Testing OAuth Bot by u/aggrolite v0.1 see source https://github.com/aggrolite/geddit",
        "",
    )
    if err != nil {
        log.Fatal(err)
    }

    // Login to perform API requests
    err = o.LoginAuth("my_username", "my_password")
    if err != nil {
        log.Fatal(err)
    }

    // No options for now
    opts := geddit.ListingOptions{}

    // Fetch dog pictures
    dogs, err := o.SubredditSubmissions("dogpictures", geddit.NewSubmissions, opts)
    if err != nil {
        log.Fatal(err)
    }

    // Fetch cat pictures
    cats, err := o.SubredditSubmissions("cats", geddit.NewSubmissions, opts)
    if err != nil {
        log.Fatal(err)
    }

    // Throttle link submissions to avoid API rate limit error
    o.Throttle(10 * time.Minute)

    // Make a slice for both types of submissions
    dogsAndCats := make([]*geddit.Submission, len(dogs)+len(cats))

    // Populate our new slice
    dogsAndCats = append(dogs, cats...)

    // Create xpost tags and submit to r/dogsandcats
    for _, p := range dogsAndCats {
        // Skip text posts
        if p.IsSelf {
            continue
        }
        title := fmt.Sprintf("%s [xpost from r/%s]", p.Title, p.Subreddit)

        // Since these links are xposts, we must mark Resubmit as true
        xpost := &geddit.NewSubmission{
            Subreddit:   "dogsandcats",
            Title:       title,
            Content:     p.URL,
            Self:        false,
            SendReplies: false,
            Resubmit:    true,
        }
        s, err := o.Submit(xpost)
        if err != nil {
            log.Printf("Problem submitting post %d: %v\n", p.ID, err)
        }
        fmt.Printf("Created new submission at %s\n", s.URL)
    }
}

Implicit Grant Authorization

So far the code examples have used confidential authorization, meaning our own account's login details.

But what if we want to authenticate on behalf of another reddit user and perform tasks through their account?

With two functions, we can do just that:

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/jzelinskie/geddit"
)

func main() {
    o, err := geddit.NewOAuthSession(
        "client_id",
        "client_secret",
        "Testing OAuth Bot by u/aggrolite v0.1 see source https://github.com/aggrolite/geddit",
        "http://localhost:9090",
    )
    if err != nil {
        log.Fatal(err)
    }
    
    // Define scopes needed
    scopes := []string{"read", "identity"}
    
    // Get authorization URL for user to visit
    url := o.AuthCodeURL("random string", scopes)
    
    fmt.Printf("Please visit the following URL and copy the code below: %s\n", url)
    
    var code string
    fmt.Scanln(&code)
    
    // Create and set token using the authorization code
    err = o.CodeAuth(code)
    if err != nil {
        log.Fatal(err)
    }
    
    // Ready to perform API calls!
}

Notice that we've defined a state string. In a real world application, state should be verified by the webserver handling the redirect URL.

See Also

I'm hoping to expand this gist as I have time to contribute to the geddit codebase. If you'd like more information, feel free to email me at [email protected]. I'm not active on Twitter, but you can bug me at @aquahats too.

Here's some related reading material and resources:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment