generated from freelawproject/new-project-template
-
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #618 from freelawproject/threads-support
feat(channels): Add support for Threads
- Loading branch information
Showing
23 changed files
with
722 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# Generated by Django 5.1.1 on 2024-10-28 17:23 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("channel", "0008_alter_channel_service_alter_post_object_id"), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterField( | ||
model_name="channel", | ||
name="service", | ||
field=models.PositiveSmallIntegerField( | ||
choices=[ | ||
(1, "Twitter"), | ||
(2, "Mastodon"), | ||
(3, "Bluesky"), | ||
(4, "Threads"), | ||
], | ||
help_text="Type of the service", | ||
), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{% extends "post.html" %} | ||
{% block title %}Threads Authorization Code{% endblock %} | ||
|
||
{% block post_title %}Threads Onboarding{% endblock %} | ||
|
||
{% block post_content %} | ||
<p class="lead">You're almost done! Use the code below in your CLI to complete the setup for this Threads account.</p> | ||
|
||
<h3>Authorization Code:</h3> | ||
<div class="flex bg-gray-200 rounded-lg p-5"> | ||
<p class="w-full break-all">{{code}}</p> | ||
</div> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,17 @@ | ||
from django.urls import path | ||
|
||
from .api_views import receive_mastodon_push | ||
from .views import threads_callback | ||
|
||
urlpatterns = [ | ||
path( | ||
"webhooks/mastodon/", | ||
receive_mastodon_push, | ||
name="mastodon_push_handler", | ||
), | ||
path( | ||
"threads_callback/", | ||
threads_callback, | ||
name="threads_code_display", | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import logging | ||
|
||
from bc.core.utils.images import TextImage | ||
|
||
from .alt_text_utils import text_image_alt_text, thumb_num_alt_text | ||
from .threads_api.client import ThreadsAPI | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class ThreadsConnector: | ||
""" | ||
A connector for interfacing with the Threads API, which complies with | ||
the BaseAPIConnector protocol. | ||
""" | ||
|
||
def __init__( | ||
self, account: str, account_id: str, access_token: str | ||
) -> None: | ||
self.account = account | ||
self.account_id = account_id | ||
self.access_token = access_token | ||
self.api = self.get_api_object() | ||
|
||
def get_api_object(self, _version=None) -> ThreadsAPI: | ||
""" | ||
Returns an instance of the ThreadsAPI class. | ||
""" | ||
api = ThreadsAPI( | ||
self.account_id, | ||
self.access_token, | ||
) | ||
return api | ||
|
||
def upload_media(self, media: bytes, _alt_text=None) -> str: | ||
""" | ||
Uploads media to public storage for Threads API compatibility. | ||
Since Threads API requires media to be accessible via public URL, | ||
this method resizes the image as needed, uploads it to S3, and | ||
returns the public URL. | ||
Args: | ||
media (bytes): The image bytes to be uploaded. | ||
_alt_text (str, optional): Alternative text for accessibility | ||
(not currently used, required by protocol). | ||
Returns: | ||
str: Public URL of the uploaded image. | ||
""" | ||
return self.api.resize_and_upload_to_public_storage(media) | ||
|
||
def add_status( | ||
self, | ||
message: str, | ||
text_image: TextImage | None = None, | ||
thumbnails: list[bytes] | None = None, | ||
) -> str: | ||
""" | ||
Creates and publishes a new status update on Threads. | ||
This method determines the type of post (text-only, single image, | ||
or carousel) based on the provided media. If multiple images are | ||
provided, a carousel post is created. Otherwise, it creates a | ||
single image or text-only post. | ||
Args: | ||
message (str): The text content of the status. | ||
text_image (TextImage | None): An optional main image with text. | ||
thumbnails (list[bytes] | None): Optional list of thumbnails for | ||
a carousel post. | ||
Returns: | ||
str: The ID of the published status. | ||
""" | ||
media: list[str] = [] | ||
|
||
# Count media elements to determine type of post: | ||
multiple_thumbnails = thumbnails is not None and len(thumbnails) > 1 | ||
text_image_and_thumbnail = ( | ||
thumbnails is not None | ||
and len(thumbnails) > 0 | ||
and text_image is not None | ||
) | ||
is_carousel_item = multiple_thumbnails or text_image_and_thumbnail | ||
|
||
if text_image: | ||
image_url = self.upload_media(text_image.to_bytes()) | ||
item_container_id = self.api.create_image_container( | ||
image_url, | ||
message, | ||
text_image_alt_text(text_image.description), | ||
is_carousel_item, | ||
) | ||
if item_container_id: | ||
media.append(item_container_id) | ||
|
||
if thumbnails: | ||
for idx, thumbnail in enumerate(thumbnails): | ||
thumbnail_url = self.upload_media(thumbnail) | ||
item_container_id = self.api.create_image_container( | ||
thumbnail_url, | ||
message, | ||
thumb_num_alt_text(idx), | ||
is_carousel_item, | ||
) | ||
if not item_container_id: | ||
continue | ||
media.append(item_container_id) | ||
|
||
# Determine container id to be published based on media count: | ||
if len(media) > 1: | ||
# Carousel post (multiple images) | ||
container_id = self.api.create_carousel_container(media, message) | ||
elif len(media) == 1: | ||
# Single image post | ||
container_id = media[0] | ||
else: | ||
# Text-only post | ||
container_id = self.api.create_text_only_container(message) | ||
|
||
if container_id is None: | ||
logger.error( | ||
"ThreadsConnector could not get container to publish!" | ||
) | ||
return "" | ||
|
||
return self.api.publish_container(container_id) | ||
|
||
def __repr__(self) -> str: | ||
return ( | ||
f"<{self.__class__.__module__}.{self.__class__.__name__}: " | ||
f"account:'{self.account}'>" | ||
) |
Empty file.
Oops, something went wrong.