diff --git a/Dockerfile b/Dockerfile index 173ec93..59d4f22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,9 @@ RUN pip install -r requirements.txt VOLUME /app/config -COPY ./config /app +RUN mkdir -p /app/config/ +RUN mkdir -p /app/secrets/ + +COPY ./config/* /app/config/ ENTRYPOINT ["python", "/app/main.py"] \ No newline at end of file diff --git a/README.md b/README.md index c2fb26a..5fa011f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ ![](./res/hype_header.png) + # hype + This Mastodon bot transfers the trends from other instances directly to your personal timeline. You decide which instances it fetches and how much you want to see per instance. ## Why -I am hosting my own mastodon instance and my server is very small (~2 active users). This is why trends simply do not work on my instance. There is just not enough activity. I used to open up the explore-pages of other interesting mastodon instances once per day to discover interesting topics and posts beyond my subscriptions. But that is a bit tedious in the long run. One afternoon I decided to write a bot for this issue and here we are :tada: + +For smaller instances the local timeline is rather empty. This is why trends simply do not work on those instances: There is just not enough activity. Instead of manually checking out other instances this bot allows to subscribe to a multitude of different mastodon compatible servers to fetch trending posts and repost them to your current server helping discoverability of accounts, people and topics within the federated social web. ## Installation + Deploy with docker-compose ```yaml @@ -17,11 +21,11 @@ services: - ./config:/app/config ``` - - ## Configuration -Create a `config.yaml` file in `./config/` and enter the credentials of your bot-account. Also define how often the bot should run. See the example below: +Create a `config.yaml` and a `auth.yaml` file in `./config/`. Enter the credentials of your bot-account into `auth.yaml`. You can define which servers to follow and how often to fetch new posts as well as how to automatically change your profile in config.yaml. See the examples below: + +`auth.yaml`: ```yaml # Credentials for your bot account @@ -29,11 +33,23 @@ bot_account: server: "mastodon.example.com" email: "hypebot@example.com" password: "averylongandsecurepassword" +``` + +`config.yaml` +```yaml # Refresh interval in minutes interval: 60 -# Define subscribed instances and +# Text to add to the bot profile befor the list of subscribed servers +profile_prefix: "I am boosting trending posts from:" + +# profile fields to fill in +fields: + code: https://github.com/tante/hype + operator: "YOUR HANDLE HERE" + +# Define subscribed instances and # their individual limit (top n trending posts) # which is again limited by the API to max 20 subscribed_instances: @@ -49,5 +65,3 @@ subscribed_instances: - Update bot profile with list of subscribed instances --- - -Hype on Mastodon diff --git a/config/auth.yaml b/config/auth.yaml new file mode 100644 index 0000000..e808ae2 --- /dev/null +++ b/config/auth.yaml @@ -0,0 +1,5 @@ +# Credentials for your bot account +bot_account: + server: "" + email: "" + password: "" diff --git a/config/config.yaml b/config/config.yaml index 9015b65..c7cc585 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,17 +1,28 @@ -# Credentials for your bot account -bot_account: - server: "" - email: "" - password: "" - # Refresh interval in minutes interval: 60 -# Define subscribed instances and +# Text to add to the bot profile befor the list of subscribed servers +profile_prefix: "I am boosting trending posts from:" + +# profile fields to fill in +fields: + code: https://github.com/v411e/hype + operator: "YOUR HANDLE HERE" + +# Define subscribed instances and # their individual limit (top n trending posts) # which is again limited by the API to max 20 subscribed_instances: chaos.social: - limit: 20 + limit: 5 mastodon.social: - limit: 5 \ No newline at end of file + limit: 5 + +# Posts originating from filtered instances will never be reposted. +# The filter checks for the instance of the original posting account, not the +# server that marked it as a popular post. +# This can be used to filter out abusive instances as well as protect small +# instances who could be overwhelmed with a repost to a significant amount of +# other instances +filtered_instances: + - example.com \ No newline at end of file diff --git a/hype/config.py b/hype/config.py index 314a730..925a0cc 100644 --- a/hype/config.py +++ b/hype/config.py @@ -1,5 +1,6 @@ from typing import List -import yaml, logging +import yaml +import logging class BotAccount: @@ -33,13 +34,20 @@ class Config: interval: int = 60 # minutes log_level: str = "INFO" subscribed_instances: List = [] + filtered_instances: List = [] + profile_prefix: str = "" + fields: dict = {} def __init__(self): - path = "/app/config/config.yaml" - # path = "../config/config-prod.yaml" - with open(path, "r") as configfile: + # auth file containing login info + auth = "/app/config/auth.yaml" + # settings file containing subscriptions + conf = "/app/config/config.yaml" + + # only load auth info + with open(auth, "r") as configfile: config = yaml.load(configfile, Loader=yaml.Loader) - logging.getLogger("Config").debug("Load config") + logging.getLogger("Config").debug("Loading auth info") if ( config and config.get("bot_account") @@ -55,20 +63,46 @@ def __init__(self): else: logging.getLogger("Config").error(config) raise ConfigException("Bot account config is incomplete or missing.") - self.interval = ( - config["interval"] if config.get("interval") else self.interval - ) - self.log_level = ( - config["log_level"] if config.get("log_level") else self.log_level - ) - self.subscribed_instances = ( - [ - Instance(name, props["limit"]) - for name, props in config["subscribed_instances"].items() - ] - if config.get("subscribed_instances") - else [] - ) + + with open(conf, "r") as configfile: + config = yaml.load(configfile, Loader=yaml.Loader) + logging.getLogger("Config").debug("Loading settings") + if config: + self.interval = ( + config["interval"] if config.get("interval") else self.interval + ) + self.log_level = ( + config["log_level"] if config.get("log_level") else self.log_level + ) + + self.profile_prefix = ( + config["profile_prefix"] + if config.get("profile_prefix") + else self.profile_prefix + ) + + self.fields = ( + {name: value for name, value in config["fields"].items()} + if config.get("fields") + else {} + ) + + self.subscribed_instances = ( + [ + Instance(name, props["limit"]) + for name, props in config["subscribed_instances"].items() + ] + if config.get("subscribed_instances") + else [] + ) + + self.filtered_instances = ( + [ + name for name in config["filtered_instances"] + ] + if config.get("filtered_instances") + else [] + ) class ConfigException(Exception): diff --git a/hype/hype.py b/hype/hype.py index 1bcfeed..16f3977 100644 --- a/hype/hype.py +++ b/hype/hype.py @@ -1,4 +1,6 @@ -import time, schedule, time, logging +import time +import schedule +import logging from mastodon import Mastodon from config import Config import os.path @@ -29,10 +31,10 @@ def update_profile(self): subscribed_instances_list = "\n".join( [f"- {instance}" for instance in self.config.subscribed_instances] ) - note = f"""I am boosting trending posts from: + note = f"""{self.config.profile_prefix} {subscribed_instances_list} """ - fields = [("Github", "https://github.com/v411e/hype")] + fields = [(key, value) for key, value in self.config.fields.items()] self.client.account_update_credentials( note=note, bot=True, discoverable=True, fields=fields ) @@ -50,18 +52,22 @@ def boost(self): counter = 0 for trending_status in trending_statuses: counter += 1 - # Get snowflake-id of status on the instance where the status will be boosted + # Get snowflake-id of status on the instance where the status will be boosted # noqa: E501 status = self.client.search_v2( trending_status["uri"], result_type="statuses" )["statuses"] if len(status) > 0: status = status[0] + # check if post comes from a filtered instance + source_account = status["account"]["acct"].split("@") + server = source_account[-1] + filtered = server in self.config.filtered_instances # Boost if not already boosted already_boosted = status["reblogged"] - if not already_boosted: + if not already_boosted and not filtered: self.client.status_reblog(status) self.log.info( - f"{instance.name}: {counter}/{len(trending_statuses)} {'ignore' if already_boosted else 'boost'}" + f"{instance.name}: {counter}/{len(trending_statuses)} {'ignore' if (already_boosted or filtered) else 'boost'}" ) else: self.log.warning(