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 @@
    - {% if source.SHORT_NAME == 'mastodon' %} + {% if source.SHORT_NAME in ('mastodon', 'pixelfed') %} {% endif %} diff --git a/templates/social_user.html b/templates/social_user.html index 26cf7d0c..b0c4a0e4 100644 --- a/templates/social_user.html +++ b/templates/social_user.html @@ -77,7 +77,7 @@ title="Disable sending responses for this account. (Won't delete responses you've already received.)" {% endif %} >Disable - {% if source.SHORT_NAME == 'mastodon' %} + {% if source.SHORT_NAME in ('mastodon', 'pixelfed') %} {% endif %}

    @@ -196,7 +196,7 @@ - {% if source.SHORT_NAME == 'mastodon' %} + {% if source.SHORT_NAME in ('mastodon', 'pixelfed') %} {% endif %}

    diff --git a/tests/test_models.py b/tests/test_models.py index d0a977f3..42ccc8c9 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -303,6 +303,19 @@ def test_create_new_already_exists(self): msg = next(iter(self.handler.messages)) self.assertIn('Updated fake (FakeSource)', msg) + def test_create_new_already_exists_preserve_domains(self): + key = FakeSource.new( + None, features=['listen'], domains=['x'], domain_urls=['http://x'], + webmention_endpoint='http://x/y').put() + + FakeSource.string_id_counter -= 1 + FakeSource.create_new(self.handler) + self.assertEqual(1, FakeSource.query().count()) + + source = FakeSource.query().get() + self.assert_equals(['x'], source.domains) + self.assert_equals(['http://x'], source.domain_urls) + def test_create_new_publish(self): """If a source is publish only, we shouldn't insert a poll task.""" FakeSource.create_new(self.handler, features=['publish']) diff --git a/tests/test_pixelfed.py b/tests/test_pixelfed.py new file mode 100644 index 00000000..e18322e0 --- /dev/null +++ b/tests/test_pixelfed.py @@ -0,0 +1,26 @@ +"""Unit tests for pixelfed.py.""" +from oauth_dropins import pixelfed as oauth_pixelfed +from oauth_dropins.webutil.util import json_dumps, json_loads + +from . import testutil +from pixelfed import Pixelfed + + +class PixelfedTest(testutil.ModelsTest): + + def setUp(self): + super(PixelfedTest, self).setUp() + + app = oauth_pixelfed.PixelfedApp(instance='https://foo.com', data='') + app.put() + self.auth_entity = oauth_pixelfed.PixelfedAuth( + id='@me@foo.com', access_token_str='towkin', app=app.key, user_json=json_dumps({ + 'id': '123', + 'username': 'me', + 'acct': 'me', + 'url': 'https://foo.com/@me', + 'display_name': 'Ryan Barrett', + 'avatar': 'http://pi.ct/ure', + })) + self.auth_entity.put() + self.p = Pixelfed.new(self.handler, auth_entity=self.auth_entity) diff --git a/tests/test_publish.py b/tests/test_publish.py index 2e6d9128..025009d1 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -298,7 +298,7 @@ def test_bad_target_url(self): 'https://brid.gy/publish/instagram', ): self.assert_error( - 'Target must be brid.gy/publish/{flickr,github,mastodon,meetup,twitter}', + 'Target must be brid.gy/publish/{flickr,github,mastodon,meetup,pixelfed,twitter}', target=target) def test_source_url_redirects(self):