post to threads
This commit is contained in:
parent
2bd3aaa78d
commit
43e9db72b7
28 changed files with 1424 additions and 1169 deletions
|
@ -227,6 +227,7 @@ ENABLE_LOCAL_ONLY = env("NEODB_ENABLE_LOCAL_ONLY")
|
|||
|
||||
# Timeout of requests to Mastodon, in seconds
|
||||
MASTODON_TIMEOUT = env("NEODB_LOGIN_MASTODON_TIMEOUT", default=5) # type: ignore
|
||||
THREADS_TIMEOUT = 10 # Threads is really slow when publishing post
|
||||
TAKAHE_REMOTE_TIMEOUT = MASTODON_TIMEOUT
|
||||
|
||||
NEODB_USER_AGENT = f"NeoDB/{NEODB_VERSION} (+{SITE_INFO.get('site_url', 'undefined')})"
|
||||
|
|
|
@ -122,7 +122,12 @@
|
|||
{% if messages %}
|
||||
<ul class="messages" style="text-align:center">
|
||||
{% for message in messages %}
|
||||
<li {% if message.tags %}class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||
<li {% if message.tags %}class="{{ message.tags }}"{% endif %}>
|
||||
{% if message.meta.url %}
|
||||
[<a href="{{ message.meta.url }}">{% trans "Open" %}</a>]
|
||||
{% endif %}
|
||||
{{ message }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
|
|
@ -123,7 +123,7 @@ class Comment(Content):
|
|||
ShelfManager.get_action_template(ShelfType.PROGRESS, self.item.category)
|
||||
)
|
||||
|
||||
def to_mastodon_params(self):
|
||||
def to_crosspost_params(self):
|
||||
spoiler_text, txt = render_spoiler_text(self.text, self.item)
|
||||
content = (
|
||||
self.get_crosspost_template().format(item=self.item.display_title)
|
||||
|
|
|
@ -5,8 +5,11 @@ from datetime import datetime
|
|||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
import django_rq
|
||||
|
||||
# from deepmerge import always_merger
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.signing import b62_decode, b62_encode
|
||||
from django.db import models
|
||||
from django.db.models import CharField, Q
|
||||
|
@ -14,10 +17,12 @@ from django.utils import timezone
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from loguru import logger
|
||||
from polymorphic.models import PolymorphicModel
|
||||
from user_messages import api as messages
|
||||
|
||||
from catalog.common.models import Item, ItemCategory
|
||||
from catalog.models import item_categories, item_content_types
|
||||
from takahe.utils import Takahe
|
||||
from users.middlewares import activate_language_for_user
|
||||
from users.models import APIdentity, User
|
||||
|
||||
from .mixins import UserOwnedObjectMixin
|
||||
|
@ -90,36 +95,10 @@ def q_item_in_category(item_category: ItemCategory):
|
|||
return Q(item__polymorphic_ctype__in=contenttype_ids)
|
||||
|
||||
|
||||
# class ImportStatus(Enum):
|
||||
# QUEUED = 0
|
||||
# PROCESSING = 1
|
||||
# FINISHED = 2
|
||||
|
||||
|
||||
# class ImportSession(models.Model):
|
||||
# owner = models.ForeignKey(APIdentity, on_delete=models.CASCADE)
|
||||
# status = models.PositiveSmallIntegerField(default=ImportStatus.QUEUED)
|
||||
# importer = models.CharField(max_length=50)
|
||||
# file = models.CharField()
|
||||
# default_visibility = models.PositiveSmallIntegerField()
|
||||
# total = models.PositiveIntegerField()
|
||||
# processed = models.PositiveIntegerField()
|
||||
# skipped = models.PositiveIntegerField()
|
||||
# imported = models.PositiveIntegerField()
|
||||
# failed = models.PositiveIntegerField()
|
||||
# logs = models.JSONField(default=list)
|
||||
# created_time = models.DateTimeField(auto_now_add=True)
|
||||
# edited_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
# class Meta:
|
||||
# indexes = [
|
||||
# models.Index(fields=["owner", "importer", "created_time"]),
|
||||
# ]
|
||||
|
||||
|
||||
class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||
if TYPE_CHECKING:
|
||||
likes: models.QuerySet["Like"]
|
||||
metadata: models.JSONField[Any, Any]
|
||||
url_path = "p" # subclass must specify this
|
||||
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
||||
local = models.BooleanField(default=True)
|
||||
|
@ -131,33 +110,16 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
def classname(self) -> str:
|
||||
return self.__class__.__name__.lower()
|
||||
|
||||
def get_mastodon_crosspost_url(self):
|
||||
return (
|
||||
(self.metadata or {}).get("shared_link")
|
||||
if hasattr(self, "metadata")
|
||||
else None
|
||||
)
|
||||
|
||||
def set_mastodon_crosspost_url(self, url: str | None):
|
||||
if not hasattr(self, "metadata"):
|
||||
logger.warning("metadata field not found")
|
||||
return
|
||||
metadata = self.metadata or {}
|
||||
if metadata.get("shared_link", None) == url:
|
||||
return
|
||||
if not url:
|
||||
metadata.pop("shared_link", None)
|
||||
else:
|
||||
metadata["shared_link"] = url
|
||||
self.metadata = metadata
|
||||
self.save(update_fields=["metadata"])
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
if self.local:
|
||||
Takahe.delete_posts(self.all_post_ids)
|
||||
toot_url = self.get_mastodon_crosspost_url()
|
||||
if toot_url and self.owner.user.mastodon:
|
||||
self.owner.user.mastodon.delete_later(toot_url)
|
||||
toot_id = (
|
||||
(self.metadata or {}).get("mastodon_id")
|
||||
if hasattr(self, "metadata")
|
||||
else None
|
||||
)
|
||||
if toot_id and self.owner.user.mastodon:
|
||||
self.owner.user.mastodon.delete_post_later(toot_id)
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
@property
|
||||
|
@ -265,7 +227,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
return {}
|
||||
|
||||
@abstractmethod
|
||||
def to_mastodon_params(self) -> dict[str, Any]:
|
||||
def to_crosspost_params(self) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
|
@ -314,74 +276,120 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
# subclass may have to add additional code to update type_data in local post
|
||||
return p
|
||||
|
||||
def sync_to_mastodon(self, delete_existing=False):
|
||||
user = self.owner.user
|
||||
if not user.mastodon:
|
||||
def get_crosspost_params(self):
|
||||
d = {
|
||||
"visibility": self.visibility,
|
||||
"update_id": (self.metadata if hasattr(self, "metadata") else {}).get(
|
||||
"mastodon_id"
|
||||
),
|
||||
}
|
||||
d.update(self.to_crosspost_params())
|
||||
return d
|
||||
|
||||
def sync_to_social_accounts(self, update_mode: int = 0):
|
||||
"""update_mode: 0 update if exists otherwise create; 1: delete if exists and create; 2: only create"""
|
||||
django_rq.get_queue("mastodon").enqueue(
|
||||
self._sync_to_social_accounts, update_mode
|
||||
)
|
||||
|
||||
def _sync_to_social_accounts(self, update_mode: int):
|
||||
activate_language_for_user(self.owner.user)
|
||||
params = self.get_crosspost_params()
|
||||
metadata = self.metadata.copy()
|
||||
self.sync_to_mastodon(params, update_mode)
|
||||
self.sync_to_threads(params, update_mode)
|
||||
if self.metadata != metadata:
|
||||
self.save(update_fields=["metadata"])
|
||||
|
||||
def sync_to_threads(self, params, update_mode):
|
||||
# skip non-public post as Threads does not support it
|
||||
# update_mode will be ignored as update/delete are not supported either
|
||||
threads = self.owner.user.threads
|
||||
if params["visibility"] != 0 or not threads:
|
||||
return
|
||||
if user.preference.mastodon_repost_mode == 1:
|
||||
if delete_existing:
|
||||
self.delete_mastodon_repost()
|
||||
return self.crosspost_to_mastodon()
|
||||
try:
|
||||
r = threads.post(**params)
|
||||
except Exception:
|
||||
logger.warning(f"{self} post to {threads} failed")
|
||||
messages.error(threads.user, _("A recent post was not posted to Threads."))
|
||||
return False
|
||||
self.metadata.update({"threads_" + k: v for k, v in r.items()})
|
||||
return True
|
||||
|
||||
def sync_to_mastodon(self, params, update_mode):
|
||||
mastodon = self.owner.user.mastodon
|
||||
if not mastodon:
|
||||
return
|
||||
if self.owner.user.preference.mastodon_repost_mode == 1:
|
||||
if update_mode == 1:
|
||||
toot_id = self.metadata.pop("mastodon_id", None)
|
||||
if toot_id:
|
||||
self.metadata.pop("mastodon_url", None)
|
||||
mastodon.delete(toot_id)
|
||||
elif update_mode == 1:
|
||||
params.pop("update_id", None)
|
||||
return self.crosspost_to_mastodon(params)
|
||||
elif self.latest_post:
|
||||
if user.mastodon:
|
||||
user.mastodon.boost_later(self.latest_post.url)
|
||||
mastodon.boost(self.latest_post.url)
|
||||
else:
|
||||
logger.warning("No post found for piece")
|
||||
|
||||
def delete_mastodon_repost(self):
|
||||
toot_url = self.get_mastodon_crosspost_url()
|
||||
if toot_url:
|
||||
self.set_mastodon_crosspost_url(None)
|
||||
if self.owner.user.mastodon:
|
||||
self.owner.user.mastodon.delete_later(toot_url)
|
||||
def crosspost_to_mastodon(self, params):
|
||||
mastodon = self.owner.user.mastodon
|
||||
if not mastodon:
|
||||
return False
|
||||
r = mastodon.post(**params)
|
||||
try:
|
||||
pass
|
||||
except PermissionDenied:
|
||||
messages.error(
|
||||
mastodon.user,
|
||||
_("A recent post was not posted to Mastodon, please re-authorize."),
|
||||
meta={"url": mastodon.get_reauthorize_url()},
|
||||
)
|
||||
return False
|
||||
except Exception:
|
||||
logger.warning(f"{self} post to {mastodon} failed")
|
||||
messages.error(
|
||||
mastodon.user, _("A recent post was not posted to Mastodon.")
|
||||
)
|
||||
return False
|
||||
self.metadata.update({"mastodon_" + k: v for k, v in r.items()})
|
||||
return True
|
||||
|
||||
def crosspost_to_mastodon(self):
|
||||
user = self.owner.user
|
||||
if not user or not user.mastodon:
|
||||
return False, -1
|
||||
d = {
|
||||
"visibility": self.visibility,
|
||||
"update_toot_url": self.get_mastodon_crosspost_url(),
|
||||
def get_ap_data(self):
|
||||
return {
|
||||
"object": {
|
||||
"tag": (
|
||||
[self.item.ap_object_ref] # type:ignore
|
||||
if hasattr(self, "item")
|
||||
else []
|
||||
),
|
||||
"relatedWith": [self.ap_object],
|
||||
}
|
||||
}
|
||||
d.update(self.to_mastodon_params())
|
||||
response = user.mastodon.post(**d)
|
||||
if response is not None and response.status_code in [200, 201]:
|
||||
j = response.json()
|
||||
if "url" in j:
|
||||
metadata = {"shared_link": j["url"]}
|
||||
if self.metadata != metadata:
|
||||
self.metadata = metadata
|
||||
self.save(update_fields=["metadata"])
|
||||
return True, 200
|
||||
else:
|
||||
logger.warning(response)
|
||||
return False, response.status_code if response is not None else -1
|
||||
|
||||
def sync_to_timeline(self, delete_existing=False):
|
||||
def sync_to_timeline(self, update_mode: int = 0):
|
||||
"""update_mode: 0 update if exists otherwise create; 1: delete if exists and create; 2: only create"""
|
||||
user = self.owner.user
|
||||
v = Takahe.visibility_n2t(self.visibility, user.preference.post_public_mode)
|
||||
existing_post = self.latest_post
|
||||
if existing_post and existing_post.state in ["deleted", "deleted_fanned_out"]:
|
||||
existing_post = None
|
||||
elif existing_post and delete_existing:
|
||||
Takahe.delete_posts([existing_post.pk])
|
||||
existing_post = None
|
||||
if existing_post:
|
||||
if (
|
||||
existing_post.state in ["deleted", "deleted_fanned_out"]
|
||||
or update_mode == 2
|
||||
):
|
||||
existing_post = None
|
||||
elif update_mode == 1:
|
||||
Takahe.delete_posts([existing_post.pk])
|
||||
existing_post = None
|
||||
params = {
|
||||
"author_pk": self.owner.pk,
|
||||
"visibility": v,
|
||||
"post_pk": existing_post.pk if existing_post else None,
|
||||
"post_time": self.created_time, # type:ignore subclass must have this
|
||||
"edit_time": self.edited_time, # type:ignore subclass must have this
|
||||
"data": {
|
||||
"object": {
|
||||
"tag": (
|
||||
[self.item.ap_object_ref] # type:ignore
|
||||
if hasattr(self, "item")
|
||||
else []
|
||||
),
|
||||
"relatedWith": [self.ap_object],
|
||||
}
|
||||
},
|
||||
"data": self.get_ap_data(),
|
||||
}
|
||||
params.update(self.to_post_params())
|
||||
post = Takahe.post(**params)
|
||||
|
|
|
@ -18,33 +18,6 @@ from .review import Review
|
|||
from .shelf import Shelf, ShelfLogEntry, ShelfManager, ShelfMember, ShelfType
|
||||
|
||||
|
||||
def share_mark(mark, post_as_new=False):
|
||||
user = mark.owner.user
|
||||
if not user or not user.mastodon:
|
||||
return
|
||||
stars = user.mastodon.rating_to_emoji(mark.rating_grade)
|
||||
spoiler_text, txt = get_spoiler_text(mark.comment_text or "", mark.item)
|
||||
content = f"{mark.get_action_for_feed()} {stars}\n{mark.item.absolute_url}\n{txt}{mark.tag_text}"
|
||||
update_url = (
|
||||
None if post_as_new else (mark.shelfmember.metadata or {}).get("shared_link")
|
||||
)
|
||||
response = user.mastodon.post(
|
||||
content,
|
||||
mark.visibility,
|
||||
update_url,
|
||||
spoiler_text,
|
||||
)
|
||||
if response is not None and response.status_code in [200, 201]:
|
||||
j = response.json()
|
||||
if "url" in j:
|
||||
mark.shelfmember.metadata = {"shared_link": j["url"]}
|
||||
mark.shelfmember.save(update_fields=["metadata"])
|
||||
return True, 200
|
||||
else:
|
||||
logger.warning(response)
|
||||
return False, response.status_code if response is not None else -1
|
||||
|
||||
|
||||
class Mark:
|
||||
"""
|
||||
Holding Mark for an item on an shelf,
|
||||
|
@ -241,6 +214,7 @@ class Mark:
|
|||
visibility = self.visibility
|
||||
last_shelf_type = self.shelf_type
|
||||
last_visibility = self.visibility if last_shelf_type else None
|
||||
update_mode = 0
|
||||
if tags is not None:
|
||||
self.owner.tag_manager.tag_item(self.item, tags, visibility)
|
||||
if shelf_type is None:
|
||||
|
@ -264,8 +238,7 @@ class Mark:
|
|||
self.shelfmember.visibility = visibility
|
||||
shelfmember_changed = True
|
||||
# retract most recent post about this status when visibility changed
|
||||
if self.shelfmember.latest_post:
|
||||
Takahe.delete_posts([self.shelfmember.latest_post.pk])
|
||||
update_mode = 1
|
||||
if created_time and created_time != self.shelfmember.created_time:
|
||||
self.shelfmember.created_time = created_time
|
||||
log_entry.timestamp = created_time
|
||||
|
@ -277,6 +250,8 @@ class Mark:
|
|||
if shelfmember_changed:
|
||||
self.shelfmember.save()
|
||||
else:
|
||||
# ignore most recent post if exists and create new one
|
||||
update_mode = 2
|
||||
shelf = Shelf.objects.get(owner=self.owner, shelf_type=shelf_type)
|
||||
d = {"parent": shelf, "visibility": visibility, "position": 0}
|
||||
if metadata:
|
||||
|
@ -305,28 +280,17 @@ class Mark:
|
|||
)
|
||||
self.rating_grade = rating_grade
|
||||
# publish a new or updated ActivityPub post
|
||||
user: User = self.owner.user
|
||||
post_as_new = shelf_type != last_shelf_type or visibility != last_visibility
|
||||
classic_crosspost = user.preference.mastodon_repost_mode == 1
|
||||
append = (
|
||||
f"@{user.mastodon.handle}\n"
|
||||
if visibility > 0
|
||||
and share_to_mastodon
|
||||
and not classic_crosspost
|
||||
and user.mastodon
|
||||
else ""
|
||||
)
|
||||
post = Takahe.post_mark(self, post_as_new, append)
|
||||
if post and self.item.category in (user.preference.auto_bookmark_cats or []):
|
||||
if shelf_type == ShelfType.PROGRESS:
|
||||
Takahe.bookmark(post.pk, self.owner.pk)
|
||||
# async boost to mastodon
|
||||
if post and share_to_mastodon:
|
||||
if classic_crosspost:
|
||||
share_mark(self, post_as_new)
|
||||
elif user.mastodon:
|
||||
user.mastodon.boost_later(post.url)
|
||||
return True
|
||||
post = self.shelfmember.sync_to_timeline(update_mode)
|
||||
if share_to_mastodon:
|
||||
self.shelfmember.sync_to_social_accounts(update_mode)
|
||||
# auto add bookmark
|
||||
if (
|
||||
post
|
||||
and shelf_type == ShelfType.PROGRESS
|
||||
and self.item.category
|
||||
in (self.owner.user.preference.auto_bookmark_cats or [])
|
||||
):
|
||||
Takahe.bookmark(post.pk, self.owner.pk)
|
||||
|
||||
def delete(self, keep_tags=False):
|
||||
self.update(None, tags=None if keep_tags else [])
|
||||
|
|
|
@ -160,21 +160,21 @@ class Note(Content):
|
|||
# if local piece is created from a post, update post type_data and fanout
|
||||
p.sync_to_timeline()
|
||||
if owner.user.preference.mastodon_default_repost and owner.user.mastodon:
|
||||
p.sync_to_mastodon()
|
||||
p.sync_to_social_accounts()
|
||||
return p
|
||||
|
||||
@cached_property
|
||||
def shelfmember(self) -> ShelfMember | None:
|
||||
return ShelfMember.objects.filter(item=self.item, owner=self.owner).first()
|
||||
|
||||
def to_mastodon_params(self):
|
||||
def to_crosspost_params(self):
|
||||
footer = f"\n—\n《{self.item.display_title}》 {self.progress_display}\n{self.item.absolute_url}"
|
||||
params = {
|
||||
"spoiler_text": self.title,
|
||||
"content": self.content + footer,
|
||||
"sensitive": self.sensitive,
|
||||
"reply_to_toot_url": (
|
||||
self.shelfmember.get_mastodon_crosspost_url()
|
||||
"reply_to_id": (
|
||||
(self.shelfmember.metadata or {}).get("mastodon_id")
|
||||
if self.shelfmember
|
||||
else None
|
||||
),
|
||||
|
|
|
@ -86,7 +86,7 @@ class Review(Content):
|
|||
def get_crosspost_template(self):
|
||||
return _(ShelfManager.get_action_template("reviewed", self.item.category))
|
||||
|
||||
def to_mastodon_params(self):
|
||||
def to_crosspost_params(self):
|
||||
content = (
|
||||
self.get_crosspost_template().format(item=self.item.display_title)
|
||||
+ f"\n{self.title}\n{self.absolute_url} "
|
||||
|
@ -153,7 +153,8 @@ class Review(Content):
|
|||
review, created = cls.objects.update_or_create(
|
||||
item=item, owner=owner, defaults=defaults
|
||||
)
|
||||
review.sync_to_timeline(delete_existing=delete_existing_post)
|
||||
update_mode = 1 if delete_existing_post else 0
|
||||
review.sync_to_timeline(update_mode)
|
||||
if share_to_mastodon:
|
||||
review.sync_to_mastodon(delete_existing=delete_existing_post)
|
||||
review.sync_to_social_accounts(update_mode)
|
||||
return review
|
||||
|
|
|
@ -2,11 +2,10 @@ from datetime import datetime
|
|||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection, models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from django.utils.translation import pgettext_lazy as __
|
||||
from loguru import logger
|
||||
|
||||
from catalog.models import Item, ItemCategory
|
||||
|
@ -15,9 +14,12 @@ from users.models import APIdentity
|
|||
|
||||
from .common import q_item_in_category
|
||||
from .itemlist import List, ListMember
|
||||
from .renderers import render_post_with_macro, render_rating
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .comment import Comment
|
||||
from .mark import Mark
|
||||
from .rating import Rating
|
||||
|
||||
|
||||
class ShelfType(models.TextChoices):
|
||||
|
@ -356,6 +358,71 @@ class ShelfMember(ListMember):
|
|||
p.link_post_id(post.id)
|
||||
return p
|
||||
|
||||
def get_crosspost_postfix(self):
|
||||
tags = render_post_with_macro(
|
||||
self.owner.user.preference.mastodon_append_tag, self.item
|
||||
)
|
||||
return "\n" + tags if tags else ""
|
||||
|
||||
def get_crosspost_template(self):
|
||||
return _(ShelfManager.get_action_template(self.shelf_type, self.item.category))
|
||||
|
||||
def to_crosspost_params(self):
|
||||
action = self.get_crosspost_template().format(item=self.item.display_title)
|
||||
if self.sibling_comment:
|
||||
spoiler, txt = Takahe.get_spoiler_text(self.sibling_comment.text, self.item)
|
||||
else:
|
||||
spoiler, txt = None, ""
|
||||
stars = (
|
||||
self.owner.user.mastodon.rating_to_emoji(self.sibling_rating.grade)
|
||||
if self.sibling_rating and self.owner.user.mastodon
|
||||
else ""
|
||||
)
|
||||
content = f"{action} {stars} \n{self.item.absolute_url}\n{txt}\n{self.get_crosspost_postfix()}"
|
||||
params = {"content": content, "spoiler_text": spoiler}
|
||||
return params
|
||||
|
||||
def to_post_params(self):
|
||||
item_link = f"{settings.SITE_INFO['site_url']}/~neodb~{self.item.url}"
|
||||
action = self.get_crosspost_template().format(
|
||||
item=f'<a href="{item_link}">{self.item.display_title}</a>'
|
||||
)
|
||||
if self.sibling_comment:
|
||||
spoiler, txt = Takahe.get_spoiler_text(self.sibling_comment.text, self.item)
|
||||
else:
|
||||
spoiler, txt = None, ""
|
||||
postfix = self.get_crosspost_postfix()
|
||||
# add @user.mastodon.handle so that user can see it on Mastodon ?
|
||||
# if self.visibility and self.owner.user.mastodon:
|
||||
# postfix += f" @{self.owner.user.mastodon.handle}"
|
||||
content = f"{render_rating(self.sibling_rating.grade, 1) if self.sibling_rating else ''} \n{txt}\n{postfix}"
|
||||
return {
|
||||
"prepend_content": action,
|
||||
"content": content,
|
||||
"summary": spoiler,
|
||||
"sensitive": spoiler is not None,
|
||||
}
|
||||
|
||||
def get_ap_data(self):
|
||||
data = super().get_ap_data()
|
||||
if self.sibling_comment:
|
||||
data["object"]["relatedWith"].append(self.sibling_comment.ap_object)
|
||||
if self.sibling_rating:
|
||||
data["object"]["relatedWith"].append(self.sibling_rating.ap_object)
|
||||
return data
|
||||
|
||||
@cached_property
|
||||
def sibling_comment(self) -> "Comment | None":
|
||||
from .comment import Comment
|
||||
|
||||
return Comment.objects.filter(owner=self.owner, item=self.item).first()
|
||||
|
||||
@cached_property
|
||||
def sibling_rating(self) -> "Rating | None":
|
||||
from .rating import Rating
|
||||
|
||||
return Rating.objects.filter(owner=self.owner, item=self.item).first()
|
||||
|
||||
@cached_property
|
||||
def mark(self) -> "Mark":
|
||||
from .mark import Mark
|
||||
|
@ -405,6 +472,7 @@ class ShelfMember(ListMember):
|
|||
def link_post_id(self, post_id: int):
|
||||
if self.local:
|
||||
self.ensure_log_entry().link_post_id(post_id)
|
||||
print(self.ensure_log_entry(), post_id)
|
||||
return super().link_post_id(post_id)
|
||||
|
||||
|
||||
|
@ -461,6 +529,11 @@ class ShelfLogEntry(models.Model):
|
|||
def link_post_id(self, post_id: int):
|
||||
ShelfLogEntryPost.objects.get_or_create(log_entry=self, post_id=post_id)
|
||||
|
||||
def all_post_ids(self):
|
||||
return ShelfLogEntryPost.objects.filter(log_entry=self).values_list(
|
||||
"post_id", flat=True
|
||||
)
|
||||
|
||||
|
||||
class ShelfLogEntryPost(models.Model):
|
||||
log_entry = models.ForeignKey(ShelfLogEntry, on_delete=models.CASCADE)
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<fieldset>
|
||||
{% if request.user.mastodon %}
|
||||
{% if request.user.mastodon or request.user.threads or request.user.bluesky %}
|
||||
<label for="id_share_to_mastodon">
|
||||
<input role="switch"
|
||||
type="checkbox"
|
||||
|
|
|
@ -180,10 +180,10 @@ def share_collection(
|
|||
)
|
||||
)
|
||||
content = f"{user_str}:{collection.title}\n{link}\n{comment}{tags}"
|
||||
response = user.mastodon.post(content, visibility)
|
||||
if response is not None and response.status_code in [200, 201]:
|
||||
try:
|
||||
user.mastodon.post(content, visibility)
|
||||
return True
|
||||
else:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
@ -183,9 +183,10 @@ def comment(request: AuthedHttpRequest, item_uuid):
|
|||
comment = Comment.objects.update_or_create(
|
||||
owner=request.user.identity, item=item, defaults=d
|
||||
)[0]
|
||||
comment.sync_to_timeline(delete_existing=delete_existing_post)
|
||||
update_mode = 1 if delete_existing_post else 0
|
||||
comment.sync_to_timeline(update_mode)
|
||||
if share_to_mastodon:
|
||||
comment.sync_to_mastodon(delete_existing=delete_existing_post)
|
||||
comment.sync_to_social_accounts(update_mode)
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
||||
|
||||
|
||||
|
|
|
@ -108,7 +108,8 @@ def note_edit(request: AuthedHttpRequest, item_uuid: str, note_uuid: str = ""):
|
|||
delete_existing_post = (
|
||||
orig_visibility is not None and orig_visibility != note.visibility
|
||||
)
|
||||
note.sync_to_timeline(delete_existing=delete_existing_post)
|
||||
update_mode = 1 if delete_existing_post else 0
|
||||
note.sync_to_timeline(update_mode)
|
||||
if form.cleaned_data["share_to_mastodon"]:
|
||||
note.sync_to_mastodon(delete_existing=delete_existing_post)
|
||||
note.sync_to_social_accounts(update_mode)
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
||||
|
|
|
@ -129,9 +129,12 @@ class WrappedShareView(LoginRequiredMixin, TemplateView):
|
|||
)
|
||||
classic_crosspost = user.preference.mastodon_repost_mode == 1
|
||||
if classic_crosspost and user.mastodon:
|
||||
user.mastodon.post(
|
||||
comment, visibility, attachments=[("year.png", img, "image/png")]
|
||||
)
|
||||
try:
|
||||
user.mastodon.post(
|
||||
comment, visibility, attachments=[("year.png", img, "image/png")]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
elif post and user.mastodon:
|
||||
user.mastodon.boost_later(post.url)
|
||||
messages.add_message(request, messages.INFO, _("Summary posted to timeline."))
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -10,6 +10,7 @@ import django_rq
|
|||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import PermissionDenied, RequestAborted
|
||||
from django.db import models
|
||||
from django.db.models import Count
|
||||
from django.http import HttpRequest
|
||||
|
@ -139,12 +140,11 @@ def boost_toot(domain, token, toot_url):
|
|||
return None
|
||||
|
||||
|
||||
def delete_toot(api_domain, access_token, toot_url):
|
||||
def delete_toot(api_domain, access_token, toot_id):
|
||||
headers = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
}
|
||||
toot_id = get_status_id_by_url(toot_url)
|
||||
url = "https://" + api_domain + API_PUBLISH_TOOT + "/" + toot_id
|
||||
try:
|
||||
response = delete(url, headers=headers)
|
||||
|
@ -159,8 +159,8 @@ def post_toot2(
|
|||
access_token: str,
|
||||
content: str,
|
||||
visibility: TootVisibilityEnum,
|
||||
update_toot_url: str | None = None,
|
||||
reply_to_toot_url: str | None = None,
|
||||
update_id: str | None = None,
|
||||
reply_to_id: str | None = None,
|
||||
sensitive: bool = False,
|
||||
spoiler_text: str | None = None,
|
||||
attachments: list = [],
|
||||
|
@ -177,8 +177,8 @@ def post_toot2(
|
|||
"status": content,
|
||||
"visibility": visibility,
|
||||
}
|
||||
update_id = get_status_id_by_url(update_toot_url)
|
||||
reply_to_id = get_status_id_by_url(reply_to_toot_url)
|
||||
# update_id = get_status_id_by_url(update_toot_url)
|
||||
# reply_to_id = get_status_id_by_url(reply_to_toot_url)
|
||||
if reply_to_id:
|
||||
payload["in_reply_to_id"] = reply_to_id
|
||||
if spoiler_text:
|
||||
|
@ -415,7 +415,9 @@ def obtain_token(site, code, request):
|
|||
try:
|
||||
response = post(url, data=payload, headers=headers, auth=auth)
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"Error {url} {response.status_code}")
|
||||
logger.warning(
|
||||
f"Error {url} {payload} {response.status_code} {response.content}"
|
||||
)
|
||||
return None, None
|
||||
except Exception as e:
|
||||
logger.warning(f"Error {url} {e}")
|
||||
|
@ -467,6 +469,13 @@ def get_or_create_fediverse_application(login_domain):
|
|||
if not app:
|
||||
app = MastodonApplication.objects.filter(api_domain__iexact=domain).first()
|
||||
if app:
|
||||
if " Firefish " in app.server_version:
|
||||
data = create_app(app.api_domain, True).json()
|
||||
app.app_id = data["id"]
|
||||
app.client_id = data["client_id"]
|
||||
app.client_secret = data["client_secret"]
|
||||
app.vapid_key = data.get("vapid_key", "")
|
||||
app.save()
|
||||
return app
|
||||
if not settings.MASTODON_ALLOW_ANY_SITE:
|
||||
logger.warning(f"Disallowed to create app for {domain}")
|
||||
|
@ -804,38 +813,55 @@ class MastodonAccount(SocialAccount):
|
|||
]
|
||||
)
|
||||
|
||||
def boost(self, post_url: str):
|
||||
boost_toot(self.api_domain, self.access_token, post_url)
|
||||
|
||||
def boost_later(self, post_url: str):
|
||||
django_rq.get_queue("fetch").enqueue(
|
||||
boost_toot, self.api_domain, self.access_token, post_url
|
||||
)
|
||||
|
||||
def delete_later(self, post_url: str):
|
||||
def delete_post(self, post_id: str):
|
||||
delete_toot(self.api_domain, self.access_token, post_id)
|
||||
|
||||
def delete_post_later(self, post_id: str):
|
||||
django_rq.get_queue("fetch").enqueue(
|
||||
delete_toot, self.api_domain, self.access_token, post_url
|
||||
delete_toot, self.api_domain, self.access_token, post_id
|
||||
)
|
||||
|
||||
def post(
|
||||
self,
|
||||
content: str,
|
||||
visibility: "VisibilityType",
|
||||
update_toot_url: str | None = None,
|
||||
reply_to_toot_url: str | None = None,
|
||||
update_id: str | None = None,
|
||||
reply_to_id: str | None = None,
|
||||
sensitive: bool = False,
|
||||
spoiler_text: str | None = None,
|
||||
attachments: list = [],
|
||||
) -> requests.Response | None:
|
||||
) -> dict:
|
||||
v = get_toot_visibility(visibility, self.user)
|
||||
return post_toot2(
|
||||
response = post_toot2(
|
||||
self.api_domain,
|
||||
self.access_token,
|
||||
content,
|
||||
v,
|
||||
update_toot_url,
|
||||
reply_to_toot_url,
|
||||
update_id,
|
||||
reply_to_id,
|
||||
sensitive,
|
||||
spoiler_text,
|
||||
attachments,
|
||||
)
|
||||
if response:
|
||||
if response.status_code in [200, 201]:
|
||||
j = response.json()
|
||||
return {"id": j["id"], "url": j["url"]}
|
||||
elif response.status_code == 401:
|
||||
raise PermissionDenied()
|
||||
raise RequestAborted()
|
||||
|
||||
def sync_later(self):
|
||||
Takahe.fetch_remote_identity(self.handle)
|
||||
# TODO
|
||||
|
||||
def get_reauthorize_url(self):
|
||||
return reverse("mastodon:connect") + "?domain=" + self.domain
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import functools
|
||||
from datetime import timedelta
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import RequestAborted
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
@ -14,22 +16,22 @@ from .common import SocialAccount
|
|||
|
||||
get = functools.partial(
|
||||
requests.get,
|
||||
timeout=settings.MASTODON_TIMEOUT,
|
||||
timeout=settings.THREADS_TIMEOUT,
|
||||
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
||||
)
|
||||
put = functools.partial(
|
||||
requests.put,
|
||||
timeout=settings.MASTODON_TIMEOUT,
|
||||
timeout=settings.THREADS_TIMEOUT,
|
||||
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
||||
)
|
||||
post = functools.partial(
|
||||
requests.post,
|
||||
timeout=settings.MASTODON_TIMEOUT,
|
||||
timeout=settings.THREADS_TIMEOUT,
|
||||
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
||||
)
|
||||
delete = functools.partial(
|
||||
requests.post,
|
||||
timeout=settings.MASTODON_TIMEOUT,
|
||||
timeout=settings.THREADS_TIMEOUT,
|
||||
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
||||
)
|
||||
|
||||
|
@ -109,7 +111,7 @@ class Threads:
|
|||
def get_profile(
|
||||
token: str, user_id: str | None = None
|
||||
) -> dict[str, str | int] | None:
|
||||
url = f'https://graph.threads.net/v1.0/{user_id or "me"}?fields=id,username,name,threads_profile_picture_url,threads_biography&access_token={token}'
|
||||
url = f'https://graph.threads.net/v1.0/{user_id or "me"}?fields=id,username,threads_profile_picture_url,threads_biography&access_token={token}'
|
||||
try:
|
||||
response = get(url)
|
||||
except Exception as e:
|
||||
|
@ -124,6 +126,30 @@ class Threads:
|
|||
return None
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def post_single(token: str, user_id: str, text: str):
|
||||
url = f"https://graph.threads.net/v1.0/{user_id}/threads?media_type=TEXT&access_token={token}&text={quote(text)}"
|
||||
response = post(url)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
media_container_id = (response.json() or {}).get("id")
|
||||
if not media_container_id:
|
||||
return None
|
||||
url = f"https://graph.threads.net/v1.0/{user_id}/threads_publish?creation_id={media_container_id}&access_token={token}"
|
||||
response = post(url)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
return (response.json() or {}).get("id")
|
||||
|
||||
@staticmethod
|
||||
def get_single(token: str, media_id: str) -> dict | None:
|
||||
# url = f"https://graph.threads.net/v1.0/{media_id}?fields=id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post&access_token={token}"
|
||||
url = f"https://graph.threads.net/v1.0/{media_id}?fields=id,permalink,is_quote_post&access_token={token}"
|
||||
response = post(url)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
@staticmethod
|
||||
def authenticate(request: HttpRequest, code: str) -> "ThreadsAccount | None":
|
||||
token, expire, uid = Threads.obtain_token(request, code)
|
||||
|
@ -195,7 +221,7 @@ class ThreadsAccount(SocialAccount):
|
|||
return False
|
||||
data = Threads.get_profile(self.access_token)
|
||||
if not data:
|
||||
logger.warning("{self} unable to get profile")
|
||||
logger.warning(f"{self} unable to get profile")
|
||||
return False
|
||||
if self.handle != data["username"]:
|
||||
if self.handle:
|
||||
|
@ -206,3 +232,16 @@ class ThreadsAccount(SocialAccount):
|
|||
if save:
|
||||
self.save(update_fields=["account_data", "handle", "last_refresh"])
|
||||
return True
|
||||
|
||||
def post(self, content: str, **kwargs):
|
||||
text = (
|
||||
content.replace(settings.STAR_SOLID + " ", "🌕")
|
||||
.replace(settings.STAR_HALF + " ", "🌗")
|
||||
.replace(settings.STAR_EMPTY + " ", "🌑")
|
||||
)
|
||||
media_id = Threads.post_single(self.access_token, self.uid, text)
|
||||
if media_id:
|
||||
d = Threads.get_single(self.access_token, media_id)
|
||||
if d:
|
||||
return {"id": media_id, "url": d["permalink"]}
|
||||
raise RequestAborted()
|
||||
|
|
|
@ -8,7 +8,7 @@ urlpatterns = [
|
|||
path("login/oauth", mastodon_oauth, name="oauth"),
|
||||
path("mastodon/login", mastodon_login, name="login"),
|
||||
path("mastodon/reconnect", mastodon_reconnect, name="reconnect"),
|
||||
path("mastodon/disconnect", mastodon_disconnect, name="mastodon_disconnect"),
|
||||
path("mastodon/disconnect", mastodon_disconnect, name="disconnect"),
|
||||
# Email
|
||||
path("email/login", email_login, name="email_login"),
|
||||
path("email/verify", email_verify, name="email_verify"),
|
||||
|
|
|
@ -80,7 +80,8 @@ def disconnect_identity(request, account):
|
|||
with transaction.atomic():
|
||||
if request.user.social_accounts.all().count() <= 1:
|
||||
return render_error(
|
||||
_("Unlink identity failed"), _("Unable to unlink last login identity.")
|
||||
_("Disconnect identity failed"),
|
||||
_("You cannot disconnect last login identity."),
|
||||
)
|
||||
account.delete()
|
||||
return redirect(reverse("users:info"))
|
||||
|
|
|
@ -49,9 +49,13 @@ def threads_oauth(request: HttpRequest):
|
|||
|
||||
@require_http_methods(["GET"])
|
||||
def threads_uninstall(request: HttpRequest):
|
||||
print(request.GET)
|
||||
print(request.POST)
|
||||
return redirect(reverse("users:data"))
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def threads_delete(request: HttpRequest):
|
||||
print(request.GET)
|
||||
print(request.POST)
|
||||
return redirect(reverse("users:data"))
|
||||
|
|
|
@ -57,6 +57,7 @@ dependencies = [
|
|||
"deepmerge>=1.1.1",
|
||||
"django-typed-models @ git+https://github.com/alphatownsman/django-typed-models.git",
|
||||
"atproto>=0.0.48",
|
||||
"pyright>=1.1.370",
|
||||
]
|
||||
|
||||
[tool.rye]
|
||||
|
@ -94,7 +95,7 @@ django_settings_module = "boofilsic.settings"
|
|||
|
||||
[tool.ruff]
|
||||
exclude = ["neodb-takahe/*", "media", ".venv", ".git", "playground", "**/tests.py", "neodb", "**/migrations", "**/commands", "**/importers", "**/sites", "legacy" ]
|
||||
lint.ignore = ["F403", "F405"]
|
||||
lint.ignore = ["F401", "F403", "F405"]
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = []
|
||||
|
|
|
@ -229,7 +229,7 @@ pygments==2.18.0
|
|||
# via mkdocs-material
|
||||
pymdown-extensions==10.8.1
|
||||
# via mkdocs-material
|
||||
pyright==1.1.369
|
||||
pyright==1.1.370
|
||||
python-dateutil==2.9.0.post0
|
||||
# via dateparser
|
||||
# via django-auditlog
|
||||
|
|
|
@ -126,6 +126,8 @@ mistune==3.0.2
|
|||
multidict==6.0.5
|
||||
# via aiohttp
|
||||
# via yarl
|
||||
nodeenv==1.9.1
|
||||
# via pyright
|
||||
oauthlib==3.2.2
|
||||
# via django-oauth-toolkit
|
||||
openpyxl==3.1.3
|
||||
|
@ -146,6 +148,7 @@ pydantic==2.7.3
|
|||
# via django-ninja
|
||||
pydantic-core==2.18.4
|
||||
# via pydantic
|
||||
pyright==1.1.370
|
||||
python-dateutil==2.9.0.post0
|
||||
# via dateparser
|
||||
# via django-auditlog
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% load user_actions %}
|
||||
{% load duration %}
|
||||
{% for event in events %}
|
||||
{% with post=event.subject_post piece=event.subject_post.piece item=event.subject_post.piece.item %}
|
||||
{% with post=event.subject_post piece=event.subject_post.piece item=event.subject_post.item %}
|
||||
{% if not post %}
|
||||
<!-- invalid data {{ event.pk }} -->
|
||||
{% else %}
|
||||
|
@ -70,7 +70,7 @@
|
|||
{% trans "wrote a note" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if piece and item and piece.classname != 'note' %}
|
||||
{% if item and piece.classname != 'note' %}
|
||||
<article>{% include "_item_card.html" with item=item allow_embed=1 %}</article>
|
||||
{% endif %}
|
||||
<div>{{ post.summary|default:'' }}</div>
|
||||
|
|
|
@ -1092,6 +1092,16 @@ class Post(models.Model):
|
|||
return pcs[0]
|
||||
return next((p for p in pcs if p.__class__ == ShelfMember), None)
|
||||
|
||||
@cached_property
|
||||
def item(self):
|
||||
from journal.models import ShelfLogEntry
|
||||
|
||||
p = self.piece
|
||||
if p:
|
||||
return p.item if hasattr(p, "item") else None # type:ignore
|
||||
log = ShelfLogEntry.objects.filter(shelflogentrypost__post_id=self.pk).first()
|
||||
return log.item if log else None
|
||||
|
||||
@classmethod
|
||||
def create_local(
|
||||
cls,
|
||||
|
|
|
@ -626,51 +626,6 @@ class Takahe:
|
|||
collection.link_post_id(post.pk)
|
||||
return post
|
||||
|
||||
@staticmethod
|
||||
def post_mark(mark, share_as_new_post: bool, append_content="") -> Post | None:
|
||||
user = mark.owner.user
|
||||
stars = _rating_to_emoji(mark.rating_grade, 1)
|
||||
item_link = f"{settings.SITE_INFO['site_url']}/~neodb~{mark.item.url}"
|
||||
prepend_content = mark.get_action_for_feed(item_link=item_link)
|
||||
spoiler, txt = Takahe.get_spoiler_text(mark.comment_text, mark.item)
|
||||
content = f"{stars} \n{txt}\n{mark.tag_text}"
|
||||
data = {
|
||||
"object": {
|
||||
"tag": [mark.item.ap_object_ref],
|
||||
"relatedWith": [mark.shelfmember.ap_object],
|
||||
}
|
||||
}
|
||||
if mark.comment:
|
||||
data["object"]["relatedWith"].append(mark.comment.ap_object)
|
||||
if mark.rating:
|
||||
data["object"]["relatedWith"].append(mark.rating.ap_object)
|
||||
v = Takahe.visibility_n2t(mark.visibility, user.preference.post_public_mode)
|
||||
existing_post = (
|
||||
None
|
||||
if share_as_new_post
|
||||
or mark.shelfmember.latest_post is None
|
||||
or mark.shelfmember.latest_post.state in ["deleted", "deleted_fanned_out"]
|
||||
else mark.shelfmember.latest_post
|
||||
)
|
||||
post = Takahe.post(
|
||||
mark.owner.pk,
|
||||
content + append_content,
|
||||
v,
|
||||
prepend_content,
|
||||
"",
|
||||
spoiler,
|
||||
spoiler is not None,
|
||||
data,
|
||||
existing_post.pk if existing_post else None,
|
||||
mark.shelfmember.created_time,
|
||||
)
|
||||
if not post:
|
||||
return
|
||||
for piece in [mark.shelfmember, mark.comment, mark.rating]:
|
||||
if piece:
|
||||
piece.link_post_id(post.pk)
|
||||
return post
|
||||
|
||||
@staticmethod
|
||||
def interact_post(post_pk: int, identity_pk: int, type: str):
|
||||
post = Post.objects.filter(pk=post_pk).first()
|
||||
|
|
|
@ -14,23 +14,43 @@
|
|||
{% include "_header.html" %}
|
||||
<main>
|
||||
<div class="grid__main">
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans "Display name, avatar and other information" %}</summary>
|
||||
<form action="{% url 'users:profile' %}?next={{ request.path }}"
|
||||
method="post"
|
||||
{% if request.user.mastodon and not request.user.preference.mastodon_skip_userinfo %}onsubmit="return confirm('{% trans "Updating profile information here will turn off automatic sync of display name, bio and avatar from your Mastodon instance. Sure to continue?" %}')"{% endif %}
|
||||
enctype="multipart/form-data">
|
||||
<label>
|
||||
{% trans "Username" %}
|
||||
<input value="{{ request.user.username }}"
|
||||
aria-invalid="false"
|
||||
readonly
|
||||
disabled />
|
||||
</label>
|
||||
{% include "_field.html" with field=profile_form.name %}
|
||||
{% include "_field.html" with field=profile_form.summary %}
|
||||
{% include "_field.html" with field=profile_form.icon %}
|
||||
{% include "_field.html" with field=profile_form.discoverable %}
|
||||
{% include "_field.html" with field=profile_form.manually_approves_followers %}
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="{% trans 'Save' %}" id="save">
|
||||
</form>
|
||||
</details>
|
||||
</article>
|
||||
{% if allow_any_site %}
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Username and Email' %}</summary>
|
||||
<summary>{% trans 'Email' %}</summary>
|
||||
<form action="{% url 'users:register' %}?next={{ request.path }}"
|
||||
method="post">
|
||||
<small>{{ error }}</small>
|
||||
<input value="{{ request.user.username }}" type="hidden" name="username" />
|
||||
<fieldset>
|
||||
<label>
|
||||
{% trans "Username" %}
|
||||
<input name="username" _="on input remove [@disabled] from #save end" placeholder="{% trans "2-30 alphabets, numbers or underscore, can't be changed once saved" %}" required {% if request.user.username %}value="{{ request.user.username }}" aria-invalid="false" readonly{% endif %} pattern="^[a-zA-Z0-9_]{2,30}$" />
|
||||
</label>
|
||||
<label>
|
||||
{% trans "Email address" %}
|
||||
<input type="email"
|
||||
name="email"
|
||||
_="on input remove [@disabled] from #save then remove [@aria-invalid] end"
|
||||
_="on input remove [@disabled] from #save_email then remove [@aria-invalid] end"
|
||||
{% if request.user.email_account %}value="{{ request.user.email_account.handle }}" aria-invalid="false"{% endif %}
|
||||
placeholder="email"
|
||||
autocomplete="email" />
|
||||
|
@ -44,7 +64,7 @@
|
|||
</label>
|
||||
</fieldset>
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="{% trans 'Save' %}" disabled id="save">
|
||||
<input type="submit" value="{% trans 'Save' %}" disabled id="save_email">
|
||||
</form>
|
||||
</details>
|
||||
</article>
|
||||
|
@ -97,6 +117,16 @@
|
|||
</small>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% if request.user.mastodon %}
|
||||
<form action="{% url 'mastodon:disconnect' %}"
|
||||
method="post"
|
||||
onsubmit="return confirm('{% trans "Once disconnected, you will no longer be able login with this identity. Are you sure to continue?" %}')">
|
||||
{% csrf_token %}
|
||||
<input type="submit"
|
||||
value="{% trans 'Disconnect with this identity' %}"
|
||||
class="secondary" />
|
||||
</form>
|
||||
{% endif %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
|
@ -120,59 +150,22 @@
|
|||
</label>
|
||||
{% endif %}
|
||||
<input type="submit"
|
||||
{% if request.user.threads %} value="{% trans 'Link with a different threads.net account' %}" {% else %} {% trans "Link with a threads.net account" %} {% endif %} />
|
||||
value="{% if request.user.threads %} {% trans 'Link with a different threads.net account' %} {% else %} {% trans "Link with a threads.net account" %} {% endif %} " />
|
||||
</fieldset>
|
||||
</form>
|
||||
{% if request.user.threads %}
|
||||
<form action="{% url 'mastodon:threads_login' %}"
|
||||
method="post"
|
||||
onsubmit="return confirm('{% trans "Once disconnected, you will no longer be able login with this identity. Are you sure to continue?" %}')">
|
||||
{% csrf_token %}
|
||||
<input type="submit"
|
||||
value="{% trans 'Disconnect with Threads' %}"
|
||||
class="secondary" />
|
||||
</form>
|
||||
{% endif %}
|
||||
</details>
|
||||
</article>
|
||||
{% endif %}
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans "Display name, avatar and other information" %}</summary>
|
||||
<form action="{% url 'users:profile' %}?next={{ request.path }}"
|
||||
method="post"
|
||||
{% if request.user.mastodon and not request.user.preference.mastodon_skip_userinfo %}onsubmit="return confirm('{% trans "Updating profile information here will turn off automatic sync of display name, bio and avatar from your Mastodon instance. Sure to continue?" %}')"{% endif %}
|
||||
enctype="multipart/form-data">
|
||||
{% include "_field.html" with field=profile_form.name %}
|
||||
{% include "_field.html" with field=profile_form.summary %}
|
||||
{% include "_field.html" with field=profile_form.icon %}
|
||||
{% include "_field.html" with field=profile_form.discoverable %}
|
||||
{% include "_field.html" with field=profile_form.manually_approves_followers %}
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="{% trans 'Save' %}" id="save">
|
||||
</form>
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Users you are following' %}</summary>
|
||||
{% include 'users/relationship_list.html' with id="follow" list=request.user.identity.following_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Users who follow you' %}</summary>
|
||||
{% include 'users/relationship_list.html' with id="follower" list=request.user.identity.follower_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Users who request to follow you' %}</summary>
|
||||
{% include 'users/relationship_list.html' with id="follow_request" list=request.user.identity.requested_follower_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Users you are muting' %}</summary>
|
||||
{% include 'users/relationship_list.html' with id="mute" list=request.user.identity.muting_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Users you are blocking' %}</summary>
|
||||
{% include 'users/relationship_list.html' with id="block" list=request.user.identity.blocking_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Sync and import social account' %}</summary>
|
||||
|
@ -219,6 +212,36 @@
|
|||
</form>
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Users you are following' %}</summary>
|
||||
{% include 'users/relationship_list.html' with id="follow" list=request.user.identity.following_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Users who follow you' %}</summary>
|
||||
{% include 'users/relationship_list.html' with id="follower" list=request.user.identity.follower_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Users who request to follow you' %}</summary>
|
||||
{% include 'users/relationship_list.html' with id="follow_request" list=request.user.identity.requested_follower_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Users you are muting' %}</summary>
|
||||
{% include 'users/relationship_list.html' with id="mute" list=request.user.identity.muting_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
<article>
|
||||
<details>
|
||||
<summary>{% trans 'Users you are blocking' %}</summary>
|
||||
{% include 'users/relationship_list.html' with id="block" list=request.user.identity.blocking_identities.all %}
|
||||
</details>
|
||||
</article>
|
||||
{% if allow_any_site %}
|
||||
<article>
|
||||
<details>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</header>
|
||||
{% if form %}
|
||||
<form action="{% url 'users:register' %}" method="post">
|
||||
<small>{{ error }}</small>
|
||||
<small>{{ error|default:"" }}</small>
|
||||
<fieldset>
|
||||
<label>
|
||||
{% blocktrans %}Your username on {{ site_name }}{% endblocktrans %}
|
||||
|
|
Loading…
Add table
Reference in a new issue