2023-08-10 11:27:31 -04:00
|
|
|
import re
|
|
|
|
import uuid
|
2023-08-26 01:27:18 +00:00
|
|
|
from functools import cached_property
|
|
|
|
from typing import TYPE_CHECKING
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
from django.conf import settings
|
|
|
|
from django.db import connection, models
|
2024-02-04 18:41:34 -05:00
|
|
|
from django.db.models import Avg, CharField, Count, Q
|
2023-08-10 11:27:31 -04:00
|
|
|
from django.utils import timezone
|
|
|
|
from django.utils.baseconv import base62
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from polymorphic.models import PolymorphicModel
|
|
|
|
|
2023-07-20 21:59:49 -04:00
|
|
|
from catalog.common.models import AvailableItemCategory, Item, ItemCategory
|
2023-08-26 01:27:18 +00:00
|
|
|
from catalog.models import item_categories, item_content_types
|
2023-07-20 21:59:49 -04:00
|
|
|
from takahe.utils import Takahe
|
|
|
|
from users.models import APIdentity, User
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
from .mixins import UserOwnedObjectMixin
|
|
|
|
|
2023-08-26 01:27:18 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from takahe.models import Post
|
2023-08-10 11:27:31 -04:00
|
|
|
|
2024-05-27 15:44:12 -04:00
|
|
|
from .like import Like
|
|
|
|
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
class VisibilityType(models.IntegerChoices):
|
2024-05-29 09:49:13 -04:00
|
|
|
Public = 0, _("Public") # type:ignore[reportCallIssue]
|
|
|
|
Follower_Only = 1, _("Followers Only") # type:ignore[reportCallIssue]
|
|
|
|
Private = 2, _("Mentioned Only") # type:ignore[reportCallIssue]
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
|
2023-07-20 21:59:49 -04:00
|
|
|
def q_owned_piece_visible_to_user(viewing_user: User, owner: APIdentity):
|
2023-12-31 13:02:37 -05:00
|
|
|
if not viewing_user or not viewing_user.is_authenticated:
|
|
|
|
if owner.anonymous_viewable:
|
|
|
|
return Q(owner=owner, visibility=0)
|
|
|
|
else:
|
|
|
|
return Q(pk__in=[])
|
2023-07-20 21:59:49 -04:00
|
|
|
viewer = viewing_user.identity
|
2023-08-10 11:27:31 -04:00
|
|
|
if viewer == owner:
|
2023-12-02 15:24:07 -05:00
|
|
|
return Q(owner=owner)
|
2023-08-10 11:27:31 -04:00
|
|
|
# elif viewer.is_blocked_by(owner):
|
|
|
|
# return Q(pk__in=[])
|
2023-07-20 21:59:49 -04:00
|
|
|
elif viewer.is_following(owner):
|
|
|
|
return Q(owner=owner, visibility__in=[0, 1])
|
2023-08-10 11:27:31 -04:00
|
|
|
else:
|
2023-07-20 21:59:49 -04:00
|
|
|
return Q(owner=owner, visibility=0)
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
|
2023-07-20 21:59:49 -04:00
|
|
|
def max_visiblity_to_user(viewing_user: User, owner: APIdentity):
|
2023-12-31 13:02:37 -05:00
|
|
|
if not viewing_user or not viewing_user.is_authenticated:
|
2023-07-20 21:59:49 -04:00
|
|
|
return 0
|
|
|
|
viewer = viewing_user.identity
|
2023-08-10 11:27:31 -04:00
|
|
|
if viewer == owner:
|
|
|
|
return 2
|
2023-07-20 21:59:49 -04:00
|
|
|
elif viewer.is_following(owner):
|
2023-08-10 11:27:31 -04:00
|
|
|
return 1
|
|
|
|
else:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
2023-12-31 13:02:37 -05:00
|
|
|
def q_piece_visible_to_user(viewing_user: User):
|
|
|
|
if not viewing_user or not viewing_user.is_authenticated:
|
2023-12-29 02:11:36 -05:00
|
|
|
return Q(visibility=0, owner__anonymous_viewable=True)
|
2023-12-31 13:02:37 -05:00
|
|
|
viewer = viewing_user.identity
|
2023-08-10 11:27:31 -04:00
|
|
|
return (
|
2023-07-20 21:59:49 -04:00
|
|
|
Q(visibility=0)
|
2023-12-31 13:02:37 -05:00
|
|
|
| Q(owner_id__in=viewer.following, visibility=1)
|
|
|
|
| Q(owner_id=viewer.pk)
|
|
|
|
) & ~Q(owner_id__in=viewer.ignoring)
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
|
2023-12-31 13:02:37 -05:00
|
|
|
def q_piece_in_home_feed_of_user(viewing_user: User):
|
|
|
|
viewer = viewing_user.identity
|
2023-12-31 15:49:26 -05:00
|
|
|
return Q(owner_id__in=viewer.following, visibility__lt=2) | Q(owner_id=viewer.pk)
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
|
2024-05-26 22:57:49 -04:00
|
|
|
def q_item_in_category(item_category: ItemCategory):
|
2023-08-10 11:27:31 -04:00
|
|
|
classes = item_categories()[item_category]
|
|
|
|
# q = Q(item__instance_of=classes[0])
|
|
|
|
# for cls in classes[1:]:
|
|
|
|
# q = q | Q(instance_of=cls)
|
|
|
|
# return q
|
|
|
|
ct = item_content_types()
|
|
|
|
contenttype_ids = [ct[cls] for cls in classes]
|
|
|
|
return Q(item__polymorphic_ctype__in=contenttype_ids)
|
|
|
|
|
|
|
|
|
|
|
|
# class ImportStatus(Enum):
|
|
|
|
# QUEUED = 0
|
|
|
|
# PROCESSING = 1
|
|
|
|
# FINISHED = 2
|
|
|
|
|
|
|
|
|
|
|
|
# class ImportSession(models.Model):
|
2023-07-20 21:59:49 -04:00
|
|
|
# owner = models.ForeignKey(APIdentity, on_delete=models.CASCADE)
|
2023-08-10 11:27:31 -04:00
|
|
|
# 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):
|
2024-05-27 15:44:12 -04:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
likes: models.QuerySet["Like"]
|
2023-08-10 11:27:31 -04:00
|
|
|
url_path = "p" # subclass must specify this
|
|
|
|
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
2023-07-20 21:59:49 -04:00
|
|
|
local = models.BooleanField(default=True)
|
2023-08-26 01:27:18 +00:00
|
|
|
posts = models.ManyToManyField(
|
|
|
|
"takahe.Post", related_name="pieces", through="PiecePost"
|
|
|
|
)
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
@property
|
|
|
|
def uuid(self):
|
|
|
|
return base62.encode(self.uid.int)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def url(self):
|
|
|
|
return f"/{self.url_path}/{self.uuid}" if self.url_path else None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def absolute_url(self):
|
|
|
|
return (settings.SITE_INFO["site_url"] + self.url) if self.url_path else None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def api_url(self):
|
|
|
|
return f"/api/{self.url}" if self.url_path else None
|
|
|
|
|
2023-07-20 21:59:49 -04:00
|
|
|
@property
|
|
|
|
def shared_link(self):
|
2023-08-26 01:27:18 +00:00
|
|
|
return Takahe.get_post_url(self.latest_post.pk) if self.latest_post else None
|
2023-07-20 21:59:49 -04:00
|
|
|
|
2023-08-10 11:27:31 -04:00
|
|
|
@property
|
|
|
|
def like_count(self):
|
2023-07-20 21:59:49 -04:00
|
|
|
return (
|
2023-08-26 01:27:18 +00:00
|
|
|
Takahe.get_post_stats(self.latest_post.pk).get("likes", 0)
|
|
|
|
if self.latest_post
|
|
|
|
else 0
|
2023-07-20 21:59:49 -04:00
|
|
|
)
|
|
|
|
|
2023-08-29 06:00:02 +00:00
|
|
|
def is_liked_by(self, identity):
|
|
|
|
return self.latest_post and Takahe.post_liked_by(
|
|
|
|
self.latest_post.pk, identity.pk
|
|
|
|
)
|
2023-08-10 11:27:31 -04:00
|
|
|
|
2023-08-15 15:46:11 -04:00
|
|
|
@property
|
|
|
|
def reply_count(self):
|
|
|
|
return (
|
2023-08-26 01:27:18 +00:00
|
|
|
Takahe.get_post_stats(self.latest_post.pk).get("replies", 0)
|
|
|
|
if self.latest_post
|
|
|
|
else 0
|
2023-08-15 15:46:11 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
def get_replies(self, viewing_identity):
|
2023-08-26 01:27:18 +00:00
|
|
|
return Takahe.get_replies_for_posts(
|
|
|
|
self.all_post_ids, viewing_identity.pk if viewing_identity else None
|
2023-08-15 15:46:11 -04:00
|
|
|
)
|
|
|
|
|
2023-08-10 11:27:31 -04:00
|
|
|
@classmethod
|
|
|
|
def get_by_url(cls, url_or_b62):
|
|
|
|
b62 = url_or_b62.strip().split("/")[-1]
|
|
|
|
if len(b62) not in [21, 22]:
|
|
|
|
r = re.search(r"[A-Za-z0-9]{21,22}", url_or_b62)
|
|
|
|
if r:
|
|
|
|
b62 = r[0]
|
|
|
|
try:
|
|
|
|
obj = cls.objects.get(uid=uuid.UUID(int=base62.decode(b62)))
|
2024-04-06 00:13:50 -04:00
|
|
|
except Exception:
|
2023-08-10 11:27:31 -04:00
|
|
|
obj = None
|
|
|
|
return obj
|
|
|
|
|
2024-04-19 20:24:34 -04:00
|
|
|
@classmethod
|
|
|
|
def get_by_post_id(cls, post_id: int):
|
|
|
|
pp = PiecePost.objects.filter(post_id=post_id).first()
|
|
|
|
return pp.piece if pp else None
|
|
|
|
|
2023-07-20 21:59:49 -04:00
|
|
|
@classmethod
|
|
|
|
def update_by_ap_object(cls, owner, item, obj, post_id, visibility):
|
2024-04-06 00:13:50 -04:00
|
|
|
raise NotImplementedError("subclass must implement this")
|
2023-07-20 21:59:49 -04:00
|
|
|
|
|
|
|
@property
|
|
|
|
def ap_object(self):
|
2024-04-06 00:13:50 -04:00
|
|
|
raise NotImplementedError("subclass must implement this")
|
2023-07-20 21:59:49 -04:00
|
|
|
|
2023-08-26 01:27:18 +00:00
|
|
|
def link_post_id(self, post_id: int):
|
|
|
|
PiecePost.objects.get_or_create(piece=self, post_id=post_id)
|
|
|
|
|
2023-11-20 01:59:26 -05:00
|
|
|
def clear_post_ids(self):
|
|
|
|
PiecePost.objects.filter(piece=self).delete()
|
|
|
|
|
2023-08-26 01:27:18 +00:00
|
|
|
@cached_property
|
2023-11-20 19:11:02 -05:00
|
|
|
def latest_post_id(self):
|
|
|
|
# post id is ordered by their created time
|
2023-08-26 01:27:18 +00:00
|
|
|
pp = PiecePost.objects.filter(piece=self).order_by("-post_id").first()
|
2023-11-20 19:11:02 -05:00
|
|
|
return pp.post_id if pp else None
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def latest_post(self):
|
|
|
|
pk = self.latest_post_id
|
|
|
|
return Takahe.get_post(pk) if pk else None
|
2023-08-26 01:27:18 +00:00
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def all_post_ids(self):
|
|
|
|
post_ids = list(
|
|
|
|
PiecePost.objects.filter(piece=self).values_list("post_id", flat=True)
|
|
|
|
)
|
|
|
|
return post_ids
|
|
|
|
|
|
|
|
|
|
|
|
class PiecePost(models.Model):
|
2023-11-20 19:11:02 -05:00
|
|
|
post_id: int
|
2023-08-26 01:27:18 +00:00
|
|
|
piece = models.ForeignKey(Piece, on_delete=models.CASCADE)
|
|
|
|
post = models.ForeignKey(
|
|
|
|
"takahe.Post", db_constraint=False, db_index=True, on_delete=models.CASCADE
|
|
|
|
)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
constraints = [
|
|
|
|
models.UniqueConstraint(fields=["piece", "post"], name="unique_piece_post"),
|
|
|
|
]
|
|
|
|
|
2023-08-10 11:27:31 -04:00
|
|
|
|
2023-12-27 09:23:41 -05:00
|
|
|
class PieceInteraction(models.Model):
|
2023-12-27 16:23:46 -05:00
|
|
|
target = models.ForeignKey(
|
|
|
|
Piece, on_delete=models.CASCADE, related_name="interactions"
|
|
|
|
)
|
|
|
|
target_type = models.CharField(max_length=50)
|
2023-12-27 09:23:41 -05:00
|
|
|
interaction_type = models.CharField(max_length=50)
|
2023-12-27 16:23:46 -05:00
|
|
|
identity = models.ForeignKey(
|
|
|
|
APIdentity, on_delete=models.CASCADE, related_name="interactions"
|
|
|
|
)
|
2023-12-27 09:23:41 -05:00
|
|
|
created_time = models.DateTimeField(default=timezone.now)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
constraints = [
|
|
|
|
models.UniqueConstraint(
|
2023-12-27 16:23:46 -05:00
|
|
|
fields=["identity", "interaction_type", "target"],
|
2023-12-27 09:23:41 -05:00
|
|
|
name="unique_interaction",
|
|
|
|
),
|
|
|
|
]
|
|
|
|
indexes = [
|
|
|
|
models.Index(fields=["identity", "interaction_type", "created_time"]),
|
2023-12-27 16:23:46 -05:00
|
|
|
models.Index(fields=["target", "interaction_type", "created_time"]),
|
2023-12-27 09:23:41 -05:00
|
|
|
]
|
|
|
|
|
|
|
|
|
2023-08-10 11:27:31 -04:00
|
|
|
class Content(Piece):
|
2023-07-20 21:59:49 -04:00
|
|
|
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
2023-08-10 11:27:31 -04:00
|
|
|
visibility = models.PositiveSmallIntegerField(
|
|
|
|
default=0
|
2024-05-27 15:44:12 -04:00
|
|
|
) # 0: Public / 1: Follower only / 2: Self only # type:ignore
|
2023-08-10 11:27:31 -04:00
|
|
|
created_time = models.DateTimeField(default=timezone.now)
|
2023-11-20 19:31:31 -05:00
|
|
|
edited_time = models.DateTimeField(auto_now=True)
|
2023-08-10 11:27:31 -04:00
|
|
|
metadata = models.JSONField(default=dict)
|
|
|
|
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
2023-07-20 21:59:49 -04:00
|
|
|
remote_id = models.CharField(max_length=200, null=True, default=None)
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
def __str__(self):
|
2023-12-24 18:04:55 -05:00
|
|
|
return f"{self.__class__.__name__}:{self.uuid}@{self.item}"
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
2024-02-04 18:41:34 -05:00
|
|
|
|
|
|
|
|
|
|
|
class Debris(Content):
|
|
|
|
class_name = CharField(max_length=50)
|
|
|
|
|
|
|
|
@classmethod
|
2024-05-27 15:44:12 -04:00
|
|
|
def create_from_piece(cls, c: Content):
|
2024-02-04 18:41:34 -05:00
|
|
|
return cls.objects.create(
|
|
|
|
class_name=c.__class__.__name__,
|
|
|
|
owner=c.owner,
|
|
|
|
visibility=c.visibility,
|
|
|
|
created_time=c.created_time,
|
|
|
|
metadata=c.ap_object,
|
|
|
|
item=c.item,
|
|
|
|
remote_id=c.remote_id if hasattr(c, "remote_id") else None,
|
|
|
|
)
|