From bd2800287659e876c0de5584b44e701cab89acc7 Mon Sep 17 00:00:00 2001 From: suecarmol Date: Thu, 14 Aug 2025 20:11:14 -0600 Subject: [PATCH 1/4] POC: Add email sender with API:Emailuser --- .../management/commands/send_email_via_api.py | 125 ++++++++++++++++++ .../commands/retrieve_monthly_users.py | 21 ++- conf/local.twlight.env | 2 + conf/production.twlight.env | 2 + conf/staging.twlight.env | 2 + docker-compose.debug_toolbar.yml | 2 - docker-compose.override.yml | 2 - docker-compose.production.yml | 2 - docker-compose.staging.yml | 2 - docker-compose.yml | 2 - 10 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 TWLight/emails/management/commands/send_email_via_api.py diff --git a/TWLight/emails/management/commands/send_email_via_api.py b/TWLight/emails/management/commands/send_email_via_api.py new file mode 100644 index 0000000000..9facf56884 --- /dev/null +++ b/TWLight/emails/management/commands/send_email_via_api.py @@ -0,0 +1,125 @@ +import logging +import os +import requests + +from django.core.management.base import BaseCommand, CommandError + +logger = logging.getLogger("django") + + +class Command(BaseCommand): + help = "Command that sends emails via API:Emailuser in MediaWiki" + + def info(self, msg): + # Log and print so that messages are visible + # in docker logs (log) and cron job logs (print) + logger.info(msg) + self.stdout.write(msg) + self.stdout.flush() + + def add_arguments(self, parser): + # Named (optional) arguments + parser.add_argument( + "--target", + type=str, + help="The Wikipedia username you want to send the email to.", + ) + parser.add_argument( + "--subject", + type=str, + help="The subject of the email.", + ) + parser.add_argument( + "--body", + type=str, + help="The body of the email.", + ) + + def handle(self, *args, **options): + if not options["target"]: + raise CommandError("You need to specify a user to send the email to") + + if not options["subject"]: + raise CommandError("You need to specify the subject of the email") + + if not options["body"]: + raise CommandError("You need to specify the body of the email") + + target = options["target"] + subject = options["subject"] + body = options["body"] + + email_bot_username = os.environ.get("EMAILWIKIBOTUSERNAME", None) + email_bot_password = os.environ.get("EMAILWIKIBOTPASSWORD", None) + + if email_bot_username is None or email_bot_password is None: + # Bot credentials not provided; exiting gracefully + raise CommandError( + "The email bot username and/or password were not provided. Exiting..." + ) + # Code taken in part from https://www.mediawiki.org/wiki/API:Emailuser#Python + session = requests.Session() + # TODO: See if we need to change this to Meta or the user's home wiki? + # Or is this wiki fine? + url = "https://test.wikipedia.org/w/api.php" + + # Step 1: GET request to fetch login token + login_token_params = { + "action": "query", + "meta": "tokens", + "type": "login", + "format": "json", + } + + self.info("Getting login token...") + response_login_token = session.get(url=url, params=login_token_params) + if response_login_token.status_code != 200: + raise CommandError( + "There was an error in the request for obtaining the login token." + ) + login_token_data = response_login_token.json() + + login_token = login_token_data["query"]["tokens"]["logintoken"] + + if not login_token: + raise CommandError("There was an error obtaining the login token.") + + # Step 2: POST request to log in. Use of main account for login is not + # supported. Obtain credentials via Special:BotPasswords + # (https://www.mediawiki.org/wiki/Special:BotPasswords) for lgname & lgpassword + login_params = { + "action": "login", + "lgname": email_bot_username, + "lgpassword": email_bot_password, + "lgtoken": login_token, + "format": "json", + } + + self.info("Signing in...") + login_response = session.post(url, data=login_params) + if login_response.status_code != 200: + raise CommandError("There was an error in the request for the login.") + + # Step 3: GET request to fetch Email token + email_token_params = {"action": "query", "meta": "tokens", "format": "json"} + + self.info("Getting emwail token...") + email_token_response = session.get(url=url, params=email_token_params) + email_token_data = email_token_response.json() + + email_token = email_token_data["query"]["tokens"]["csrftoken"] + + # Step 4: POST request to send an email + email_params = { + "action": "emailuser", + "target": target, + "subject": subject, + "text": body, + "token": email_token, + "format": "json", + } + + self.info("Sending email...") + email_response = session.post(url, data=email_params) + if email_response.status_code != 200: + raise CommandError("There was an error when trying to send the email.") diff --git a/TWLight/users/management/commands/retrieve_monthly_users.py b/TWLight/users/management/commands/retrieve_monthly_users.py index a2b5c72e0c..ebf7b9bc37 100644 --- a/TWLight/users/management/commands/retrieve_monthly_users.py +++ b/TWLight/users/management/commands/retrieve_monthly_users.py @@ -1,16 +1,25 @@ import calendar import datetime from dateutil.relativedelta import relativedelta +import logging +from django.core.management import call_command from django.core.management.base import BaseCommand from django.db import connection -from TWLight.users.signals import UserLoginRetrieval +logger = logging.getLogger("django") class Command(BaseCommand): help = "Retrieves user names that have logged-in in the past month and have approved applications and current authorizations." + def info(self, msg): + # Log and print so that messages are visible + # in docker logs (log) and cron job logs (print) + logger.info(msg) + self.stdout.write(msg) + self.stdout.flush() + def handle(self, *args, **options): current_date = datetime.datetime.now(datetime.timezone.utc).date() last_month = current_date - relativedelta(months=1) @@ -18,6 +27,7 @@ def handle(self, *args, **options): _, last_day = calendar.monthrange(last_month.year, last_month.month) last_day_last_month = datetime.date(last_month.year, last_month.month, last_day) + self.info("Getting query...") raw_query = """SELECT users_editor.wp_username, IF( -- has application status APPROVED = 2 SENT = 4 (applications_application.status = 2 OR applications_application.status = 4), 'true', 'false') AS has_approved_apps, @@ -44,12 +54,17 @@ def handle(self, *args, **options): last_day_last_month=last_day_last_month, ) + self.info("Creating monthly users...") with connection.cursor() as cursor: cursor.execute(raw_query) columns = [col[0] for col in cursor.description] monthly_users = [dict(zip(columns, row)) for row in cursor.fetchall()] if monthly_users: - UserLoginRetrieval.user_retrieve_monthly_logins.send( - sender=self.__class__, monthly_users=monthly_users + self.info("Sending email...") + call_command( + "send_email_via_api", + target="Suecarmol", # TODO: change to Vipin's user + subject="Monthly user report", + body=f"Here is a list of monthly users: {monthly_users}", ) diff --git a/conf/local.twlight.env b/conf/local.twlight.env index d61d8e4be3..3333b0ae90 100644 --- a/conf/local.twlight.env +++ b/conf/local.twlight.env @@ -21,3 +21,5 @@ TWLIGHT_API_PROVIDER_ENDPOINT=https://meta.wikimedia.org/w/api.php TWLIGHT_EZPROXY_URL=https://ezproxy.dev.localdomain:2443 # seeem to be having troubles with --workers > 1 GUNICORN_CMD_ARGS=--name twlight --workers 1 --backlog 2048 --timeout 300 --bind=0.0.0.0:80 --forwarded-allow-ips * --reload --log-level=info +EMAILWIKIBOTUSERNAME=Add-bot-name +EMAILWIKIBOTPASSWORD=Add-bot-password diff --git a/conf/production.twlight.env b/conf/production.twlight.env index 26ed9cf526..dbabeda2ef 100644 --- a/conf/production.twlight.env +++ b/conf/production.twlight.env @@ -21,3 +21,5 @@ TWLIGHT_OAUTH_PROVIDER_URL=https://meta.wikimedia.org/w/index.php TWLIGHT_API_PROVIDER_ENDPOINT=https://meta.wikimedia.org/w/api.php TWLIGHT_EZPROXY_URL=https://wikipedialibrary.idm.oclc.org GUNICORN_CMD_ARGS=--name twlight --worker-class gthread --workers 9 --threads 1 --timeout 30 --backlog 2048 --bind=0.0.0.0:80 --forwarded-allow-ips * --reload --log-level=info +EMAILWIKIBOTUSERNAME=Add-bot-name +EMAILWIKIBOTPASSWORD=Add-bot-password diff --git a/conf/staging.twlight.env b/conf/staging.twlight.env index ef8cbf693d..375fa9ba28 100644 --- a/conf/staging.twlight.env +++ b/conf/staging.twlight.env @@ -21,3 +21,5 @@ TWLIGHT_OAUTH_PROVIDER_URL=https://meta.wikimedia.org/w/index.php TWLIGHT_API_PROVIDER_ENDPOINT=https://meta.wikimedia.org/w/api.php TWLIGHT_EZPROXY_URL=https://wikipedialibrary.idm.oclc.org:9443 GUNICORN_CMD_ARGS=--name twlight --worker-class gthread --workers 4 --threads 1 --timeout 30 --backlog 2048 --bind=0.0.0.0:80 --forwarded-allow-ips * --reload --log-level=info +EMAILWIKIBOTUSERNAME=Add-bot-name +EMAILWIKIBOTPASSWORD=Add-bot-password diff --git a/docker-compose.debug_toolbar.yml b/docker-compose.debug_toolbar.yml index 0a69b18a40..b9e6590160 100644 --- a/docker-compose.debug_toolbar.yml +++ b/docker-compose.debug_toolbar.yml @@ -1,7 +1,5 @@ --- -version: '3.4' - services: twlight: build: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 778f059af9..937d18c01a 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,7 +1,5 @@ --- -version: '3.4' - # Local environment should mount plaintext files as secrets secrets: DJANGO_DB_NAME: diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 6f8c59b93e..5095b978ae 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -1,7 +1,5 @@ --- -version: '3.4' - secrets: DJANGO_DB_NAME: external: true diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 0fde1bb0a6..5f45a4737b 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -1,7 +1,5 @@ --- -version: '3.4' - secrets: DJANGO_DB_NAME: external: true diff --git a/docker-compose.yml b/docker-compose.yml index 83b8b0778a..53c03e7239 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,5 @@ --- -version: '3.4' - volumes: mysql: From 351f63fa05dd8e638460de9a4c81df1cd3f38018 Mon Sep 17 00:00:00 2001 From: suecarmol Date: Fri, 15 Aug 2025 11:11:16 -0600 Subject: [PATCH 2/4] Fix typo --- TWLight/emails/management/commands/send_email_via_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TWLight/emails/management/commands/send_email_via_api.py b/TWLight/emails/management/commands/send_email_via_api.py index 9facf56884..750b4f63b0 100644 --- a/TWLight/emails/management/commands/send_email_via_api.py +++ b/TWLight/emails/management/commands/send_email_via_api.py @@ -103,7 +103,7 @@ def handle(self, *args, **options): # Step 3: GET request to fetch Email token email_token_params = {"action": "query", "meta": "tokens", "format": "json"} - self.info("Getting emwail token...") + self.info("Getting email token...") email_token_response = session.get(url=url, params=email_token_params) email_token_data = email_token_response.json() From ffd515140daa2c13c96d61aea439f4ffcaca58b3 Mon Sep 17 00:00:00 2001 From: suecarmol Date: Fri, 15 Aug 2025 11:22:57 -0600 Subject: [PATCH 3/4] Add TODO comments --- TWLight/emails/management/commands/send_email_via_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TWLight/emails/management/commands/send_email_via_api.py b/TWLight/emails/management/commands/send_email_via_api.py index 750b4f63b0..1e24df4d5a 100644 --- a/TWLight/emails/management/commands/send_email_via_api.py +++ b/TWLight/emails/management/commands/send_email_via_api.py @@ -19,6 +19,7 @@ def info(self, msg): def add_arguments(self, parser): # Named (optional) arguments + # TODO: Check if we can add multiple targets, or if we have to make one request at a time parser.add_argument( "--target", type=str, @@ -58,6 +59,7 @@ def handle(self, *args, **options): "The email bot username and/or password were not provided. Exiting..." ) # Code taken in part from https://www.mediawiki.org/wiki/API:Emailuser#Python + # TODO: Add solution in case of rate-limiting session = requests.Session() # TODO: See if we need to change this to Meta or the user's home wiki? # Or is this wiki fine? From e7801a5441ce8e8fbc06f44a1ef08fbe67149e06 Mon Sep 17 00:00:00 2001 From: suecarmol Date: Fri, 15 Aug 2025 11:25:04 -0600 Subject: [PATCH 4/4] Add new-line to string of body --- TWLight/users/management/commands/retrieve_monthly_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TWLight/users/management/commands/retrieve_monthly_users.py b/TWLight/users/management/commands/retrieve_monthly_users.py index ebf7b9bc37..e7d6b4cfb9 100644 --- a/TWLight/users/management/commands/retrieve_monthly_users.py +++ b/TWLight/users/management/commands/retrieve_monthly_users.py @@ -66,5 +66,5 @@ def handle(self, *args, **options): "send_email_via_api", target="Suecarmol", # TODO: change to Vipin's user subject="Monthly user report", - body=f"Here is a list of monthly users: {monthly_users}", + body=f"Here is a list of monthly users: \n\n {monthly_users}", )