Your Sucks Programming Language Favourite
Much to my chagrin, I've found myself lately becoming a defender of C++. People who a) know me and b) appreciate irony should feel free to smirk right about now.
To be fair, modern C++ has improved significantly, reaching rarified hights of not-badness only dreamed of fifteen years ago. But that's kind of beside the point. When you choose a programming language for a project, the quality of the language itself is often less important than external stuff; the quality of available implementations, tools, research, etc.
If I'm going to bet my (hypothetical) business on investing a zillion dollars to write a program that I can then sell, I want to know that:
- The development tools aren't going to rot or disappear because the vendor lost interest (e.g. Visual Basic).
- I'll be able to hire skilled developers whenever I need to.
- Good quality tools, books, training, etc. will all be available when I need them.
(And as a developer, I want to bet my non-hypothetical livelihood on developing the skills that are most likely to keep me employed. Being a really badass Haskell programmer doesn't really do much for my job search1.)
So let's concede that Rust (for example) is a better language than C++. C++ will still be a better choice for most commercial ventures in that space because it has:
- Multiple high-quality implementations, two of which are FOSS2
- A huge selection of high-quality third-party tools
- An enormous community of developers with whom you can exchange knowledge
- A literal half-century of concerted research on how to use it effectively
C++ sucks in a variety of ways but we know exactly how it sucks and how to work around it. Rust's suckage is still unknown, and I want the thing that keeps me from being homeless3 to have a really good track record.
And this principle applies to Scala-vs-Java, Zig-vs-C, Haskell-vs-anything, anything-please-anything-vs-PHP or any other language debate. $FAVORITE_LANGUAGE may be a better language than $CHOSEN_LANGUAGE but that doesn't mean it's going to get the job done better, faster, cheaper or more reliably. All of that depends on the entirety of the language's ecosystem.
Note that I'm not saying don't use $FAVORITE_LANGUAGE.
Just
be aware of what's riding on that decision. For a hobby project or an
in-house tool that took a month to write, it's going to be fine. But
for the hundred person-year project the business depends on? I mean,
I'd really like $FAVORITE_LANGUAGE to be viable in ten years but I'm
not going to bet the mortgage on it.
Also, you should go out and learn all kinds of programming
languages--especially wierd ones that will never fly in
Industry--because it will make you a better programmer. I got a
lot of benefit as a C programmer from asking myself, How would I
do this in Smalltalk?
I'm a programming language nerd. I've spent a lot of time thinking about how languages work and how they make people think about programming. I learn new languages for fun and I've designed and implemented several. So I absolutely get the desire to use better languages and the frustration of having to deal with the broken status quo. In a perfect world, we'd all be using Smalltalk.
Unfortunately, our world is fallen and so C++ is a necessary evil.
-
Okay, that's an exaggeration. A good hiring process will recognize that Haskell skills are often transferable to whatever the company is using. Unfortunately, a lot of otherwise-fine employers have terrible hiring processes and will reject any résumé not listing the exact version of their preferred web framework. As those companies have money they will exchange for relatively pleasant work, I would like to retain the option of working there. ↩
-
But if $FAVORITE_LANGUAGE is FOSS, that means it will be available forever!
No, not really. If nobody else is working on it, you'll find yourself having to maintain the toolchain by yourself. At that point, it's almost always easier to just rewrite your program in something else. ↩ -
Yeah, yeah, I know; the real problem is Capitalism. ↩
# Posted 2021-07-04 17:16:50 UTC; last changed 2021-07-04 18:09:16 UTC
Getting the Singleton Class of a BasicObject in Ruby
Ruby objects provide the method singleton_class
which returns the
object's singleton class. Unfortunately, BasicObject
doesn't have
this because it's Object
's superclass. So to get it, we need to be
somewhat clever.
And having spent way too much time figuring out how to do this, I'm writing it here so a) that I don't lose it again and b) so that others will have less trouble than me. (I'm not on Medium so, um, hello from the fourth page of your Google search results.)
TL; DR, How do I do it?
In an instance, you'd do something like this:
obj = BasicObject.new
obj.instance_exec(obj) {
class << self
lself = self
self.define_method(:my_singleton_class) { lself }
end
}
Notice how I copy self
to lself
on line 4. That's because self
will have changed when the method is called but the block that forms
the body of my_singleton_class
captures the local variable.
Also: this won't work on Ruby versions from sometime before 2.7
because define_method
is private before then; see your version's
Module
documentation for define_method
for a hacky workaround if
it's too old.
Doing this with a BasicObject
subclass is even simpler:
class Thingy < BasicObject
def my_singleton_class
class << self
return self
end
end
end
What's it good for?
Any case where you want an object to handle a method call by doing something other than call a method. For example, a DSL or a proxy object that forwards the call to something else.
Typically, you'd create a class with no methods, then implement
method_missing
to catch the failing method lookups and do the right
thing with them.
class Proxy
def initialize(target) @target = target; end
def method_missing(name, args)
log "Called #{name} with #{args}"
return @target.send(name, args)
end
end
BasicObject
is the ideal base class for this because it has very few
methods but if that's not enough–if you need to get rid of those few
as well–you can always override (most of) them with a method that
calls method_missing
directly. This is straightforward when
creating a subclass but there are times when it's necessary or easier
to add methods to the object instead, and for that you need to get the
singleton class.
In my case, I'm writing a DSL where every method whose name starts
with a letter is valid; this means they all need to turn into calls to
method_missing
.
(Handling the case where the user uses method_missing
as a name in
the DSL is left as an exercise to the reader.)
What's a singleton class anyway?
So normally in OOP, an object is an instance of a class and this is the case with Ruby as well:
[] # => []
[].class # => Array
[].class.class # => Class
But, when Ruby creates an object from a class, it also first creates
another anonymous class called the singleton class
. This gets
inserted in the new object's inheritance heirarchy: that is, the
singleton class becomes a subclass of the new object's class and the
object becomes an instance of the singleton instead of the original
class.
x = [] # => []
x.class # => Array
x.singleton_class # => #<Class:#<Array:0x00007fbc862d41b0>>
x.singleton_class.superclass # => Array
This is how you can add methods to individual Ruby objects: you're actually defining them in the object's singleton class.
Fun fact: singleton classes are also objects and thus have their own singleton classes:
x.singleton_class
# => #<Class:#<Array:0x00007fbc869b0060>>
x.singleton_class.singleton_class
# => #<Class:#<Class:#<Array:0x00007fbc869b0060>>>
x.singleton_class.singleton_class.singleton_class
# => #<Class:#<Class:#<Class:#<Array:0x00007fbc869b0060>>>>
This can go as deeply as you want it to.
The reason Ruby doesn't immediately fill up all available RAM with singleton classes and then die is because they are not created until the first time a program uses them. As a result, most objects don't have singleton classes at all.
Isn't this whole singleton class thing kind of overkill?
Not really.
See, Ruby is a language where everything is an object (in the OOP sense of the term), and so this means that classes are also objects. But since all objects have classes, that means each class is also an instance of a class. And so is that class. And this is if we ignore the singleton classes, which we are for the moment.
So how does this end? Well, it's pretty boring actually. Each class
is an instance of the class named Class
, including Class
itself. Class
is an instance of itself and that's all we really
need.
[] # => []
[].class # => Array
[].class.class # => Class
[].class.class.class # => Class
[].class.class.class.class # => Class
But wait! How do we do class methods or class instance variables:
class Thing
def self.instance
@instance = Thing.new unless @instance
return @instance
end
# ...etc...
end
In Smalltalk, this gets done by giving each class object its own
distinct class (the metaclass
) to hold the methods and variable
declarations. They are unnamed but you can get it with the class
method just like Ruby. The metaclass's inheritance tree mirrors the
class's tree (i.e. if Item
is derived from Thing
, then
Thing.class
is derived from Item.class
) with class Class
as the
abstract base class of the heirarchy.
t class. => Thing
t class superclass. => Object
t class superclass superclass. => nil
t class class. => Unnamed class ('Thing class')
t class class superclass. => Unnamed class ('Object class')
t class class superclass superclass. => Class
All metaclasses are instances of the class Metaclass
:
t class. => Thing
t class class. => Unnamed class ('Thing class')
t class class class. => Metaclass
This includes Metaclass
itself, which is how the loop closes:
Metaclass class => 'Metaclass class'
Metaclass class class => Metaclass
(Disclaimer: I've somewhat simplified the above. I also haven't run it.)
In Ruby, each Class
instance (i.e. class) has a singleton class that
holds the class methods and variables. That is, singleton classes
serve as metaclasses. The nice thing about this is that it's a
generalization of what Smalltalk does for classes, and it gives you
instance methods for free.
This is not to say that it's necessarily a better way than Smalltalk's. There are advantages and disadvantages to each approach but I'm far too lazy to write about them here.
# Posted 2021-05-07 01:55:31 UTC; last changed 2021-05-07 01:57:49 UTC
Star Trek Phone Chargers
In this Fediverse thread about how the most implausible part of Star Trek is that all of the devices just kind of interoperate when we can't even get phone chargers to work right, I got inspired:
Don't use the Klingon one. It'll blast your device with random voltages and then call it weak if it explodes.
--
The Borg one works with anything but after the first charge, your device will no longer be compatible with any other charger.
--
The Ferengi one is crap, plus it bills you per minute of charging.
--
The Romulan one is widely believed to install spyware but nobody has managed to prove it.
--
The Vulcan one is a featureless gray sphere with a slight flat spot so it doesn't roll. Nobody has any idea how to use it. When asked, Vulcans will respond either implicitly or explicitly that it's obvious and your question makes no sense.
--
Cardassian chargers have astoundingly terrible ergonomics. It's possible to get one to work with most phones but it's usually not worth the effort.
--
The Bajoran chargers are just Cardassian chargers with pieces hacked off and the gaps filled with epoxy and duct tape.
They're still easier to use than the Cardassian chargers, though.
# Posted 2019-10-10 01:28:01 UTC; last changed 2019-10-10 01:30:21 UTC
Sic: Yet Another Mediocre Small Lisp Dialect
Like many bad ideas, this one came from seeking attention on social media. I had envisioned tooting something like this on Mastodon:
(dontforgettolikeandsubscribe)
This was the result of a series of realizations I had while learning more about modern C++. Specifically:
1. $
is a valid 'word' character in C++ these days.
That means you can use it in names. So this is valid C++:
const int $20_same_as_in_town = 20;
But since this not widely known, I can use it for all kinds of shenanigans.
2. You can use variadic templates to fake Lisp-style expressions.
As you know Bob, variadic templates are function templates that take arbitrarily many arguments. And since they're templates, their type is also up for grabs.
And as you also know Bob, Lisp-style lists are just linked lists of pairs of pointers where the first pointer holds the value and the second the next pair in the sequence. And Lisp expressions are just lists of expressions where the first value is the function to call while the rest are its arguments.
So if we create a bunch of C++ classes to represent basic Lispish types:
class string : public obj { ... };
class symbol : public obj { ... };
class number : public obj { ... };
plus a common base class:
class obj { ... };
plus a simple C++ class to hold a pair:
class pair : public obj {
public:
obj * const first; // Not named 'car'; cope.
obj * const rest; // Ditto for 'cdr'.
pair(obj *a, obj *d) : car(a), cdr(d) {}
}
and a set of overloaded helper functions to convert basic C++ types to Lispish types:
static inline obj* _w(std::string s) { return new string(s); }
static inline obj* _w(int i) { return new number((long)i); }
static inline obj* _w(long l) { return new number(l); }
static inline obj* _w(double d) { return new number(d); }
static inline obj* _w(obj *o) { return o; }
we can create a variadic function template that constructs a Lispish list:
template<typename T> obj* $(T o) { return new pair(_w(o), nil); }
template<typename T, typename... Objects>
obj* $(T first, Objects... rest) {
return new pair(_w(first), $(rest...));
}
(The first definition of $
works on any call with one argument; the
second expands to a function that takes the first argument and
recurses on the rest.)
Calling it looks like this:
$("foo", 42, $("add", 2, 2))
which looks Lispish if you squint hard enough. But the resulting list is an actual Lisp-style list.
3. Evaluating functions is pretty straightforward.
A function is just one of the Lispish C++ types. It implements a
method named call
, which takes an argument list and returns a
result:
class callable : public obj {
public:
virtual obj* call(obj* actualArgs) const = 0;
};
Yeah, yeah, this is an abstract base class. We actually need two types of callables:
class function : public callable { ... }
class builtin : public callable { ... }
function
is a function written in Sic. It holds a Lispish list of
expressions (also Lispish lists) and evaluates them by calling eval
on each of them. builtin
holds a built-in function--a C++ lambda
(or other function pointer, in theory)--that it calls instead.
eval
is the function that evaluates a Sic expression. You give it
the expression and if it's a list, it recursively calls itself on each
item and collects the results. Then, it calls the first item's call
method with the rest of the list as an argument and returns the
result. If the argument isn't a list, it just returns it.
So like any good Lisp function, it either does nothing significant or recurses.
4. Also, I can do macros because I hate myself.
As you know Bob--hey, why are you walking away? I NEED YOU FOR THIS RHETORICAL DEVICE, BOB!
Anyway, as Bob over there already knows, a macro is a powerful and elegent way to let programmers mutate their Lisp into a completely different language while introducing subtle and impossible-to-find bugs.
More precisely, It's a function that gets called on the raw, unevaluated argument list, does something with that and returns something else that does get evaluated as a normal Lispish expression. On compiled Lisps (i.e. not this one), it gets called by the compiler.
The thing is, we need macros for system-ish and control-flow-ish
stuff. You can't do an if
statement if eval
is always going to
evaluate the THEN and ELSE expressions regardless.
So we do macros by adding the isMacro
flag to callable:
class callable : public obj {
public:
const bool isMacro;
virtual obj* call(obj* actualArgs) const = 0;
};
(isMacro
gets set by the constructor.)
Then we make eval
check if it's true. If it is, it calls the
function on the arguments first, before they're evaluated, captures
the result and recursively evaluates that.
And there you go. Self-modifying code made easy.
(The Scheme community has new and interesting ways to make macros safer and easier to use. I strongly disagree with this. If macros are easy to use, people might start using them, and that's only going to lead to trouble.)
5. Errors are just C++ exceptions
The easy way to handle errors here is to just throw a C++ exception where necessary. We give them a common base class to distinguish them from other types of exceptions, but that's pretty much it.
Need a stack trace? Put a std::vector
in the base class and wrap
eval
or call
in a try block that adds the details to it. Easy!
6. Local namespaces are a linked-list of std::map
-holding objects
Now that we're actually interpreting code, we need variables. This is
pretty simple, right? C++ has std::map
which does pretty much
everything we need. Just wrap it with a class:
class context {
private:
std::map<std::string, obj*> items;
public:
void set(const std::string& name, obj* value) { ... }
obj* get(const std::string& name) const { ... }
}
And that's all we need--no, wait, there's also a global scope so it needs to fall through to that. So we add a pointer to an outer scope:
context * const parent;
and make set
and get
fall through to the parent if it's not
local. Easy!
No, wait. set
falling through means I have to make defining a
variable in a local scope so stuff like this will work:
(let ( (outer nil) )
(let ( (x 1) )
(setq outer 42)))
We don't want setq
to define a new variable outer
in its
scope; it should be writing to the existing outer
. So we need to
make defining variables and assigning to them separate things. We do
this by making set
throw an exception if it can't find the variable,
then adding a define
method:
void define(const std::string& name, obj* value) { ... }
And that... works?
Looks around nervously.
Next, we need to add the context as an argument to Callable::call
:
virtual obj* call(obj* actualArgs, context* outer) const = 0;
and propagate it to eval
and whatever else needs it.
Because built-in functions now get a pointer to the caller's context,
they can modify the caller's variables. Which is generally a Bad
Thing unless we need to write set
, which we do. So it's actually a
good thing, I guess.
(We also have to write the macro setq
--which expands to
set
--because typing that extra quote is so burdensome. No,
seriously, it's a huge pain--the number of times I forgot it is
basically the number of times I wrote buggy Sic code.)
We also need this when defining lambdas because (as Bob over there knows), they can access the scope in which they are defined. That is, this:
(defun return-x-f (x) (lambda () x))
(setq fn (return-x-f 42))
(print (fn))
will print 42, because the lambda holds on to the outer function's context.
So lambda
(well, its back-end--it's a macro, after all) gets a
pointer to the caller's context and stashes it in the function
object:
context *outer;
When the lambda gets called, it creates its context (the equivalent of
a stack frame in C++) and sets outer
as the parent.
Conveniently, non-lambda functions (ironically-named fun
) are just
like lambdas except that their outer
pointer just points to the
global context (i.e. the outermost parent).
Finally, we need to actually look up variables. That ends up
being a one-liner in eval
:
if (expr->isSymbol()) { return context->get(expr->text); }
And that's all.
7. Oh yeah, I forgot to talk about Symbols
As Bob--hey, where'd he go? Anyway, as Bob knows, Lispish languages have this concept of a symbol, which is different from a string. A symbol is a chunk of text but it represents an internal variable name.
If you hand eval
a string, it just gives you the string back but if
you hand it a symbol, it'll look it up in the current context and give
you the value back instead. Which you already know, because you read
the last section. Right?
So in Sic, a symbol
class is just an obj
subclass that holds a
std::string
. Except the field has a different name from the Sic
string
(text
vs contents
) so that I can't accidentally use one
instead of the other.
class symbol : public obj {
public:
const std::string text;
explicit symbol(std::string& v) : text(v) {}
};
There's nothing really magical about it except that eval
can tell
the difference between the two and handles them differently.
Oh, and also, I did a clever thing in the symbol
class where I
guarantee that there's only ever one symbol
for a particular series
of characters. This ensures the Lispish requirement that symbols be
unique and also lets you test equality in C++ by comparing the
pointers with ==
.
So it really looks (more) like this:
class symbol : public obj {
private:
inline static std::map<std::string, symbol*> symbols;
explicit symbol(std::string& v) : text(v) {}
public:
static symbol* intern(std::string s) {
if (symbols.count(s) == 0) { symbols[s] = new symbol(s); }
return symbols[s];
}
};
(Basically, I make the constructor private and provide a public static
method called intern
that calls it, but only if there's not already
an instance in symbols
. In that case, it first stashes the symbol
there before returning it. Otherwise, it returns a pointer to the
stashed symbol already.
This all works because Sic types are immutable (barring C++ type abuse, that is).)
The other thing I need to mention is that code like
$("setq", "foo", 42)
that I used above doesn't actually work the way you'd naively
expect. The arguments are strings which eval
won't look up. We
need to make them into symbols.
Unfortunately, C++ doesn't have a symbol type and we already transparently turn C++ strings into Sic strings, so there's not an obvious conversion.
So we make it explicit with a helper function. Which I name $$
,
'cuz why not:
static inline obj* $$(const std::string& s) { return symbol::intern(s); }
So now, we can do explicit symbols like this:
$( $$("setq"), $$("foo"), 42)
Which is only slightly uglier.
(To make this slightly easier, sic.hpp
also defines a bunch of
global consts that hold pointers to the corresponding functions. This
lets you replace the above with:
$(set, $$("foo"), 42)
which is a bit nicer.)
8. read
is complex and ugly but uninteresting
So you'll note that at this point (assuming I've also written a bunch of useful built-in functions), we pretty much have a working(ish) programming language. I can do stuff like this:
$(progn,
$(print, "starting!\n"),
$(defun, $$("fib"), $( $$("n") ),
$(if_op, $(le, $$("n"), 1),
$(list, 1),
$(if_op, $(eq_p, $$("n"), 2),
$(list, 1, 1),
$(let, $( $( $$("prev"), $( $$("fib"), $(sub, $$("n"), 1) ) ) ),
$(pair_op,
$(add, $(first, $$("prev")),
$(second, $$("prev"))), $$("prev") )
)
)
)
),
$(print,
$( $$("fib"), $(str_to_num, $(third, $$("argv")) ) ),
"\n")
)
;
and it works.
Lispish source code is basically just text serialization of its data types--primordial JSON, as it were, so all I really need to do to write and evaluate scripts is a function to parse lists, names and literal types.
In most Lisps, this is called read
so I called it that too.
read
is written in pure C++ and does pretty much what you'd expect
with std::stream
and std::string
. Boring, in other words. But it
works.
I did make one attempt to be innovative and modern and made the
comment character #
instead of ;
because that's more Unixy and you
can do the #!
thing to launch scripts. Of course, editors still
expect ;
so I ended up putting back ;
as an alternate comment
character.
So now you have two comment characters for the price of one. Lucky you.
8. Garbage Collection would be nice but it's too much work.
Unlike most Lispish languages, Sic implements garbage collection by having me suggest that you exit your program sometime before your computer runs out of RAM. After that, object memory is reclaimed very efficiently.
(It would be (sigh) relatively straightforward to create an abstract
base class for obj
and context
that stores all of the instances in
a global registry and provides marking for mark-and-sweep, but that's
a lot more work than I want to do right now. It's probably easier to
just grab the Boehm GC and use
that.)
9. It's no longer fun but I can't stop. Help!
So now that I have read
, I can split Sic up into a library and a
script runner.
The library gets a function called root_context()
that creates a
global context
(i.e. one with no parent) and loads it up with all of
the built-in functions. A neat side effect of this is that you can
now have multiple Sic instances in your program; just create a new
root context for each.
The runner links to the library, calls root_context()
and either
loads in the script you give it or lets you type in commands. (If you
want readline support, rlwrap
is available.)
Oh, but it'd be nice to have a unit test framework. So I'll add extra
testing builtins but only if the script name ends with .sictest
.
Which turns out to be much trickier than it looks.
So once that's working, I should probably test most of these
functions. But I don't really have proper equality testing so I
should add that. And tests. Also, I should write examples. But
this example would be much easier if I had cond
(plus tests).
And cond
makes or
really easy, so I should add that (plus tests).
But or
implies and
, so I should add that as well (plus tests).
And I really should--
On second thought, it's done now.
10. Screw it. It's on Github now.
If you want to play with the code, it's here. I'm releasing it under the terms of the wxWidgets license, which is basically the GNU LGPL but with less restrictions on using it in your own programs.
Have fun. Or not.
# Posted 2019-06-22 23:49:10 UTC; last changed 2021-05-07 02:00:43 UTC
A Quick and Dirty Tutorial for Writing Mastodon Bots in Ruby
My most recent project has been this Mastodon bot. Every n hours (where n == 8, for now) it toots an interesting section of the Mandelbrot Set.
Unfortunately, when getting to the actual bot part of the project, I found very little in the way of tutorials or examples in Ruby, so I'm writing down what I did while it's still fresh in my mind.
1. Create an Account
First, we need an account on a Mastodon instance. I'm using http://botsin.space, an instance for bot writers. This is pretty straightfoward: just fill in your details in the signup form and away you go.
2. Get Credentials
Getting credentials is an overly complicated process that (fortunately) there are tools to help with. Basically you:
Register your application on the Mastodon instance. This gives you two big numbers (the client ID and client secret).
Using these numbers plus the username and password of your bot's account, log in. This gives you your bot's access token. Hold on to this.
From now on, the bot will use the access token to authenticate itself. You can invalidate the access token at any time from the bot account's settings (Settings -> Authorized Apps).
Unfortunately, the Ruby Mastodon client API library doesn't have an
easy way to do this for you. After wrestling with it for a while, I
ended up just using Darius Kazemi's registration app at
https://tinysubversions.com/notes/mastodon-bot/. It's a nice little
webapp that does everything reasonably securely. Just log into your
instance and, while logged in, go to the above URL and follow the
directions. (You do need to have curl
installed and available,
though.)
Failing that and if you have a Python installation handy, you can use the Python Mastodon module. Allison Parish's excellent guide for doing that is here.
3. Install the Mastodon API Gem
Assuming you have Ruby installed, it's a simple matter of typing
gem install mastodon-api
Unfortunately, as of this writing, there's a bug in the current release which has been fixed in development but which breaks the bot.
I worked around this by creating a Gemfile and listing a recent git
commit as the version. (Did you know that bundle
can install gems
from GitHub? It can!)
It looks like this:
source 'https://rubygems.org'
gem "mastodon-api", require: "mastodon", github: "tootsuite/mastodon-api",
branch: "master", ref: "a3ff60a"
Then, you cd
to the project directory and run bundle:
bundle
(Or, if you want to install the gems locally and aren't using rvm
or
rbenv
, you can install the gems locally:
bundle install --path ~/gems/
But you knew that, right?)
4. Basic Tooting
We are now ready to write a simple bot. This is done in two steps.
First, we create the client object:
client = Mastodon::REST::Client.new(
base_url: 'https://botsin.space',
bearer_token: 'bdc1fe7113d7cb9ea0f5d25')
The bearer_token
argument is the token you got in step 2. (The real
one will be longer than the one in the example.)
Then, we post the toot:
client.create_status("test status!!!!!")
And that should do it.
Here's the complete script:
require 'rubygems'
require 'bundler/setup'
require 'mastodon'
client = Mastodon::REST::Client.new(
base_url: 'https://botsin.space',
bearer_token: 'bdc1fe7113d7cb9ea0f5d25')
client.create_status("test status!!!!!")
One thing: because it's using bundler, the script must either be run
from the directory containing the Gemfile or you will need to set the
BUNDLE_GEMFILE
environment variable to point to it.
I ended up doing the latter by writing a shell script to set things up and then launch the bot:
#!/bin/bash
botdir="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)"
export BUNDLE_GEMFILE="$botdir/Gemfile"
ruby "$botdir/basic_toot.rb" "$@"
The tricky expression in the second non-empty line extracts the directory containing the shell script itself; it figures out the location of the Gemfile from that.
5. Tooting With Media
Going from here to attaching a picture is pretty simple.
You set up the client as before and then upload the picture with
upload_media()
:
form = HTTP::FormData::File.new("#{FILE}.png")
media = client.upload_media(form)
It's possible that you don't need to wrap your filename in a
HTTP::FormData::File
object (the API docs seem to imply that) but I
wasn't able to get that to work and anyway, this doesn't hurt.
The media
object you get back from the call to upload_media()
contains various bits of information about it including an ID. This
gets attached to the toot:
status = File.open("#{FILE}.txt") {|fh| fh.read} # Get the toot text
sr = client.create_status(status, nil, [media.id])
It's in an array because (of course) you can attach multiple images to a toot.
Here's the entire script:
require 'rubygems'
require 'bundler/setup'
require 'mastodon'
FILE = "media_dir/toot"
client = Mastodon::REST::Client.new(
base_url: 'https://botsin.space',
bearer_token: 'bdc1fe7113d7cb9ea0f5d25')
form = HTTP::FormData::File.new("#{FILE}.png")
media = client.upload_media(form)
status = File.open("#{FILE}.txt") {|fh| fh.read}
sr = client.create_status(status, nil, [media.id])
This is mostly I use, plus the whole image plotting stuff, some logging stuff and a bunch of safety and error detection stuff.
6. Auth Token Management
You will note that I store the auth token as a string literal. I do this to simplify the example code but in general, it's a Bad Idea; mixing your credentials with your source code is just asking for your login details to get published on the open Internet. Keep them far away from each other.
For my bot, I use a YAML file that sits in the bot's work directory (which is not part of the source tree). Other options might be to pass it as a command-line argument or store it in an environment variable.
Other Resources
- The docs for Mastodon::REST::API provide a good overview of the API module's capabilities.
- Allison Parish's Python Tutorial (mentioned above) was also very helpful.
- Twittodon is another bot written in Ruby. I found it very useful as an example.
# Posted 2017-12-15 03:46:21 UTC; last changed 2018-10-05 01:04:04 UTC