We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
urls in parens don't parse as links


--- 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/outgoing.py`
Content:
```
1 ''' handles all the activity coming out of the server '''
2 import re
3 
4 from django.db import IntegrityError, transaction
5 from django.http import JsonResponse
6 from django.shortcuts import get_object_or_404
7 from django.views.decorators.csrf import csrf_exempt
8 from django.views.decorators.http import require_GET
9 from markdown import markdown
10 from requests import HTTPError
11 
12 from bookwyrm import activitypub
13 from bookwyrm import models
14 from bookwyrm.connectors import get_data, ConnectorException
15 from bookwyrm.broadcast import broadcast
16 from bookwyrm.sanitize_html import InputHtmlParser
17 from bookwyrm.status import create_notification
18 from bookwyrm.status import create_generated_note
19 from bookwyrm.status import delete_status
20 from bookwyrm.settings import DOMAIN
21 from bookwyrm.utils import regex
22 
23 
24 @csrf_exempt
25 @require_GET
26 def outbox(request, username):
27     ''' outbox for the requested user '''
28     user = get_object_or_404(models.User, localname=username)
29     filter_type = request.GET.get('type')
30     if filter_type not in models.status_models:
31         filter_type = None
32 
33     return JsonResponse(
34         user.to_outbox(**request.GET, filter_type=filter_type),
35         encoder=activitypub.ActivityEncoder
36     )
37 
38 
39 def handle_remote_webfinger(query):
40     ''' webfingerin' other servers '''
41     user = None
42 
43     # usernames could be @user@domain or user@domain
44     if not query:
45         return None
46 
47     if query[0] == '@':
48         query = query[1:]
49 
50     try:
51         domain = query.split('@')[1]
52     except IndexError:
53         return None
54 
55     try:
56         user = models.User.objects.get(username=query)
57     except models.User.DoesNotExist:
58         url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \
59             (domain, query)
60         try:
61             data = get_data(url)
62         except (ConnectorException, HTTPError):
63             return None
64 
65         for link in data.get('links'):
66             if link.get('rel') == 'self':
67                 try:
68                     user = activitypub.resolve_remote_id(
69                         models.User, link['href']
70                     )
71                 except KeyError:
72                     return None
73     return user
74 
75 
76 def handle_follow(user, to_follow):
77     ''' someone local wants to follow someone '''
78     relationship, _ = models.UserFollowRequest.objects.get_or_create(
79         user_subject=user,
80         user_object=to_follow,
81     )
82     activity = relationship.to_activity()
83     broadcast(user, activity, privacy='direct', direct_recipients=[to_follow])
84 
85 
86 def handle_unfollow(user, to_unfollow):
87     ''' someone local wants to follow someone '''
88     relationship = models.UserFollows.objects.get(
89         user_subject=user,
90         user_object=to_unfollow
91     )
92     activity = relationship.to_undo_activity(user)
93     broadcast(user, activity, privacy='direct', direct_recipients=[to_unfollow])
94     to_unfollow.followers.remove(user)
95 
96 
97 def handle_accept(follow_request):
98     ''' send an acceptance message to a follow request '''
99     user = follow_request.user_subject
100     to_follow = follow_request.user_object
101     with transaction.atomic():
102         relationship = models.UserFollows.from_request(follow_request)
103         follow_request.delete()
104         relationship.save()
105 
106     activity = relationship.to_accept_activity()
107     broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
108 
109 
110 def handle_reject(follow_request):
111     ''' a local user who managed follows rejects a follow request '''
112     user = follow_request.user_subject
113     to_follow = follow_request.user_object
114     activity = follow_request.to_reject_activity()
115     follow_request.delete()
116     broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
117 
118 
119 def handle_shelve(user, book, shelf):
120     ''' a local user is getting a book put on their shelf '''
121     # update the database
122     shelve = models.ShelfBook(book=book, shelf=shelf, added_by=user)
123     shelve.save()
124 
125     broadcast(user, shelve.to_add_activity(user))
126 
127 
128 def handle_unshelve(user, book, shelf):
129     ''' a local user is getting a book put on their shelf '''
130     # update the database
131     row = models.ShelfBook.objects.get(book=book, shelf=shelf)
132     activity = row.to_remove_activity(user)
133     row.delete()
134 
135     broadcast(user, activity)
136 
137 
138 def handle_reading_status(user, shelf, book, privacy):
139     ''' post about a user reading a book '''
140     # tell the world about this cool thing that happened
141     try:
142         message = {
143             'to-read': 'wants to read',
144             'reading': 'started reading',
145             'read': 'finished reading'
146         }[shelf.identifier]
147     except KeyError:
148         # it's a non-standard shelf, don't worry about it
149         return
150 
151     status = create_generated_note(
152         user,
153         message,
154         mention_books=[book],
155         privacy=privacy
156     )
157     status.save()
158 
159     broadcast(user, status.to_create_activity(user))
160 
161 
162 def handle_imported_book(user, item, include_reviews, privacy):
163     ''' process a goodreads csv and then post about it '''
164     if isinstance(item.book, models.Work):
165         item.book = item.book.default_edition
166     if not item.book:
167         return
168 
169     existing_shelf = models.ShelfBook.objects.filter(
170         book=item.book, added_by=user).exists()
171 
172     # shelve the book if it hasn't been shelved already
173     if item.shelf and not existing_shelf:
174         desired_shelf = models.Shelf.objects.get(
175             identifier=item.shelf,
176             user=user
177         )
178         shelf_book = models.ShelfBook.objects.create(
179             book=item.book, shelf=desired_shelf, added_by=user)
180         broadcast(user, shelf_book.to_add_activity(user), privacy=privacy)
181 
182     for read in item.reads:
183         read.book = item.book
184         read.user = user
185         read.save()
186 
187     if include_reviews and (item.rating or item.review):
188         review_title = 'Review of {!r} on Goodreads'.format(
189             item.book.title,
190         ) if item.review else ''
191 
192         # we don't know the publication date of the review,
193         # but "now" is a bad guess
194         published_date_guess = item.date_read or item.date_added
195         review = models.Review.objects.create(
196             user=user,
197             book=item.book,
198             name=review_title,
199             content=item.review,
200             rating=item.rating,
201             published_date=published_date_guess,
202             privacy=privacy,
203         )
204         # we don't need to send out pure activities because non-bookwyrm
205         # instances don't need this data
206         broadcast(user, review.to_create_activity(user), privacy=privacy)
207 
208 
209 def handle_delete_status(user, status):
210     ''' delete a status and broadcast deletion to other servers '''
211     delete_status(status)
212     broadcast(user, status.to_delete_activity(user))
213 
214 
215 def handle_status(user, form):
216     ''' generic handler for statuses '''
217     status = form.save(commit=False)
218     if not status.sensitive and status.content_warning:
219         # the cw text field remains populated when you click "remove"
220         status.content_warning = None
221     status.save()
222 
223     # inspect the text for user tags
224     content = status.content
225     for (mention_text, mention_user) in find_mentions(content):
226         # add them to status mentions fk
227         status.mention_users.add(mention_user)
228 
229         # turn the mention into a link
230         content = re.sub(
231             r'%s([^@]|$)' % mention_text,
232             r'<a href="%s">%s</a>\g<1>' % \
233                 (mention_user.remote_id, mention_text),
234             content)
235 
236     # add reply parent to mentions and notify
237     if status.reply_parent:
238         status.mention_users.add(status.reply_parent.user)
239         for mention_user in status.reply_parent.mention_users.all():
240             status.mention_users.add(mention_user)
241 
242         if status.reply_parent.user.local:
243             create_notification(
244                 status.reply_parent.user,
245                 'REPLY',
246                 related_user=user,
247                 related_status=status
248             )
249 
250     # deduplicate mentions
251     status.mention_users.set(set(status.mention_users.all()))
252     # create mention notifications
253     for mention_user in status.mention_users.all():
254         if status.reply_parent and mention_user == status.reply_parent.user:
255             continue
256         if mention_user.local:
257             create_notification(
258                 mention_user,
259                 'MENTION',
260                 related_user=user,
261                 related_status=status
262             )
263 
264     # don't apply formatting to generated notes
265     if not isinstance(status, models.GeneratedNote):
266         status.content = to_markdown(content)
267     # do apply formatting to quotes
268     if hasattr(status, 'quote'):
269         status.quote = to_markdown(status.quote)
270 
271     status.save()
272 
273     broadcast(user, status.to_create_activity(user), software='bookwyrm')
274 
275     # re-format the activity for non-bookwyrm servers
276     remote_activity = status.to_create_activity(user, pure=True)
277     broadcast(user, remote_activity, software='other')
278 
279 
280 def find_mentions(content):
281     ''' detect @mentions in raw status content '''
282     for match in re.finditer(regex.strict_username, content):
283         username = match.group().strip().split('@')[1:]
284         if len(username) == 1:
285             # this looks like a local user (@user), fill in the domain
286             username.append(DOMAIN)
287         username = '@'.join(username)
288 
289         mention_user = handle_remote_webfinger(username)
290         if not mention_user:
291             # we can ignore users we don't know about
292             continue
293         yield (match.group(), mention_user)
294 
295 
296 def format_links(content):
297     ''' detect and format links '''
298     return re.sub(
299         r'([^(href=")]|^)(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \
300                 regex.domain,
301         r'\g<1><a href="\g<2>">\g<3></a>',
302         content)
303 
304 def to_markdown(content):
305     ''' catch links and convert to markdown '''
306     content = format_links(content)
307     content = markdown(content)
308     # sanitize resulting html
309     sanitizer = InputHtmlParser()
310     sanitizer.feed(content)
311     return sanitizer.get_output()
312 
313 
314 def handle_favorite(user, status):
315     ''' a user likes a status '''
316     try:
317         favorite = models.Favorite.objects.create(
318             status=status,
319             user=user
320         )
321     except IntegrityError:
322         # you already fav'ed that
323         return
324 
325     fav_activity = favorite.to_activity()
326     broadcast(
327         user, fav_activity, privacy='direct', direct_recipients=[status.user])
328     create_notification(
329         status.user,
330         'FAVORITE',
331         related_user=user,
332         related_status=status
333     )
334 
335 
336 def handle_unfavorite(user, status):
337     ''' a user likes a status '''
338     try:
339         favorite = models.Favorite.objects.get(
340             status=status,
341             user=user
342         )
343     except models.Favorite.DoesNotExist:
344         # can't find that status, idk
345         return
346 
347     fav_activity = favorite.to_undo_activity(user)
348     favorite.delete()
349     broadcast(user, fav_activity, direct_recipients=[status.user])
350 
351 
352 def handle_boost(user, status):
353     ''' a user wishes to boost a status '''
354     # is it boostable?
355     if not status.boostable:
356         return
357 
358     if models.Boost.objects.filter(
359             boosted_status=status, user=user).exists():
360         # you already boosted that.
361         return
362     boost = models.Boost.objects.create(
363         boosted_status=status,
364         privacy=status.privacy,
365         user=user,
366     )
367 
368     boost_activity = boost.to_activity()
369     broadcast(user, boost_activity)
370 
371     create_notification(
372         status.user,
373         'BOOST',
374         related_user=user,
375         related_status=status
376     )
377 
378 
379 def handle_unboost(user, status):
380     ''' a user regrets boosting a status '''
381     boost = models.Boost.objects.filter(
382         boosted_status=status, user=user
383     ).first()
384     activity = boost.to_undo_activity(user)
385 
386     boost.delete()
387     broadcast(user, activity)
388 
389 
390 def handle_update_book_data(user, item):
391     ''' broadcast the news about our book '''
392     broadcast(user, item.to_update_activity(user))
393 
394 
395 def handle_update_user(user):
396     ''' broadcast editing a user's profile '''
397     broadcast(user, user.to_update_activity(user))
398
```
--- 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/outgoing.py b/bookwyrm/outgoing.py
--- a/bookwyrm/outgoing.py
+++ b/bookwyrm/outgoing.py
@@ -296,7 +296,7 @@
 def format_links(content):
     ''' detect and format links '''
     return re.sub(
-        r'([^(href=")]|^)(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \
+        r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \
                 regex.domain,
         r'\g<1><a href="\g<2>">\g<3></a>',
         content)

{"golden_diff": "diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py\n--- a/bookwyrm/outgoing.py\n+++ b/bookwyrm/outgoing.py\n@@ -296,7 +296,7 @@\n def format_links(content):\n     ''' detect and format links '''\n     return re.sub(\n-        r'([^(href=\")]|^)(https?:\\/\\/(%s([\\w\\.\\-_\\/+&\\?=:;,])*))' % \\\n+        r'([^(href=\")]|^|\\()(https?:\\/\\/(%s([\\w\\.\\-_\\/+&\\?=:;,])*))' % \\\n                 regex.domain,\n         r'\\g<1><a href=\"\\g<2>\">\\g<3></a>',\n         content)\n", "issue": "urls in parens don't parse as links\n\n", "before_files": [{"content": "''' handles all the activity coming out of the server '''\nimport re\n\nfrom django.db import IntegrityError, transaction\nfrom django.http import JsonResponse\nfrom django.shortcuts import get_object_or_404\nfrom django.views.decorators.csrf import csrf_exempt\nfrom django.views.decorators.http import require_GET\nfrom markdown import markdown\nfrom requests import HTTPError\n\nfrom bookwyrm import activitypub\nfrom bookwyrm import models\nfrom bookwyrm.connectors import get_data, ConnectorException\nfrom bookwyrm.broadcast import broadcast\nfrom bookwyrm.sanitize_html import InputHtmlParser\nfrom bookwyrm.status import create_notification\nfrom bookwyrm.status import create_generated_note\nfrom bookwyrm.status import delete_status\nfrom bookwyrm.settings import DOMAIN\nfrom bookwyrm.utils import regex\n\n\n@csrf_exempt\n@require_GET\ndef outbox(request, username):\n    ''' outbox for the requested user '''\n    user = get_object_or_404(models.User, localname=username)\n    filter_type = request.GET.get('type')\n    if filter_type not in models.status_models:\n        filter_type = None\n\n    return JsonResponse(\n        user.to_outbox(**request.GET, filter_type=filter_type),\n        encoder=activitypub.ActivityEncoder\n    )\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' % \\\n            (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                        models.User, link['href']\n                    )\n                except KeyError:\n                    return None\n    return user\n\n\ndef handle_follow(user, to_follow):\n    ''' someone local wants to follow someone '''\n    relationship, _ = models.UserFollowRequest.objects.get_or_create(\n        user_subject=user,\n        user_object=to_follow,\n    )\n    activity = relationship.to_activity()\n    broadcast(user, activity, privacy='direct', direct_recipients=[to_follow])\n\n\ndef handle_unfollow(user, to_unfollow):\n    ''' someone local wants to follow someone '''\n    relationship = models.UserFollows.objects.get(\n        user_subject=user,\n        user_object=to_unfollow\n    )\n    activity = relationship.to_undo_activity(user)\n    broadcast(user, activity, privacy='direct', direct_recipients=[to_unfollow])\n    to_unfollow.followers.remove(user)\n\n\ndef handle_accept(follow_request):\n    ''' send an acceptance message to a follow request '''\n    user = follow_request.user_subject\n    to_follow = follow_request.user_object\n    with transaction.atomic():\n        relationship = models.UserFollows.from_request(follow_request)\n        follow_request.delete()\n        relationship.save()\n\n    activity = relationship.to_accept_activity()\n    broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])\n\n\ndef handle_reject(follow_request):\n    ''' a local user who managed follows rejects a follow request '''\n    user = follow_request.user_subject\n    to_follow = follow_request.user_object\n    activity = follow_request.to_reject_activity()\n    follow_request.delete()\n    broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])\n\n\ndef handle_shelve(user, book, shelf):\n    ''' a local user is getting a book put on their shelf '''\n    # update the database\n    shelve = models.ShelfBook(book=book, shelf=shelf, added_by=user)\n    shelve.save()\n\n    broadcast(user, shelve.to_add_activity(user))\n\n\ndef handle_unshelve(user, book, shelf):\n    ''' a local user is getting a book put on their shelf '''\n    # update the database\n    row = models.ShelfBook.objects.get(book=book, shelf=shelf)\n    activity = row.to_remove_activity(user)\n    row.delete()\n\n    broadcast(user, activity)\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(\n        user,\n        message,\n        mention_books=[book],\n        privacy=privacy\n    )\n    status.save()\n\n    broadcast(user, status.to_create_activity(user))\n\n\ndef handle_imported_book(user, item, include_reviews, privacy):\n    ''' process a goodreads csv and then post about it '''\n    if isinstance(item.book, models.Work):\n        item.book = item.book.default_edition\n    if not item.book:\n        return\n\n    existing_shelf = models.ShelfBook.objects.filter(\n        book=item.book, added_by=user).exists()\n\n    # shelve the book if it hasn't been shelved already\n    if item.shelf and not existing_shelf:\n        desired_shelf = models.Shelf.objects.get(\n            identifier=item.shelf,\n            user=user\n        )\n        shelf_book = models.ShelfBook.objects.create(\n            book=item.book, shelf=desired_shelf, added_by=user)\n        broadcast(user, shelf_book.to_add_activity(user), privacy=privacy)\n\n    for read in item.reads:\n        read.book = item.book\n        read.user = user\n        read.save()\n\n    if include_reviews and (item.rating or item.review):\n        review_title = 'Review of {!r} on Goodreads'.format(\n            item.book.title,\n        ) if item.review else ''\n\n        # we don't know the publication date of the review,\n        # but \"now\" is a bad guess\n        published_date_guess = item.date_read or item.date_added\n        review = models.Review.objects.create(\n            user=user,\n            book=item.book,\n            name=review_title,\n            content=item.review,\n            rating=item.rating,\n            published_date=published_date_guess,\n            privacy=privacy,\n        )\n        # we don't need to send out pure activities because non-bookwyrm\n        # instances don't need this data\n        broadcast(user, review.to_create_activity(user), privacy=privacy)\n\n\ndef handle_delete_status(user, status):\n    ''' delete a status and broadcast deletion to other servers '''\n    delete_status(status)\n    broadcast(user, status.to_delete_activity(user))\n\n\ndef handle_status(user, form):\n    ''' generic handler for statuses '''\n    status = form.save(commit=False)\n    if not status.sensitive and status.content_warning:\n        # the cw text field remains populated when you click \"remove\"\n        status.content_warning = None\n    status.save()\n\n    # inspect the text for user tags\n    content = status.content\n    for (mention_text, mention_user) in find_mentions(content):\n        # add them to status mentions fk\n        status.mention_users.add(mention_user)\n\n        # turn the mention into a link\n        content = re.sub(\n            r'%s([^@]|$)' % mention_text,\n            r'<a href=\"%s\">%s</a>\\g<1>' % \\\n                (mention_user.remote_id, mention_text),\n            content)\n\n    # add reply parent to mentions and notify\n    if status.reply_parent:\n        status.mention_users.add(status.reply_parent.user)\n        for mention_user in status.reply_parent.mention_users.all():\n            status.mention_users.add(mention_user)\n\n        if status.reply_parent.user.local:\n            create_notification(\n                status.reply_parent.user,\n                'REPLY',\n                related_user=user,\n                related_status=status\n            )\n\n    # deduplicate mentions\n    status.mention_users.set(set(status.mention_users.all()))\n    # create mention notifications\n    for mention_user in status.mention_users.all():\n        if status.reply_parent and mention_user == status.reply_parent.user:\n            continue\n        if mention_user.local:\n            create_notification(\n                mention_user,\n                'MENTION',\n                related_user=user,\n                related_status=status\n            )\n\n    # don't apply formatting to generated notes\n    if not isinstance(status, models.GeneratedNote):\n        status.content = to_markdown(content)\n    # do apply formatting to quotes\n    if hasattr(status, 'quote'):\n        status.quote = to_markdown(status.quote)\n\n    status.save()\n\n    broadcast(user, status.to_create_activity(user), software='bookwyrm')\n\n    # re-format the activity for non-bookwyrm servers\n    remote_activity = status.to_create_activity(user, pure=True)\n    broadcast(user, remote_activity, software='other')\n\n\ndef find_mentions(content):\n    ''' detect @mentions in raw status content '''\n    for match in re.finditer(regex.strict_username, content):\n        username = match.group().strip().split('@')[1:]\n        if len(username) == 1:\n            # this looks like a local user (@user), fill in the domain\n            username.append(DOMAIN)\n        username = '@'.join(username)\n\n        mention_user = handle_remote_webfinger(username)\n        if not mention_user:\n            # we can ignore users we don't know about\n            continue\n        yield (match.group(), mention_user)\n\n\ndef format_links(content):\n    ''' detect and format links '''\n    return re.sub(\n        r'([^(href=\")]|^)(https?:\\/\\/(%s([\\w\\.\\-_\\/+&\\?=:;,])*))' % \\\n                regex.domain,\n        r'\\g<1><a href=\"\\g<2>\">\\g<3></a>',\n        content)\n\ndef to_markdown(content):\n    ''' catch links and convert to markdown '''\n    content = format_links(content)\n    content = markdown(content)\n    # sanitize resulting html\n    sanitizer = InputHtmlParser()\n    sanitizer.feed(content)\n    return sanitizer.get_output()\n\n\ndef handle_favorite(user, status):\n    ''' a user likes a status '''\n    try:\n        favorite = models.Favorite.objects.create(\n            status=status,\n            user=user\n        )\n    except IntegrityError:\n        # you already fav'ed that\n        return\n\n    fav_activity = favorite.to_activity()\n    broadcast(\n        user, fav_activity, privacy='direct', direct_recipients=[status.user])\n    create_notification(\n        status.user,\n        'FAVORITE',\n        related_user=user,\n        related_status=status\n    )\n\n\ndef handle_unfavorite(user, status):\n    ''' a user likes a status '''\n    try:\n        favorite = models.Favorite.objects.get(\n            status=status,\n            user=user\n        )\n    except models.Favorite.DoesNotExist:\n        # can't find that status, idk\n        return\n\n    fav_activity = favorite.to_undo_activity(user)\n    favorite.delete()\n    broadcast(user, fav_activity, direct_recipients=[status.user])\n\n\ndef handle_boost(user, status):\n    ''' a user wishes to boost a status '''\n    # is it boostable?\n    if not status.boostable:\n        return\n\n    if models.Boost.objects.filter(\n            boosted_status=status, user=user).exists():\n        # you already boosted that.\n        return\n    boost = models.Boost.objects.create(\n        boosted_status=status,\n        privacy=status.privacy,\n        user=user,\n    )\n\n    boost_activity = boost.to_activity()\n    broadcast(user, boost_activity)\n\n    create_notification(\n        status.user,\n        'BOOST',\n        related_user=user,\n        related_status=status\n    )\n\n\ndef handle_unboost(user, status):\n    ''' a user regrets boosting a status '''\n    boost = models.Boost.objects.filter(\n        boosted_status=status, user=user\n    ).first()\n    activity = boost.to_undo_activity(user)\n\n    boost.delete()\n    broadcast(user, activity)\n\n\ndef handle_update_book_data(user, item):\n    ''' broadcast the news about our book '''\n    broadcast(user, item.to_update_activity(user))\n\n\ndef handle_update_user(user):\n    ''' broadcast editing a user's profile '''\n    broadcast(user, user.to_update_activity(user))\n", "path": "bookwyrm/outgoing.py"}], "after_files": [{"content": "''' handles all the activity coming out of the server '''\nimport re\n\nfrom django.db import IntegrityError, transaction\nfrom django.http import JsonResponse\nfrom django.shortcuts import get_object_or_404\nfrom django.views.decorators.csrf import csrf_exempt\nfrom django.views.decorators.http import require_GET\nfrom markdown import markdown\nfrom requests import HTTPError\n\nfrom bookwyrm import activitypub\nfrom bookwyrm import models\nfrom bookwyrm.connectors import get_data, ConnectorException\nfrom bookwyrm.broadcast import broadcast\nfrom bookwyrm.sanitize_html import InputHtmlParser\nfrom bookwyrm.status import create_notification\nfrom bookwyrm.status import create_generated_note\nfrom bookwyrm.status import delete_status\nfrom bookwyrm.settings import DOMAIN\nfrom bookwyrm.utils import regex\n\n\n@csrf_exempt\n@require_GET\ndef outbox(request, username):\n    ''' outbox for the requested user '''\n    user = get_object_or_404(models.User, localname=username)\n    filter_type = request.GET.get('type')\n    if filter_type not in models.status_models:\n        filter_type = None\n\n    return JsonResponse(\n        user.to_outbox(**request.GET, filter_type=filter_type),\n        encoder=activitypub.ActivityEncoder\n    )\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' % \\\n            (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                        models.User, link['href']\n                    )\n                except KeyError:\n                    return None\n    return user\n\n\ndef handle_follow(user, to_follow):\n    ''' someone local wants to follow someone '''\n    relationship, _ = models.UserFollowRequest.objects.get_or_create(\n        user_subject=user,\n        user_object=to_follow,\n    )\n    activity = relationship.to_activity()\n    broadcast(user, activity, privacy='direct', direct_recipients=[to_follow])\n\n\ndef handle_unfollow(user, to_unfollow):\n    ''' someone local wants to follow someone '''\n    relationship = models.UserFollows.objects.get(\n        user_subject=user,\n        user_object=to_unfollow\n    )\n    activity = relationship.to_undo_activity(user)\n    broadcast(user, activity, privacy='direct', direct_recipients=[to_unfollow])\n    to_unfollow.followers.remove(user)\n\n\ndef handle_accept(follow_request):\n    ''' send an acceptance message to a follow request '''\n    user = follow_request.user_subject\n    to_follow = follow_request.user_object\n    with transaction.atomic():\n        relationship = models.UserFollows.from_request(follow_request)\n        follow_request.delete()\n        relationship.save()\n\n    activity = relationship.to_accept_activity()\n    broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])\n\n\ndef handle_reject(follow_request):\n    ''' a local user who managed follows rejects a follow request '''\n    user = follow_request.user_subject\n    to_follow = follow_request.user_object\n    activity = follow_request.to_reject_activity()\n    follow_request.delete()\n    broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])\n\n\ndef handle_shelve(user, book, shelf):\n    ''' a local user is getting a book put on their shelf '''\n    # update the database\n    shelve = models.ShelfBook(book=book, shelf=shelf, added_by=user)\n    shelve.save()\n\n    broadcast(user, shelve.to_add_activity(user))\n\n\ndef handle_unshelve(user, book, shelf):\n    ''' a local user is getting a book put on their shelf '''\n    # update the database\n    row = models.ShelfBook.objects.get(book=book, shelf=shelf)\n    activity = row.to_remove_activity(user)\n    row.delete()\n\n    broadcast(user, activity)\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(\n        user,\n        message,\n        mention_books=[book],\n        privacy=privacy\n    )\n    status.save()\n\n    broadcast(user, status.to_create_activity(user))\n\n\ndef handle_imported_book(user, item, include_reviews, privacy):\n    ''' process a goodreads csv and then post about it '''\n    if isinstance(item.book, models.Work):\n        item.book = item.book.default_edition\n    if not item.book:\n        return\n\n    existing_shelf = models.ShelfBook.objects.filter(\n        book=item.book, added_by=user).exists()\n\n    # shelve the book if it hasn't been shelved already\n    if item.shelf and not existing_shelf:\n        desired_shelf = models.Shelf.objects.get(\n            identifier=item.shelf,\n            user=user\n        )\n        shelf_book = models.ShelfBook.objects.create(\n            book=item.book, shelf=desired_shelf, added_by=user)\n        broadcast(user, shelf_book.to_add_activity(user), privacy=privacy)\n\n    for read in item.reads:\n        read.book = item.book\n        read.user = user\n        read.save()\n\n    if include_reviews and (item.rating or item.review):\n        review_title = 'Review of {!r} on Goodreads'.format(\n            item.book.title,\n        ) if item.review else ''\n\n        # we don't know the publication date of the review,\n        # but \"now\" is a bad guess\n        published_date_guess = item.date_read or item.date_added\n        review = models.Review.objects.create(\n            user=user,\n            book=item.book,\n            name=review_title,\n            content=item.review,\n            rating=item.rating,\n            published_date=published_date_guess,\n            privacy=privacy,\n        )\n        # we don't need to send out pure activities because non-bookwyrm\n        # instances don't need this data\n        broadcast(user, review.to_create_activity(user), privacy=privacy)\n\n\ndef handle_delete_status(user, status):\n    ''' delete a status and broadcast deletion to other servers '''\n    delete_status(status)\n    broadcast(user, status.to_delete_activity(user))\n\n\ndef handle_status(user, form):\n    ''' generic handler for statuses '''\n    status = form.save(commit=False)\n    if not status.sensitive and status.content_warning:\n        # the cw text field remains populated when you click \"remove\"\n        status.content_warning = None\n    status.save()\n\n    # inspect the text for user tags\n    content = status.content\n    for (mention_text, mention_user) in find_mentions(content):\n        # add them to status mentions fk\n        status.mention_users.add(mention_user)\n\n        # turn the mention into a link\n        content = re.sub(\n            r'%s([^@]|$)' % mention_text,\n            r'<a href=\"%s\">%s</a>\\g<1>' % \\\n                (mention_user.remote_id, mention_text),\n            content)\n\n    # add reply parent to mentions and notify\n    if status.reply_parent:\n        status.mention_users.add(status.reply_parent.user)\n        for mention_user in status.reply_parent.mention_users.all():\n            status.mention_users.add(mention_user)\n\n        if status.reply_parent.user.local:\n            create_notification(\n                status.reply_parent.user,\n                'REPLY',\n                related_user=user,\n                related_status=status\n            )\n\n    # deduplicate mentions\n    status.mention_users.set(set(status.mention_users.all()))\n    # create mention notifications\n    for mention_user in status.mention_users.all():\n        if status.reply_parent and mention_user == status.reply_parent.user:\n            continue\n        if mention_user.local:\n            create_notification(\n                mention_user,\n                'MENTION',\n                related_user=user,\n                related_status=status\n            )\n\n    # don't apply formatting to generated notes\n    if not isinstance(status, models.GeneratedNote):\n        status.content = to_markdown(content)\n    # do apply formatting to quotes\n    if hasattr(status, 'quote'):\n        status.quote = to_markdown(status.quote)\n\n    status.save()\n\n    broadcast(user, status.to_create_activity(user), software='bookwyrm')\n\n    # re-format the activity for non-bookwyrm servers\n    remote_activity = status.to_create_activity(user, pure=True)\n    broadcast(user, remote_activity, software='other')\n\n\ndef find_mentions(content):\n    ''' detect @mentions in raw status content '''\n    for match in re.finditer(regex.strict_username, content):\n        username = match.group().strip().split('@')[1:]\n        if len(username) == 1:\n            # this looks like a local user (@user), fill in the domain\n            username.append(DOMAIN)\n        username = '@'.join(username)\n\n        mention_user = handle_remote_webfinger(username)\n        if not mention_user:\n            # we can ignore users we don't know about\n            continue\n        yield (match.group(), mention_user)\n\n\ndef format_links(content):\n    ''' detect and format links '''\n    return re.sub(\n        r'([^(href=\")]|^|\\()(https?:\\/\\/(%s([\\w\\.\\-_\\/+&\\?=:;,])*))' % \\\n                regex.domain,\n        r'\\g<1><a href=\"\\g<2>\">\\g<3></a>',\n        content)\n\ndef to_markdown(content):\n    ''' catch links and convert to markdown '''\n    content = format_links(content)\n    content = markdown(content)\n    # sanitize resulting html\n    sanitizer = InputHtmlParser()\n    sanitizer.feed(content)\n    return sanitizer.get_output()\n\n\ndef handle_favorite(user, status):\n    ''' a user likes a status '''\n    try:\n        favorite = models.Favorite.objects.create(\n            status=status,\n            user=user\n        )\n    except IntegrityError:\n        # you already fav'ed that\n        return\n\n    fav_activity = favorite.to_activity()\n    broadcast(\n        user, fav_activity, privacy='direct', direct_recipients=[status.user])\n    create_notification(\n        status.user,\n        'FAVORITE',\n        related_user=user,\n        related_status=status\n    )\n\n\ndef handle_unfavorite(user, status):\n    ''' a user likes a status '''\n    try:\n        favorite = models.Favorite.objects.get(\n            status=status,\n            user=user\n        )\n    except models.Favorite.DoesNotExist:\n        # can't find that status, idk\n        return\n\n    fav_activity = favorite.to_undo_activity(user)\n    favorite.delete()\n    broadcast(user, fav_activity, direct_recipients=[status.user])\n\n\ndef handle_boost(user, status):\n    ''' a user wishes to boost a status '''\n    # is it boostable?\n    if not status.boostable:\n        return\n\n    if models.Boost.objects.filter(\n            boosted_status=status, user=user).exists():\n        # you already boosted that.\n        return\n    boost = models.Boost.objects.create(\n        boosted_status=status,\n        privacy=status.privacy,\n        user=user,\n    )\n\n    boost_activity = boost.to_activity()\n    broadcast(user, boost_activity)\n\n    create_notification(\n        status.user,\n        'BOOST',\n        related_user=user,\n        related_status=status\n    )\n\n\ndef handle_unboost(user, status):\n    ''' a user regrets boosting a status '''\n    boost = models.Boost.objects.filter(\n        boosted_status=status, user=user\n    ).first()\n    activity = boost.to_undo_activity(user)\n\n    boost.delete()\n    broadcast(user, activity)\n\n\ndef handle_update_book_data(user, item):\n    ''' broadcast the news about our book '''\n    broadcast(user, item.to_update_activity(user))\n\n\ndef handle_update_user(user):\n    ''' broadcast editing a user's profile '''\n    broadcast(user, user.to_update_activity(user))\n", "path": "bookwyrm/outgoing.py"}]}