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
|
# Timeout of requests to Mastodon, in seconds
|
||||||
MASTODON_TIMEOUT = env("NEODB_LOGIN_MASTODON_TIMEOUT", default=5) # type: ignore
|
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
|
TAKAHE_REMOTE_TIMEOUT = MASTODON_TIMEOUT
|
||||||
|
|
||||||
NEODB_USER_AGENT = f"NeoDB/{NEODB_VERSION} (+{SITE_INFO.get('site_url', 'undefined')})"
|
NEODB_USER_AGENT = f"NeoDB/{NEODB_VERSION} (+{SITE_INFO.get('site_url', 'undefined')})"
|
||||||
|
|
|
@ -122,7 +122,12 @@
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<ul class="messages" style="text-align:center">
|
<ul class="messages" style="text-align:center">
|
||||||
{% for message in messages %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -123,7 +123,7 @@ class Comment(Content):
|
||||||
ShelfManager.get_action_template(ShelfType.PROGRESS, self.item.category)
|
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)
|
spoiler_text, txt = render_spoiler_text(self.text, self.item)
|
||||||
content = (
|
content = (
|
||||||
self.get_crosspost_template().format(item=self.item.display_title)
|
self.get_crosspost_template().format(item=self.item.display_title)
|
||||||
|
|
|
@ -5,8 +5,11 @@ from datetime import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import TYPE_CHECKING, Any, Self
|
from typing import TYPE_CHECKING, Any, Self
|
||||||
|
|
||||||
|
import django_rq
|
||||||
|
|
||||||
# from deepmerge import always_merger
|
# from deepmerge import always_merger
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.signing import b62_decode, b62_encode
|
from django.core.signing import b62_decode, b62_encode
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import CharField, Q
|
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 django.utils.translation import gettext_lazy as _
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from polymorphic.models import PolymorphicModel
|
from polymorphic.models import PolymorphicModel
|
||||||
|
from user_messages import api as messages
|
||||||
|
|
||||||
from catalog.common.models import Item, ItemCategory
|
from catalog.common.models import Item, ItemCategory
|
||||||
from catalog.models import item_categories, item_content_types
|
from catalog.models import item_categories, item_content_types
|
||||||
from takahe.utils import Takahe
|
from takahe.utils import Takahe
|
||||||
|
from users.middlewares import activate_language_for_user
|
||||||
from users.models import APIdentity, User
|
from users.models import APIdentity, User
|
||||||
|
|
||||||
from .mixins import UserOwnedObjectMixin
|
from .mixins import UserOwnedObjectMixin
|
||||||
|
@ -90,36 +95,10 @@ def q_item_in_category(item_category: ItemCategory):
|
||||||
return Q(item__polymorphic_ctype__in=contenttype_ids)
|
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):
|
class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
likes: models.QuerySet["Like"]
|
likes: models.QuerySet["Like"]
|
||||||
|
metadata: models.JSONField[Any, Any]
|
||||||
url_path = "p" # subclass must specify this
|
url_path = "p" # subclass must specify this
|
||||||
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
||||||
local = models.BooleanField(default=True)
|
local = models.BooleanField(default=True)
|
||||||
|
@ -131,33 +110,16 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
def classname(self) -> str:
|
def classname(self) -> str:
|
||||||
return self.__class__.__name__.lower()
|
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):
|
def delete(self, *args, **kwargs):
|
||||||
if self.local:
|
if self.local:
|
||||||
Takahe.delete_posts(self.all_post_ids)
|
Takahe.delete_posts(self.all_post_ids)
|
||||||
toot_url = self.get_mastodon_crosspost_url()
|
toot_id = (
|
||||||
if toot_url and self.owner.user.mastodon:
|
(self.metadata or {}).get("mastodon_id")
|
||||||
self.owner.user.mastodon.delete_later(toot_url)
|
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)
|
return super().delete(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -265,7 +227,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def to_mastodon_params(self) -> dict[str, Any]:
|
def to_crosspost_params(self) -> dict[str, Any]:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -314,74 +276,120 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
# subclass may have to add additional code to update type_data in local post
|
# subclass may have to add additional code to update type_data in local post
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def sync_to_mastodon(self, delete_existing=False):
|
def get_crosspost_params(self):
|
||||||
user = self.owner.user
|
d = {
|
||||||
if not user.mastodon:
|
"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
|
return
|
||||||
if user.preference.mastodon_repost_mode == 1:
|
try:
|
||||||
if delete_existing:
|
r = threads.post(**params)
|
||||||
self.delete_mastodon_repost()
|
except Exception:
|
||||||
return self.crosspost_to_mastodon()
|
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:
|
elif self.latest_post:
|
||||||
if user.mastodon:
|
mastodon.boost(self.latest_post.url)
|
||||||
user.mastodon.boost_later(self.latest_post.url)
|
|
||||||
else:
|
else:
|
||||||
logger.warning("No post found for piece")
|
logger.warning("No post found for piece")
|
||||||
|
|
||||||
def delete_mastodon_repost(self):
|
def crosspost_to_mastodon(self, params):
|
||||||
toot_url = self.get_mastodon_crosspost_url()
|
mastodon = self.owner.user.mastodon
|
||||||
if toot_url:
|
if not mastodon:
|
||||||
self.set_mastodon_crosspost_url(None)
|
return False
|
||||||
if self.owner.user.mastodon:
|
r = mastodon.post(**params)
|
||||||
self.owner.user.mastodon.delete_later(toot_url)
|
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):
|
def get_ap_data(self):
|
||||||
user = self.owner.user
|
return {
|
||||||
if not user or not user.mastodon:
|
"object": {
|
||||||
return False, -1
|
"tag": (
|
||||||
d = {
|
[self.item.ap_object_ref] # type:ignore
|
||||||
"visibility": self.visibility,
|
if hasattr(self, "item")
|
||||||
"update_toot_url": self.get_mastodon_crosspost_url(),
|
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
|
user = self.owner.user
|
||||||
v = Takahe.visibility_n2t(self.visibility, user.preference.post_public_mode)
|
v = Takahe.visibility_n2t(self.visibility, user.preference.post_public_mode)
|
||||||
existing_post = self.latest_post
|
existing_post = self.latest_post
|
||||||
if existing_post and existing_post.state in ["deleted", "deleted_fanned_out"]:
|
if existing_post:
|
||||||
existing_post = None
|
if (
|
||||||
elif existing_post and delete_existing:
|
existing_post.state in ["deleted", "deleted_fanned_out"]
|
||||||
Takahe.delete_posts([existing_post.pk])
|
or update_mode == 2
|
||||||
existing_post = None
|
):
|
||||||
|
existing_post = None
|
||||||
|
elif update_mode == 1:
|
||||||
|
Takahe.delete_posts([existing_post.pk])
|
||||||
|
existing_post = None
|
||||||
params = {
|
params = {
|
||||||
"author_pk": self.owner.pk,
|
"author_pk": self.owner.pk,
|
||||||
"visibility": v,
|
"visibility": v,
|
||||||
"post_pk": existing_post.pk if existing_post else None,
|
"post_pk": existing_post.pk if existing_post else None,
|
||||||
"post_time": self.created_time, # type:ignore subclass must have this
|
"post_time": self.created_time, # type:ignore subclass must have this
|
||||||
"edit_time": self.edited_time, # type:ignore subclass must have this
|
"edit_time": self.edited_time, # type:ignore subclass must have this
|
||||||
"data": {
|
"data": self.get_ap_data(),
|
||||||
"object": {
|
|
||||||
"tag": (
|
|
||||||
[self.item.ap_object_ref] # type:ignore
|
|
||||||
if hasattr(self, "item")
|
|
||||||
else []
|
|
||||||
),
|
|
||||||
"relatedWith": [self.ap_object],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
params.update(self.to_post_params())
|
params.update(self.to_post_params())
|
||||||
post = Takahe.post(**params)
|
post = Takahe.post(**params)
|
||||||
|
|
|
@ -18,33 +18,6 @@ from .review import Review
|
||||||
from .shelf import Shelf, ShelfLogEntry, ShelfManager, ShelfMember, ShelfType
|
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:
|
class Mark:
|
||||||
"""
|
"""
|
||||||
Holding Mark for an item on an shelf,
|
Holding Mark for an item on an shelf,
|
||||||
|
@ -241,6 +214,7 @@ class Mark:
|
||||||
visibility = self.visibility
|
visibility = self.visibility
|
||||||
last_shelf_type = self.shelf_type
|
last_shelf_type = self.shelf_type
|
||||||
last_visibility = self.visibility if last_shelf_type else None
|
last_visibility = self.visibility if last_shelf_type else None
|
||||||
|
update_mode = 0
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
self.owner.tag_manager.tag_item(self.item, tags, visibility)
|
self.owner.tag_manager.tag_item(self.item, tags, visibility)
|
||||||
if shelf_type is None:
|
if shelf_type is None:
|
||||||
|
@ -264,8 +238,7 @@ class Mark:
|
||||||
self.shelfmember.visibility = visibility
|
self.shelfmember.visibility = visibility
|
||||||
shelfmember_changed = True
|
shelfmember_changed = True
|
||||||
# retract most recent post about this status when visibility changed
|
# retract most recent post about this status when visibility changed
|
||||||
if self.shelfmember.latest_post:
|
update_mode = 1
|
||||||
Takahe.delete_posts([self.shelfmember.latest_post.pk])
|
|
||||||
if created_time and created_time != self.shelfmember.created_time:
|
if created_time and created_time != self.shelfmember.created_time:
|
||||||
self.shelfmember.created_time = created_time
|
self.shelfmember.created_time = created_time
|
||||||
log_entry.timestamp = created_time
|
log_entry.timestamp = created_time
|
||||||
|
@ -277,6 +250,8 @@ class Mark:
|
||||||
if shelfmember_changed:
|
if shelfmember_changed:
|
||||||
self.shelfmember.save()
|
self.shelfmember.save()
|
||||||
else:
|
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)
|
shelf = Shelf.objects.get(owner=self.owner, shelf_type=shelf_type)
|
||||||
d = {"parent": shelf, "visibility": visibility, "position": 0}
|
d = {"parent": shelf, "visibility": visibility, "position": 0}
|
||||||
if metadata:
|
if metadata:
|
||||||
|
@ -305,28 +280,17 @@ class Mark:
|
||||||
)
|
)
|
||||||
self.rating_grade = rating_grade
|
self.rating_grade = rating_grade
|
||||||
# publish a new or updated ActivityPub post
|
# publish a new or updated ActivityPub post
|
||||||
user: User = self.owner.user
|
post = self.shelfmember.sync_to_timeline(update_mode)
|
||||||
post_as_new = shelf_type != last_shelf_type or visibility != last_visibility
|
if share_to_mastodon:
|
||||||
classic_crosspost = user.preference.mastodon_repost_mode == 1
|
self.shelfmember.sync_to_social_accounts(update_mode)
|
||||||
append = (
|
# auto add bookmark
|
||||||
f"@{user.mastodon.handle}\n"
|
if (
|
||||||
if visibility > 0
|
post
|
||||||
and share_to_mastodon
|
and shelf_type == ShelfType.PROGRESS
|
||||||
and not classic_crosspost
|
and self.item.category
|
||||||
and user.mastodon
|
in (self.owner.user.preference.auto_bookmark_cats or [])
|
||||||
else ""
|
):
|
||||||
)
|
Takahe.bookmark(post.pk, self.owner.pk)
|
||||||
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
|
|
||||||
|
|
||||||
def delete(self, keep_tags=False):
|
def delete(self, keep_tags=False):
|
||||||
self.update(None, tags=None if keep_tags else [])
|
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
|
# if local piece is created from a post, update post type_data and fanout
|
||||||
p.sync_to_timeline()
|
p.sync_to_timeline()
|
||||||
if owner.user.preference.mastodon_default_repost and owner.user.mastodon:
|
if owner.user.preference.mastodon_default_repost and owner.user.mastodon:
|
||||||
p.sync_to_mastodon()
|
p.sync_to_social_accounts()
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def shelfmember(self) -> ShelfMember | None:
|
def shelfmember(self) -> ShelfMember | None:
|
||||||
return ShelfMember.objects.filter(item=self.item, owner=self.owner).first()
|
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}"
|
footer = f"\n—\n《{self.item.display_title}》 {self.progress_display}\n{self.item.absolute_url}"
|
||||||
params = {
|
params = {
|
||||||
"spoiler_text": self.title,
|
"spoiler_text": self.title,
|
||||||
"content": self.content + footer,
|
"content": self.content + footer,
|
||||||
"sensitive": self.sensitive,
|
"sensitive": self.sensitive,
|
||||||
"reply_to_toot_url": (
|
"reply_to_id": (
|
||||||
self.shelfmember.get_mastodon_crosspost_url()
|
(self.shelfmember.metadata or {}).get("mastodon_id")
|
||||||
if self.shelfmember
|
if self.shelfmember
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
|
|
@ -86,7 +86,7 @@ class Review(Content):
|
||||||
def get_crosspost_template(self):
|
def get_crosspost_template(self):
|
||||||
return _(ShelfManager.get_action_template("reviewed", self.item.category))
|
return _(ShelfManager.get_action_template("reviewed", self.item.category))
|
||||||
|
|
||||||
def to_mastodon_params(self):
|
def to_crosspost_params(self):
|
||||||
content = (
|
content = (
|
||||||
self.get_crosspost_template().format(item=self.item.display_title)
|
self.get_crosspost_template().format(item=self.item.display_title)
|
||||||
+ f"\n{self.title}\n{self.absolute_url} "
|
+ f"\n{self.title}\n{self.absolute_url} "
|
||||||
|
@ -153,7 +153,8 @@ class Review(Content):
|
||||||
review, created = cls.objects.update_or_create(
|
review, created = cls.objects.update_or_create(
|
||||||
item=item, owner=owner, defaults=defaults
|
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:
|
if share_to_mastodon:
|
||||||
review.sync_to_mastodon(delete_existing=delete_existing_post)
|
review.sync_to_social_accounts(update_mode)
|
||||||
return review
|
return review
|
||||||
|
|
|
@ -2,11 +2,10 @@ from datetime import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db import connection, models
|
from django.db import connection, models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
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 loguru import logger
|
||||||
|
|
||||||
from catalog.models import Item, ItemCategory
|
from catalog.models import Item, ItemCategory
|
||||||
|
@ -15,9 +14,12 @@ from users.models import APIdentity
|
||||||
|
|
||||||
from .common import q_item_in_category
|
from .common import q_item_in_category
|
||||||
from .itemlist import List, ListMember
|
from .itemlist import List, ListMember
|
||||||
|
from .renderers import render_post_with_macro, render_rating
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .comment import Comment
|
||||||
from .mark import Mark
|
from .mark import Mark
|
||||||
|
from .rating import Rating
|
||||||
|
|
||||||
|
|
||||||
class ShelfType(models.TextChoices):
|
class ShelfType(models.TextChoices):
|
||||||
|
@ -356,6 +358,71 @@ class ShelfMember(ListMember):
|
||||||
p.link_post_id(post.id)
|
p.link_post_id(post.id)
|
||||||
return p
|
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
|
@cached_property
|
||||||
def mark(self) -> "Mark":
|
def mark(self) -> "Mark":
|
||||||
from .mark import Mark
|
from .mark import Mark
|
||||||
|
@ -405,6 +472,7 @@ class ShelfMember(ListMember):
|
||||||
def link_post_id(self, post_id: int):
|
def link_post_id(self, post_id: int):
|
||||||
if self.local:
|
if self.local:
|
||||||
self.ensure_log_entry().link_post_id(post_id)
|
self.ensure_log_entry().link_post_id(post_id)
|
||||||
|
print(self.ensure_log_entry(), post_id)
|
||||||
return super().link_post_id(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):
|
def link_post_id(self, post_id: int):
|
||||||
ShelfLogEntryPost.objects.get_or_create(log_entry=self, post_id=post_id)
|
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):
|
class ShelfLogEntryPost(models.Model):
|
||||||
log_entry = models.ForeignKey(ShelfLogEntry, on_delete=models.CASCADE)
|
log_entry = models.ForeignKey(ShelfLogEntry, on_delete=models.CASCADE)
|
||||||
|
|
|
@ -109,7 +109,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% if request.user.mastodon %}
|
{% if request.user.mastodon or request.user.threads or request.user.bluesky %}
|
||||||
<label for="id_share_to_mastodon">
|
<label for="id_share_to_mastodon">
|
||||||
<input role="switch"
|
<input role="switch"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
|
@ -180,10 +180,10 @@ def share_collection(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
content = f"{user_str}:{collection.title}\n{link}\n{comment}{tags}"
|
content = f"{user_str}:{collection.title}\n{link}\n{comment}{tags}"
|
||||||
response = user.mastodon.post(content, visibility)
|
try:
|
||||||
if response is not None and response.status_code in [200, 201]:
|
user.mastodon.post(content, visibility)
|
||||||
return True
|
return True
|
||||||
else:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -183,9 +183,10 @@ def comment(request: AuthedHttpRequest, item_uuid):
|
||||||
comment = Comment.objects.update_or_create(
|
comment = Comment.objects.update_or_create(
|
||||||
owner=request.user.identity, item=item, defaults=d
|
owner=request.user.identity, item=item, defaults=d
|
||||||
)[0]
|
)[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:
|
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", "/"))
|
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 = (
|
delete_existing_post = (
|
||||||
orig_visibility is not None and orig_visibility != note.visibility
|
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"]:
|
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", "/"))
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
||||||
|
|
|
@ -129,9 +129,12 @@ class WrappedShareView(LoginRequiredMixin, TemplateView):
|
||||||
)
|
)
|
||||||
classic_crosspost = user.preference.mastodon_repost_mode == 1
|
classic_crosspost = user.preference.mastodon_repost_mode == 1
|
||||||
if classic_crosspost and user.mastodon:
|
if classic_crosspost and user.mastodon:
|
||||||
user.mastodon.post(
|
try:
|
||||||
comment, visibility, attachments=[("year.png", img, "image/png")]
|
user.mastodon.post(
|
||||||
)
|
comment, visibility, attachments=[("year.png", img, "image/png")]
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
elif post and user.mastodon:
|
elif post and user.mastodon:
|
||||||
user.mastodon.boost_later(post.url)
|
user.mastodon.boost_later(post.url)
|
||||||
messages.add_message(request, messages.INFO, _("Summary posted to timeline."))
|
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
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.core.exceptions import PermissionDenied, RequestAborted
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
@ -139,12 +140,11 @@ def boost_toot(domain, token, toot_url):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def delete_toot(api_domain, access_token, toot_url):
|
def delete_toot(api_domain, access_token, toot_id):
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": USER_AGENT,
|
"User-Agent": USER_AGENT,
|
||||||
"Authorization": f"Bearer {access_token}",
|
"Authorization": f"Bearer {access_token}",
|
||||||
}
|
}
|
||||||
toot_id = get_status_id_by_url(toot_url)
|
|
||||||
url = "https://" + api_domain + API_PUBLISH_TOOT + "/" + toot_id
|
url = "https://" + api_domain + API_PUBLISH_TOOT + "/" + toot_id
|
||||||
try:
|
try:
|
||||||
response = delete(url, headers=headers)
|
response = delete(url, headers=headers)
|
||||||
|
@ -159,8 +159,8 @@ def post_toot2(
|
||||||
access_token: str,
|
access_token: str,
|
||||||
content: str,
|
content: str,
|
||||||
visibility: TootVisibilityEnum,
|
visibility: TootVisibilityEnum,
|
||||||
update_toot_url: str | None = None,
|
update_id: str | None = None,
|
||||||
reply_to_toot_url: str | None = None,
|
reply_to_id: str | None = None,
|
||||||
sensitive: bool = False,
|
sensitive: bool = False,
|
||||||
spoiler_text: str | None = None,
|
spoiler_text: str | None = None,
|
||||||
attachments: list = [],
|
attachments: list = [],
|
||||||
|
@ -177,8 +177,8 @@ def post_toot2(
|
||||||
"status": content,
|
"status": content,
|
||||||
"visibility": visibility,
|
"visibility": visibility,
|
||||||
}
|
}
|
||||||
update_id = get_status_id_by_url(update_toot_url)
|
# update_id = get_status_id_by_url(update_toot_url)
|
||||||
reply_to_id = get_status_id_by_url(reply_to_toot_url)
|
# reply_to_id = get_status_id_by_url(reply_to_toot_url)
|
||||||
if reply_to_id:
|
if reply_to_id:
|
||||||
payload["in_reply_to_id"] = reply_to_id
|
payload["in_reply_to_id"] = reply_to_id
|
||||||
if spoiler_text:
|
if spoiler_text:
|
||||||
|
@ -415,7 +415,9 @@ def obtain_token(site, code, request):
|
||||||
try:
|
try:
|
||||||
response = post(url, data=payload, headers=headers, auth=auth)
|
response = post(url, data=payload, headers=headers, auth=auth)
|
||||||
if response.status_code != 200:
|
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
|
return None, None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error {url} {e}")
|
logger.warning(f"Error {url} {e}")
|
||||||
|
@ -467,6 +469,13 @@ def get_or_create_fediverse_application(login_domain):
|
||||||
if not app:
|
if not app:
|
||||||
app = MastodonApplication.objects.filter(api_domain__iexact=domain).first()
|
app = MastodonApplication.objects.filter(api_domain__iexact=domain).first()
|
||||||
if app:
|
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
|
return app
|
||||||
if not settings.MASTODON_ALLOW_ANY_SITE:
|
if not settings.MASTODON_ALLOW_ANY_SITE:
|
||||||
logger.warning(f"Disallowed to create app for {domain}")
|
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):
|
def boost_later(self, post_url: str):
|
||||||
django_rq.get_queue("fetch").enqueue(
|
django_rq.get_queue("fetch").enqueue(
|
||||||
boost_toot, self.api_domain, self.access_token, post_url
|
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(
|
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(
|
def post(
|
||||||
self,
|
self,
|
||||||
content: str,
|
content: str,
|
||||||
visibility: "VisibilityType",
|
visibility: "VisibilityType",
|
||||||
update_toot_url: str | None = None,
|
update_id: str | None = None,
|
||||||
reply_to_toot_url: str | None = None,
|
reply_to_id: str | None = None,
|
||||||
sensitive: bool = False,
|
sensitive: bool = False,
|
||||||
spoiler_text: str | None = None,
|
spoiler_text: str | None = None,
|
||||||
attachments: list = [],
|
attachments: list = [],
|
||||||
) -> requests.Response | None:
|
) -> dict:
|
||||||
v = get_toot_visibility(visibility, self.user)
|
v = get_toot_visibility(visibility, self.user)
|
||||||
return post_toot2(
|
response = post_toot2(
|
||||||
self.api_domain,
|
self.api_domain,
|
||||||
self.access_token,
|
self.access_token,
|
||||||
content,
|
content,
|
||||||
v,
|
v,
|
||||||
update_toot_url,
|
update_id,
|
||||||
reply_to_toot_url,
|
reply_to_id,
|
||||||
sensitive,
|
sensitive,
|
||||||
spoiler_text,
|
spoiler_text,
|
||||||
attachments,
|
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):
|
def sync_later(self):
|
||||||
Takahe.fetch_remote_identity(self.handle)
|
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
|
import functools
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import RequestAborted
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -14,22 +16,22 @@ from .common import SocialAccount
|
||||||
|
|
||||||
get = functools.partial(
|
get = functools.partial(
|
||||||
requests.get,
|
requests.get,
|
||||||
timeout=settings.MASTODON_TIMEOUT,
|
timeout=settings.THREADS_TIMEOUT,
|
||||||
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
||||||
)
|
)
|
||||||
put = functools.partial(
|
put = functools.partial(
|
||||||
requests.put,
|
requests.put,
|
||||||
timeout=settings.MASTODON_TIMEOUT,
|
timeout=settings.THREADS_TIMEOUT,
|
||||||
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
||||||
)
|
)
|
||||||
post = functools.partial(
|
post = functools.partial(
|
||||||
requests.post,
|
requests.post,
|
||||||
timeout=settings.MASTODON_TIMEOUT,
|
timeout=settings.THREADS_TIMEOUT,
|
||||||
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
||||||
)
|
)
|
||||||
delete = functools.partial(
|
delete = functools.partial(
|
||||||
requests.post,
|
requests.post,
|
||||||
timeout=settings.MASTODON_TIMEOUT,
|
timeout=settings.THREADS_TIMEOUT,
|
||||||
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
headers={"User-Agent": settings.NEODB_USER_AGENT},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -109,7 +111,7 @@ class Threads:
|
||||||
def get_profile(
|
def get_profile(
|
||||||
token: str, user_id: str | None = None
|
token: str, user_id: str | None = None
|
||||||
) -> dict[str, str | int] | 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:
|
try:
|
||||||
response = get(url)
|
response = get(url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -124,6 +126,30 @@ class Threads:
|
||||||
return None
|
return None
|
||||||
return data
|
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
|
@staticmethod
|
||||||
def authenticate(request: HttpRequest, code: str) -> "ThreadsAccount | None":
|
def authenticate(request: HttpRequest, code: str) -> "ThreadsAccount | None":
|
||||||
token, expire, uid = Threads.obtain_token(request, code)
|
token, expire, uid = Threads.obtain_token(request, code)
|
||||||
|
@ -195,7 +221,7 @@ class ThreadsAccount(SocialAccount):
|
||||||
return False
|
return False
|
||||||
data = Threads.get_profile(self.access_token)
|
data = Threads.get_profile(self.access_token)
|
||||||
if not data:
|
if not data:
|
||||||
logger.warning("{self} unable to get profile")
|
logger.warning(f"{self} unable to get profile")
|
||||||
return False
|
return False
|
||||||
if self.handle != data["username"]:
|
if self.handle != data["username"]:
|
||||||
if self.handle:
|
if self.handle:
|
||||||
|
@ -206,3 +232,16 @@ class ThreadsAccount(SocialAccount):
|
||||||
if save:
|
if save:
|
||||||
self.save(update_fields=["account_data", "handle", "last_refresh"])
|
self.save(update_fields=["account_data", "handle", "last_refresh"])
|
||||||
return True
|
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("login/oauth", mastodon_oauth, name="oauth"),
|
||||||
path("mastodon/login", mastodon_login, name="login"),
|
path("mastodon/login", mastodon_login, name="login"),
|
||||||
path("mastodon/reconnect", mastodon_reconnect, name="reconnect"),
|
path("mastodon/reconnect", mastodon_reconnect, name="reconnect"),
|
||||||
path("mastodon/disconnect", mastodon_disconnect, name="mastodon_disconnect"),
|
path("mastodon/disconnect", mastodon_disconnect, name="disconnect"),
|
||||||
# Email
|
# Email
|
||||||
path("email/login", email_login, name="email_login"),
|
path("email/login", email_login, name="email_login"),
|
||||||
path("email/verify", email_verify, name="email_verify"),
|
path("email/verify", email_verify, name="email_verify"),
|
||||||
|
|
|
@ -80,7 +80,8 @@ def disconnect_identity(request, account):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if request.user.social_accounts.all().count() <= 1:
|
if request.user.social_accounts.all().count() <= 1:
|
||||||
return render_error(
|
return render_error(
|
||||||
_("Unlink identity failed"), _("Unable to unlink last login identity.")
|
_("Disconnect identity failed"),
|
||||||
|
_("You cannot disconnect last login identity."),
|
||||||
)
|
)
|
||||||
account.delete()
|
account.delete()
|
||||||
return redirect(reverse("users:info"))
|
return redirect(reverse("users:info"))
|
||||||
|
|
|
@ -49,9 +49,13 @@ def threads_oauth(request: HttpRequest):
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def threads_uninstall(request: HttpRequest):
|
def threads_uninstall(request: HttpRequest):
|
||||||
|
print(request.GET)
|
||||||
|
print(request.POST)
|
||||||
return redirect(reverse("users:data"))
|
return redirect(reverse("users:data"))
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def threads_delete(request: HttpRequest):
|
def threads_delete(request: HttpRequest):
|
||||||
|
print(request.GET)
|
||||||
|
print(request.POST)
|
||||||
return redirect(reverse("users:data"))
|
return redirect(reverse("users:data"))
|
||||||
|
|
|
@ -57,6 +57,7 @@ dependencies = [
|
||||||
"deepmerge>=1.1.1",
|
"deepmerge>=1.1.1",
|
||||||
"django-typed-models @ git+https://github.com/alphatownsman/django-typed-models.git",
|
"django-typed-models @ git+https://github.com/alphatownsman/django-typed-models.git",
|
||||||
"atproto>=0.0.48",
|
"atproto>=0.0.48",
|
||||||
|
"pyright>=1.1.370",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.rye]
|
[tool.rye]
|
||||||
|
@ -94,7 +95,7 @@ django_settings_module = "boofilsic.settings"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
exclude = ["neodb-takahe/*", "media", ".venv", ".git", "playground", "**/tests.py", "neodb", "**/migrations", "**/commands", "**/importers", "**/sites", "legacy" ]
|
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]
|
[tool.setuptools]
|
||||||
py-modules = []
|
py-modules = []
|
||||||
|
|
|
@ -229,7 +229,7 @@ pygments==2.18.0
|
||||||
# via mkdocs-material
|
# via mkdocs-material
|
||||||
pymdown-extensions==10.8.1
|
pymdown-extensions==10.8.1
|
||||||
# via mkdocs-material
|
# via mkdocs-material
|
||||||
pyright==1.1.369
|
pyright==1.1.370
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
# via dateparser
|
# via dateparser
|
||||||
# via django-auditlog
|
# via django-auditlog
|
||||||
|
|
|
@ -126,6 +126,8 @@ mistune==3.0.2
|
||||||
multidict==6.0.5
|
multidict==6.0.5
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
# via yarl
|
# via yarl
|
||||||
|
nodeenv==1.9.1
|
||||||
|
# via pyright
|
||||||
oauthlib==3.2.2
|
oauthlib==3.2.2
|
||||||
# via django-oauth-toolkit
|
# via django-oauth-toolkit
|
||||||
openpyxl==3.1.3
|
openpyxl==3.1.3
|
||||||
|
@ -146,6 +148,7 @@ pydantic==2.7.3
|
||||||
# via django-ninja
|
# via django-ninja
|
||||||
pydantic-core==2.18.4
|
pydantic-core==2.18.4
|
||||||
# via pydantic
|
# via pydantic
|
||||||
|
pyright==1.1.370
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
# via dateparser
|
# via dateparser
|
||||||
# via django-auditlog
|
# via django-auditlog
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
{% load user_actions %}
|
{% load user_actions %}
|
||||||
{% load duration %}
|
{% load duration %}
|
||||||
{% for event in events %}
|
{% 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 %}
|
{% if not post %}
|
||||||
<!-- invalid data {{ event.pk }} -->
|
<!-- invalid data {{ event.pk }} -->
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
{% trans "wrote a note" %}
|
{% trans "wrote a note" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</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>
|
<article>{% include "_item_card.html" with item=item allow_embed=1 %}</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div>{{ post.summary|default:'' }}</div>
|
<div>{{ post.summary|default:'' }}</div>
|
||||||
|
|
|
@ -1092,6 +1092,16 @@ class Post(models.Model):
|
||||||
return pcs[0]
|
return pcs[0]
|
||||||
return next((p for p in pcs if p.__class__ == ShelfMember), None)
|
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
|
@classmethod
|
||||||
def create_local(
|
def create_local(
|
||||||
cls,
|
cls,
|
||||||
|
|
|
@ -626,51 +626,6 @@ class Takahe:
|
||||||
collection.link_post_id(post.pk)
|
collection.link_post_id(post.pk)
|
||||||
return post
|
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
|
@staticmethod
|
||||||
def interact_post(post_pk: int, identity_pk: int, type: str):
|
def interact_post(post_pk: int, identity_pk: int, type: str):
|
||||||
post = Post.objects.filter(pk=post_pk).first()
|
post = Post.objects.filter(pk=post_pk).first()
|
||||||
|
|
|
@ -14,23 +14,43 @@
|
||||||
{% include "_header.html" %}
|
{% include "_header.html" %}
|
||||||
<main>
|
<main>
|
||||||
<div class="grid__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 %}
|
{% if allow_any_site %}
|
||||||
<article>
|
<article>
|
||||||
<details>
|
<details>
|
||||||
<summary>{% trans 'Username and Email' %}</summary>
|
<summary>{% trans 'Email' %}</summary>
|
||||||
<form action="{% url 'users:register' %}?next={{ request.path }}"
|
<form action="{% url 'users:register' %}?next={{ request.path }}"
|
||||||
method="post">
|
method="post">
|
||||||
<small>{{ error }}</small>
|
<input value="{{ request.user.username }}" type="hidden" name="username" />
|
||||||
<fieldset>
|
<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>
|
<label>
|
||||||
{% trans "Email address" %}
|
{% trans "Email address" %}
|
||||||
<input type="email"
|
<input type="email"
|
||||||
name="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 %}
|
{% if request.user.email_account %}value="{{ request.user.email_account.handle }}" aria-invalid="false"{% endif %}
|
||||||
placeholder="email"
|
placeholder="email"
|
||||||
autocomplete="email" />
|
autocomplete="email" />
|
||||||
|
@ -44,7 +64,7 @@
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="submit" value="{% trans 'Save' %}" disabled id="save">
|
<input type="submit" value="{% trans 'Save' %}" disabled id="save_email">
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
</article>
|
</article>
|
||||||
|
@ -97,6 +117,16 @@
|
||||||
</small>
|
</small>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</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>
|
</details>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
|
@ -120,59 +150,22 @@
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input type="submit"
|
<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>
|
</fieldset>
|
||||||
</form>
|
</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>
|
</details>
|
||||||
</article>
|
</article>
|
||||||
{% endif %}
|
{% 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>
|
<article>
|
||||||
<details>
|
<details>
|
||||||
<summary>{% trans 'Sync and import social account' %}</summary>
|
<summary>{% trans 'Sync and import social account' %}</summary>
|
||||||
|
@ -219,6 +212,36 @@
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
</article>
|
</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 %}
|
{% if allow_any_site %}
|
||||||
<article>
|
<article>
|
||||||
<details>
|
<details>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
</header>
|
</header>
|
||||||
{% if form %}
|
{% if form %}
|
||||||
<form action="{% url 'users:register' %}" method="post">
|
<form action="{% url 'users:register' %}" method="post">
|
||||||
<small>{{ error }}</small>
|
<small>{{ error|default:"" }}</small>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label>
|
||||||
{% blocktrans %}Your username on {{ site_name }}{% endblocktrans %}
|
{% blocktrans %}Your username on {{ site_name }}{% endblocktrans %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue