From bd900cf6436e221eb36e6179b163de13720fcd07 Mon Sep 17 00:00:00 2001
From: Ryan Barrett
Date: Sat, 18 Apr 2020 10:52:33 -0700
Subject: [PATCH] add pixelfed!
for #927
---
README.md | 14 +++++
admin.py | 4 +-
app.py | 3 +-
docs/source/modules.rst | 4 ++
handlers.py | 2 +-
mastodon.py | 90 +++++++++++++++++--------------
models.py | 12 +++--
pixelfed.py | 73 +++++++++++++++++++++++++
publish.py | 14 ++---
scripts/dump_domains.py | 2 +-
static/pixelfed_icon.png | Bin 0 -> 3889 bytes
static/robots.txt | 1 +
static/style.css | 8 ++-
tasks.py | 4 +-
templates/about.html | 9 ++--
templates/choose_instance.html | 32 +++++++++++
templates/index.html | 1 +
templates/mastodon_instance.html | 34 ------------
templates/pixelfed_user.html | 7 +++
templates/preview.html | 2 +-
templates/social_user.html | 4 +-
tests/test_models.py | 13 +++++
tests/test_pixelfed.py | 26 +++++++++
tests/test_publish.py | 2 +-
24 files changed, 261 insertions(+), 100 deletions(-)
create mode 100644 pixelfed.py
create mode 100644 static/pixelfed_icon.png
create mode 100644 templates/choose_instance.html
delete mode 100644 templates/mastodon_instance.html
create mode 100644 templates/pixelfed_user.html
create mode 100644 tests/test_pixelfed.py
diff --git a/README.md b/README.md
index 44d96f81..2b21e7c6 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,20 @@ dev_appserver.py --log_level debug --enable_host_checking false \
Open [localhost:8080](http://localhost:8080/) and you should see the Bridgy home page!
+To test polling interactively, temporarily comment out the `manual_scaling` section in [background.yaml](https://github.com/snarfed/bridgy/blob/master/background.yaml), uncomment the `automatic_scaling` section, and use `curl` to run the task handler directly:
+
+```sh
+curl -d 'source_key=[KEY]&last_polled=1970-01-01-00-00-00' http://localhost:8081/_ah/queue/poll
+```
+
+...where `[KEY]` is the url-safe ndb key for the source entity you want to poll, e.g. `ndb.Key('Twitter', 'schnarfed').urlsafe()`.
+
+Similarly, to run a webmention (aka propagate) task, look in `dev_appserver`'s logs for lines beginning with _Would add task: ..._, grab the request body there, and plug it into a similar `curl` command, eg:
+
+```sh
+curl -d 'response_key=...' localhost:8081/_ah/queue/propagate
+```
+
If you hit an error during setup, check out the [oauth-dropins Troubleshooting/FAQ section](https://github.com/snarfed/oauth-dropins#troubleshootingfaq). For searchability, here are a handful of error messages that [have solutions there](https://github.com/snarfed/oauth-dropins#troubleshootingfaq):
```
diff --git a/admin.py b/admin.py
index e67c435c..78d8e18f 100644
--- a/admin.py
+++ b/admin.py
@@ -16,7 +16,7 @@
import webapp2
# Import source class files so their metaclasses are initialized.
-import blogger, flickr, github, instagram, mastodon, medium, tumblr, twitter, wordpress_rest
+import blogger, flickr, github, instagram, mastodon, medium, pixelfed, tumblr, twitter, wordpress_rest
class ResponsesHandler(handlers.TemplateHandler):
@@ -59,7 +59,7 @@ def template_file(self):
def template_vars(self):
CLASSES = (flickr.Flickr, github.GitHub, twitter.Twitter,
- instagram.Instagram, mastodon.Mastodon)
+ instagram.Instagram, mastodon.Mastodon, pixelfed.Pixelfed)
queries = [cls.query(Source.status == 'enabled',
Source.poll_status == 'error',
Source.rate_limited.IN((False, None)),
diff --git a/app.py b/app.py
index 017e2639..f79116bc 100644
--- a/app.py
+++ b/app.py
@@ -39,6 +39,7 @@
'mastodon',
'medium',
'meetup',
+ 'pixelfed',
'publish',
'tumblr',
'twitter',
@@ -733,7 +734,7 @@ def get(self):
routes += [
('/?', FrontPageHandler),
('/users/?', UsersHandler),
- ('/(blogger|fake|fake_blog|flickr|github|instagram|mastodon|medium|meetup|tumblr|twitter|wordpress)/([^/]+)/?',
+ ('/(blogger|fake|fake_blog|flickr|github|instagram|mastodon|medium|meetup|pixelfed|tumblr|twitter|wordpress)/([^/]+)/?',
UserHandler),
('/facebook/.*', FacebookIsDeadHandler),
('/googleplus/.*', GooglePlusIsDeadHandler),
diff --git a/docs/source/modules.rst b/docs/source/modules.rst
index 26cb16dd..8627ddfd 100644
--- a/docs/source/modules.rst
+++ b/docs/source/modules.rst
@@ -54,6 +54,10 @@ original_post_discovery
-----------------------
.. automodule:: original_post_discovery
+pixelfed
+--------
+.. automodule:: pixelfed
+
publish
-------
.. automodule:: publish
diff --git a/handlers.py b/handlers.py
index 3c2fc06f..1c01c75f 100644
--- a/handlers.py
+++ b/handlers.py
@@ -35,7 +35,7 @@
import util
# Import source class files so their metaclasses are initialized.
-import blogger, flickr, github, instagram, mastodon, medium, tumblr, twitter, wordpress_rest
+import blogger, flickr, github, instagram, mastodon, medium, pixelfed, tumblr, twitter, wordpress_rest
CACHE_TIME = 60 * 15 # 15m
diff --git a/mastodon.py b/mastodon.py
index d57e38f8..f3a5fe0a 100644
--- a/mastodon.py
+++ b/mastodon.py
@@ -12,20 +12,6 @@
import models
import util
-# https://docs.joinmastodon.org/api/oauth-scopes/
-LISTEN_SCOPES = (
- 'read:accounts',
- 'read:blocks',
- 'read:notifications',
- 'read:search',
- 'read:statuses',
-)
-PUBLISH_SCOPES = LISTEN_SCOPES + (
- 'write:statuses',
- 'write:favourites',
- 'write:media',
-)
-
class StartHandler(oauth_dropins.mastodon.StartHandler):
"""Abstract base OAuth starter class with our redirect URLs."""
@@ -56,12 +42,7 @@ class Mastodon(models.Source):
SHORT_NAME = 'mastodon'
CAN_PUBLISH = True
HAS_BLOCKS = True
- TYPE_LABELS = {
- 'post': 'toot',
- 'comment': 'reply',
- 'repost': 'boost',
- 'like': 'favorite',
- }
+ TYPE_LABELS = GR_CLASS.TYPE_LABELS
DISABLE_HTTP_CODES = ('401', '403')
@property
@@ -71,8 +52,8 @@ def URL_CANONICALIZER(self):
domain=self.gr_source.DOMAIN,
headers=util.REQUEST_HEADERS)
- @staticmethod
- def new(handler, auth_entity=None, **kwargs):
+ @classmethod
+ def new(cls, handler, auth_entity=None, **kwargs):
"""Creates and returns a :class:`Mastodon` entity.
Args:
@@ -81,12 +62,12 @@ def new(handler, auth_entity=None, **kwargs):
kwargs: property values
"""
user = json_loads(auth_entity.user_json)
- return Mastodon(id=auth_entity.key_id(),
- auth_entity=auth_entity.key,
- url=user.get('url'),
- name=user.get('display_name') or user.get('username'),
- picture=user.get('avatar'),
- **kwargs)
+ return cls(id=auth_entity.key_id(),
+ auth_entity=auth_entity.key,
+ url=user.get('url'),
+ name=user.get('display_name') or user.get('username'),
+ picture=user.get('avatar'),
+ **kwargs)
def username(self):
"""Returns the Mastodon username, e.g. alice."""
@@ -119,13 +100,18 @@ def button_html(cls, feature, **kwargs):
source = kwargs.get('source')
instance = source.instance() if source else ''
return """\
-
-""" % ('post' if instance else 'get', feature, instance)
+""".format(instance=instance,
+ method='post' if instance else 'get',
+ feature=feature,
+ name=cls.GR_CLASS.NAME,
+ short_name=cls.SHORT_NAME,
+ **kwargs)
def is_private(self):
"""Returns True if this Mastodon account is protected.
@@ -166,20 +152,44 @@ def load_blocklist(self):
class InstanceHandler(TemplateHandler, util.Handler):
"""Serves the "Enter your instance" form page."""
+ SITE = 'mastodon'
+ START_HANDLER = StartHandler
+
+ # https://docs.joinmastodon.org/api/oauth-scopes/
+ LISTEN_SCOPES = (
+ 'read:accounts',
+ 'read:blocks',
+ 'read:notifications',
+ 'read:search',
+ 'read:statuses',
+ )
+ PUBLISH_SCOPES = LISTEN_SCOPES + (
+ 'write:statuses',
+ 'write:favourites',
+ 'write:media',
+ )
+
def template_file(self):
- return 'mastodon_instance.html'
+ return 'choose_instance.html'
+
+ def template_vars(self):
+ return {
+ 'site': self.SITE,
+ 'logo_file': 'mastodon_logo_large.png',
+ 'join_url': 'https://joinmastodon.org/#getting-started',
+ }
def post(self):
feature = self.request.get('feature')
- start_cls = util.oauth_starter(StartHandler).to('/mastodon/callback',
- scopes=PUBLISH_SCOPES if feature == 'publish' else LISTEN_SCOPES)
+ start_cls = util.oauth_starter(self.START_HANDLER).to('/%s/callback' % self.SITE,
+ scopes=self.PUBLISH_SCOPES if feature == 'publish' else self.LISTEN_SCOPES)
start = start_cls(self.request, self.response)
instance = util.get_required_param(self, 'instance')
try:
self.redirect(start.redirect_url(instance=instance))
except ValueError as e:
- logging.warning('Bad Mastodon instance', stack_info=True)
+ logging.warning('Bad %s instance' % self.SITE.capitalize(), stack_info=True)
self.messages.add(util.linkify(str(e), pretty=True))
return self.redirect(self.request.path)
@@ -194,6 +204,6 @@ def finish(self, auth_entity, state=None):
('/mastodon/start', InstanceHandler),
('/mastodon/callback', CallbackHandler),
('/mastodon/delete/finish', oauth_dropins.mastodon.CallbackHandler.to('/delete/finish')),
- ('/mastodon/publish/start', StartHandler.to('/publish/mastodon/finish',
- scopes=PUBLISH_SCOPES)),
+ ('/mastodon/publish/start', StartHandler.to(
+ '/publish/mastodon/finish', scopes=InstanceHandler.PUBLISH_SCOPES)),
]
diff --git a/models.py b/models.py
index c12f54b5..91d0ddce 100644
--- a/models.py
+++ b/models.py
@@ -213,7 +213,7 @@ def __getattr__(self, name):
kwargs = {'user_id': self.key_id()}
elif self.key.kind() == 'Instagram':
kwargs = {'scrape': True, 'cookie': INSTAGRAM_SESSIONID_COOKIE}
- elif self.key.kind() == 'Mastodon':
+ elif self.key.kind() in ('Mastodon', 'Pixelfed'):
args = (auth_entity.instance(),) + args
kwargs = {'user_id': json_loads(auth_entity.user_json).get('id')}
elif self.key.kind() == 'Twitter':
@@ -499,10 +499,12 @@ def create_new(cls, handler, user_url=None, **kwargs):
if existing:
# merge some fields
source.features = set(source.features + existing.features)
- source.populate(**existing.to_dict(include=(
- 'created', 'last_hfeed_refetch', 'last_poll_attempt', 'last_polled',
- 'last_syndication_url', 'last_webmention_sent', 'superfeedr_secret',
- 'webmention_endpoint')))
+ props = ('created', 'last_hfeed_refetch', 'last_poll_attempt',
+ 'last_polled', 'last_syndication_url', 'last_webmention_sent',
+ 'superfeedr_secret', 'webmention_endpoint')
+ if existing.domain_urls and not source.domain_urls:
+ props += ('domains', 'domain_urls')
+ source.populate(**existing.to_dict(include=props))
verb = 'Updated'
else:
verb = 'Added'
diff --git a/pixelfed.py b/pixelfed.py
new file mode 100644
index 00000000..c27e961c
--- /dev/null
+++ b/pixelfed.py
@@ -0,0 +1,73 @@
+"""Pixelfed source and datastore model classes."""
+from granary import pixelfed as gr_pixelfed
+import oauth_dropins.pixelfed
+import webapp2
+
+import mastodon
+import util
+
+
+class StartHandler(oauth_dropins.pixelfed.StartHandler):
+ """Abstract base OAuth starter class with our redirect URLs."""
+ REDIRECT_PATHS = (
+ '/pixelfed/callback',
+ # TODO: uncomment when https://github.com/pixelfed/pixelfed/issues/2106 is fixed
+ # '/publish/pixelfed/finish',
+ # '/pixelfed/delete/finish',
+ # '/delete/finish',
+ )
+
+ def app_name(self):
+ return 'Bridgy'
+
+ def app_url(self):
+ if self.request.host in util.OTHER_DOMAINS:
+ return util.HOST_URL
+
+ return super().app_url()
+
+
+class Pixelfed(mastodon.Mastodon):
+ """A Pixelfed account.
+
+ The key name is the fully qualified address, eg '@snarfed@piconic.co'.
+ """
+ GR_CLASS = gr_pixelfed.Pixelfed
+ OAUTH_START_HANDLER = StartHandler
+ SHORT_NAME = 'pixelfed'
+ CAN_PUBLISH = True
+ HAS_BLOCKS = False
+ TYPE_LABELS = GR_CLASS.TYPE_LABELS
+
+ def search_for_links(self):
+ return []
+
+class InstanceHandler(mastodon.InstanceHandler):
+ """Serves the "Enter your instance" form page."""
+ SITE = 'pixelfed'
+ START_HANDLER = StartHandler
+
+ # https://docs.pixelfed.org/technical-documentation/api-v1.html
+ LISTEN_SCOPES = ('read',)
+ PUBLISH_SCOPES = LISTEN_SCOPES + ('write',)
+
+ def template_vars(self):
+ return {
+ 'site': self.SITE,
+ 'logo_file': 'pixelfed_logo.png',
+ 'join_url': 'https://pixelfed.org/join',
+ }
+
+
+class CallbackHandler(oauth_dropins.pixelfed.CallbackHandler, util.Handler):
+ def finish(self, auth_entity, state=None):
+ source = self.maybe_add_or_delete_source(Pixelfed, auth_entity, state)
+
+
+ROUTES = [
+ ('/pixelfed/start', InstanceHandler),
+ ('/pixelfed/callback', CallbackHandler),
+ ('/pixelfed/delete/finish', oauth_dropins.pixelfed.CallbackHandler.to('/delete/finish')),
+ ('/pixelfed/publish/start', StartHandler.to(
+ '/publish/pixelfed/finish', scopes=InstanceHandler.PUBLISH_SCOPES)),
+]
diff --git a/publish.py b/publish.py
index 3b24c57c..875220f8 100644
--- a/publish.py
+++ b/publish.py
@@ -19,6 +19,7 @@
github as oauth_github,
mastodon as oauth_mastodon,
meetup as oauth_meetup,
+ pixelfed as oauth_pixelfed,
twitter as oauth_twitter,
)
from oauth_dropins.webutil import appengine_info
@@ -32,13 +33,14 @@
from mastodon import Mastodon
from meetup import Meetup
from models import Publish, PublishedPage
+from pixelfed import Pixelfed
from twitter import Twitter
import models
import util
import webmention
-SOURCES = (Flickr, GitHub, Mastodon, Meetup, Twitter)
+SOURCES = (Flickr, GitHub, Mastodon, Meetup, Pixelfed, Twitter)
SOURCE_NAMES = {cls.SHORT_NAME: cls for cls in SOURCES}
SOURCE_DOMAINS = {cls.GR_CLASS.DOMAIN: cls for cls in SOURCES}
# image URLs matching this regexp should be ignored.
@@ -147,7 +149,7 @@ def _run(self):
if (domain not in util.DOMAINS or
len(path_parts) != 2 or path_parts[0] != '/publish' or not source_cls):
return self.error(
- 'Target must be brid.gy/publish/{flickr,github,mastodon,meetup,twitter}')
+ 'Target must be brid.gy/publish/{flickr,github,mastodon,meetup,pixelfed,twitter}')
elif source_cls == Instagram:
return self.error('Sorry, %s is not supported.' %
source_cls.GR_CLASS.NAME)
@@ -691,18 +693,17 @@ def error(self, error, html=None, status=400, data=None, report=False, **kwargs)
class FlickrSendHandler(oauth_flickr.CallbackHandler, SendHandler):
finish = SendHandler.finish
-
class GitHubSendHandler(oauth_github.CallbackHandler, SendHandler):
finish = SendHandler.finish
-
class MastodonSendHandler(oauth_mastodon.CallbackHandler, SendHandler):
finish = SendHandler.finish
-
class MeetupSendHandler(oauth_meetup.CallbackHandler, SendHandler):
finish = SendHandler.finish
+class PixelfedSendHandler(oauth_pixelfed.CallbackHandler, SendHandler):
+ finish = SendHandler.finish
class TwitterSendHandler(oauth_twitter.CallbackHandler, SendHandler):
finish = SendHandler.finish
@@ -741,11 +742,12 @@ def authorize(self):
ROUTES = [
('/publish/preview', PreviewHandler),
('/publish/webmention', WebmentionHandler),
- ('/publish/(flickr|github|mastodon|meetup|twitter)',
+ ('/publish/(flickr|github|mastodon|meetup|pixelfed|twitter)',
webmention.WebmentionGetHandler),
('/publish/flickr/finish', FlickrSendHandler),
('/publish/github/finish', GitHubSendHandler),
('/publish/mastodon/finish', MastodonSendHandler),
('/meetup/publish/finish', MeetupSendHandler), # because Meetup's `redirect_uri` handling is a little more restrictive
+ ('/publish/pixelfed/finish', PixelfedSendHandler),
('/publish/twitter/finish', TwitterSendHandler),
]
diff --git a/scripts/dump_domains.py b/scripts/dump_domains.py
index f530733a..29fedc7e 100755
--- a/scripts/dump_domains.py
+++ b/scripts/dump_domains.py
@@ -11,7 +11,7 @@
import models
from models import Response
-import blogger, flickr, github, instagram, mastodon, medium, tumblr, twitter, wordpress_rest
+import blogger, flickr, github, instagram, mastodon, medium, pixelfed, tumblr, twitter, wordpress_rest
domains = collections.defaultdict(int) # maps domain to # of users
diff --git a/static/pixelfed_icon.png b/static/pixelfed_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2f402d2a9339cf143d1897e02022f0fd64f0cb3
GIT binary patch
literal 3889
zcmV-156004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006
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 users sign up for Bridgy
without leaving my site or application?
-Not exactly, but you can get close. Direct your users to POST
to a Bridgy registration URL of the form https://brid.gy/SITE/start
(where SITE is
-twitter
, instagram
, flickr
, github
, or mastodon
) with the parameters:
+Not exactly, but you can get close. Direct your users to POST
to a Bridgy registration URL of the form https://brid.gy/SITE/start
(where SITE is twitter
, instagram
, flickr
, github
, mastodon
, meetup
, or pixelfed
) with the parameters:
feature
, a comma-separated list of features,
@@ -452,7 +451,8 @@ Pulling back responses
- Instagram comments and likes on your pictures.
- Flickr comments and favorites on your photos.
- GitHub comments and emoji reactions on your issues and pull requests.
-- Mastodon replies, favorites, boosts, mentions, and links to your site.
+- Mastodon replies, favorites, boosts, mentions, and links on your toots.
+- Pixelfed comments, likes, shares, and emoji reactions on your posts.
@@ -786,6 +786,7 @@ Publishing
GitHub issues, stars, comments on issues and PRs, and emoji reactions to issues, PRs, and comments.
Adding new labels to GitHub issues.
Mastodon toots, replies, favorites, and boosts. Toots and replies may include pictures and videos. Toots may also be deleted.
+Pixelfed posts, comments, likes, shares, and reactions. Posts may also be deleted.
Nothing on Instagram, sorry. Likes used to be possible, but not since Instagram started locking down their API. Photos, videos, and comments were never possible.
@@ -813,6 +814,7 @@ Publishing
https://brid.gy/publish/github
https://brid.gy/publish/mastodon
https://brid.gy/publish/meetup
+https://brid.gy/publish/pixelfed
https://brid.gy/publish/twitter
Your post HTML must also include that same
@@ -826,6 +828,7 @@ Publishing
<a href="https://brid.gy/publish/github"></a>
<a href="https://brid.gy/publish/mastodon"></a>
<a href="https://brid.gy/publish/meetup"></a>
+<a href="https://brid.gy/publish/pixelfed"></a>
<a href="https://brid.gy/publish/twitter"></a>
diff --git a/templates/choose_instance.html b/templates/choose_instance.html
new file mode 100644
index 00000000..5451877c
--- /dev/null
+++ b/templates/choose_instance.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+
+{% block title %}{{ site.capitalize() }} signup - Bridgy{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+...or if you're technical, try Bridgy Fed instead! No {{ site.capitalize() }} account needed.
+
+
+{% endblock %}
diff --git a/templates/index.html b/templates/index.html
index 12240116..21f71902 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -16,6 +16,7 @@
{{ sources['github'].button_html('listen')|safe }}
{{ sources['mastodon'].button_html('listen')|safe }}
{{ sources['meetup'].button_html('publish')|safe }}
+{{ sources['pixelfed'].button_html('listen')|safe }}
diff --git a/templates/mastodon_instance.html b/templates/mastodon_instance.html
deleted file mode 100644
index 8b57c7a1..00000000
--- a/templates/mastodon_instance.html
+++ /dev/null
@@ -1,34 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Mastodon signup - Bridgy{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
-
-...or if you're technical, try Bridgy Fed instead! No Mastodon account needed.
-
-
-{% endblock %}
diff --git a/templates/pixelfed_user.html b/templates/pixelfed_user.html
new file mode 100644
index 00000000..a624d8ad
--- /dev/null
+++ b/templates/pixelfed_user.html
@@ -0,0 +1,7 @@
+{% extends "social_user.html" %}
+
+{% block edit_profile %}
+
+ Edit your Pixelfed profile,
+enter it in Website or Bio, then click here:
+{% endblock %}
diff --git a/templates/preview.html b/templates/preview.html
index 33acbf0e..27230070 100644
--- a/templates/preview.html
+++ b/templates/preview.html
@@ -13,7 +13,7 @@