bluesky post links and embeds

This commit is contained in:
Your Name 2024-07-05 16:26:26 -04:00 committed by Henri Dickson
parent 4e97cb7083
commit b727397cd1
16 changed files with 152 additions and 124 deletions

View file

@ -498,6 +498,10 @@ class Item(PolymorphicModel):
def display_title(self) -> str: def display_title(self) -> str:
return self.title return self.title
@property
def display_description(self):
return self.brief[:155]
@classmethod @classmethod
def get_by_url(cls, url_or_b62: str, resolve_merge=False) -> "Self | None": def get_by_url(cls, url_or_b62: str, resolve_merge=False) -> "Self | None":
b62 = url_or_b62.strip().split("/")[-1] b62 = url_or_b62.strip().split("/")[-1]

View file

@ -12,12 +12,7 @@ from users.models import APIdentity
from .common import Content from .common import Content
from .rating import Rating from .rating import Rating
from .renderers import ( from .renderers import render_post_with_macro, render_spoiler_text, render_text
render_post_with_macro,
render_rating,
render_spoiler_text,
render_text,
)
from .shelf import ShelfManager, ShelfType from .shelf import ShelfManager, ShelfType
@ -125,15 +120,14 @@ class Comment(Content):
def to_crosspost_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 = ( txt = "\n" + txt if txt else ""
self.get_crosspost_template().format(item=self.item.display_title) action = self.get_crosspost_template().format(item="##obj##")
+ f"\n{txt}\n{settings.SITE_INFO['site_url']}{self.item_url}" content = f"{action}\n##obj_link_if_plain##{txt}{self.get_crosspost_postfix()}"
+ self.get_crosspost_postfix()
)
params = { params = {
"content": content, "content": content,
"spoiler_text": spoiler_text, "spoiler_text": spoiler_text,
"sensitive": bool(spoiler_text), "sensitive": bool(spoiler_text),
"obj": self.item,
} }
return params return params

View file

@ -3,7 +3,6 @@ import uuid
from abc import abstractmethod from abc import abstractmethod
from datetime import datetime from datetime import datetime
from functools import cached_property from functools import cached_property
from operator import pos
from typing import TYPE_CHECKING, Any, Self from typing import TYPE_CHECKING, Any, Self
import django_rq import django_rq
@ -123,11 +122,11 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
@property @property
def url(self): def url(self):
return f"/{self.url_path}/{self.uuid}" if self.url_path else None return f"/{self.url_path}/{self.uuid}"
@property @property
def absolute_url(self): def absolute_url(self):
return (settings.SITE_INFO["site_url"] + self.url) if self.url_path else None return settings.SITE_INFO["site_url"] + self.url
@property @property
def api_url(self): def api_url(self):
@ -219,10 +218,18 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
@abstractmethod @abstractmethod
def to_post_params(self) -> dict[str, Any]: def to_post_params(self) -> dict[str, Any]:
"""
returns a dict of parameter to create a post
"""
return {} return {}
@abstractmethod @abstractmethod
def to_crosspost_params(self) -> dict[str, Any]: def to_crosspost_params(self) -> dict[str, Any]:
"""
returns a dict of parameter to create a post for each platform
"content" - required, may contain ##obj## / ##obj_link_if_plain## / ##rating##
...
"""
return {} return {}
@classmethod @classmethod
@ -343,7 +350,8 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
except Exception as e: except Exception as e:
logger.warning(f"Delete {bluesky} post {post_id} error {e}") logger.warning(f"Delete {bluesky} post {post_id} error {e}")
r = bluesky.post(**params) r = bluesky.post(**params)
self.metadata.update({"bluesky_" + k: v for k, v in r.items()}) if r:
self.metadata.update({"bluesky_" + k: v for k, v in r.items()})
return True return True
def sync_to_threads(self, params, update_mode): def sync_to_threads(self, params, update_mode):
@ -359,7 +367,8 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
logger.warning(f"{self} post to {threads} failed") logger.warning(f"{self} post to {threads} failed")
messages.error(threads.user, _("A recent post was not posted to Threads.")) messages.error(threads.user, _("A recent post was not posted to Threads."))
return False return False
self.metadata.update({"threads_" + k: v for k, v in r.items()}) if r:
self.metadata.update({"threads_" + k: v for k, v in r.items()})
return True return True
def sync_to_mastodon(self, params, update_mode): def sync_to_mastodon(self, params, update_mode):
@ -495,6 +504,14 @@ class Content(Piece):
def __str__(self): def __str__(self):
return f"{self.__class__.__name__}:{self.uuid}@{self.item}" return f"{self.__class__.__name__}:{self.uuid}@{self.item}"
@property
def display_title(self) -> str:
raise NotImplementedError("subclass should override this")
@property
def display_description(self) -> str:
raise NotImplementedError("subclass should override this")
class Meta: class Meta:
abstract = True abstract = True

View file

@ -4,12 +4,10 @@ from typing import Any
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 loguru import logger
from catalog.models import Item from catalog.models import Item
from mastodon.models import get_spoiler_text
from takahe.utils import Takahe from takahe.utils import Takahe
from users.models import APIdentity, User from users.models import APIdentity
from .comment import Comment from .comment import Comment
from .note import Note from .note import Note

View file

@ -73,7 +73,7 @@ def render_rating(score: int | None, star_mode=0) -> str:
solid_stars = score // 2 solid_stars = score // 2
half_star = int(bool(score % 2)) half_star = int(bool(score % 2))
empty_stars = 5 - solid_stars if not half_star else 5 - solid_stars - 1 empty_stars = 5 - solid_stars if not half_star else 5 - solid_stars - 1
if star_mode == 1: if star_mode == 0:
emoji_code = "🌕" * solid_stars + "🌗" * half_star + "🌑" * empty_stars emoji_code = "🌕" * solid_stars + "🌗" * half_star + "🌑" * empty_stars
else: else:
emoji_code = ( emoji_code = (
@ -87,12 +87,10 @@ def render_rating(score: int | None, star_mode=0) -> str:
def render_spoiler_text(text, item): def render_spoiler_text(text, item):
if not text: if text and text.find(">!") != -1:
return None, ""
if text.find(">!") != -1:
spoiler_text = _( spoiler_text = _(
"regarding {item_title}, may contain spoiler or triggering content" "regarding {item_title}, may contain spoiler or triggering content"
).format(item_title=item.display_title) ).format(item_title=item.display_title)
return spoiler_text, text.replace(">!", "").replace("!<", "") return spoiler_text, text.replace(">!", "").replace("!<", "")
else: else:
return None, text return None, text or ""

View file

@ -27,6 +27,14 @@ class Review(Content):
title = models.CharField(max_length=500, blank=False, null=False) title = models.CharField(max_length=500, blank=False, null=False)
body = MarkdownxField() body = MarkdownxField()
@property
def display_title(self):
return self.title
@property
def display_description(self):
return self.plain_content[:155]
@property @property
def html_content(self): def html_content(self):
return render_md(self.body) return render_md(self.body)
@ -89,10 +97,10 @@ class Review(Content):
def to_crosspost_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} " + " ##rating##\n##obj##\n##obj_link_if_plain##"
+ self.get_crosspost_postfix() + self.get_crosspost_postfix()
) )
params = {"content": content} params = {"content": content, "obj": self, "rating": self.rating_grade}
return params return params
def to_post_params(self): def to_post_params(self):
@ -103,9 +111,7 @@ class Review(Content):
) )
+ f'<br><a href="{self.absolute_url}">{self.title}</a>' + f'<br><a href="{self.absolute_url}">{self.title}</a>'
) )
content = ( content = f"{render_rating(self.rating_grade)}\n{self.get_crosspost_postfix()}"
f"{render_rating(self.rating_grade, 1)}\n{self.get_crosspost_postfix()}"
)
return { return {
"prepend_content": prepend_content, "prepend_content": prepend_content,
"content": content, "content": content,

View file

@ -14,7 +14,7 @@ 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 from .renderers import render_post_with_macro, render_rating, render_spoiler_text
if TYPE_CHECKING: if TYPE_CHECKING:
from .comment import Comment from .comment import Comment
@ -368,16 +368,20 @@ class ShelfMember(ListMember):
return _(ShelfManager.get_action_template(self.shelf_type, self.item.category)) return _(ShelfManager.get_action_template(self.shelf_type, self.item.category))
def to_crosspost_params(self): def to_crosspost_params(self):
action = self.get_crosspost_template().format(item=self.item.display_title) action = self.get_crosspost_template().format(item="##obj##")
if self.sibling_comment: if self.sibling_comment:
spoiler, txt = Takahe.get_spoiler_text(self.sibling_comment.text, self.item) spoiler, txt = render_spoiler_text(self.sibling_comment.text, self.item)
else: else:
spoiler, txt = None, "" spoiler, txt = None, ""
stars = ( rating = self.sibling_rating.grade if self.sibling_rating else ""
render_rating(self.sibling_rating.grade, 1) if self.sibling_rating else "" txt = "\n" + txt if txt else ""
) content = f"{action} ##rating## \n##obj_link_if_plain##{txt}{self.get_crosspost_postfix()}"
content = f"{action} {stars} \n{self.item.absolute_url}\n{txt}\n{self.get_crosspost_postfix()}" params = {
params = {"content": content, "spoiler_text": spoiler} "content": content,
"spoiler_text": spoiler,
"obj": self.item,
"rating": rating,
}
return params return params
def to_post_params(self): def to_post_params(self):
@ -386,14 +390,14 @@ class ShelfMember(ListMember):
item=f'<a href="{item_link}">{self.item.display_title}</a>' item=f'<a href="{item_link}">{self.item.display_title}</a>'
) )
if self.sibling_comment: if self.sibling_comment:
spoiler, txt = Takahe.get_spoiler_text(self.sibling_comment.text, self.item) spoiler, txt = render_spoiler_text(self.sibling_comment.text, self.item)
else: else:
spoiler, txt = None, "" spoiler, txt = None, ""
postfix = self.get_crosspost_postfix() postfix = self.get_crosspost_postfix()
# add @user.mastodon.handle so that user can see it on Mastodon ? # add @user.mastodon.handle so that user can see it on Mastodon ?
# if self.visibility and self.owner.user.mastodon: # if self.visibility and self.owner.user.mastodon:
# postfix += f" @{self.owner.user.mastodon.handle}" # 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}" content = f"{render_rating(self.sibling_rating.grade) if self.sibling_rating else ''} \n{txt}\n{postfix}"
return { return {
"prepend_content": action, "prepend_content": action,
"content": content, "content": content,

View file

@ -6,7 +6,6 @@ from .mastodon import (
MastodonAccount, MastodonAccount,
MastodonApplication, MastodonApplication,
detect_server_info, detect_server_info,
get_spoiler_text,
verify_client, verify_client,
) )
from .threads import Threads, ThreadsAccount from .threads import Threads, ThreadsAccount

View file

@ -1,12 +1,11 @@
import re import re
import typing
from functools import cached_property from functools import cached_property
from operator import pos
from atproto import Client, SessionEvent, client_utils from atproto import Client, SessionEvent, client_utils
from atproto_client import models from atproto_client import models
from atproto_identity.did.resolver import DidResolver from atproto_identity.did.resolver import DidResolver
from atproto_identity.handle.resolver import HandleResolver from atproto_identity.handle.resolver import HandleResolver
from django.db.models import base
from django.utils import timezone from django.utils import timezone
from loguru import logger from loguru import logger
@ -14,11 +13,15 @@ from catalog.common import jsondata
from .common import SocialAccount from .common import SocialAccount
if typing.TYPE_CHECKING:
from catalog.common.models import Item
from journal.models.common import Content
class Bluesky: class Bluesky:
_DOMAIN = "-" _DOMAIN = "-"
_RE_HANDLE = re.compile( _RE_HANDLE = re.compile(
r"/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/" r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$"
) )
# for BlueskyAccount # for BlueskyAccount
# uid is did and the only unique identifier # uid is did and the only unique identifier
@ -103,7 +106,7 @@ class BlueskyAccount(SocialAccount):
@property @property
def url(self): def url(self):
return f"{self.base_url}/profile/{self.handle}" return f"https://{self.handle}"
def refresh(self, save=True, did_refresh=True): def refresh(self, save=True, did_refresh=True):
if did_refresh: if did_refresh:
@ -159,7 +162,16 @@ class BlueskyAccount(SocialAccount):
] ]
) )
def post(self, content, reply_to_id=None, **kwargs): def post(
self,
content,
reply_to_id=None,
obj: "Item | Content | None" = None,
rating=None,
**kwargs,
):
from journal.models.renderers import render_rating
reply_to = None reply_to = None
if reply_to_id: if reply_to_id:
posts = self._client.get_posts([reply_to_id]).posts posts = self._client.get_posts([reply_to_id]).posts
@ -168,10 +180,30 @@ class BlueskyAccount(SocialAccount):
reply_to = models.AppBskyFeedPost.ReplyRef( reply_to = models.AppBskyFeedPost.ReplyRef(
parent=root_post_ref, root=root_post_ref parent=root_post_ref, root=root_post_ref
) )
text = client_utils.TextBuilder().text(content) text = (
# todo OpenGraph content.replace("##rating##", render_rating(rating))
# .link("Python SDK", "https://atproto.blue") .replace("##obj_link_if_plain##", "")
post = self._client.send_post(text, reply_to=reply_to) .split("##obj##")
)
richtext = client_utils.TextBuilder()
first = True
for t in text:
if not first and obj:
richtext.link(obj.display_title, obj.absolute_url)
else:
first = False
richtext.text(t)
if obj:
embed = models.AppBskyEmbedExternal.Main(
external=models.AppBskyEmbedExternal.External(
title=obj.display_title,
description=obj.display_description,
uri=obj.absolute_url,
)
)
else:
embed = None
post = self._client.send_post(richtext, reply_to=reply_to, embed=embed)
# return AT uri as id since it's used as so. # return AT uri as id since it's used as so.
return {"cid": post.cid, "id": post.uri} return {"cid": post.cid, "id": post.uri}

View file

@ -27,7 +27,8 @@ from takahe.utils import Takahe
from .common import SocialAccount from .common import SocialAccount
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from journal.models.common import VisibilityType from catalog.common.models import Item
from journal.models.common import Content, VisibilityType
class TootVisibilityEnum(StrEnum): class TootVisibilityEnum(StrEnum):
@ -177,8 +178,6 @@ def post_toot2(
"status": content, "status": content,
"visibility": visibility, "visibility": visibility,
} }
# 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: 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:
@ -263,26 +262,6 @@ def random_string_generator(n):
return "".join(random.choice(s) for i in range(n)) return "".join(random.choice(s) for i in range(n))
def rating_to_emoji(score, star_mode=0):
"""convert score to mastodon star emoji code"""
if score is None or score == "" or score == 0:
return ""
solid_stars = score // 2
half_star = int(bool(score % 2))
empty_stars = 5 - solid_stars if not half_star else 5 - solid_stars - 1
if star_mode == 1:
emoji_code = "🌕" * solid_stars + "🌗" * half_star + "🌑" * empty_stars
else:
emoji_code = (
settings.STAR_SOLID * solid_stars
+ settings.STAR_HALF * half_star
+ settings.STAR_EMPTY * empty_stars
)
emoji_code = emoji_code.replace("::", ": :")
emoji_code = " " + emoji_code + " "
return emoji_code
def verify_account(site, token): def verify_account(site, token):
url = "https://" + get_api_domain(site) + API_VERIFY_ACCOUNT url = "https://" + get_api_domain(site) + API_VERIFY_ACCOUNT
try: try:
@ -426,25 +405,6 @@ def obtain_token(site, code, request):
return data.get("access_token"), data.get("refresh_token", "") return data.get("access_token"), data.get("refresh_token", "")
def get_status_id_by_url(url):
if not url:
return None
r = re.match(
r".+/(\w+)$", url
) # might be re.match(r'.+/([^/]+)$', u) if Pleroma supports edit
return r[1] if r else None
def get_spoiler_text(text, item):
if text.find(">!") != -1:
spoiler_text = _(
"regarding {item_title}, may contain spoiler or triggering content"
).format(item_title=item.display_title)
return spoiler_text, text.replace(">!", "").replace("!<", "")
else:
return None, text
def get_toot_visibility(visibility, user) -> TootVisibilityEnum: def get_toot_visibility(visibility, user) -> TootVisibilityEnum:
if visibility == 2: if visibility == 2:
return TootVisibilityEnum.DIRECT return TootVisibilityEnum.DIRECT
@ -662,16 +622,18 @@ class MastodonAccount(SocialAccount):
return app return app
@functools.cached_property @functools.cached_property
def api_domain(self) -> str: def _api_domain(self) -> str:
app = self.application app = self.application
return app.api_domain if app else self.domain return app.api_domain if app else self.domain
def rating_to_emoji(self, rating_grade: int) -> str: def rating_to_emoji(self, rating_grade: int | None) -> str:
app = self.application from journal.models.renderers import render_rating
return rating_to_emoji(rating_grade, app.star_mode if app else 0)
app = self.application # TODO fix star mode data flip in app
return render_rating(rating_grade, (0 if app.star_mode else 1) if app else 0)
def _get(self, url: str): def _get(self, url: str):
url = url if url.startswith("https://") else f"https://{self.api_domain}{url}" url = url if url.startswith("https://") else f"https://{self._api_domain}{url}"
headers = { headers = {
"User-Agent": settings.NEODB_USER_AGENT, "User-Agent": settings.NEODB_USER_AGENT,
"Authorization": f"Bearer {self.access_token}", "Authorization": f"Bearer {self.access_token}",
@ -679,7 +641,7 @@ class MastodonAccount(SocialAccount):
return get(url, headers=headers) return get(url, headers=headers)
def _post(self, url: str, data, files=None): def _post(self, url: str, data, files=None):
url = url if url.startswith("https://") else f"https://{self.api_domain}{url}" url = url if url.startswith("https://") else f"https://{self._api_domain}{url}"
return post( return post(
url, url,
data=data, data=data,
@ -692,7 +654,7 @@ class MastodonAccount(SocialAccount):
) )
def _delete(self, url: str, data, files=None): def _delete(self, url: str, data, files=None):
url = url if url.startswith("https://") else f"https://{self.api_domain}{url}" url = url if url.startswith("https://") else f"https://{self._api_domain}{url}"
return delete( return delete(
url, url,
headers={ headers={
@ -702,7 +664,7 @@ class MastodonAccount(SocialAccount):
) )
def _put(self, url: str, data, files=None): def _put(self, url: str, data, files=None):
url = url if url.startswith("https://") else f"https://{self.api_domain}{url}" url = url if url.startswith("https://") else f"https://{self._api_domain}{url}"
return put( return put(
url, url,
data=data, data=data,
@ -814,19 +776,19 @@ class MastodonAccount(SocialAccount):
) )
def boost(self, post_url: str): def boost(self, post_url: str):
boost_toot(self.api_domain, self.access_token, post_url) 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_post(self, post_id: str): def delete_post(self, post_id: str):
delete_toot(self.api_domain, self.access_token, post_id) delete_toot(self._api_domain, self.access_token, post_id)
def delete_post_later(self, post_id: str): 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_id delete_toot, self._api_domain, self.access_token, post_id
) )
def post( def post(
@ -838,12 +800,21 @@ class MastodonAccount(SocialAccount):
sensitive: bool = False, sensitive: bool = False,
spoiler_text: str | None = None, spoiler_text: str | None = None,
attachments: list = [], attachments: list = [],
obj: "Item | Content | None" = None,
rating: int | None = None,
) -> dict: ) -> dict:
from journal.models.renderers import render_rating
v = get_toot_visibility(visibility, self.user) v = get_toot_visibility(visibility, self.user)
text = (
content.replace("##rating##", self.rating_to_emoji(rating))
.replace("##obj_link_if_plain##", obj.absolute_url + "\n" if obj else "")
.replace("##obj##", obj.display_title if obj else "")
)
response = post_toot2( response = post_toot2(
self.api_domain, self._api_domain,
self.access_token, self.access_token,
content, text,
v, v,
update_id, update_id,
reply_to_id, reply_to_id,

View file

@ -1,4 +1,5 @@
import functools import functools
import typing
from datetime import timedelta from datetime import timedelta
from urllib.parse import quote from urllib.parse import quote
@ -14,6 +15,10 @@ from catalog.common import jsondata
from .common import SocialAccount from .common import SocialAccount
if typing.TYPE_CHECKING:
from catalog.common.models import Item
from journal.models.common import Content, VisibilityType
get = functools.partial( get = functools.partial(
requests.get, requests.get,
timeout=settings.THREADS_TIMEOUT, timeout=settings.THREADS_TIMEOUT,
@ -239,11 +244,21 @@ class ThreadsAccount(SocialAccount):
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, reply_to_id=None, **kwargs): def post(
self,
content: str,
visibility: "VisibilityType",
reply_to_id=None,
obj: "Item | Content | None" = None,
rating=None,
**kwargs,
):
from journal.models.renderers import render_rating
text = ( text = (
content.replace(settings.STAR_SOLID + " ", "🌕") content.replace("##rating##", render_rating(rating))
.replace(settings.STAR_HALF + " ", "🌗") .replace("##obj_link_if_plain##", obj.absolute_url + "\n" if obj else "")
.replace(settings.STAR_EMPTY + " ", "🌑") .replace("##obj##", obj.display_title if obj else "")
) )
media_id = Threads.post_single(self.access_token, self.uid, text, reply_to_id) media_id = Threads.post_single(self.access_token, self.uid, text, reply_to_id)
if not media_id: if not media_id:

View file

@ -56,7 +56,7 @@ dependencies = [
"validators", "validators",
"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.49",
"pyright>=1.1.370", "pyright>=1.1.370",
] ]

View file

@ -20,7 +20,7 @@ asgiref==3.8.1
# via django # via django
# via django-cors-headers # via django-cors-headers
# via django-stubs # via django-stubs
atproto==0.0.48 atproto==0.0.49
attrs==23.2.0 attrs==23.2.0
# via aiohttp # via aiohttp
babel==2.15.0 babel==2.15.0

View file

@ -19,7 +19,7 @@ anyio==4.4.0
asgiref==3.8.1 asgiref==3.8.1
# via django # via django
# via django-cors-headers # via django-cors-headers
atproto==0.0.48 atproto==0.0.49
attrs==23.2.0 attrs==23.2.0
# via aiohttp # via aiohttp
beautifulsoup4==4.12.3 beautifulsoup4==4.12.3

View file

@ -543,16 +543,6 @@ class Takahe:
# TimelineEvent.objects.filter(subject_post__in=[post.pk]).delete() # TimelineEvent.objects.filter(subject_post__in=[post.pk]).delete()
PostInteraction.objects.filter(post__in=post_pks).update(state="undone") PostInteraction.objects.filter(post__in=post_pks).update(state="undone")
@staticmethod
def get_spoiler_text(text, item):
if text and text.find(">!") != -1:
spoiler_text = _(
"regarding {item_title}, may contain spoiler or triggering content"
).format(item_title=item.display_title)
return spoiler_text, text.replace(">!", "").replace("!<", "")
else:
return None, text or ""
@staticmethod @staticmethod
def visibility_n2t(visibility: int, post_public_mode: int) -> Visibilities: def visibility_n2t(visibility: int, post_public_mode: int) -> Visibilities:
if visibility == 1: if visibility == 1:

View file

@ -108,14 +108,14 @@
required="" required=""
id="mastodon_repost_mode_0" id="mastodon_repost_mode_0"
{% if request.user.preference.mastodon_repost_mode == 0 %}checked{% endif %}> {% if request.user.preference.mastodon_repost_mode == 0 %}checked{% endif %}>
<label for="mastodon_repost_mode_0">{% trans "boost" %}</label> <label for="mastodon_repost_mode_0">{% trans "Boost if possible" %}</label>
<input type="radio" <input type="radio"
name="mastodon_repost_mode" name="mastodon_repost_mode"
value="1" value="1"
required="" required=""
id="mastodon_repost_mode_1" id="mastodon_repost_mode_1"
{% if request.user.preference.mastodon_repost_mode == 1 %}checked{% endif %}> {% if request.user.preference.mastodon_repost_mode == 1 %}checked{% endif %}>
<label for="mastodon_repost_mode_1">{% trans "create a new post" %}</label> <label for="mastodon_repost_mode_1">{% trans "Create a new post" %}</label>
<em data-tooltip="{% trans "this method is less optimal, may generate duplicated posts and miss reactions." %}"><i class="fa fa-question-circle"></i></em> <em data-tooltip="{% trans "this method is less optimal, may generate duplicated posts and miss reactions." %}"><i class="fa fa-question-circle"></i></em>
</fieldset> </fieldset>
{% endif %} {% endif %}