Merge branch 'main' into form-perms
This commit is contained in:
commit
b0236b95bd
88 changed files with 4213 additions and 2669 deletions
|
@ -4,6 +4,7 @@ from .admin.announcements import Announcements, Announcement
|
|||
from .admin.announcements import EditAnnouncement, delete_announcement
|
||||
from .admin.automod import AutoMod, automod_delete, run_automod
|
||||
from .admin.automod import schedule_automod_task, unschedule_automod_task
|
||||
from .admin.celery_status import CeleryStatus
|
||||
from .admin.dashboard import Dashboard
|
||||
from .admin.federation import Federation, FederatedServer
|
||||
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
||||
|
|
56
bookwyrm/views/admin/celery_status.py
Normal file
56
bookwyrm/views/admin/celery_status.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
""" celery status """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
import redis
|
||||
|
||||
from celerywyrm import settings
|
||||
from bookwyrm.tasks import app as celery
|
||||
|
||||
r = redis.Redis(
|
||||
host=settings.REDIS_BROKER_HOST,
|
||||
port=settings.REDIS_BROKER_PORT,
|
||||
password=settings.REDIS_BROKER_PASSWORD,
|
||||
db=settings.REDIS_BROKER_DB_INDEX,
|
||||
)
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class CeleryStatus(View):
|
||||
"""Are your tasks running? Well you'd better go catch them"""
|
||||
|
||||
def get(self, request):
|
||||
"""See workers and active tasks"""
|
||||
errors = []
|
||||
try:
|
||||
inspect = celery.control.inspect()
|
||||
stats = inspect.stats()
|
||||
active_tasks = inspect.active()
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
stats = active_tasks = None
|
||||
errors.append(err)
|
||||
|
||||
try:
|
||||
queues = {
|
||||
"low_priority": r.llen("low_priority"),
|
||||
"medium_priority": r.llen("medium_priority"),
|
||||
"high_priority": r.llen("high_priority"),
|
||||
}
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
queues = None
|
||||
errors.append(err)
|
||||
|
||||
data = {
|
||||
"stats": stats,
|
||||
"active_tasks": active_tasks,
|
||||
"queues": queues,
|
||||
"errors": errors,
|
||||
}
|
||||
return TemplateResponse(request, "settings/celery.html", data)
|
|
@ -59,7 +59,7 @@ def is_bookwyrm_request(request):
|
|||
return True
|
||||
|
||||
|
||||
def handle_remote_webfinger(query):
|
||||
def handle_remote_webfinger(query, unknown_only=False):
|
||||
"""webfingerin' other servers"""
|
||||
user = None
|
||||
|
||||
|
@ -75,6 +75,11 @@ def handle_remote_webfinger(query):
|
|||
|
||||
try:
|
||||
user = models.User.objects.get(username__iexact=query)
|
||||
|
||||
if unknown_only:
|
||||
# In this case, we only want to know about previously undiscovered users
|
||||
# So the fact that we found a match in the database means no results
|
||||
return None
|
||||
except models.User.DoesNotExist:
|
||||
url = f"https://{domain}/.well-known/webfinger?resource=acct:{query}"
|
||||
try:
|
||||
|
|
|
@ -47,6 +47,7 @@ class ImportStatus(View):
|
|||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"show_progress": True,
|
||||
"item_count": item_count,
|
||||
"complete_count": item_count - pending_item_count,
|
||||
"percent": math.floor( # pylint: disable=c-extension-no-member
|
||||
|
|
|
@ -18,14 +18,17 @@ class Isbn(View):
|
|||
|
||||
if is_api_request(request):
|
||||
return JsonResponse(
|
||||
[book_search.format_search_result(r) for r in book_results], safe=False
|
||||
[book_search.format_search_result(r) for r in book_results[:10]],
|
||||
safe=False,
|
||||
)
|
||||
|
||||
paginated = Paginator(book_results, PAGE_LENGTH).get_page(
|
||||
request.GET.get("page")
|
||||
)
|
||||
paginated = Paginator(book_results, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"results": [{"results": paginated}],
|
||||
"results": page,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"query": isbn,
|
||||
"type": "book",
|
||||
}
|
||||
|
|
|
@ -23,22 +23,14 @@ class Search(View):
|
|||
|
||||
def get(self, request):
|
||||
"""that search bar up top"""
|
||||
query = request.GET.get("q")
|
||||
# check if query is isbn
|
||||
query = isbn_check(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
search_type = request.GET.get("type")
|
||||
search_remote = (
|
||||
request.GET.get("remote", False) and request.user.is_authenticated
|
||||
)
|
||||
|
||||
if is_api_request(request):
|
||||
# only return local book results via json so we don't cascade
|
||||
book_results = search(query, min_confidence=min_confidence)
|
||||
return JsonResponse(
|
||||
[format_search_result(r) for r in book_results], safe=False
|
||||
)
|
||||
return api_book_search(request)
|
||||
|
||||
query = request.GET.get("q")
|
||||
if not query:
|
||||
return TemplateResponse(request, "search/book.html")
|
||||
|
||||
search_type = request.GET.get("type")
|
||||
if query and not search_type:
|
||||
search_type = "user" if "@" in query else "book"
|
||||
|
||||
|
@ -50,49 +42,67 @@ class Search(View):
|
|||
if not search_type in endpoints:
|
||||
search_type = "book"
|
||||
|
||||
data = {
|
||||
"query": query or "",
|
||||
"type": search_type,
|
||||
"remote": search_remote,
|
||||
}
|
||||
if query:
|
||||
results, search_remote = endpoints[search_type](
|
||||
query, request.user, min_confidence, search_remote
|
||||
)
|
||||
if results:
|
||||
paginated = Paginator(results, PAGE_LENGTH).get_page(
|
||||
request.GET.get("page")
|
||||
)
|
||||
data["results"] = paginated
|
||||
data["remote"] = search_remote
|
||||
|
||||
return TemplateResponse(request, f"search/{search_type}.html", data)
|
||||
return endpoints[search_type](request)
|
||||
|
||||
|
||||
def book_search(query, user, min_confidence, search_remote=False):
|
||||
def api_book_search(request):
|
||||
"""Return books via API response"""
|
||||
query = request.GET.get("q")
|
||||
query = isbn_check(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
# only return local book results via json so we don't cascade
|
||||
book_results = search(query, min_confidence=min_confidence)
|
||||
return JsonResponse(
|
||||
[format_search_result(r) for r in book_results[:10]], safe=False
|
||||
)
|
||||
|
||||
|
||||
def book_search(request):
|
||||
"""the real business is elsewhere"""
|
||||
query = request.GET.get("q")
|
||||
# check if query is isbn
|
||||
query = isbn_check(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
search_remote = request.GET.get("remote", False) and request.user.is_authenticated
|
||||
|
||||
# try a local-only search
|
||||
results = [{"results": search(query, min_confidence=min_confidence)}]
|
||||
if not user.is_authenticated or (results[0]["results"] and not search_remote):
|
||||
return results, False
|
||||
|
||||
# if there were no local results, or the request was for remote, search all sources
|
||||
results += connector_manager.search(query, min_confidence=min_confidence)
|
||||
return results, True
|
||||
local_results = search(query, min_confidence=min_confidence)
|
||||
paginated = Paginator(local_results, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"query": query,
|
||||
"results": page,
|
||||
"type": "book",
|
||||
"remote": search_remote,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
}
|
||||
# if a logged in user requested remote results or got no local results, try remote
|
||||
if request.user.is_authenticated and (not local_results or search_remote):
|
||||
data["remote_results"] = connector_manager.search(
|
||||
query, min_confidence=min_confidence
|
||||
)
|
||||
data["remote"] = True
|
||||
return TemplateResponse(request, "search/book.html", data)
|
||||
|
||||
|
||||
def user_search(query, viewer, *_):
|
||||
def user_search(request):
|
||||
"""cool kids members only user search"""
|
||||
viewer = request.user
|
||||
query = request.GET.get("q")
|
||||
query = query.strip()
|
||||
data = {"type": "user", "query": query}
|
||||
# logged out viewers can't search users
|
||||
if not viewer.is_authenticated:
|
||||
return models.User.objects.none(), None
|
||||
return TemplateResponse(request, "search/user.html", data)
|
||||
|
||||
# use webfinger for mastodon style account@domain.com username to load the user if
|
||||
# they don't exist locally (handle_remote_webfinger will check the db)
|
||||
if re.match(regex.FULL_USERNAME, query):
|
||||
handle_remote_webfinger(query)
|
||||
|
||||
return (
|
||||
results = (
|
||||
models.User.viewer_aware_objects(viewer)
|
||||
.annotate(
|
||||
similarity=Greatest(
|
||||
|
@ -104,14 +114,23 @@ def user_search(query, viewer, *_):
|
|||
similarity__gt=0.5,
|
||||
)
|
||||
.order_by("-similarity")
|
||||
), None
|
||||
)
|
||||
paginated = Paginator(results, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data["results"] = page
|
||||
data["page_range"] = paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
)
|
||||
return TemplateResponse(request, "search/user.html", data)
|
||||
|
||||
|
||||
def list_search(query, viewer, *_):
|
||||
def list_search(request):
|
||||
"""any relevent lists?"""
|
||||
return (
|
||||
query = request.GET.get("q")
|
||||
data = {"query": query, "type": "list"}
|
||||
results = (
|
||||
models.List.privacy_filter(
|
||||
viewer,
|
||||
request.user,
|
||||
privacy_levels=["public", "followers"],
|
||||
)
|
||||
.annotate(
|
||||
|
@ -124,7 +143,14 @@ def list_search(query, viewer, *_):
|
|||
similarity__gt=0.1,
|
||||
)
|
||||
.order_by("-similarity")
|
||||
), None
|
||||
)
|
||||
paginated = Paginator(results, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data["results"] = page
|
||||
data["page_range"] = paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
)
|
||||
return TemplateResponse(request, "search/list.html", data)
|
||||
|
||||
|
||||
def isbn_check(query):
|
||||
|
|
|
@ -6,6 +6,7 @@ from urllib.parse import urlparse
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -16,7 +17,6 @@ from django.views.decorators.http import require_POST
|
|||
|
||||
from markdown import markdown
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.utils import regex, sanitizer
|
||||
from .helpers import handle_remote_webfinger, is_api_request
|
||||
from .helpers import load_date_in_user_tz_as_utc
|
||||
|
@ -93,14 +93,16 @@ class CreateStatus(View):
|
|||
|
||||
# inspect the text for user tags
|
||||
content = status.content
|
||||
for (mention_text, mention_user) in find_mentions(content):
|
||||
for (mention_text, mention_user) in find_mentions(
|
||||
request.user, content
|
||||
).items():
|
||||
# add them to status mentions fk
|
||||
status.mention_users.add(mention_user)
|
||||
|
||||
# turn the mention into a link
|
||||
content = re.sub(
|
||||
rf"{mention_text}([^@]|$)",
|
||||
rf'<a href="{mention_user.remote_id}">{mention_text}</a>\g<1>',
|
||||
rf"{mention_text}\b(?!@)",
|
||||
rf'<a href="{mention_user.remote_id}">{mention_text}</a>',
|
||||
content,
|
||||
)
|
||||
# add reply parent to mentions
|
||||
|
@ -195,22 +197,35 @@ def edit_readthrough(request):
|
|||
return redirect("/")
|
||||
|
||||
|
||||
def find_mentions(content):
|
||||
def find_mentions(user, content):
|
||||
"""detect @mentions in raw status content"""
|
||||
if not content:
|
||||
return
|
||||
for match in re.finditer(regex.STRICT_USERNAME, content):
|
||||
username = match.group().strip().split("@")[1:]
|
||||
if len(username) == 1:
|
||||
# this looks like a local user (@user), fill in the domain
|
||||
username.append(DOMAIN)
|
||||
username = "@".join(username)
|
||||
return {}
|
||||
# The regex has nested match groups, so the 0th entry has the full (outer) match
|
||||
# And beacuse the strict username starts with @, the username is 1st char onward
|
||||
usernames = [m[0][1:] for m in re.findall(regex.STRICT_USERNAME, content)]
|
||||
|
||||
mention_user = handle_remote_webfinger(username)
|
||||
known_users = (
|
||||
models.User.viewer_aware_objects(user)
|
||||
.filter(Q(username__in=usernames) | Q(localname__in=usernames))
|
||||
.distinct()
|
||||
)
|
||||
# Prepare a lookup based on both username and localname
|
||||
username_dict = {
|
||||
**{f"@{u.username}": u for u in known_users},
|
||||
**{f"@{u.localname}": u for u in known_users.filter(local=True)},
|
||||
}
|
||||
|
||||
# Users not captured here could be blocked or not yet loaded on the server
|
||||
not_found = set(usernames) - set(username_dict.keys())
|
||||
for username in not_found:
|
||||
mention_user = handle_remote_webfinger(username, unknown_only=True)
|
||||
if not mention_user:
|
||||
# we can ignore users we don't know about
|
||||
# this user is blocked or can't be found
|
||||
continue
|
||||
yield (match.group(), mention_user)
|
||||
username_dict[f"@{mention_user.username}"] = mention_user
|
||||
username_dict[f"@{mention_user.localname}"] = mention_user
|
||||
return username_dict
|
||||
|
||||
|
||||
def format_links(content):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue