|
|
# A reactive bot framework for Pleroma
|
|
|
|
|
|
## What is this?
|
|
|
|
|
|
This is a modification of Pleroma to support reactive server-side bots. By _reactive_ I mean they react when you talk to them, they don't push content to you uninvited. Examples are my pixelbot which lets you paint pixels on a shared canvas, my pollbot, a work in progress to support fedivers polls, or a bot to translate content or to let you bookmark content. Counter-examples are e.g. a followbot or a timeline scraper bot, as these are not reacting to posts from users.
|
|
|
|
|
|
## How does it work?
|
|
|
|
|
|
Every bot is implemented as an asynchronous worker process. It receives a message, parses it and reacts to it by modifying its own state and/or replying to the received message.
|
|
|
|
|
|
## How do I use it?
|
|
|
|
|
|
- I recommend that you put your main bot code in `lib/pleroma/bots/yourbot.ex` an helper code in `lib/pleroma/bots/yourbot/`, with the namespace `Pleroma.Bots.YourBot`.
|
|
|
- In `config/config.exs`, enable support for the bot as follows:
|
|
|
|
|
|
```elixir
|
|
|
config :pleroma, bots: [ :pixebot, :yourbot ]
|
|
|
config :pleroma, :yourbot,
|
|
|
module: Pleroma.Bots.YourBot
|
|
|
# add specific config info (atom, value pairs) for your bot
|
|
|
|
|
|
# pixelbot uses a canvas with a configurable size
|
|
|
config :pleroma, :pixelbot,
|
|
|
module: Pleroma.Bots.PixelBot,
|
|
|
canvas_size: {32,32}
|
|
|
```
|
|
|
|
|
|
## Example: hellobot
|
|
|
|
|
|
This is a trivial bot which replies to any message with "Hello" and the nickname of the sender.
|
|
|
|
|
|
```elixir
|
|
|
# WV: server for hellobot.
|
|
|
# hellobot has no state, let's give it an empty list
|
|
|
defmodule Pleroma.Bots.HelloBot do
|
|
|
use GenServer
|
|
|
|
|
|
alias Pleroma.Bots.Activity
|
|
|
alias Pleroma.Bots.Common
|
|
|
# add your own declarations here if required
|
|
|
|
|
|
# Starts a GenServer server process.
|
|
|
# Once the server is started, the init/1 function is called with `state` as its arguments to initialize the server.
|
|
|
# This function does not return until init/1 has returned.
|
|
|
# Normally you don't have to change this.
|
|
|
def start_link(state) do
|
|
|
GenServer.start_link(__MODULE__, state, name: __MODULE__)
|
|
|
end
|
|
|
|
|
|
# I ignore the argument, hence the `_`
|
|
|
# If your init takes an argument, it would be `def init(arg) do yourbot_init(arg) end`
|
|
|
def init(_) do
|
|
|
hellobot_init()
|
|
|
end
|
|
|
|
|
|
# Normally you don't have to change this apart from the name of your main function.
|
|
|
def handle_cast( msg, state) do
|
|
|
hellobot_main( msg, state )
|
|
|
end
|
|
|
|
|
|
# Implement this if you want a synchronous bot
|
|
|
#def handle_call(msg, _from, state) do
|
|
|
## See https://hexdocs.pm/elixir/GenServer.html#c:handle_call/3
|
|
|
#end
|
|
|
|
|
|
## These are the actual functions implementing the bot
|
|
|
|
|
|
defp hellobot_init() do
|
|
|
{:ok, [] }
|
|
|
end
|
|
|
|
|
|
defp hellobot_main( msg, state ) do
|
|
|
{status,msg_content} = Activity.get_content(msg)
|
|
|
# Usually you will want to parse `msg_content` so your bot can react based on the message.
|
|
|
# The hellobot ignores the message. Note that this works with empty messages too.
|
|
|
if status do
|
|
|
hellobot_post_status(msg)
|
|
|
{:noreply, state}
|
|
|
else
|
|
|
{:noreply, state}
|
|
|
end
|
|
|
end
|
|
|
|
|
|
defp hellobot_post_status(received_activity) do
|
|
|
# Usually you want to reply to the sender
|
|
|
{_,sender} = Activity.get_actor(received_activity)
|
|
|
# `sender` is a url with the nickname as the last part
|
|
|
sender_nickname = List.last( String.split(sender,"/") )
|
|
|
content = "Hello, "<>sender_nickname<>"!"
|
|
|
# This will return the visibility of the sender's post (direct, followers-only aka private, unlisted, public)
|
|
|
{_,visibility} = Activity.get_visibility(received_activity["object"])
|
|
|
|
|
|
# You can provide your own visibility but normally you would inherit the visibility from the sender
|
|
|
# The other fields in `status_details` are `attachment:`, discussed below,
|
|
|
# and `to:` and `cc:` which you can used instead of `visibility:`.
|
|
|
status_details=%{
|
|
|
content: content,
|
|
|
visibility: visibility
|
|
|
}
|
|
|
Common.bot_post_status(received_activity,status_details)
|
|
|
end
|
|
|
|
|
|
end
|
|
|
```
|
|
|
|
|
|
## Content
|
|
|
|
|
|
The content you provide can contain some limited HTML tags (e.g. `<br>` and `<a>`), but the HTML is sanitized by the Pleroma server so full HTML is not supported. For example, pixelbot has the following content:
|
|
|
|
|
|
```elixir
|
|
|
content = "Canvas at "<>now<>"<br><a href=\"https://pynq.limited.systems/pixelbot/canvas_512x512.png\" class='attachment'>canvas.png</a>"
|
|
|
```
|
|
|
|
|
|
## Attachments
|
|
|
|
|
|
|
|
|
The structure of the attachment to be provided in the `attachment:` field of the status details to be posted is specified as follows:
|
|
|
|
|
|
```elixir
|
|
|
attachments= [
|
|
|
%{"name" => name_string, "type" => "Image",
|
|
|
"url" => [%{"href" => url_string,
|
|
|
"mediaType" => "image/png", "type" => "Link"}],
|
|
|
"uuid" => uuid_string
|
|
|
},...
|
|
|
]
|
|
|
```
|
|
|
|
|
|
## Message parsing
|
|
|
|
|
|
I have provided two helper functions for parsing the incoming messages:
|
|
|
|
|
|
- `Pleroma.Bots.Parser.strip_tags(msg_str)` strips all HTML tags
|
|
|
- `Pleroma.Bots.Parser.replace_entities(msg_str)` replaces all HTML entities with their corresponding characters.
|
|
|
|
|
|
## API Overview
|
|
|
|
|
|
In module `Pleroma.Bots.Common` (`bots_common.ex`), `bot_post_status` takes the original received message and the status details as discussed above, i.e. a map with fields `:attachment`, `:content` and `:visibility` or `:to` and `:cc`.
|
|
|
|
|
|
```elixir
|
|
|
bot_post_status(orig_msg,status_details)
|
|
|
```
|
|
|
|
|
|
Module Pleroma.Bots.Activity (`bots_activity.ex`) contains functions to get certain field from an ActivityPub message. Most of the names are self-explanitary. The _object_ refers to the internal ActivityPub object inside a message; _context_ and _conversation_ are urls relating to the origin of the received message, usually you just pass these on as-is to indicate that your message is part of a thread.
|
|
|
|
|
|
```elixir
|
|
|
get_nickname(params)
|
|
|
get_object(params)
|
|
|
get_content(activity)
|
|
|
get_object_id(activity)
|
|
|
get_attachments(activity)
|
|
|
get_attachment_urls(activity)
|
|
|
get_actor(params)
|
|
|
get_conversation(activity)
|
|
|
get_context(activity)
|
|
|
get_visibility(activity)
|
|
|
get_sender(activity)
|
|
|
```
|
|
|
|
|
|
Module Pleroma.Bots.Parser (`bots_parser.ex`) provides convenience functions for parsing the incoming message.
|
|
|
|
|
|
```elixir
|
|
|
strip_tags(msg)
|
|
|
replace_entities(str)
|
|
|
```
|
|
|
|
|
|
|
|
|
|