diff --git a/Dockerfile b/Dockerfile index 75145b2..f2450f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,18 @@ -FROM python:3.10.0 +FROM python:3.10.1 # Addding requirements COPY requirements.txt requirements.txt RUN pip install -U pip && pip install -r requirements.txt --no-cache-dir # Setting working directory WORKDIR /home/sholex +RUN mkdir static +RUN mkdir state +RUN mkdir healthchecks # Healthcheks COPY healthchecks/gunicorn.py healthchecks/gunicorn.py -# Adding Static Directory -RUN mkdir static # Copying source COPY LilSholex LilSholex COPY templates templates COPY manage.py manage.py COPY persianmeme persianmeme -# Setting Volumes -VOLUME /home/sholex/persianmeme/migrations # Running -CMD gunicorn --workers=2 --bind=0.0.0.0:80 --access-logfile /dev/null --error-logfile /dev/stderr LilSholex.wsgi \ No newline at end of file +CMD gunicorn --workers=2 --bind=0.0.0.0:80 --error-logfile /dev/stderr -t 15 LilSholex.wsgi \ No newline at end of file diff --git a/LilSholex/__init__.py b/LilSholex/__init__.py index e69de29..c9f18d7 100755 --- a/LilSholex/__init__.py +++ b/LilSholex/__init__.py @@ -0,0 +1,3 @@ +from .celery import celery_app + +__all__ = ('celery_app',) diff --git a/LilSholex/celery.py b/LilSholex/celery.py new file mode 100644 index 0000000..bb0f790 --- /dev/null +++ b/LilSholex/celery.py @@ -0,0 +1,7 @@ +import os +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'LilSholex.settings') +celery_app = Celery('LilSholex') +celery_app.config_from_object('django.conf:settings', namespace='CELERY') +celery_app.autodiscover_tasks() diff --git a/LilSholex/decorators.py b/LilSholex/decorators.py index 9fa349d..249ad49 100755 --- a/LilSholex/decorators.py +++ b/LilSholex/decorators.py @@ -1,6 +1,7 @@ from aiohttp import ClientError from requests import RequestException -from asyncio import sleep +from asyncio import sleep as async_sleep +from time import sleep as sync_sleep from .exceptions import TooManyRequests @@ -10,7 +11,7 @@ async def check_exception(*args, **kwargs): try: return await func(*args, **kwargs) except ClientError: - await sleep(0.4) + await async_sleep(0.4) return check_exception @@ -21,7 +22,7 @@ def check_exception(*args, **kwargs): try: return func(*args, **kwargs) except TooManyRequests as e: - sleep(e.retry_after) + sync_sleep(e.retry_after) except RequestException: continue diff --git a/LilSholex/settings.py b/LilSholex/settings.py index 68c5ded..ddbb403 100755 --- a/LilSholex/settings.py +++ b/LilSholex/settings.py @@ -35,8 +35,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'background_task', - 'persianmeme.apps.PersianmemeConfig' + 'persianmeme.apps.PersianmemeConfig', ] MIDDLEWARE = [ @@ -122,8 +121,6 @@ STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'static' MAX_ATTEMPTS = 2 -BACKGROUND_TASK_RUN_ASYNC = True -BACKGROUND_TASK_ASYNC_THREADS = 4 # Pagination Limit Broadcast PAGINATION_LIMIT = 1500 @@ -146,5 +143,12 @@ SPAM_TIME = 5 SPAM_PENALTY = 1800 VIOLATION_REPORT_LIMIT = 5 -VIDEO_DURATION_LIMIT = 180 -VIDEO_SIZE_LIMIT = 15728640 +VIDEO_DURATION_LIMIT = 240 +VIDEO_SIZE_LIMIT = 20971520 +# Celery +CELERY_BROKER_URL = 'amqp://guest:guest@rabbitmq:5672/' +CELERY_WORKER_STATE_DB = str(BASE_DIR / 'state' / 'celery_state') +REVOKE_REVIEW_COUNTDOWN = 3600 +CHECK_MEME_COUNTDOWN = 21600 +# CSRF +CSRF_TRUSTED_ORIGINS = [f'https://{ALLOWED_HOSTS[0]}'] diff --git a/LilSholex/urls.py b/LilSholex/urls.py index c61660a..ade1a6f 100755 --- a/LilSholex/urls.py +++ b/LilSholex/urls.py @@ -2,6 +2,6 @@ from django.urls import path, include urlpatterns = [ - path('adminpanel/', admin.site.urls), + path('admin/', admin.site.urls), path('persianmeme/', include('persianmeme.urls')) ] diff --git a/Nginx b/Nginx index 677a18b..5d379d8 100644 --- a/Nginx +++ b/Nginx @@ -1,6 +1,6 @@ FROM ubuntu:impish -ARG NGINX_VERSION=nginx-1.21.4 -ARG OPENSSL_VERSION=openssl-3.0.0 +ARG NGINX_VERSION=nginx-1.21.5 +ARG OPENSSL_VERSION=openssl-3.0.1 # Compiling RUN apt update && DEBIAN_FRONTEND="noninteractive" apt install -y build-essential \ libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev python3.9 python3-pip wget \ diff --git a/conf/my.cnf b/conf/my.cnf index 448e578..0d159ca 100644 --- a/conf/my.cnf +++ b/conf/my.cnf @@ -1,5 +1,5 @@ [mysqld] -innodb_buffer_pool_chunk_size=536870912 -innodb_buffer_pool_instances=4 -innodb_buffer_pool_size=2147483648 -key_buffer_size=1073741824 \ No newline at end of file +innodb_buffer_pool_chunk_size=335544320 +innodb_buffer_pool_instances=8 +innodb_buffer_pool_size=2684354560 +key_buffer_size=2684354560 \ No newline at end of file diff --git a/conf/nginx.conf b/conf/nginx.conf index 57f2225..b5ae6d4 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -7,13 +7,6 @@ events { } http { - server { - access_log off; - listen 80; - server_name localhost; - return 200 'I\'m Up and Running !'; - } - # Buffering client_max_body_size 500k; client_body_buffer_size 100k; @@ -24,6 +17,7 @@ http { send_timeout 3s; tcp_nopush on; sendfile on; + sendfile_max_chunk 256m; # Includes include mime.types; @@ -34,9 +28,9 @@ http { gzip_types text/javascript text/css; # SSL - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_conf_command Options KTLS; ssl_prefer_server_ciphers on; - ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; ssl_session_cache shared:SSL:10m; ssl_session_tickets on; ssl_session_timeout 10m; @@ -45,8 +39,24 @@ http { # Proxy proxy_set_header Host $host; - proxy_buffer_size 4k; - proxy_buffers 4 4k; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffers 4 2m; + + # Servers + server_tokens off; + + server { + access_log off; + listen 80; + server_name localhost; + return 200 'I\'m Up and Running !'; + } + + server { + listen 443 ssl default_server; + access_log /var/log/nginx/rejected.log; + ssl_reject_handshake on; + } server { listen 443 ssl http2; @@ -74,6 +84,14 @@ http { proxy_pass http://gunicorn; } + location /flower/ { + expires 7d; + etag on; + add_header Cache-Control public; + access_log /var/log/nginx/flower.log; + proxy_pass http://celery_flower:5555; + } + location /persianmeme/{persianmeme_token}/ { access_log off; keepalive_requests 100; @@ -86,7 +104,7 @@ http { access_log off; expires 7d; etag on; - root /root; + root /root/lilsholex; try_files $uri @not_found; } diff --git a/conf/rabbitmq.conf b/conf/rabbitmq.conf new file mode 100644 index 0000000..67987d3 --- /dev/null +++ b/conf/rabbitmq.conf @@ -0,0 +1 @@ +consumer_timeout = 28800000 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 381bc4c..7451546 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.8" services: db: - image: mysql:8.0.26 + image: mysql:8.0.27 command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ROOT_PASSWORD_FILE: "/run/secrets/db_password" @@ -37,15 +37,57 @@ services: delay: 5s failure_action: pause monitor: 10s - order: start-first + order: stop-first networks: - db_django volumes: - - type: volume - source: db - target: /var/lib/mysql + - type: volume + source: db + target: /var/lib/mysql + configs: + - source: mysql + target: /etc/mysql/conf.d/mysql.cnf + rabbitmq: + image: rabbitmq:3.9.12 + healthcheck: + test: "rabbitmq-diagnostics -q ping" + interval: 30s + timeout: 5s + start_period: 5m + retries: 3 + networks: + - tasks_django + deploy: + replicas: 1 + placement: + constraints: + - node.labels.task_master==true + restart_policy: + condition: any + max_attempts: 3 + window: 20s + delay: 10s + update_config: + parallelism: 1 + delay: 2s + failure_action: rollback + monitor: 10s + order: stop-first + rollback_config: + parallelism: 1 + delay: 5s + failure_action: pause + monitor: 10s + order: stop-first + volumes: + - type: volume + source: rabbitmq_dump + target: /var/lib/rabbitmq + configs: + - source: rabbitmq + target: /etc/rabbitmq/rabbitmq.conf nginx: - image: ghcr.io/sholex-team/nginx:3.7 + image: ghcr.io/sholex-team/nginx:3.8 networks: - nginx_lilsholex - internet @@ -59,7 +101,7 @@ services: - gunicorn - daphne volumes: - - ./static:/root/static:ro + - ./lilsholex-dev/static:/root/lilsholex/static:ro - type: volume source: nginx target: /var/log/nginx @@ -92,46 +134,51 @@ services: - ssl_key - dhparam ports: - - "443:443/tcp" - - "80:80/tcp" - gunicorn: - image: ghcr.io/sholex-team/lilsholex:5.2 + - "443:443/tcp" + - "80:80/tcp" + lilsholex: + image: ghcr.io/sholex-team/lilsholex:5.3 networks: - db_django - nginx_lilsholex - - internet - cache_django + - internet + - tasks_django secrets: - db_password - persianmeme_channel - - persianmeme_help_messages + - persianmeme_reports + - source: persianmeme_help_messages + target: persianmeme_help_messages - persianmeme_token - secret_key - persianmeme_logs - persianmeme_messages - - persianmeme_reports + - domain depends_on: - db - migrations healthcheck: test: python healthchecks/gunicorn.py - interval: 30s - timeout: 5s - start_period: 1m + interval: 20s + timeout: 20s + start_period: 30s retries: 3 deploy: - replicas: 2 + replicas: 6 placement: + preferences: + - spread: node.labels.zone constraints: - node.labels.webserver==true update_config: - parallelism: 1 + parallelism: 3 delay: 5s failure_action: rollback - monitor: 10s + monitor: 5s order: start-first rollback_config: - parallelism: 1 + parallelism: 2 delay: 5s failure_action: pause monitor: 10s @@ -141,31 +188,37 @@ services: max_attempts: 3 window: 10s delay: 5s - volumes: - - type: volume - source: persianmeme - target: /home/sholex/persianmeme/migrations - tasks: - image: ghcr.io/sholex-team/lilsholex:5.2 - command: bash -c "python manage.py clear_tasks && python manage.py process_tasks --sleep 15" + celery: &celery_base + image: ghcr.io/sholex-team/lilsholex-dev:6.1.5 + command: "celery -A LilSholex worker -Q celery -l info -c 2 -E" networks: - db_django - internet + - tasks_django secrets: - db_password - secret_key - persianmeme_token - persianmeme_channel - - persianmeme_help_messages + - persianmeme_reports + - source: persianmeme_help_messages + target: persianmeme_help_messages - persianmeme_logs - persianmeme_messages - - persianmeme_reports + - domain + volumes: + - type: volume + source: state + target: /home/sholex/state depends_on: - db - migrations + - rabbitmq deploy: replicas: 1 placement: + preferences: + - spread: node.labels.zone constraints: - node.labels.task_master==true update_config: @@ -179,18 +232,21 @@ services: delay: 5s failure_action: pause monitor: 10s - order: start-first + order: stop-first restart_policy: condition: any max_attempts: 3 window: 10s delay: 5s - volumes: - - type: volume - source: persianmeme - target: /home/sholex/persianmeme/migrations - broadcasts: - image: ghcr.io/sholex-team/lilsholex:5.2 + celery_flower: + <<: *celery_base + command: "celery -A LilSholex flower --url_prefix=flower" + depends_on: + - celery + environment: + FLOWER_BASIC_AUTH: "sholex:flower_password" + lilsholex_broadcasts: + image: ghcr.io/sholex-team/lilsholex:5.3 command: "python manage.py process_broadcasts" networks: - db_django @@ -200,10 +256,12 @@ services: - secret_key - persianmeme_token - persianmeme_channel - - persianmeme_help_messages + - persianmeme_reports + - source: persianmeme_help_messages + target: persianmeme_help_messages - persianmeme_logs - persianmeme_messages - - persianmeme_reports + - domain depends_on: - db - migrations @@ -223,16 +281,12 @@ services: delay: 5s failure_action: pause monitor: 10s - order: start-first + order: stop-first restart_policy: condition: any max_attempts: 3 window: 10s delay: 5s - volumes: - - type: volume - source: persianmeme - target: /home/sholex/persianmeme/migrations memcached: image: memcached:1.6.12 command: "memcached -m 256" @@ -262,25 +316,30 @@ services: failure_action: pause monitor: 10s order: start-first - migrations: - image: ghcr.io/sholex-team/lilsholex:5.2 - command: "bash -c 'yes | python manage.py makemigrations persianmeme background_task && \ + lilsholex_migrations: + image: ghcr.io/sholex-team/lilsholex:5.3 + command: "bash -c 'yes | python manage.py makemigrations persianmeme && \ yes | python manage.py migrate && python manage.py collectstatic --noinput'" secrets: - db_password - secret_key - persianmeme_token - persianmeme_channel - - persianmeme_help_messages + - persianmeme_reports + - source: persianmeme_help_messages + target: persianmeme_help_messages - persianmeme_logs - persianmeme_messages - - persianmeme_reports + - domain depends_on: - db networks: - db_django deploy: replicas: 1 + placement: + constraints: + - node.labels.db==true update_config: parallelism: 1 delay: 5s @@ -292,7 +351,7 @@ services: delay: 5s failure_action: pause monitor: 10s - order: start-first + order: stop-first restart_policy: condition: on-failure max_attempts: 3 @@ -311,12 +370,16 @@ networks: driver: "overlay" name: "nginx_lilsholex" internal: true + cache_django: + driver: "overlay" + name: "cache_django" + internal: true internet: driver: "overlay" name: "internet" - cache_django: + tasks_django: driver: "overlay" - name: "cache_django" + name: "tasks_django" internal: true volumes: db: @@ -325,6 +388,10 @@ volumes: name: "persianmeme" nginx: name: "nginx" + state: + name: "state" + rabbitmq_dump: + name: "rabbitmq_dump" secrets: db_password: external: true @@ -336,10 +403,10 @@ secrets: external: true persianmeme_messages: external: true - persianmeme_help_messages: - file: secrets/help_messages.json persianmeme_reports: external: true + persianmeme_help_messages: + file: lilsholex-dev/secrets/help_messages.json # Replace file_id of animations. secret_key: external: true ssl_certificate: @@ -350,3 +417,8 @@ secrets: file: ./ssl/dhparam.pem domain: external: true +configs: + mysql: + file: conf/my.cnf # Change MySQL configuration based on your host. + rabbitmq: + file: conf/rabbitmq.conf diff --git a/persianmeme/admin.py b/persianmeme/admin.py index 72dc87a..de5dcff 100755 --- a/persianmeme/admin.py +++ b/persianmeme/admin.py @@ -221,7 +221,8 @@ def recover_memes(self, request: HttpRequest, queryset): 'deny_vote', 'usage_count', 'reviewed', - 'previous_status' + 'previous_status', + 'task_id' )}) ) @@ -282,7 +283,7 @@ class Message(admin.ModelAdmin): @admin.register(models.MemeTag) -class MemeTa(admin.ModelAdmin): +class MemeTag(admin.ModelAdmin): list_display = ('tag',) search_fields = ('tag',) list_per_page = 30 @@ -319,7 +320,7 @@ class Report(admin.ModelAdmin): 'meme__id', 'meme__name', 'meme__file_id', - 'meme__unique_file_id' + 'meme__file_unique_id' ) raw_id_fields = ('meme', 'reporters') fieldsets = (('Information', {'fields': ('meme', 'reporters')}), ('Status', {'fields': ('status',)})) diff --git a/persianmeme/classes.py b/persianmeme/classes.py index c3059af..65bc419 100755 --- a/persianmeme/classes.py +++ b/persianmeme/classes.py @@ -64,7 +64,7 @@ def get_user(self): self.__ads = models.Ad.objects.exclude(seen=user) return user - def delete_current_voice(self): + def delete_current_meme(self): if self.database.current_meme: self.database.current_meme.delete(admin=self.database, log=True) self.send_message(translations.admin_messages['deleted']) @@ -78,18 +78,9 @@ def delete_meme(self, file_unique_id, meme_type: models.MemeType): status=models.Meme.Status.ACTIVE, type=meme_type )).exists(): - result.first().delete(admin=self.database, log=True) - - @sync_fix - def __delete_voting(self, message_id: int): - with self.session.get( - f'{self._BASE_URL}deleteMessage', - params={'chat_id': settings.MEME_CHANNEL, 'message_id': message_id}, - timeout=settings.REQUESTS_TIMEOUT - ) as response: - if response.status_code != 429: - return - raise TooManyRequests(response.json()['parameters']['retry_after']) + target_meme = result.first() + target_meme.assigned_admin = self.database + target_meme.delete(admin=self.database, log=True) def cancel_voting(self, meme_type: models.MemeType): if not (pending_memes := models.Meme.objects.filter( @@ -100,7 +91,7 @@ def cancel_voting(self, meme_type: models.MemeType): )) return for pending_meme in pending_memes: - self.__delete_voting(pending_meme.message_id) + pending_meme.delete_vote() pending_meme.delete() self.send_message(translations.user_messages['voting_canceled'].format( translations.user_messages['voice' if meme_type == models.MemeType.VOICE else 'video'] @@ -508,7 +499,7 @@ def translate(self, key: str, *formatting_args): self.database.menu_mode == self.database.MenuMode.USER else \ translations.admin_messages[key].format(*formatting_args) - def __check_voice_tags(self, tags: str): + def __check_meme_tags(self, tags: str): if 'tags:' in tags: raise InvalidMemeTag() if len(tags) >= len(punctuation): @@ -530,7 +521,7 @@ def process_meme_tags(self, tags: str): self.send_message(self.translate('send_meme_tags', self.temp_meme_translation)) return False try: - self.__check_voice_tags(tags) + self.__check_meme_tags(tags) except ValueError as e: self.send_message(self.translate(str(e), self.temp_meme_translation)) return False @@ -570,7 +561,7 @@ def add_meme(self, message: dict, status: models.Meme.Status): def validate_meme_name(self, message: dict, text: str, meme_type: models.MemeType or int): if not text or \ - message.get('entities') or len(text) > 50 or text.startswith('tags:') or \ + message.get('entities') or len(text) > 80 or text.startswith('tags:') or \ text.startswith('names:'): self.send_message( self.translate('invalid_meme_name', self.translate( @@ -696,7 +687,7 @@ def assign_meme(self): self.database.current_meme = new_meme.first() self.database.current_meme.assigned_admin = self.database self.database.current_meme.save() - revoke_review(self.database.current_meme.id) + revoke_review.apply_async((self.database.current_meme.id,), countdown=settings.REVOKE_REVIEW_COUNTDOWN) else: self.send_message(translations.admin_messages['no_meme_to_review']) return False diff --git a/persianmeme/functions.py b/persianmeme/functions.py index 663b146..13125f9 100755 --- a/persianmeme/functions.py +++ b/persianmeme/functions.py @@ -294,3 +294,4 @@ def fake_deny_vote(queryset): meme.deny_vote.count() < (random_fake := randint(fake_min, fake_max)): faked_count += 1 meme.deny_vote.set(models.User.objects.all()[:random_fake]) + return faked_count diff --git a/persianmeme/handlers/callback_query/handlers.py b/persianmeme/handlers/callback_query/handlers.py index 9179d0a..5b337d6 100644 --- a/persianmeme/handlers/callback_query/handlers.py +++ b/persianmeme/handlers/callback_query/handlers.py @@ -15,6 +15,9 @@ delete_logs, reports ) +from persianmeme.translations import admin_messages +from persianmeme.keyboards import processed +from django.conf import settings def handler(request, callback_query, user_chat_id): @@ -64,6 +67,10 @@ def handler(request, callback_query, user_chat_id): try: report = Report.objects.get(meme__id=meme_id, status=Report.Status.PENDING) except Report.DoesNotExist: + answer_query(query_id, admin_messages['meme_already_processed'], False) + functions.edit_message_reply_markup( + settings.MEME_REPORTS_CHANNEL, processed, message_id, session=inliner.session + ) inliner.database.save() raise RequestInterruption() reports.handler(command, query_id, message_id, answer_query, report, inliner) diff --git a/persianmeme/handlers/callback_query/menus/reports.py b/persianmeme/handlers/callback_query/menus/reports.py index fb7ee41..497cc15 100644 --- a/persianmeme/handlers/callback_query/menus/reports.py +++ b/persianmeme/handlers/callback_query/menus/reports.py @@ -34,7 +34,7 @@ def handler(command: str, query_id: str, message_id: int, answer_query, report: ) answer_query(query_id, translations.admin_messages['deleted'], True) edit_message_reply_markup( - settings.MEME_REPORTS_CHANNEL, deleted, message_id=message_id, session=inliner.session + settings.MEME_REPORTS_CHANNEL, deleted, message_id, session=inliner.session ) else: if report.meme.status == Meme.Status.REPORTED: diff --git a/persianmeme/handlers/message/menus/admin/menus/meme_review.py b/persianmeme/handlers/message/menus/admin/menus/meme_review.py index cfd0078..5d02f7a 100644 --- a/persianmeme/handlers/message/menus/admin/menus/meme_review.py +++ b/persianmeme/handlers/message/menus/admin/menus/meme_review.py @@ -26,7 +26,7 @@ def handler(text: str, message_id: int, user: UserClass): admin_messages['edit_meme_description'].format(user.current_meme_translation), en_back ) case 'Delete 🗑': - user.delete_current_voice() + user.delete_current_meme() if not user.assign_meme(): user.go_back() case 'Check the Meme': diff --git a/persianmeme/keyboards.py b/persianmeme/keyboards.py index 31fa68b..f11bcfd 100755 --- a/persianmeme/keyboards.py +++ b/persianmeme/keyboards.py @@ -81,6 +81,7 @@ deleted = {'inline_keyboard': [[{'text': 'Deleted 🗑', 'callback_data': 'none'}]]} recovered = {'inline_keyboard': [[{'text': 'Recovered ♻', 'callback_data': 'none'}]]} dismissed = {'inline_keyboard': [[{'text': 'Dismissed ✔', 'callback_data': 'none'}]]} +processed = {'inline_keyboard': [[{'text': 'Processed ✔', 'callback_data': 'none'}]]} def suggestion_vote(meme_id: int): diff --git a/persianmeme/models.py b/persianmeme/models.py index f6a4e1e..010b87c 100755 --- a/persianmeme/models.py +++ b/persianmeme/models.py @@ -9,6 +9,7 @@ from . import translations from functools import cached_property from LilSholex.exceptions import TooManyRequests +from LilSholex.celery import celery_app class MemeType(models.IntegerChoices): @@ -120,7 +121,7 @@ class Menu(models.IntegerChoices): status = models.CharField(max_length=1, choices=Status.choices, default=Status.ACTIVE) rank = models.CharField(max_length=1, choices=Rank.choices, default=Rank.USER) username = models.CharField(max_length=35, null=True, blank=True) - temp_meme_name = models.CharField(max_length=50, null=True, verbose_name='Temporary Meme Name', blank=True) + temp_meme_name = models.CharField(max_length=80, null=True, verbose_name='Temporary Meme Name', blank=True) temp_user_id = models.BigIntegerField(null=True, verbose_name='Temporary User ID', blank=True) temp_meme_tags = models.ManyToManyField( MemeTag, 'user_voice_tags', blank=True, verbose_name='Temporary Voice Tags' @@ -199,6 +200,7 @@ class Visibility(models.TextChoices): reviewed = models.BooleanField('Is Reviewed', default=False) type = models.PositiveSmallIntegerField('Meme Type', choices=MemeType.choices, default=MemeType.VOICE) description = models.CharField(max_length=120, blank=True, null=True) + task_id = models.CharField(max_length=36, blank=True, null=True) class Meta: db_table = 'persianmeme_memes' @@ -281,7 +283,7 @@ def delete(self, *args, **kwargs): meme_recovery(self.id), translations.admin_messages['deleted_by_admins'].format( translations.admin_messages[self.type_string], - kwargs.pop("admin") if kwargs.get('admin') else '', + kwargs.pop('admin'), self.file_id ) ) @@ -291,15 +293,15 @@ def delete(self, *args, **kwargs): @sync_fix def delete_vote(self, session: requests.Session = requests.Session()): - from background_task.models import Task - - Task.objects.filter(task_name='persianmeme.tasks.check_meme', task_params=f'[[{self.id}], ''{}]').delete() + celery_app.control.revoke(self.task_id) with session.get( f'https://api.telegram.org/bot{settings.MEME}/deleteMessage', params={'chat_id': settings.MEME_CHANNEL, 'message_id': self.message_id}, timeout=settings.REQUESTS_TIMEOUT - ): - return + ) as response: + if response.status_code != 429: + return + raise TooManyRequests(response.json()['parameters']['retry_after']) def send_vote(self, session: requests.Session = requests.Session()): from persianmeme.tasks import check_meme @@ -310,7 +312,8 @@ def send_vote(self, session: requests.Session = requests.Session()): suggestion_vote(self.id) ) self.save() - check_meme(self.id) + self.task_id = check_meme.apply_async((self.id,), countdown=settings.CHECK_MEME_COUNTDOWN) + self.save() class Ad(models.Model): diff --git a/persianmeme/tasks.py b/persianmeme/tasks.py index 69fa949..85bb778 100755 --- a/persianmeme/tasks.py +++ b/persianmeme/tasks.py @@ -1,11 +1,11 @@ from .models import Meme -from background_task import background from zoneinfo import ZoneInfo from datetime import datetime -from background_task.models import CompletedTask +from LilSholex import celery_app +from django.conf import settings -@background(schedule=3600) +@celery_app.task def revoke_review(meme_id: int): try: target_meme = Meme.objects.get(id=meme_id, reviewed=False, status=Meme.Status.ACTIVE) @@ -15,19 +15,21 @@ def revoke_review(meme_id: int): target_meme.save() -@background(schedule=21600) +@celery_app.task def check_meme(meme_id: int): - CompletedTask.objects.all().delete() try: meme = Meme.objects.get(id=meme_id, status=Meme.Status.PENDING) except Meme.DoesNotExist: return if datetime.now(ZoneInfo('Asia/Tehran')).hour < 8: - return check_meme(meme_id) + meme.task_id = check_meme.apply_async((meme_id,), countdown=settings.CHECK_MEME_COUNTDOWN) + meme.save() + return accept_count = meme.accept_vote.count() deny_count = meme.deny_vote.count() if accept_count == deny_count == 0: - return check_meme(meme_id) + meme.task_id = check_meme.apply_async((meme_id,), countdown=settings.CHECK_MEME_COUNTDOWN) + meme.save() else: meme.delete_vote() if accept_count >= deny_count: diff --git a/persianmeme/translations.py b/persianmeme/translations.py index 12ff484..2001286 100644 --- a/persianmeme/translations.py +++ b/persianmeme/translations.py @@ -96,7 +96,8 @@ 'unknown_meme': 'Meme was not found !', 'new_delete_request': 'New delete request 🗑', 'report_dismissed': 'Report has been dismissed ✔', - 'description': 'Description: {0}\n\n' + 'description': 'Description: {0}\n\n', + 'meme_already_processed': 'Meme was already processed ✔' } user_messages = { 'back': 'شما به منوی اصلی بازگشتید 🔙', @@ -105,19 +106,6 @@ 'vote_before': 'شما قبلا به این {0} رای داده اید ⚠️\nنتایج هر ۳ دقیقه به روزرسانی می شوند 🔄', 'voted': 'رای شما ثبت شد ✔️', 'donate': '''برای حمایت مالی از ما می توانید از روش های زیر استفاده کنید 👇 - - PayPing : https://payping.ir/RezFD - - IDPay : https://idpay.ir/persianmeme - - Bitcoin : `12wL8ggGqNA52JKUGtAP9TrNNxKUw5E7tT` - - Ether : `0x15ce953E6dd57b64f4360DE14a2DE00f87d7be06` - - Tether : `0x15ce953E6dd57b64f4360DE14a2DE00f87d7be06` - - Litecoin: `Lc7rPW4vgbeKwEYQw7gt7kmJ1grY9vWvoR` - از حمایت های شما مچکریم 🙏''', 'managing_playlist': 'مدیریت پلی لیست ⚙️', 'manage_meme': 'مدیریت {0} ⚙️', @@ -152,7 +140,7 @@ '@Persian_Meme_Bot {1}\n\n از این {0} استفاده کنید 😁', 'meme_not_found': 'نتونستم این {0} رو پیدا کنم ☹', 'message_sent': 'پیام شما به مدیریت ارسال شد ✔', - 'invalid_meme_name': 'نام {0} معتبر نیست ❌\nنام باید متن ساده و حداکثر ۵۰ کارکتر باشد !', + 'invalid_meme_name': 'نام {0} معتبر نیست ❌\nنام باید متن ساده و حداکثر ۸۰ کارکتر باشد !', 'meme_added': '{0} شما برای تایید به کانال رای گیری ارسال شد 👇\n🆔 @PersianMemeVoting', 'meme_already_exists': 'این {0} در ربات موجود میباشد ⚠', 'meme_deleted': '{0} شما با موفقیت حذف شد 🗑', @@ -225,5 +213,5 @@ 'search_item_videos_and_voices': 'جستجو در بین ویس ها و ویدئو ها انجام خواهد شد ✔', 'select_search_items': 'جستجو در بین کدام یک از آیتم های زیر انجام شود ؟', 'send_a_video': 'لطفا ویدئو مورد نظر را ارسال کنید.' - '\n\n🔴 ویدئو باید در تلگرام قابل پخش، حداکثر ۳ دقیقه و کوچک تر از ۱۵ مگابایت باشد ⚠' + '\n\n🔴 ویدئو باید در تلگرام قابل پخش، حداکثر ۴ دقیقه و کوچک تر از ۲۰ مگابایت باشد ⚠' } diff --git a/requirements.txt b/requirements.txt index 076927b..0b6563c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,49 +1,63 @@ aiodns==3.0.0 -aiohttp==3.7.4.post0 +aiohttp==3.8.1 +aiosignal==1.2.0 +amqp==5.0.9 asgiref==3.4.1 asn1crypto==1.4.0 -astroid==2.8.0 -async-timeout==3.0.1 -attrs==21.2.0 -autobahn==21.3.1 +astroid==2.9.3 +async-timeout==4.0.2 +attrs==21.4.0 +autobahn==21.11.1 Automat==20.2.0 +billiard==3.6.4.0 brotlipy==0.7.0 cchardet==2.1.7 -certifi==2021.5.30 -cffi==1.14.6 +celery==5.2.3 +certifi==2021.10.8 +cffi==1.15.0 chardet==4.0.0 -charset-normalizer==2.0.5 +charset-normalizer==2.0.10 +click==8.0.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.2.0 constantly==15.1.0 -cryptography==3.4.8 -Django==3.2.7 -django-background-tasks==1.2.5 -django-compat==1.0.15 +cryptography==36.0.1 +Django==4.0.1 +flower==1.0.0 +frozenlist==1.2.0 gunicorn==20.1.0 +humanize==3.13.1 hyperlink==21.0.0 -idna==2.10 +idna==3.3 incremental==21.3.0 -isort==5.9.3 -lazy-object-proxy==1.6.0 +isort==5.10.1 +kombu==5.2.3 +lazy-object-proxy==1.7.1 mccabe==0.6.1 -multidict==5.1.0 -mysqlclient==2.0.3 +multidict==5.2.0 +mysqlclient==2.1.0 +prometheus-client==0.12.0 +prompt-toolkit==3.0.24 pyasn1==0.4.8 pyasn1-modules==0.2.8 -pycares==4.0.0 -pycparser==2.20 -PyHamcrest==2.0.2 +pycares==4.1.2 +pycparser==2.21 +PyHamcrest==2.0.3 pymemcache==3.5.0 -pyOpenSSL==20.0.1 -pytz==2021.1 -requests==2.25.1 -sentry-sdk==1.3.1 +pyOpenSSL==21.0.0 +pytz==2021.3 +requests==2.27.1 +sentry-sdk==1.5.2 service-identity==21.1.0 six==1.16.0 sqlparse==0.4.2 toml==0.10.2 +tornado==6.1 txaio==21.2.1 -typing-extensions==3.10.0.2 -urllib3==1.26.6 -wrapt==1.12.1 -yarl==1.6.3 +urllib3==1.26.8 +vine==5.0.0 +wcwidth==0.2.5 +wrapt==1.13.3 +yarl==1.7.2 zope.interface==5.4.0 diff --git a/secrets/help_messages.json b/secrets/help_messages.json new file mode 100644 index 0000000..cef127c --- /dev/null +++ b/secrets/help_messages.json @@ -0,0 +1,26 @@ +{ + "جستجو با نام": { + "animation": "CgACAgQAAxkBAAFZLulgZtrVVZ_JySfT11ISeu8o6YwTHgACIwoAAuZbIFNBhpI1BkXZwR4E", + "caption": "راهنمای جستجو ویس با استفاده از نام \uD83D\uDC46" + }, + "جستجو با تگ": { + "animation": "CgACAgQAAxkBAAFZLvxgZtsgqYrRFxNphpJ0bCVvTb7ZdgACGwoAAuZbIFPmydlQTWchxR4E", + "caption": "راهنمای جستجو ویس با استفاده تگ ها \uD83D\uDC46" + }, + "جستجوی عمومی": { + "animation": "CgACAgQAAxkBAAFZLxNgZtttHJ0QNelkWA83A1z3QbgLDwACBQgAAgYtuFK12Zuzheg3Lh4E", + "caption": "راهنمای جستجوی عمومی ویس ها \uD83D\uDC46" + }, + "پلی لیست ها": { + "animation": "CgACAgQAAxkBAAFZLxxgZtunnHA8-VQEIdDmaNiDFgfCWgACrggAA3HRURHVLtAwpklyHgQ", + "caption": "آموزش ساخت و استفاده از پلی لیست ها \uD83D\uDC46" + }, + "افزودن ویس عمومی": { + "animation": "CgACAgQAAxkBAROpXGHNKYlqTtOM_neWenIFB-4IlCBjAALJDAACKi-4UKkOJQI3G3bxIwQ", + "caption": "آموزش اضافه کردن ویس عمومی به ربات \uD83D\uDC46" + }, + "افزودن ویدئو عمومی": { + "animation": "CgACAgQAAxkBAROpUGHNKLZNkH0jemgSzsrLD_FfbCB1AAIDDQACKi-4UJ6ZuNXt5uZeIwQ", + "caption": "آموزش اضافه کردن ویدئو عمومی به ربات \uD83D\uDC46" + } +}