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 00000000..d2f402d2 Binary files /dev/null and b/static/pixelfed_icon.png differ diff --git a/static/robots.txt b/static/robots.txt index 8573558b..92683c87 100644 --- a/static/robots.txt +++ b/static/robots.txt @@ -12,6 +12,7 @@ Disallow: /like Disallow: /log Disallow: /mastodon Disallow: /medium +Disallow: /pixelfed Disallow: /post Disallow: /react Disallow: /repost diff --git a/static/style.css b/static/style.css index 49be390e..fc385926 100644 --- a/static/style.css +++ b/static/style.css @@ -305,6 +305,12 @@ button[disabled]:hover { background-color: #323946; } +.pixelfed-button { + height: 50px; + padding: 3px; + background-color: #EEEEEE; +} + #preview-text hr { border-color: #cccccc; } @@ -315,7 +321,7 @@ button[disabled]:hover { margin-top: 1em; } -.mastodon-embed { +.mastodon-embed, .pixelfed-embed { margin: 1em; } diff --git a/tasks.py b/tasks.py index 69cd4fb5..53473f35 100644 --- a/tasks.py +++ b/tasks.py @@ -23,7 +23,7 @@ import original_post_discovery import util # need to import model class definitions since poll creates and saves entities. -import blogger, flickr, github, instagram, mastodon, medium, tumblr, twitter, wordpress_rest +import blogger, flickr, github, instagram, mastodon, medium, pixelfed, tumblr, twitter, wordpress_rest class Poll(webapp2.RequestHandler): @@ -56,7 +56,7 @@ def _last_poll_url(self, source): def post(self, *path_args): self.request.headers['Content-Type'] = 'application/x-www-form-urlencoded' - logging.debug('Params: %s', list(self.request.params.items())) + logging.info('Params: %s', list(self.request.params.items())) key = self.request.params['source_key'] source = self.source = ndb.Key(urlsafe=key).get() diff --git a/templates/about.html b/templates/about.html index fffb4240..b5650df4 100644 --- a/templates/about.html +++ b/templates/about.html @@ -288,8 +288,7 @@
  • Can 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:

    @@ -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):