federated follow/block/mute

This commit is contained in:
Your Name 2023-08-13 23:11:12 -04:00 committed by Henri Dickson
parent c1ef7b3892
commit 9aed05a560
36 changed files with 262 additions and 184 deletions

View file

@ -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 %}

View file

@ -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">

View file

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

View file

@ -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"] = _("互相关注")

View file

@ -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
"""

View file

@ -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 %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

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

View file

@ -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"

View file

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

View file

@ -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),
},
)

View file

@ -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,

View file

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

View file

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

View file

@ -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, _("电子邮件地址已取消关联。"))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 %}

View file

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

View file

@ -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">

View file

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

View file

@ -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 %}

View file

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

View file

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

View file

@ -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"),

View file

@ -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", "/"))