Skip to content

Commit 55cba53

Browse files
committed
add preview image on bluesky post, migrate monthly top contributors
1 parent 8f7d45e commit 55cba53

File tree

8 files changed

+75
-35
lines changed

8 files changed

+75
-35
lines changed

backend/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,5 @@ requests==2.32.2
6060
tweepy==4.14.0
6161
# dotenv for .env file
6262
python-dotenv==1.0.0
63-
63+
# AT protocol library for Bluesky
64+
atproto==0.0.58

backend/twitterbot/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def get_twitter_link(obj):
3737
"""Return the URI of the tweet."""
3838
return format_html(
3939
'<a href="{}" target="_blank">Link</a>',
40-
obj.tweet_url,
40+
obj.message_url,
4141
)
4242

4343
@staticmethod

backend/twitterbot/management/commands/run_twitterbot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@ def handle(self, *args, **options):
5252

5353
return
5454

55-
tweet_video_recommendation(bot_name, assumeyes=options["assumeyes"])
55+
tweet_video_recommendation(bot_name, assumeyes=options["assumeyes"], dest=["bluesky"])

backend/twitterbot/management/commands/tweet_top_contributors.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@ def add_arguments(self, parser):
2626
)
2727

2828
def handle(self, *args, **options):
29-
30-
tweet_top_contributor_graph(options["bot_name"], assumeyes=options["assumeyes"])
29+
tweet_top_contributor_graph(
30+
options["bot_name"],
31+
assumeyes=options["assumeyes"],
32+
dest=["bluesky"],
33+
)

backend/twitterbot/settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@
4747
),
4848
}
4949

50+
top_contrib_tweet_image_alt = {
51+
"en": (
52+
"A bar plot showing the users who contributed the most comparisons on Tournesol "
53+
"in the last month."
54+
),
55+
"fr": (
56+
"Un graphique en barres représentant les utilisateur·rice·s ayant effecturé le "
57+
"plus de comparaisons sur Tournesol durant le mois dernier."
58+
),
59+
}
60+
5061
# Name of the Discord channel where the twitterbot will post its tweets.
5162
# An empty value won't trigger any post.
5263
TWITTERBOT_DISCORD_CHANNEL = "twitter"

backend/twitterbot/tests/test_tournesolbot.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
generate_top_contributor_figure,
1717
get_best_criteria,
1818
get_video_recommendations,
19-
prepare_tweet,
19+
prepare_text,
2020
tweet_top_contributor_graph,
2121
)
2222

@@ -174,7 +174,7 @@ def test_prepare_tweet(self, mock_get_twitter_account_from_video_id):
174174

175175
mock_get_twitter_account_from_video_id.return_value = "@TournesolApp"
176176

177-
assert prepare_tweet(self.videos[8], dest="twitter") == tweet_text
177+
assert prepare_text(self.videos[8], dest="twitter") == tweet_text
178178

179179
# Test automatic shortening of the video title to fit in the tweet
180180
self.videos[8].metadata[
@@ -190,7 +190,7 @@ def test_prepare_tweet(self, mock_get_twitter_account_from_video_id):
190190
"\ntournesol.app/entities/yt:AAAAAAAAAAA"
191191
)
192192

193-
assert prepare_tweet(self.videos[8], dest="twitter") == tweet_text_too_long
193+
assert prepare_text(self.videos[8], dest="twitter") == tweet_text_too_long
194194

195195
# Test replacement of special characters in the video title
196196
self.videos[8].metadata["name"] = "Tournesol.app is great but mention @twitter are not..."
@@ -204,7 +204,7 @@ def test_prepare_tweet(self, mock_get_twitter_account_from_video_id):
204204
"\ntournesol.app/entities/yt:AAAAAAAAAAA"
205205
)
206206

207-
assert prepare_tweet(self.videos[8], dest="twitter") == tweet_special_characters
207+
assert prepare_text(self.videos[8], dest="twitter") == tweet_special_characters
208208

209209
def test_get_video_recommendations(self):
210210
"""
@@ -297,8 +297,8 @@ def test_tweet_top_contributor_graph(self, api_mock, client_mock):
297297
mocked_api_client = api_mock.return_value
298298
mocked_v2_client = client_mock.return_value
299299

300-
tweet_top_contributor_graph("@TournesolBot", assumeyes=True)
301-
tweet_top_contributor_graph("@TournesolBotFR", assumeyes=True)
300+
tweet_top_contributor_graph("@TournesolBot", assumeyes=True, dest=["twitter"])
301+
tweet_top_contributor_graph("@TournesolBotFR", assumeyes=True, dest=["twitter"])
302302

303303
self.assertEqual(
304304
mocked_api_client.media_upload.call_count, 2, mocked_api_client.media_upload.calls

backend/twitterbot/tournesolbot.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def get_best_criteria(video, nb_criteria):
4444
return [(crit.criteria, crit.score) for crit in criteria_list]
4545

4646

47-
def prepare_tweet(video: Entity, dest: Literal["twitter", "bluesky"]):
47+
def prepare_text(video: Entity, dest: Literal["twitter", "bluesky"]):
4848
"""Create the tweet text from the video."""
4949

5050
uploader = video.metadata["uploader"]
@@ -68,11 +68,12 @@ def prepare_tweet(video: Entity, dest: Literal["twitter", "bluesky"]):
6868
CriteriaLocale.objects.filter(language=language).values_list("criteria__name", "label")
6969
)
7070

71-
# Replace "@" by a smaller "@" to avoid false mentions in the tweet
72-
video_title = video.metadata["name"].replace("@", "﹫")
71+
if dest == "twitter":
72+
# Replace "@" by a smaller "@" to avoid false mentions in the tweet
73+
video_title = video.metadata["name"].replace("@", "﹫")
7374

74-
# Replace "." in between words to avoid in the tweet false detection of links
75-
video_title = re.sub(r"\b(?:\.)\b", "․", video_title)
75+
# Replace "." in between words to avoid in the tweet false detection of links
76+
video_title = re.sub(r"\b(?:\.)\b", "․", video_title)
7677

7778
# Generate the text of the tweet
7879
poll_rating = video.all_poll_ratings.get(poll__name=DEFAULT_POLL_NAME)
@@ -103,7 +104,8 @@ def prepare_tweet(video: Entity, dest: Literal["twitter", "bluesky"]):
103104
video_id=video_id,
104105
)
105106

106-
if dest == "twitter":
107+
if dest != "bluesky":
108+
# on Bluesky the URL preview is attached separately as "embed"
107109
tweet_text += f"\n{get_video_short_url(video)}"
108110

109111
return tweet_text
@@ -194,11 +196,11 @@ def tweet_video_recommendation(bot_name, dest: list[str], assumeyes=False):
194196
atproto_uri = None
195197
# Tweet the video
196198
if "twitter" in dest:
197-
tweet_text = prepare_tweet(video, dest="twitter")
199+
tweet_text = prepare_text(video, dest="twitter")
198200
tweet_id = twitterbot.create_tweet(text=tweet_text)
199201

200202
if "bluesky" in dest:
201-
text = prepare_tweet(video, dest="bluesky")
203+
text = prepare_text(video, dest="bluesky")
202204
atproto_uri = twitterbot.create_bluesky_post(text=text, embed_video=video)
203205

204206
# Add the video to the TweetInfo table
@@ -268,7 +270,7 @@ def generate_top_contributor_figure(top_contributors_qs, language="en") -> Path:
268270
return figure_path
269271

270272

271-
def tweet_top_contributor_graph(bot_name, assumeyes=False):
273+
def tweet_top_contributor_graph(bot_name, dest: list[str], assumeyes=False):
272274
"""Tweet the top contibutor graph of last month.
273275
274276
Args:
@@ -297,15 +299,28 @@ def tweet_top_contributor_graph(bot_name, assumeyes=False):
297299
if confirmation not in ["y", "yes"]:
298300
return
299301

300-
tweet_id = twitterbot.create_tweet(
301-
text=settings.top_contrib_tweet_text_template[language],
302-
media_files=[top_contributor_figure]
303-
)
302+
message_url = None
303+
if "twitter" in dest:
304+
tweet_id = twitterbot.create_tweet(
305+
text=settings.top_contrib_tweet_text_template[language],
306+
media_files=[top_contributor_figure]
307+
)
308+
message_url = f"https://twitter.com/{bot_name}/status/{tweet_id}"
304309

305-
# Post the tweet on Discord
306-
discord_channel = settings.TWITTERBOT_DISCORD_CHANNEL
307-
if discord_channel:
308-
write_in_channel(
309-
discord_channel,
310-
message=f"https://twitter.com/{bot_name}/status/{tweet_id}",
310+
if "bluesky" in dest:
311+
post_uri = twitterbot.create_bluesky_post(
312+
text=settings.top_contrib_tweet_text_template[language],
313+
image_files=[top_contributor_figure],
314+
image_alts=[settings.top_contrib_tweet_image_alt[language]]
311315
)
316+
post_id = post_uri.rsplit("/", 1)[-1]
317+
message_url = f'https://bsky.app/profile/{twitterbot.bluesky_handle}/post/{post_id}'
318+
319+
if message_url is not None:
320+
# Post the tweet on Discord
321+
discord_channel = settings.TWITTERBOT_DISCORD_CHANNEL
322+
if discord_channel:
323+
write_in_channel(
324+
discord_channel,
325+
message=message_url,
326+
)

backend/twitterbot/twitter_api.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33
from typing import List, Optional
44

5+
import requests
56
from django.conf import settings
67

78
from tournesol.models import Entity
@@ -43,9 +44,13 @@ def tweepy_client(self):
4344
access_token_secret=self.account_cred["ACCESS_TOKEN_SECRET"],
4445
)
4546

47+
@property
48+
def bluesky_handle(self):
49+
return self.account_cred["ATPROTO_HANDLE"]
50+
4651
@cached_property
4752
def atproto_client(self):
48-
from atproto import Client
53+
from atproto import Client # pylint:disable=import-outside-toplevel
4954
client = Client()
5055
client.login(
5156
self.account_cred["ATPROTO_HANDLE"],
@@ -73,18 +78,23 @@ def create_bluesky_post(
7378
image_files: Optional[List[Path]] = None,
7479
image_alts: Optional[List[str]] = None,
7580
):
76-
from atproto import models
81+
from atproto import models # pylint:disable=import-outside-toplevel
7782
if image_files is None:
7883
if embed_video is None:
7984
embed = None
8085
else:
86+
preview_response = requests.get(
87+
f"https://api.tournesol.app/preview/entities/{embed_video.uid}"
88+
)
89+
preview_response.raise_for_status()
90+
img_data = preview_response.content
91+
thumb_blob = self.atproto_client.upload_blob(img_data).blob
8192
embed = models.AppBskyEmbedExternal.Main(
8293
external=models.AppBskyEmbedExternal.External(
8394
title=embed_video.metadata.get("name", ""),
8495
description=embed_video.metadata.get("uploader", ""),
85-
uri=f"https://tournesolapp/entities/yt:{embed_video.video_id}",
86-
# TODO: fetch thumbnail from tournesol.app and upload blob
87-
# thumb=blob
96+
uri=f"https://tournesolapp/entities/{embed_video.uid}",
97+
thumb=thumb_blob,
8898
)
8999
)
90100
resp = self.atproto_client.send_post(

0 commit comments

Comments
 (0)