unique username and unique email; move mastodon username to a separate field

This commit is contained in:
Your Name 2023-06-30 23:53:53 -04:00 committed by Henri Dickson
parent ae22923027
commit 83aeec1f2b
38 changed files with 300 additions and 185 deletions

View file

@ -227,7 +227,6 @@ STATICFILES_FINDERS = [
AUTH_USER_MODEL = "users.User"
SILENCED_SYSTEM_CHECKS = [
"auth.W004", # User.username is non-unique
"admin.E404", # Required by django-user-messages
]

View file

@ -45,9 +45,9 @@
</span>
<span>
{% if comment.rating_grade %}{{ comment.rating_grade|rating_star }}{% endif %}
<a href="{% url 'journal:user_profile' comment.owner.mastodon_username %}"
<a href="{{ comment.owner.url }}"
class="nickname"
title="@{{ comment.owner.mastodon_username }}">{{ comment.owner.display_name }}</a>
title="@{{ comment.owner.handler }}">{{ comment.owner.display_name }}</a>
</span>
<span class="action inline">
<span class="timestamp">

View file

@ -63,9 +63,9 @@
</span>
<span>
{% if comment.rating_grade %}{{ comment.rating_grade|rating_star }}{% endif %}
<a href="{% url 'journal:user_profile' comment.owner.mastodon_username %}"
<a href="{{ comment.owner.url }}"
class="nickname"
title="@{{ comment.owner.mastodon_username }}">{{ comment.owner.display_name }}</a>
title="@{{ comment.owner.handler }}">{{ comment.owner.display_name }}</a>
</span>
<span class="action inline">
<span class="timestamp">

View file

@ -23,9 +23,9 @@
</span>
<span>
{% if review.rating_grade %}{{ review.rating_grade|rating_star }}{% endif %}
<a href="{% url 'journal:user_profile' review.owner.mastodon_username %}"
<a href="{{ review.owner.url }}"
class="nickname"
title="@{{ review.owner.mastodon_username }}">{{ review.owner.display_name }}</a>
title="@{{ review.owner.handler }}">{{ review.owner.display_name }}</a>
</span>
<span class="action inline">
<span class="timestamp">{{ review.created_time|date }}</span>

View file

@ -25,7 +25,7 @@
<div class="tag-list">
{% for tag in mark.tags %}
<span>
<a href="{% url 'journal:user_tag_member_list' request.user.mastodon_username tag %}">{{ tag }}</a>
<a href="{% url 'journal:user_tag_member_list' request.user.handler tag %}">{{ tag }}</a>
</span>
{% endfor %}
</div>

View file

@ -146,7 +146,7 @@
{% if item.last_editor and item.last_editor.preference.show_last_edit %}
<span>
{% trans '最近编辑:' %}
<a href="{% url 'journal:user_profile' item.last_editor.mastodon_username %}">{{ item.last_editor | default:"" }}</a>
<a href="{{ item.last_editor.url }}">{{ item.last_editor | default:"" }}</a>
</span>
{% endif %}
</div>

View file

@ -48,8 +48,7 @@
<span class="timestamp">{{ mark.created_time|date }}</span>
</div>
<div>
<a href="{% url 'journal:user_profile' mark.owner.mastodon_username %}"
title="@{{ mark.owner.mastodon_username }}">{{ mark.owner.display_name }}</a>
<a href="{{ mark.owner.url }}" title="@{{ mark.owner.handler }}">{{ mark.owner.display_name }}</a>
<span>{{ mark.action_label }}</span>
{% if mark.rating_grade %}{{ mark.rating_grade|rating_star }}{% endif %}
{% if mark.comment.focus_item %}

View file

@ -46,9 +46,9 @@
</span>
-
<span>
<a href="{% url 'journal:user_profile' review.owner.mastodon_username %}"
<a href="{{ review.owner.url }}"
class="nickname"
title="@{{ review.owner.mastodon_username }}">{{ review.owner.display_name }}</a>
title="@{{ review.owner.handler }}">{{ review.owner.display_name }}</a>
</span>
</div>
<div>{{ review.plain_content | truncate:200 }}</div>

View file

@ -38,8 +38,7 @@
<summary>
<div>
<div class="avatar">
<a href="{% url 'journal:user_profile' user.mastodon_username %}"
onclick="window.location = this.href">
<a href="{{ user.url }}" onclick="window.location = this.href">
{% comment %} onclick to workaround webkit issue with <a /> in <summary /> {% endcomment %}
<img src="{{ user.mastodon_account.avatar }}" alt="">
</a>
@ -52,7 +51,7 @@
target="_blank"
rel="noopener"
onclick="window.open(this.href)">
<span class="handler">@{{ user.mastodon_username }}</span>
<span class="handler">@{{ user.handler }}</span>
</a>
{% current_user_relationship user as relationship %}
{% if relationship %}<span>{{ relationship }}</span>{% endif %}
@ -188,7 +187,7 @@
<div class="tag-list">
{% for t in top_tags %}
<span>
<a href="{% url 'journal:user_tag_member_list' user.mastodon_username t %}">{{ t }}</a>
<a href="{% url 'journal:user_tag_member_list' user.handler t %}">{{ t }}</a>
</span>
{% empty %}
<div class="empty">暂无可见标签</div>
@ -196,7 +195,7 @@
</div>
<small>
{% if top_tags %}
<a href="{% url 'journal:user_tag_list' user.mastodon_username %}">...{% trans '全部' %}</a>
<a href="{% url 'journal:user_tag_list' user.handler %}">...{% trans '全部' %}</a>
{% endif %}
</small>
</details>

View file

@ -6,18 +6,14 @@ from django.contrib.auth.decorators import login_required
@login_required
def me(request):
return redirect(
reverse("journal:user_profile", args=[request.user.mastodon_username])
)
return redirect(request.user.url)
def home(request):
if request.user.is_authenticated:
home = request.user.get_preference().classic_homepage
if home == 1:
return redirect(
reverse("journal:user_profile", args=[request.user.mastodon_username])
)
return redirect(request.user.url)
elif home == 2:
return redirect(reverse("social:feed"))
else:

View file

@ -10,7 +10,7 @@
{% csrf_token %}
{% if not application.is_official %}
<p>
<b>{{ application.name }}</b> 是由 <a href="{% url 'journal:user_profile' application.user.mastodon_username %}">{{ application.user.mastodon_username }}</a> 创建和维护的应用程序。
<b>{{ application.name }}</b> 是由 <a href="{{ application.user.url }}">@{{ application.user.handler }}</a> 创建和维护的应用程序。
{{ site_name }}无法保证其安全性和有效性,请自行验证确认后再授权。
</p>
{% endif %}

View file

@ -36,9 +36,10 @@ class OPMLImporter:
def import_from_file_task(self, feeds):
print(f"{self.user} import opml start")
skip = 0
collection = None
if self.mode == 1:
collection = Collection.objects.create(
owner=self.user, title=f"{self.user.username}的播客订阅列表"
owner=self.user, title=f"{self.user.display_name}的播客订阅列表"
)
for feed in feeds:
print(f"{self.user} import {feed.url}")
@ -56,7 +57,7 @@ class OPMLImporter:
mark.update(
ShelfType.PROGRESS, None, None, visibility=self.visibility
)
elif self.mode == 1:
elif self.mode == 1 and collection:
collection.append_item(item)
print(f"{self.user} import opml end")
msg.success(

View file

@ -44,7 +44,7 @@ class UserOwnedObjectMixin:
filter(
lambda _entity: _entity.is_visible_to(request_user)
and (
_entity.owner.mastodon_username in request_user.mastodon_following
_entity.owner.mastodon_acct in request_user.mastodon_following
if following_only
else True
),

View file

@ -60,7 +60,7 @@
<span class="timestamp">{{ mark.created_time|date }}</span>
</div>
<div>
{% comment %} <a href="{% url 'journal:user_profile' mark.owner.mastodon_username %}" title="@{{ mark.owner.mastodon_username }}">{{ mark.owner.display_name }}</a> {% endcomment %}
{% comment %} <a href="{{mark.owner.url }}" title="@{{ mark.owner.handler }}">{{ mark.owner.display_name }}</a> {% endcomment %}
<span>{{ mark.action_label }}</span>
{% if mark.rating_grade %}{{ mark.rating_grade|rating_star }}{% endif %}
{% if mark.comment.focus_item %}

View file

@ -19,7 +19,7 @@
<meta property="og:description" content="{{ collection.description }}">
<meta property="og:type" content="article">
<meta property="og:article:author"
content="{{ collection.owner.username }}">
content="{{ collection.owner.display_name }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ collection.cover|thumb:'normal' }}">
<title>{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}</title>
@ -56,13 +56,13 @@
<div class="owner">
<span class="avatar">
<img src="{{ collection.owner.mastodon_account.avatar }}"
alt="{{ collection.owner.username }}">
alt="{{ collection.owner.display_name }}">
</span>
</div>
<div class="info">
<p>
<a href="{% url 'journal:user_profile' collection.owner.mastodon_username %}">{{ collection.owner.mastodon_account.display_name }}</a>
<span class="handler">@{{ collection.owner.mastodon_username }}</span>
<a href="{{ collection.owner.url }}">{{ collection.owner.mastodon_account.display_name }}</a>
<span class="handler">@{{ collection.owner.handler }}</span>
</p>
<p>
{% for cat, count in collection.get_summary.items %}

View file

@ -22,7 +22,7 @@
<div>
<h6>{{ piece.title }}</h6>
{% if piece.visibility > 0 %}<i class="fa-solid fa-lock"></i>{% endif %}
<a href="{% url 'journal:user_profile' piece.owner.mastodon_username %}">{{ piece.owner.username }}</a>
<a href="{{ piece.owner.url }}">{{ piece.owner.display_name }}</a>
{{ piece.edited_time }}
</div>
<footer>

View file

@ -15,15 +15,14 @@
{% else %}
<title>{{ site_name }} - {{ user.display_name }}</title>
{% endif %}
<meta property="og:title"
content="{{ site_name }}用户 - @{{ user.mastodon_username }}">
<meta property="og:title" content="{{ site_name }}用户 - @{{ user.handler }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ user.mastodon_account.avatar }}">
<meta property="og:site_name" content="{{ site_name }}">
{% if user.preference.no_anonymous_view %}<meta name="robots" content="noindex">{% endif %}
<link rel="alternate"
type="application/rss+xml"
title="{{ site_name }} - {{ user.mastodon_username }}的评论"
title="{{ site_name }} - @{{ user.handler }}的评论"
href="{{ request.build_absolute_uri }}feed/reviews/">
{% include "common_libs.html" with jquery=0 v2=1 %}
<script src="{% static 'lib/js/calendar_yearview_blocks.js' %}" defer></script>
@ -49,7 +48,7 @@
</p>
</div>
<span class="calendar_data"
hx-get="{% url 'journal:user_calendar_data' user.mastodon_username %}"
hx-get="{% url 'journal:user_calendar_data' user.handler %}"
hx-trigger="load"
hx-swap="innerHTML"
style="display:none"></span>
@ -63,7 +62,7 @@
<h5>
{{ shelf.title }}
<small>
<a href="{% if shelf_type == 'reviewed' %}{% url 'journal:user_review_list' user.mastodon_username category %}{% else %}{% url 'journal:user_mark_list' user.mastodon_username shelf_type category %}{% endif %}">{{ shelf.count }}</a>
<a href="{% if shelf_type == 'reviewed' %}{% url 'journal:user_review_list' user.handler category %}{% else %}{% url 'journal:user_mark_list' user.handler shelf_type category %}{% endif %}">{{ shelf.count }}</a>
</small>
</h5>
<ul class="cards">
@ -90,7 +89,7 @@
<h5>
{% trans '创建的收藏单' %}
<small>
<a href="{% url 'journal:user_collection_list' user.mastodon_username %}">{{ collections_count }}</a>
<a href="{% url 'journal:user_collection_list' user.handler %}">{{ collections_count }}</a>
{% if user == request.user %}
<a href="{% url 'journal:collection_create' %}">
<i class="fa-regular fa-square-plus"></i>
@ -118,7 +117,7 @@
<h5>
{% trans '关注的收藏单' %}
<small>
<a href="{% url 'journal:user_liked_collection_list' user.mastodon_username %}">{{ liked_collections_count }}</a>
<a href="{% url 'journal:user_liked_collection_list' user.handler %}">{{ liked_collections_count }}</a>
</small>
</h5>
<ul class="cards">

View file

@ -16,7 +16,8 @@
<meta property="og:title"
content="{{ site_name }} - {{ review.title }} ({{ review.item.title }} {% trans '评论' %})">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ review.owner.username }}">
<meta property="og:article:author"
content="{{ review.owner.display_name }}">
<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 }}">
@ -51,12 +52,12 @@
<div class="owner">
<span class="avatar">
<img src="{{ review.owner.mastodon_account.avatar }}"
alt="{{ review.owner.username }}">
alt="{{ review.owner.display_name }}">
</span>
</div>
<div class="info">
<a href="{% url 'journal:user_profile' review.owner.mastodon_username %}">{{ review.owner.display_name }}</a>
<span class="handler">@{{ review.owner.mastodon_username }}</span>
<a href="{{ review.owner.url }}">{{ review.owner.display_name }}</a>
<span class="handler">@{{ review.owner.handler }}</span>
<br>
评论
<a href="{{ review.item.url }}">{{ review.item.title }}</a>

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.mastodon_username }} -
<title>{{ site_name }} - {{ user.display_name }} -
{% if liked %}关注的{% endif %}
收藏单</title>
{% include "common_libs.html" with jquery=0 v2=1 %}
@ -44,8 +44,8 @@
<span><a href="{{ collection.url }}">{{ collection.title }}</a></span>
{% if liked %}
-
<a href="{% url 'journal:user_profile' collection.owner.mastodon_username %}"
title="@{{ collection.owner.mastodon_username }}">{{ collection.owner.display_name }}</a>
<a href="{{ collection.owner.url }}"
title="@{{ collection.owner.handler }}">{{ collection.owner.display_name }}</a>
{% endif %}
</p>
{% empty %}

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.mastodon_username }}</title>{% endblock %}
{% block title %}<title>{{ site_name }} - {{ user.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.mastodon_username }}{% endblock %}
{% block head %}{{ user.display_name }}{% endblock %}
</h5>
<div>
{% for member in members %}

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 }} - 的标签</title>
<title>{{ site_name }} - {{ user.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.mastodon_username v.title %}">{{ v.title }}</a>
<a href="{% url 'journal:user_tag_member_list' user.handler v.title %}">{{ v.title }}</a>
</span>
<span>({{ v.total }})</span>
</span>

View file

@ -1,14 +1,14 @@
{% extends "user_item_list_base.html" %}
{% load i18n %}
{% block title %}
<title>{{ site_name }} - {{ user.mastodon_username }} - {{ tag.title }} {% trans '标签' %}</title>
<title>{{ site_name }} - {{ user.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.mastodon_username }}的{% trans '标签' %}
{{ user.display_name }}的{% trans '标签' %}
{% if user == request.user %}
<form style="display:inline"
hx-get="{% url 'journal:user_tag_edit' %}"

View file

@ -114,7 +114,7 @@ def add_to_collection(request, item_uuid):
cid = int(request.POST.get("collection_id", default=0))
if not cid:
cid = Collection.objects.create(
owner=request.user, title=f"{request.user.username}的收藏单"
owner=request.user, title=f"{request.user.display_name}的收藏单"
).id
collection = Collection.objects.get(owner=request.user, id=cid)
collection.append_item(item, note=request.POST.get("note"))
@ -738,7 +738,7 @@ def user_tag_edit(request):
tag.delete()
msg.info(request.user, _("标签已删除"))
return redirect(
reverse("journal:user_tag_list", args=[request.user.mastodon_username])
reverse("journal:user_tag_list", args=[request.user.mastodon_acct])
)
elif (
tag_title != tag.title
@ -754,7 +754,7 @@ def user_tag_edit(request):
return redirect(
reverse(
"journal:user_tag_member_list",
args=[request.user.mastodon_username, tag.title],
args=[request.user.mastodon_acct, tag.title],
)
)
raise BadRequest()

View file

@ -110,12 +110,12 @@ def post_toot(
try:
if update_id:
response = put(url + "/" + update_id, headers=headers, data=payload)
if update_id is None or response.status_code != 200:
if update_id is None or (response and response.status_code != 200):
headers["Idempotency-Key"] = random_string_generator(16)
response = post(url, headers=headers, data=payload)
if response.status_code == 201:
if response and response.status_code == 201:
response.status_code = 200
if response.status_code != 200:
if response and response.status_code != 200:
logger.error(f"Error {url} {response.status_code}")
except Exception:
response = None
@ -470,7 +470,7 @@ def share_mark(mark):
mark.shared_link = j["url"]
elif "data" in j:
mark.shared_link = (
f"https://twitter.com/{user.username}/status/{j['data']['id']}"
f"https://twitter.com/{user.mastodon_username}/status/{j['data']['id']}"
)
if mark.shared_link:
mark.save(update_fields=["shared_link"])
@ -539,7 +539,11 @@ def share_collection(collection, comment, user, visibility_no):
user_str = (
""
if user == collection.owner
else " @" + collection.owner.mastodon_username + " "
else (
" @" + collection.owner.mastodon_acct + " "
if collection.owner.mastodon_acct
else " " + collection.owner.username + " "
)
)
content = f"分享{user_str}的收藏单《{collection.title}\n{collection.absolute_url}\n{comment}{tags}"
response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)

View file

@ -13,23 +13,17 @@ class OAuth2Backend(ModelBackend):
"""when username is provided, assume that token is newly obtained and valid"""
if token is None or site is None:
return
mastodon_id = None
if username is None:
code, user_data = verify_account(site, token)
if code == 200:
userid = user_data["id"]
else:
# aquiring user data fail means token is invalid thus auth fail
return None
# when username is provided, assume that token is newly obtained and valid
if code == 200 and user_data:
mastodon_id = user_data["id"]
if not mastodon_id:
return None
try:
user = UserModel._default_manager.get(
mastodon_id=userid, mastodon_site=site
mastodon_id=mastodon_id, mastodon_site=site
)
return user if self.user_can_authenticate(user) else None
except UserModel.DoesNotExist:
return None
else:
if self.user_can_authenticate(user):
return user
return None

View file

@ -17,7 +17,7 @@
</span>
<div class="spacing">
关注了
<a href="{% url 'journal:user_profile' collection.owner.mastodon_username %}">{{ collection.owner.display_name }}</a>
<a href="{{ collection.owner.url }}">{{ collection.owner.display_name }}</a>
的收藏单
<a href="{{ collection.url }}">{{ collection.title }}</a>
</div>

View file

@ -21,12 +21,10 @@
</span>
<div class="spacing">
<span>
<a href="{% url 'journal:user_profile' activity.owner.mastodon_username %}"
class="nickname">{{ activity.owner.display_name }}</a>
<a href="{{ activity.owner.url }}" class="nickname">{{ activity.owner.display_name }}</a>
</span>
<span>
<a href="{% url 'journal:user_profile' activity.owner.mastodon_username %}"
class="handler">@{{ activity.owner.mastodon_username }}</a>
<a href="{{ activity.owner.url }}" class="handler">@{{ activity.owner.handler }}</a>
</span>
</div>
{% with "activity/"|add:activity.template|add:".html" as template %}

View file

@ -10,8 +10,10 @@ class SocialTest(TestCase):
self.book1 = Edition.objects.create(title="Hyperion")
self.book2 = Edition.objects.create(title="Andymion")
self.movie = Edition.objects.create(title="Fight Club")
self.alice = User.objects.create(mastodon_site="MySpace", username="Alice")
self.bob = User.objects.create(mastodon_site="KKCity", username="Bob")
self.alice = User.objects.create(
mastodon_site="MySpace", mastodon_username="Alice"
)
self.bob = User.objects.create(mastodon_site="KKCity", mastodon_username="Bob")
def test_timeline(self):
# alice see 0 activity in timeline in the beginning

View file

@ -1,22 +1,19 @@
from django.shortcuts import reverse, redirect, render, get_object_or_404
from django.shortcuts import redirect, render, get_object_or_404
from django.urls import reverse
from django.contrib.auth.decorators import login_required
from django.contrib import auth
from django.contrib.auth import authenticate
from django.core.paginator import Paginator
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist, BadRequest
from django.db.models import Count
from .models import User, Report, Preference
from .forms import ReportForm
from .models import User, Preference
from mastodon.api import *
from mastodon import mastodon_request_included
from common.config import *
from mastodon.models import MastodonApplication
from mastodon.api import verify_account
from django.conf import settings
from urllib.parse import quote
import django_rq
from .account import *
from .tasks import *
from datetime import timedelta
from django.utils import timezone
@ -113,7 +110,7 @@ def OAuth2_login(request):
{"msg": _("认证失败😫"), "secondary_msg": _("Mastodon服务未能返回有效认证信息")},
)
site = request.COOKIES.get("mastodon_domain")
if not code:
if not site:
return render(
request,
"common/error.html",
@ -137,8 +134,8 @@ def OAuth2_login(request):
user = authenticate(request, token=token, site=site)
if user: # existing user
user.mastodon_token = token
user.mastodon_refresh_token = refresh_token
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:
@ -152,7 +149,8 @@ def OAuth2_login(request):
if code != 200 or user_data is None:
return render(request, "common/error.html", {"msg": _("联邦网络访问失败😫")})
new_user = User(
username=user_data["username"],
username=None,
mastodon_username=user_data["username"],
mastodon_id=user_data["id"],
mastodon_site=site,
mastodon_token=token,
@ -206,18 +204,23 @@ def swap_login(request, token, site, refresh_token):
current_user = request.user
if code == 200 and data is not None:
username = data["username"]
if username == current_user.username and site == current_user.mastodon_site:
if (
username == current_user.mastodon_username
and site == current_user.mastodon_site
):
messages.add_message(
request, messages.ERROR, _(f"该身份 {username}@{site} 与当前账号相同。")
)
else:
try:
existing_user = User.objects.get(username=username, mastodon_site=site)
existing_user = User.objects.get(
mastodon_username=username, mastodon_site=site
)
messages.add_message(
request, messages.ERROR, _(f"该身份 {username}@{site} 已被用于其它账号。")
)
except ObjectDoesNotExist:
current_user.username = username
current_user.mastodon_username = username
current_user.mastodon_id = data["id"]
current_user.mastodon_site = site
current_user.mastodon_token = token
@ -227,6 +230,7 @@ def swap_login(request, token, site, refresh_token):
update_fields=[
"username",
"mastodon_id",
"mastodon_username",
"mastodon_site",
"mastodon_token",
"mastodon_refresh_token",
@ -264,22 +268,9 @@ def clear_data(request):
if request.META.get("HTTP_AUTHORIZATION"):
raise BadRequest("Only for web login")
if request.method == "POST":
if request.POST.get("verification") == request.user.mastodon_username:
if request.POST.get("verification") == request.user.mastodon_acct:
remove_data_by_user(request.user)
request.user.first_name = request.user.username
request.user.last_name = request.user.mastodon_site
request.user.is_active = False
request.user.username = "removed_" + str(request.user.id)
request.user.mastodon_id = 0
request.user.mastodon_site = "removed"
request.user.mastodon_token = ""
request.user.mastodon_locked = False
request.user.mastodon_followers = []
request.user.mastodon_following = []
request.user.mastodon_mutes = []
request.user.mastodon_blocks = []
request.user.mastodon_domain_blocks = []
request.user.mastodon_account = {}
request.user.clear()
request.user.save()
auth_logout(request)
return redirect(reverse("users:login"))

View file

@ -19,7 +19,7 @@ class UserSchema(Schema):
def me(request):
return 200, {
"url": settings.SITE_INFO["site_url"] + request.user.url,
"external_acct": request.user.mastodon_username,
"external_acct": request.user.mastodon_acct,
"display_name": request.user.display_name,
"avatar": request.user.mastodon_account.get("avatar"),
}

View file

@ -12,8 +12,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
h = int(options["id"])
u = User.objects.get(id=h)
u.username = "(duplicated)" + u.username
u = User.objects.get(pk=h)
u.is_active = False
u.save()
print(f"{u} updated")
print(f"{u} disabled")

View file

@ -0,0 +1,72 @@
# Generated by Django 3.2.19 on 2023-06-30 02:39
import django.contrib.auth.validators
from django.db import migrations, models
import users.models
def set_username(apps, schema_editor):
User = apps.get_model("users", "User")
for u in User.objects.all():
u.mastodon_username = u.username
u.username = None
u.save()
class Migration(migrations.Migration):
dependencies = [
("users", "0004_alter_preference_classic_homepage"),
]
operations = [
migrations.RemoveConstraint(
model_name="user",
name="unique_user_identity",
),
migrations.AddField(
model_name="user",
name="mastodon_username",
field=models.CharField(default=None, max_length=100, null=True),
preserve_default=False,
),
migrations.AlterField(
model_name="user",
name="mastodon_site",
field=models.CharField(default=None, max_length=100, null=True),
),
migrations.AlterField(
model_name="user",
name="mastodon_id",
field=models.CharField(default=None, max_length=100, null=True),
),
migrations.RunPython(set_username),
migrations.RunSQL(
"UPDATE users_user SET mastodon_id = null where mastodon_id = '0';"
),
migrations.AlterField(
model_name="user",
name="username",
field=models.CharField(
error_messages={"unique": "A user with that username already exists."},
help_text="Required. 50 characters or fewer. Letters, digits and _ only.",
max_length=50,
null=True,
unique=True,
validators=[users.models.UsernameValidator()],
verbose_name="username",
),
),
migrations.AddConstraint(
model_name="user",
constraint=models.UniqueConstraint(
fields=("mastodon_username", "mastodon_site"),
name="unique_mastodon_username",
),
),
migrations.AddConstraint(
model_name="user",
constraint=models.UniqueConstraint(
fields=("mastodon_id", "mastodon_site"), name="unique_mastodon_id"
),
),
]

View file

@ -0,0 +1,35 @@
# Generated by Django 3.2.19 on 2023-06-30 13:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0005_add_dedicated_username"),
]
operations = [
migrations.AlterField(
model_name="user",
name="email",
field=models.EmailField(
default=None,
null=True,
max_length=254,
unique=False,
verbose_name="email address",
),
),
migrations.RunSQL("UPDATE users_user SET email = null;"),
migrations.AlterField(
model_name="user",
name="email",
field=models.EmailField(
default=None,
null=True,
max_length=254,
unique=True,
verbose_name="email address",
),
),
]

View file

@ -1,4 +1,7 @@
import uuid
import re
from django.core import validators
from django.utils.deconstruct import deconstructible
import django.contrib.postgres.fields as postgres
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
@ -13,6 +16,16 @@ from mastodon.api import *
from django.urls import reverse
@deconstructible
class UsernameValidator(validators.RegexValidator):
regex = r"^[a-zA-Z0-9_]{2,50}$"
message = _(
"Enter a valid username. This value may contain only unaccented lowercase a-z "
"and uppercase A-Z letters, numbers, and _ characters."
)
flags = re.ASCII
def report_image_path(instance, filename):
return GenerateDateUUIDMediaFilePath(
instance, filename, settings.REPORT_MEDIA_PATH_ROOT
@ -20,19 +33,23 @@ def report_image_path(instance, filename):
class User(AbstractUser):
if settings.MASTODON_ALLOW_ANY_SITE:
username = models.CharField(
_("username"),
max_length=150,
unique=False,
help_text=_(
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
)
username_validator = UsernameValidator()
username = models.CharField(
_("username"),
max_length=50,
unique=True,
null=True, # allow null for newly registered users who has not set a user name
help_text=_("Required. 50 characters or fewer. Letters, digits and _ only."),
validators=[username_validator],
error_messages={
"unique": _("A user with that username already exists."),
},
)
email = models.EmailField(_("email address"), unique=True, default=None, null=True)
following = models.JSONField(default=list)
mastodon_id = models.CharField(max_length=100, blank=False)
# mastodon domain name, eg donotban.com
mastodon_site = models.CharField(max_length=100, blank=False)
mastodon_id = models.CharField(max_length=100, default=None, null=True)
mastodon_username = models.CharField(max_length=100, default=None, null=True)
mastodon_site = models.CharField(max_length=100, default=None, null=True)
mastodon_token = models.CharField(max_length=2048, default="")
mastodon_refresh_token = models.CharField(max_length=2048, default="")
mastodon_locked = models.BooleanField(default=False)
@ -50,8 +67,13 @@ class User(AbstractUser):
class Meta:
constraints = [
models.UniqueConstraint(
fields=["username", "mastodon_site"], name="unique_user_identity"
)
fields=["mastodon_username", "mastodon_site"],
name="unique_mastodon_username",
),
models.UniqueConstraint(
fields=["mastodon_id", "mastodon_site"],
name="unique_mastodon_id",
),
]
# def save(self, *args, **kwargs):
@ -60,25 +82,31 @@ class User(AbstractUser):
# return super().save(*args, **kwargs)
@property
def mastodon_username(self):
return self.username + "@" + self.mastodon_site
def mastodon_acct(self):
return (
f"{self.mastodon_username}@{self.mastodon_site}"
if self.mastodon_username
else ""
)
@property
def display_name(self):
return (
self.mastodon_account["display_name"]
self.mastodon_account.get("display_name")
if self.mastodon_account
and "display_name" in self.mastodon_account
and self.mastodon_account["display_name"]
else self.mastodon_username
else (self.username or self.mastodon_acct or "")
)
@property
def handler(self):
return self.mastodon_acct or self.username or f"~{self.pk}"
@property
def url(self):
return reverse("journal:user_profile", args=[self.mastodon_username])
return reverse("journal:user_profile", args=[self.handler])
def __str__(self):
return self.mastodon_username
return f'{self.pk}:{self.username or ""}:{self.mastodon_acct}'
def get_preference(self):
pref = Preference.objects.filter(user=self).first() # self.preference
@ -86,6 +114,27 @@ class User(AbstractUser):
pref = Preference.objects.create(user=self)
return pref
def clear(self):
if self.mastodon_site == "removed" and not self.is_active:
return
self.first_name = self.mastodon_username
self.last_name = self.mastodon_site
self.is_active = False
self.email = None
# self.username = "~removed~" + str(self.pk)
# to get ready for federation, username has to be reserved
self.mastodon_username = "~removed~" + str(self.pk)
self.mastodon_id = None
self.mastodon_site = "removed"
self.mastodon_token = ""
self.mastodon_locked = False
self.mastodon_followers = []
self.mastodon_following = []
self.mastodon_mutes = []
self.mastodon_blocks = []
self.mastodon_domain_blocks = []
self.mastodon_account = {}
def refresh_mastodon_data(self):
"""Try refresh account data from mastodon server, return true if refreshed successfully, note it will not save to db"""
self.mastodon_last_refresh = timezone.now()
@ -102,9 +151,11 @@ class User(AbstractUser):
if mastodon_account:
self.mastodon_account = mastodon_account
self.mastodon_locked = mastodon_account["locked"]
if self.username != mastodon_account["username"]:
print(f"username changed from {self} to {mastodon_account['username']}")
self.username = mastodon_account["username"]
if self.mastodon_username != mastodon_account["username"]:
logger.warn(
f"username changed from {self} to {mastodon_account['username']}"
)
self.mastodon_username = mastodon_account["username"]
# self.mastodon_token = token
# user.mastodon_id = mastodon_account['id']
self.mastodon_followers = get_related_acct_list(
@ -139,7 +190,7 @@ class User(AbstractUser):
target = User.get(m)
if target and (
(not target.mastodon_locked)
or self.mastodon_username in target.mastodon_followers
or self.mastodon_acct in target.mastodon_followers
):
fl.append(target.pk)
return fl
@ -147,25 +198,25 @@ class User(AbstractUser):
def is_blocking(self, target):
return (
(
target.mastodon_username in self.mastodon_blocks
target.mastodon_acct in self.mastodon_blocks
or target.mastodon_site in self.mastodon_domain_blocks
)
if target.is_authenticated
else self.preference.no_anonymous_view
else self.preference.no_anonymous_view # type: ignore
)
def is_blocked_by(self, target):
return target.is_authenticated and target.is_blocking(self)
def is_muting(self, target):
return target.mastodon_username in self.mastodon_mutes
return target.mastodon_acct in self.mastodon_mutes
def is_following(self, target):
return (
self.mastodon_username in target.mastodon_followers
self.mastodon_acct in target.mastodon_followers
if target.mastodon_locked
else self.mastodon_username in target.mastodon_followers
or target.mastodon_username in self.mastodon_following
else self.mastodon_acct in target.mastodon_followers
or target.mastodon_acct in self.mastodon_following
)
def is_followed_by(self, target):
@ -196,14 +247,15 @@ class User(AbstractUser):
return unread_announcements
@classmethod
def get(cls, id):
if isinstance(id, str):
try:
username = id.split("@")[0]
site = id.split("@")[1]
except IndexError:
def get(cls, name):
if isinstance(name, str):
sp = name.split("@")
if len(sp) == 1:
query_kwargs = {"username": name}
elif len(sp) == 2:
query_kwargs = {"mastodon_username": sp[0], "mastodon_site": sp[1]}
else:
return None
query_kwargs = {"username": username, "mastodon_site": site}
elif isinstance(id, int):
query_kwargs = {"pk": id}
else:
@ -248,8 +300,6 @@ class Report(models.Model):
)
image = models.ImageField(
upload_to=report_image_path,
height_field=None,
width_field=None,
blank=True,
default="",
)

View file

@ -1,26 +1,4 @@
from django.shortcuts import reverse, redirect, render, get_object_or_404
from django.http import HttpResponseBadRequest, HttpResponse
from django.contrib.auth.decorators import login_required
from django.contrib import auth
from django.contrib.auth import authenticate
from django.core.paginator import Paginator
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count
from .models import User, Report, Preference
from .forms import ReportForm
from mastodon.api import *
from mastodon import mastodon_request_included
from common.config import *
from common.utils import PageLinksGenerator
from management.models import Announcement
from mastodon.models import MastodonApplication
from django.conf import settings
from urllib.parse import quote
from openpyxl import Workbook
from common.utils import GenerateDateUUIDMediaFilePath
from datetime import datetime
import os
def refresh_mastodon_data_task(user, token=None):

View file

@ -18,9 +18,9 @@
{% for report in reports %}
<article>
<header>
<a href="{% url 'journal:user_profile' report.submit_user.mastodon_username %}">{{ report.submit_user.username }}</a>
<a href="{{ report.submit_user.url }}">{{ report.submit_user.display_name }}</a>
{% trans '投诉了' %}
<a href="{% url 'journal:user_profile' report.reported_user.mastodon_username %}">{{ report.reported_user.username }}</a>
<a href="{{ report.reported_user.url }}">{{ report.reported_user.display_name }}</a>
@{{ report.submitted_time }}
</header>
<p>{{ report.message }}</p>

View file

@ -58,7 +58,7 @@
{% else %}
<div id="userMastodonID" hidden="true"></div>
{% endif %}
<div id="userPageURL" hidden="true">{% url 'journal:user_profile' 0 %}</div>
<div id="userPageURL" hidden="true">/users/0/</div>
<div id="spinner" hidden>
<div class="spinner">
<div></div>

View file

@ -2,6 +2,7 @@ from django.shortcuts import redirect, render, get_object_or_404
from django.urls import reverse
from django.contrib.auth.decorators import login_required
from django.utils.translation import gettext_lazy as _
from management.models import Announcement
from .models import User, Report, Preference
from .forms import ReportForm
from mastodon.api import *
@ -81,15 +82,12 @@ def set_layout(request):
if request.POST.get("name") == "profile":
request.user.preference.profile_layout = layout
request.user.preference.save(update_fields=["profile_layout"])
return redirect(
reverse("journal:user_profile", args=[request.user.mastodon_username])
)
return redirect(request.user.url)
elif request.POST.get("name") == "discover":
request.user.preference.discover_layout = layout
request.user.preference.save(update_fields=["discover_layout"])
return redirect(reverse("catalog:discover"))
else:
raise BadRequest()
raise BadRequest()
@login_required