From c452ac24e056262d0b8cb432aadab878f54d2a30 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Wed, 30 Jul 2025 18:30:18 -0400 Subject: [PATCH] [notifications] Send mood check-in --- PROJECT.org | 5 +++ vrobbler/apps/boardgames/sources/lichess.py | 4 +- vrobbler/apps/books/koreader.py | 4 +- ...erprofile_mood_checkin_enabled_and_more.py | 23 ++++++++++ vrobbler/apps/profiles/models.py | 3 ++ .../management/commands/send_mood_checkin.py | 10 +++++ vrobbler/apps/scrobbles/models.py | 4 +- vrobbler/apps/scrobbles/notifications.py | 44 +++++++++++++++++-- vrobbler/apps/scrobbles/scrobblers.py | 4 +- vrobbler/apps/scrobbles/utils.py | 17 ++++++- 10 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 vrobbler/apps/profiles/migrations/0027_userprofile_mood_checkin_enabled_and_more.py create mode 100644 vrobbler/apps/scrobbles/management/commands/send_mood_checkin.py diff --git a/PROJECT.org b/PROJECT.org index a4d9476..266b159 100644 --- a/PROJECT.org +++ b/PROJECT.org @@ -443,6 +443,11 @@ it's annoying. ** TODO [#C] Allow users to see tasks on calendar view :vrobbler:personal:project:templates:feature: https://codepen.io/oliviale/pen/QYqybo ** TODO [#C] Come up with a possible flow using WebDAV and super-productivity for tasks :personal:feature:project:vrobbler:tasks: +* Version 19.0 +** DONE Add periodic check for mood :vrobbler:feature:moods:personal:project: +:PROPERTIES: +:ID: 55404488-c69f-0dd5-838e-1d1e15c873eb +:END: * Version 18.7 ** DONE Use the timezone history log to fix old Scrobbles that fall into those timezone blocks :vrobbler:chore:scrobbles:project:personal: :PROPERTIES: diff --git a/vrobbler/apps/boardgames/sources/lichess.py b/vrobbler/apps/boardgames/sources/lichess.py index 2ea30db..d7d50c4 100644 --- a/vrobbler/apps/boardgames/sources/lichess.py +++ b/vrobbler/apps/boardgames/sources/lichess.py @@ -3,7 +3,7 @@ from boardgames.models import BoardGame from django.conf import settings from django.contrib.auth import get_user_model from scrobbles.models import Scrobble -from scrobbles.notifications import NtfyNotification +from scrobbles.notifications import ScrobbleNtfyNotification User = get_user_model() @@ -124,5 +124,5 @@ def import_chess_games_for_all_users(): if scrobbles_to_create: created = Scrobble.objects.bulk_create(scrobbles_to_create) for scrobble in created: - NtfyNotification(scrobble).send() + ScrobbleNtfyNotification(scrobble).send() return scrobbles_to_create diff --git a/vrobbler/apps/books/koreader.py b/vrobbler/apps/books/koreader.py index 1d6d7d9..be7de80 100644 --- a/vrobbler/apps/books/koreader.py +++ b/vrobbler/apps/books/koreader.py @@ -9,7 +9,7 @@ import requests from books.constants import BOOKS_TITLES_TO_IGNORE from django.apps import apps from django.contrib.auth import get_user_model -from scrobbles.notifications import NtfyNotification +from scrobbles.notifications import ScrobbleNtfyNotification from stream_sqlite import stream_sqlite from webdav.client import get_webdav_client @@ -409,7 +409,7 @@ def process_koreader_sqlite_file(file_path, user_id) -> list: if new_scrobbles: created = Scrobble.objects.bulk_create(new_scrobbles) if created: - NtfyNotification(created[-1]).send() + ScrobbleNtfyNotification(created[-1]).send() fix_long_play_stats_for_scrobbles(created) logger.info( f"Created {len(created)} scrobbles", diff --git a/vrobbler/apps/profiles/migrations/0027_userprofile_mood_checkin_enabled_and_more.py b/vrobbler/apps/profiles/migrations/0027_userprofile_mood_checkin_enabled_and_more.py new file mode 100644 index 0000000..a2e991b --- /dev/null +++ b/vrobbler/apps/profiles/migrations/0027_userprofile_mood_checkin_enabled_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.19 on 2025-07-30 22:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0026_userprofile_timezone_change_log'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='mood_checkin_enabled', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='userprofile', + name='mood_checkin_frequency', + field=models.CharField(default='hourly', max_length=20), + ), + ] diff --git a/vrobbler/apps/profiles/models.py b/vrobbler/apps/profiles/models.py index 0d97998..75981f5 100644 --- a/vrobbler/apps/profiles/models.py +++ b/vrobbler/apps/profiles/models.py @@ -56,6 +56,9 @@ class UserProfile(TimeStampedModel): imap_pass = EncryptedField(**BNULL) imap_auto_import = models.BooleanField(default=False) + mood_checkin_enabled = models.BooleanField(default=False) + mood_checkin_frequency = models.CharField(max_length=20, default="hourly") + ntfy_url = models.CharField(max_length=255, **BNULL) ntfy_enabled = models.BooleanField(default=False) diff --git a/vrobbler/apps/scrobbles/management/commands/send_mood_checkin.py b/vrobbler/apps/scrobbles/management/commands/send_mood_checkin.py new file mode 100644 index 0000000..9f6fb34 --- /dev/null +++ b/vrobbler/apps/scrobbles/management/commands/send_mood_checkin.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand +from vrobbler.apps.scrobbles.utils import ( + send_mood_checkin_reminders +) + + +class Command(BaseCommand): + def handle(self, *args, **options): + sent_count = send_mood_checkin_reminders() + print(f"Sent {sent_count} mood check-in notifications") diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index 1a11f2a..cd2bd9d 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -42,7 +42,7 @@ from puzzles.models import Puzzle from scrobbles import dataclasses as logdata from scrobbles.constants import LONG_PLAY_MEDIA, MEDIA_END_PADDING_SECONDS from scrobbles.importers.lastfm import LastFM -from scrobbles.notifications import NtfyNotification +from scrobbles.notifications import ScrobbleNtfyNotification from scrobbles.stats import build_charts from scrobbles.utils import get_file_md5_hash, media_class_to_foreign_key from sports.models import SportEvent @@ -1316,7 +1316,7 @@ class Scrobble(TimeStampedModel): scrobble_data: dict, ) -> "Scrobble": scrobble = cls.objects.create(**scrobble_data) - NtfyNotification(scrobble).send() + ScrobbleNtfyNotification(scrobble).send() return scrobble def stop(self, timestamp=None, force_finish=False) -> None: diff --git a/vrobbler/apps/scrobbles/notifications.py b/vrobbler/apps/scrobbles/notifications.py index 3c6b073..a2b6b6c 100644 --- a/vrobbler/apps/scrobbles/notifications.py +++ b/vrobbler/apps/scrobbles/notifications.py @@ -4,12 +4,24 @@ import requests from django.conf import settings from django.contrib.sites.models import Site -class Notification(ABC): - scrobble: "Scrobble" +class BasicNtfyNotification(ABC): ntfy_headers: dict = {} ntfy_url: str = "" title: str = "" + def __init__(self, profile: "UserProfile"): + self.user = profile.user + protocol = "http" if settings.DEBUG else "https" + domain = Site.objects.get_current().domain + self.url_tmpl = f'{protocol}://{domain}' + '{path}' + + @abstractmethod + def send(self) -> None: + pass + +class ScrobbleNotification(BasicNtfyNotification): + scrobble: "Scrobble" + def __init__(self, scrobble: "Scrobble"): self.scrobble = scrobble self.user = scrobble.user @@ -19,13 +31,12 @@ class Notification(ABC): self.url_tmpl = f'{protocol}://{domain}' + '{path}' - @abstractmethod def send(self) -> None: pass -class NtfyNotification(Notification): +class ScrobbleNtfyNotification(ScrobbleNotification): def __init__(self, scrobble, **kwargs): super().__init__(scrobble) self.ntfy_str: str = f"{self.scrobble.media_obj}" @@ -55,3 +66,28 @@ class NtfyNotification(Notification): "Click": self.click_url, }, ) + +class MoodNtfyNotification(BasicNtfyNotification): + def __init__(self, profile, **kwargs): + super().__init__(scrobble) + self.profile = profile + self.ntfy_str: str = "Would you like to check in about your mood?" + self.click_url = self.url_tmpl.format(path=reverse("moods:mood-list")) + self.title = "Mood Check-in!" + + def send(self): + if ( + self.profile + and self.profile.ntfy_enabled + and self.profile.ntfy_url + ): + requests.post( + self.user.profile.ntfy_url, + data=self.ntfy_str.encode(encoding="utf-8"), + headers={ + "Title": self.title, + "Priority": "high", + "Tags": "smiley, check", + "Click": self.click_url, + }, + ) diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index c5730ff..d99d0fb 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -26,7 +26,7 @@ from scrobbles.constants import ( SCROBBLE_CONTENT_URLS, ) from scrobbles.models import Scrobble -from scrobbles.notifications import NtfyNotification +from scrobbles.notifications import ScrobbleNtfyNotification from scrobbles.utils import convert_to_seconds, extract_domain from sports.models import SportEvent from sports.thesportsdb import lookup_event_from_thesportsdb @@ -505,7 +505,7 @@ def email_scrobble_board_game( scrobble.played_to_completion = True scrobble.save() scrobbles_created.append(scrobble) - NtfyNotification(scrobble).send() + ScrobbleNtfyNotification(scrobble).send() return scrobbles_created diff --git a/vrobbler/apps/scrobbles/utils.py b/vrobbler/apps/scrobbles/utils.py index fee5093..12044e8 100644 --- a/vrobbler/apps/scrobbles/utils.py +++ b/vrobbler/apps/scrobbles/utils.py @@ -13,7 +13,7 @@ from django.utils import timezone from profiles.models import UserProfile from profiles.utils import now_user_timezone from scrobbles.constants import LONG_PLAY_MEDIA -from scrobbles.notifications import NtfyNotification +from scrobbles.notifications import MoodNtfyNotification, ScrobbleNtfyNotification from scrobbles.tasks import ( process_koreader_import, process_lastfm_import, @@ -318,7 +318,20 @@ def send_stop_notifications_for_in_progress_scrobbles() -> int: ).seconds if elapsed_scrobble_seconds > scrobble.media_obj.run_time_seconds: - NtfyNotification(scrobble, end=True).send() + ScrobbleNtfyNotification(scrobble, end=True).send() + notifications_sent += 1 + + return notifications_sent + +def send_mood_checkin_reminders() -> int: + """Get all profiles with mood check-ins enabled and checkin!""" + from profiles.models import UserProfile + + now = timezone.now() + notifications_sent = 0 + for profile in UserProfile.objects.filter(mood_checkin_enabled=True): + if profile.mood_checkin_period == "hourly" and now.minute == 0: + MoodNtfyNotification(profile).send() notifications_sent += 1 return notifications_sent