Subtitles section Play video
(ding) - Hello.
In this video, I am going to make a bot
and I am going to make an image bot
and I'm very excited about this project
because it pulls together a whole lot of different things.
I am going to use processing to generate an image.
I am going to use node to call processing
to generate the image and I'm going to use node
to talk to an API and the API is Mastodon.
So one thing you might want to do
is I'll refer you to all of my,
a tutorial series about the basics of making
a Mastodon bot, I will start largely from scratch here,
but I am going, I've already gotten my API keys
and I already have imported and installed
this node package called mastodon.api
and this .env package that loads my API keys for me.
So that is stuff that I've done in another video.
If you want to see that, I'll link to that
in the video description but I'm just going to get started.
So what is the first thing that I want to do?
Okay, actually, let's go to, so this is my inspiration.
Tree bot, made by Alix Novosi for National Bot Making Month
and I am going to try to recreate this.
My trees won't be nearly as nice.
So the first thing I want to do is I want to use processing.
I don't have to, I could find some node package
that does drawing or generates an image,
svg, whatever, but I love processing, it's my happy place
and I'm pretty sure I could get processing
to generate a tree, in fact, I don't even have to write
the code for this because I have before.
I'm just going to go to examples, right,
isn't it in here somewhere?
Topics, simulate,
no, no, no?
Okay, I'll be back in a second.
Oh yeah, I'm back, I found it under fractals
and L-systems tree.
Well look, recursive tree by Daniel Shiffman.
This is my, this is a recursive tree
and as I move the mouse, it changes the angle.
So let's alter this code a little bit.
Let's say that, we don't care about a frame rate.
And let's not, let's have the angle be random
between 0 and two pi.
Oh and it's converting it to radians,
so okay, between 0 and 360.
Whoa and it's doing it over and over again in draw,
that's exciting, I'm going to say no loop
so it's done each time I run this.
I get a random tree.
There's so much more that I could do to the design
of the tree and I'm so tempted to recode the tree
but I'm going to just leave it as is, ya know maybe I want
to not have so many large angles and there we go,
beautiful tree, okay.
So now, now that I've done that, what do I want?
I want my processing sketch to also save
and I'm just going to call it tree.png.
So when it's done drawing, it's going to save an image.
I've got my code here.
This is the code that will talk to the Mastodon service
and what I want is for this code to actually run
a processing sketch but this is node, this is JavaScript.
Processing is java and it's a desktop environment
where I hit the play button, how can I possibly do this?
Well the first thing I'm going to do is actually go
and save this processing sketch in where my bot is
and I'm actually working in this folder,
Mastodon bot 3, you can see node modules, bot.js,
so I'm going to save it there and I'm going to call it treegen.
So I'm going to save it treegen, I'm going to make sure
it still runs, it still runs.
And now, what I'm going to do,
I'm going to show you a little trick.
Now wouldn't it be nice if I could just execute
a command like processing run.
Of course, in terminal, in the shell,
it's not going to know what that is
but a little known fact about processing,
which I have done this in other videos before
but just to start anew again, processing command line.
There is actually a way to run a processing sketch
command line by saying processing-java,
the path to the sketch and then --run
but the only way you can do that is by installing first
processing.java to your system so this is installing
a command line command to run a processing sketch.
So I'm going to do this, install processing java,
I'm going to say yes for all users.
It's going to want to use my password.
So I'm going to enter my password 'cause it needs
to be able to put that where it needs to go
and now I can actually say processing-java
and you can see, I get all sorts of stuff.
It doesn't know what I want to do but watch this,
I can say, let me just look in here,
there is the treegen processing sketch.
So I can say processing-java, sketch=,
is that what it was?
Let's go look at the, sketch=, now I need the full path
so the full path and I'm going to show you a way around this
in a second, is this, treegen, right,
this is the full path to that processing sketch.
Then I can say --run, here we go.
Magic.
It ran it, look, there's the processing sketch.
This is really cool, now watch.
What's exciting about this and it's bothering me
that the background isn't 51 which is very important.
I can actually quit processing completely.
I don't need to have processing open and still do this.
Look at that, there it is.
There's my tree, ah, but I actually do want
to have processing open 'cause I've got
a little bit of a problem here and we can see here,
look, that image is there, it's now saved.
What I want to do is go back and open this again,
open, open, open, open, open and I want to add one thing.
I want it to go away so I actually want to say
after I save it, exit, and I'm also,
this is going to be helpful for me later,
I want to say println("Tree generated");
We'll see where this comes up later.
Okay, now, quit processing.
And now I'm going to run this.
See it, there it is, oh, finished, tree generated, finished.
One more time.
There it is, oh, finished.
Okay, this is really exciting
'cause there's so much stuff you could do.
Now, here's the thing.
I am executing this command via the shell
but I want node to execute it.
How can I execute it in node and it turns out
there is a way to execute any generic shell command
from node and it is with the child process package.
So if I go to child process, you'll see here
that there is a method called exec.
Child_process.exec spawns a shell
and runs a command within that shell.
Truth of the matter is it might useful to use
this execSync because I'm going to want to,
sync means synchronous, meaning wait until it's done
to go on to the next thing but I've got a crazy plan here.
I want to do asynchronous stuff with es8,
this await and async function,
you may or may not have heard about.
So I'm going to use this one, exec.
So what I need to do is in my node code,
I need to say const exec = require('')
and where was this, it is in child process.exec,
child_process, no there's no, yeah, that's right, huh,
oh .exec, yes I'm confused.
So this is me requiring the child process package
and I don't have to mpm install this.
You'll notice that I'm on the node.js documentation page.
I'm not in some separate third party mpm package,
this is built into node, but, just like file system is.
But I've got to say that I want to use it.
So require child process and all I want
is that exec function.
So now, there's no reason why I can't just say
exec and then pass in what?
Exactly this.
Oh let me make, this is so unwieldy.
So a nice little trick that I can do
is I can actually always get the current path.
If I want to get the current, I just type pwd,
print working directory.
Well guess what?
I can actually have pwd executed within this command
by using these back ticks.
I'm pretty sure this is how I do it.
So now I'm saying run and I think I don't actually even need
this first slash, right, run sketch =
print working directory, then treegen run.
Let's see if this works.
It is done, okay.
So now, so great so I can grab this, this is my command.
We're going to get to the Mastodon stuff in a second.
Uh (clicking tongue).
Constant command = let's just put this
in a variable like this.
Then I could say exec command and then that's going to have
a callback, oh, I don't want the,
let me just run this, let's just run this,
let's see what happens.
Node bot.js.
Hey, look what happened, it made it happen.
Now here's the thing, the next thing I was going to do
was add a callback but I'm a new,
I'm turning over a new leaf, I'm turning a new page
in the book of JavaScript and I'm a kind of person
who uses promises and I'm not only a kind of person
uses promises, I even use the async and await keyword
to really make my life full of just (sighs) ease.
Eh, it's not full of ease but I'm doing my best.
So what does that mean?
The thing is, this particular node package,
child process.exec, and I have a feeling if I bother
to look at the chat (laughs) which I'm going to open up
for a second, someone's going to tell me I could just use
something else now that natively supports promises.
Oh I'm being told that node.js has underscore
directory names so there are other ways,
people are telling me other ways I could get
the directory name but what I'm going to do
is I'm going to promisify, which is a word apparently,
promisify, I'm going to, oh look at this,
I must have googled this another time,
unless it's just very common, by using java,
not java, sorry, the node package util.
So node package util, if I go look at util
and I actually just want to be here,
there is a promisify,
util.promisify.
So what I can actually do is I can say
const util = require('util') and then I can say
util.promisify, what a weird word,
what happens if I promisify myself, uh, this.
So now, this require child process exec function
no longer uses a callback, now it uses a promise
and what does that mean?
That means I can say .then and whatever
the result, the response is.
I can console log that response.
And then I can also catch any error
and I console.error that error.
Now, this might be and there's no semicolon there,
there's a semicolon there.
This might look completely insane to you
if you haven't seen promises used before in JavaScript.
It's very similar to a callback but instead of saying
a callback, I basically have this callback
that happens in .then.
I'm also using the arrow syntax.
The arrow syntax is a nice way of sort of shorthanding this.
I'm getting the response as the argument to the callback
and I'm console logging it.
So if you want to know about those things,
I have a whole playlist about promises
and a video about the arrow function
that you could go and look at but this is the basic idea.
This is a little bit of an advanced video here.
Not advanced but I'm using kind of modern JavaScript stuff
if you consider three years ago modern.
Okay, so now I've got this exec function
so let's actually run this one more time.
And see, it should do exactly the same thing
but look at this, that response has standard out
standard error so in other words, standard out it what?
That's the thing that, I closed my processing sketch,
I guess I should have left it open.
The processing sketch has a print line in it
so I can read whatever that print line is
so I could actually get more information
from processing if I want.
For example, I could get the angle.
Actually this is great, let's add a little feature to this,
this will be fun 'cause why not make this video
longer than it already is?
Where am I, desktop, desktop, Mastodon, Mastodon bot 3,
treegen, so what I'm going to do here,
look at this, this is great.
Let's leave this open because maybe I'm going to want to
do more stuff with it.
I am going to, let's make the angle between 0 and 90,
that's what it says in the comments
and then I'm also going to say print line
and I'm just going to say angle or was it angle?
Theta, whatever, A, I'll just keep the A
and I'm going to say floor(a) so just get the,
or int(a), I'm just going to convert it to an integer,
so watch, so now, when I do this in node,
I'm going to say console.log response.standard out.
So I don't need to see that whole object,
I just want to see the stuff that came out of processing.
Let's run it one more time.
And you can see it, that was the angle 42.
Ooh, spooky, spooky 42, meaning of life.
Okay, so.
Great, so here's the thing, I want to once I have
that image, I want to post that image to Mastodon.
Isn't this all about Mastodon?
I've loaded up and connected to Mastodon through this bot.
If you don't know what Mastodon is, did I say this already?
I've got a whole set of videos describing that
and then you can go back and look
but I want to somehow post it to this particular bot,
the coding train bot, okay.
So now, in order to do that, I need to look up
the functions in the Mastodon API.
So I'm using this Mastodon API node package
and if I go here, I can look and see
that it has Mastodon, get Mastodon post.
So this is what I want, Mastodon post.
Here's the thing, I didn't realize this
when I made my other videos about Mastodon
'cause it says path, parameters and callback
but guess what, this supports promises.
So I actually am going to do this without a callback,
with promises, I'm going to break this out
and didn't I say I was going to use async and await?
I was going to write it with just that
but I think I have to start a little bit,
I have to go a little bit further
with the full promises syntax, then I'm going to clean it up
with async and await in a second.
So now, there's no semicolon there.
So now what I want to do is I want to basically say this.
I want to post the image and I want to return this
'cause if this returns a promise, guess what I get to do?
I get to say .then response and have another function,
right, so this is the idea of chaining promises.
And this is what in theory, I mean,
basically the whole theory of this is to avoid callback hell
and really we're just in promises hell, it's all hell
but eventually we will float into the clouds
and feel like we're like butterflies on wings or something,
I don't know, okay, so if I return the post
that I want to do when that's done,
so I'm executing this command when that's done,
then do this, and then when the next promise is done,
then do this.
All right so, what do I need here?
The path, so the first thing I need,
what's weird about and actually it might be worth
just taking a minute to write these steps out
'cause it'll make it more clear.
I want to exec processing,
that's one.
Then two, I want to upload image
and then three, I want to toot.
All right so what's, this is the same thing for
the Twitter API, it doesn't work that you just,
if you want to tweet an image, you don't just simply
send your tweet along with the image,
you have to first upload the media, get that path
to the media and then you can tweet
with that media reference.
So this will actually return an ID for the media
and then as long as I attach the idea to this.
So this creates output.png.
This creates an ID and then I use that ID here
and then I'm done, then we're just done.
This is the three step process.
Each one of these returns a promise.
So when you do this, then this, than that.
All right, that's the process I'm working with here.
All right so (vocalizing) now, so, okay so the path.
So let's go to the Mastodon API docs
and I'm actually looking for media.
So we can click here and this is what I,
this is what I would do, media upload.
I think media upload is just this.
I post to here and then these are the things I need to send.
Okay so file, description, focus, let's look at that.
Okay so I need to, I'm going to create some parameters,
oh no, I'm up here.
I'm just going to create some parameters
and I need the file which is presumably output.png
and this is not exactly right yet.
I can't just put the filename there
but I'll get to that in a second.
Then, I want the description.
The description is really important,
this is not the text that is going along
with the actual post.
What this is is alt text, alternative text.
This is for accessibility.
So somebody who's blind or with low vision
who's using a screen reader instead of seeing this image
would actually hear this description.
So I would say a randomly generated
fractal tree.
Oh and I want to get, this is,
I'm going to say const angle = response.stdout
with and then, oh and I need to use my,
the new thing that I always use now
which is template literals for strings with angle.
So this would be the description.
And then there's one other.
So the file is required, the description it says is optional
but it really shouldn't be optional,
you should be alt text for accessibility
and then focus, this really is optional.
I am assuming, actually I have not tried this yet
but I'm assuming this has to do with where the crop is
if it's showing a preview image, something like that.
Okay so then, I can say so this is done.
And then path and then those parameters.
'Cause this could be an array.
Oh path, no, but okay.
Path, oh yeah the path is media, sorry,
I'm posting to this path, the media path of the API
so almost there but this actually is incorrect.
It doesn't work.
The API is not going to just accept a string
of the filename and figure out how to read that file
and post all the data of that file to the server.
What I actually need to do is give it a readable stream
and so the code for doing that, it's part
of the file system package so I need
to actually also require that.
We could look up the documentation for it
but I happen to know it I think.
So I need to say, I'm going to say const stream = file system
create, there it is, read stream auto complete, thank you.
Output.png, oh, and this actually isn't even right
because guess what, remember, output.png was saved
in the processing sketch which is
the folder treegen/output.png.
So now this is the stream and this is the description
that goes with it and then I want to make sure this worked
so I want to actually console log the response.
So now in theory I have the code all the way
for executing processing, uploading the image,
I need to, when I get the response,
I need to get the ID and then I go
and actually just post the status.
Okay here we go.
Let's try this.
Ugh, unhandled stream error in pipe, no such foul.
No such foul, no such file.
Oh it's not called output.png, I called it tree.png.
Okay.
Let's try that again.
Finished and great.
So look, this is all the stuff that I got back.
Now it's kind of too much stuff for me to look through.
Ugh, what a pain.
Did this actually work?
My goodness, craziness.
But this is all I care about.
There's more important stuff, there's more stuff,
lots of tons of metadata about the image
that you just uploaded but all I really need
is data.id.
So let me just go here and say console log response.data.id.
And I apologize, apologies to the bots and dots space
for overloading your server.
You can see, there's the ID.
Now, the next step would be to use this ID
and then actually
in this response, right, I exec, I execute the command,
I upload the image and then I should be saying
return m.post again and I'm going to,
the path I'm going to is statuses.
So this is what it should be doing
but I can't resist.
Do you see how this is, I mean it's great,
it's kind of nice.
Start the promise chain, then do this,
return a new promise, then do this, return a new promise
and if any error happens anywhere in here it catch,
so why not but this is the thing,
now is there a way, there is a new way for me
to write all of this in what feels more sequential,
more synchronous in fact with less kind of indentation
and brackets and stuff and that is using
this async and await syntax.
So what I'm actually going to do is I'm going
to write a function.
I'm going to call it tooter or just toot
and I'm going to modify this function
with a keyword async.
This means this is an asynchronous function.
This is indicating to JavaScript,
this is a feature of es8, a new feature of JavaScript
that's indicating that this function will be handled
asynchronously and always, always, always return a promise
and it will automagically, it will basically automatically
return that promise and we'll look at that later,
how it does that.
So in other words, but this, I want to do the same thing.
What I want to do here is I want to say exec,
execute the command, right, these are the steps.
And then I want to say post the media.
Post the media.
The nice thing about using the syntax is I don't have
to separate it out with these, chaining these dot thens.
I can actually just use the await keyword.
What the await keyword means is don't go to the next line
of code, await the end of this function.
So if I have a bunch of asynchronous things,
if I package them altogether in an asynchronous function,
I can write them sequentially as line after line
after line and essentially,
and I'm going to have some other params here,
so this is basically I get to write it like this.
Now I need to do stuff in between, that's the thing.
For example, this returns a response and then what I want
to do is get the angle and create the stream
so this goes here.
So this is my step one, right, execute processing.
Look at it, it's just exactly like this and now
I have my step two where I need to create
these parameters that are getting passed
and by the way, I don't have to make a separate variable,
it could be embedded right in here but it's just sort of
a little bit, for legibility,
I'm making it something separate
and so I'm not going to say basically
step two
is upload,
upload,
upload, ha, media and that's going to have a response
and then what do I need?
I need to get the ID out of that
and then I am now going to
say step three and by the way, I suppose
if I'm being accurate about this, this is all,
I suppose this is part of step two,
I don't know, it doesn't really, this is a little bit silly
what I'm getting myself worked up about
but which line of code is part of which step, I don't know,
but step three and maybe I'll name this params one
just to be, just for, I don't know why, why not,
have them have different variable names.
Then step three which is params two,
I want the status to be,
and this is, I'd have to, I think I know what it's supposed
to be but behold my beautiful tree
with angle
and then I should use the template literals again.
Degrees.
And then I need ah, so now, so this, okay,
let me look this up in the
Mastodon API docs.
Okay I found it.
This is the API, this is the path statuses
for posting a status and this is the text of the status.
There are other things you could do here
that I've talked about in previous videos
but this is what I want, media IDs.
Notice this is an array.
It's an array of media IDs because a particular status
could have and it says here maximum four,
more than one image associated with it
so you can upload more than one image.
So that might be an exercise that you try
to do after this video but basically now I can say
comma media_ids and I just have that one ID
so I just put, I can make an array and put it there
and then this has to be statuses params
and then, what I can do, is I can, and this is the response,
and I can just now say return response.
So if we could look at this all together,
I think I have to make this a tiny bit smaller
for you to see it.
Can I fit it all in one
nice place?
Look at this, look how beautiful this is,
it's almost like I've written this code
in a language like Java or C that's perfectly synchronous
and linear and procedural because what I'm saying
is await executing this command.
Then, do some stuff, then await uploading the media.
Then do some other stuff, then await posting
the actual status and when you're done, return,
and guess what, if I now say, let me make this bigger again.
Whoops.
If I say toot, guess what, this toot function
that I've written async, remember when I said
it natively returns a promise,
well by the way, I said return right down here
so this thing right there is the promise it's returning
and I actually might, I could configure my own object
which could be kind of interesting, like what if I said,
return, I could just say something like success true
and status,
status behold my beautiful tree, whatever,
and I could just say angle, whatever,
I could put angle
and then that angle there.
I could actually configure an object
and I could pull stuff out of the response there.
I could return that and then I would say then,
then response,
I can't spell today, I mean I can never spell,
console.log that response, catch any error,
consol.error error.
And this needs a period here and I need to hit save
and something's wrong, oh there's too many dots.
Too many dots, Mozart, too many dots.
Still too many dots?
(ding) Okay.
It's not too many, it was too many dots but I fixed that.
The issue is the dot goes on the next line,
that's the convention, that's what it's looking for.
There, now it's formatted the way I want it to be.
So I still have to engage, oh boy, with this idea
of a promise but I can basically wrap that all
into this one asynchronous function and people on the chat
are telling me I can actually, I don't need to do the then
and catch in this way 'cause I could actually put
a try and catch inside of here but aah, one step at a time.
I think I might be done.
All right let's see, let's see what happens.
Okay, no, response, await, ooh.
So I have also reused so I should call this response one,
response two, maybe there's a more thoughtful way
of doing this, response three.
I didn't actually use the response but whatever.
There we go, start the bot, make the image,
the angle's 72, finished, params is not defined,
oh I was so close, I was so close.
Params one, params two, params two here.
And I think params one there.
All right.
(light drum roll)
I'm feeling pretty confident.
Aah, success, angle is 56 slash finished.
(buzzer buzzing)
That's a little weird but let's see here.
Let's go to the bot.
Look, behold my beautiful tree
with angle 56 finished degrees.
Okay, so there's a little bit of awkwardness in there
in that the standard out is just always giving me
the word finished so because I would like this to work,
work without that, I think, guess what I'm going to have
to do is, is that just something that happens
with processing java?
Because I didn't write finished anywhere in here, right?
That's not a print line I put in here.
So I think what I could do is when that comes,
the standard out comes, I could say response,
I mean I could just do a substring, I could do
a regular expression, there's so many things,
so many things I could do.
Why don't I split it?
Out = response, response standard out split
and I could just split it by the line break,
right, 'cause finish comes after the line break
and then, whoops, and then I could just take the first one.
So this split is a function that takes a string
and splits it up into chunks based on a limiter
and you could get really fancy with that
but I think and then this should just be out
without the caps lock.
I mean I should really test this
but I'm going to have to just rely on the fact
that I think I wrote that code correctly.
That's my way of testing it.
Let's run this one more time.
(light drum roll) Undefined true angle 55.
What's that undefined there?
I liked seeing that.
Oh there we go.
(celebratory horn)
Behold my beautiful tree with angle 55 degrees.
What was undefined?
What did I console log in an undefined way?
Console log response standard out.
Is that it?
No, no, that's 10 finished.
Wait, if it was 10, why did this become 55?
I'm so confused.
(laughing) Let's be a little
more methodical here.
Oh because, huh?
Oh what am I looking at?
Oh I did, aah, it does it twice, I have it happening twice.
Let's take that out.
Apologies, everybody.
This thing is happening twice 'cause I had
the old code in there.
I mean, I'm just going to delete that.
Throw caution to the wind.
What a little mess here.
Okay, that's weird that it did that twice.
So confused.
Okay.
(light drum roll)
Angle 74, just did it once.
(celebratory horn) There we go.
Okay one more thing that I need to do
just so this becomes a true bot
is what I want to do is I want to say set interval
and have it do this thing that it just did there
every, well, let's have it do it every five sec,
every 10 seconds right now.
I don't want it to do it every 10 seconds
but just to,
what's wrong here?
This is opening the function, that's closing it,
no, there's no parentheses there.
That's the end of the function then this, then this.
We're good, we're good, this is it.
Oh but look out weird, I hate, I don't like this at all.
This is making me crazy.
I'm going to do, I'm going to do this.
I mean it's so silly.
Function tooter and then I'm going to put,
this is, I'm a ridiculous person.
I'm going to put this in ooh, no, aah, oh, help me, help, help,
I'm going to get this and put this
and then I'm just going to name my function.
Forget about this anonymous stuff.
This is what I want, right?
I have a separate function which calls
my asynchronous function, does the then the catch
and then I'm going to have that happen every 10 seconds.
Okay.
By the way someone in the chat, k1nhjulian is asking
would it be possible to ask the botch to create
a tree with a specified angle, yes and that is what
I'm going to do in a followup tutorial.
You'll see that in the next one.
Okay here we go.
(light drum roll) And we have
a tree, oh no, I forgot, it's going to wait,
so one thing about set interval, womp womp.
(sad horn)
Which is that, I mean it works,
okay, I'll just keep going now.
Set interval will not execute that function immediately,
it will wait the amount of time before doing it
the first time but now we did it twice every 10 seconds,
first one was angle 44 and the second was angle 28.
We can wait 10 seconds, you can watch this video
for 10 more seconds, we could also speed this up now,
let it happen four or five times but I think that's good.
So let's just go and check,
here, and we can see 83 degrees, 28, 44, there we go
and 44, 28 83.
44,
28,
83.
Wonderful, okay, so this is working.
You know, now of course, I don't want to leave it like this,
right, having a bot post every 10 seconds,
no one wants to follow a bot that posts every 10 seconds.
Maybe I want it to just once a day it's going to do,
maybe once an hour might be the maximum,
the most I would do but let's just,
if I want to do it once a day it would be 24 hours
times 60 minutes, there's 60 seconds in a minute
and 1000 milliseconds in a second.
So this would now be but, I wouldn't want it
to wait a whole day so I probably want to call it once.
So I'll call it once, then set the interval
and I'm sure there's a more elegant way to do that
that all of you will someday will write in the comments
and here we go and ah, ooh, ah, ooh, node bot, aah,
bot.js, here we go.
It does the first one and then now,
we're going to wait, 24, we're going to wait 24 hours,
I'll wait, yes we'll wait and then in 24 hours,
I'll still be standing here.
I could technically go home, have dinner,
go to sleep, come back, this laptop would still be here
and it would do the next one.
We're just going to have to believe that that's going to happen.
Again, there is a question of well where
would I actually want to deploy this?
I've got to talk about that in a separate
but the quick answer is you're going to want to find
server, maybe you have one through a hosting company,
maybe you happen to have a computer that's always on
in your home or raspberry pie that can act
as a server but you need somewhere where
you can just let it run over and over again.
Forever.
Okay, so thanks for watching this video.
(ding) Making a Mastodon image bot.
And what I'm going to do, I want to do one more tutorial
because how would you do it so that if someone
at mentions the bot, let's say with an angle,
then the bot replies back with a tree with that angle.
Let's see if we can make that work,
that's going to be fun.
Okay, see ya in the next video.
(upbeat music)