# mastoposter - easy-to-use mastodon-to-[everything] reposter
Mastoposter is a simple zero-headache* service that forwards your toots from any
Mastodon-compatible Fediverse software (Pleroma also works\*\*!) to any of your
other services! For now it supports only Discord webhooks and Telegram, but it
can be easily extended to support pretty much anything!
## Installation
### Via pip
Since recently we have package published to pip (thanks to
[@cybertailor@deadinsi.de](https://deadinsi.de/@cybertailor) for adding
pyproject file), so now you can just do the following:
```sh
pip install mastoposter
```
Note that you would still have to clone repository to build a Docker image.
### Old way
You can run it either on your host machine, or inside a Docker container.
In any case, you have to clone that repo first in order to do anything:
```sh
git clone https://github.com/hatkidchan/mastoposter && cd mastoposter
```
After that, you can either run it in Docker, set up a standalone systemd
service, or just run it as it is!
### Docker
```sh
docker build -t mastoposter .
docker run -d \
--restart=unless-stopped \
-v /path/to/config.ini:/config.ini:ro \
--name mastoposter mastoposter
```
And you should be good to go
### Systemd
Let's say that you've cloned that repo to the `$MASTOPOSTER_ROOT`, then
configuration should look something like that:
```systemd
[Unit]
Description=Crossposter from Mastodon
After=network.target
[Service]
Type=simple
User=$MASTOPOSTER_USER
ExecStart=/usr/bin/python3 -m mastoposter config.ini
WorkingDirectory=$MASTOPOSTER_ROOT
Restart=on-failure
[Install]
WantedBy=network.target
```
Before running it though, don't forget to install dependencies from the
./requirements.txt, but it's a good idea to use a virtual environment for that.
Though, that's outside of the scope of that, so I won't cover it here.
### Running manually
Just be in the folder with it, have dependencies installed and run:
```sh
python3 -m mastoposter config.ini
```
## Configuration
Configuration file is just a regular INI file with a couple sections.
Configuration wizard is still in progress, but we have a couple examples for
common use-cases. If you have troubles configuring it yourself, you could
either use discussions feature, or ask me on Fedi directly (links on profile).
### [main]
Section `main` contains settings of your account (ie, your instance, list ID,
user ID, access token), as well as list of modules to load.
#### instance
This is your instance. It should be written without the `https://` part, so,
for example, `mastodon.social`.
#### token
This is your access token.
On Mastodon, you can acquire it by creating an application with the minimum of
`read:statuses` and `read:lists` permissions.
On Pleroma you're out of luck and have to manually lure your token out of the
frontend you're using. For example, in Pleroma FE you can look in the "Network"
tab of the devtools and look for `chats` request. Inside the request headers,
there should be `Authorization: Bearer XXXXXXXXXXX` header. That's your token.
#### user
It's still not properly tested, but you could just leave it as `auto` for now.
In case it fails, on Mastodon you can get your user ID by looking at your
profile picture URL. The part between "/avatars/" and "original/" without all of
the slashes is your user ID.
On Pleroma you're out of luck again, I don't remember how I got mine. Just hope
that "auto" will work, lol.
#### list
That's the main problem of this crossposter: it requires a list to be created
to function properly. Both Pleroma and Mastodon support them, so it shouldn't be
a big deal. Just create a list, add yourself into it and copy its ID (it should
be in the address bar).
List is required to filter incoming events. You can't just listen for home
timeline 'cause some events are not guaranteed to be there (boosts at least).
#### auto-reconnect
You can set it to either `yes` or `no`. When set to `yes`, it will reconnect
on any websocket error, but not on any error related to modules (even if it's a
connection error!!!)
#### modules
More about them later
#### loglevel
Self-explanatory, logging level. Can be either `DEBUG`, `INFO`, `WARNING` or
`ERROR`. Defaults to `INFO`
### Modules
There's two types of modules supported at this point: `telegram` and `discord`.
Both of them are self-explanatory, but we'll go over them real quick.
Each module should contain at least `type` property and its name should start
with the `module/`. `filters` field is also can be specified. Check the
corresponding section to learn more about them.
To use module, add it to the `modules` field in the `main` section. It should
not have the `module/` prefix since it's always there. You can use multiple
modules and separate them using spaces.
#### `type = telegram`
Module with that type will work in Telegram mode.
It requires your Bot token to be set in the `token` field, as well as `chat`
to be set with your chat ID. You can use `@username` if the chat is public.
Also there's a `silent` field, when it's set to `true`, it'll set
`disable_notification` flag on every post sent.
`template` field contains your template for the message. It's pretty much
Jinja2 template. Since we use `parse_mode=html`, your `template` should be
formatted appropriately. Template itself has only `status` variable exposed,
which contains the status/post/toot itself. There's also some handy properties
such as `reblog_or_status` which points to either reblog, or status itself. Or
`name_emojiless` which contains the name without emojis. Or `name` which
contains either `display_name` or `username`, if first one is empty.
#### `type = discord`
Module for Discord webhooks. The only required parameter (besides the `type`) is
`webhook`. It **should** have `wait=true` set. You can also use `thread_id` as a
GET parameter to that. You also can use filters, nothing special about that.
### Filters
Filters are the most powerful feature of this crossposter. They allow you to...
Filter out where posts should and shouldn't go! It's that easy!
There's a couple of filters with different types and options, but all of them
should be contained in sections with names starting with `filter/`, as well
as have a `type` field with filter type.
Also, you can specify multiple filters and they'll be chained together using
AND operator. You can also prefix filter name with either `~` or `!` to invert
its behavior.
#### `type = boost`
Simple filter that passes through posts that are boosted from someone.
It also has an optional `list` property where you can specify the list of
accounts to check from. You can use globbing, but be aware, that it uses
`fnmatch` function to glob stuff, so `@*` doesn't mean "any local user", but
rather it means "any user". NOTE that his behavior is not intended and may be
changed to more appropriate one later. If `list` is empty, any boost will
trigger that filter. If list is not empty, it will allow only users from that
list.
#### `type = mention`
This filter is kinda similar to the `boost` one, but works with mentions.
Also has `list` property, yada yada you got the idea, same deal with fnmatch.
#### `type = spoiler`
Matches posts with spoilers/content-warnings.
Has an optional `regexp` parameter that will allow you to specify regular
expression to match your spoiler text.
#### `type = content`
Filter to match post content against either a regular expression, or a list of
tags. Matching is done on the plaintext version of the post.
You can have one of two properties (but not both because fuck you): `tags` or
`regexp`. It's obvious what does what: if you have `regexp` set, plaintext
version of status is checked against that regular expression, and if you have
`tags` set, then only statuses that have those tags will be allowed.
Please note that in case of tags, you should NOT use `#` symbol in front of
them.
#### `type = visibility`
Simple filter that just checks for post visibility.
Has a single property `options` that is a space-separated list of allowed
visibility levels. Note that `direct` visibility is always ignored so cannot
be used here.
#### `type = media`
Filter that allows only some media types to be posted.
`valid_media` is a space-separated list of media types from Mastodon API
(`image`, `gifv`, `video`, `audio` or `unknown`). If your Fedi software has
support for other types, they also should work.
`mode` option defines the mode of operation: it can be either `include`,
`exclude` or `only`. In case of `include`, filter will trigger when post
has media with that type, but others are allowed as well. `exclude` is the
opposite: if status has media with that type, filter won't trigger. `only`
allows statuses with either no media, or listed types only.
#### `type = combined`
The most powerful filter 'cause it allows you to combine multiple filters using
different operations.
`filters` option should contain space-separated list of filters. You also can
negate them using `!` or `~` prefixes.
`operator` is a type of operation to be used. Can be either `all`, `any` or
`single`. `all` means that all of the filters should be used. `any` means
that if any filter is triggered, this one will also trigger. `single` means
that only one filter should be triggered. Think of it as an XOR operation of
some sort.
## Sample configurations
### For Telegram
```ini
[main]
modules = tg
instance = expired.mentality.rip
token = haha-no
list = 42
user = auto
[module/tg]
type = telegram
token = 12345:blahblahblah
chat = 12345
```
### For Telegram with a separate shitpost channel
```ini
[main]
modules = tg tg-shitpost
instance = expired.mentality.rip
token = haha-no
list = 42
user = auto
[module/tg]
type = telegram
token = 12345:blahblahblah
chat = 12345
filters = !shitpost
[module/tg-shitpost]
type = telegram
token = ${module/tg:token}
chat = @shitposting
filters = shitpost
[filters/shitpost]
type = content
mode = tag
tags = shitpost
```
## Asterisks
1. Well, most of the time that is.
2. Works only when it has lists support.
Raw data
{
"_id": null,
"home_page": null,
"name": "mastoposter",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "mastodon,discord,telegram",
"author": null,
"author_email": "hatkidchan <hatkidchan@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/9d/c1/e72126112b001e45a109266971fb0cddc7f7d39c42e24f81523eb2aa0191/mastoposter-0.2.tar.gz",
"platform": null,
"description": "# mastoposter - easy-to-use mastodon-to-[everything] reposter\n\nMastoposter is a simple zero-headache* service that forwards your toots from any\nMastodon-compatible Fediverse software (Pleroma also works\\*\\*!) to any of your\nother services! For now it supports only Discord webhooks and Telegram, but it\ncan be easily extended to support pretty much anything!\n\n## Installation\n\n### Via pip\n\nSince recently we have package published to pip (thanks to\n[@cybertailor@deadinsi.de](https://deadinsi.de/@cybertailor) for adding\npyproject file), so now you can just do the following:\n\n```sh\npip install mastoposter\n```\n\nNote that you would still have to clone repository to build a Docker image.\n\n### Old way\n\nYou can run it either on your host machine, or inside a Docker container.\nIn any case, you have to clone that repo first in order to do anything:\n\n```sh\ngit clone https://github.com/hatkidchan/mastoposter && cd mastoposter\n```\n\nAfter that, you can either run it in Docker, set up a standalone systemd\nservice, or just run it as it is!\n\n### Docker\n\n```sh\ndocker build -t mastoposter .\ndocker run -d \\\n --restart=unless-stopped \\\n -v /path/to/config.ini:/config.ini:ro \\\n --name mastoposter mastoposter\n```\n\nAnd you should be good to go\n\n### Systemd\n\nLet's say that you've cloned that repo to the `$MASTOPOSTER_ROOT`, then\nconfiguration should look something like that:\n\n```systemd\n[Unit]\nDescription=Crossposter from Mastodon\nAfter=network.target\n\n[Service]\nType=simple\nUser=$MASTOPOSTER_USER\nExecStart=/usr/bin/python3 -m mastoposter config.ini\nWorkingDirectory=$MASTOPOSTER_ROOT\nRestart=on-failure\n\n[Install]\nWantedBy=network.target\n```\n\nBefore running it though, don't forget to install dependencies from the\n./requirements.txt, but it's a good idea to use a virtual environment for that.\nThough, that's outside of the scope of that, so I won't cover it here.\n\n### Running manually\n\nJust be in the folder with it, have dependencies installed and run:\n\n```sh\npython3 -m mastoposter config.ini\n```\n\n## Configuration\n\nConfiguration file is just a regular INI file with a couple sections.\n\nConfiguration wizard is still in progress, but we have a couple examples for\ncommon use-cases. If you have troubles configuring it yourself, you could\neither use discussions feature, or ask me on Fedi directly (links on profile).\n\n### [main]\n\nSection `main` contains settings of your account (ie, your instance, list ID,\nuser ID, access token), as well as list of modules to load.\n\n#### instance\n\nThis is your instance. It should be written without the `https://` part, so,\nfor example, `mastodon.social`.\n\n#### token\n\nThis is your access token.\n\nOn Mastodon, you can acquire it by creating an application with the minimum of\n`read:statuses` and `read:lists` permissions.\n\nOn Pleroma you're out of luck and have to manually lure your token out of the\nfrontend you're using. For example, in Pleroma FE you can look in the \"Network\"\ntab of the devtools and look for `chats` request. Inside the request headers,\nthere should be `Authorization: Bearer XXXXXXXXXXX` header. That's your token.\n\n#### user\n\nIt's still not properly tested, but you could just leave it as `auto` for now.\n\nIn case it fails, on Mastodon you can get your user ID by looking at your\nprofile picture URL. The part between \"/avatars/\" and \"original/\" without all of\nthe slashes is your user ID.\n\nOn Pleroma you're out of luck again, I don't remember how I got mine. Just hope\nthat \"auto\" will work, lol.\n\n#### list\n\nThat's the main problem of this crossposter: it requires a list to be created\nto function properly. Both Pleroma and Mastodon support them, so it shouldn't be\na big deal. Just create a list, add yourself into it and copy its ID (it should\nbe in the address bar).\n\nList is required to filter incoming events. You can't just listen for home\ntimeline 'cause some events are not guaranteed to be there (boosts at least).\n\n#### auto-reconnect\n\nYou can set it to either `yes` or `no`. When set to `yes`, it will reconnect\non any websocket error, but not on any error related to modules (even if it's a\nconnection error!!!)\n\n#### modules\n\nMore about them later\n\n#### loglevel\n\nSelf-explanatory, logging level. Can be either `DEBUG`, `INFO`, `WARNING` or\n`ERROR`. Defaults to `INFO`\n\n### Modules\n\nThere's two types of modules supported at this point: `telegram` and `discord`.\nBoth of them are self-explanatory, but we'll go over them real quick.\n\nEach module should contain at least `type` property and its name should start\nwith the `module/`. `filters` field is also can be specified. Check the\ncorresponding section to learn more about them.\n\nTo use module, add it to the `modules` field in the `main` section. It should\nnot have the `module/` prefix since it's always there. You can use multiple\nmodules and separate them using spaces.\n\n#### `type = telegram`\n\nModule with that type will work in Telegram mode.\nIt requires your Bot token to be set in the `token` field, as well as `chat`\nto be set with your chat ID. You can use `@username` if the chat is public.\nAlso there's a `silent` field, when it's set to `true`, it'll set\n`disable_notification` flag on every post sent.\n\n`template` field contains your template for the message. It's pretty much\nJinja2 template. Since we use `parse_mode=html`, your `template` should be\nformatted appropriately. Template itself has only `status` variable exposed,\nwhich contains the status/post/toot itself. There's also some handy properties\nsuch as `reblog_or_status` which points to either reblog, or status itself. Or\n`name_emojiless` which contains the name without emojis. Or `name` which\ncontains either `display_name` or `username`, if first one is empty.\n\n#### `type = discord`\n\nModule for Discord webhooks. The only required parameter (besides the `type`) is\n`webhook`. It **should** have `wait=true` set. You can also use `thread_id` as a\nGET parameter to that. You also can use filters, nothing special about that.\n\n### Filters\n\nFilters are the most powerful feature of this crossposter. They allow you to...\n\nFilter out where posts should and shouldn't go! It's that easy!\n\nThere's a couple of filters with different types and options, but all of them\nshould be contained in sections with names starting with `filter/`, as well\nas have a `type` field with filter type.\n\nAlso, you can specify multiple filters and they'll be chained together using\nAND operator. You can also prefix filter name with either `~` or `!` to invert\nits behavior.\n\n#### `type = boost`\n\nSimple filter that passes through posts that are boosted from someone.\n\nIt also has an optional `list` property where you can specify the list of\naccounts to check from. You can use globbing, but be aware, that it uses\n`fnmatch` function to glob stuff, so `@*` doesn't mean \"any local user\", but\nrather it means \"any user\". NOTE that his behavior is not intended and may be\nchanged to more appropriate one later. If `list` is empty, any boost will\ntrigger that filter. If list is not empty, it will allow only users from that\nlist.\n\n#### `type = mention`\n\nThis filter is kinda similar to the `boost` one, but works with mentions.\nAlso has `list` property, yada yada you got the idea, same deal with fnmatch.\n\n#### `type = spoiler`\n\nMatches posts with spoilers/content-warnings.\n\nHas an optional `regexp` parameter that will allow you to specify regular\nexpression to match your spoiler text.\n\n#### `type = content`\n\nFilter to match post content against either a regular expression, or a list of\ntags. Matching is done on the plaintext version of the post.\n\nYou can have one of two properties (but not both because fuck you): `tags` or\n`regexp`. It's obvious what does what: if you have `regexp` set, plaintext\nversion of status is checked against that regular expression, and if you have\n`tags` set, then only statuses that have those tags will be allowed.\n\nPlease note that in case of tags, you should NOT use `#` symbol in front of\nthem.\n\n#### `type = visibility`\n\nSimple filter that just checks for post visibility.\nHas a single property `options` that is a space-separated list of allowed\nvisibility levels. Note that `direct` visibility is always ignored so cannot\nbe used here.\n\n#### `type = media`\n\nFilter that allows only some media types to be posted.\n\n`valid_media` is a space-separated list of media types from Mastodon API\n(`image`, `gifv`, `video`, `audio` or `unknown`). If your Fedi software has\nsupport for other types, they also should work.\n\n`mode` option defines the mode of operation: it can be either `include`,\n`exclude` or `only`. In case of `include`, filter will trigger when post\nhas media with that type, but others are allowed as well. `exclude` is the\nopposite: if status has media with that type, filter won't trigger. `only`\nallows statuses with either no media, or listed types only.\n\n#### `type = combined`\n\nThe most powerful filter 'cause it allows you to combine multiple filters using\ndifferent operations.\n\n`filters` option should contain space-separated list of filters. You also can\nnegate them using `!` or `~` prefixes.\n\n`operator` is a type of operation to be used. Can be either `all`, `any` or\n`single`. `all` means that all of the filters should be used. `any` means\nthat if any filter is triggered, this one will also trigger. `single` means\nthat only one filter should be triggered. Think of it as an XOR operation of\nsome sort.\n\n## Sample configurations\n\n### For Telegram\n\n```ini\n[main]\nmodules = tg\ninstance = expired.mentality.rip\ntoken = haha-no\nlist = 42\nuser = auto\n\n[module/tg]\ntype = telegram\ntoken = 12345:blahblahblah\nchat = 12345\n```\n\n### For Telegram with a separate shitpost channel\n\n```ini\n[main]\nmodules = tg tg-shitpost\ninstance = expired.mentality.rip\ntoken = haha-no\nlist = 42\nuser = auto\n\n[module/tg]\ntype = telegram\ntoken = 12345:blahblahblah\nchat = 12345\nfilters = !shitpost\n\n[module/tg-shitpost]\ntype = telegram\ntoken = ${module/tg:token}\nchat = @shitposting\nfilters = shitpost\n\n[filters/shitpost]\ntype = content\nmode = tag\ntags = shitpost\n```\n\n## Asterisks\n\n1. Well, most of the time that is.\n2. Works only when it has lists support.\n",
"bugtrack_url": null,
"license": null,
"summary": "Configurable reposter from Mastodon-compatible Fediverse servers",
"version": "0.2",
"project_urls": {
"Source": "https://github.com/hatkidchan/mastoposter"
},
"split_keywords": [
"mastodon",
"discord",
"telegram"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "732bddfc78f91ba6ff87ae922ec3eb96ec1816b62ea055b17fa8f91e2c82d55b",
"md5": "7c9ee7be673e2d2e46f6f40595b2b40c",
"sha256": "a85a1b237781352a227417ec35f4cbe065408a7b6046cb32df17ed1566a84fdc"
},
"downloads": -1,
"filename": "mastoposter-0.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "7c9ee7be673e2d2e46f6f40595b2b40c",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 47948,
"upload_time": "2023-06-16T21:55:18",
"upload_time_iso_8601": "2023-06-16T21:55:18.191761Z",
"url": "https://files.pythonhosted.org/packages/73/2b/ddfc78f91ba6ff87ae922ec3eb96ec1816b62ea055b17fa8f91e2c82d55b/mastoposter-0.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "9dc1e72126112b001e45a109266971fb0cddc7f7d39c42e24f81523eb2aa0191",
"md5": "933482fb5717736ab735d6b5cb0eb81d",
"sha256": "8bae02cc98d34785940d20a4433018c99be0b753253e4fcf48c26f224f5e6d8c"
},
"downloads": -1,
"filename": "mastoposter-0.2.tar.gz",
"has_sig": false,
"md5_digest": "933482fb5717736ab735d6b5cb0eb81d",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 39036,
"upload_time": "2023-06-16T21:55:20",
"upload_time_iso_8601": "2023-06-16T21:55:20.650533Z",
"url": "https://files.pythonhosted.org/packages/9d/c1/e72126112b001e45a109266971fb0cddc7f7d39c42e24f81523eb2aa0191/mastoposter-0.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-06-16 21:55:20",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "hatkidchan",
"github_project": "mastoposter",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [
{
"name": "anyio",
"specs": [
[
"==",
"3.6.1"
]
]
},
{
"name": "beautifulsoup4",
"specs": [
[
"==",
"4.11.1"
]
]
},
{
"name": "bs4",
"specs": [
[
"==",
"0.0.1"
]
]
},
{
"name": "certifi",
"specs": [
[
"==",
"2022.6.15"
]
]
},
{
"name": "emoji",
"specs": [
[
"==",
"2.0.0"
]
]
},
{
"name": "h11",
"specs": [
[
"==",
"0.12.0"
]
]
},
{
"name": "httpcore",
"specs": [
[
"==",
"0.15.0"
]
]
},
{
"name": "httpx",
"specs": [
[
"==",
"0.23.0"
]
]
},
{
"name": "idna",
"specs": [
[
"==",
"3.3"
]
]
},
{
"name": "Jinja2",
"specs": [
[
"==",
"3.1.2"
]
]
},
{
"name": "lxml",
"specs": [
[
"==",
"4.9.1"
]
]
},
{
"name": "MarkupSafe",
"specs": [
[
"==",
"2.1.1"
]
]
},
{
"name": "rfc3986",
"specs": [
[
"==",
"1.5.0"
]
]
},
{
"name": "sniffio",
"specs": [
[
"==",
"1.2.0"
]
]
},
{
"name": "soupsieve",
"specs": [
[
"==",
"2.3.2.post1"
]
]
},
{
"name": "websockets",
"specs": [
[
"==",
"10.3"
]
]
}
],
"lcname": "mastoposter"
}