diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8c4b167 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.old +**/bin +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +processed_entries.txt +rss2newsletter.egg-info +LICENSE +README.md \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..69f94d6 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,26 @@ +name: ci + +on: + push: + branches: + - "main" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - + name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Build and push + uses: docker/build-push-action@v6 + with: + push: true + tags: ${{ vars.DOCKERHUB_USERNAME }}/rss2newsletter:latest diff --git a/.gitignore b/.gitignore index 91cf2ee..2f00311 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ processed_entries.txt *.swp *.swo +*.old rss2newsletter.egg-info dist diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c7ca463 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3-alpine +WORKDIR /rss2newsletter +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +CMD ["python", "-u", "./rss2newsletter"] \ No newline at end of file diff --git a/README.md b/README.md index c38a483..d05ccb2 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,20 @@ options: Program configuration file (default: rss2newsletter.conf) ``` +## Supported RSS tags in the newsletter template +You can design your own newsletter template by modifying the [newsletter template file](https://raw.githubusercontent.com/ElliotKillick/rss2newsletter/main/newsletter_template.html). Certain keywords in the template are automatically replaced by their corresponding RSS tags. + +| **template keyword** | **RSS tag** | +| :---: | :---: | +| LINK_HERE | link | +| TITLE_HERE | title | +| SUMMARY_HERE | description | +| PUBLISHED_HERE | pubdate | +| CONTENT_HERE | content | +| AUTHOR_NAME_HERE | author (name) | +| AUTHOR_EMAIL_HERE | author (email) | +| TAGS_HERE | category (as comma separated string) | +| MEDIA_HERE | media (url) | ## Support the Author diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1b01bc2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + rss2newsletter: + image: rss2newsletter + container_name: rss2newsletter + restart: unless-stopped + volumes: + - ./rss2newsletter.conf:/rss2newsletter/rss2newsletter.conf + - ./newsletter_template.html:/rss2newsletter/newsletter_template.html + - type: volume + source: rss2newsletter-data + target: /rss2newsletter/data + +volumes: + rss2newsletter-data: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3c9f73e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests +feedparser +lxml \ No newline at end of file diff --git a/rss2newsletter b/rss2newsletter index ef51404..c855c7c 100755 --- a/rss2newsletter +++ b/rss2newsletter @@ -100,7 +100,7 @@ class rss2newsletter: for new_entry in self.check_for_new_entries( processed_entries_last_update, feed.entries ): - campaign_id = self.create_newsletter(new_entry.link, new_entry.title) + campaign_id = self.create_newsletter(new_entry) send_successful = self.send_newsletter(campaign_id) if send_successful: self.update_processed_entries_file(new_entry.link) @@ -111,8 +111,7 @@ class rss2newsletter: feed = feedparser.parse(self.config["FEED"]["URL"]) # In case of failure if hasattr(feed, "bozo_exception"): - print(f"Error fetching RSS from: {self.config['FEED']['URL']}") - return None + raise Exception(f"Error fetching RSS from: {self.config['FEED']['URL']}") return feed @@ -166,13 +165,13 @@ class rss2newsletter: if entry.link not in proceseed_entries_last_update: yield entry - def create_newsletter(self, link: str, title: str) -> int | None: + def create_newsletter(self, feed: feedparser.FeedParserDict) -> int | None: """Create newsletter with content and add campaign to Listmonk""" - print("Creating newsletter for:", title) - return self.create_campaign(title, self.create_content(link, title)) + print("Creating newsletter for:", feed.title) + return self.create_campaign(feed.title, self.create_content(feed)) - def create_content(self, link: str, title: str) -> str: + def create_content(self, feed: feedparser.FeedParserDict) -> str: """Create content to be used as body of newsletter""" with open( @@ -180,10 +179,32 @@ class rss2newsletter: ) as f: content = f.read() - content = content.replace("LINK_HERE", link) - content = content.replace("TITLE_HERE", title) - og_image = self.get_og_image(self.fetch_url(link)) + if hasattr(feed, "link"): + content = content.replace("LINK_HERE", feed.link) + if hasattr(feed, "title"): + content = content.replace("TITLE_HERE", feed.title) + if hasattr(feed, "summary"): + content = content.replace("SUMMARY_HERE", feed.summary) + if hasattr(feed, "published_parsed"): + content = content.replace("PUBLISHED_HERE", time.strftime("%d-%m-%Y", feed.published_parsed)) + if hasattr(feed, "content") and len(feed.content) > 0: + content = content.replace("CONTENT_HERE", feed.content[0].value) + if hasattr(feed, "author"): + email = feed.author.split(' ')[0] + content = content.replace("AUTHOR_EMAIL_HERE", email) + start_index_author = feed.author.find('(') + end_index_author = feed.author.find(')') + if start_index_author != -1 and end_index_author != -1: + author = feed.author[start_index_author + 1:end_index_author] + content = content.replace("AUTHOR_NAME_HERE", author) + if hasattr(feed, "tags"): + tags_string = ", ".join([tag.term for tag in feed.tags]) + content = content.replace("TAGS_HERE", tags_string) + if hasattr(feed, "media_content") and len(feed.media_content) > 0: + content = content.replace("MEDIA_HERE", feed.media_content[0]["url"]) + + og_image = self.get_og_image(self.fetch_url(feed.link)) if og_image: content = content.replace("IMAGE_HERE", og_image) else: @@ -245,7 +266,7 @@ class rss2newsletter: json_data = { "name": name, "subject": name, - "lists": [1], + "lists": [int(self.config["LISTMONK"]["LIST_ID"])], "content_type": "richtext", "body": body, "messenger": "email", diff --git a/rss2newsletter.conf b/rss2newsletter.conf index dd649a7..c194418 100644 --- a/rss2newsletter.conf +++ b/rss2newsletter.conf @@ -1,12 +1,12 @@ [FEED] # Full URL to your website's feed -URL = https://elliotonsecurity.com/atom.xml +URL = YOUR_FEED_URL # How often to check for new feed entries in seconds POLL_INTERVAL = 300 # rss2newsletter uses this file to keep track of new feed entries -PROCESSED_ENTRIES_FILE = processed_entries.txt +PROCESSED_ENTRIES_FILE = data/processed_entries.txt [LISTMONK] @@ -14,7 +14,7 @@ PROCESSED_ENTRIES_FILE = processed_entries.txt URL = http://localhost:9000 # Credentials -USERNAME = ElliotKillick +USERNAME = YOUR_USERNAME PASSWORD = YOUR_PASSWORD # The ID of your "rss2newsletter" list (create this list in listmonk)