bluesky post links and embeds
This commit is contained in:
parent
4e97cb7083
commit
b727397cd1
16 changed files with 152 additions and 124 deletions
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue