Follow/unfollow behavior

Fixes #23
This commit is contained in:
Mouse Reeve 2020-02-18 22:44:13 -08:00
parent 14d300162d
commit 95bfb61cf3
10 changed files with 138 additions and 102 deletions

View file

@ -3,5 +3,5 @@ from .actor import get_actor
from .collection import get_outbox, get_outbox_page, get_add, get_remove, \
get_following, get_followers
from .create import get_create
from .follow import get_follow_request, get_accept
from .follow import get_follow_request, get_unfollow, get_accept
from .status import get_review, get_review_article, get_status, get_replies

View file

@ -16,6 +16,21 @@ def get_follow_request(user, to_follow):
'object': to_follow.actor,
}
def get_unfollow(relationship):
''' undo that precious bond of friendship '''
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': '%s/undo' % relationship.absolute_id,
'type': 'Undo',
'actor': relationship.user_subject.actor,
'object': {
'id': relationship.relationship_id,
'type': 'Follow',
'actor': relationship.user_subject.actor,
'object': relationship.user_object.actor,
}
}
def get_accept(user, request_activity):
''' accept a follow request '''

View file

@ -37,6 +37,9 @@ def shared_inbox(request):
if activity['type'] == 'Follow':
response = handle_incoming_follow(activity)
elif activity['type'] == 'Undo':
response = handle_incoming_undo(activity)
elif activity['type'] == 'Create':
response = handle_incoming_create(activity)
@ -183,10 +186,32 @@ def handle_incoming_follow(activity):
# figure out who they are
user = get_or_create_remote_user(activity['actor'])
# TODO: allow users to manually approve requests
models.UserRelationship.objects.create(
user_subject=to_follow,
user_object=user,
status='follow_request',
relationship_id=activity['id']
)
outgoing.handle_outgoing_accept(user, to_follow, activity)
return HttpResponse()
def handle_incoming_undo(activity):
''' unfollow a local user '''
obj = activity['object']
if not obj['type'] == 'Follow':
#idk how to undo other things
return HttpResponseNotFound()
try:
requester = get_or_create_remote_user(obj['actor'])
to_unfollow = models.User.objects.get(actor=obj['object'])
except models.User.DoesNotExist:
return HttpResponseNotFound()
to_unfollow.followers.remove(requester)
return HttpResponse()
def handle_incoming_follow_accept(activity):
''' hurray, someone remote accepted a follow request '''
# figure out who they want to follow

View file

@ -1,4 +1,4 @@
# Generated by Django 3.0.3 on 2020-02-17 02:39
# Generated by Django 3.0.3 on 2020-02-19 06:43
from django.conf import settings
import django.contrib.auth.models
@ -41,11 +41,10 @@ class Migration(migrations.Migration):
('outbox', models.CharField(max_length=255, unique=True)),
('summary', models.TextField(blank=True, null=True)),
('local', models.BooleanField(default=True)),
('fedireads_user', models.BooleanField(default=True)),
('localname', models.CharField(max_length=255, null=True, unique=True)),
('name', models.CharField(blank=True, max_length=100, null=True)),
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
('created_date', models.DateTimeField(auto_now_add=True)),
('updated_date', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'user',
@ -60,66 +59,96 @@ class Migration(migrations.Migration):
name='Author',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(blank=True, null=True)),
('created_date', models.DateTimeField(auto_now_add=True)),
('openlibrary_key', models.CharField(max_length=255)),
('data', fedireads.utils.fields.JSONField()),
('added_date', models.DateTimeField(auto_now_add=True)),
('updated_date', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Book',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(blank=True, null=True)),
('created_date', models.DateTimeField(auto_now_add=True)),
('openlibrary_key', models.CharField(max_length=255, unique=True)),
('data', fedireads.utils.fields.JSONField()),
('cover', models.ImageField(blank=True, null=True, upload_to='covers/')),
('added_date', models.DateTimeField(auto_now_add=True)),
('updated_date', models.DateTimeField(auto_now=True)),
('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('authors', models.ManyToManyField(to='fedireads.Author')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FederatedServer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(blank=True, null=True)),
('created_date', models.DateTimeField(auto_now_add=True)),
('server_name', models.CharField(max_length=255, unique=True)),
('status', models.CharField(default='federated', max_length=255)),
('application_type', models.CharField(max_length=255, null=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Shelf',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(blank=True, null=True)),
('created_date', models.DateTimeField(auto_now_add=True)),
('name', models.CharField(max_length=100)),
('identifier', models.CharField(max_length=100)),
('editable', models.BooleanField(default=True)),
('created_date', models.DateTimeField(auto_now_add=True)),
('updated_date', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='Status',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(blank=True, null=True)),
('created_date', models.DateTimeField(auto_now_add=True)),
('status_type', models.CharField(default='Note', max_length=255)),
('activity_type', models.CharField(default='Note', max_length=255)),
('local', models.BooleanField(default=True)),
('privacy', models.CharField(default='public', max_length=255)),
('sensitive', models.BooleanField(default=False)),
('content', models.TextField(blank=True, null=True)),
('created_date', models.DateTimeField(auto_now_add=True)),
('mention_books', models.ManyToManyField(related_name='mention_book', to='fedireads.Book')),
('mention_users', models.ManyToManyField(related_name='mention_user', to=settings.AUTH_USER_MODEL)),
('reply_parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fedireads.Status')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='UserRelationship',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(blank=True, null=True)),
('created_date', models.DateTimeField(auto_now_add=True)),
('status', models.CharField(default='follows', max_length=100, null=True)),
('relationship_id', models.CharField(max_length=100)),
('user_object', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_object', to=settings.AUTH_USER_MODEL)),
('user_subject', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_subject', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ShelfBook',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(blank=True, null=True)),
('created_date', models.DateTimeField(auto_now_add=True)),
('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
@ -152,7 +181,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='user',
name='followers',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
field=models.ManyToManyField(through='fedireads.UserRelationship', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='user',
@ -176,6 +205,9 @@ class Migration(migrations.Migration):
('rating', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])),
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
],
options={
'abstract': False,
},
bases=('fedireads.status',),
),
]

View file

@ -1,80 +0,0 @@
# Generated by Django 3.0.3 on 2020-02-19 01:18
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='author',
old_name='added_date',
new_name='created_date',
),
migrations.RenameField(
model_name='book',
old_name='added_date',
new_name='created_date',
),
migrations.RemoveField(
model_name='author',
name='updated_date',
),
migrations.RemoveField(
model_name='book',
name='updated_date',
),
migrations.RemoveField(
model_name='shelf',
name='updated_date',
),
migrations.RemoveField(
model_name='user',
name='created_date',
),
migrations.RemoveField(
model_name='user',
name='updated_date',
),
migrations.AddField(
model_name='author',
name='content',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='book',
name='content',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='federatedserver',
name='content',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='federatedserver',
name='created_date',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='shelf',
name='content',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='shelfbook',
name='content',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='user',
name='fedireads_user',
field=models.BooleanField(default=True),
),
]

View file

@ -1,5 +1,5 @@
''' bring all the models into the app namespace '''
from .book import Shelf, ShelfBook, Book, Author
from .user import User, FederatedServer
from .user import User, UserRelationship, FederatedServer
from .activity import Status, Review

View file

@ -34,7 +34,12 @@ class User(AbstractUser):
# name is your display name, which you can change at will
name = models.CharField(max_length=100, blank=True, null=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
followers = models.ManyToManyField('self', symmetrical=False)
followers = models.ManyToManyField(
'self',
symmetrical=False,
through='UserRelationship',
through_fields=('user_subject', 'user_object')
)
@property
def absolute_id(self):
@ -43,6 +48,29 @@ class User(AbstractUser):
return 'https://%s/%s/%s' % (DOMAIN, model_name, self.localname)
class UserRelationship(FedireadsModel):
''' many-to-many through table for followers '''
user_subject = models.ForeignKey(
'User',
on_delete=models.PROTECT,
related_name='user_subject'
)
user_object = models.ForeignKey(
'User',
on_delete=models.PROTECT,
related_name='user_object'
)
# follow or follow_request for pending TODO: blocking?
status = models.CharField(max_length=100, default='follows', null=True)
relationship_id = models.CharField(max_length=100)
@property
def absolute_id(self):
''' use shelf identifier as absolute id '''
base_path = self.user_subject.absolute_id
return '%s#%s/%d' % (base_path, self.status, self.id)
class FederatedServer(FedireadsModel):
''' store which server's we federate with '''
server_name = models.CharField(max_length=255, unique=True)

View file

@ -77,9 +77,26 @@ def handle_outgoing_follow(user, to_follow):
raise(error['error'])
def handle_outgoing_unfollow(user, to_unfollow):
''' someone local wants to follow someone '''
relationship = models.UserRelationship.objects.get(
user_object=user,
user_subject=to_unfollow
)
activity = activitypub.get_unfollow(relationship)
errors = broadcast(user, activity, [to_unfollow.inbox])
to_unfollow.followers.remove(user)
for error in errors:
raise(error['error'])
def handle_outgoing_accept(user, to_follow, request_activity):
''' send an acceptance message to a follow request '''
to_follow.followers.add(user)
relationship = models.UserRelationship.objects.get(
relationship_id=request_activity['id']
)
relationship.status = 'follow'
relationship.save()
activity = activitypub.get_accept(to_follow, request_activity)
recipient = get_recipients(to_follow, 'direct', direct_recipients=[user])
broadcast(to_follow, activity, recipient)

View file

@ -28,7 +28,7 @@
{% if user.is_authenticated %}
<form name="logout" action="/logout/" method="post">
{% csrf_token %}
Welcome, {% include 'snippets/username.html' %}
Welcome, {% include 'snippets/username.html' with user=request.user %}
<input type="submit" value="Log out"></input>
</form>
{% else %}

View file

@ -278,11 +278,10 @@ def follow(request):
@login_required
def unfollow(request):
''' unfollow a user '''
# TODO: this is not an implementation!!
followed = request.POST.get('user')
followed = models.User.objects.get(id=followed)
followed.followers.remove(request.user)
return redirect('/user/%s' % followed.username)
user = request.user
to_unfollow = models.User.objects.get(id=request.POST.get('user'))
outgoing.handle_outgoing_unfollow(user, to_unfollow)
return redirect('/user/%s' % to_unfollow.username)
@login_required