1
0
Fork 0

Merge branch 'main' into misc/add_signatures_to_requests_for_masto_compat

This commit is contained in:
Mouse Reeve 2022-03-01 10:20:39 -08:00 committed by GitHub
commit d2dab0f2db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
376 changed files with 28405 additions and 10510 deletions

View file

@ -29,7 +29,7 @@ from .import_job import ImportJob, ImportItem
from .site import SiteSettings, SiteInvite
from .site import PasswordReset, InviteRequest
from .announcement import Announcement
from .antispam import EmailBlocklist, IPBlocklist
from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task
from .notification import Notification

View file

@ -2,10 +2,21 @@
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from .base_model import BookWyrmModel
DisplayTypes = [
("white-ter", _("None")),
("primary-light", _("Primary")),
("success-light", _("Success")),
("link-light", _("Link")),
("warning-light", _("Warning")),
("danger-light", _("Danger")),
]
class Announcement(BookWyrmModel):
"""The admin has something to say"""
@ -16,6 +27,13 @@ class Announcement(BookWyrmModel):
start_date = models.DateTimeField(blank=True, null=True)
end_date = models.DateTimeField(blank=True, null=True)
active = models.BooleanField(default=True)
display_type = models.CharField(
max_length=20,
blank=False,
null=False,
choices=DisplayTypes,
default="white-ter",
)
@classmethod
def active_announcements(cls):

View file

@ -1,6 +1,13 @@
""" Lets try NOT to sell viagra """
from django.db import models
from functools import reduce
import operator
from django.apps import apps
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from bookwyrm.tasks import app
from .user import User
@ -33,3 +40,107 @@ class IPBlocklist(models.Model):
"""default sorting"""
ordering = ("-created_date",)
class AutoMod(models.Model):
"""rules to automatically flag suspicious activity"""
string_match = models.CharField(max_length=200, unique=True)
flag_users = models.BooleanField(default=True)
flag_statuses = models.BooleanField(default=True)
created_by = models.ForeignKey("User", on_delete=models.PROTECT)
@app.task(queue="low_priority")
def automod_task():
"""Create reports"""
if not AutoMod.objects.exists():
return
reporter = AutoMod.objects.first().created_by
reports = automod_users(reporter) + automod_statuses(reporter)
if reports:
admins = User.objects.filter(
models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
| models.Q(is_superuser=True)
).all()
notification_model = apps.get_model(
"bookwyrm", "Notification", require_ready=True
)
for admin in admins:
notification_model.objects.bulk_create(
[
notification_model(
user=admin,
related_report=r,
notification_type="REPORT",
)
for r in reports
]
)
def automod_users(reporter):
"""check users for moderation flags"""
user_rules = AutoMod.objects.filter(flag_users=True).values_list(
"string_match", flat=True
)
if not user_rules:
return []
filters = []
for field in ["username", "summary", "name"]:
filters += [{f"{field}__icontains": r} for r in user_rules]
users = User.objects.filter(
reduce(operator.or_, (Q(**f) for f in filters)),
is_active=True,
local=True,
report__isnull=True, # don't flag users that already have reports
).distinct()
report_model = apps.get_model("bookwyrm", "Report", require_ready=True)
return report_model.objects.bulk_create(
[
report_model(
reporter=reporter,
note=_("Automatically generated report"),
user=u,
)
for u in users
]
)
def automod_statuses(reporter):
"""check statues for moderation flags"""
status_rules = AutoMod.objects.filter(flag_statuses=True).values_list(
"string_match", flat=True
)
if not status_rules:
return []
filters = []
for field in ["content", "content_warning", "quotation__quote", "review__name"]:
filters += [{f"{field}__icontains": r} for r in status_rules]
status_model = apps.get_model("bookwyrm", "Status", require_ready=True)
statuses = status_model.objects.filter(
reduce(operator.or_, (Q(**f) for f in filters)),
deleted=False,
local=True,
report__isnull=True, # don't flag statuses that already have reports
).distinct()
report_model = apps.get_model("bookwyrm", "Report", require_ready=True)
return report_model.objects.bulk_create(
[
report_model(
reporter=reporter,
note=_("Automatically generated report"),
user=s.user,
status=s,
)
for s in statuses
]
)

View file

@ -21,9 +21,6 @@ class Author(BookDataModel):
isni = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
viaf_id = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
gutenberg_id = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)

View file

@ -46,6 +46,15 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
bnf_id = fields.CharField( # Bibliothèque nationale de France
max_length=255, blank=True, null=True, deduplication_field=True
)
viaf = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
wikidata = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
asin = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
search_vector = SearchVectorField(null=True)
last_edited_by = fields.ForeignKey(
@ -271,9 +280,6 @@ class Edition(Book):
oclc_number = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
asin = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
pages = fields.IntegerField(blank=True, null=True)
physical_format = fields.CharField(
max_length=255, choices=FormatChoices, null=True, blank=True
@ -342,6 +348,11 @@ class Edition(Book):
# set rank
self.edition_rank = self.get_rank()
# clear author cache
if self.id:
for author_id in self.authors.values_list("id", flat=True):
cache.delete(f"author-books-{author_id}")
return super().save(*args, **kwargs)
@classmethod

View file

@ -1,6 +1,5 @@
""" activitypub-aware django model fields """
from dataclasses import MISSING
import imghdr
import re
from uuid import uuid4
from urllib.parse import urljoin
@ -9,7 +8,6 @@ import dateutil.parser
from dateutil.parser import ParserError
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.db import models
from django.forms import ClearableFileInput, ImageField as DjangoImageField
from django.utils import timezone
@ -443,12 +441,10 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
except ValidationError:
return None
response = get_image(url)
if not response:
image_content, extension = get_image(url)
if not image_content:
return None
image_content = ContentFile(response.content)
extension = imghdr.what(None, image_content.read()) or ""
image_name = f"{uuid4()}.{extension}"
return [image_name, image_content]

View file

@ -47,13 +47,23 @@ class Link(ActivitypubMixin, BookWyrmModel):
return super().save(*args, **kwargs)
AvailabilityChoices = [
("free", _("Free")),
("purchase", _("Purchasable")),
("loan", _("Available for loan")),
]
class FileLink(Link):
"""a link to a file"""
book = models.ForeignKey(
"Book", on_delete=models.CASCADE, related_name="file_links", null=True
)
filetype = fields.CharField(max_length=5, activitypub_field="mediaType")
filetype = fields.CharField(max_length=50, activitypub_field="mediaType")
availability = fields.CharField(
max_length=100, choices=AvailabilityChoices, default="free"
)
StatusChoices = [

View file

@ -2,6 +2,7 @@
import uuid
from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.db import models
from django.db.models import Q
from django.utils import timezone
@ -74,6 +75,22 @@ class List(OrderedCollectionMixin, BookWyrmModel):
return
super().raise_not_editable(viewer)
def raise_not_submittable(self, viewer):
"""can the user submit a book to the list?"""
# if you can't view the list you can't submit to it
self.raise_visible_to_user(viewer)
# all good if you're the owner or the list is open
if self.user == viewer or self.curation in ["open", "curated"]:
return
if self.curation == "group":
is_group_member = GroupMember.objects.filter(
group=self.group, user=viewer
).exists()
if is_group_member:
return
raise PermissionDenied()
@classmethod
def followers_filter(cls, queryset, viewer):
"""Override filter for "followers" privacy level to allow non-following
@ -125,7 +142,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor"
)
notes = fields.TextField(blank=True, null=True)
notes = fields.HtmlField(blank=True, null=True, max_length=300)
approved = models.BooleanField(default=True)
order = fields.IntegerField()
endorsement = models.ManyToManyField("User", related_name="endorsers")

View file

@ -1,5 +1,6 @@
""" flagged for moderation """
from django.db import models
from bookwyrm.settings import DOMAIN
from .base_model import BookWyrmModel
@ -11,10 +12,18 @@ class Report(BookWyrmModel):
)
note = models.TextField(null=True, blank=True)
user = models.ForeignKey("User", on_delete=models.PROTECT)
statuses = models.ManyToManyField("Status", blank=True)
status = models.ForeignKey(
"Status",
null=True,
blank=True,
on_delete=models.PROTECT,
)
links = models.ManyToManyField("Link", blank=True)
resolved = models.BooleanField(default=False)
def get_remote_id(self):
return f"https://{DOMAIN}/settings/reports/{self.id}"
class Meta:
"""set order by default"""

View file

@ -1,5 +1,6 @@
""" puttin' books on shelves """
import re
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils import timezone
@ -94,8 +95,15 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
def save(self, *args, **kwargs):
if not self.user:
self.user = self.shelf.user
if self.id and self.user.local:
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
if self.id and self.user.local:
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
super().delete(*args, **kwargs)
class Meta:
"""an opinionated constraint!
you can't put a book on shelf twice"""

View file

@ -1,6 +1,7 @@
""" the particulars for this instance of BookWyrm """
import datetime
from urllib.parse import urljoin
import uuid
from django.db import models, IntegrityError
from django.dispatch import receiver
@ -24,6 +25,10 @@ class SiteSettings(models.Model):
instance_description = models.TextField(default="This instance has no description.")
instance_short_description = models.CharField(max_length=255, blank=True, null=True)
# admin setup options
install_mode = models.BooleanField(default=False)
admin_code = models.CharField(max_length=50, default=uuid.uuid4)
# about page
registration_closed_text = models.TextField(
default="We aren't taking new users at this time. You can find an open "
@ -38,7 +43,7 @@ class SiteSettings(models.Model):
privacy_policy = models.TextField(default="Add a privacy policy here.")
# registration
allow_registration = models.BooleanField(default=True)
allow_registration = models.BooleanField(default=False)
allow_invite_requests = models.BooleanField(default=True)
require_confirm_email = models.BooleanField(default=True)
@ -90,6 +95,14 @@ class SiteSettings(models.Model):
return get_absolute_url(uploaded)
return urljoin(STATIC_FULL_URL, default_path)
def save(self, *args, **kwargs):
"""if require_confirm_email is disabled, make sure no users are pending"""
if not self.require_confirm_email:
User.objects.filter(is_active=False, deactivation_reason="pending").update(
is_active=True, deactivation_reason=None
)
super().save(*args, **kwargs)
class SiteInvite(models.Model):
"""gives someone access to create an account on the instance"""

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re
from django.apps import apps
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@ -226,7 +227,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@classmethod
def privacy_filter(cls, viewer, privacy_levels=None):
queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels)
return queryset.filter(deleted=False)
return queryset.filter(deleted=False, user__is_active=True)
@classmethod
def direct_filter(cls, queryset, viewer):
@ -373,6 +374,12 @@ class Review(BookStatus):
activity_serializer = activitypub.Review
pure_type = "Article"
def save(self, *args, **kwargs):
"""clear rating caches"""
if self.book.parent_work:
cache.delete(f"book-rating-{self.book.parent_work.id}-*")
super().save(*args, **kwargs)
class ReviewRating(Review):
"""a subtype of review that only contains a rating"""

View file

@ -136,6 +136,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
updated_date = models.DateTimeField(auto_now=True)
last_active_date = models.DateTimeField(default=timezone.now)
manually_approves_followers = fields.BooleanField(default=False)
hide_follows = fields.BooleanField(default=False)
# options to turn features on and off
show_goal = models.BooleanField(default=True)
@ -478,10 +479,13 @@ def set_remote_server(user_id):
get_remote_reviews.delay(user.outbox)
def get_or_create_remote_server(domain):
def get_or_create_remote_server(domain, refresh=False):
"""get info on a remote server"""
server = FederatedServer()
try:
return FederatedServer.objects.get(server_name=domain)
server = FederatedServer.objects.get(server_name=domain)
if not refresh:
return server
except FederatedServer.DoesNotExist:
pass
@ -496,13 +500,15 @@ def get_or_create_remote_server(domain):
application_type = data.get("software", {}).get("name")
application_version = data.get("software", {}).get("version")
except ConnectorException:
if server.id:
return server
application_type = application_version = None
server = FederatedServer.objects.create(
server_name=domain,
application_type=application_type,
application_version=application_version,
)
server.server_name = domain
server.application_type = application_type
server.application_version = application_version
server.save()
return server