Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,36 @@ name: Test
on:
push:
paths:
- '**.py'
- 'requirements-dev.lock'
- '.github/workflows/test.yml'
- "**.py"
- "**.yml"
- "uv.lock"
branches:
- main
pull_request:
paths:
- '**.py'
- 'requirements-dev.lock'
- "**.py"
- "uv.lock"

jobs:
test:
name: test py${{ matrix.python-version }} with pg${{ matrix.postgres-version }}
runs-on: ubuntu-latest
services:
redis:
image: redis
ports:
- 6379:6379
timeout-minutes: 8
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
postgres-version: [15, 16, 17]
python-version: ["3.13", "3.14"]
postgres-version: [14, 18]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
run: uv python install ${{ matrix.python-version }}
- name: Set up Postgres ${{ matrix.postgres-version }}
uses: ankane/setup-postgres@v1
with:
Expand All @@ -37,12 +41,12 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get install -y libwebp-dev libjpeg-dev
python -m pip install -r requirements-dev.lock
uv sync --frozen
- name: Run pytest
env:
TAKAHE_DATABASE_SERVER: "postgres://localhost/takahe"
run: |
python -m pytest
uv run pytest
- name: Run pre-commit
run: |
pre-commit run -a --show-diff-on-failure
uv run pre-commit run -a --show-diff-on-failure
31 changes: 22 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.0
hooks:
# Run the linter.
- id: ruff-check
args: [--fix]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-case-conflict
- id: check-merge-conflict
- id: check-yaml
- id: end-of-file-fixer
- id: file-contents-sorter
args: ["--ignore-case", "--unique"]
files: ^(\.gitignore|\.dockerignore)$
- id: mixed-line-ending
args: ["--fix=lf"]
- id: pretty-format-json
args: ["--autofix", "--indent=4", "--no-ensure-ascii"]
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.9
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ This fork has a number of changes/additions:
* Trending hashtags and statuses
* Fetch follow/post counts for non-local identities
* Relay support
* Conversations API
* Report API
* Quote posts (FEP-044f)
* Group actor support
* ActivityPub replies collection sync
* Preview cards
73 changes: 73 additions & 0 deletions activities/management/commands/backfill_conversations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from django.core.management.base import BaseCommand

from activities.models import Post
from activities.models.conversation import Conversation, ConversationMembership


class Command(BaseCommand):
help = (
"Backfill conversations from existing direct-visibility posts. Safe to rerun."
)

def add_arguments(self, parser):
parser.add_argument(
"--batch-size",
type=int,
default=500,
help="Number of posts to process per batch",
)

def handle(self, batch_size: int, *args, **options):
direct_posts = (
Post.objects.filter(visibility=Post.Visibilities.mentioned)
.prefetch_related("mentions")
.order_by("id")
)
total = direct_posts.count()
self.stdout.write(f"Found {total} direct-visibility posts to process")

created_convs = 0
updated_posts = 0
created_memberships = 0

for post in direct_posts.iterator(chunk_size=batch_size):
participant_ids = set(post.mentions.values_list("pk", flat=True))
participant_ids.add(post.author_id)
if len(participant_ids) < 2:
continue

h = Conversation.compute_participant_hash(participant_ids)
conversation, created = Conversation.objects.get_or_create(
participant_hash=h,
)
if created:
conversation.participants.set(participant_ids)
created_convs += 1

# Only update post if not already assigned
if post.conversation_id != conversation.pk:
Post.objects.filter(pk=post.pk).update(conversation=conversation)
updated_posts += 1

# Update last_post if this is the newest (posts are ordered by id)
if conversation.last_post_id is None or post.pk > conversation.last_post_id:
conversation.last_post = post
conversation.save(update_fields=["last_post", "updated"])

# Create memberships (idempotent via get_or_create)
for pid in participant_ids:
_, created = ConversationMembership.objects.get_or_create(
identity_id=pid,
conversation=conversation,
defaults={"unread": False},
)
if created:
created_memberships += 1

self.stdout.write(
self.style.SUCCESS(
f"Done: {created_convs} conversations created, "
f"{updated_posts} posts updated, "
f"{created_memberships} memberships created"
)
)
72 changes: 72 additions & 0 deletions activities/management/commands/fetch_preview_cards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import datetime

from core.html import FediverseHtmlParser
from django.core.management.base import BaseCommand
from django.db import transaction
from django.utils import timezone

from activities.models import Post
from activities.models.post import _attach_preview_card
from activities.models.preview_card import PreviewCard, PreviewCardStates


class Command(BaseCommand):
help = (
"Backfill preview cards for posts published in the last N days. Safe to rerun."
)

def add_arguments(self, parser):
parser.add_argument(
"--days",
type=int,
required=True,
help="Process posts published within this many days",
)

def handle(self, days: int, *args, **options):
since = timezone.now() - datetime.timedelta(days=days)
posts = (
Post.objects.filter(published__gte=since)
.select_related("preview_card")
.order_by("published")
)
total = posts.count()
self.stdout.write(
f"Scanning {total} posts published in the last {days} days..."
)

created = requeued = skipped = 0

for post in posts.iterator():
with transaction.atomic():
card = post.preview_card

if card is not None:
if card.state == PreviewCardStates.fetched:
skipped += 1
continue
if card.state == PreviewCardStates.needs_fetch:
skipped += 1
continue
if card.state == PreviewCardStates.fetch_failed:
# Reset to needs_fetch for explicit retry
card.transition_perform(PreviewCardStates.needs_fetch)
PreviewCard.objects.filter(pk=card.pk).update(
last_referenced_at=timezone.now()
)
requeued += 1
continue

# No card yet — extract URL and create
matches = FediverseHtmlParser.URL_REGEX.findall(post.content or "")
url = matches[0].lstrip("(") if matches else None
if not url or not url.startswith(("http://", "https://")):
skipped += 1
continue

_attach_preview_card(post.pk, post.content)
created += 1

self.stdout.write(
f"Done. Created: {created}, Re-queued: {requeued}, Skipped: {skipped}"
)
Loading
Loading