2
0
Fork 0

Compare commits

...

4 Commits

Author SHA1 Message Date
Luca f03542b584 fix(notify): output spacing
continuous-integration/drone/push Build is passing Details
2024-05-12 22:16:16 +02:00
Luca 69c9faa4bc chore: add custom logging config 2024-05-12 22:08:41 +02:00
Luca 10dbb06957 feat(feedback): add command to notify helpers who opted-in last year 2024-05-12 21:44:00 +02:00
Luca b685ad800f chore: add script to extract opt-in helpers from database dump 2024-05-12 19:27:02 +02:00
8 changed files with 184 additions and 8 deletions

3
.gitignore vendored
View File

@ -161,3 +161,6 @@ cython_debug/
# E.g. copies of old dev database # E.g. copies of old dev database
*.bkp *.bkp
# Database dumps
/*.json

View File

@ -0,0 +1,24 @@
#!/bin/sh
jq '[
JOIN(
INDEX(
.[] | select(
.model == "app.helper"
);
.pk | tostring
);
.[] | select(
.model == "feedback.feedback"
and .fields.next_year
);
.pk | tostring;
.[0].fields as $feedback
| .[1].fields as $helper
| {
"name": $helper.name,
"next_year": $feedback.next_year,
"phone": $helper.phone,
}
)
]'

View File

@ -0,0 +1,129 @@
import json
import pathlib
import sys
from django.core.management.base import BaseCommand, CommandError
from django.template import Context, Template
from phonenumber_field.phonenumber import PhoneNumber
from shiftregister.app.models import Helper
from shiftregister.messaging import Message, MessageType
from shiftregister.messaging.outbound import send
class Command(BaseCommand):
help = (
"Notify a list of phone numbers excluding those already found in the database."
)
def add_arguments(self, parser):
parser.add_argument(
"-n",
"--dry-run",
action="store_true",
help="print messages that would be sent, but do not actually send them",
)
parser.add_argument(
"numbers", help="path to numbers list as JSON", type=pathlib.Path
)
def handle(self, *args, **options):
with options["numbers"].open() as f:
numbers = json.load(f)
if not isinstance(numbers, list) or not all(
map(lambda o: isinstance(o, dict) and "phone" in o, numbers)
):
raise CommandError(
"JSON document must be an array of objects containing (at least) the key 'phone'"
)
template = Template(sys.stdin.read().strip())
numbers_count = len(numbers)
existing_numbers = set(Helper.objects.values_list("phone", flat=True))
numbers = list(
filter(
lambda o: PhoneNumber.from_string(o["phone"]) not in existing_numbers,
numbers,
)
)
self.stderr.write(
self.style.WARNING(
f"{numbers_count-len(numbers)} already found in database"
)
)
messages = (
Message(
i,
recipient=o["phone"],
text=template.render(Context(o)),
type=MessageType.OUTBOUND,
)
for i, o in enumerate(numbers)
)
if options["dry_run"]:
messages = list(messages)
self.stderr.write(
self.style.WARNING(f"would send {len(messages)} message(s)")
)
self.stderr.write()
for message in messages:
if int(message.key) != 0:
self.stderr.write()
self.stderr.write(self.style.WARNING("---"))
self.stderr.write()
self.stderr.write(self.style.WARNING(f"to: {message.recipient}"))
self.stderr.write(self.style.WARNING(f"length: {len(message.text)}"))
self.stderr.write()
self.stderr.write(self.style.WARNING(message.text))
if len(messages) > 0:
self.stderr.write()
return
self.stderr.write()
all_messages = set(messages)
sent_messages = set()
error = None
try:
for message in send(all_messages):
sent_messages.add(message)
self.stderr.write()
self.stderr.write(
self.style.SUCCESS(
f"\r{str(len(sent_messages)).rjust(len(str(len(all_messages))))} / {len(all_messages)}"
)
)
self.stderr.write()
except Exception as e:
error = e
self.stderr.write(
self.style.WARNING(f"sent {len(sent_messages)} out of {len(all_messages)}")
)
self.stderr.write(
self.style.WARNING(f"numbers to which messages could not be sent:")
)
self.stdout.write(
self.style.ERROR(
json.dumps(
[
{"phone": message.recipient}
for message in all_messages - sent_messages
]
)
)
)
if error:
raise CommandError(error)

View File

@ -41,7 +41,8 @@ class WebhookReceiver(BaseReceiver):
if not sender: if not sender:
raise ValueError("message has no sender") raise ValueError("message has no sender")
logging.getLogger("django.server").info( logger.info(
f"received sms via webhook\nkey: {key}\nfrom: {sender}\nadditional fields: {pformat(kwargs)}\n\n{text}" f"received sms via webhook\nkey: {key}\nfrom: {sender}\nadditional fields: {pformat(kwargs)}\n\n{text}"
) )
yield Message(key, sender=sender, text=text, type=MessageType.INBOUND) yield Message(key, sender=sender, text=text, type=MessageType.INBOUND)

View File

@ -45,11 +45,10 @@ class Message:
self.created_at = created_at self.created_at = created_at
def __eq__(self, other): def __eq__(self, other):
if other is None: if isinstance(other, Message):
return False return self.key == other.key
elif isinstance(other, Message):
other = other.key
else:
other = str(other)
return self.key == other return NotImplemented
def __hash__(self):
return hash(self.key)

View File

@ -240,3 +240,23 @@ SMS_OUTBOUND_BACKEND = ".".join(
) )
SMS_SETTINGS = env.dict("SMS_SETTINGS", default={}) SMS_SETTINGS = env.dict("SMS_SETTINGS", default={})
SMS_WEBHOOK_SECRET = env("SMS_WEBHOOK_SECRET", default=None) SMS_WEBHOOK_SECRET = env("SMS_WEBHOOK_SECRET", default=None)
# Logging
# https://docs.djangoproject.com/en/5.0/howto/logging/#customize-logging-configuration
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"level": "DEBUG" if DEBUG else "WARNING",
"class": "logging.StreamHandler",
},
},
"loggers": {
"shiftregister": {
"level": "DEBUG" if DEBUG else "WARNING",
"handlers": ["console"],
},
},
}