federated follow/block/mute
This commit is contained in:
parent
c1ef7b3892
commit
9aed05a560
36 changed files with 262 additions and 184 deletions
|
@ -102,7 +102,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
{% if request.user.is_authenticated %}
|
||||
{% include "_sidebar.html" with show_progress=1 %}
|
||||
{% include "_sidebar.html" with show_progress=1 identity=request.user.identity %}
|
||||
{% else %}
|
||||
{% include "_sidebar_anonymous.html" %}
|
||||
{% endif %}
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
{% empty %}
|
||||
<p>
|
||||
无站内条目匹配。
|
||||
{% if user.is_authenticated %}系统会尝试搜索其它网站的条目,点击标题可添加到本站。{% endif %}
|
||||
{% if request.user.is_authenticated %}系统会尝试搜索其它网站的条目,点击标题可添加到本站。{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
如果你在
|
||||
|
@ -116,7 +116,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
<div class="item-card-list">
|
||||
{% if request.GET.q and user.is_authenticated %}
|
||||
{% if request.GET.q and request.user.is_authenticated %}
|
||||
<p hx-get="{% url 'catalog:external_search' %}?q={{ request.GET.q }}&c={{ request.GET.c }}&page={% if pagination.current_page %}{{ pagination.current_page }}{% else %}1{% endif %}"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML">
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
target="_blank"
|
||||
rel="noopener"
|
||||
onclick="window.open(this.href); return false;"
|
||||
title="@{{ user.mastodon_acct }}">
|
||||
title="@{{ identity.user.mastodon_acct }}">
|
||||
<i class="fa-solid fa-circle-nodes"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -88,7 +88,7 @@
|
|||
{% for featured_collection in identity.featured_collections.all %}
|
||||
{% user_visibility_of featured_collection as visible %}
|
||||
{% if visible %}
|
||||
{% user_stats_of collection=featured_collection user=user as stats %}
|
||||
{% user_stats_of collection=featured_collection identity=identity as stats %}
|
||||
<div>
|
||||
<a href="{{ featured_collection.collection.url }}">{{ featured_collection.collection.title }}</a> <small>{{ stats.complete }} / {{ stats.total }}</small>
|
||||
<br>
|
||||
|
@ -96,7 +96,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
{% if request.user == user %}
|
||||
{% if request.user == identity.user %}
|
||||
<div class="empty">将自己或他人的收藏单设为目标,这里就会显示进度</div>
|
||||
{% else %}
|
||||
<div class="empty">暂未设置目标</div>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
from django import template
|
||||
from django.conf import settings
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from users.models import APIdentity, User
|
||||
from users.models import APIdentity
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
@ -16,7 +14,7 @@ def mastodon(domain):
|
|||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def current_user_relationship(context, target_identity: "APIdentity"):
|
||||
current_identity = (
|
||||
current_identity: "APIdentity | None" = (
|
||||
context["request"].user.identity
|
||||
if context["request"].user.is_authenticated
|
||||
else None
|
||||
|
@ -24,9 +22,7 @@ def current_user_relationship(context, target_identity: "APIdentity"):
|
|||
r = {
|
||||
"requesting": False,
|
||||
"following": False,
|
||||
"unfollowable": False,
|
||||
"muting": False,
|
||||
"unmutable": False,
|
||||
"rejecting": False,
|
||||
"status": "",
|
||||
}
|
||||
|
@ -36,10 +32,9 @@ def current_user_relationship(context, target_identity: "APIdentity"):
|
|||
) or current_identity.is_blocked_by(target_identity):
|
||||
r["rejecting"] = True
|
||||
else:
|
||||
r["requesting"] = current_identity.is_requesting(target_identity)
|
||||
r["muting"] = current_identity.is_muting(target_identity)
|
||||
r["unmutable"] = r["muting"]
|
||||
r["following"] = current_identity.is_following(target_identity)
|
||||
r["unfollowable"] = r["following"]
|
||||
if r["following"]:
|
||||
if current_identity.is_followed_by(target_identity):
|
||||
r["status"] = _("互相关注")
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import functools
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
@ -26,6 +27,27 @@ class HTTPResponseHXRedirect(HttpResponseRedirect):
|
|||
status_code = 200
|
||||
|
||||
|
||||
def target_identity_required(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(request, user_name, *args, **kwargs):
|
||||
from users.models import APIdentity
|
||||
from users.views import render_user_blocked, render_user_not_found
|
||||
|
||||
try:
|
||||
target = APIdentity.get_by_handler(user_name)
|
||||
except APIdentity.DoesNotExist:
|
||||
return render_user_not_found(request)
|
||||
if not target.is_visible_to_user(request.user):
|
||||
return render_user_blocked(request)
|
||||
request.target_identity = target
|
||||
# request.identity = (
|
||||
# request.user.identity if request.user.is_authenticated else None
|
||||
# )
|
||||
return func(request, user_name, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class PageLinksGenerator:
|
||||
# TODO inherit django paginator
|
||||
"""
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{% csrf_token %}
|
||||
{% if not application.is_official %}
|
||||
<p>
|
||||
<b>{{ application.name }}</b> 是由 <a href="{{ application.user.url }}">@{{ application.user.handler }}</a> 创建和维护的应用程序。
|
||||
<b>{{ application.name }}</b> 是由 <a href="{{ application.user.identity.url }}">@{{ application.user.identity.handler }}</a> 创建和维护的应用程序。
|
||||
{{ site_name }}无法保证其安全性和有效性,请自行验证确认后再授权。
|
||||
</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -115,7 +115,7 @@
|
|||
</section>
|
||||
<section>
|
||||
<div class="action">
|
||||
{% if request.user == collection.owner %}
|
||||
{% if request.user.identity == collection.owner %}
|
||||
<span>
|
||||
<a href="{% url 'journal:collection_edit' collection.uuid %}">{% trans '编辑' %}</a>
|
||||
</span>
|
||||
|
@ -129,7 +129,7 @@
|
|||
<span>创建于 {{ collection.created_time | date }}</span>
|
||||
</section>
|
||||
</div>
|
||||
{% include "_sidebar.html" with user=collection.owner show_profile=1 %}
|
||||
{% include "_sidebar.html" with identity=collection.owner show_profile=1 %}
|
||||
</main>
|
||||
{% include "_footer.html" %}
|
||||
</body>
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include "_sidebar.html" with show_profile=1 fold_profile=1 %}
|
||||
{% include "_sidebar.html" with show_profile=1 fold_profile=1 identity=collection.owner|default:request.user.identity %}
|
||||
</main>
|
||||
{% include "_footer.html" %}
|
||||
</body>
|
||||
|
|
|
@ -137,7 +137,7 @@
|
|||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
{% if user == request.user %}
|
||||
{% if identity.user == request.user %}
|
||||
<div class="entity-sort-control">
|
||||
<div class="entity-sort-control__button" id="sortEditButton">
|
||||
<span class="entity-sort-control__text" id="sortEditText">{% trans '编辑布局' %}</span>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<meta property="og:url" content="{{ request.build_absolute_uri }}">
|
||||
<meta property="og:image" content="{{ review.item.cover|thumb:'normal' }}">
|
||||
<meta property="og:site_name" content="{{ site_name }}">
|
||||
{% if user.preference.no_anonymous_view %}<meta name="robots" content="noindex">{% endif %}
|
||||
{% if identity.preference.no_anonymous_view %}<meta name="robots" content="noindex">{% endif %}
|
||||
<title>{{ site_name }}{% trans '评论' %} - {{ review.title }}</title>
|
||||
{% include "common_libs.html" with jquery=0 v2=1 %}
|
||||
</head>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ site_name }} - {{ user.display_name }} -
|
||||
<title>{{ site_name }} - {{ identity.display_name }} -
|
||||
{% if liked %}关注的{% endif %}
|
||||
收藏单</title>
|
||||
{% include "common_libs.html" with jquery=0 v2=1 %}
|
||||
|
@ -23,7 +23,7 @@
|
|||
<main>
|
||||
<div class="grid__main">
|
||||
<h5>
|
||||
{{ user.display_name }} -
|
||||
{{ identity.display_name }} -
|
||||
{% if liked %}关注的{% endif %}
|
||||
收藏单
|
||||
</h5>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% block title %}<title>{{ site_name }} - {{ user.display_name }}</title>{% endblock %}
|
||||
{% block title %}<title>{{ site_name }} - {{ identity.display_name }}</title>{% endblock %}
|
||||
{% include "common_libs.html" with jquery=0 v2=1 %}
|
||||
</head>
|
||||
<body>
|
||||
|
@ -19,7 +19,7 @@
|
|||
<main>
|
||||
<div class="grid__main">
|
||||
<h5>
|
||||
{% block head %}{{ user.display_name }}{% endblock %}
|
||||
{% block head %}{{ identity.display_name }}{% endblock %}
|
||||
</h5>
|
||||
<div>
|
||||
{% for member in members %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends 'user_item_list_base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
<title>{{ site_name }} - {{ user.display_name }} - {% trans '标记' %}</title>
|
||||
<title>{{ site_name }} - {{ identity.display_name }} - {% trans '标记' %}</title>
|
||||
{% endblock %}
|
||||
{% block head %}
|
||||
{{ user.display_name }} - {% trans '标记' %}
|
||||
{{ identity.display_name }} - {% trans '标记' %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "user_item_list_base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
<title>{{ site_name }} - {{ user.display_name }} - {% trans '评论' %}</title>
|
||||
<title>{{ site_name }} - {{ identity.display_name }} - {% trans '评论' %}</title>
|
||||
{% endblock %}
|
||||
{% block head %}
|
||||
{{ user.display_name }} - {% trans '评论' %}
|
||||
{{ identity.display_name }} - {% trans '评论' %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ site_name }} - {{ user.display_name }} 的标签</title>
|
||||
<title>{{ site_name }} - {{ identity.display_name }} 的标签</title>
|
||||
{% include "common_libs.html" with jquery=0 v2=1 %}
|
||||
</head>
|
||||
<body>
|
||||
|
@ -25,7 +25,7 @@
|
|||
{% for v in tags %}
|
||||
<span style="margin-right:2em; white-space: nowrap;">
|
||||
<span>
|
||||
<a href="{% url 'journal:user_tag_member_list' user.handler v.title %}">{{ v.title }}</a>
|
||||
<a href="{% url 'journal:user_tag_member_list' identity.handler v.title %}">{{ v.title }}</a>
|
||||
</span>
|
||||
<span>({{ v.total }})</span>
|
||||
</span>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
{% extends "user_item_list_base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
<title>{{ site_name }} - {{ user.display_name }} - {{ tag.title }} {% trans '标签' %}</title>
|
||||
<title>{{ site_name }} - {{ identity.display_name }} - {{ tag.title }} {% trans '标签' %}</title>
|
||||
{% endblock %}
|
||||
{% block head %}
|
||||
{{ tag.title }}
|
||||
<br>
|
||||
<small>
|
||||
{% if tag.visibility > 0 %}<i class="fa-solid fa-user" title="个人标签"></i>{% endif %}
|
||||
{{ user.display_name }}的{% trans '标签' %}
|
||||
{% if user == request.user %}
|
||||
{{ identity.display_name }}的{% trans '标签' %}
|
||||
{% if identity.user == request.user %}
|
||||
<form style="display:inline"
|
||||
hx-get="{% url 'journal:user_tag_edit' %}"
|
||||
hx-target="body"
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.template.defaultfilters import stringfilter
|
|||
|
||||
from journal.models import Collection
|
||||
from journal.models.mixins import UserOwnedObjectMixin
|
||||
from users.models.apidentity import APIdentity
|
||||
from users.models.user import User
|
||||
|
||||
register = template.Library()
|
||||
|
@ -22,8 +23,8 @@ def user_progress_of(collection: Collection, user: User):
|
|||
|
||||
|
||||
@register.simple_tag()
|
||||
def user_stats_of(collection: Collection, user: User):
|
||||
return collection.get_stats(user.identity) if user and user.is_authenticated else {}
|
||||
def user_stats_of(collection: Collection, identity: APIdentity):
|
||||
return collection.get_stats(identity) if identity else {}
|
||||
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
|
|
|
@ -48,7 +48,7 @@ def collection_retrieve(request: AuthedHttpRequest, collection_uuid):
|
|||
raise PermissionDenied()
|
||||
follower_count = collection.likes.all().count()
|
||||
following = (
|
||||
Like.user_liked_piece(request.user, collection)
|
||||
Like.user_liked_piece(request.user.identity, collection)
|
||||
if request.user.is_authenticated
|
||||
else False
|
||||
)
|
||||
|
@ -85,6 +85,7 @@ def collection_retrieve(request: AuthedHttpRequest, collection_uuid):
|
|||
"stats": stats,
|
||||
"available_as_featured": available_as_featured,
|
||||
"featured_since": featured_since,
|
||||
"editable": collection.is_editable_by(request.user),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import functools
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied
|
||||
from django.core.paginator import Paginator
|
||||
|
@ -8,9 +6,12 @@ from django.urls import reverse
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from catalog.models import *
|
||||
from common.utils import AuthedHttpRequest, PageLinksGenerator, get_uuid_or_404
|
||||
from users.models import APIdentity
|
||||
from users.views import render_user_blocked, render_user_not_found
|
||||
from common.utils import (
|
||||
AuthedHttpRequest,
|
||||
PageLinksGenerator,
|
||||
get_uuid_or_404,
|
||||
target_identity_required,
|
||||
)
|
||||
|
||||
from ..forms import *
|
||||
from ..models import *
|
||||
|
@ -18,24 +19,6 @@ from ..models import *
|
|||
PAGE_SIZE = 10
|
||||
|
||||
|
||||
def target_identity_required(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(request, user_name, *args, **kwargs):
|
||||
try:
|
||||
target = APIdentity.get_by_handler(user_name)
|
||||
except:
|
||||
return render_user_not_found(request)
|
||||
if not target.is_visible_to_user(request.user):
|
||||
return render_user_blocked(request)
|
||||
request.target_identity = target
|
||||
# request.identity = (
|
||||
# request.user.identity if request.user.is_authenticated else None
|
||||
# )
|
||||
return func(request, user_name, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def render_relogin(request):
|
||||
return render(
|
||||
request,
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "_sidebar.html" with show_progress=1 %}
|
||||
{% include "_sidebar.html" with show_progress=1 identity=request.user.identity %}
|
||||
</main>
|
||||
{% include "_footer.html" %}
|
||||
</body>
|
||||
|
|
|
@ -70,6 +70,7 @@ class Takahe:
|
|||
"""
|
||||
from users.models import APIdentity
|
||||
|
||||
logger.info(f"User {u} initialize identity")
|
||||
if not u.username:
|
||||
logger.warning(f"User {u} has no username")
|
||||
return None
|
||||
|
|
|
@ -20,6 +20,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from loguru import logger
|
||||
|
||||
from common.config import *
|
||||
from common.utils import AuthedHttpRequest
|
||||
from journal.models import remove_data_by_user
|
||||
from mastodon import mastodon_request_included
|
||||
from mastodon.api import *
|
||||
|
@ -162,18 +163,12 @@ def OAuth2_login(request):
|
|||
): # swap login for existing user
|
||||
return swap_login(request, token, site, refresh_token)
|
||||
|
||||
user = authenticate(request, token=token, site=site)
|
||||
user: User = authenticate(request, token=token, site=site) # type: ignore
|
||||
if user: # existing user
|
||||
user.mastodon_token = token # type: ignore
|
||||
user.mastodon_refresh_token = refresh_token # type: ignore
|
||||
user.save(update_fields=["mastodon_token", "mastodon_refresh_token"])
|
||||
auth_login(request, user)
|
||||
if request.session.get("next_url") is not None:
|
||||
response = redirect(request.session.get("next_url"))
|
||||
del request.session["next_url"]
|
||||
else:
|
||||
response = redirect(reverse("common:home"))
|
||||
return response
|
||||
return login_existing_user(request, user)
|
||||
else: # newly registered user
|
||||
code, user_data = verify_account(site, token)
|
||||
if code != 200 or user_data is None:
|
||||
|
@ -199,6 +194,18 @@ def register_new_user(request, **param):
|
|||
return redirect(reverse("users:register"))
|
||||
|
||||
|
||||
def login_existing_user(request, existing_user):
|
||||
auth_login(request, existing_user)
|
||||
if not existing_user.username or not existing_user.identity:
|
||||
response = redirect(reverse("account:register"))
|
||||
elif request.session.get("next_url") is not None:
|
||||
response = redirect(request.session.get("next_url"))
|
||||
del request.session["next_url"]
|
||||
else:
|
||||
response = redirect(reverse("common:home"))
|
||||
return response
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def logout(request):
|
||||
|
@ -317,8 +324,7 @@ def verify_email(request):
|
|||
elif action == "login":
|
||||
user = User.objects.get(pk=s["i"])
|
||||
if user.email == email:
|
||||
auth_login(request, user)
|
||||
return redirect(reverse("common:home"))
|
||||
return login_existing_user(request, user)
|
||||
else:
|
||||
error = _("电子邮件地址不匹配")
|
||||
elif action == "register":
|
||||
|
@ -336,7 +342,7 @@ def verify_email(request):
|
|||
|
||||
|
||||
@login_required
|
||||
def register(request):
|
||||
def register(request: AuthedHttpRequest):
|
||||
form = None
|
||||
if settings.MASTODON_ALLOW_ANY_SITE:
|
||||
form = RegistrationForm(request.POST)
|
||||
|
@ -352,7 +358,7 @@ def register(request):
|
|||
email_cleared = False
|
||||
if not form.is_valid():
|
||||
return render(request, "users/register.html", {"form": form})
|
||||
if request.user.username is None and form.cleaned_data["username"]:
|
||||
if not request.user.username and form.cleaned_data["username"]:
|
||||
if User.objects.filter(
|
||||
username__iexact=form.cleaned_data["username"]
|
||||
).exists():
|
||||
|
@ -390,13 +396,14 @@ def register(request):
|
|||
if request.user.pending_email:
|
||||
django_rq.get_queue("mastodon").enqueue(
|
||||
send_verification_link,
|
||||
request.user.id,
|
||||
request.user.pk,
|
||||
"verify",
|
||||
request.user.pending_email,
|
||||
)
|
||||
messages.add_message(request, messages.INFO, _("已发送验证邮件,请查收。"))
|
||||
if request.user.username and not request.user.identity_linked():
|
||||
request.user.initialize()
|
||||
if username_changed:
|
||||
request.user.initiatialize()
|
||||
messages.add_message(request, messages.INFO, _("用户名已设置。"))
|
||||
if email_cleared:
|
||||
messages.add_message(request, messages.INFO, _("电子邮件地址已取消关联。"))
|
||||
|
|
|
@ -63,6 +63,7 @@ def init_identity(apps, schema_editor):
|
|||
local=True,
|
||||
discoverable=not user.preference.no_anonymous_view,
|
||||
)
|
||||
takahe_identity.generate_keypair()
|
||||
takahe_user.identities.add(takahe_identity)
|
||||
|
||||
|
||||
|
|
|
@ -97,6 +97,10 @@ class APIdentity(models.Model):
|
|||
def following(self):
|
||||
return Takahe.get_following_ids(self.pk)
|
||||
|
||||
@property
|
||||
def followers(self):
|
||||
return Takahe.get_follower_ids(self.pk)
|
||||
|
||||
@property
|
||||
def muting(self):
|
||||
return Takahe.get_muting_ids(self.pk)
|
||||
|
@ -105,6 +109,26 @@ class APIdentity(models.Model):
|
|||
def blocking(self):
|
||||
return Takahe.get_blocking_ids(self.pk)
|
||||
|
||||
@property
|
||||
def following_identities(self):
|
||||
return APIdentity.objects.filter(pk__in=self.following)
|
||||
|
||||
@property
|
||||
def follower_identities(self):
|
||||
return APIdentity.objects.filter(pk__in=self.followers)
|
||||
|
||||
@property
|
||||
def muting_identities(self):
|
||||
return APIdentity.objects.filter(pk__in=self.muting)
|
||||
|
||||
@property
|
||||
def blocking_identities(self):
|
||||
return APIdentity.objects.filter(pk__in=self.blocking)
|
||||
|
||||
@property
|
||||
def follow_requesting_identities(self):
|
||||
return APIdentity.objects.filter(pk__in=self.following_request)
|
||||
|
||||
@property
|
||||
def rejecting(self):
|
||||
return Takahe.get_rejecting_ids(self.pk)
|
||||
|
@ -119,11 +143,13 @@ class APIdentity(models.Model):
|
|||
def unfollow(self, target: "APIdentity"): # this also cancels follow request
|
||||
Takahe.unfollow(self.pk, target.pk)
|
||||
|
||||
@property
|
||||
def requested_followers(self):
|
||||
Takahe.get_requested_follower_ids(self.pk)
|
||||
return Takahe.get_requested_follower_ids(self.pk)
|
||||
|
||||
@property
|
||||
def following_request(self):
|
||||
Takahe.get_following_request_ids(self.pk)
|
||||
return Takahe.get_following_request_ids(self.pk)
|
||||
|
||||
def accept_follow_request(self, target: "APIdentity"):
|
||||
Takahe.accept_follow_request(self.pk, target.pk)
|
||||
|
@ -160,6 +186,9 @@ class APIdentity(models.Model):
|
|||
def is_following(self, target: "APIdentity"):
|
||||
return target.pk in self.following
|
||||
|
||||
def is_requesting(self, target: "APIdentity"):
|
||||
return target.pk in self.following_request
|
||||
|
||||
def is_followed_by(self, target: "APIdentity"):
|
||||
return target.is_following(self)
|
||||
|
||||
|
|
|
@ -333,8 +333,14 @@ class User(AbstractUser):
|
|||
new_user.initialize()
|
||||
return new_user
|
||||
|
||||
def identity_linked(self):
|
||||
from .apidentity import APIdentity
|
||||
|
||||
return APIdentity.objects.filter(user=self).exists()
|
||||
|
||||
def initialize(self):
|
||||
Takahe.init_identity_for_local_user(self)
|
||||
self.identity.shelf_manager
|
||||
|
||||
|
||||
# TODO the following models should be deprecated soon
|
||||
|
|
|
@ -91,28 +91,34 @@
|
|||
</details>
|
||||
</article>
|
||||
{% endif %}
|
||||
<article id="local_following">
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans '正在关注的用户' %}</summary>
|
||||
{% include 'users/relationship_list.html' with name="关注" id="follow" list=request.user.local_following.all %}
|
||||
{% include 'users/relationship_list.html' with name="关注" id="follow" list=request.user.identity.following_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article id="local_following">
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans '关注了你的用户' %}</summary>
|
||||
{% include 'users/relationship_list.html' with name="关注者" id="follower" list=request.user.local_followers.all %}
|
||||
{% include 'users/relationship_list.html' with name="关注者" id="follower" list=request.user.identity.follower_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans '请求关注你的用户' %}</summary>
|
||||
{% include 'users/relationship_list.html' with name="请求关注者" id="follow_request" list=request.user.identity.follow_requesting_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans '已隐藏的用户' %}</summary>
|
||||
{% include 'users/relationship_list.html' with name="隐藏" id="mute" list=request.user.local_muting.all %}
|
||||
{% include 'users/relationship_list.html' with name="隐藏" id="mute" list=request.user.identity.muting_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans '已屏蔽的用户' %}</summary>
|
||||
{% include 'users/relationship_list.html' with name="屏蔽" id="block" list=request.user.local_blocking.all %}
|
||||
{% include 'users/relationship_list.html' with name="屏蔽" id="block" list=request.user.identity.blocking_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
|
@ -126,7 +132,7 @@
|
|||
value="{% trans '同步' %}"
|
||||
{% if not request.user.mastodon_username %}disabled{% endif %} />
|
||||
<small>
|
||||
{% if user.mastodon_last_refresh %}上次更新时间 {{ user.mastodon_last_refresh }}{% endif %}
|
||||
{% if request.user.mastodon_last_refresh %}上次更新时间 {{ request.user.mastodon_last_refresh }}{% endif %}
|
||||
</small>
|
||||
<div>
|
||||
为了正确高效的展示短评和评论,{{ site_name }}会缓存你在联邦宇宙的关注、屏蔽和隐藏列表。如果你刚刚更新过帐户的上锁状态、增减过关注、隐藏或屏蔽,希望立即生效,可以点击这里立刻更新;这类信息也会每天自动同步。
|
||||
|
@ -167,7 +173,7 @@
|
|||
</details>
|
||||
</article>
|
||||
</div>
|
||||
{% include "_sidebar.html" with show_profile=1 %}
|
||||
{% include "_sidebar.html" with show_profile=1 identity=request.user.identity %}
|
||||
</main>
|
||||
{% include "_footer.html" %}
|
||||
</body>
|
||||
|
|
|
@ -177,7 +177,7 @@
|
|||
</details>
|
||||
</article>
|
||||
</div>
|
||||
{% include "_sidebar.html" with show_profile=1 %}
|
||||
{% include "_sidebar.html" with show_profile=1 identity=request.user.identity %}
|
||||
</main>
|
||||
{% include "_footer.html" %}
|
||||
</body>
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
<strong>在联邦宇宙关注用户</strong>
|
||||
</header>
|
||||
<div>
|
||||
<p>{{ user.display_name | default:user.mastodon_acct }} 已经开启了关注审核,请复制以下ID,到你所在的联邦宇宙实例中去关注ta。</p>
|
||||
<p>{{ identity.display_name }} 已经开启了关注审核,请复制以下ID,到你所在的联邦宇宙实例中去关注ta。</p>
|
||||
<p style="text-align:center;">
|
||||
<code onclick="navigator.clipboard.writeText(this.innerText);"
|
||||
data-tooltip="点击复制">@{{ user.mastodon_acct }}</code>
|
||||
data-tooltip="点击复制">@{{ identity.user.mastodon_acct }}</code>
|
||||
</p>
|
||||
<p>如果你已经关注了ta,请耐心等待ta的审核。</p>
|
||||
{% if not request.user.mastodon_acct %}
|
||||
|
|
|
@ -6,21 +6,23 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh"
|
||||
content="0;URL={% url 'users:login' %}?next={{ request.path }}" />
|
||||
<title>{{ site_name }} - {{ user.display_name }}</title>
|
||||
<title>{{ site_name }} - {{ identity.handler }}</title>
|
||||
<link rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title="{{ site_name }} - {{ user.display_name }}的评论"
|
||||
title="{{ site_name }} - {{ identity.handler }}的评论"
|
||||
href="{{ request.build_absolute_uri }}feed/reviews/">
|
||||
<meta property="og:title"
|
||||
content="{{ site_name }} - {{ user.display_name }}的主页">
|
||||
content="{{ site_name }} - {{ identity.handler }}的主页">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}">
|
||||
<meta property="og:image"
|
||||
content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.jpg' %}">
|
||||
</head>
|
||||
<body>
|
||||
{% if user.mastodon_account.url %}
|
||||
<a href="{{ user.mastodon_account.url }}" rel="me" style="display:none">Mastodon verification</a>
|
||||
{% if identity.user.mastodon_account.url %}
|
||||
<a href="{{ identity.user.mastodon_account.url }}"
|
||||
rel="me"
|
||||
style="display:none">Mastodon verification</a>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
<img src="{% static 'img/logo.svg' %}" class="logo" alt="logo">
|
||||
</header>
|
||||
<div>
|
||||
{% if user.is_authenticated %}
|
||||
{% if request.user.is_authenticated %}
|
||||
<a href="{% url 'common:home' %}" class="button">{% trans '前往首页' %}</a>
|
||||
{% else %}
|
||||
<form action="{% url 'users:connect' %}" method="post">
|
||||
|
|
|
@ -159,7 +159,7 @@
|
|||
</details>
|
||||
</article>
|
||||
</div>
|
||||
{% include "_sidebar.html" with show_profile=1 %}
|
||||
{% include "_sidebar.html" with show_profile=1 identity=request.user.identity %}
|
||||
</main>
|
||||
{% include "_footer.html" %}
|
||||
</body>
|
||||
|
|
|
@ -24,39 +24,34 @@
|
|||
</span>
|
||||
{% endif %}
|
||||
{% if relationship.following %}
|
||||
{% if relationship.unfollowable %}
|
||||
<span>
|
||||
<a title="已关注,点击可取消关注"
|
||||
class="activated"
|
||||
hx-post="{% url 'users:unfollow' identity.handler %}"
|
||||
hx-target="closest .action"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-user-check"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% else %}
|
||||
<span><a title="已在联邦宇宙关注该用户" class="activated"><i class="fa-solid fa-circle-check"></i></a></span>
|
||||
{% endif %}
|
||||
<span>
|
||||
<a title="已关注,点击可取消关注"
|
||||
class="activated"
|
||||
hx-post="{% url 'users:unfollow' identity.handler %}"
|
||||
hx-target="closest .action"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-user-check"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% elif relationship.requesting %}
|
||||
<span>
|
||||
<a title="已发送关注请求,点击可取消"
|
||||
class="activated"
|
||||
hx-post="{% url 'users:unfollow' identity.handler %}"
|
||||
hx-target="closest .action"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-user-clock"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% else %}
|
||||
{% if identity.locked %}
|
||||
<span>
|
||||
<a title="用户已开启关注审核"
|
||||
hx-post="{% url 'users:locked' identity.handler %}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
<i class="fa-solid fa-user-shield"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% else %}
|
||||
<span>
|
||||
<a title="点击可关注该用户"
|
||||
hx-post="{% url 'users:follow' identity.handler %}"
|
||||
hx-target="closest .action"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-user-plus"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span>
|
||||
<a title="点击可关注该用户"
|
||||
hx-post="{% url 'users:follow' identity.handler %}"
|
||||
hx-target="closest .action"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-user-plus"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if not relationship.muting %}
|
||||
<span>
|
||||
|
@ -64,23 +59,17 @@
|
|||
hx-post="{% url 'users:mute' identity.handler %}"
|
||||
hx-target="closest .action"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-volume-high"></i>
|
||||
<i class="fa-regular fa-eye"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% elif relationship.unmutable %}
|
||||
{% else %}
|
||||
<span>
|
||||
<a title="已隐藏,点击可取消隐藏"
|
||||
class="activated"
|
||||
hx-post="{% url 'users:unmute' identity.handler %}"
|
||||
hx-target="closest .action"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-volume-off"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% else %}
|
||||
<span>
|
||||
<a title="已在联邦宇宙中隐藏" class="activated">
|
||||
<i class="fa-solid fa-volume-xmark"></i>
|
||||
<i class="fa-regular fa-eye-slash"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{% for user in list %}
|
||||
{% for identity in list %}
|
||||
<p style="border-bottom: gray 1px dashed; padding-bottom:4px;">
|
||||
<span class="action">{% include 'users/profile_actions.html' with show_home=1 %}</span>
|
||||
<code class="{{ id }}_handler"
|
||||
style="cursor:pointer"
|
||||
onmouseleave="$(this).removeAttr('data-tooltip')"
|
||||
onclick="navigator.clipboard.writeText(this.innerText);$(this).data('tooltip','copied');">{{ user.handler }}</code>
|
||||
onclick="navigator.clipboard.writeText(this.innerText);$(this).data('tooltip','copied');">{{ identity.handler }}</code>
|
||||
</p>
|
||||
{% empty %}
|
||||
<p class="empty">无数据</p>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<h4>验证电子邮件</h4>
|
||||
{% if success %}
|
||||
<p>
|
||||
{{ user.email }} 验证成功,<a href="{% url 'common:home' %}">点击这里返回首页</a>。
|
||||
{{ request.user.email }} 验证成功,<a href="{% url 'common:home' %}">点击这里返回首页</a>。
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
|
|
|
@ -27,7 +27,6 @@ urlpatterns = [
|
|||
path("preferences", preferences, name="preferences"),
|
||||
path("logout", logout, name="logout"),
|
||||
path("layout", set_layout, name="set_layout"),
|
||||
path("locked/<str:user_name>", follow_locked, name="locked"),
|
||||
path("follow/<str:user_name>", follow, name="follow"),
|
||||
path("unfollow/<str:user_name>", unfollow, name="unfollow"),
|
||||
path("mute/<str:user_name>", mute, name="mute"),
|
||||
|
|
126
users/views.py
126
users/views.py
|
@ -9,7 +9,11 @@ from django.urls import reverse
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.config import *
|
||||
from common.utils import HTTPResponseHXRedirect
|
||||
from common.utils import (
|
||||
AuthedHttpRequest,
|
||||
HTTPResponseHXRedirect,
|
||||
target_identity_required,
|
||||
)
|
||||
from management.models import Announcement
|
||||
from mastodon.api import *
|
||||
from takahe.utils import Takahe
|
||||
|
@ -76,81 +80,113 @@ def fetch_refresh(request):
|
|||
|
||||
|
||||
@login_required
|
||||
def follow(request, user_name):
|
||||
@target_identity_required
|
||||
def follow(request: AuthedHttpRequest, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.follow(user):
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
request.user.identity.follow(request.target_identity)
|
||||
return render(
|
||||
request,
|
||||
"users/profile_actions.html",
|
||||
context={"identity": request.target_identity},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def unfollow(request, user_name):
|
||||
@target_identity_required
|
||||
def unfollow(request: AuthedHttpRequest, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.unfollow(user):
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
request.user.identity.unfollow(request.target_identity)
|
||||
return render(
|
||||
request,
|
||||
"users/profile_actions.html",
|
||||
context={"identity": request.target_identity},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def mute(request, user_name):
|
||||
@target_identity_required
|
||||
def mute(request: AuthedHttpRequest, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.mute(user):
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
request.user.identity.mute(request.target_identity)
|
||||
return render(
|
||||
request,
|
||||
"users/profile_actions.html",
|
||||
context={"identity": request.target_identity},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def unmute(request, user_name):
|
||||
@target_identity_required
|
||||
def unmute(request: AuthedHttpRequest, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.unmute(user):
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
request.user.identity.unmute(request.target_identity)
|
||||
return render(
|
||||
request,
|
||||
"users/profile_actions.html",
|
||||
context={"identity": request.target_identity},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def block(request, user_name):
|
||||
@target_identity_required
|
||||
def block(request: AuthedHttpRequest, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.block(user):
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
request.user.identity.block(request.target_identity)
|
||||
return render(
|
||||
request,
|
||||
"users/profile_actions.html",
|
||||
context={"identity": request.target_identity},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def unblock(request, user_name):
|
||||
@target_identity_required
|
||||
def unblock(request: AuthedHttpRequest, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.unblock(user):
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
request.user.identity.unblock(request.target_identity)
|
||||
return render(
|
||||
request,
|
||||
"users/profile_actions.html",
|
||||
context={"identity": request.target_identity},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@target_identity_required
|
||||
def accept_follow_request(request: AuthedHttpRequest, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
request.user.identity.accept_follow_request(request.target_identity)
|
||||
return render(
|
||||
request,
|
||||
"users/profile_actions.html",
|
||||
context={"identity": request.target_identity},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def follow_locked(request, user_name):
|
||||
user = User.get(user_name)
|
||||
return render(request, "users/follow_locked.html", context={"user": user})
|
||||
@target_identity_required
|
||||
def reject_follow_request(request: AuthedHttpRequest, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
request.user.identity.reject_follow_request(request.target_identity)
|
||||
return render(
|
||||
request,
|
||||
"users/profile_actions.html",
|
||||
context={"identity": request.target_identity},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def set_layout(request):
|
||||
def set_layout(request: AuthedHttpRequest):
|
||||
if request.method == "POST":
|
||||
layout = json.loads(request.POST.get("layout"))
|
||||
layout = json.loads(request.POST.get("layout", {})) # type: ignore
|
||||
if request.POST.get("name") == "profile":
|
||||
request.user.preference.profile_layout = layout
|
||||
request.user.preference.save(update_fields=["profile_layout"])
|
||||
|
@ -163,7 +199,7 @@ def set_layout(request):
|
|||
|
||||
|
||||
@login_required
|
||||
def report(request):
|
||||
def report(request: AuthedHttpRequest):
|
||||
if request.method == "GET":
|
||||
user_id = request.GET.get("user_id")
|
||||
if user_id:
|
||||
|
@ -204,7 +240,7 @@ def report(request):
|
|||
|
||||
|
||||
@login_required
|
||||
def manage_report(request):
|
||||
def manage_report(request: AuthedHttpRequest):
|
||||
if not request.user.is_staff:
|
||||
raise PermissionDenied()
|
||||
if request.method == "GET":
|
||||
|
@ -224,7 +260,7 @@ def manage_report(request):
|
|||
|
||||
|
||||
@login_required
|
||||
def mark_announcements_read(request):
|
||||
def mark_announcements_read(request: AuthedHttpRequest):
|
||||
if request.method == "POST":
|
||||
try:
|
||||
request.user.read_announcement_index = Announcement.objects.latest("pk").pk
|
||||
|
@ -232,4 +268,4 @@ def mark_announcements_read(request):
|
|||
except ObjectDoesNotExist:
|
||||
# when there is no annoucenment
|
||||
pass
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
||||
|
|
Loading…
Add table
Reference in a new issue