mirror of
https://github.com/aserper/masto-rss.git
synced 2025-12-17 13:25:25 +00:00
Compare commits
21 Commits
chore/swit
...
patch-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4e5a22a0f | ||
|
|
fc5a4bec00 | ||
|
|
6ff72f7f97 | ||
|
|
c42d230053 | ||
|
|
22677de465 | ||
|
|
760d84d104 | ||
|
|
faf443b5b7 | ||
|
|
f795520ad6 | ||
|
|
6ae6978d68 | ||
|
|
4c043a596b | ||
|
|
d3c02fddac | ||
|
|
0860fa555d | ||
|
|
7a16534593 | ||
|
|
b4810530ab | ||
|
|
83fe193d67 | ||
|
|
6e947528cb | ||
|
|
2e11ea076a | ||
|
|
43c9ab7bdb | ||
|
|
ec657cb5af | ||
|
|
a96fcf9ca6 | ||
|
|
8ef903a720 |
23
.dockerignore
Normal file
23
.dockerignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Git-related files
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Docker-related files
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# Python-specific files and directories
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# IDE / Editor-specific directories
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
25
.github/workflows/docker-publish.yml
vendored
Normal file
25
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Push to Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/masto-rss-advanced:latest
|
||||
20
.github/workflows/masto-rss.yml
vendored
20
.github/workflows/masto-rss.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: Masto-rss
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Build & Push Image
|
||||
run: |
|
||||
echo "${{ secrets.DH_PASSWORD }}" | docker login -u "amitserper" --password-stdin
|
||||
docker build -t amitserper/masto-rss .
|
||||
docker push amitserper/masto-rss
|
||||
25
Dockerfile
25
Dockerfile
@@ -1,17 +1,22 @@
|
||||
# Use an appropriate base image with Python pre-installed
|
||||
FROM alpine:3.18
|
||||
# Step 1: Use an official, slim Python image as a base
|
||||
# 'slim' is a good balance between size and functionality.
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the entire current directory into the container at /app
|
||||
COPY . /app
|
||||
# Step 2: Copy only the requirements file
|
||||
# This makes optimal use of Docker's layer caching.
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install any Python dependencies
|
||||
# Step 3: Install the dependencies
|
||||
# The --no-cache-dir option keeps the image smaller.
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
RUN apk add python3
|
||||
RUN apk add py3-pip
|
||||
RUN pip install -r requirements.txt
|
||||
# Step 4: Now, copy the rest of the application code
|
||||
# Because the code changes more often than the dependencies, this step is placed later.
|
||||
COPY . .
|
||||
|
||||
# Run Python script
|
||||
CMD ["python", "main.py"]
|
||||
# Step 5: Execute the correct Python script
|
||||
# Note the correct filename.
|
||||
CMD ["python3", "main.py"]
|
||||
|
||||
162
README.md
162
README.md
@@ -1,20 +1,158 @@
|
||||
# Masto-rss
|
||||
# Masto-RSS Advanced Bot
|
||||
|
||||
A simple Mastodon bot written in python that posts updates from an RSS feed to a Mastodon account.
|
||||
This project is meant to be built to a docker container, so all of the options need to be set as environment variables:
|
||||
A flexible and robust Python script to automatically post the latest entry from an RSS feed to Mastodon, designed to run continuously in a Docker container.
|
||||
|
||||
MASTODON_CLIENT_ID = Mastodon client ID
|
||||
This project is based on the original concept of [masto-rss](https://github.com/aserper/masto-rss) but has been significantly rewritten and extended with modern features for stability and customization.
|
||||
|
||||
MASTODON_CLIENT_SECRET = Mastodon client secret
|
||||
## Features
|
||||
|
||||
MASTODON_ACCESS_TOKEN = Mastodon access token
|
||||
* **Posts Only the Latest Item**: Checks an RSS feed periodically and posts only the single most recent entry, preventing feed spam.
|
||||
* **Prevents Duplicates**: Keeps a history of posted items to ensure the same link is never posted twice.
|
||||
* **History Limit**: The history file is automatically pruned to a configurable size (`MAX_HISTORY_SIZE`) to prevent it from growing indefinitely.
|
||||
* **Custom Toot Format**: Fully customize the look of your posts using a template string with placeholders (`{title}`, `{link}`, `{hashtags}`).
|
||||
* **Hashtag Support**: Automatically append a configurable list of hashtags to every post.
|
||||
* **Dockerized**: Runs as a pre-built container image for easy, reliable, and isolated deployment. You don't need to manage Python or dependencies.
|
||||
* **Robust Error Handling**: Designed to run forever, it handles connection errors to the RSS feed or Mastodon gracefully without crashing.
|
||||
|
||||
MASTODON_INSTANCE_URL = Mastodon instance URL
|
||||
## Getting Started
|
||||
|
||||
RSS_FEED_URL = URL of RSS/xml feed
|
||||
The recommended and simplest way to run this bot is by using the pre-built Docker image from Docker Hub with Docker Compose.
|
||||
|
||||
TOOT_VISIBILITY = 'public', 'unlisted', 'private', or 'direct'
|
||||
### 1. Prerequisites
|
||||
|
||||
The best way to use this project is by using [its docker container](https://hub.docker.com/r/amitserper/masto-rss)
|
||||
When using docker, make a bind mount between /state on the container to whatever directory you want on your machine in order to keep the state of the feeds that were already posted
|
||||

|
||||
* Docker and Docker Compose installed on your system.
|
||||
* A Mastodon account and an application set up via **Preferences > Development**. You will need the **Client Key**, **Client Secret**, and **Access Token**.
|
||||
|
||||
### 2. Configuration
|
||||
|
||||
You only need to create one file: `docker-compose.yml`.
|
||||
|
||||
First, create a directory for your project and navigate into it:
|
||||
|
||||
```bash
|
||||
mkdir my-masto-bot
|
||||
cd my-masto-bot
|
||||
```
|
||||
|
||||
Next, create a file named `docker-compose.yml` and paste the following content into it. **You must edit the `environment` section with your own credentials and settings.**
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
masto-rss:
|
||||
# Use the pre-built image from Docker Hub
|
||||
image: doorknob2947/masto-rss-advanced:latest
|
||||
container_name: masto-rss-bot
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# This volume persists the history of posted links
|
||||
- ./state:/state
|
||||
environment:
|
||||
# --- Mastodon API Credentials (Required) ---
|
||||
- MASTODON_CLIENT_ID=YOUR_CLIENT_KEY_HERE
|
||||
- MASTODON_CLIENT_SECRET=YOUR_CLIENT_SECRET_HERE
|
||||
- MASTODON_ACCESS_TOKEN=YOUR_ACCESS_TOKEN_HERE
|
||||
- MASTODON_INSTANCE_URL=[https://mastodon.social](https://mastodon.social)
|
||||
|
||||
# --- RSS Feed Configuration (Required) ---
|
||||
- RSS_FEED_URL=[https://www.theverge.com/rss/index.xml](https://www.theverge.com/rss/index.xml)
|
||||
|
||||
# --- Bot Behavior (Optional) ---
|
||||
- CHECK_INTERVAL=3600 # Time in seconds between checks (e.g., 1 hour)
|
||||
- MAX_HISTORY_SIZE=500 # Max number of posted links to remember
|
||||
- TOOT_VISIBILITY=public # public, unlisted, private, or direct
|
||||
|
||||
# --- Toot Content (Optional) ---
|
||||
- MASTO_RSS_HASHTAGS="news tech python" # Space-separated list of hashtags
|
||||
- TOOT_TEMPLATE='{title}\n\n{link}\n\n{hashtags}'
|
||||
|
||||
# --- System (Do not change) ---
|
||||
- PYTHONUNBUFFERED=1
|
||||
```
|
||||
|
||||
### 3. Environment Variables
|
||||
|
||||
All configuration is handled through the `environment` section in your `docker-compose.yml` file.
|
||||
|
||||
| Variable | Description | Example |
|
||||
| ------------------------ | ------------------------------------------------------------------------ | --------------------------------------------------- |
|
||||
| `MASTODON_CLIENT_ID` | Your Mastodon application's Client Key. | `abc...` |
|
||||
| `MASTODON_CLIENT_SECRET` | Your Mastodon application's Client Secret. | `def...` |
|
||||
| `MASTODON_ACCESS_TOKEN` | Your Mastodon application's Access Token. | `ghi...` |
|
||||
| `MASTODON_INSTANCE_URL` | The base URL of your Mastodon instance. | `https://mastodon.social` |
|
||||
| `RSS_FEED_URL` | The full URL of the RSS feed you want to monitor. | `https://www.theverge.com/rss/index.xml` |
|
||||
| `CHECK_INTERVAL` | (Optional) The time in seconds between checks. | `3600` (for 1 hour) |
|
||||
| `MASTO_RSS_HASHTAGS` | (Optional) A space-separated list of hashtags to add to each post. | `news tech python` |
|
||||
| `MAX_HISTORY_SIZE` | (Optional) The maximum number of post URLs to remember. | `500` |
|
||||
| `TOOT_VISIBILITY` | (Optional) The visibility of the toot (`public`, `unlisted`, `private`, `direct`). | `public` |
|
||||
| `TOOT_TEMPLATE` | (Optional) A string to format the toot. See "Customizing the Toot Format" below. | `'{title}\n\n{link}\n\n{hashtags}'` |
|
||||
| `PYTHONUNBUFFERED` | Should be kept at `1` to ensure logs appear in real-time in Docker. | `1` |
|
||||
|
||||
### 4. Customizing the Toot Format
|
||||
|
||||
You can change the layout of your posts using the `TOOT_TEMPLATE` variable. Use the following placeholders:
|
||||
|
||||
* `{title}`: The title of the RSS entry.
|
||||
* `{link}`: The URL of the RSS entry.
|
||||
* `{hashtags}`: The configured hashtags.
|
||||
|
||||
**Examples in `docker-compose.yml`:**
|
||||
|
||||
* **Compact Format:**
|
||||
```yaml
|
||||
- TOOT_TEMPLATE='{title} - {link} {hashtags}'
|
||||
```
|
||||
* **Personalized Format:**
|
||||
```yaml
|
||||
- TOOT_TEMPLATE='New on the blog: {title}\nRead it here: {link}\n\n{hashtags}'
|
||||
```
|
||||
|
||||
## Running the Bot
|
||||
|
||||
1. **Create the `state` directory.** This is required for the bot to remember which links it has already posted.
|
||||
```bash
|
||||
mkdir ./state
|
||||
```
|
||||
|
||||
2. **Start the container** in detached (background) mode:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
The bot is now running!
|
||||
|
||||
## Managing the Bot
|
||||
|
||||
* **View logs in real-time:**
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
* **Stop the container:**
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
* **Restart the container:**
|
||||
```bash
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
## Updating the Bot
|
||||
|
||||
To update to the latest version of the image from Docker Hub:
|
||||
|
||||
1. **Pull the latest image:**
|
||||
```bash
|
||||
docker-compose pull
|
||||
```
|
||||
2. **Restart the container** to apply the update:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
* This project is heavily inspired by and based on the original [masto-rss](https://github.com/aserper/masto-rss). This version aims to add more robustness, flexibility, and ease of deployment using modern practices.
|
||||
* The Python script, Docker configuration, and this README were written and modified with the assistance of AI.
|
||||
|
||||
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
masto-rss:
|
||||
# Use the pre-built image from Docker Hub
|
||||
image: doorknob2947/masto-rss-advanced:latest
|
||||
container_name: masto-rss-bot
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# This volume persists the history of posted links
|
||||
- ./state:/state
|
||||
environment:
|
||||
# --- Mastodon API Credentials (Required) ---
|
||||
- MASTODON_CLIENT_ID=YOUR_CLIENT_KEY_HERE
|
||||
- MASTODON_CLIENT_SECRET=YOUR_CLIENT_SECRET_HERE
|
||||
- MASTODON_ACCESS_TOKEN=YOUR_ACCESS_TOKEN_HERE
|
||||
- MASTODON_INSTANCE_URL=[https://mastodon.social](https://mastodon.social)
|
||||
|
||||
# --- RSS Feed Configuration (Required) ---
|
||||
- RSS_FEED_URL=[https://www.theverge.com/rss/index.xml](https://www.theverge.com/rss/index.xml)
|
||||
|
||||
# --- Bot Behavior (Optional) ---
|
||||
- CHECK_INTERVAL=3600 # Time in seconds between checks (e.g., 1 hour)
|
||||
- MAX_HISTORY_SIZE=500 # Max number of posted links to remember
|
||||
- TOOT_VISIBILITY=public # public, unlisted, private, or direct
|
||||
|
||||
# --- Toot Content (Optional) ---
|
||||
- MASTO_RSS_HASHTAGS="news tech python" # Space-separated list of hashtags
|
||||
- TOOT_TEMPLATE='{title}\n\n{link}\n\n{hashtags}'
|
||||
|
||||
# --- System (Do not change) ---
|
||||
- PYTHONUNBUFFERED=1
|
||||
135
main.py
135
main.py
@@ -2,73 +2,130 @@ import feedparser
|
||||
from mastodon import Mastodon
|
||||
import os
|
||||
import time
|
||||
# Replace these with your Mastodon application details and access token
|
||||
MASTODON_CLIENT_ID = os.environ['MASTODON_CLIENT_ID']
|
||||
MASTODON_CLIENT_SECRET = os.environ['MASTODON_CLIENT_SECRET']
|
||||
MASTODON_ACCESS_TOKEN = os.environ['MASTODON_ACCESS_TOKEN']
|
||||
MASTODON_INSTANCE_URL = os.environ['MASTODON_INSTANCE_URL']
|
||||
TOOT_VISIBILITY = os.environ['TOOT_VISIBILITY'] # Toot visibility ('public', 'unlisted', 'private', or 'direct')
|
||||
# RSS feed URL
|
||||
RSS_FEED_URL = os.environ['RSS_FEED_URL']
|
||||
from collections import deque
|
||||
|
||||
# File to store the processed entry URLs. Note that /state directory is for the docker setup
|
||||
# --- Configuration ---
|
||||
# Get configuration from environment variables
|
||||
MASTODON_CLIENT_ID = os.environ.get('MASTODON_CLIENT_ID')
|
||||
MASTODON_CLIENT_SECRET = os.environ.get('MASTODON_CLIENT_SECRET')
|
||||
MASTODON_ACCESS_TOKEN = os.environ.get('MASTODON_ACCESS_TOKEN')
|
||||
MASTODON_INSTANCE_URL = os.environ.get('MASTODON_INSTANCE_URL')
|
||||
RSS_FEED_URL = os.environ.get('RSS_FEED_URL')
|
||||
|
||||
# Optional configuration with default values
|
||||
TOOT_VISIBILITY = os.environ.get('TOOT_VISIBILITY', 'public')
|
||||
CHECK_INTERVAL = int(os.environ.get('CHECK_INTERVAL', 3600))
|
||||
HASHTAGS = os.environ.get('MASTO_RSS_HASHTAGS', '')
|
||||
MAX_HISTORY_SIZE = int(os.environ.get('MAX_HISTORY_SIZE', 500))
|
||||
|
||||
# NEW: Template for the toot format.
|
||||
# Use {title}, {link}, and {hashtags} as placeholders.
|
||||
DEFAULT_TEMPLATE = '{title}\n\n{link}\n\n{hashtags}'
|
||||
TOOT_TEMPLATE = os.environ.get('TOOT_TEMPLATE', DEFAULT_TEMPLATE)
|
||||
|
||||
# File to store the processed entry URLs.
|
||||
PROCESSED_ENTRIES_FILE = '/state/processed_entries.txt'
|
||||
|
||||
# Time delay between RSS checks (in seconds)
|
||||
CHECK_INTERVAL = os.environ['CHECK_INTERVAL'] # Check interval in seconds
|
||||
# Check if all required configuration is present
|
||||
if not all([MASTODON_CLIENT_ID, MASTODON_CLIENT_SECRET, MASTODON_ACCESS_TOKEN, MASTODON_INSTANCE_URL, RSS_FEED_URL]):
|
||||
print("Error: Not all required environment variables are set.")
|
||||
exit(1)
|
||||
|
||||
# Initialize Mastodon client
|
||||
mastodon = Mastodon(
|
||||
# --- Mastodon Initialization ---
|
||||
try:
|
||||
mastodon = Mastodon(
|
||||
client_id=MASTODON_CLIENT_ID,
|
||||
client_secret=MASTODON_CLIENT_SECRET,
|
||||
access_token=MASTODON_ACCESS_TOKEN,
|
||||
api_base_url=MASTODON_INSTANCE_URL
|
||||
)
|
||||
)
|
||||
mastodon.account_verify_credentials()
|
||||
print("Successfully logged in to Mastodon.")
|
||||
except Exception as e:
|
||||
print(f"Error initializing Mastodon client: {e}")
|
||||
exit(1)
|
||||
|
||||
# --- Functions ---
|
||||
|
||||
# Function to load processed entry URLs from a file
|
||||
def load_processed_entries():
|
||||
"""Loads processed entry URLs from a file into a deque."""
|
||||
os.makedirs(os.path.dirname(PROCESSED_ENTRIES_FILE), exist_ok=True)
|
||||
try:
|
||||
with open(PROCESSED_ENTRIES_FILE, 'r') as file:
|
||||
return set(file.read().splitlines())
|
||||
lines = file.read().splitlines()
|
||||
return deque(lines, maxlen=MAX_HISTORY_SIZE)
|
||||
except FileNotFoundError:
|
||||
return set()
|
||||
return deque(maxlen=MAX_HISTORY_SIZE)
|
||||
|
||||
# Function to save processed entry URLs to a file
|
||||
def save_processed_entries(processed_entries):
|
||||
def save_processed_entries(processed_entries_deque):
|
||||
"""Saves the processed entry URLs from the deque to a file."""
|
||||
with open(PROCESSED_ENTRIES_FILE, 'w') as file:
|
||||
file.write('\n'.join(processed_entries))
|
||||
file.write('\n'.join(processed_entries_deque))
|
||||
|
||||
def format_hashtags(hashtag_string):
|
||||
"""Formats a string of hashtags into a correct list."""
|
||||
if not hashtag_string:
|
||||
return ""
|
||||
clean_string = hashtag_string.strip(' "\'')
|
||||
tags = filter(None, clean_string.split(' '))
|
||||
return " ".join([f"#{tag.lstrip('#')}" for tag in tags])
|
||||
|
||||
# Function to check and post new RSS items
|
||||
def check_and_post_new_items():
|
||||
while True:
|
||||
print("Checking for new RSS items...")
|
||||
# Load processed entry URLs from the file
|
||||
"""Checks the RSS feed and only posts the latest item if it's new."""
|
||||
|
||||
formatted_hashtags = format_hashtags(HASHTAGS)
|
||||
if formatted_hashtags:
|
||||
print(f"Hashtags configured: {formatted_hashtags}")
|
||||
else:
|
||||
print("INFO: No hashtags configured.")
|
||||
|
||||
print(f"INFO: Using toot template: {TOOT_TEMPLATE.replace(chr(10), ' ')}")
|
||||
|
||||
processed_entries = load_processed_entries()
|
||||
|
||||
# Parse the RSS feed
|
||||
while True:
|
||||
print(f"Checking for new RSS items from: {RSS_FEED_URL}")
|
||||
|
||||
feed = feedparser.parse(RSS_FEED_URL)
|
||||
|
||||
for entry in feed.entries:
|
||||
entry_url = entry.link
|
||||
if feed.bozo:
|
||||
print(f"Warning: RSS feed may be malformed. Error: {feed.bozo_exception}")
|
||||
|
||||
# Check if the entry is new (not in the processed_entries set)
|
||||
if entry_url not in processed_entries:
|
||||
print(f"Found a new RSS item: {entry.title}")
|
||||
# Create a Mastodon status
|
||||
status = f"\n{entry.title}\n\n{entry.link}"
|
||||
if not feed.entries:
|
||||
print("No items found in the RSS feed.")
|
||||
else:
|
||||
latest_entry = feed.entries[0]
|
||||
entry_url = latest_entry.get('link')
|
||||
entry_title = latest_entry.get('title', 'No title')
|
||||
|
||||
# Post the status to Mastodon
|
||||
if not entry_url:
|
||||
print(f"Skipping latest item '{entry_title}' (no link).")
|
||||
elif entry_url not in processed_entries:
|
||||
print(f"Found new latest item: {entry_title}")
|
||||
|
||||
# Compose the Mastodon status based on the template
|
||||
status = TOOT_TEMPLATE.format(
|
||||
title=entry_title,
|
||||
link=entry_url,
|
||||
hashtags=formatted_hashtags
|
||||
).strip()
|
||||
|
||||
try:
|
||||
print(f"Posting: {status.replace(chr(10), ' ')}")
|
||||
mastodon.status_post(status, visibility=TOOT_VISIBILITY)
|
||||
print("Post successful.")
|
||||
|
||||
# Add the entry URL to the processed_entries set
|
||||
processed_entries.add(entry_url)
|
||||
|
||||
# Save the updated processed_entries set to the file
|
||||
processed_entries.append(entry_url)
|
||||
save_processed_entries(processed_entries)
|
||||
|
||||
print("Sleeping for", CHECK_INTERVAL, "seconds...")
|
||||
# Wait for the specified interval before checking again
|
||||
except Exception as e:
|
||||
print(f"Error posting to Mastodon: {e}")
|
||||
else:
|
||||
print("The latest item has already been posted.")
|
||||
|
||||
print(f"Waiting for {CHECK_INTERVAL} seconds...")
|
||||
time.sleep(int(CHECK_INTERVAL))
|
||||
|
||||
# --- Main Program ---
|
||||
if __name__ == "__main__":
|
||||
check_and_post_new_items()
|
||||
|
||||
Reference in New Issue
Block a user