Botfriend is a Python library that runs on a server. If you're not
comfortable with setting up a cron job, or writing Python code, I
recommend you instead check out Cheap Bots, Done
Quick or Cheap Bots, Toot
Sweet!, as a simpler way to express
your creativity.

The Story

I wrote Botfriend to manage about thirty different Twitter
bots that I created. I found myself
constantly copying and pasting, writing the same code over and
over. Every bot does something different, but they all have certain
basic needs: connecting to various services and APIs, deciding when to
post something, managing backlogs of content, and so on. There's no
reason each bot needs its own version of this code. The only part of a
bot that needs new code is the creative bit.

My other big problem was, I've come to dislike Twitter. It's a great
platform for creative bots, but every time I created a bot, I felt
guilty about increasing the value of an platform I think is making the
world worse. (Also, Twitter has started suspending my bots for no reason.)

I didn't want to give up my botmaking hobby, so I started
investigating the world of Mastodon
bots. This created another problem: it's a big pain to rewrite thirty
bots to post to a different service. If I was going to do that much
work, I wanted the end product to be a reusable library that could
save everyone time.

So I went through my thirty bots, rewrote everything, and moved all of
the reusable code into Botfriend. Now my bots are a lot smaller and
easier to manage. All of the tedious code is in one place, and I can
focus on the fun part of bot-writing.

Consider a bot like A Dull Bot
(source). Creating
this bot was a fair amount of work. But all of the work went into the
fun part: creating an accurate software model of the typewriter from
The Shining. There's no code for making sure the bot posts once an
hour, or for pushing the typewritten text through the Twitter or
Mastodon APIs. Botfriend takes care of all that stuff.

If you want to save code on your own bot projects, port your bots from
Twitter to Mastodon, or just expand the reach of your bots, I hope
you'll consider Botfriend.

Setup

I recommend you run Botfriend in a Python virtual environment. Here's
how to create a virtual environment called env and install Botfriend
into it.

$ virtualenv env
$ source env/bin/activate
$ pip install botfriend

You'll interact with Botfriend exclusively through command-line
scripts. From this point on I'll be giving lots of example command
lines. All of my examples assume you've entered the Botfriend virtual
environment by running this command beforehand:

$ source env/bin/activate

A simple example: Number Jokes

By default, Botfriend expects you to put the source code for the bots
in a directory bots/, located in the same directory as your virtual
environment. So if your virtual environment is located in
/home/myusername/botfriend/env, Botfriend will expect your bots to
live underneath /home/myusername/botfriend/bots.

The Botfriend database itself will be stored in the bot directory as
botfriend.sqlite.

If you want to store your Botfriend data somewhere other than bots/,
every Botfriend script takes a --config argument that points to your
bot directory. But most of the time, bots/ is fine.

Each individual bot will live in a subdirectory of your bot directory,
named after the bot. Let's get started with a simple example, called
number-jokes.

$ mkdir bots
$ mkdir bots/number-jokes

Each bot needs to contain two special files: __init__.py for source
code and bot.yaml for configuration.

__init__.py: Coming up with the joke

Imagine walking up to to a comedian and saying "Tell me a joke!" A
human comedian probably won't appreciate it, but this is what bots
live for. For a Botfriend bot, __init__.py is where the comedian
comes up with their jokes.

To get started, we'll make a simple bot that makes up observational
humor about numbers.

To get started, open up bots/number-jokes/__init__.py in a text
editor and write this in there:

Botfriend provides a lot of utilities to help you write a good
__init__.py, but there's only one hard-and-fast rule: by the end of
the file, you have to have a class called Bot. The Botfriend scripts
are going to load your Bot class, instantiate it, and use it to
do... whatever the bot does.

Some bots do a lot of work to come up with a single "joke". They might
draw pictures, do database queries, make API calls, all sorts of
complicated things. As befits an example, NumberJokes here does
almost no work. It just picks a random number and puts it into a string.

bot.yaml: Telling the joke

Like most comedians, bots are constantly coming up with jokes. But if
no one ever hears the joke, what's the point? The bot.yaml file
explains how a Botfriend bot should tell its jokes to the public.

The basic scripts

botfriend.post

Now look at the file you configured in bot.yaml. You told Number
Jokes to post its jokes to bots/number-jokes/number-jokes.txt. That file
didn't exist before you ran botfriend.post, but now it does exist,
and it's got a joke in it:

Hilarious, right? You'll be running this script a lot, probably as
part of an automated process. On my site I run botfriend.post every
five minutes. (I show an example of how to do this at the end of this
document.)

If your bot isn't scheduled to tell a joke, botfriend.post will do
nothing. Run it again now -- nothing will happen.

$ botfriend.post

Number Jokes told a joke the first time you ran it, and (as you told
it in bot.yaml) it's only supposed to tell one joke an hour. So, no
new joke.

If you were to wait an hour and run botfriend.post again, you'd get
another joke. But don't wait -- keep going through this tutorial!

Running a script on just one bot

By specifying a directory name on the command line, you can make
botfriend.post (and most other Botfriend scripts) operate on just
one bot, not all of your bots. Right now, it doesn't make a
difference, because you only have one bot, but here's how to do it:

$ botfriend.post number-jokes

Forcing a bot to post

You can use --force to make a bot tell a joke even if its schedule
wouldn't normally allow it.

botfriend.bots

If you have a lot of bots, it can be annoying to remember all their
names. The botfriend.bots script just lists all the bots known to
Botfriend.

$ botfriend.bots
# number-jokes

Still only one bot so far.

botfriend.test.stress

It's difficult to test a bot that does random things. You might have a
bug that makes the bot crash only one time in a thousand. Or your bot
might never crash, but sometimes take a long time to run.

This is why we have the botfriend.test.stress script, which asks a
bot to come up with ten thousand jokes in a row. The jokes aren't
published anywhere; the goal is just to give a good test of all the
possible cases that might happen inside your bot.

Since Number Jokes is really simple, it can generate ten thousand jokes
with no problem, although some of them are repeats:

If you've got a complicated bot, it can be a good idea to run
botfriend.test.stress on it a couple of times before using it for real.

How to publish

There are a few more interesting features of Botfriend, but let's take
a minute to talk about the boring features. It's easy to make a bot
write its posts to a file, but nobody's going to see that. What you
really need is to get some Twitter or Mastodon credentials. (Specific
instructions are below.)

Once you have some credentials, open up your bot's bot.yaml file,
add your credentials to the publish configuration setting. This will
give your bot additional ways to publish its posts.

Here's an example. This is what the configuration for Number Jokes
would look like if it had Twitter and Mastodon connections set up, in
addition to writing everything to a file.

(These credentials won't work -- I made them up to resemble real
Twitter and Mastodon credentials.)

Publish to a file

This is the simplest publication technique, and it's really only good
for testing and for keeping a log. The file publisher takes one
configuration setting: filename, the name of the file to write to.

publish:
file:
filename: "anniversary.txt"

Publish to Twitter

To get your bot on Twitter, you need to create a Twitter account for
the bot, log in as the bot, and then get four different values:
consumer_key, consumer_secret, access_token and
access_token_secret. These four values, when inserted into
bot.yaml, give you the ability to post to a specific Twitter account
using the Twitter API.

Once you have these four values, put them into bot.yaml, and your bot
will be able to post to its Twitter account.

Publish to a Mastodon instance

To connect your bot to Mastodon, you create a Mastodon account for the
bot, log in as the bot, and then get four values.

First, api_base_url-- this is easy, it's just the URL to the
Mastodon instance you used to create the account. I like to use
botsin.space, a Mastodon instance created
especially for bots.

Then you need to get client_id, client_secret, and access_token.
You can get these values by logging in as your bot, going to the
Settings page, clicking "Development", and creating a new
application. (If that's not working for you, try Darius Kazemi's
instructions.)

Once you have these four values, put them into bot.yaml, and your bot
will be able to post to its Mastodon account.

Okay, now back to the cool bots you can write with Botfriend.

botfriend.test.publisher: test your publishing credentials

The botfriend.test.publisher script tries out all of your bots'
publishing credentials to make sure they work. If a bot is having
trouble posting, the problem will show up here.

For every bot with a publishing technique that works, you'll get a
line that starts with GOOD. For every publishing technique that's
broken, you'll get a line that starts with FAIL.

In this example, writing to a file works fine, but since the Twitter
and Mastodon credentials are made up, Twitter and Mastodon won't
actually accept them.

Bots that keep a backlog: Boat Names

Some comedians can come up with original jokes on the fly, over and
over again. Others keep a Private Joke File: a list of jokes assembled
ahead of time which they can dip into as necessary.

Instead of writing a bunch of generator code in a Botfriend bot, you
can generate a backlog of posts, however you like. It's easy to
create a bot that simply posts items from its backlog, in order, one
at a time.

If your style is more writing than programming, you can just write the
backlog in a text editor. This way you can create a Botfriend bot
without writing any code at all.

Let's make a simple backlog bot that posts interesting names for
boats, taken from the website Ten Thousand Boat
Names. (What I'm about to describe is
exactly how my real bot Boat Names
works.)

First, make a directory for the bot:

$ mkdir bots/boat-names

This bot is so simple that you don't need any code to program its
behavior. Just create an empty __init__.py file, so that Botfriend
knows this is a bot and not some random directory.

$ touch bots/boat-names/__init__.py

Just like Number Jokes, Boat Names needs a bot.yaml to tell it where
to post and how often. Create bots/boat-names/bot.yaml and put this
text in there:

The schedule here is a little different than in the first example
bot. The first example's posts will come exactly one hour apart. This
bot posts every eight hours (480 minutes), on average, but there is
some random variation--usually up to fifteen or thirty minutes in
either direction.

Now, running botfriend.bots will list both of your bots.

$ botfriend.bots
# boat-names
# number-jokes

Running botfriend.post will tell both bots to post something if they
want, but Boat Names can never post anything, because it has no
backlog and no logic for generating new posts. It can't come up with
its own jokes -- you have to help it.

botfriend.backlog.load

The botfriend.backlog.load script lets you add items to a bot's
backlog from a file. The simplest way to do this is with a text file
containing one post per line.

Let's create a backlog file. This can go anywhere, but I
recommend keeping it in the same directory as the rest of the bot, in
case something goes wrong and you need to recreate it. Open up a file
bots/boat-names/backlog.txt and put a few boat names in it:

Bots that keep state: Web Words

Sometimes a bot needs to do something that takes a long time, or
something that might be annoying if it happened
frequently. Botfriend allows this difficult or annoying thing to be
done rarely. The results are stored in the bot's state for later
reference.

Let's create one more example bot. This one's called "Web Words". Its
job is to download random web pages and pick random phrases from them.

$ mkdir bots/web-words

We're going to split the "download a random web page" part of the bot
from the "pick a random phrase" part. The "pick a random phrase" part
will run every time the bot is asked to post something. The "download
a random web page" part will only run once a day, because it involves
making a bunch of HTTP requests to random domain names. But once you
have a web page downloaded, it's quick and easy to pull a random chunk
out of it.

The first time you tell Botfriend to post something for this bot,
Botfriend will call the update_state() method. This method may try
several times to find a web page it can use, but it will eventually
succeed.

$ botfriend.post web-words
Web Words | Trying to get new state from http://www.stenographical.com/
Web Words | That was too small, trying again.
Web Words | Trying to get new state from http://www.bronchologic.org/
Web Words | That didn't work, trying again.
Web Words | Trying to get new state from http://www.dentonomy.org/
Web Words | That was too small, trying again.
Web Words | Trying to get new state from http://www.crummy.com/
Web Words | Success!
Web Words | file | Published 2019-01-10 01:45 | e experimental group

Once the state is in place, running botfriend.post again won't
download a whole new web page every time. Instead, Web Words will
choose another random string from the webpage it's already downloaded.

This bot's state expires in one day (this was set in its
bot.yaml). 24 hours after update_state() is called for the first
time, running botfriend.post will cause Botfriend to call that
method again. A brand new web page will be downloaded, and for the
next 24 hours all of the Web Words posts will come from that new web page.

botfriend.state.show - Showing the state

This script simply prints out a bot's current state.

$ botfriend.state.show web-words

botfriend.state.refresh - Refreshing the state

You can use this script to forcibly refresh a bot's state by calling
update_state(), even if the bot's configured state_update_schedule
says it's not time to call that method yet.

$ botfriend.state.refresh web-words
# Web Words | Trying to get new state from http://www.choristoblastoma.org/
# ...

botfriend.state.set - Setting the state to a specific value

You can use this script to set a bot's state to a specific value,
rather than setting the state by calling the update_state()
method. Here, instead of telling Web Words to pick random strings from
a web page, we're telling it to pick random strings from its own
source code.

botfriend.state.clear - Clearing the state

This script will completely erase a bot's script, making it as though
update_state() has never been called.

$ bin/state.clear web-words

More examples

The the botfriend source
repository includes complete
source code for about ten bots, including the three covered in this
document and several actual bots that I run. To try them out, check
out the repository and copy the contents of its bots.sample
directory into your bots directory.

Web Words - The third example bot described in this help document. A bot that keeps randomly selected web pages as state.

Posting on a regular basis

Once you have a few bots, you'll need to run the botfriend.post script
regularly to keep new content flowing. The best way to do this is to
set up a cron job to schedule the botfriend.post script to run every few
minutes. Don't worry about posting too often. Bots that need to post
something will post when they're ready. Bots that don't need to post
anything right when botfriend.post is run, will be quiet, and bide their time.

Sophisticated configuration

The name option should be self-explanatory -- it's the human-readable name of
the bot. Now let's take a detailed look at the other two options.

schedule

The schedule configuration option controls how often your bot should
post. There are basically three strategies.

Set schedule to a number of minutes. (This is what Number Jokes
does.) Your bot will post at exact
intervals, with that number of minutes between posts.

Give schedule a mean number of minutes. Your bot will post at
randomly determined intervals, with approximately that number of
minutes between posts, but with a fair amount of random variation.

To fine-tune the randomness, you can specify a stdev to go along
with the mean. This sets the standard deviation used when calculating
when the next post should be. Set it to a low number, and posts will
nearly mean minutes apart. Set it to a high number, and the posting
schedule will vary widely.

You can omit schedule if your bot schedules all of its posts ahead
of time (like Frances
Daily
does).

state_update_schedule

There's a related option, state_update_schedule, which you only need
to set if your bot keeps internal state, like Web Words does. This
option works the same way as schedule, but instead of controlling
how often the bot should post, it controls how often your
update_state() method is called.

Other configuration settings

Certain types of bots have other specific configuration settings. A
subclass of RetweetBot, like Best of
RHP,
will use a special configuration setting called retweet-user. This
controls which Twitter account the bot retweets. Podcast Roulette has a number of configuration settings like description that are copied into the RSS feed.

Your bot can define its own custom configuration options--the
configuration object is parsed as YAML and passed into the Bot
constructor as the config argument. You can look in there and pick
out whatever information you want.

Defaults

If you put a file called default.yaml in your Botfriend directory
(next to botfriend.sqlite), all of your bots will inherit the values
in that file.

Almost all my bots use the same Mastodon and Twitter client keys (but
different application keys), and all my Mastodon bots are hosted at
botsin.space. I keep these configuration settings in default.yaml so
I don't have to repeat them in every single bot.yaml file. My
default.yaml looks like this:

Programmatic access to an API

Sometimes you'll need to use a site's API for more than just posting
to the site. Every bot has a number of publishers configured through
its publish settings, and the corresponding Publisher objects are available from inside a Botasself.publishers. Once you have a Publisherobject, the raw API client will always be available asPublisher.api`.

See the IAmABot
constructor
for an example. This bot needs a Twitter API client to get its data,
so it looks through self.publishers until it finds the Twitter
publisher, and grabs its .api, storing it for later.

Conclusion

There are a lot of features of Botfriend that I've barely touched or
not mentioned at all: bots that retweet other Twitter accounts, bots
that get their posts by scraping a web page for their content, scripts
for republishing posts that weren't posted properly the first time.

But the features I've covered are the main ones you need to get
started and to see the power of Botfriend. I hope you enjoy it!