forked from mirrors/bookwyrm
commit
857bc6adae
15 changed files with 487 additions and 5 deletions
|
@ -411,6 +411,21 @@ let BookWyrm = new class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display pop up window.
|
||||||
|
*
|
||||||
|
* @param {string} url Url to open
|
||||||
|
* @param {string} windowName windowName
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
displayPopUp(url, windowName) {
|
||||||
|
window.open(
|
||||||
|
url,
|
||||||
|
windowName,
|
||||||
|
"left=100,top=100,width=430,height=600"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
duplicateInput (event ) {
|
duplicateInput (event ) {
|
||||||
const trigger = event.currentTarget;
|
const trigger = event.currentTarget;
|
||||||
const input_id = trigger.dataset['duplicate']
|
const input_id = trigger.dataset['duplicate']
|
||||||
|
|
|
@ -267,5 +267,6 @@
|
||||||
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
|
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
|
||||||
|
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
70
bookwyrm/templates/ostatus/error.html
Normal file
70
bookwyrm/templates/ostatus/error.html
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="block">
|
||||||
|
{% if error == 'invalid_username' %}
|
||||||
|
<div class="notification is-warning has-text-centered" role="status">
|
||||||
|
<p>{% blocktrans %}<strong>{{ account }}</strong> is not a valid username{% endblocktrans %}.</p>
|
||||||
|
<p>{% trans 'Check you have the correct username before trying again' %}.</p>
|
||||||
|
</div>
|
||||||
|
{% elif error == 'user_not_found' %}
|
||||||
|
<div class="notification is-warning has-text-centered" role="status">
|
||||||
|
<p>{% blocktrans %}<strong>{{ account }}</strong> could not be found or <code>{{ remote_domain }}</code> does not support identity discovery{% endblocktrans %}.</p>
|
||||||
|
<p>{% trans 'Check you have the correct username before trying again' %}.</p>
|
||||||
|
</div>
|
||||||
|
{% elif error == 'not_supported' %}
|
||||||
|
<div class="notification is-warning has-text-centered" role="status">
|
||||||
|
<p>{% blocktrans %}<strong>{{ account }}</strong> was found but <code>{{ remote_domain }}</code> does not support 'remote follow'{% endblocktrans %}.</p>
|
||||||
|
<p>{% blocktrans %}Try searching for <strong>{{ user }}</strong> on <code>{{ remote_domain }}</code> instead{% endblocktrans %}.</p>
|
||||||
|
</div>
|
||||||
|
{% elif not request.user.is_authenticated %}
|
||||||
|
<div class="navbar-item">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<form name="login" method="post" action="{% url 'login' %}?next={{ request.path }}?acct={{ user.remote_id }}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="columns is-variable is-1">
|
||||||
|
<div class="column">
|
||||||
|
<label class="is-sr-only" for="id_localname">{% trans "Username:" %}</label>
|
||||||
|
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname" placeholder="{% trans 'username' %}">
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<label class="is-sr-only" for="id_password">{% trans "Password:" %}</label>
|
||||||
|
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="{% trans 'password' %}">
|
||||||
|
<p class="help"><a href="{% url 'password-reset' %}">{% trans "Forgot your password?" %}</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif error == 'ostatus_subscribe' %}
|
||||||
|
<div class="notification is-warning has-text-centered" role="status">
|
||||||
|
<p>{% blocktrans %}Something went wrong trying to follow <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||||
|
<p>{% trans 'Check you have the correct username before trying again.' %}</p>
|
||||||
|
</div>
|
||||||
|
{% elif error == 'is_blocked' %}
|
||||||
|
<div class="notification is-danger has-text-centered" role="status">
|
||||||
|
<p>{% blocktrans %}You have blocked <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||||
|
</div>
|
||||||
|
{% elif error == 'has_blocked' %}
|
||||||
|
<div class="notification is-danger has-text-centered" role="status">
|
||||||
|
<p>{% blocktrans %}<strong>{{ account }}</strong> has blocked you{% endblocktrans %}</p>
|
||||||
|
</div>
|
||||||
|
{% elif error == 'already_following' %}
|
||||||
|
<div class="notification is-success has-text-centered" role="status">
|
||||||
|
<p>{% blocktrans %}You are already following <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||||
|
</div>
|
||||||
|
{% elif error == 'already_requested' %}
|
||||||
|
<div class="notification is-success has-text-centered" role="status">
|
||||||
|
<p>{% blocktrans %}You have already requested to follow <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="block is-pulled-right">
|
||||||
|
<button type="button" class="button" onclick="closeWindow()">Close window</button>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
46
bookwyrm/templates/ostatus/remote_follow.html
Normal file
46
bookwyrm/templates/ostatus/remote_follow.html
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{% extends 'ostatus/template.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% blocktrans with username=user.localname sitename=site.name %}Follow {{ username }} on the fediverse{% endblocktrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="block card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<a href="{{ user.local_path }}" class="media-left">
|
||||||
|
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||||
|
</a>
|
||||||
|
<div class="media-content">
|
||||||
|
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||||
|
<span class="title is-4 is-block">
|
||||||
|
{{ user.display_name }}
|
||||||
|
{% if user.manually_approves_followers %}
|
||||||
|
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||||
|
<span class="is-sr-only">{% trans "Locked account" %}</span>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="block">
|
||||||
|
<p>{% blocktrans with username=user.display_name %}Follow {{ username }} from another Fediverse account like BookWyrm, Mastodon, or Pleroma.{% endblocktrans %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<section class="card-content content">
|
||||||
|
<form name="remote-follow" method="post" action="{% url 'remote-follow' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="user" value="{{ user.id }}">
|
||||||
|
<label class="label" for="remote_user">{% trans 'User handle to follow from:' %}</label>
|
||||||
|
<input class="input" type="text" name="remote_user" id="remote_user" placeholder="user@example.social" required>
|
||||||
|
<button class="button mt-1 is-primary" type="submit">{% trans 'Follow!' %}</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
15
bookwyrm/templates/ostatus/remote_follow_button.html
Normal file
15
bookwyrm/templates/ostatus/remote_follow_button.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% if request.user == user %}
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<div class="field mb-0">
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-small is-link" href="{% url 'remote-follow-page' %}?user={{ user.username }}" target="_blank" rel="noopener noreferrer" onclick="BookWyrm.displayPopUp(`{% url 'remote-follow-page' %}?user={{ user.username }}`, `remoteFollow`); return false;" aria-describedby="remote_follow_warning">
|
||||||
|
{% blocktrans with username=user.localname %}Follow on Fediverse{% endblocktrans %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p id="remote_follow_warning" class="mt-1 is-size-7 has-text-weight-light">
|
||||||
|
{% trans 'This link opens in a pop-up window' %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
63
bookwyrm/templates/ostatus/subscribe.html
Normal file
63
bookwyrm/templates/ostatus/subscribe.html
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
{% extends 'ostatus/template.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
{% load markdown %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if not request.user.is_authenticated %}
|
||||||
|
{% blocktrans with sitename=site.name %}Log in to {{ sitename }}{% endblocktrans %}
|
||||||
|
{% elif error %}
|
||||||
|
{% blocktrans with sitename=site.name %}Error following from {{ sitename }}{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans with sitename=site.name %}Follow from {{ sitename }}{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% if error %}
|
||||||
|
{% trans 'Uh oh...' %}
|
||||||
|
{% elif not request.user.is_authenticated %}
|
||||||
|
{% trans "Let's log in first..." %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans with sitename=site.name %}Follow from {{ sitename }}{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if error or not request.user.is_authenticated %}
|
||||||
|
{% include 'ostatus/error.html' with error=error user=user account=account %}
|
||||||
|
{% else %}
|
||||||
|
<div class="block card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<a href="{{ user.local_path }}" class="media-left">
|
||||||
|
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||||
|
</a>
|
||||||
|
<div class="media-content">
|
||||||
|
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||||
|
<span class="title is-4 is-block">
|
||||||
|
{{ user.display_name }}
|
||||||
|
{% if user.manually_approves_followers %}
|
||||||
|
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||||
|
<span class="is-sr-only">{% trans 'Locked account' %}</span>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||||
|
</a>
|
||||||
|
<form name="follow" method="post" action="{% url 'follow' %}/?next={% url 'ostatus-success' %}?following={{ user.username }}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input name="user" value="{{ user.username }}" hidden>
|
||||||
|
<button class="button is-link" type="submit">{% blocktrans with username=user.display_name %}Follow {{ username }}{% endblocktrans %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% if user.summary %}
|
||||||
|
{{ user.summary|to_markdown|safe|truncatechars_html:120 }}
|
||||||
|
{% else %} {% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
35
bookwyrm/templates/ostatus/success.html
Normal file
35
bookwyrm/templates/ostatus/success.html
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends 'ostatus/template.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="block card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<a href="{{ user.local_path }}" class="media-left">
|
||||||
|
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||||
|
</a>
|
||||||
|
<div class="media-content">
|
||||||
|
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||||
|
<span class="title is-4 is-block">
|
||||||
|
{{ user.display_name }}
|
||||||
|
{% if user.manually_approves_followers %}
|
||||||
|
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||||
|
<span class="is-sr-only">{% trans "Locked account" %}</span>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="notification is-success">
|
||||||
|
<span class="icon icon-check m-0-mobile" aria-hidden="true"></span>
|
||||||
|
<span>{% blocktrans with display_name=user.display_name %}You are now following {{ display_name }}!{% endblocktrans %}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="block is-pulled-right">
|
||||||
|
<button type="button" class="button" onclick="closeWindow()">Close window</button>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
41
bookwyrm/templates/ostatus/template.html
Normal file
41
bookwyrm/templates/ostatus/template.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{% load layout %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
{% load utilities %}
|
||||||
|
{% load markdown %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{% get_lang %}">
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/vendor/bulma.min.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/vendor/icons.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/bookwyrm.css' %}">
|
||||||
|
<script>
|
||||||
|
function closeWindow() {
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar" aria-label="main navigation">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<img class="image logo navbar-item" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static 'images/logo-small.png' %}{% endif %}" alt="Home page">
|
||||||
|
<h2 class="navbar-item subtitle">{% block heading %}{% endblock %}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="section is-flex-grow-1 columns is-centered">
|
||||||
|
<div class="block column is-one-third">
|
||||||
|
{% block content%}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var csrf_token = '{{ csrf_token }}';
|
||||||
|
</script>
|
||||||
|
<script src="{% static 'js/bookwyrm.js' %}?v={{ js_cache }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -39,6 +39,9 @@
|
||||||
{% if not is_self and request.user.is_authenticated %}
|
{% if not is_self and request.user.is_authenticated %}
|
||||||
{% include 'snippets/follow_button.html' with user=user %}
|
{% include 'snippets/follow_button.html' with user=user %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not is_self %}
|
||||||
|
{% include 'ostatus/remote_follow_button.html' with user=user %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if is_self and user.follower_requests.all %}
|
{% if is_self and user.follower_requests.all %}
|
||||||
<div class="follow-requests">
|
<div class="follow-requests">
|
||||||
|
|
|
@ -4,10 +4,12 @@ from unittest.mock import patch
|
||||||
|
|
||||||
from django.contrib.auth.models import Group, Permission
|
from django.contrib.auth.models import Group, Permission
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
from bookwyrm import models, views
|
from bookwyrm import models, views
|
||||||
|
from bookwyrm.tests.validate_html import validate_html
|
||||||
|
|
||||||
|
|
||||||
@patch("bookwyrm.activitystreams.add_user_statuses_task.delay")
|
@patch("bookwyrm.activitystreams.add_user_statuses_task.delay")
|
||||||
|
@ -16,6 +18,7 @@ class FollowViews(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""we need basic test data and mocks"""
|
"""we need basic test data and mocks"""
|
||||||
|
models.SiteSettings.objects.create()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||||
|
@ -174,3 +177,43 @@ class FollowViews(TestCase):
|
||||||
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
|
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
|
||||||
# follow relationship should not exist
|
# follow relationship should not exist
|
||||||
self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0)
|
self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0)
|
||||||
|
|
||||||
|
def test_ostatus_follow_request(self, _):
|
||||||
|
"""check ostatus subscribe template loads"""
|
||||||
|
request = self.factory.get(
|
||||||
|
"", {"acct": "https%3A%2F%2Fexample.com%2Fusers%2Frat"}
|
||||||
|
)
|
||||||
|
request.user = self.local_user
|
||||||
|
result = views.ostatus_follow_request(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
validate_html(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_remote_follow_page(self, _):
|
||||||
|
"""check remote follow page loads"""
|
||||||
|
request = self.factory.get("", {"acct": "mouse@local.com"})
|
||||||
|
request.user = self.remote_user
|
||||||
|
result = views.remote_follow_page(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
validate_html(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_ostatus_follow_success(self, _):
|
||||||
|
"""check remote follow success page loads"""
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.remote_user
|
||||||
|
request.following = "mouse@local.com"
|
||||||
|
result = views.ostatus_follow_success(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
validate_html(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_remote_follow(self, _):
|
||||||
|
"""check follow from remote page loads"""
|
||||||
|
request = self.factory.post("", {"user": self.remote_user.id})
|
||||||
|
request.user = self.remote_user
|
||||||
|
request.remote_user = "mouse@local.com"
|
||||||
|
result = views.remote_follow(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
validate_html(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
|
@ -44,6 +44,7 @@ urlpatterns = [
|
||||||
re_path(r"^api/v1/instance/?$", views.instance_info),
|
re_path(r"^api/v1/instance/?$", views.instance_info),
|
||||||
re_path(r"^api/v1/instance/peers/?$", views.peers),
|
re_path(r"^api/v1/instance/peers/?$", views.peers),
|
||||||
re_path(r"^opensearch.xml$", views.opensearch, name="opensearch"),
|
re_path(r"^opensearch.xml$", views.opensearch, name="opensearch"),
|
||||||
|
re_path(r"^ostatus_subscribe/?$", views.ostatus_follow_request),
|
||||||
# polling updates
|
# polling updates
|
||||||
re_path("^api/updates/notifications/?$", views.get_notification_count),
|
re_path("^api/updates/notifications/?$", views.get_notification_count),
|
||||||
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count),
|
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count),
|
||||||
|
@ -450,4 +451,9 @@ urlpatterns = [
|
||||||
re_path(r"^unfollow/?$", views.unfollow, name="unfollow"),
|
re_path(r"^unfollow/?$", views.unfollow, name="unfollow"),
|
||||||
re_path(r"^accept-follow-request/?$", views.accept_follow_request),
|
re_path(r"^accept-follow-request/?$", views.accept_follow_request),
|
||||||
re_path(r"^delete-follow-request/?$", views.delete_follow_request),
|
re_path(r"^delete-follow-request/?$", views.delete_follow_request),
|
||||||
|
re_path(r"^ostatus_follow/?$", views.remote_follow, name="remote-follow"),
|
||||||
|
re_path(r"^remote_follow/?$", views.remote_follow_page, name="remote-follow-page"),
|
||||||
|
re_path(
|
||||||
|
r"^ostatus_success/?$", views.ostatus_follow_success, name="ostatus-success"
|
||||||
|
),
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
|
@ -58,7 +58,14 @@ from .author import Author, EditAuthor
|
||||||
from .directory import Directory
|
from .directory import Directory
|
||||||
from .discover import Discover
|
from .discover import Discover
|
||||||
from .feed import DirectMessage, Feed, Replies, Status
|
from .feed import DirectMessage, Feed, Replies, Status
|
||||||
from .follow import follow, unfollow
|
from .follow import (
|
||||||
|
follow,
|
||||||
|
unfollow,
|
||||||
|
ostatus_follow_request,
|
||||||
|
ostatus_follow_success,
|
||||||
|
remote_follow,
|
||||||
|
remote_follow_page,
|
||||||
|
)
|
||||||
from .follow import accept_follow_request, delete_follow_request
|
from .follow import accept_follow_request, delete_follow_request
|
||||||
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
|
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
|
||||||
from .goal import Goal, hide_goal
|
from .goal import Goal, hide_goal
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
""" views for actions you can take in the application """
|
""" views for actions you can take in the application """
|
||||||
|
import urllib.parse
|
||||||
|
import re
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from .helpers import get_user_from_username
|
from .helpers import (
|
||||||
|
get_user_from_username,
|
||||||
|
handle_remote_webfinger,
|
||||||
|
subscribe_remote_webfinger,
|
||||||
|
WebFingerError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -23,6 +31,9 @@ def follow(request):
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if request.GET.get("next"):
|
||||||
|
return redirect(request.GET.get("next", "/"))
|
||||||
|
|
||||||
return redirect(to_follow.local_path)
|
return redirect(to_follow.local_path)
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,3 +95,91 @@ def delete_follow_request(request):
|
||||||
|
|
||||||
follow_request.delete()
|
follow_request.delete()
|
||||||
return redirect(f"/user/{request.user.localname}")
|
return redirect(f"/user/{request.user.localname}")
|
||||||
|
|
||||||
|
|
||||||
|
def ostatus_follow_request(request):
|
||||||
|
"""prepare an outgoing remote follow request"""
|
||||||
|
uri = urllib.parse.unquote(request.GET.get("acct"))
|
||||||
|
username_parts = re.search(
|
||||||
|
r"(?:^http(?:s?):\/\/)([\w\-\.]*)(?:.)*(?:(?:\/)([\w]*))", uri
|
||||||
|
)
|
||||||
|
account = f"{username_parts[2]}@{username_parts[1]}"
|
||||||
|
user = handle_remote_webfinger(account)
|
||||||
|
error = None
|
||||||
|
|
||||||
|
if user is None or user == "":
|
||||||
|
error = "ostatus_subscribe"
|
||||||
|
|
||||||
|
# don't do these checks for AnonymousUser before they sign in
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
|
||||||
|
# you have blocked them so you probably don't want to follow
|
||||||
|
if hasattr(request.user, "blocks") and user in request.user.blocks.all():
|
||||||
|
error = "is_blocked"
|
||||||
|
# they have blocked you
|
||||||
|
if hasattr(user, "blocks") and request.user in user.blocks.all():
|
||||||
|
error = "has_blocked"
|
||||||
|
# you're already following them
|
||||||
|
if hasattr(user, "followers") and request.user in user.followers.all():
|
||||||
|
error = "already_following"
|
||||||
|
# you're not following yet but you already asked
|
||||||
|
if (
|
||||||
|
hasattr(user, "follower_requests")
|
||||||
|
and request.user in user.follower_requests.all()
|
||||||
|
):
|
||||||
|
error = "already_requested"
|
||||||
|
|
||||||
|
data = {"account": account, "user": user, "error": error}
|
||||||
|
|
||||||
|
return TemplateResponse(request, "ostatus/subscribe.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def ostatus_follow_success(request):
|
||||||
|
"""display success message for remote follow"""
|
||||||
|
user = get_user_from_username(request.user, request.GET.get("following"))
|
||||||
|
data = {"account": user.name, "user": user, "error": None}
|
||||||
|
return TemplateResponse(request, "ostatus/success.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
def remote_follow_page(request):
|
||||||
|
"""display remote follow page"""
|
||||||
|
user = get_user_from_username(request.user, request.GET.get("user"))
|
||||||
|
data = {"user": user}
|
||||||
|
return TemplateResponse(request, "ostatus/remote_follow.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def remote_follow(request):
|
||||||
|
"""direct user to follow from remote account using ostatus subscribe protocol"""
|
||||||
|
remote_user = request.POST.get("remote_user")
|
||||||
|
try:
|
||||||
|
if remote_user[0] == "@":
|
||||||
|
remote_user = remote_user[1:]
|
||||||
|
remote_domain = remote_user.split("@")[1]
|
||||||
|
except (TypeError, IndexError):
|
||||||
|
remote_domain = None
|
||||||
|
|
||||||
|
wf_response = subscribe_remote_webfinger(remote_user)
|
||||||
|
user = get_object_or_404(models.User, id=request.POST.get("user"))
|
||||||
|
|
||||||
|
if wf_response is None:
|
||||||
|
data = {
|
||||||
|
"account": remote_user,
|
||||||
|
"user": user,
|
||||||
|
"error": "not_supported",
|
||||||
|
"remote_domain": remote_domain,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "ostatus/subscribe.html", data)
|
||||||
|
|
||||||
|
if isinstance(wf_response, WebFingerError):
|
||||||
|
data = {
|
||||||
|
"account": remote_user,
|
||||||
|
"user": user,
|
||||||
|
"error": str(wf_response),
|
||||||
|
"remote_domain": remote_domain,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "ostatus/subscribe.html", data)
|
||||||
|
|
||||||
|
url = wf_response.replace("{uri}", urllib.parse.quote(user.remote_id))
|
||||||
|
return redirect(url)
|
||||||
|
|
|
@ -16,6 +16,13 @@ from bookwyrm.status import create_generated_note
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unnecessary-pass
|
||||||
|
class WebFingerError(Exception):
|
||||||
|
"""empty error class for problems finding user information with webfinger"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_user_from_username(viewer, username):
|
def get_user_from_username(viewer, username):
|
||||||
"""helper function to resolve a localname or a username to a user"""
|
"""helper function to resolve a localname or a username to a user"""
|
||||||
if viewer.is_authenticated and viewer.localname == username:
|
if viewer.is_authenticated and viewer.localname == username:
|
||||||
|
@ -57,10 +64,8 @@ def handle_remote_webfinger(query):
|
||||||
# usernames could be @user@domain or user@domain
|
# usernames could be @user@domain or user@domain
|
||||||
if not query:
|
if not query:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if query[0] == "@":
|
if query[0] == "@":
|
||||||
query = query[1:]
|
query = query[1:]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
domain = query.split("@")[1]
|
domain = query.split("@")[1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
@ -86,6 +91,35 @@ def handle_remote_webfinger(query):
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def subscribe_remote_webfinger(query):
|
||||||
|
"""get subscribe template from other servers"""
|
||||||
|
template = None
|
||||||
|
# usernames could be @user@domain or user@domain
|
||||||
|
if not query:
|
||||||
|
return WebFingerError("invalid_username")
|
||||||
|
|
||||||
|
if query[0] == "@":
|
||||||
|
query = query[1:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
domain = query.split("@")[1]
|
||||||
|
except IndexError:
|
||||||
|
return WebFingerError("invalid_username")
|
||||||
|
|
||||||
|
url = f"https://{domain}/.well-known/webfinger?resource=acct:{query}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = get_data(url)
|
||||||
|
except (ConnectorException, HTTPError):
|
||||||
|
return WebFingerError("user_not_found")
|
||||||
|
|
||||||
|
for link in data.get("links"):
|
||||||
|
if link.get("rel") == "http://ostatus.org/schema/1.0/subscribe":
|
||||||
|
template = link["template"]
|
||||||
|
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
def get_edition(book_id):
|
def get_edition(book_id):
|
||||||
"""look up a book in the db and return an edition"""
|
"""look up a book in the db and return an edition"""
|
||||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||||
|
|
|
@ -30,7 +30,11 @@ def webfinger(request):
|
||||||
"rel": "self",
|
"rel": "self",
|
||||||
"type": "application/activity+json",
|
"type": "application/activity+json",
|
||||||
"href": user.remote_id,
|
"href": user.remote_id,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||||
|
"template": f"https://{DOMAIN}/ostatus_subscribe?acct={{uri}}",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue