GithubHelp home page GithubHelp logo

ananas's Introduction

Ananas

What is Ananas?

Ananas allows you to write simple (or complicated!) mastodon bots without having to rewrite config file loading, interval-based posting, scheduled posting, auto-replying, and so on.

Some bots are as simple as a configuration file:

[bepis]
class = tracery.TraceryBot
access_token = ....
grammar_file = "bepis.json"

But it's easy to write one with customized behavior:

class MyBot(ananas.PineappleBot):
    def start(self):
        with open('trivia.txt', 'r') as trivia_file:
           self.trivia = trivia_file.lines()

    @hourly(minute=17)
    def post_trivia(self):
        self.mastodon.toot(random.choice(self.trivia))

    @reply
    def respond_trivia(self, status, user):
        self.mastodon.toot("@{}: {}".format(user["acct"], random.choice(self.trivia)))

Run multiple bots on multiple instances out of a single config file:

[jorts]
class = custom.JortsBot
domain = botsin.space
access_token = ....
line = 632

[roll]
class = roll.DiceBot
domain = cybre.space
access_token = ....

And use the DEFAULT section to share common configuration options between them:

[DEFAULT]
domain = cybre.space
client_id = ....
client_secret = ....

Getting started

pip install ananas

The ananas pip package comes with a script to help you manage your bots.

Simply give it a config file and it'll load your bots and close them safely when it receives a keyboard interrupt, SIGINT, SIGTERM, or SIGKILL.

ananas config.cfg

If you haven't specified a client id/secret or access token, the script will exit unless you run it with the --interactive flag, which allows it to prompt you for the instance login information. (The only part of the input you enter here that's stored in the config file is the instance name -- the email and password are only used to generate the access token).

Configuration

The following fields are interpreted by the PineappleBot base classs and will work for every bot:

class: the fully-specified python class that the runner script should instantiate to start your bot. e.g. "ananas.default.TraceryBot"

domain ¹: the domain of the instance to run the bot on. Must support https connections. Only include the domain, no protocol or slashes. e.g. "mastodon.social"

client_id ¹, client_secret ¹: the tokens that the instance uses to identify what client this bot is posting from/as. Will be used to determine what's displayed underneath all the posts made by this bot.

access_token ¹: the access token used to authenticate API requests with the instance. Make sure this is secret, don't distribute config files with this field filled out or people will be able to post under the account this token was created with.

admin: the full username (without leading @) of the user to DM error reports to. Can be left unspecified, but is useful for keeping an eye on the health of the bot without constantly monitoring the script logs. e.g. [email protected]

¹: Filled out automatically if the bot is run in interactive mode.

Additional fields are specific to the type of bot, refer to the documentation for the bot's class for more information about the fields it expects.

Writing Bots

Custom bot classes should be subclasses of ananas.PineappleBot. If you override __init__, be sure to call the base class's __init__.

Decorators

In order for the bot to do anything, you should add a method decorated with at least one of the following decorators:

@ananas.reply: Calls the decorated function when the bot is mentioned by any other user. Decorator takes no parameters, but should only be called on functions matching this signature: def reply_fn(self, mention, user). mention will be the dictionary corresponding to the status containing the mention (as returned by the mastodon API), user will be the dictionary corresponding to the user that mentioned the bot (again, according to the API).

@ananas.interval(secs): Calls the decorated function every secs seconds, starting when the bot is initialized. For intervals longer than ~an hour, you may want to use @schedule instead. e.g. @ananas.interval(60)

@ananas.schedule(**kwargs): Allows you to schedule, cron-style, the decorated function. Accepted keywords are "second", "minute", "hour", "day_of_week" or "day_of_month" (but not both), "month", and "year". If any of these keywords are not specified, they will be treated like cron treats an *, that is, as long as the time matches the other values, any value will be accepted. Speaking of which, the cron-like syntax "*" as well as "*/3" are both accepted, and will expand to the expected thing: for example, schedule(hour="*/2", minute="*/10") will post every 10 minutes during hours which are multiples of 2.

@ananas.hourly(minute=0), @ananas.daily(hour=0, minute=0): Shortcuts for @ananas.schedule() that call the decorated function once an hour at the specified minute or once a day at the specified hour and minute. If parameters are omitted they'll post at the top of the hour or midnight (UTC).

@ananas.error_reporter: specifies custom behavior for reporting errors. The decorated function should match this signature: def err(self, error) where error is a string representation of the error.

Overrideable Functions

You can also define the following functions and they will be called at the relevant points in the bot's lifecycle:

init(self): called before the configuration file has been loaded, so that you can set default values for config fields in case the config file doesn't specify them.

start(self): called after all of the internal PineappleBot initialization is complete and the mastodon API is ready to use. A good place to load files specified in the config, post a startup notice, or otherwise do bot-specific setup.

stop(self): called when the bot has received a shutdown signal and needs to stop. The config file will be saved after this, so if you need to make any last minute changes to the config, do that here.

Configuration Fields

All of the configuration fields for the current bot are available through the self.config object, which exposes them with both field-accessor syntax and dictionary-accessor syntax, for example:

foo = self.config.foo
bar = self.config["bar"]

These can be read (to get the user's configuration data) or written to (to affect the config file on next save) or deleted (to remove that field from the config file).

You can call self.config.load() to get the latest values from the config file. load takes an optional parameter name, which is the name of the section to load in the config file in case you want to load a different one than the bot was started with.

You can also call self.config.save() to write any changes made since the last load back to the config file.

Note that if you call self.config.load() during bot operation, without first calling self.config.save(), you will discard any changes made to the configuration since the last load.

Distributing Bots

You can distribute bots however you want; as long as the class is available in some module in python's sys.path or a module accessible from the current directory, the runner script will be able to load it.

If you think your bot might be generally useful to other people, feel free to create a pull request on this repository to get it added to the collection of default bots.

Questions? Ping me on Mastodon at @[email protected] or shoot me an email at [email protected] and I'll answer as best I can!

ananas's People

Contributors

ashkitten avatar chr-1x avatar codl avatar elizafox avatar joyeusenoelle avatar puphime avatar starlitghost avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

ananas's Issues

History rewritten to remove deadname

If you have a fork of this repo, heads up that the history was rewritten to remove my deadname; this roughly corresponded with a move from master to main for the default branch of the repo so hopefully it should be easy to update. Thank you for contributing!

Make error reporting configurable

I run a number of bots and when they error out, which happens pretty regularly due to network flakiness etc, they keep sending DMs that I cannot even delete without logging in as the bot and removing them.

Would be great if that could be redirected into a logfile or even as email and I can filter it there. I am not sure just because it is a mastodon bot that DMing error messages is the best thing.

Can't delete keys from config

Seems like deleted config keys and values still get written to the config file

$ cat ananas.cfg
[test]
class = test.TestBot
domain = chitter.xyz
client_id = xxxxxxxxxxxx
client_secret = yyyyyyyyyyyy
access_token = zzzzzzzzzzzz
$ cat test.py
import ananas

class TestBot(ananas.PineappleBot):
    def start(self):
        self.config["secret"] = "my brain IBM"
        self.config.save()
        del self.config["secret"]
        self.config.save()
$ ananas ananas.cfg
range(0, 60, 2)
range(0, 60, 2)
range(0, 60, 1)
range(0, 60, 1)
[2022-04-22 10:25:16] test.config: Loading configuration from ananas.cfg
[2022-04-22 10:25:16] test: Starting TestBot test
[2022-04-22 10:25:16] test.config: Saving configuration to ananas.cfg...
[2022-04-22 10:25:16] test.config: Done.
[2022-04-22 10:25:16] test.config: Saving configuration to ananas.cfg...
[2022-04-22 10:25:16] test.config: Done.
[2022-04-22 10:25:16] test: Startup complete.
^C[2022-04-22 10:25:31] test: Stopping TestBot test
[2022-04-22 10:25:31] test.config: Saving configuration to ananas.cfg...
[2022-04-22 10:25:31] test.config: Done.
Shutdown complete
$ cat ananas.cfg
[test]
class = test.TestBot
domain = chitter.xyz
client_id = xxxxxxxxxxxx
client_secret = yyyyyyyyyyyy
access_token = zzzzzzzzzzzz
secret = my brain IBM

configparser removes comments from config files

When ananas closes, it writes the running bots' current configuration information into the indicated config file. However, this appears to have the undesired result of removing any comments that were in the original configuration file.

This means, among other things, that you can't simply comment out the configuration of a bot you temporarily don't want to run.

Is there a way to change this so that configparser.ConfigParser preserves the comments in the original file?

Handle config.cfg files with no DEFAULT section

Currently if a config.cfg file has no DEFAULT section an ugly error is thrown. This should be handled more gracefully.

In particular the four-line config.cfg in the README fails because there is no DEFAULT section.

Gah! just discovered that this has already been fixed in 5c8a191 but apparently not pushed downstream far enough (yet).

TypeError: 'module' object is not callable

exec("from {0} import {1}; bots.append({1}('{2}', name='{3}', interactive={4}, verbose={5}))"

I've had an issue with this line, and had to change it to the following one in order to make my project working:

exec("from {0}.{1} import {1}; bots.append({1}('{2}', name='{3}', interactive={4}, verbose={5}))"

I've got a bot python file in custom/ArchiveBot.py and my config.cfg contains the following class: class = custom.ArchiveBot

With the current line exec("from {0} import {1}, it return the following error:

/home/alx/.local/bin/ananas: fatal exception loading bot archive_bot: TypeError("'module' object is not callable",)
Traceback (most recent call last):
  File "/home/alx/.local/lib/python3.6/site-packages/ananas/run.py", line 44, in main
    .format(module, botclass, args.config, bot, args.interactive, args.verbose))
  File "<string>", line 3, in <module>
TypeError: 'module' object is not callable

Urllib3 failure after a few hours running.

After running a bot or bots for a while (on a scale of days) this (extremely long, so pastebinned) error is thrown and the bots no longer respond to @replies. @shutdown functions are still called, however. May be related to psf/requests#4248 . Seems to show up when people are making GET requests with the Requests library. Might be caused by poor behavior by the mastodon server? Could be worked-around by catching this error (seems to be about a failure of a persistent connection) and reopening that connection as a result.

Is it possible to support cron divisions?

In true cron, to schedule something bi-hourly, you'd set the hours column to "*/2"

As far as I can tell, in ananas, the only way to do it is by setting

@schedule(hour=2)
@schedule(hour=4)
@schedule(hour=6)
@schedule(hour=8)
# etc...

or by using an interval of 3600*2

It'd be nice if ananas could support either */2 syntax, or at least passing lists so you could supply the hours in one statement like:

@schedule(hour=[hour for hour in range(0, 24, 2)])

Bot stops running if error reporting fails

I've been running a bot with ananas for years now and occasionally it gets stucks and stops running its interval functions anymore, I could not figure out why for the longest time, but I think I've got it: it's when an error occurs during normal function and subsequent error reporting fails.

I have an admin set in the bot's config, the bot encounters an error (say, the server is down), the default error handler tries to report the error by sending a DM to the admin, which also doesn't work because the server is down, and it goes completely silent until I restart ananas.

An example log
[2023-08-08 01:53:01] apod.accept_one_page_of_follow_requests: Exception encountered in @interval function: MastodonInternalServerError('Mastodon API returned error', 500, 'Internal Server Error', None)
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/ananas/ananas.py", line 377, in interval_threadproc
    f()
  File "/usr/local/lib/python3.10/site-packages/apod/__init__.py", line 360, in accept_one_page_of_follow_requests
    follow_requests = self.mastodon.follow_requests()
  File "/usr/local/lib/python3.10/site-packages/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/usr/local/lib/python3.10/site-packages/mastodon/utility.py", line 49, in wrapper
    return function(self, *args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/mastodon/relationships.py", line 71, in follow_requests
    return self.__api_request('GET', '/api/v1/follow_requests', params)
  File "/usr/local/lib/python3.10/site-packages/mastodon/internals.py", line 297, in __api_request
    raise ex_type('Mastodon API returned error', response_object.status_code, response_object.reason, error_msg)
mastodon.errors.MastodonInternalServerError: ('Mastodon API returned error', 500, 'Internal Server Error', None)

Exception in thread Thread-1 (interval_threadproc):
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/ananas/ananas.py", line 377, in interval_threadproc
    f()
  File "/usr/local/lib/python3.10/site-packages/apod/__init__.py", line 360, in accept_one_page_of_follow_requests
    follow_requests = self.mastodon.follow_requests()
  File "/usr/local/lib/python3.10/site-packages/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/usr/local/lib/python3.10/site-packages/mastodon/utility.py", line 49, in wrapper
    return function(self, *args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/mastodon/relationships.py", line 71, in follow_requests
    return self.__api_request('GET', '/api/v1/follow_requests', params)
  File "/usr/local/lib/python3.10/site-packages/mastodon/internals.py", line 297, in __api_request
    raise ex_type('Mastodon API returned error', response_object.status_code, response_object.reason, error_msg)
mastodon.errors.MastodonInternalServerError: ('Mastodon API returned error', 500, 'Internal Server Error', None)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.10/threading.py", line 1009, in _bootstrap_inner
    self.run()
  File "/usr/local/lib/python3.10/threading.py", line 946, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.10/site-packages/ananas/ananas.py", line 380, in interval_threadproc
    self.report_error(error, f.__name__)
  File "/usr/local/lib/python3.10/site-packages/ananas/ananas.py", line 509, in report_error
    f(error)
  File "/usr/local/lib/python3.10/site-packages/ananas/ananas.py", line 498, in default_report_handler
    self.mastodon.status_post(("@{} ERROR REPORT from {}:\n{}"
  File "/usr/local/lib/python3.10/site-packages/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/usr/local/lib/python3.10/site-packages/mastodon/utility.py", line 49, in wrapper
    return function(self, *args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/mastodon/statuses.py", line 248, in status_post
    return self.__status_internal(
  File "/usr/local/lib/python3.10/site-packages/mastodon/statuses.py", line 182, in __status_internal
    return self.__api_request('POST', '/api/v1/statuses', params, headers=headers, use_json=use_json)
  File "/usr/local/lib/python3.10/site-packages/mastodon/internals.py", line 297, in __api_request
    raise ex_type('Mastodon API returned error', response_object.status_code, response_object.reason, error_msg)
mastodon.errors.MastodonInternalServerError: ('Mastodon API returned error', 500, 'Internal Server Error', None)

Note the second exception that happens within the default error handler

Update PyPI release

The ananas version currently in PyPI doesn't include the "async->run_async" fix (b33bba8) yet, while the current version of Mastodon.py already does, resulting in errors like this:

Traceback (most recent call last):
File "/home/lordminx/.local/share/virtualenvs/glitch_art-cmsPNbAw/lib/python3.6/site-packages/ananas/run.py", line 44, in main
.format(module, botclass, args.config, bot, args.interactive, args.verbose))
File "", line 1, in
File "/home/lordminx/.local/share/virtualenvs/glitch_art-cmsPNbAw/lib/python3.6/site-packages/ananas/ananas.py", line 269, in init
self.startup()
File "/home/lordminx/.local/share/virtualenvs/glitch_art-cmsPNbAw/lib/python3.6/site-packages/ananas/ananas.py", line 332, in startup
self.stream = self.mastodon.stream_user(self, async=True)
TypeError: stream_user() got an unexpected keyword argument 'async'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.