kopia lustrzana https://gitlab.com/marnanel/chapeau
Porównaj commity
8 Commity
aee804b6ae
...
1c3a4733d8
Autor | SHA1 | Data |
---|---|---|
Marnanel Thurman | 1c3a4733d8 | |
Marnanel Thurman | 44b4aaaad7 | |
Marnanel Thurman | c8a5b5308b | |
Marnanel Thurman | 94309cc77d | |
Marnanel Thurman | b00f844d84 | |
Marnanel Thurman | 8a0deb6c93 | |
Marnanel Thurman | 4d63fd669d | |
Thomas Thurman | 07875e03ab |
|
@ -1,5 +1,3 @@
|
|||
from django.dispatch import Signal
|
||||
|
||||
received = Signal(
|
||||
providing_args=[
|
||||
])
|
||||
received = Signal()
|
||||
|
|
|
@ -19,4 +19,5 @@ urlpatterns = [
|
|||
path('users/<str:username>/following', bowler_pub_views.FollowingView.as_view()),
|
||||
path('users/<str:username>/featured', bowler_pub_views.FeaturedView.as_view()),
|
||||
path('sharedInbox', bowler_pub_views.InboxView.as_view()),
|
||||
path('inbox', bowler_pub_views.InboxView.as_view()), # config error, marnanel.org specific
|
||||
]
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import logging
|
||||
import sys
|
||||
import django
|
||||
|
||||
logger = logging.getLogger('kepi')
|
||||
|
||||
class KepiTestCase(django.test.TestCase):
|
||||
"""
|
||||
A test case.
|
||||
|
||||
It turns on logging to stdout.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._logging_stream_handler = logging.StreamHandler(sys.stdout)
|
||||
logger.addHandler(self._logging_stream_handler)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
logger.removeHandler(self._logging_stream_handler)
|
|
@ -0,0 +1,5 @@
|
|||
import django.apps
|
||||
|
||||
class SombreroApiConfig(django.apps.AppConfig):
|
||||
name = 'kepi.sombrero_sendpub'
|
||||
default_auto_field = 'django.db.models.AutoField'
|
|
@ -8,12 +8,12 @@ import logging
|
|||
logger = logging.getLogger(name="kepi")
|
||||
|
||||
from unittest import skip
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
from kepi.sombrero_sendpub.fetch import fetch
|
||||
from kepi.trilby_api.models import RemotePerson, Person, Status
|
||||
from kepi.trilby_api.tests import create_local_person
|
||||
from kepi.sombrero_sendpub.collections import Collection
|
||||
from kepi.kepi.testing import KepiTestCase
|
||||
from . import suppress_thread_exceptions
|
||||
import httpretty
|
||||
import requests
|
||||
|
@ -166,7 +166,7 @@ EXAMPLE_COMPLEX_COLLECTION_PAGE_2 = """{
|
|||
EXAMPLE_COMPLEX_COLLECTION_URL,
|
||||
)
|
||||
|
||||
class TestFetchRemoteUser(TestCase):
|
||||
class TestFetchRemoteUser(KepiTestCase):
|
||||
|
||||
@httpretty.activate
|
||||
def test_fetch(self):
|
||||
|
@ -527,9 +527,10 @@ class TestFetchRemoteUser(TestCase):
|
|||
len(EXAMPLE_COMPLEX_COLLECTION_MEMBERS),
|
||||
msg="Collection has a length")
|
||||
|
||||
class TestFetchLocalUser(TestCase):
|
||||
class TestFetchLocalUser(KepiTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._alice = create_local_person(
|
||||
name = 'alice',
|
||||
)
|
||||
|
@ -588,5 +589,5 @@ class TestFetchLocalUser(TestCase):
|
|||
None,
|
||||
)
|
||||
|
||||
class TestFetchStatus(TestCase):
|
||||
class TestFetchStatus(KepiTestCase):
|
||||
pass
|
||||
|
|
|
@ -2,4 +2,11 @@
|
|||
|
||||
{% block content %}
|
||||
{{bio}}
|
||||
{% endblock %}
|
||||
|
||||
{% for s in statuses %}
|
||||
{{s}}
|
||||
<p xml:lang="{{ s.object__find.language }}">
|
||||
{{ s.object__find.content | escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
<{% endblock %}
|
||||
|
|
|
@ -13,6 +13,7 @@ from django.conf import settings
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.decorators import method_decorator
|
||||
import kepi.trilby_api.models as trilby_models
|
||||
import kepi.tophat_ui.models as tophat_models
|
||||
|
||||
class RootPage(View):
|
||||
|
||||
|
@ -46,6 +47,10 @@ class UserPage(View):
|
|||
local_user__username = username,
|
||||
)
|
||||
|
||||
statuses = trilby_models.Status.objects.filter(
|
||||
account = user,
|
||||
)
|
||||
|
||||
result = render(
|
||||
request=request,
|
||||
template_name='user-page.html',
|
||||
|
@ -53,6 +58,7 @@ class UserPage(View):
|
|||
'title': user.username,
|
||||
'subtitle': user.display_name,
|
||||
'bio': user.note,
|
||||
'statuses': statuses,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
import django.apps
|
||||
|
||||
|
||||
class TrilbyApiConfig(AppConfig):
|
||||
class TrilbyApiConfig(django.apps.AppConfig):
|
||||
name = 'kepi.trilby_api'
|
||||
default_auto_field = 'django.db.models.AutoField'
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
# Copyright (c) 2018-2021 Marnanel Thurman.
|
||||
# Licensed under the GNU Public License v2.
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.test import Client
|
||||
from kepi.kepi.testing import KepiTestCase
|
||||
from rest_framework.test import force_authenticate, APIClient
|
||||
from kepi.trilby_api.models import *
|
||||
from django.conf import settings
|
||||
|
@ -65,7 +66,7 @@ STATUS_EXPECTED = {
|
|||
'pinned': False,
|
||||
}
|
||||
|
||||
class TrilbyTestCase(TestCase):
|
||||
class TrilbyTestCase(KepiTestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
from kepi.trilby_api.utils import *
|
||||
from kepi.kepi.testing import KepiTestCase
|
||||
|
||||
class Tests(TestCase):
|
||||
class Tests(KepiTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
settings.KEPI['LOCAL_OBJECT_HOSTNAME'] = 'testserver'
|
||||
|
||||
def test_is_local_user_url(self):
|
||||
|
|
|
@ -5,48 +5,48 @@
|
|||
# Licensed under the GNU Public License v2.
|
||||
|
||||
from django.urls import path
|
||||
from .views import *
|
||||
import kepi.trilby_api.views as views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path('api/v1/instance', Instance.as_view()),
|
||||
path('api/v1/instance/', Instance.as_view()), # keep tootstream happy
|
||||
path('api/v1/apps', Apps.as_view()),
|
||||
path('api/v1/instance', views.Instance.as_view()),
|
||||
path('api/v1/instance/', views.Instance.as_view()), # keep tootstream happy
|
||||
path('api/v1/apps', views.Apps.as_view()),
|
||||
|
||||
path('api/v1/accounts/verify_credentials', VerifyCredentials.as_view()),
|
||||
path('api/v1/accounts/verify_credentials', views.VerifyCredentials.as_view()),
|
||||
path('api/v1/accounts/update_credentials',
|
||||
UpdateCredentials.as_view()),
|
||||
views.UpdateCredentials.as_view()),
|
||||
|
||||
path('api/v1/accounts/search', AccountsSearch.as_view()),
|
||||
path('api/v1/accounts/search', views.AccountsSearch.as_view()),
|
||||
|
||||
path('api/v1/accounts/<user>', User.as_view()),
|
||||
path('api/v1/accounts/<user>/statuses', Statuses.as_view()),
|
||||
path('api/v1/accounts/<user>/following', Following.as_view()),
|
||||
path('api/v1/accounts/<user>/followers', Followers.as_view()),
|
||||
path('api/v1/accounts/<user>/follow', Follow.as_view()),
|
||||
path('api/v1/accounts/<user>/unfollow', Unfollow.as_view()),
|
||||
path('api/v1/accounts/<user>', views.User.as_view()),
|
||||
path('api/v1/accounts/<user>/statuses', views.Statuses.as_view()),
|
||||
path('api/v1/accounts/<user>/following', views.Following.as_view()),
|
||||
path('api/v1/accounts/<user>/followers', views.Followers.as_view()),
|
||||
path('api/v1/accounts/<user>/follow', views.FollowUser.as_view()),
|
||||
path('api/v1/accounts/<user>/unfollow', views.UnfollowUser.as_view()),
|
||||
|
||||
path('api/v1/statuses', Statuses.as_view()),
|
||||
path('api/v1/statuses/<status>', SpecificStatus.as_view()),
|
||||
path('api/v1/statuses/<status>/context', StatusContext.as_view()),
|
||||
path('api/v1/statuses', views.Statuses.as_view()),
|
||||
path('api/v1/statuses/<status>', views.SpecificStatus.as_view()),
|
||||
path('api/v1/statuses/<status>/context', views.StatusContext.as_view()),
|
||||
|
||||
# Favourite, aka like
|
||||
path('api/v1/statuses/<status>/favourite', Favourite.as_view()),
|
||||
path('api/v1/statuses/<status>/unfavourite', Unfavourite.as_view()),
|
||||
path('api/v1/statuses/<status>/favourited_by', StatusFavouritedBy.as_view()),
|
||||
path('api/v1/statuses/<status>/favourite', views.Favourite.as_view()),
|
||||
path('api/v1/statuses/<status>/unfavourite', views.Unfavourite.as_view()),
|
||||
path('api/v1/statuses/<status>/favourited_by', views.StatusFavouritedBy.as_view()),
|
||||
|
||||
# Reblog, aka boost
|
||||
path('api/v1/statuses/<status>/reblog', Reblog.as_view()),
|
||||
path('api/v1/statuses/<status>/unreblog', Unreblog.as_view()),
|
||||
path('api/v1/statuses/<status>/reblogged_by', StatusRebloggedBy.as_view()),
|
||||
path('api/v1/statuses/<status>/reblog', views.Reblog.as_view()),
|
||||
path('api/v1/statuses/<status>/unreblog', views.Unreblog.as_view()),
|
||||
path('api/v1/statuses/<status>/reblogged_by', views.StatusRebloggedBy.as_view()),
|
||||
|
||||
path('api/v1/notifications', Notifications.as_view()),
|
||||
path('api/v1/filters', Filters.as_view()),
|
||||
path('api/v1/custom_emojis', Emojis.as_view()),
|
||||
path('api/v1/timelines/public', PublicTimeline.as_view()),
|
||||
path('api/v1/timelines/home', HomeTimeline.as_view()),
|
||||
path('api/v1/notifications', views.Notifications.as_view()),
|
||||
path('api/v1/filters', views.Filters.as_view()),
|
||||
path('api/v1/custom_emojis', views.Emojis.as_view()),
|
||||
path('api/v1/timelines/public', views.PublicTimeline.as_view()),
|
||||
path('api/v1/timelines/home', views.HomeTimeline.as_view()),
|
||||
|
||||
path('api/v1/search', Search.as_view()),
|
||||
path('api/v1/search', views.Search.as_view()),
|
||||
|
||||
path('users/<username>/feed', UserFeed.as_view()),
|
||||
path('users/<username>/feed', views.UserFeed.as_view()),
|
||||
]
|
||||
|
|
|
@ -1,73 +1,38 @@
|
|||
# trilby_api/views/__init__.py
|
||||
#
|
||||
# Part of kepi.
|
||||
# Copyright (c) 2018-2021 Marnanel Thurman.
|
||||
# Licensed under the GNU Public License v2.
|
||||
|
||||
from .other import \
|
||||
Instance, \
|
||||
Emojis, \
|
||||
Filters, \
|
||||
Search, \
|
||||
AccountsSearch
|
||||
|
||||
from .statuses import \
|
||||
Favourite, Unfavourite, \
|
||||
Reblog, Unreblog, \
|
||||
SpecificStatus, \
|
||||
Statuses, \
|
||||
StatusContext, \
|
||||
StatusFavouritedBy, \
|
||||
StatusRebloggedBy, \
|
||||
Notifications
|
||||
|
||||
from .persons import \
|
||||
Follow, Unfollow, \
|
||||
UpdateCredentials, \
|
||||
VerifyCredentials,\
|
||||
User, \
|
||||
Followers, Following
|
||||
|
||||
from .oauth import \
|
||||
Apps, \
|
||||
fix_oauth2_redirects
|
||||
|
||||
from .timelines import \
|
||||
PublicTimeline, \
|
||||
HomeTimeline, \
|
||||
UserFeed
|
||||
from .oauth import *
|
||||
from .other import *
|
||||
from .persons import *
|
||||
from .statuses import *
|
||||
from .timelines import *
|
||||
|
||||
__all__ = [
|
||||
# other
|
||||
'Instance',
|
||||
'Emojis',
|
||||
'Filters',
|
||||
'Search',
|
||||
'AbstractTimeline',
|
||||
'AccountsSearch',
|
||||
|
||||
# statuses
|
||||
'Favourite', 'Unfavourite',
|
||||
'Reblog', 'Unreblog',
|
||||
'Apps',
|
||||
'DoSomethingWithPerson',
|
||||
'DoSomethingWithStatus',
|
||||
'Emojis',
|
||||
'Favourite',
|
||||
'Filters',
|
||||
'FollowUser',
|
||||
'Followers',
|
||||
'Followers_or_Following',
|
||||
'Following',
|
||||
'HomeTimeline',
|
||||
'Instance',
|
||||
'Notifications',
|
||||
'PublicTimeline',
|
||||
'Reblog',
|
||||
'Search',
|
||||
'SpecificStatus',
|
||||
'Statuses',
|
||||
'StatusContext',
|
||||
'Statuses',
|
||||
'StatusFavouritedBy',
|
||||
'StatusRebloggedBy',
|
||||
'Notifications',
|
||||
|
||||
# persons
|
||||
'Follow', 'Unfollow',
|
||||
'Unfavourite',
|
||||
'UnfollowUser',
|
||||
'Unreblog',
|
||||
'UpdateCredentials',
|
||||
'VerifyCredentials',
|
||||
'User',
|
||||
'Followers', 'Following',
|
||||
|
||||
# oauth
|
||||
'Apps',
|
||||
'fix_oauth2_redirects',
|
||||
|
||||
# timelines
|
||||
'PublicTimeline',
|
||||
'HomeTimeline',
|
||||
'UserFeed',
|
||||
'VerifyCredentials',
|
||||
]
|
||||
|
|
|
@ -71,7 +71,7 @@ class DoSomethingWithPerson(generics.GenericAPIView):
|
|||
reason = 'Done',
|
||||
)
|
||||
|
||||
class Follow(DoSomethingWithPerson):
|
||||
class FollowUser(DoSomethingWithPerson):
|
||||
|
||||
def _do_something_with(self, the_person, request):
|
||||
|
||||
|
@ -119,7 +119,7 @@ class Follow(DoSomethingWithPerson):
|
|||
except IntegrityError:
|
||||
logger.info(' -- not creating a follow; it already exists')
|
||||
|
||||
class Unfollow(DoSomethingWithPerson):
|
||||
class UnfollowUser(DoSomethingWithPerson):
|
||||
|
||||
def _do_something_with(self, the_person, request):
|
||||
|
||||
|
@ -142,76 +142,6 @@ class Unfollow(DoSomethingWithPerson):
|
|||
logger.info(' -- not unfollowing; they weren\'t following '+\
|
||||
'in the first place')
|
||||
|
||||
class UpdateCredentials(generics.GenericAPIView):
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
|
||||
if request.user is None:
|
||||
logger.debug(' -- user not logged in')
|
||||
return error_response(401, 'Not logged in')
|
||||
|
||||
who = request.user.localperson
|
||||
|
||||
# The Mastodon spec doesn't say what to do
|
||||
# if the user submits field names which don't
|
||||
# exist!
|
||||
|
||||
unknown_fields = []
|
||||
|
||||
# FIXME: the data in "v" needs cleaning.
|
||||
|
||||
logger.info('-- updating user: %s', who)
|
||||
|
||||
for f,v in request.data.items():
|
||||
|
||||
logger.info(' -- setting %s = %s', f, v)
|
||||
|
||||
if f=='discoverable':
|
||||
raise Http404("discoverable is not yet supported")
|
||||
elif f=='bot':
|
||||
who.bot = v
|
||||
elif f=='display_name':
|
||||
who.display_name = v
|
||||
elif f=='note':
|
||||
who.note = v
|
||||
elif f=='avatar':
|
||||
raise Http404("images are not yet supported")
|
||||
elif f=='header':
|
||||
raise Http404("images are not yet supported")
|
||||
elif f=='locked':
|
||||
who.locked = v
|
||||
elif f=='source[privacy]':
|
||||
who.default_visibility = v
|
||||
elif f=='source[sensitive]':
|
||||
who.default_sensitive = v
|
||||
elif f=='source[language]':
|
||||
who.language = v
|
||||
elif f=='fields_attributes':
|
||||
raise Http404("fields are not yet supported")
|
||||
else:
|
||||
logger.info(' -- field does not exist')
|
||||
unknown_fields.append(f)
|
||||
|
||||
if unknown_fields:
|
||||
logger.info(' -- aborting because of unknown fields')
|
||||
raise Http404(f"some fields do not exist: {unknown_fields}")
|
||||
|
||||
who.save()
|
||||
logger.info(' -- done.')
|
||||
|
||||
serializer = UserSerializerWithSource(
|
||||
who,
|
||||
context = {
|
||||
'request': request,
|
||||
},
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
serializer.data,
|
||||
status = 200,
|
||||
reason = 'Done',
|
||||
)
|
||||
|
||||
###########################
|
||||
|
||||
class VerifyCredentials(generics.GenericAPIView):
|
||||
|
|
|
@ -183,117 +183,6 @@ class Unreblog(DoSomethingWithStatus):
|
|||
|
||||
###########################
|
||||
|
||||
class DoSomethingWithPerson(generics.GenericAPIView):
|
||||
|
||||
serializer_class = UserSerializer
|
||||
queryset = trilby_models.Person.objects.all()
|
||||
|
||||
def _do_something_with(self, the_person, request):
|
||||
raise NotImplementedError()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
if request.user is None:
|
||||
logger.debug(' -- user not logged in')
|
||||
return error_response(401, 'Not logged in')
|
||||
|
||||
try:
|
||||
the_person = get_object_or_404(
|
||||
self.get_queryset(),
|
||||
id = int(kwargs['user']),
|
||||
)
|
||||
except ValueError:
|
||||
return error_response(404, 'Non-decimal ID')
|
||||
|
||||
result = self._do_something_with(the_person, request)
|
||||
|
||||
if result is None:
|
||||
result = the_person
|
||||
|
||||
serializer = UserSerializer(
|
||||
result,
|
||||
context = {
|
||||
'request': request,
|
||||
},
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
serializer.data,
|
||||
status = 200,
|
||||
reason = 'Done',
|
||||
)
|
||||
|
||||
class Follow(DoSomethingWithPerson):
|
||||
|
||||
def _do_something_with(self, the_person, request):
|
||||
|
||||
try:
|
||||
|
||||
if the_person.auto_follow:
|
||||
offer = None
|
||||
else:
|
||||
number = random.randint(0, 0xffffffff)
|
||||
offer = uri_to_url(settings.KEPI['FOLLOW_REQUEST_LINK'] % {
|
||||
'username': request.user.username,
|
||||
'number': number,
|
||||
})
|
||||
|
||||
follow = trilby_models.Follow(
|
||||
follower = request.user.localperson,
|
||||
following = the_person,
|
||||
offer = offer,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
follow.save(
|
||||
send_signal = True,
|
||||
)
|
||||
|
||||
logger.info(' -- follow: %s', follow)
|
||||
logger.debug(' -- offer ID: %s', offer)
|
||||
|
||||
if the_person.auto_follow:
|
||||
follow_back = trilby_models.Follow(
|
||||
follower = the_person,
|
||||
following = request.user.localperson,
|
||||
offer = None,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
follow_back.save(
|
||||
send_signal = True,
|
||||
)
|
||||
|
||||
logger.info(' -- follow back: %s', follow_back)
|
||||
|
||||
return the_person
|
||||
|
||||
except IntegrityError:
|
||||
logger.info(' -- not creating a follow; it already exists')
|
||||
|
||||
class Unfollow(DoSomethingWithPerson):
|
||||
|
||||
def _do_something_with(self, the_person, request):
|
||||
|
||||
try:
|
||||
follow = trilby_models.Follow.objects.get(
|
||||
follower = request.user.localperson,
|
||||
following = the_person,
|
||||
)
|
||||
|
||||
logger.info(' -- unfollowing: %s', follow)
|
||||
|
||||
with transaction.atomic():
|
||||
follow.delete(
|
||||
send_signal = True,
|
||||
)
|
||||
|
||||
return the_person
|
||||
|
||||
except trilby_models.Follow.DoesNotExist:
|
||||
logger.info(' -- not unfollowing; they weren\'t following '+\
|
||||
'in the first place')
|
||||
|
||||
class SpecificStatus(generics.GenericAPIView):
|
||||
|
||||
queryset = trilby_models.Status.objects.filter(remote_url=None)
|
||||
|
|
Ładowanie…
Reference in New Issue