Skip to content

Commit e306d98

Browse files
authored
Merge pull request #87 from dtinit/refactor-follow-activities-and-test-data-previously
Refactor follow activities and test data previously
2 parents e0d6dc8 + 6fd6e7f commit e306d98

File tree

5 files changed

+177
-30
lines changed

5 files changed

+177
-30
lines changed

testbed/core/factories.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ class Meta:
3232
model = Actor
3333

3434
user = factory.SubFactory(UserFactory)
35-
username = factory.SelfAttribute("user.username")
36-
full_name = factory.Faker("name")
37-
previously = factory.Dict({})
35+
username = factory.SelfAttribute('user.username')
36+
full_name = factory.Faker('name')
37+
previously = factory.List([])
3838

3939

4040
class NoteFactory(DjangoModelFactory):

testbed/core/management/commands/seed.py

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.contrib.auth import get_user_model
55
from django.conf import settings
66
from django.utils import timezone
7-
from testbed.core.models import LikeActivity
7+
from testbed.core.models import LikeActivity, FollowActivity
88
from testbed.core.factories import (
99
ActorFactory,
1010
CreateActivityFactory,
@@ -57,6 +57,39 @@ def create_remote_like(self, actor):
5757
visibility="public",
5858
)
5959

60+
def create_remote_follow(self, actor):
61+
server, usernames = random.choice(REMOTE_SERVERS)
62+
username = random.choice(usernames)
63+
64+
return FollowActivity.objects.create(
65+
actor=actor,
66+
target_actor=None,
67+
target_actor_url=f"https://{server}/users/{username}",
68+
target_actor_data={
69+
"type": "Person",
70+
"preferredUsername": username,
71+
"name": username,
72+
"url": f"https://{server}/users/{username}",
73+
},
74+
visibility="public"
75+
)
76+
77+
# Create actor with previous server history
78+
def create_actor_with_history(self):
79+
actor = ActorFactory.create()
80+
81+
# Add 1-2 previous servers
82+
num_previous = random.randint(1, 2)
83+
84+
for _ in range(num_previous):
85+
server, usernames = random.choice(REMOTE_SERVERS)
86+
username = random.choice(usernames)
87+
move_date = timezone.now() - timedelta(days=random.randint(30, 365))
88+
89+
actor.record_move(server, username, move_date)
90+
91+
return actor
92+
6093
def handle(self, *args, **kwargs):
6194
try:
6295
# Check if seeding is allowed in current environment
@@ -104,15 +137,22 @@ def handle(self, *args, **kwargs):
104137
self.style.WARNING("Skipping admin user creation.")
105138
)
106139
else:
107-
self.stdout.write(self.style.SUCCESS("Admin user already exists."))
108-
109-
# Create multiple actors (Outbox will be created automatically)
110-
self.stdout.write("Creating actors...")
111-
actors = ActorFactory.create_batch(10)
112-
113-
# Track different types of likes
140+
self.stdout.write(self.style.SUCCESS('Admin user already exists.'))
141+
142+
# Create multiple actors - mix of regular and those with history
143+
# (Outbox will be created automatically)
144+
self.stdout.write('Creating actors...')
145+
regular_actors = ActorFactory.create_batch(7) # 7 regular actors
146+
history_actors = [self.create_actor_with_history() for _ in range(3)] # 3 actors with history
147+
actors = regular_actors + history_actors
148+
149+
# Track different types of activities
114150
local_like_count = 0
115151
remote_like_count = 0
152+
local_follow_count = 0
153+
remote_follow_count = 0
154+
moved_actors_count = len(history_actors) # Counts the number of actors with history
155+
regular_actors_count = len(regular_actors) # Counts the number of regular actors
116156

117157
# Create notes and various activities
118158
self.stdout.write("Creating notes and activities...")
@@ -142,30 +182,40 @@ def handle(self, *args, **kwargs):
142182
actor.portability_outbox.first().add_activity(remote_like)
143183
remote_like_count += 1
144184

145-
# Create some follow relationships
146-
for _ in range(2): # Each actor follows 2 other actors
185+
# Create local follows
186+
for _ in range(1): # Each actor follows 1 local actor
187+
147188
target = random.choice([a for a in actors if a != actor])
148189
follow_activity = FollowActivityFactory(
149190
actor=actor, target_actor=target, visibility="public"
150191
)
151192
actor.portability_outbox.first().add_activity(follow_activity)
193+
local_follow_count += 1
194+
195+
# Create remote follows
196+
for _ in range(1): # Each actor follows 1 remote actor
197+
remote_follow = self.create_remote_follow(actor)
198+
actor.portability_outbox.first().add_activity(remote_follow)
199+
remote_follow_count += 1
152200

153201
# Count all activities
154202
total_actors = len(actors)
155203
total_notes = len(actors) * 3
156-
total_creates = total_notes + total_actors # Notes + Actor creates
157-
total_follows = len(actors) * 2
204+
total_creates = total_notes + total_actors # Notes + Actor creates
158205

159206
self.stdout.write(
160207
self.style.SUCCESS(
161-
f"Successfully created:\n"
162-
f"- {total_actors} actors\n"
163-
f"- {total_notes} notes\n"
164-
f"- {total_creates} Create activities ({total_actors} for actors, {total_notes} for notes)\n"
165-
f"- {local_like_count} Local Like activities\n"
166-
f"- {remote_like_count} Remote Like activities\n"
167-
f"- {total_follows} Follow activities\n\n"
168-
f"Federation seeding created with servers: f{','.join(server for server, _ in REMOTE_SERVERS)}"
208+
f'Successfully created:\n'
209+
f'- {total_actors} actors\n'
210+
f'- {moved_actors_count} actors with history\n'
211+
f'- {regular_actors_count} regular actors\n'
212+
f'- {total_notes} notes\n'
213+
f'- {total_creates} Create activities ({total_actors} for actors, {total_notes} for notes)\n'
214+
f'- {local_like_count} Local Like activities\n'
215+
f'- {remote_like_count} Remote Like activities\n'
216+
f'- {local_follow_count} Local Follow activities\n'
217+
f'- {remote_follow_count} Remote Follow activities\n\n'
218+
f'Federation seeding created with servers: {", ".join(server for server, _ in REMOTE_SERVERS)}'
169219
)
170220
)
171221

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 5.1.3 on 2025-03-19 16:33
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('core', '0003_likeactivity_object_data_likeactivity_object_url_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='followactivity',
16+
name='target_actor_data',
17+
field=models.JSONField(blank=True, help_text='Metadata of the followed actor', null=True),
18+
),
19+
migrations.AddField(
20+
model_name='followactivity',
21+
name='target_actor_url',
22+
field=models.URLField(blank=True, help_text='URL of the followed actor in the fediverse', null=True),
23+
),
24+
migrations.AlterField(
25+
model_name='followactivity',
26+
name='target_actor',
27+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='follow_activities_received', to='core.actor'),
28+
),
29+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.3 on 2025-03-19 22:16
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('core', '0004_followactivity_target_actor_data_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='actor',
15+
name='previously',
16+
field=models.JSONField(blank=True, default=list, null=True),
17+
),
18+
]

testbed/core/models.py

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.db import models
33
from django.contrib.auth.models import User
44
from django.core.exceptions import ValidationError
5+
from datetime import timezone
56

67
logger = logging.getLogger(__name__)
78

@@ -22,11 +23,28 @@ class Actor(models.Model):
2223
full_name = models.CharField(max_length=100)
2324
created_at = models.DateTimeField(auto_now_add=True)
2425
updated_at = models.DateTimeField(auto_now=True)
25-
previously = models.JSONField(default=dict, null=True, blank=True)
26+
previously = models.JSONField(default=list, null=True, blank=True)
2627

2728
def __str__(self):
2829
return self.username
30+
31+
# Record a previous location of this account
32+
def record_move(self, previous_server, previous_username, move_date=None):
33+
print(f"Previously type: {type(self.previously)}") # Debug line
34+
print(f"Previously value: {self.previously}") # Debug line
35+
36+
if self.previously is None:
37+
self.previously = []
38+
39+
move_record = {
40+
'type': 'Move',
41+
'object': f"https://{previous_server}/users/{previous_username}",
42+
'published': (move_date or timezone.now()).isoformat()
43+
}
2944

45+
self.previously.append(move_record)
46+
self.save()
47+
3048
def save(self, *args, **kwargs):
3149
is_new = self._state.adding
3250
super().save(*args, **kwargs)
@@ -60,7 +78,7 @@ def get_json_ld(self):
6078
"id": f"https://example.com/users/{self.username}",
6179
"preferredUsername": self.username,
6280
"name": self.username,
63-
"previously": self.previously,
81+
"previously": self.previously or [], # Ensure it's always a list
6482
}
6583

6684

@@ -171,23 +189,55 @@ def get_json_ld(self):
171189

172190
class FollowActivity(Activity):
173191
target_actor = models.ForeignKey(
174-
Actor, on_delete=models.CASCADE, related_name="follow_activities_received"
192+
Actor,
193+
on_delete=models.CASCADE,
194+
related_name="follow_activities_received",
195+
null=True,
196+
blank=True
197+
)
198+
target_actor_url = models.URLField(
199+
max_length=200,
200+
help_text="URL of the followed actor in the fediverse",
201+
null=True,
202+
blank=True
203+
)
204+
target_actor_data = models.JSONField(
205+
help_text="Metadata of the followed actor",
206+
null=True,
207+
blank=True,
175208
)
176209

177-
def __str__(self):
178-
return f"Follow by {self.actor.username}: {self.target_actor.username}"
210+
def clean(self):
211+
super().clean()
212+
if not self.target_actor and not (self.target_actor_url and self.target_actor_data):
213+
raise ValidationError("Either local target_actor or remote actor data (URL and metadata) must be provided")
179214

215+
def __str__(self):
216+
if self.target_actor:
217+
return f'Follow by {self.actor.username}: {self.target_actor.username}'
218+
username = self.target_actor_data.get('preferredUsername', '')
219+
return f'Follow by {self.actor.username}: {username} (remote)'
220+
180221
def get_json_ld(self):
181-
return {
222+
base = {
182223
"@context": "https://www.w3.org/ns/activitystreams",
183224
"type": "Follow",
184225
"id": f"https://example.com/activities/{self.id}",
185226
"actor": f"https://example.com/users/{self.actor.username}",
186-
"object": self.target_actor.get_json_ld(),
187227
"published": self.timestamp.isoformat(),
188228
"visibility": self.visibility,
189229
}
190230

231+
if self.target_actor:
232+
base["object"] = self.target_actor.get_json_ld()
233+
else:
234+
base["object"] = {
235+
"@context": "https://www.w3.org/ns/activitystreams",
236+
**self.target_actor_data,
237+
"id": self.target_actor_url
238+
}
239+
240+
return base
191241

192242
class Note(models.Model):
193243
actor = models.ForeignKey(Actor, on_delete=models.CASCADE, related_name="notes")

0 commit comments

Comments
 (0)