Compare commits

...

10 commits

Author SHA1 Message Date
9a010aa97b FIX(notifications): configuration for GOTIFY_BASE_PUBLIC_URL
configuration for GOTIFY_BASE_PUBLIC_URL
2026-05-09 14:12:13 +03:30
Sayyid Hamid Mahdavi
66862e8d5d fail_silently=True 2026-04-22 14:20:37 +03:30
Sayyid Hamid Mahdavi
d5acae765c edit trusted origin 2026-04-22 14:12:47 +03:30
Sayyid Hamid Mahdavi
fac2f3e483 extras 2025-10-29 16:11:17 +03:30
Sayyid Hamid Mahdavi
6452eac332 email content 2025-09-07 15:14:20 +03:30
Sayyid Hamid Mahdavi
f58704aa5b email content 2025-09-07 15:13:05 +03:30
Sayyid Hamid Mahdavi
f91f279a5a celery beat 2025-09-06 17:44:07 +03:30
Sayyid Hamid Mahdavi
4c7587e6e0 celery beat 2025-09-06 17:26:10 +03:30
Sayyid Hamid Mahdavi
6795e415f1 celery beat 2025-09-06 17:00:21 +03:30
Sayyid Hamid Mahdavi
64644a5a67 email 2025-09-06 12:22:56 +03:30
14 changed files with 170 additions and 43 deletions

View file

@ -1,4 +1,4 @@
openapi-python-client
https://push.gooyal.com/swagger?base=push.gooyal.com
https://push.gooyal.ir/swagger?base=push.gooyal.ir
npm install -g swagger2openapi

View file

@ -1,7 +1,8 @@
from django.contrib import admin
from .models import Email
from .models import Email, EmailQueue
admin.site.register(Email)
admin.site.register(EmailQueue)

View file

@ -0,0 +1,41 @@
# Generated by Django 5.2.6 on 2025-09-06 13:55
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('emails', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='email',
name='extras',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.CreateModel(
name='EmailQueue',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('title', models.CharField(max_length=255, verbose_name='title')),
('last_sent', models.DateTimeField(auto_now_add=True, verbose_name='last sent')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='email',
name='queue',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='emails.emailqueue'),
),
]

View file

@ -1,4 +1,5 @@
from django.db import models
from django.db.models import F
from oauth2_provider.settings import oauth2_settings
from apps.users.models import User
@ -6,6 +7,18 @@ from utils.models import BaseModel
from django.utils.translation import gettext_lazy as _
class EmailQueueQuerySet(models.query.QuerySet):
pass
class EmailQueue(BaseModel):
title = models.CharField(_('title'), max_length=255)
user = models.ForeignKey(User, verbose_name=_('user'), on_delete=models.PROTECT)
last_sent = models.DateTimeField(_('last sent'), auto_now_add=True)
objects = EmailQueueQuerySet.as_manager()
class EmailQuerySet(models.QuerySet):
pass
@ -27,6 +40,8 @@ class Email(BaseModel):
priority = models.IntegerField(default=5)
extras = models.JSONField(default=dict, null=True, blank=True)
stats = models.IntegerField(default=StateChoices.INIT, choices=StateChoices.choices)
queue = models.ForeignKey(EmailQueue, on_delete=models.PROTECT, related_name='+', null=True, blank=True)
# bulk = models.ForeignKey("BulkEmail", null=True, blank=True, on_delete=models.PROTECT)
@ -37,7 +52,7 @@ class Email(BaseModel):
def send(self):
from apps.emails.tasks import send_email
send_email(self.uuid)
send_email.delay(self.uuid)
def _send_email(self):
from django.core.mail import send_mail

View file

@ -7,6 +7,7 @@ class EmailSerializer(serializers.ModelSerializer):
class Meta:
model = Email
fields = (
"queue",
"title",
"message",
"user",

View file

@ -1,6 +1,11 @@
from django.db.models import F
from django.utils import timezone
import logging
from apps.emails.models import Email, EmailQueue
from main import celery_app
from utils.clients import gotify
logger = logging.getLogger(__name__)
@celery_app.task
def send_email(email_uuid):
@ -10,3 +15,40 @@ def send_email(email_uuid):
email.state = email.StateChoices.DONE
email.save()
@celery_app.task
def send_email_for_queued_events():
logger.info('Sending email for queued events')
for email_queue in EmailQueue.objects.all():
logger.info('Sending email for queue %s', email_queue.title)
qs = Email.objects.filter(queue=email_queue, stats=Email.StateChoices.INIT, created_at__gte=F('queue__last_sent'))
if email_queue.last_sent <= timezone.now() - timezone.timedelta(minutes=10) and qs.exists():
logger.info('Sending email for queue %s', email_queue.title)
from django.core.mail import send_mail
from django.conf import settings
title = 'you have some queued task(s)'
message = f"you have {qs.count()} queued task(s) \n"
for index, email in enumerate(qs, start=1):
message += f"{index} - {email.title} \n"
send_mail(
title,
message,
settings.EMAIL_HOST_USER,
["xdshia49@gmail.com"],
fail_silently=True,
)
email_queue.last_sent = timezone.now()
email_queue.save()
celery_app.conf.beat_schedule = {
'print-every-minute': {
'task': 'apps.emails.tasks.send_email_for_queued_events',
'schedule': 10.0, # هر 60 ثانیه
# یا می‌توانید از crontab استفاده کنید:
# 'schedule': crontab(minute='*/1'),
},
}

View file

@ -43,4 +43,5 @@ class ApplicationEmailViewSet(
user = get_object_or_404(User, uuid=user_uuid)
application = get_application(self.request)
instance = serializer.save(user=user, application=application)
instance.send()
if not instance.queue:
instance.send()

View file

@ -5,4 +5,4 @@ while ! nc -z $DB_HOST 5432 ; do
done
#python3 manage.py collectstatic --noinput
#python3 manage.py migrate
celery -A main worker --loglevel=info
celery -A main worker --loglevel=info -B

View file

@ -48,6 +48,7 @@ INSTALLED_APPS = [
'crispy_bootstrap5',
'django_filters',
'jalali_date',
"django_celery_beat",
# 'django_minio_backend.apps.DjangoMinioBackendConfig',
# local apps
@ -223,7 +224,9 @@ SPECTACULAR_SETTINGS = {
AUTH_USER_MODEL = 'users.User'
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_ALL_ORIGINS = True
CSRF_TRUSTED_ORIGINS = ['http://*', 'https://*', 'http://*.gooyal.com', 'https://*.gooyal.com']
CSRF_TRUSTED_ORIGINS = ['http://*', 'https://*',
'http://*.gooyal.com', 'https://*.gooyal.com',
'http://*.gooyal.ir', 'https://*.gooyal.ir']
JALALI_DATE_DEFAULTS = {
'Strftime': {
@ -233,6 +236,7 @@ JALALI_DATE_DEFAULTS = {
}
REDIS_BASE_URL = config('REDIS_BASE_URL')
GOTIFY_ADMIN_CLIENT_TOKEN = config('GOTIFY_ADMIN_CLIENT_TOKEN')
GOTIFY_BASE_PUBLIC_URL = config('GOTIFY_BASE_PUBLIC_URL')
CACHES = {
"default": {

View file

@ -18,4 +18,6 @@ celery
httpx
django-redis
django_minio_backend
xlrd
xlrd
django-celery-beat
# aiosmtpd #??

View file

@ -1,32 +1,37 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
# pip-compile
#
aiosmtpd==1.4.6
# via -r requirements.in
amqp==5.3.1
# via kombu
anyio==4.9.0
anyio==4.10.0
# via httpx
argon2-cffi==25.1.0
# via minio
argon2-cffi-bindings==21.2.0
argon2-cffi-bindings==25.1.0
# via argon2-cffi
asgiref==3.8.1
asgiref==3.9.1
# via
# django
# django-cors-headers
async-timeout==5.0.1
# via redis
atpublic==6.0.1
# via aiosmtpd
attrs==25.3.0
# via
# aiosmtpd
# jsonschema
# referencing
billiard==4.2.1
# via celery
celery==5.5.3
# via -r requirements.in
certifi==2025.6.15
# via
# -r requirements.in
# django-celery-beat
certifi==2025.8.3
# via
# httpcore
# httpx
@ -36,7 +41,7 @@ cffi==1.17.1
# via
# argon2-cffi-bindings
# cryptography
charset-normalizer==3.4.2
charset-normalizer==3.4.3
# via requests
click==8.2.1
# via
@ -52,12 +57,15 @@ click-repl==0.3.0
# via celery
crispy-bootstrap5==2025.6
# via -r requirements.in
cryptography==45.0.4
cron-descriptor==2.0.6
# via django-celery-beat
cryptography==45.0.7
# via jwcrypto
django==5.2.3
django==5.2.6
# via
# -r requirements.in
# crispy-bootstrap5
# django-celery-beat
# django-cors-headers
# django-crispy-forms
# django-filter
@ -65,8 +73,11 @@ django==5.2.3
# django-minio-backend
# django-oauth-toolkit
# django-redis
# django-timezone-field
# djangorestframework
# drf-spectacular
django-celery-beat==2.8.1
# via -r requirements.in
django-cors-headers==4.7.0
# via -r requirements.in
django-crispy-forms==2.4
@ -83,15 +94,17 @@ django-oauth-toolkit==3.0.1
# via -r requirements.in
django-redis==6.0.0
# via -r requirements.in
djangorestframework==3.16.0
django-timezone-field==7.1
# via django-celery-beat
djangorestframework==3.16.1
# via
# -r requirements.in
# drf-spectacular
drf-spectacular==0.28.0
# via -r requirements.in
gevent==25.5.1
gevent==25.8.2
# via -r requirements.in
greenlet==3.2.3
greenlet==3.2.4
# via gevent
gunicorn==23.0.0
# via -r requirements.in
@ -112,7 +125,7 @@ jalali-core==1.0.0
# via jdatetime
jdatetime==5.2.0
# via django-jalali-date
jsonschema==4.24.0
jsonschema==4.25.1
# via drf-spectacular
jsonschema-specifications==2025.4.1
# via jsonschema
@ -120,7 +133,7 @@ jwcrypto==1.5.6
# via django-oauth-toolkit
kombu==5.5.4
# via celery
minio==7.2.15
minio==7.2.16
# via django-minio-backend
oauthlib==3.3.1
# via django-oauth-toolkit
@ -128,9 +141,9 @@ packaging==25.0
# via
# gunicorn
# kombu
pillow==11.2.1
pillow==11.3.0
# via -r requirements.in
prompt-toolkit==3.0.51
prompt-toolkit==3.0.52
# via click-repl
psycopg[binary,pool]==3.2.9
# via -r requirements.in
@ -142,13 +155,15 @@ pycparser==2.22
# via cffi
pycryptodome==3.23.0
# via minio
python-crontab==3.3.0
# via django-celery-beat
python-dateutil==2.9.0.post0
# via celery
python-decouple==3.8
# via -r requirements.in
pyyaml==6.0.2
# via drf-spectacular
redis==6.2.0
redis==6.4.0
# via
# -r requirements.in
# django-redis
@ -156,11 +171,11 @@ referencing==0.36.2
# via
# jsonschema
# jsonschema-specifications
requests==2.32.4
requests==2.32.5
# via
# -r requirements.in
# django-oauth-toolkit
rpds-py==0.25.1
rpds-py==0.27.1
# via
# jsonschema
# referencing
@ -170,16 +185,16 @@ sniffio==1.3.1
# via anyio
sqlparse==0.5.3
# via django
typing-extensions==4.14.0
typing-extensions==4.15.0
# via
# anyio
# cron-descriptor
# jwcrypto
# minio
# psycopg
# psycopg-pool
# referencing
tzdata==2025.2
# via kombu
# via
# django-celery-beat
# kombu
uritemplate==4.2.0
# via drf-spectacular
urllib3==2.5.0
@ -195,7 +210,7 @@ wcwidth==0.2.13
# via prompt-toolkit
xlrd==2.0.2
# via -r requirements.in
zope-event==5.1
zope-event==5.1.1
# via gevent
zope-interface==7.2
# via gevent

View file

@ -19,6 +19,7 @@ from gotify_rest_api_client.api.application import create_app
from gotify_rest_api_client.types import Unset
ADMIN_CLIENT_TOKEN = settings.GOTIFY_ADMIN_CLIENT_TOKEN
GOTIFY_BASE_PUBLIC_URL = settings.GOTIFY_BASE_PUBLIC_URL
import logging
@ -44,7 +45,7 @@ def log_response(response):
def get_client(access_token):
client = AuthenticatedClient(base_url="https://push.gooyal.com", token=access_token,
client = AuthenticatedClient(base_url=GOTIFY_BASE_PUBLIC_URL, token=access_token,
httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}},
)
return client
@ -54,12 +55,12 @@ def get_basic_client(username, password):
auth = httpx.BasicAuth(username=username, password=password)
prefix, token = auth._auth_header.split()
# httpx_client = httpx.Client(auth=auth, base_url="https://push.gooyal.com")
client = AuthenticatedClient(base_url="https://push.gooyal.com", token=token, prefix=prefix,
# httpx_client = httpx.Client(auth=auth, base_url=GOTIFY_BASE_PUBLIC_URL)
client = AuthenticatedClient(base_url=GOTIFY_BASE_PUBLIC_URL, token=token, prefix=prefix,
httpx_args={
"event_hooks": {"request": [log_request], "response": [log_response]},
# "auth": auth,
# "base_url": "https://push.gooyal.com"
# "base_url": GOTIFY_BASE_PUBLIC_URL
}
)
# client.set_httpx_client(httpx_client)
@ -67,7 +68,11 @@ def get_basic_client(username, password):
def send_notif(token, title=None, message=None, priority=None, extras=None):
extras = extras or Unset()
if extras:
extras = MessageExternalModelExtras.from_dict(extras)
else:
extras = Unset()
final_data = MessageExternalModel(appid=1, date=timezone.now(), id=1, title=title, message=message, priority=priority, extras=extras)
client = get_client(access_token=token)
response: Any = create_message.sync(client=client, body=final_data)

View file

@ -19,7 +19,7 @@
},
"version": "2.0.2"
},
"host": "push.gooyal.com",
"host": "push.gooyal.ir",
"paths": {
"/application": {
"get": {

View file

@ -1,6 +1,6 @@
openapi: 3.0.0
x-origin:
- url: https://push.gooyal.com/swagger?base=push.gooyal.com%2F
- url: https://push.gooyal.ir/swagger?base=push.gooyal.ir%2F
format: swagger
version: "2.0"
converter:
@ -1452,8 +1452,8 @@ paths:
schema:
$ref: "#/components/schemas/VersionInfo"
servers:
- url: http://push.gooyal.com/
- url: https://push.gooyal.com/
- url: http://push.gooyal.ir/
- url: https://push.gooyal.ir/
components:
securitySchemes:
appTokenAuthorizationHeader: