diff --git a/README.md b/README.md index 6487137c2..10f77c042 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,6 @@ Social reading and reviewing, decentralized with ActivityPub ## Joining BookWyrm BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://github.com/mouse-reeve/bookwyrm/blob/main/instances.md) list. -You can request an invite to https://bookwyrm.social by [email](mailto:mousereeve@riseup.net), [Mastodon direct message](https://friend.camp/@tripofmice), or [Twitter direct message](https://twitter.com/tripofmice). - - ## Contributing There are many ways you can contribute to this project, regardless of your level of technical expertise. diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index c7536876d..7069286d7 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -1,27 +1,48 @@ """ send emails """ -from django.core.mail import send_mail +from django.core.mail import EmailMultiAlternatives +from django.template.loader import get_template from bookwyrm import models from bookwyrm.tasks import app +def invite_email(invite_request): + """ send out an invite code """ + site = models.SiteSettings.objects.get() + data = { + "site_name": site.name, + "invite_link": invite_request.invite.link, + } + send_email.delay(invite_request.email, "invite", data) + + def password_reset_email(reset_code): """ generate a password reset email """ - site = models.SiteSettings.get() - send_email.delay( - reset_code.user.email, - "Reset your password on %s" % site.name, - "Your password reset link: %s" % reset_code.link, - ) + site = models.SiteSettings.objects.get() + data = { + "site_name": site.name, + "reset_link": reset_code.link, + } + send_email.delay(reset_code.user.email, "password_reset", data) @app.task -def send_email(recipient, subject, message): +def send_email(recipient, message_name, data): """ use a task to send the email """ - send_mail( - subject, - message, - None, # sender will be the config default - [recipient], - fail_silently=False, + subject = ( + get_template("email/{}/subject.html".format(message_name)).render(data).strip() ) + html_content = ( + get_template("email/{}/html_content.html".format(message_name)) + .render(data) + .strip() + ) + text_content = ( + get_template("email/{}/text_content.html".format(message_name)) + .render(data) + .strip() + ) + + email = EmailMultiAlternatives(subject, text_content, None, [recipient]) + email.attach_alternative(html_content, "text/html") + email.send() diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index d723ebdbf..d330211c1 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -3,6 +3,7 @@ import datetime from collections import defaultdict from django import forms +from django.core.exceptions import ValidationError from django.forms import ModelForm, PasswordInput, widgets from django.forms.widgets import Textarea from django.utils import timezone @@ -202,6 +203,19 @@ class ExpiryWidget(widgets.Select): return timezone.now() + interval +class InviteRequestForm(CustomForm): + def clean(self): + """ make sure the email isn't in use by a registered user """ + cleaned_data = super().clean() + email = cleaned_data.get("email") + if email and models.User.objects.filter(email=email).exists(): + self.add_error("email", _("A user with this email already exists.")) + + class Meta: + model = models.InviteRequest + fields = ["email"] + + class CreateInviteForm(CustomForm): class Meta: model = models.SiteInvite diff --git a/bookwyrm/migrations/0056_auto_20210321_0303.py b/bookwyrm/migrations/0056_auto_20210321_0303.py new file mode 100644 index 000000000..aa475e033 --- /dev/null +++ b/bookwyrm/migrations/0056_auto_20210321_0303.py @@ -0,0 +1,59 @@ +# Generated by Django 3.1.6 on 2021-03-21 03:03 + +import bookwyrm.models.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0055_auto_20210321_0101"), + ] + + operations = [ + migrations.AddField( + model_name="sitesettings", + name="allow_invite_requests", + field=models.BooleanField(default=True), + ), + migrations.CreateModel( + name="InviteRequest", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ( + "remote_id", + bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), + ), + ("email", models.EmailField(max_length=255, unique=True)), + ("invite_sent", models.BooleanField(default=False)), + ("ignored", models.BooleanField(default=False)), + ( + "invite", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="bookwyrm.siteinvite", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 326a673e1..35e32c2cf 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -26,7 +26,7 @@ from .federated_server import FederatedServer from .import_job import ImportJob, ImportItem -from .site import SiteSettings, SiteInvite, PasswordReset +from .site import SiteSettings, SiteInvite, PasswordReset, InviteRequest cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) activity_models = { diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 7fde6781e..2c8d25539 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -3,10 +3,11 @@ import base64 import datetime from Crypto import Random -from django.db import models +from django.db import models, IntegrityError from django.utils import timezone from bookwyrm.settings import DOMAIN +from .base_model import BookWyrmModel from .user import User @@ -24,6 +25,7 @@ class SiteSettings(models.Model): code_of_conduct = models.TextField(default="Add a code of conduct here.") privacy_policy = models.TextField(default="Add a privacy policy here.") allow_registration = models.BooleanField(default=True) + allow_invite_requests = models.BooleanField(default=True) logo = models.ImageField(upload_to="logos/", null=True, blank=True) logo_small = models.ImageField(upload_to="logos/", null=True, blank=True) favicon = models.ImageField(upload_to="logos/", null=True, blank=True) @@ -69,6 +71,23 @@ class SiteInvite(models.Model): return "https://{}/invite/{}".format(DOMAIN, self.code) +class InviteRequest(BookWyrmModel): + """ prospective users can request an invite """ + + email = models.EmailField(max_length=255, unique=True) + invite = models.ForeignKey( + SiteInvite, on_delete=models.SET_NULL, null=True, blank=True + ) + invite_sent = models.BooleanField(default=False) + ignored = models.BooleanField(default=False) + + def save(self, *args, **kwargs): + """ don't create a request for a registered email """ + if User.objects.filter(email=self.email).exists(): + raise IntegrityError() + super().save(*args, **kwargs) + + def get_passowrd_reset_expiry(): """ give people a limited time to use the link """ now = timezone.now() diff --git a/bookwyrm/templates/discover/landing_layout.html b/bookwyrm/templates/discover/landing_layout.html index 5cfa1fd39..8e507531e 100644 --- a/bookwyrm/templates/discover/landing_layout.html +++ b/bookwyrm/templates/discover/landing_layout.html @@ -45,9 +45,33 @@
+ {% else %} +{{ site.registration_closed_text | safe}}
+ + {% if site.allow_invite_requests %} + {% if request_received %} ++ {% trans "Thank you! Your request has been received." %} +
+ {% else %} +