From bdc72c289f8147da0c5614c2f90d7f252e99e28b Mon Sep 17 00:00:00 2001 From: Ed Summers Date: Sat, 9 Jun 2018 12:53:44 -0400 Subject: [PATCH] initial --- .gitignore | 4 ++ LICENSE | 22 ++++++++++ README.md | 58 +++++++++++++++++++++++++++ feediverse.py | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 18 +++++++++ 5 files changed, 210 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 feediverse.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..474fdb8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +feediverse.egg-info +Pipfile* +__pycache__ +.config.yml.swp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..43ddc20 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018 Ed Summers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..da84228 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +*feediverse* will read RSS/Atom feeds and send the messages as Mastodon posts. +It's kind of the same thing as [feed2toot] but just one module that works with +Python 3. + +## Install + + pip install feediverse + +## Run + +The first time you run *feediverse* you'll need to tell it your Mastodon +instance and get an access token which it will save in a configuration file. If +you don't specify a config file it will use `~/.feediverse`: + + feediverse + +Once *feediverse* is configured you can add it to your crontab: + + */15 * * * * /usr/local/bin/feediverse + +## Post Format + +You can customize the post format by opening the configuration file (default is +~/.feediverse) and updating the *template* property of your feed. The default +format is: + + {title} {url} + +But you can use the `{summary}` as well, and also add text like so: + + Bookmark: {title} {url} {summary} + +## Multiple Feeds + +Since *feeds* is a list you can add additional feeds to watch if you want. + + ... + feeds: + - url: https://example.com/feed/ + template: "dot com: {title} {url}" + - url: https://example.org/feed/ + template: "dot org: {title} {url}" + +## Why? + +I created *feediverse* because I wanted to send my Pinboard bookmarks to Mastodon. +I've got an IFTTT recipe that does this for Twitter, but IFTTT doesn't appear to +work with Mastodon yet. + +That being said *feediverse* should work with any RSS or Atom feed (thanks to +[feedparser]). But please be responsible. Don't fill up Mastodon with tons of +junk just because you can. That kind of toxic behavior is why a lot of people +are leaving other social media platforms and trying to start over in Mastodon. + +[feed2toot]: https://gitlab.com/chaica/feed2toot/ +[feedparser]: http://feedparser.org/ + + diff --git a/feediverse.py b/feediverse.py new file mode 100755 index 0000000..bd95714 --- /dev/null +++ b/feediverse.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +import os +import sys +import yaml +import dateutil +import feedparser + +from mastodon import Mastodon +from datetime import datetime, timezone + +def main(): + config_file = get_config_file() + if not os.path.isfile(config_file): + setup(config_file) + + config = read_config(config_file) + + masto = Mastodon( + api_base_url=config['url'], + client_id=config['client_id'], + client_secret=config['client_secret'], + access_token=config['access_token'] + ) + + for feed in config['feeds']: + for entry in get_feed(feed['url'], config['updated']): + print(feed['template'].format(**entry)) + #masto.status_post(get_text(entry)) + + save_config(config, config_file) + +def save_config(config, config_file): + copy = dict(config) + copy['updated'] = datetime.now(tz=timezone.utc).isoformat() + with open(config_file, 'w') as fh: + fh.write(yaml.dump(copy, default_flow_style=False)) + +def read_config(config_file): + config = {} + with open(config_file) as fh: + config = yaml.load(fh) + if 'updated' in config: + config['updated'] = dateutil.parser.parse(config['updated']) + else: + config['updated'] = datetime.now(tz=timezone.utc) + return config + +def get_feed(feed_url, last_update): + new_entries = 0 + feed = feedparser.parse(feed_url) + for entry in feed.entries: + e = get_entry(entry) + if last_update is None or e['updated'] > last_update: + new_entries += 1 + yield e + return new_entries + +def get_entry(entry): + return { + 'url': entry.id, + 'title': entry.title, + 'summary': entry.get('summary', ''), + 'updated': dateutil.parser.parse(entry['updated']), + } + +def setup(config_file): + url = input('What is your Mastodon Instance URL? ') + have_app = input('Do you have your app credentials already? [y/n] ') + if have_app.lower() == 'y': + name = 'feediverse' + client_id = input('What is your app\'s client id: ') + client_secret = input('What is your client secret: ') + access_token = input('access_token: ') + else: + print("Ok, I'll need a few things in order to get your access token") + name = input('app name (e.g. feediverse): ') + client_id, client_secret = Mastodon.create_app(name, scopes=['read', 'write'], website='https://github.com/edsu/feediverse') + username = input('mastodon username (email): ') + password = input('mastodon password (not stored): ') + access_token = m.log_in(username, password) + + m = Mastodon(client_id, client_secret) + feed_url = input('RSS/Atom feed URL to watch: ') + config = { + 'name': name, + 'url': url, + 'client_id': client_id, + 'client_secret': client_secret, + 'access_token': access_token, + 'feeds': [ + {'url': feed_url, 'template': '{title} {url}'} + ] + } + save_config(config, config_file) + print("Your feediverse configuration has been saved to {}".format(config_file)) + print("Add a line line this to your crontab to check every 15 minutes:") + print("*/15 * * * * /usr/local/bin/feediverse") + +def get_config_file(): + if __name__ == "__main__" and len(sys.argv) > 1: + config_file = sys.argv[1] + else: + config_file = os.path.join(os.path.expanduser("~"), ".feediverse") + return config_file + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7837253 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup + +with open("README.md") as f: + long_description = f.read() + +setup( + name='feediverse', + version='0.0.1', + url='https://github.com/edsu/feediverse', + author='Ed Summers', + author_email='ehs@pobox.com', + py_modules=['feediverse', ], + description='Connect an RSS Feed to Mastodon', + long_description=long_description, + long_description_content_type="text/markdown", + install_requires=['feedparser', 'mastodon.py', 'python-dateutil', 'pyyaml'], + entry_points={'console_scripts': ['feediverse = feediverse:main']} +)