feat: Add "Post to Fediverse"
This commit is contained in:
@@ -8,7 +8,7 @@ from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
|
||||
SpeciesSpecificURL, ImportantLocation
|
||||
SpeciesSpecificURL, ImportantLocation, SocialMediaPost
|
||||
|
||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, Notification
|
||||
@@ -162,6 +162,9 @@ class LocationAdmin(admin.ModelAdmin):
|
||||
ImportantLocationInline,
|
||||
]
|
||||
|
||||
@admin.register(SocialMediaPost)
|
||||
class SocialMediaPostAdmin(admin.ModelAdmin):
|
||||
list_filter = ("platform",)
|
||||
|
||||
admin.site.register(Animal)
|
||||
admin.site.register(Species)
|
||||
|
25
src/fellchensammlung/migrations/0058_socialmediapost.py
Normal file
25
src/fellchensammlung/migrations/0058_socialmediapost.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2.1 on 2025-07-19 17:48
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0057_delete_speciesspecialization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SocialMediaPost',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am')),
|
||||
('platform', models.CharField(choices=[('fediverse', 'Fediverse')], max_length=255, verbose_name='Social Media Platform')),
|
||||
('url', models.URLField(verbose_name='URL')),
|
||||
('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
|
||||
],
|
||||
),
|
||||
]
|
@@ -475,7 +475,6 @@ class AdoptionNotice(models.Model):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_disabled
|
||||
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
@@ -516,6 +515,14 @@ class AdoptionNotice(models.Model):
|
||||
text=text,
|
||||
title=notification_title)
|
||||
|
||||
def last_posted(self, platform=None):
|
||||
if platform is None:
|
||||
last_post = SocialMediaPost.objects.filter(adoption_notice=self).order_by('-created_at').first()
|
||||
else:
|
||||
last_post = SocialMediaPost.objects.filter(adoption_notice=self, platform=platform).order_by(
|
||||
'-created_at').first()
|
||||
return last_post.created_at
|
||||
|
||||
|
||||
class AdoptionNoticeStatus(models.Model):
|
||||
"""
|
||||
@@ -1038,3 +1045,24 @@ class SpeciesSpecificURL(models.Model):
|
||||
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
|
||||
verbose_name=_("Tierschutzorganisation"))
|
||||
url = models.URLField(verbose_name=_("Tierartspezifische URL"))
|
||||
|
||||
|
||||
class PlatformChoices(models.TextChoices):
|
||||
FEDIVERSE = "fediverse", _("Fediverse")
|
||||
|
||||
|
||||
class SocialMediaPost(models.Model):
|
||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
||||
platform = models.CharField(max_length=255, verbose_name=_("Social Media Platform"),
|
||||
choices=PlatformChoices.choices)
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
|
||||
url = models.URLField(verbose_name=_("URL"))
|
||||
|
||||
@staticmethod
|
||||
def get_an_to_post():
|
||||
adoption_notices_without_post = AdoptionNotice.objects.filter(socialmediapost__isnull=True,
|
||||
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
|
||||
return adoption_notices_without_post.first()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.platform} - {self.adoption_notice}"
|
||||
|
@@ -0,0 +1,14 @@
|
||||
{% extends "fellchensammlung/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title is-1">404 Not Found</h1>
|
||||
<p>
|
||||
{% blocktranslate %}
|
||||
Diese Seite existiert nicht.
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
{% endblock %}
|
@@ -7,13 +7,52 @@
|
||||
<div class="block">
|
||||
<h1 class="title is-1">{% translate 'Moderationstools' %}</h1>
|
||||
<div class="block">
|
||||
<a class="button is-primary is-fullwidth" href="{% url 'modqueue' %}">{% translate 'Moderationswarteschlange' %}</a>
|
||||
<a class="button is-primary is-fullwidth"
|
||||
href="{% url 'modqueue' %}">{% translate 'Moderationswarteschlange' %}</a>
|
||||
</div>
|
||||
<div class="block">
|
||||
<a class="button is-primary is-fullwidth" href="{% url 'updatequeue' %}">{% translate 'Up-To-Date Check' %}</a>
|
||||
<a class="button is-primary is-fullwidth"
|
||||
href="{% url 'updatequeue' %}">{% translate 'Up-To-Date Check' %}</a>
|
||||
</div>
|
||||
<div class="block">
|
||||
<a class="button is-primary is-fullwidth" href="{% url 'organization-check' %}">{% translate 'Organisations Check' %}</a>
|
||||
<a class="button is-primary is-fullwidth"
|
||||
href="{% url 'organization-check' %}">{% translate 'Organisations Check' %}</a>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
{% if action_was_posting %}
|
||||
{% if posted_successfully %}
|
||||
<div class="message is-success">
|
||||
<div class="message-header">
|
||||
{% translate 'Vermittlung gepostet' %}
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{% blocktranslate with post_url=post.url %}
|
||||
Link zum Post: <a href={{ post_url }}>{{ post_url }}</a>
|
||||
{% endblocktranslate %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="message is-danger">
|
||||
<div class="message-header">
|
||||
{% translate 'Vermittlung konnte nicht gepostet werden' %}
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{{ error_message }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<form class="cell" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="post_to_fedi">
|
||||
<button class="button is-fullwidth is-warning is-primary" type="submit" id="submit">
|
||||
<i class="fa-solid fa-bullhorn fa-fw"></i> {% translate "Vermittlung ins Fediverse posten" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
95
src/fellchensammlung/tools/fedi.py
Normal file
95
src/fellchensammlung/tools/fedi.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from fellchensammlung.models import SocialMediaPost, PlatformChoices
|
||||
from notfellchen import settings
|
||||
|
||||
|
||||
class FediClient:
|
||||
def __init__(self, access_token, api_base_url):
|
||||
"""
|
||||
:param access_token: Your server API access token.
|
||||
:param api_base_url: The base URL of the Fediverse instance (e.g., 'https://gay-pirate-assassins.de').
|
||||
"""
|
||||
self.access_token = access_token
|
||||
self.api_base_url = api_base_url.rstrip('/')
|
||||
self.headers = {
|
||||
'Authorization': f'Bearer {self.access_token}',
|
||||
}
|
||||
|
||||
def upload_media(self, image_path, alt_text):
|
||||
"""
|
||||
Uploads media (image) to the server and returns the media ID.
|
||||
:param image_path: Path to the image file to upload.
|
||||
:param alt_text: Description (alt text) for the image.
|
||||
:return: The media ID of the uploaded image.
|
||||
"""
|
||||
|
||||
media_endpoint = f'{self.api_base_url}/api/v2/media'
|
||||
|
||||
with open(image_path, 'rb') as image_file:
|
||||
files = {
|
||||
'file': image_file,
|
||||
'description': (None, alt_text)
|
||||
}
|
||||
response = requests.post(media_endpoint, headers=self.headers, files=files)
|
||||
|
||||
# Raise exception if upload fails
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse and return the media ID from the response
|
||||
media_id = response.json().get('id')
|
||||
return media_id
|
||||
|
||||
def post_status(self, status, media_ids=None):
|
||||
"""
|
||||
Posts a status to Mastodon with optional media.
|
||||
:param status: The text of the status to post.
|
||||
:param media_ids: A list of media IDs to attach to the status (optional).
|
||||
:return: The response from the Mastodon API.
|
||||
"""
|
||||
status_endpoint = f'{self.api_base_url}/api/v1/statuses'
|
||||
|
||||
payload = {
|
||||
'status': status,
|
||||
'media_ids[]': media_ids if media_ids else []
|
||||
}
|
||||
response = requests.post(status_endpoint, headers=self.headers, data=payload)
|
||||
|
||||
# Raise exception if posting fails
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
def post_status_with_images(self, status, images):
|
||||
"""
|
||||
Uploads one or more image, then posts a status with that images and alt text.
|
||||
:param status: The text of the status.
|
||||
:param image_paths: The paths to the image file.
|
||||
:param alt_text: The alt text for the image.
|
||||
:return: The response from the Mastodon API.
|
||||
"""
|
||||
media_ids = []
|
||||
for image in images:
|
||||
# Upload the image and get the media ID
|
||||
media_ids = self.upload_media(f"{settings.MEDIA_ROOT}/{image.image}", image.alt_text)
|
||||
|
||||
# Post the status with the uploaded image's media ID
|
||||
return self.post_status(status, media_ids=media_ids)
|
||||
|
||||
|
||||
def post_an_to_fedi(adoption_notice):
|
||||
client = FediClient(settings.fediverse_access_token, settings.fediverse_api_base_url)
|
||||
|
||||
status_text = adoption_notice.name
|
||||
images = adoption_notice.get_photos()
|
||||
|
||||
if images is not None:
|
||||
response = client.post_status_with_images(status_text, images)
|
||||
else:
|
||||
response = client.post_status(status_text)
|
||||
logging.info(response)
|
||||
post = SocialMediaPost.objects.create(adoption_notice=adoption_notice,
|
||||
platform=PlatformChoices.FEDIVERSE,
|
||||
url=response['url'], )
|
||||
return post
|
@@ -14,6 +14,7 @@ from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.serializers import serialize
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import json
|
||||
import requests
|
||||
|
||||
from .mail import notify_mods_new_report
|
||||
from notfellchen import settings
|
||||
@@ -22,11 +23,12 @@ from fellchensammlung import logger
|
||||
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
||||
User, Location, AdoptionNoticeStatus, Subscriptions, Notification, RescueOrganization, \
|
||||
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \
|
||||
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices
|
||||
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost
|
||||
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
|
||||
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment
|
||||
from .models import Language, Announcement
|
||||
from .tools import i18n
|
||||
from .tools.fedi import post_an_to_fedi
|
||||
from .tools.geo import GeoAPI, zoom_level_for_radius
|
||||
from .tools.metrics import gather_metrics_data
|
||||
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
||||
@@ -864,7 +866,31 @@ def rescue_organization_check_dq(request):
|
||||
|
||||
@user_passes_test(user_is_trust_level_or_above)
|
||||
def moderation_tools_overview(request):
|
||||
return render(request, 'fellchensammlung/mod-tool-overview.html')
|
||||
context = None
|
||||
if request.method == "POST":
|
||||
action = request.POST.get("action")
|
||||
if action == "post_to_fedi":
|
||||
adoption_notice = SocialMediaPost.get_an_to_post()
|
||||
if adoption_notice is not None:
|
||||
try:
|
||||
post = post_an_to_fedi(adoption_notice)
|
||||
context = {"action_was_posting": True, "post": post, "posted_successfully": True}
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logging.error(f"Could not post fediverse post: {e}")
|
||||
context = {"action_was_posting": True,
|
||||
"posted_successfully": False,
|
||||
"error_message": _("Verbindungsfehler. Vermittlung wurde nicht gepostet")}
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logging.error(f"Could not post fediverse post: {e}")
|
||||
context = {"action_was_posting": True,
|
||||
"posted_successfully": False,
|
||||
"error_message": _("Fehler beim Posten. Vermittlung wurde nicht gepostet. Das kann "
|
||||
"z.B. an falschen Zugangsdaten liegen. Kontaktieren einen Admin.")}
|
||||
else:
|
||||
context = {"action_was_posting": True,
|
||||
"posted_successfully": False,
|
||||
"error_message": _("Keine Vermittlung zum Posten gefunden.")}
|
||||
return render(request, 'fellchensammlung/mod-tool-overview.html', context=context)
|
||||
|
||||
|
||||
def deactivate_an(request, adoption_notice_id):
|
||||
|
@@ -118,6 +118,12 @@ else:
|
||||
EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
|
||||
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
|
||||
|
||||
""" Fediverse """
|
||||
fediverse_enabled = config.get('fediverse', 'enabled', fallback=False)
|
||||
if fediverse_enabled:
|
||||
fediverse_api_base_url = config.get('fediverse', 'api_base_url')
|
||||
fediverse_access_token = config.get('fediverse', 'access_token')
|
||||
|
||||
"""USER MANAGEMENT"""
|
||||
AUTH_USER_MODEL = "fellchensammlung.User"
|
||||
ACCOUNT_ACTIVATION_DAYS = 7 # One-week activation window
|
||||
|
Reference in New Issue
Block a user