We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Make user handle search case insensitive
**Is your feature request related to a problem? Please describe.**
I'm not sure if this is intentional, but currently, you can only look up users if you spell their name witht he correct cases. This is also the case when you try to find BW users from Mastodon. I don't think case sensitivity makes much sense here.

**Additional context**
Mastodon hase case insensitive user names as well.

One can also register `Tofuwabohu` when there is a `tofuwabohu` already. I think that should be prevented before instances get opened?

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bookwyrm/views/helpers.py`
Content:
```
1 """ helper functions used in various views """
2 import re
3 from requests import HTTPError
4 from django.core.exceptions import FieldError
5 from django.db.models import Count, Max, Q
6 
7 from bookwyrm import activitypub, models
8 from bookwyrm.connectors import ConnectorException, get_data
9 from bookwyrm.status import create_generated_note
10 from bookwyrm.utils import regex
11 
12 
13 def get_user_from_username(viewer, username):
14     """ helper function to resolve a localname or a username to a user """
15     # raises DoesNotExist if user is now found
16     try:
17         return models.User.viewer_aware_objects(viewer).get(localname=username)
18     except models.User.DoesNotExist:
19         return models.User.viewer_aware_objects(viewer).get(username=username)
20 
21 
22 def is_api_request(request):
23     """ check whether a request is asking for html or data """
24     return "json" in request.headers.get("Accept", "") or request.path[-5:] == ".json"
25 
26 
27 def is_bookwyrm_request(request):
28     """ check if the request is coming from another bookwyrm instance """
29     user_agent = request.headers.get("User-Agent")
30     if user_agent is None or re.search(regex.bookwyrm_user_agent, user_agent) is None:
31         return False
32     return True
33 
34 
35 def object_visible_to_user(viewer, obj):
36     """ is a user authorized to view an object? """
37     if not obj:
38         return False
39 
40     # viewer can't see it if the object's owner blocked them
41     if viewer in obj.user.blocks.all():
42         return False
43 
44     # you can see your own posts and any public or unlisted posts
45     if viewer == obj.user or obj.privacy in ["public", "unlisted"]:
46         return True
47 
48     # you can see the followers only posts of people you follow
49     if obj.privacy == "followers" and obj.user.followers.filter(id=viewer.id).first():
50         return True
51 
52     # you can see dms you are tagged in
53     if isinstance(obj, models.Status):
54         if obj.privacy == "direct" and obj.mention_users.filter(id=viewer.id).first():
55             return True
56     return False
57 
58 
59 def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
60     """ filter objects that have "user" and "privacy" fields """
61     privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"]
62     # if there'd a deleted field, exclude deleted items
63     try:
64         queryset = queryset.filter(deleted=False)
65     except FieldError:
66         pass
67 
68     # exclude blocks from both directions
69     if not viewer.is_anonymous:
70         blocked = models.User.objects.filter(id__in=viewer.blocks.all()).all()
71         queryset = queryset.exclude(Q(user__in=blocked) | Q(user__blocks=viewer))
72 
73     # you can't see followers only or direct messages if you're not logged in
74     if viewer.is_anonymous:
75         privacy_levels = [p for p in privacy_levels if not p in ["followers", "direct"]]
76 
77     # filter to only privided privacy levels
78     queryset = queryset.filter(privacy__in=privacy_levels)
79 
80     # only include statuses the user follows
81     if following_only:
82         queryset = queryset.exclude(
83             ~Q(  # remove everythign except
84                 Q(user__in=viewer.following.all())
85                 | Q(user=viewer)  # user following
86                 | Q(mention_users=viewer)  # is self  # mentions user
87             ),
88         )
89     # exclude followers-only statuses the user doesn't follow
90     elif "followers" in privacy_levels:
91         queryset = queryset.exclude(
92             ~Q(  # user isn't following and it isn't their own status
93                 Q(user__in=viewer.following.all()) | Q(user=viewer)
94             ),
95             privacy="followers",  # and the status is followers only
96         )
97 
98     # exclude direct messages not intended for the user
99     if "direct" in privacy_levels:
100         try:
101             queryset = queryset.exclude(
102                 ~Q(Q(user=viewer) | Q(mention_users=viewer)), privacy="direct"
103             )
104         except FieldError:
105             queryset = queryset.exclude(~Q(user=viewer), privacy="direct")
106 
107     return queryset
108 
109 
110 def handle_remote_webfinger(query):
111     """ webfingerin' other servers """
112     user = None
113 
114     # usernames could be @user@domain or user@domain
115     if not query:
116         return None
117 
118     if query[0] == "@":
119         query = query[1:]
120 
121     try:
122         domain = query.split("@")[1]
123     except IndexError:
124         return None
125 
126     try:
127         user = models.User.objects.get(username=query)
128     except models.User.DoesNotExist:
129         url = "https://%s/.well-known/webfinger?resource=acct:%s" % (domain, query)
130         try:
131             data = get_data(url)
132         except (ConnectorException, HTTPError):
133             return None
134 
135         for link in data.get("links"):
136             if link.get("rel") == "self":
137                 try:
138                     user = activitypub.resolve_remote_id(
139                         link["href"], model=models.User
140                     )
141                 except (KeyError, activitypub.ActivitySerializerError):
142                     return None
143     return user
144 
145 
146 def get_edition(book_id):
147     """ look up a book in the db and return an edition """
148     book = models.Book.objects.select_subclasses().get(id=book_id)
149     if isinstance(book, models.Work):
150         book = book.get_default_edition()
151     return book
152 
153 
154 def handle_reading_status(user, shelf, book, privacy):
155     """ post about a user reading a book """
156     # tell the world about this cool thing that happened
157     try:
158         message = {
159             "to-read": "wants to read",
160             "reading": "started reading",
161             "read": "finished reading",
162         }[shelf.identifier]
163     except KeyError:
164         # it's a non-standard shelf, don't worry about it
165         return
166 
167     status = create_generated_note(user, message, mention_books=[book], privacy=privacy)
168     status.save()
169 
170 
171 def is_blocked(viewer, user):
172     """ is this viewer blocked by the user? """
173     if viewer.is_authenticated and viewer in user.blocks.all():
174         return True
175     return False
176 
177 
178 def get_discover_books():
179     """ list of books for the discover page """
180     return list(
181         set(
182             models.Edition.objects.filter(
183                 review__published_date__isnull=False,
184                 review__deleted=False,
185                 review__user__local=True,
186                 review__privacy__in=["public", "unlisted"],
187             )
188             .exclude(cover__exact="")
189             .annotate(Max("review__published_date"))
190             .order_by("-review__published_date__max")[:6]
191         )
192     )
193 
194 
195 def get_suggested_users(user):
196     """ bookwyrm users you don't already know """
197     return (
198         get_annotated_users(
199             user,
200             ~Q(id=user.id),
201             ~Q(followers=user),
202             ~Q(follower_requests=user),
203             bookwyrm_user=True,
204         )
205         .order_by("-mutuals", "-last_active_date")
206         .all()[:5]
207     )
208 
209 
210 def get_annotated_users(user, *args, **kwargs):
211     """ Users, annotated with things they have in common """
212     return (
213         models.User.objects.filter(discoverable=True, is_active=True, *args, **kwargs)
214         .exclude(Q(id__in=user.blocks.all()) | Q(blocks=user))
215         .annotate(
216             mutuals=Count(
217                 "following",
218                 filter=Q(
219                     ~Q(id=user.id),
220                     ~Q(id__in=user.following.all()),
221                     following__in=user.following.all(),
222                 ),
223                 distinct=True,
224             ),
225             shared_books=Count(
226                 "shelfbook",
227                 filter=Q(
228                     ~Q(id=user.id),
229                     shelfbook__book__parent_work__in=[
230                         s.book.parent_work for s in user.shelfbook_set.all()
231                     ],
232                 ),
233                 distinct=True,
234             ),
235         )
236     )
237
```

Path: `bookwyrm/views/search.py`
Content:
```
1 """ search views"""
2 import re
3 
4 from django.contrib.postgres.search import TrigramSimilarity
5 from django.db.models.functions import Greatest
6 from django.http import JsonResponse
7 from django.template.response import TemplateResponse
8 from django.views import View
9 
10 from bookwyrm import models
11 from bookwyrm.connectors import connector_manager
12 from bookwyrm.utils import regex
13 from .helpers import is_api_request, privacy_filter
14 from .helpers import handle_remote_webfinger
15 
16 
17 # pylint: disable= no-self-use
18 class Search(View):
19     """ search users or books """
20 
21     def get(self, request):
22         """ that search bar up top """
23         query = request.GET.get("q")
24         min_confidence = request.GET.get("min_confidence", 0.1)
25 
26         if is_api_request(request):
27             # only return local book results via json so we don't cascade
28             book_results = connector_manager.local_search(
29                 query, min_confidence=min_confidence
30             )
31             return JsonResponse([r.json() for r in book_results], safe=False)
32 
33         # use webfinger for mastodon style account@domain.com username
34         if query and re.match(regex.full_username, query):
35             handle_remote_webfinger(query)
36 
37         # do a  user search
38         user_results = (
39             models.User.viewer_aware_objects(request.user)
40             .annotate(
41                 similarity=Greatest(
42                     TrigramSimilarity("username", query),
43                     TrigramSimilarity("localname", query),
44                 )
45             )
46             .filter(
47                 similarity__gt=0.5,
48             )
49             .order_by("-similarity")[:10]
50         )
51 
52         # any relevent lists?
53         list_results = (
54             privacy_filter(
55                 request.user,
56                 models.List.objects,
57                 privacy_levels=["public", "followers"],
58             )
59             .annotate(
60                 similarity=Greatest(
61                     TrigramSimilarity("name", query),
62                     TrigramSimilarity("description", query),
63                 )
64             )
65             .filter(
66                 similarity__gt=0.1,
67             )
68             .order_by("-similarity")[:10]
69         )
70 
71         book_results = connector_manager.search(query, min_confidence=min_confidence)
72         data = {
73             "book_results": book_results,
74             "user_results": user_results,
75             "list_results": list_results,
76             "query": query or "",
77         }
78         return TemplateResponse(request, "search_results.html", data)
79
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py
--- a/bookwyrm/views/helpers.py
+++ b/bookwyrm/views/helpers.py
@@ -124,7 +124,7 @@
         return None
 
     try:
-        user = models.User.objects.get(username=query)
+        user = models.User.objects.get(username__iexact=query)
     except models.User.DoesNotExist:
         url = "https://%s/.well-known/webfinger?resource=acct:%s" % (domain, query)
         try:
diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py
--- a/bookwyrm/views/search.py
+++ b/bookwyrm/views/search.py
@@ -34,7 +34,7 @@
         if query and re.match(regex.full_username, query):
             handle_remote_webfinger(query)
 
-        # do a  user search
+        # do a user search
         user_results = (
             models.User.viewer_aware_objects(request.user)
             .annotate(

{"golden_diff": "diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py\n--- a/bookwyrm/views/helpers.py\n+++ b/bookwyrm/views/helpers.py\n@@ -124,7 +124,7 @@\n         return None\n \n     try:\n-        user = models.User.objects.get(username=query)\n+        user = models.User.objects.get(username__iexact=query)\n     except models.User.DoesNotExist:\n         url = \"https://%s/.well-known/webfinger?resource=acct:%s\" % (domain, query)\n         try:\ndiff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py\n--- a/bookwyrm/views/search.py\n+++ b/bookwyrm/views/search.py\n@@ -34,7 +34,7 @@\n         if query and re.match(regex.full_username, query):\n             handle_remote_webfinger(query)\n \n-        # do a  user search\n+        # do a user search\n         user_results = (\n             models.User.viewer_aware_objects(request.user)\n             .annotate(\n", "issue": "Make user handle search case insensitive\n**Is your feature request related to a problem? Please describe.**\r\nI'm not sure if this is intentional, but currently, you can only look up users if you spell their name witht he correct cases. This is also the case when you try to find BW users from Mastodon. I don't think case sensitivity makes much sense here.\r\n\r\n**Additional context**\r\nMastodon hase case insensitive user names as well.\r\n\r\nOne can also register `Tofuwabohu` when there is a `tofuwabohu` already. I think that should be prevented before instances get opened?\n", "before_files": [{"content": "\"\"\" helper functions used in various views \"\"\"\nimport re\nfrom requests import HTTPError\nfrom django.core.exceptions import FieldError\nfrom django.db.models import Count, Max, Q\n\nfrom bookwyrm import activitypub, models\nfrom bookwyrm.connectors import ConnectorException, get_data\nfrom bookwyrm.status import create_generated_note\nfrom bookwyrm.utils import regex\n\n\ndef get_user_from_username(viewer, username):\n    \"\"\" helper function to resolve a localname or a username to a user \"\"\"\n    # raises DoesNotExist if user is now found\n    try:\n        return models.User.viewer_aware_objects(viewer).get(localname=username)\n    except models.User.DoesNotExist:\n        return models.User.viewer_aware_objects(viewer).get(username=username)\n\n\ndef is_api_request(request):\n    \"\"\" check whether a request is asking for html or data \"\"\"\n    return \"json\" in request.headers.get(\"Accept\", \"\") or request.path[-5:] == \".json\"\n\n\ndef is_bookwyrm_request(request):\n    \"\"\" check if the request is coming from another bookwyrm instance \"\"\"\n    user_agent = request.headers.get(\"User-Agent\")\n    if user_agent is None or re.search(regex.bookwyrm_user_agent, user_agent) is None:\n        return False\n    return True\n\n\ndef object_visible_to_user(viewer, obj):\n    \"\"\" is a user authorized to view an object? \"\"\"\n    if not obj:\n        return False\n\n    # viewer can't see it if the object's owner blocked them\n    if viewer in obj.user.blocks.all():\n        return False\n\n    # you can see your own posts and any public or unlisted posts\n    if viewer == obj.user or obj.privacy in [\"public\", \"unlisted\"]:\n        return True\n\n    # you can see the followers only posts of people you follow\n    if obj.privacy == \"followers\" and obj.user.followers.filter(id=viewer.id).first():\n        return True\n\n    # you can see dms you are tagged in\n    if isinstance(obj, models.Status):\n        if obj.privacy == \"direct\" and obj.mention_users.filter(id=viewer.id).first():\n            return True\n    return False\n\n\ndef privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):\n    \"\"\" filter objects that have \"user\" and \"privacy\" fields \"\"\"\n    privacy_levels = privacy_levels or [\"public\", \"unlisted\", \"followers\", \"direct\"]\n    # if there'd a deleted field, exclude deleted items\n    try:\n        queryset = queryset.filter(deleted=False)\n    except FieldError:\n        pass\n\n    # exclude blocks from both directions\n    if not viewer.is_anonymous:\n        blocked = models.User.objects.filter(id__in=viewer.blocks.all()).all()\n        queryset = queryset.exclude(Q(user__in=blocked) | Q(user__blocks=viewer))\n\n    # you can't see followers only or direct messages if you're not logged in\n    if viewer.is_anonymous:\n        privacy_levels = [p for p in privacy_levels if not p in [\"followers\", \"direct\"]]\n\n    # filter to only privided privacy levels\n    queryset = queryset.filter(privacy__in=privacy_levels)\n\n    # only include statuses the user follows\n    if following_only:\n        queryset = queryset.exclude(\n            ~Q(  # remove everythign except\n                Q(user__in=viewer.following.all())\n                | Q(user=viewer)  # user following\n                | Q(mention_users=viewer)  # is self  # mentions user\n            ),\n        )\n    # exclude followers-only statuses the user doesn't follow\n    elif \"followers\" in privacy_levels:\n        queryset = queryset.exclude(\n            ~Q(  # user isn't following and it isn't their own status\n                Q(user__in=viewer.following.all()) | Q(user=viewer)\n            ),\n            privacy=\"followers\",  # and the status is followers only\n        )\n\n    # exclude direct messages not intended for the user\n    if \"direct\" in privacy_levels:\n        try:\n            queryset = queryset.exclude(\n                ~Q(Q(user=viewer) | Q(mention_users=viewer)), privacy=\"direct\"\n            )\n        except FieldError:\n            queryset = queryset.exclude(~Q(user=viewer), privacy=\"direct\")\n\n    return queryset\n\n\ndef handle_remote_webfinger(query):\n    \"\"\" webfingerin' other servers \"\"\"\n    user = None\n\n    # usernames could be @user@domain or user@domain\n    if not query:\n        return None\n\n    if query[0] == \"@\":\n        query = query[1:]\n\n    try:\n        domain = query.split(\"@\")[1]\n    except IndexError:\n        return None\n\n    try:\n        user = models.User.objects.get(username=query)\n    except models.User.DoesNotExist:\n        url = \"https://%s/.well-known/webfinger?resource=acct:%s\" % (domain, query)\n        try:\n            data = get_data(url)\n        except (ConnectorException, HTTPError):\n            return None\n\n        for link in data.get(\"links\"):\n            if link.get(\"rel\") == \"self\":\n                try:\n                    user = activitypub.resolve_remote_id(\n                        link[\"href\"], model=models.User\n                    )\n                except (KeyError, activitypub.ActivitySerializerError):\n                    return None\n    return user\n\n\ndef get_edition(book_id):\n    \"\"\" look up a book in the db and return an edition \"\"\"\n    book = models.Book.objects.select_subclasses().get(id=book_id)\n    if isinstance(book, models.Work):\n        book = book.get_default_edition()\n    return book\n\n\ndef handle_reading_status(user, shelf, book, privacy):\n    \"\"\" post about a user reading a book \"\"\"\n    # tell the world about this cool thing that happened\n    try:\n        message = {\n            \"to-read\": \"wants to read\",\n            \"reading\": \"started reading\",\n            \"read\": \"finished reading\",\n        }[shelf.identifier]\n    except KeyError:\n        # it's a non-standard shelf, don't worry about it\n        return\n\n    status = create_generated_note(user, message, mention_books=[book], privacy=privacy)\n    status.save()\n\n\ndef is_blocked(viewer, user):\n    \"\"\" is this viewer blocked by the user? \"\"\"\n    if viewer.is_authenticated and viewer in user.blocks.all():\n        return True\n    return False\n\n\ndef get_discover_books():\n    \"\"\" list of books for the discover page \"\"\"\n    return list(\n        set(\n            models.Edition.objects.filter(\n                review__published_date__isnull=False,\n                review__deleted=False,\n                review__user__local=True,\n                review__privacy__in=[\"public\", \"unlisted\"],\n            )\n            .exclude(cover__exact=\"\")\n            .annotate(Max(\"review__published_date\"))\n            .order_by(\"-review__published_date__max\")[:6]\n        )\n    )\n\n\ndef get_suggested_users(user):\n    \"\"\" bookwyrm users you don't already know \"\"\"\n    return (\n        get_annotated_users(\n            user,\n            ~Q(id=user.id),\n            ~Q(followers=user),\n            ~Q(follower_requests=user),\n            bookwyrm_user=True,\n        )\n        .order_by(\"-mutuals\", \"-last_active_date\")\n        .all()[:5]\n    )\n\n\ndef get_annotated_users(user, *args, **kwargs):\n    \"\"\" Users, annotated with things they have in common \"\"\"\n    return (\n        models.User.objects.filter(discoverable=True, is_active=True, *args, **kwargs)\n        .exclude(Q(id__in=user.blocks.all()) | Q(blocks=user))\n        .annotate(\n            mutuals=Count(\n                \"following\",\n                filter=Q(\n                    ~Q(id=user.id),\n                    ~Q(id__in=user.following.all()),\n                    following__in=user.following.all(),\n                ),\n                distinct=True,\n            ),\n            shared_books=Count(\n                \"shelfbook\",\n                filter=Q(\n                    ~Q(id=user.id),\n                    shelfbook__book__parent_work__in=[\n                        s.book.parent_work for s in user.shelfbook_set.all()\n                    ],\n                ),\n                distinct=True,\n            ),\n        )\n    )\n", "path": "bookwyrm/views/helpers.py"}, {"content": "\"\"\" search views\"\"\"\nimport re\n\nfrom django.contrib.postgres.search import TrigramSimilarity\nfrom django.db.models.functions import Greatest\nfrom django.http import JsonResponse\nfrom django.template.response import TemplateResponse\nfrom django.views import View\n\nfrom bookwyrm import models\nfrom bookwyrm.connectors import connector_manager\nfrom bookwyrm.utils import regex\nfrom .helpers import is_api_request, privacy_filter\nfrom .helpers import handle_remote_webfinger\n\n\n# pylint: disable= no-self-use\nclass Search(View):\n    \"\"\" search users or books \"\"\"\n\n    def get(self, request):\n        \"\"\" that search bar up top \"\"\"\n        query = request.GET.get(\"q\")\n        min_confidence = request.GET.get(\"min_confidence\", 0.1)\n\n        if is_api_request(request):\n            # only return local book results via json so we don't cascade\n            book_results = connector_manager.local_search(\n                query, min_confidence=min_confidence\n            )\n            return JsonResponse([r.json() for r in book_results], safe=False)\n\n        # use webfinger for mastodon style account@domain.com username\n        if query and re.match(regex.full_username, query):\n            handle_remote_webfinger(query)\n\n        # do a  user search\n        user_results = (\n            models.User.viewer_aware_objects(request.user)\n            .annotate(\n                similarity=Greatest(\n                    TrigramSimilarity(\"username\", query),\n                    TrigramSimilarity(\"localname\", query),\n                )\n            )\n            .filter(\n                similarity__gt=0.5,\n            )\n            .order_by(\"-similarity\")[:10]\n        )\n\n        # any relevent lists?\n        list_results = (\n            privacy_filter(\n                request.user,\n                models.List.objects,\n                privacy_levels=[\"public\", \"followers\"],\n            )\n            .annotate(\n                similarity=Greatest(\n                    TrigramSimilarity(\"name\", query),\n                    TrigramSimilarity(\"description\", query),\n                )\n            )\n            .filter(\n                similarity__gt=0.1,\n            )\n            .order_by(\"-similarity\")[:10]\n        )\n\n        book_results = connector_manager.search(query, min_confidence=min_confidence)\n        data = {\n            \"book_results\": book_results,\n            \"user_results\": user_results,\n            \"list_results\": list_results,\n            \"query\": query or \"\",\n        }\n        return TemplateResponse(request, \"search_results.html\", data)\n", "path": "bookwyrm/views/search.py"}], "after_files": [{"content": "\"\"\" helper functions used in various views \"\"\"\nimport re\nfrom requests import HTTPError\nfrom django.core.exceptions import FieldError\nfrom django.db.models import Count, Max, Q\n\nfrom bookwyrm import activitypub, models\nfrom bookwyrm.connectors import ConnectorException, get_data\nfrom bookwyrm.status import create_generated_note\nfrom bookwyrm.utils import regex\n\n\ndef get_user_from_username(viewer, username):\n    \"\"\" helper function to resolve a localname or a username to a user \"\"\"\n    # raises DoesNotExist if user is now found\n    try:\n        return models.User.viewer_aware_objects(viewer).get(localname=username)\n    except models.User.DoesNotExist:\n        return models.User.viewer_aware_objects(viewer).get(username=username)\n\n\ndef is_api_request(request):\n    \"\"\" check whether a request is asking for html or data \"\"\"\n    return \"json\" in request.headers.get(\"Accept\", \"\") or request.path[-5:] == \".json\"\n\n\ndef is_bookwyrm_request(request):\n    \"\"\" check if the request is coming from another bookwyrm instance \"\"\"\n    user_agent = request.headers.get(\"User-Agent\")\n    if user_agent is None or re.search(regex.bookwyrm_user_agent, user_agent) is None:\n        return False\n    return True\n\n\ndef object_visible_to_user(viewer, obj):\n    \"\"\" is a user authorized to view an object? \"\"\"\n    if not obj:\n        return False\n\n    # viewer can't see it if the object's owner blocked them\n    if viewer in obj.user.blocks.all():\n        return False\n\n    # you can see your own posts and any public or unlisted posts\n    if viewer == obj.user or obj.privacy in [\"public\", \"unlisted\"]:\n        return True\n\n    # you can see the followers only posts of people you follow\n    if obj.privacy == \"followers\" and obj.user.followers.filter(id=viewer.id).first():\n        return True\n\n    # you can see dms you are tagged in\n    if isinstance(obj, models.Status):\n        if obj.privacy == \"direct\" and obj.mention_users.filter(id=viewer.id).first():\n            return True\n    return False\n\n\ndef privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):\n    \"\"\" filter objects that have \"user\" and \"privacy\" fields \"\"\"\n    privacy_levels = privacy_levels or [\"public\", \"unlisted\", \"followers\", \"direct\"]\n    # if there'd a deleted field, exclude deleted items\n    try:\n        queryset = queryset.filter(deleted=False)\n    except FieldError:\n        pass\n\n    # exclude blocks from both directions\n    if not viewer.is_anonymous:\n        blocked = models.User.objects.filter(id__in=viewer.blocks.all()).all()\n        queryset = queryset.exclude(Q(user__in=blocked) | Q(user__blocks=viewer))\n\n    # you can't see followers only or direct messages if you're not logged in\n    if viewer.is_anonymous:\n        privacy_levels = [p for p in privacy_levels if not p in [\"followers\", \"direct\"]]\n\n    # filter to only privided privacy levels\n    queryset = queryset.filter(privacy__in=privacy_levels)\n\n    # only include statuses the user follows\n    if following_only:\n        queryset = queryset.exclude(\n            ~Q(  # remove everythign except\n                Q(user__in=viewer.following.all())\n                | Q(user=viewer)  # user following\n                | Q(mention_users=viewer)  # is self  # mentions user\n            ),\n        )\n    # exclude followers-only statuses the user doesn't follow\n    elif \"followers\" in privacy_levels:\n        queryset = queryset.exclude(\n            ~Q(  # user isn't following and it isn't their own status\n                Q(user__in=viewer.following.all()) | Q(user=viewer)\n            ),\n            privacy=\"followers\",  # and the status is followers only\n        )\n\n    # exclude direct messages not intended for the user\n    if \"direct\" in privacy_levels:\n        try:\n            queryset = queryset.exclude(\n                ~Q(Q(user=viewer) | Q(mention_users=viewer)), privacy=\"direct\"\n            )\n        except FieldError:\n            queryset = queryset.exclude(~Q(user=viewer), privacy=\"direct\")\n\n    return queryset\n\n\ndef handle_remote_webfinger(query):\n    \"\"\" webfingerin' other servers \"\"\"\n    user = None\n\n    # usernames could be @user@domain or user@domain\n    if not query:\n        return None\n\n    if query[0] == \"@\":\n        query = query[1:]\n\n    try:\n        domain = query.split(\"@\")[1]\n    except IndexError:\n        return None\n\n    try:\n        user = models.User.objects.get(username__iexact=query)\n    except models.User.DoesNotExist:\n        url = \"https://%s/.well-known/webfinger?resource=acct:%s\" % (domain, query)\n        try:\n            data = get_data(url)\n        except (ConnectorException, HTTPError):\n            return None\n\n        for link in data.get(\"links\"):\n            if link.get(\"rel\") == \"self\":\n                try:\n                    user = activitypub.resolve_remote_id(\n                        link[\"href\"], model=models.User\n                    )\n                except (KeyError, activitypub.ActivitySerializerError):\n                    return None\n    return user\n\n\ndef get_edition(book_id):\n    \"\"\" look up a book in the db and return an edition \"\"\"\n    book = models.Book.objects.select_subclasses().get(id=book_id)\n    if isinstance(book, models.Work):\n        book = book.get_default_edition()\n    return book\n\n\ndef handle_reading_status(user, shelf, book, privacy):\n    \"\"\" post about a user reading a book \"\"\"\n    # tell the world about this cool thing that happened\n    try:\n        message = {\n            \"to-read\": \"wants to read\",\n            \"reading\": \"started reading\",\n            \"read\": \"finished reading\",\n        }[shelf.identifier]\n    except KeyError:\n        # it's a non-standard shelf, don't worry about it\n        return\n\n    status = create_generated_note(user, message, mention_books=[book], privacy=privacy)\n    status.save()\n\n\ndef is_blocked(viewer, user):\n    \"\"\" is this viewer blocked by the user? \"\"\"\n    if viewer.is_authenticated and viewer in user.blocks.all():\n        return True\n    return False\n\n\ndef get_discover_books():\n    \"\"\" list of books for the discover page \"\"\"\n    return list(\n        set(\n            models.Edition.objects.filter(\n                review__published_date__isnull=False,\n                review__deleted=False,\n                review__user__local=True,\n                review__privacy__in=[\"public\", \"unlisted\"],\n            )\n            .exclude(cover__exact=\"\")\n            .annotate(Max(\"review__published_date\"))\n            .order_by(\"-review__published_date__max\")[:6]\n        )\n    )\n\n\ndef get_suggested_users(user):\n    \"\"\" bookwyrm users you don't already know \"\"\"\n    return (\n        get_annotated_users(\n            user,\n            ~Q(id=user.id),\n            ~Q(followers=user),\n            ~Q(follower_requests=user),\n            bookwyrm_user=True,\n        )\n        .order_by(\"-mutuals\", \"-last_active_date\")\n        .all()[:5]\n    )\n\n\ndef get_annotated_users(user, *args, **kwargs):\n    \"\"\" Users, annotated with things they have in common \"\"\"\n    return (\n        models.User.objects.filter(discoverable=True, is_active=True, *args, **kwargs)\n        .exclude(Q(id__in=user.blocks.all()) | Q(blocks=user))\n        .annotate(\n            mutuals=Count(\n                \"following\",\n                filter=Q(\n                    ~Q(id=user.id),\n                    ~Q(id__in=user.following.all()),\n                    following__in=user.following.all(),\n                ),\n                distinct=True,\n            ),\n            shared_books=Count(\n                \"shelfbook\",\n                filter=Q(\n                    ~Q(id=user.id),\n                    shelfbook__book__parent_work__in=[\n                        s.book.parent_work for s in user.shelfbook_set.all()\n                    ],\n                ),\n                distinct=True,\n            ),\n        )\n    )\n", "path": "bookwyrm/views/helpers.py"}, {"content": "\"\"\" search views\"\"\"\nimport re\n\nfrom django.contrib.postgres.search import TrigramSimilarity\nfrom django.db.models.functions import Greatest\nfrom django.http import JsonResponse\nfrom django.template.response import TemplateResponse\nfrom django.views import View\n\nfrom bookwyrm import models\nfrom bookwyrm.connectors import connector_manager\nfrom bookwyrm.utils import regex\nfrom .helpers import is_api_request, privacy_filter\nfrom .helpers import handle_remote_webfinger\n\n\n# pylint: disable= no-self-use\nclass Search(View):\n    \"\"\" search users or books \"\"\"\n\n    def get(self, request):\n        \"\"\" that search bar up top \"\"\"\n        query = request.GET.get(\"q\")\n        min_confidence = request.GET.get(\"min_confidence\", 0.1)\n\n        if is_api_request(request):\n            # only return local book results via json so we don't cascade\n            book_results = connector_manager.local_search(\n                query, min_confidence=min_confidence\n            )\n            return JsonResponse([r.json() for r in book_results], safe=False)\n\n        # use webfinger for mastodon style account@domain.com username\n        if query and re.match(regex.full_username, query):\n            handle_remote_webfinger(query)\n\n        # do a user search\n        user_results = (\n            models.User.viewer_aware_objects(request.user)\n            .annotate(\n                similarity=Greatest(\n                    TrigramSimilarity(\"username\", query),\n                    TrigramSimilarity(\"localname\", query),\n                )\n            )\n            .filter(\n                similarity__gt=0.5,\n            )\n            .order_by(\"-similarity\")[:10]\n        )\n\n        # any relevent lists?\n        list_results = (\n            privacy_filter(\n                request.user,\n                models.List.objects,\n                privacy_levels=[\"public\", \"followers\"],\n            )\n            .annotate(\n                similarity=Greatest(\n                    TrigramSimilarity(\"name\", query),\n                    TrigramSimilarity(\"description\", query),\n                )\n            )\n            .filter(\n                similarity__gt=0.1,\n            )\n            .order_by(\"-similarity\")[:10]\n        )\n\n        book_results = connector_manager.search(query, min_confidence=min_confidence)\n        data = {\n            \"book_results\": book_results,\n            \"user_results\": user_results,\n            \"list_results\": list_results,\n            \"query\": query or \"\",\n        }\n        return TemplateResponse(request, \"search_results.html\", data)\n", "path": "bookwyrm/views/search.py"}]}