diff --git a/blt/.env.example b/.env.example similarity index 79% rename from blt/.env.example rename to .env.example index 66f481813..8c1b9eeb6 100644 --- a/blt/.env.example +++ b/.env.example @@ -16,3 +16,9 @@ LANGCHAIN_API_KEY=langchain_api_key LANGCHAIN_TRACING_V2=true LANGCHAIN_PROJECT=default LANGCHAIN_ENDPOINT="https://api.smith.langchain.com" + +#Database URL +DATABASE_URL=postgres://user:password@localhost:5432/dbname + +#Sentry DSN +SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 \ No newline at end of file diff --git a/blt/middleware/ip_restrict.py b/blt/middleware/ip_restrict.py index b9d2ac145..ef5f6b860 100644 --- a/blt/middleware/ip_restrict.py +++ b/blt/middleware/ip_restrict.py @@ -152,8 +152,9 @@ def __call__(self, request): ip_record.path = request.path ip_record.save(update_fields=["agent", "count", "path"]) - # Delete all but the first record - ip_records.exclude(pk=ip_record.pk).delete() + # Check if a transaction is already active before starting a new one + if not transaction.get_autocommit(): + ip_records.exclude(pk=ip_record.pk).delete() else: # If no record exists, create a new one IP.objects.create(address=ip, agent=agent, count=1, path=request.path) diff --git a/blt/settings.py b/blt/settings.py index 7b951f2e8..d38d2a3d4 100644 --- a/blt/settings.py +++ b/blt/settings.py @@ -423,11 +423,39 @@ }, } -SOCIALACCOUNT_PROVIDER = { - "github": {"scope": ("user:email",)}, - "google": {"scope": ("user:email",)}, +SOCIALACCOUNT_PROVIDERS = { + "github": { + "SCOPE": ["user:email"], + "AUTH_PARAMS": {"access_type": "online"}, + }, + "google": { + "SCOPE": ["profile", "email"], + "AUTH_PARAMS": {"access_type": "online"}, + }, + "facebook": { + "METHOD": "oauth2", + "SCOPE": ["email"], + "FIELDS": [ + "id", + "email", + "name", + "first_name", + "last_name", + "verified", + "locale", + "timezone", + "link", + ], + "EXCHANGE_TOKEN": True, + "LOCALE_FUNC": lambda request: "en_US", + "VERIFIED_EMAIL": False, + "VERSION": "v7.0", + }, } +ACCOUNT_ADAPTER = "allauth.account.adapter.DefaultAccountAdapter" +SOCIALACCOUNT_ADAPTER = "allauth.socialaccount.adapter.DefaultSocialAccountAdapter" + X_FRAME_OPTIONS = "SAMEORIGIN" MDEDITOR_CONFIGS = { diff --git a/blt/urls.py b/blt/urls.py index 2e8963dbd..bca61e59b 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -37,7 +37,7 @@ UserIssueViewSet, UserProfileViewSet, ) -from website.views import ( # TODO AutoLabel, +from website.class_views import ( AllIssuesView, CompanySettings, ContributorStatsView, @@ -64,9 +64,9 @@ ListHunts, OngoingHunts, PreviousHunts, + ProjectBadgeView, ProjectDetailView, ProjectListView, - SaveBiddingData, ScoreboardView, SpecificIssuesView, SpecificMonthLeaderboardView, @@ -76,6 +76,9 @@ UserDeleteView, UserProfileDetailsView, UserProfileDetailView, +) +from website.views import ( # TODO AutoLabel, + SaveBiddingData, add_suggestions, blt_tomato, change_bid_status, @@ -452,6 +455,7 @@ re_path(r"^api/v1/count/$", website.views.issue_count, name="api_count"), re_path(r"^api/v1/contributors/$", website.views.contributors, name="api_contributor"), path("project//", ProjectDetailView.as_view(), name="project_view"), + path("projects//badge/", ProjectBadgeView.as_view(), name="project-badge"), re_path( r"^api/v1/createissues/$", csrf_exempt(IssueCreate.as_view()), @@ -558,6 +562,11 @@ ), path("auth/delete", AuthApiViewset.as_view({"delete": "delete"}), name="auth-delete-api"), path("api/v1/tags", TagApiViewset.as_view({"get": "list", "post": "create"}), name="tags-api"), + path("sizzle/", website.views.sizzle, name="sizzle"), + path("sizzle-docs/", website.views.sizzle_docs, name="sizzle-docs"), + path("api/timelogsreport/", website.views.TimeLogListAPIView, name="timelogsreport"), + path("time-logs/", website.views.TimeLogListView, name="time_logs"), + path("sizzle-daily-log/", website.views.sizzle_daily_log, name="sizzle_daily_log"), ] if settings.DEBUG: diff --git a/comments/views.py b/comments/views.py index 7c8935d83..5388781a0 100644 --- a/comments/views.py +++ b/comments/views.py @@ -143,6 +143,7 @@ def reply_comment(request, pk): @login_required(login_url="/accounts/login") def autocomplete(request): q_string = request.GET.get("search", "") + q_string = escape(q_string) if len(q_string) == 0: return HttpResponse( request.GET["callback"] + "(" + json.dumps([]) + ");", content_type="application/json" diff --git a/company/views.py b/company/views.py index d2c62e9fb..85f883a10 100644 --- a/company/views.py +++ b/company/views.py @@ -1,14 +1,13 @@ import json import uuid from datetime import datetime, timedelta -from urllib.parse import urlparse, urlunparse +from urllib.parse import urlparse import requests from django.contrib import messages from django.contrib.auth.models import AnonymousUser, User from django.core.exceptions import ValidationError from django.core.files.storage import default_storage -from django.core.validators import URLValidator from django.db import transaction from django.db.models import Count, OuterRef, Q, Subquery, Sum from django.db.models.functions import ExtractMonth @@ -18,35 +17,12 @@ from django.views.decorators.http import require_http_methods from django.views.generic import View -from website.models import ( - Company, - Domain, - Hunt, - HuntPrize, - Issue, - IssueScreenshot, - Trademark, - Winner, -) +from website.models import Company, Domain, Hunt, HuntPrize, Issue, IssueScreenshot, Winner +from website.utils import is_valid_https_url, rebuild_safe_url restricted_domain = ["gmail.com", "hotmail.com", "outlook.com", "yahoo.com", "proton.com"] -def is_valid_https_url(url): - validate = URLValidator(schemes=["https"]) # Only allow HTTPS URLs - try: - validate(url) - return True - except ValidationError: - return False - - -def rebuild_safe_url(url): - parsed_url = urlparse(url) - # Rebuild the URL with scheme, netloc, and path only - return urlunparse((parsed_url.scheme, parsed_url.netloc, parsed_url.path, "", "", "")) - - def get_email_domain(email): domain = email.split("@")[-1] return domain diff --git a/docker-build-test.yml b/docker-build-test.yml new file mode 100644 index 000000000..6f5d19be0 --- /dev/null +++ b/docker-build-test.yml @@ -0,0 +1,47 @@ +name: Docker Build and Serve Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build-and-serve: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Build Docker image + run: docker build -t owasp-blt . + + - name: Run Docker container + run: docker run -d -p 8000:8000 owasp-blt + + - name: Wait for container to be ready + run: | + for i in {1..30}; do + if curl -s http://localhost:8000 > /dev/null; then + exit 0 + fi + sleep 1 + done + exit 1 + + - name: Verify site is served + run: curl -s http://localhost:8000 diff --git a/poetry.lock b/poetry.lock index 21cdc70c9..75cea7f8c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -325,78 +325,78 @@ files = [ [[package]] name = "cffi" -version = "1.17.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, - {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, - {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, - {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, - {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, - {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, - {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, - {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, - {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, - {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, - {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, - {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, - {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, - {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, - {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, - {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -773,17 +773,17 @@ files = [ [[package]] name = "dj-database-url" -version = "2.2.0" +version = "2.3.0" description = "Use Database URLs in your Django Application." optional = false python-versions = "*" files = [ - {file = "dj_database_url-2.2.0-py3-none-any.whl", hash = "sha256:3e792567b0aa9a4884860af05fe2aa4968071ad351e033b6db632f97ac6db9de"}, - {file = "dj_database_url-2.2.0.tar.gz", hash = "sha256:9f9b05058ddf888f1e6f840048b8d705ff9395e3b52a07165daa3d8b9360551b"}, + {file = "dj_database_url-2.3.0-py3-none-any.whl", hash = "sha256:bb0d414ba0ac5cd62773ec7f86f8cc378a9dbb00a80884c2fc08cc570452521e"}, + {file = "dj_database_url-2.3.0.tar.gz", hash = "sha256:ae52e8e634186b57e5a45e445da5dc407a819c2ceed8a53d1fac004cc5288787"}, ] [package.dependencies] -Django = ">=3.2" +Django = ">=4.2" typing_extensions = ">=3.10.0.0" [[package]] @@ -805,17 +805,17 @@ with-social = ["django-allauth (>=0.56.0,<0.58.0)"] [[package]] name = "django" -version = "5.0.8" +version = "5.1.1" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.0.8-py3-none-any.whl", hash = "sha256:333a7988f7ca4bc14d360d3d8f6b793704517761ae3813b95432043daec22a45"}, - {file = "Django-5.0.8.tar.gz", hash = "sha256:ebe859c9da6fead9c9ee6dbfa4943b04f41342f4cea2c4d8c978ef0d10694f2b"}, + {file = "Django-5.1.1-py3-none-any.whl", hash = "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f"}, + {file = "Django-5.1.1.tar.gz", hash = "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2"}, ] [package.dependencies] -asgiref = ">=3.7.0,<4" +asgiref = ">=3.8.1,<4" sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} @@ -890,18 +890,18 @@ Django = ">=2.2" [[package]] name = "django-cors-headers" -version = "4.4.0" +version = "4.6.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"}, - {file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"}, + {file = "django_cors_headers-4.6.0-py3-none-any.whl", hash = "sha256:8edbc0497e611c24d5150e0055d3b178c6534b8ed826fb6f53b21c63f5d48ba3"}, + {file = "django_cors_headers-4.6.0.tar.gz", hash = "sha256:14d76b4b4c8d39375baeddd89e4f08899051eeaf177cb02a29bd6eae8cf63aa8"}, ] [package.dependencies] asgiref = ">=3.6" -django = ">=3.2" +django = ">=4.2" [[package]] name = "django-debug-toolbar" @@ -960,13 +960,13 @@ Django = ">=4.2" [[package]] name = "django-gravatar2" -version = "1.4.4" +version = "1.4.5" description = "Essential Gravatar support for Django. Features helper methods, templatetags and a full test suite!" optional = false python-versions = "*" files = [ - {file = "django-gravatar2-1.4.4.tar.gz", hash = "sha256:c813280967511ced93eea0359f60e5369c35b3311efe565c3e5d4ab35c10c9ee"}, - {file = "django_gravatar2-1.4.4-py2.py3-none-any.whl", hash = "sha256:545a6c2c5c624c7635dec29c7bc0be1a2cb89c9b8821af8616ae9838827cc35b"}, + {file = "django_gravatar2-1.4.5-py2.py3-none-any.whl", hash = "sha256:7e6c8f63f59b0077d42402531684807ea6295867ebd2195a638d87b851f0d41c"}, + {file = "django_gravatar2-1.4.5.tar.gz", hash = "sha256:2dbb56465e395dd8b3920d4017e27a4756912cc2ad9b11ba48cf143871a80364"}, ] [[package]] @@ -1159,13 +1159,13 @@ django = ">=4.2" [[package]] name = "drf-yasg" -version = "1.21.7" +version = "1.21.8" description = "Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code." optional = false python-versions = ">=3.6" files = [ - {file = "drf-yasg-1.21.7.tar.gz", hash = "sha256:4c3b93068b3dfca6969ab111155e4dd6f7b2d680b98778de8fd460b7837bdb0d"}, - {file = "drf_yasg-1.21.7-py3-none-any.whl", hash = "sha256:f85642072c35e684356475781b7ecf5d218fff2c6185c040664dd49f0a4be181"}, + {file = "drf-yasg-1.21.8.tar.gz", hash = "sha256:cbb7f81c3d140f2207392b4bc5dde65384eeb58e1b7eea1a6d641dec2f7352a9"}, + {file = "drf_yasg-1.21.8-py3-none-any.whl", hash = "sha256:a410b235e7cc2c0f6b9d4f671e8efe6f2d27cba398fbd16064e16ef814998444"}, ] [package.dependencies] @@ -1676,6 +1676,27 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "html5lib" +version = "1.1" +description = "HTML parser based on the WHATWG HTML specification" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, + {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, +] + +[package.dependencies] +six = ">=1.9" +webencodings = "*" + +[package.extras] +all = ["chardet (>=2.2)", "genshi", "lxml"] +chardet = ["chardet (>=2.2)"] +genshi = ["genshi"] +lxml = ["lxml"] + [[package]] name = "httpcore" version = "1.0.5" @@ -1904,18 +1925,18 @@ files = [ [[package]] name = "langchain" -version = "0.2.14" +version = "0.2.16" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain-0.2.14-py3-none-any.whl", hash = "sha256:eed76194ee7d9c081037a3df7868d4de90e0410b51fc1ca933a8379e464bf40c"}, - {file = "langchain-0.2.14.tar.gz", hash = "sha256:dc2aa5a58882054fb5d043c39ab8332ebd055f88f17839da68e1c7fd0a4fefe2"}, + {file = "langchain-0.2.16-py3-none-any.whl", hash = "sha256:8f59ee8b45f268df4b924ea3b9c63e49286efa756d16b3f6a9de5c6e502c36e1"}, + {file = "langchain-0.2.16.tar.gz", hash = "sha256:ffb426a76a703b73ac69abad77cd16eaf03dda76b42cff55572f592d74944166"}, ] [package.dependencies] aiohttp = ">=3.8.3,<4.0.0" -langchain-core = ">=0.2.32,<0.3.0" +langchain-core = ">=0.2.38,<0.3.0" langchain-text-splitters = ">=0.2.0,<0.3.0" langsmith = ">=0.1.17,<0.2.0" numpy = {version = ">=1,<2", markers = "python_version < \"3.12\""} @@ -1927,21 +1948,21 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" [[package]] name = "langchain-community" -version = "0.2.11" +version = "0.2.17" description = "Community contributed LangChain integrations." optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_community-0.2.11-py3-none-any.whl", hash = "sha256:465c03ba1603975d141533424185e09546ecf09e379c93aee2671bdc9b325cda"}, - {file = "langchain_community-0.2.11.tar.gz", hash = "sha256:ede261ff8202f1433f004ee90baf89f371cee37cb1abfc16dd0f8392db10b23e"}, + {file = "langchain_community-0.2.17-py3-none-any.whl", hash = "sha256:d07c31b641e425fb8c3e7148ad6a62e1b54a9adac6e1173021a7dd3148266063"}, + {file = "langchain_community-0.2.17.tar.gz", hash = "sha256:b0745c1fcf1bd532ed4388f90b47139d6a6c6ba48a87aa68aa32d4d6bb97259d"}, ] [package.dependencies] aiohttp = ">=3.8.3,<4.0.0" dataclasses-json = ">=0.5.7,<0.7" -langchain = ">=0.2.12,<0.3.0" -langchain-core = ">=0.2.27,<0.3.0" -langsmith = ">=0.1.0,<0.2.0" +langchain = ">=0.2.16,<0.3.0" +langchain-core = ">=0.2.39,<0.3.0" +langsmith = ">=0.1.112,<0.2.0" numpy = {version = ">=1,<2", markers = "python_version < \"3.12\""} PyYAML = ">=5.3" requests = ">=2,<3" @@ -1950,18 +1971,18 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" [[package]] name = "langchain-core" -version = "0.2.34" +version = "0.2.42" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_core-0.2.34-py3-none-any.whl", hash = "sha256:c4fd158273e28cef758b4eccc956b424b76d4bb9117ce6014ae6eb2fb985801d"}, - {file = "langchain_core-0.2.34.tar.gz", hash = "sha256:50048d90b175c0d5a7e28164628b3c7f8c82b0dc2cd766a663d346a18d5c9eb2"}, + {file = "langchain_core-0.2.42-py3-none-any.whl", hash = "sha256:09503fdfb9efa163e51f2d9762894fde04797d0a41462c0e6072ef78028e48fd"}, + {file = "langchain_core-0.2.42.tar.gz", hash = "sha256:e4ea04b22bd6398048d0ef97cd3132fbdd80e6c749863ee96e6b7c88502ff913"}, ] [package.dependencies] jsonpatch = ">=1.33,<2.0" -langsmith = ">=0.1.75,<0.2.0" +langsmith = ">=0.1.112,<0.2.0" packaging = ">=23.2,<25" pydantic = {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""} PyYAML = ">=5.3" @@ -1970,17 +1991,17 @@ typing-extensions = ">=4.7" [[package]] name = "langchain-openai" -version = "0.1.22" +version = "0.1.25" description = "An integration package connecting OpenAI and LangChain" optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_openai-0.1.22-py3-none-any.whl", hash = "sha256:e184ab867a30f803dc210a388537186b1b670a33d910a7e0fa4e0329d3b6c654"}, - {file = "langchain_openai-0.1.22.tar.gz", hash = "sha256:0cf93133f230a893e3b0cc2a792bbf2580950e879b577f6e8d4ff9963a7de44b"}, + {file = "langchain_openai-0.1.25-py3-none-any.whl", hash = "sha256:f0b34a233d0d9cb8fce6006c903e57085c493c4f0e32862b99063b96eaedb109"}, + {file = "langchain_openai-0.1.25.tar.gz", hash = "sha256:eb116f744f820247a72f54313fb7c01524fba0927120d4e899e5e4ab41ad3928"}, ] [package.dependencies] -langchain-core = ">=0.2.33,<0.3.0" +langchain-core = ">=0.2.40,<0.3.0" openai = ">=1.40.0,<2.0.0" tiktoken = ">=0.7,<1" @@ -2014,16 +2035,17 @@ six = "*" [[package]] name = "langsmith" -version = "0.1.98" +version = "0.1.117" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langsmith-0.1.98-py3-none-any.whl", hash = "sha256:f79e8a128652bbcee4606d10acb6236973b5cd7dde76e3741186d3b97b5698e9"}, - {file = "langsmith-0.1.98.tar.gz", hash = "sha256:e07678219a0502e8f26d35294e72127a39d25e32fafd091af5a7bb661e9a6bd1"}, + {file = "langsmith-0.1.117-py3-none-any.whl", hash = "sha256:e936ee9bcf8293b0496df7ba462a3702179fbe51f9dc28744b0fbec0dbf206ae"}, + {file = "langsmith-0.1.117.tar.gz", hash = "sha256:a1b532f49968b9339bcaff9118d141846d52ed3d803f342902e7448edf1d662b"}, ] [package.dependencies] +httpx = ">=0.23.0,<1" orjson = ">=3.9.14,<4.0.0" pydantic = {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""} requests = ">=2,<3" @@ -2438,15 +2460,29 @@ rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] +[[package]] +name = "olefile" +version = "0.47" +description = "Python package to parse, read and write Microsoft OLE2 files (Structured Storage or Compound Document, Microsoft Office)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f"}, + {file = "olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + [[package]] name = "openai" -version = "1.41.1" +version = "1.53.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.41.1-py3-none-any.whl", hash = "sha256:56fb04105263f79559aff3ceea2e1dd16f8c5385e8238cb66cf0e6888fa8bfcf"}, - {file = "openai-1.41.1.tar.gz", hash = "sha256:e38e376efd91e0d4db071e2a6517b6b4cac1c2a6fd63efdc5ec6be10c5967c1b"}, + {file = "openai-1.53.0-py3-none-any.whl", hash = "sha256:20f408c32fc5cb66e60c6882c994cdca580a5648e10045cd840734194f033418"}, + {file = "openai-1.53.0.tar.gz", hash = "sha256:be2c4e77721b166cce8130e544178b7d579f751b4b074ffbaade3854b6f85ec5"}, ] [package.dependencies] @@ -2781,83 +2817,78 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "psycopg2-binary" -version = "2.9.9" +version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, + {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, ] [[package]] @@ -3198,6 +3229,22 @@ files = [ {file = "python-openid-2.2.5.zip", hash = "sha256:c2d133e47e0a7705c9272eef00d7a09c174f5bf17a127fed8e2c6499556cc782"}, ] +[[package]] +name = "python-oxmsg" +version = "0.0.1" +description = "Extract attachments from Outlook .msg files." +optional = false +python-versions = ">=3.9" +files = [ + {file = "python_oxmsg-0.0.1-py3-none-any.whl", hash = "sha256:8ea7d5dda1bc161a413213da9e18ed152927c1fda2feaf5d1f02192d8ad45eea"}, + {file = "python_oxmsg-0.0.1.tar.gz", hash = "sha256:b65c1f93d688b85a9410afa824192a1ddc39da359b04a0bd2cbd3874e84d4994"}, +] + +[package.dependencies] +click = "*" +olefile = "*" +typing-extensions = ">=4.9.0" + [[package]] name = "python3-openid" version = "3.2.0" @@ -3560,40 +3607,40 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.6.2" +version = "0.7.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, - {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, - {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, - {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, - {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, - {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, - {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, + {file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"}, + {file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"}, + {file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"}, + {file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"}, + {file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"}, + {file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"}, + {file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"}, ] [[package]] name = "selenium" -version = "4.23.1" +version = "4.26.0" description = "Official Python bindings for Selenium WebDriver" optional = false python-versions = ">=3.8" files = [ - {file = "selenium-4.23.1-py3-none-any.whl", hash = "sha256:3a8d9f23dc636bd3840dd56f00c2739e32ec0c1e34a821dd553e15babef24477"}, - {file = "selenium-4.23.1.tar.gz", hash = "sha256:128d099e66284437e7128d2279176ec7a06e6ec7426e167f5d34987166bd8f46"}, + {file = "selenium-4.26.0-py3-none-any.whl", hash = "sha256:48013f36e812de5b3948ef53d04e73f77bc923ee3e1d7d99eaf0618179081b99"}, + {file = "selenium-4.26.0.tar.gz", hash = "sha256:f0780f85f10310aa5d085b81e79d73d3c93b83d8de121d0400d543a50ee963e8"}, ] [package.dependencies] @@ -3621,13 +3668,13 @@ starkbank-ecdsa = ">=2.0.1" [[package]] name = "sentry-sdk" -version = "2.13.0" +version = "2.17.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" files = [ - {file = "sentry_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6"}, - {file = "sentry_sdk-2.13.0.tar.gz", hash = "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260"}, + {file = "sentry_sdk-2.17.0-py2.py3-none-any.whl", hash = "sha256:625955884b862cc58748920f9e21efdfb8e0d4f98cca4ab0d3918576d5b606ad"}, + {file = "sentry_sdk-2.17.0.tar.gz", hash = "sha256:dd0a05352b78ffeacced73a94e86f38b32e2eae15fff5f30ca5abb568a72eacf"}, ] [package.dependencies] @@ -3650,6 +3697,7 @@ falcon = ["falcon (>=1.4)"] fastapi = ["fastapi (>=0.79.0)"] flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] +http2 = ["httpcore[http2] (==1.*)"] httpx = ["httpx (>=0.16.0)"] huey = ["huey (>=2)"] huggingface-hub = ["huggingface-hub (>=0.22)"] @@ -3888,20 +3936,6 @@ xls = ["xlrd", "xlwt"] xlsx = ["openpyxl (>=2.6.0)"] yaml = ["pyyaml"] -[[package]] -name = "tabulate" -version = "0.9.0" -description = "Pretty-print tabular data" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, - {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, -] - -[package.extras] -widechars = ["wcwidth"] - [[package]] name = "tenacity" version = "8.5.0" @@ -4118,13 +4152,13 @@ files = [ [[package]] name = "unstructured" -version = "0.15.7" +version = "0.16.3" description = "A library that prepares raw documents for downstream ML tasks." optional = false python-versions = "<3.13,>=3.9.0" files = [ - {file = "unstructured-0.15.7-py3-none-any.whl", hash = "sha256:9b176f18776142feed1f058f11d16046ae24d077fa96648979ae9c474819f56c"}, - {file = "unstructured-0.15.7.tar.gz", hash = "sha256:ac55bf31b1d4c19c33c0e2ec5f615d96d03a2bf49a784f23b29d5530b90d6830"}, + {file = "unstructured-0.16.3-py3-none-any.whl", hash = "sha256:e0e3b56531b44e62154d17cbfdae7fd7fa1d795b7cf510fb654c6714d4257655"}, + {file = "unstructured-0.16.3.tar.gz", hash = "sha256:f9528636773c910a53c8a34e32d4733ea54b79cbd507d0e956e299ab1da3003f"}, ] [package.dependencies] @@ -4134,6 +4168,7 @@ chardet = "*" dataclasses-json = "*" emoji = "*" filetype = "*" +html5lib = "*" langdetect = "*" lxml = "*" nltk = "*" @@ -4141,78 +4176,33 @@ numpy = "<2" psutil = "*" python-iso639 = "*" python-magic = "*" +python-oxmsg = "*" rapidfuzz = "*" requests = "*" -tabulate = "*" tqdm = "*" typing-extensions = "*" unstructured-client = "*" wrapt = "*" [package.extras] -airtable = ["pyairtable"] -all-docs = ["effdet", "google-cloud-vision", "markdown", "networkx", "onnx", "openpyxl", "pandas", "pdf2image", "pdfminer.six", "pikepdf", "pillow-heif", "pypandoc", "pypdf", "python-docx (>=1.1.2)", "python-oxmsg", "python-pptx (>=1.0.1)", "unstructured-inference (==0.7.36)", "unstructured.pytesseract (>=0.3.12)", "xlrd"] -astradb = ["astrapy"] -azure = ["adlfs", "fsspec"] -azure-cognitive-search = ["azure-search-documents"] -bedrock = ["boto3", "langchain-community"] -biomed = ["bs4"] -box = ["boxfs", "fsspec"] -chroma = ["chromadb", "importlib-metadata (>=8.2.0)", "tenacity (==8.5.0)", "typer (<=0.9.0)"] -clarifai = ["clarifai"] -confluence = ["atlassian-python-api"] +all-docs = ["effdet", "google-cloud-vision", "markdown", "networkx", "onnx", "openpyxl", "pandas", "pdf2image", "pdfminer.six", "pi-heif", "pikepdf", "pypandoc", "pypdf", "python-docx (>=1.1.2)", "python-pptx (>=1.0.1)", "unstructured-inference (==0.8.1)", "unstructured.pytesseract (>=0.3.12)", "xlrd"] csv = ["pandas"] -databricks-volumes = ["databricks-sdk"] -delta-table = ["deltalake", "fsspec"] -discord = ["discord-py"] doc = ["python-docx (>=1.1.2)"] docx = ["python-docx (>=1.1.2)"] -dropbox = ["dropboxdrivefs", "fsspec"] -elasticsearch = ["elasticsearch[async]"] -embed-huggingface = ["langchain-huggingface"] -embed-octoai = ["openai", "tiktoken"] -embed-vertexai = ["langchain", "langchain-community", "langchain-google-vertexai"] -embed-voyageai = ["langchain", "langchain-voyageai"] epub = ["pypandoc"] -gcs = ["bs4", "fsspec", "gcsfs"] -github = ["pygithub (>1.58.0)"] -gitlab = ["python-gitlab"] -google-drive = ["google-api-python-client"] -hubspot = ["hubspot-api-client", "urllib3"] huggingface = ["langdetect", "sacremoses", "sentencepiece", "torch", "transformers"] -image = ["effdet", "google-cloud-vision", "onnx", "pdf2image", "pdfminer.six", "pikepdf", "pillow-heif", "pypdf", "unstructured-inference (==0.7.36)", "unstructured.pytesseract (>=0.3.12)"] -jira = ["atlassian-python-api"] -kafka = ["confluent-kafka"] -local-inference = ["effdet", "google-cloud-vision", "markdown", "networkx", "onnx", "openpyxl", "pandas", "pdf2image", "pdfminer.six", "pikepdf", "pillow-heif", "pypandoc", "pypdf", "python-docx (>=1.1.2)", "python-oxmsg", "python-pptx (>=1.0.1)", "unstructured-inference (==0.7.36)", "unstructured.pytesseract (>=0.3.12)", "xlrd"] +image = ["effdet", "google-cloud-vision", "onnx", "pdf2image", "pdfminer.six", "pi-heif", "pikepdf", "pypdf", "unstructured-inference (==0.8.1)", "unstructured.pytesseract (>=0.3.12)"] +local-inference = ["effdet", "google-cloud-vision", "markdown", "networkx", "onnx", "openpyxl", "pandas", "pdf2image", "pdfminer.six", "pi-heif", "pikepdf", "pypandoc", "pypdf", "python-docx (>=1.1.2)", "python-pptx (>=1.0.1)", "unstructured-inference (==0.8.1)", "unstructured.pytesseract (>=0.3.12)", "xlrd"] md = ["markdown"] -mongodb = ["pymongo"] -msg = ["python-oxmsg"] -notion = ["htmlBuilder", "notion-client"] odt = ["pypandoc", "python-docx (>=1.1.2)"] -onedrive = ["Office365-REST-Python-Client", "bs4", "msal"] -openai = ["langchain-openai"] -opensearch = ["opensearch-py"] org = ["pypandoc"] -outlook = ["Office365-REST-Python-Client", "msal"] -paddleocr = ["paddlepaddle (==3.0.0b1)", "unstructured.paddleocr (==2.8.0.1)"] -pdf = ["effdet", "google-cloud-vision", "onnx", "pdf2image", "pdfminer.six", "pikepdf", "pillow-heif", "pypdf", "unstructured-inference (==0.7.36)", "unstructured.pytesseract (>=0.3.12)"] -pinecone = ["pinecone-client (>=3.7.1)"] -postgres = ["psycopg2-binary"] +paddleocr = ["paddlepaddle (==3.0.0b1)", "unstructured.paddleocr (==2.8.1.0)"] +pdf = ["effdet", "google-cloud-vision", "onnx", "pdf2image", "pdfminer.six", "pi-heif", "pikepdf", "pypdf", "unstructured-inference (==0.8.1)", "unstructured.pytesseract (>=0.3.12)"] ppt = ["python-pptx (>=1.0.1)"] pptx = ["python-pptx (>=1.0.1)"] -qdrant = ["qdrant-client"] -reddit = ["praw"] rst = ["pypandoc"] rtf = ["pypandoc"] -s3 = ["fsspec", "s3fs"] -salesforce = ["simple-salesforce"] -sftp = ["fsspec", "paramiko"] -sharepoint = ["Office365-REST-Python-Client", "msal"] -singlestore = ["singlestoredb"] -slack = ["slack-sdk"] tsv = ["pandas"] -weaviate = ["weaviate-client"] -wikipedia = ["wikipedia"] xlsx = ["networkx", "openpyxl", "pandas", "xlrd"] [[package]] @@ -4331,6 +4321,17 @@ packaging = "*" python-dotenv = "*" requests = "*" +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + [[package]] name = "websocket-client" version = "1.8.0" @@ -4349,13 +4350,13 @@ test = ["websockets"] [[package]] name = "whitenoise" -version = "6.7.0" +version = "6.8.2" description = "Radically simplified static file serving for WSGI applications" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "whitenoise-6.7.0-py3-none-any.whl", hash = "sha256:a1ae85e01fdc9815d12fa33f17765bc132ed2c54fa76daf9e39e879dd93566f6"}, - {file = "whitenoise-6.7.0.tar.gz", hash = "sha256:58c7a6cd811e275a6c91af22e96e87da0b1109e9a53bb7464116ef4c963bf636"}, + {file = "whitenoise-6.8.2-py3-none-any.whl", hash = "sha256:df12dce147a043d1956d81d288c6f0044147c6d2ab9726e5772ac50fb45d2280"}, + {file = "whitenoise-6.8.2.tar.gz", hash = "sha256:486bd7267a375fa9650b136daaec156ac572971acc8bf99add90817a530dd1d4"}, ] [package.extras] @@ -4560,4 +4561,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "3.11.2" -content-hash = "166a363767cc2110613f7a241933984b3cb25ba09416aa22ae9df4767857223a" +content-hash = "e28da64daa986113324ee9f841d830a7f3fd0ed0fbd737987dc552557373f6d8" diff --git a/pyproject.toml b/pyproject.toml index 4785fbd7d..01d56d8cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,26 +10,26 @@ license = "AGPLv3" python = "3.11.2" python-dotenv = "^1.0.1" toml = "^0.10.2" -Django = "^5.0.8" -dj-database-url = "^2.2.0" +Django = "^5.1.1" +dj-database-url = "^2.3.0" django-allauth = "^0.61.1" beautifulsoup4 = "^4.12.3" colorthief = "^0.2.1" django-email-obfuscator = "^0.1.5" -django-gravatar2 = "^1.4.4" +django-gravatar2 = "^1.4.5" django-import-export = "^4.1.0" django-annoying = "^0.10.7" dj-rest-auth = "^5.0.2" tweepy = "^4.8.0" Unidecode = "^1.3.8" user-agents = "^2.2.0" -whitenoise = "^6.7.0" +whitenoise = "^6.8.2" django-debug-toolbar = "^4.4.6" -selenium = "^4.23.1" +selenium = "^4.26.0" pylibmc = "^1.6.1" -psycopg2-binary = "^2.9.9" +psycopg2-binary = "^2.9.10" boto = "^2.49.0" -django-cors-headers = "^4.4.0" +django-cors-headers = "^4.6.0" protobuf = "^4.25.3" django-storages = { extras = ["google"], version = "^1.14.3" } django-timedeltafield = "^0.7.10" @@ -48,7 +48,7 @@ six = "^1.16.0" tablib = "^3.2.0" ua-parser = "^0.18.0" djangorestframework = "^3.15.2" -cffi = "^1.17.0" +cffi = "^1.17.1" django-mdeditor = "^0.1.20" django-tz-detect = "^0.4.0" django-tellme = "^0.7.3" @@ -57,24 +57,24 @@ django-star-ratings = "^0.9.2" stripe = "^8.4.0" django-environ = "^0.11.2" django-humanize = "^0.1.2" -drf-yasg = "^1.20.0" +drf-yasg = "^1.21.8" django-simple-captcha = "^0.6.0" django-filter = "^24.3" webdriver-manager = "^4.0.2" pillow = "^10.4.0" chromedriver-autoinstaller = "^0.6.4" -sentry-sdk = "^2.13.0" +sentry-sdk = "^2.17.0" bitcash = "^1.0.2" pydantic = "^2.7.3" pydantic_core = "^2.18.4" -langchain = "^0.2.14" -langchain-community = "^0.2.11" -langchain-core = "^0.2.34" -langchain-openai = "^0.1.22" -unstructured = "^0.15.7" +langchain = "^0.2.16" +langchain-community = "^0.2.17" +langchain-core = "^0.2.42" +langchain-openai = "^0.1.25" +unstructured = "^0.16.3" Markdown = "^3.6" faiss-cpu = "^1.8.0" -openai = "^1.41.1" +openai = "^1.53.0" psutil = "^5.9.8" python-bitcoinrpc = "^1.0" sendgrid = "^6.11.0" @@ -82,7 +82,7 @@ sendgrid = "^6.11.0" [tool.poetry.group.dev.dependencies] black = "^24.8.0" isort = "^5.13.2" -ruff = "^0.6.2" +ruff = "^0.7.1" pre-commit = "^3.8.0" [tool.isort] diff --git a/website/admin.py b/website/admin.py index 5025b549a..529f3f9bd 100644 --- a/website/admin.py +++ b/website/admin.py @@ -28,6 +28,7 @@ SuggestionVotes, Tag, Trademark, + TimeLog, Transaction, UserProfile, Wallet, @@ -417,6 +418,7 @@ class TagAdmin(admin.ModelAdmin): admin.site.register(Blocked, BlockedAdmin) admin.site.register(Suggestion, SuggestionAdmin) admin.site.register(SuggestionVotes, SuggestionVotesAdmin) +admin.site.register(TimeLog) # Register missing models admin.site.register(InviteFriend) diff --git a/website/api/views.py b/website/api/views.py index 22bce7a07..d1c413126 100644 --- a/website/api/views.py +++ b/website/api/views.py @@ -20,6 +20,7 @@ from rest_framework.response import Response from rest_framework.views import APIView +from website.class_views import LeaderboardBase from website.models import ( ActivityLog, Company, @@ -51,7 +52,7 @@ TimeLogSerializer, UserProfileSerializer, ) -from website.views import LeaderboardBase, image_validator +from website.utils import image_validator # API's @@ -755,6 +756,67 @@ def list(self, request, *args, **kwargs): status=200, ) + @action(detail=False, methods=["get"]) + def search(self, request, *args, **kwargs): + query = request.query_params.get("q", "") + projects = Project.objects.filter( + Q(name__icontains=query) + | Q(description__icontains=query) + | Q(tags__name__icontains=query) + | Q(stars__icontains=query) + | Q(forks__icontains=query) + ).distinct() + + project_data = [] + for project in projects: + contributors_data = [] + for contributor in project.contributors.all(): + contributor_info = ContributorSerializer(contributor) + contributors_data.append(contributor_info.data) + contributors_data.sort(key=lambda x: x["contributions"], reverse=True) + project_info = ProjectSerializer(project).data + project_info["contributors"] = contributors_data + project_data.append(project_info) + + return Response( + {"count": len(project_data), "projects": project_data}, + status=200, + ) + + @action(detail=False, methods=["get"]) + def filter(self, request, *args, **kwargs): + freshness = request.query_params.get("freshness", None) + stars = request.query_params.get("stars", None) + forks = request.query_params.get("forks", None) + tags = request.query_params.get("tags", None) + + projects = Project.objects.all() + + if freshness: + projects = projects.filter(freshness__icontains=freshness) + if stars: + projects = projects.filter(stars__gte=stars) + if forks: + projects = projects.filter(forks__gte=forks) + if tags: + projects = projects.filter(tags__name__in=tags.split(",")).distinct() + + project_data = [] + for project in projects: + contributors_data = [] + for contributor in project.contributors.all(): + contributor_info = ContributorSerializer(contributor) + contributors_data.append(contributor_info.data) + contributors_data.sort(key=lambda x: x["contributions"], reverse=True) + project_info = ProjectSerializer(project).data + project_info["contributors"] = contributors_data + project_data.append(project_info) + + return Response( + {"count": len(project_data), "projects": project_data}, + status=200, + ) + class AuthApiViewset(viewsets.ModelViewSet): http_method_names = ("delete",) diff --git a/website/class_views.py b/website/class_views.py new file mode 100644 index 000000000..effb372ee --- /dev/null +++ b/website/class_views.py @@ -0,0 +1,2209 @@ +import base64 +import json +import os +import uuid +from datetime import datetime, timedelta, timezone +from decimal import Decimal +from urllib.parse import urlparse + +import requests +import six +import stripe +import tweepy +from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter +from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter +from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter +from allauth.socialaccount.providers.oauth2.client import OAuth2Client +from bs4 import BeautifulSoup +from dj_rest_auth.registration.views import SocialConnectView, SocialLoginView +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import get_user_model, logout +from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.models import User +from django.core.files import File +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from django.core.mail import send_mail +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.db.models import Count, Q, Sum +from django.db.models.functions import ExtractMonth +from django.db.transaction import atomic +from django.http import ( + Http404, + HttpResponse, + HttpResponseNotFound, + HttpResponseRedirect, + JsonResponse, +) +from django.shortcuts import get_object_or_404, redirect, render +from django.template.loader import render_to_string +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.timezone import now +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import DetailView, ListView, TemplateView, View +from django.views.generic.edit import CreateView +from PIL import Image, ImageDraw, ImageFont +from rest_framework.authtoken.models import Token +from rest_framework.authtoken.views import ObtainAuthToken +from rest_framework.response import Response +from rest_framework.views import APIView +from user_agents import parse + +from blt import settings +from website.forms import CaptchaForm, GitHubURLForm, HuntForm, UserDeleteForm, UserProfileForm +from website.models import ( + IP, + BaconToken, + Bid, + ChatBotLog, + Company, + CompanyAdmin, + Contribution, + Contributor, + ContributorStats, + Domain, + Hunt, + HuntPrize, + InviteFriend, + Issue, + IssueScreenshot, + Monitor, + Payment, + Points, + Project, + Subscription, + Suggestion, + SuggestionVotes, + Tag, + Transaction, + User, + UserProfile, + Wallet, + Winner, +) +from website.utils import ( + get_client_ip, + get_email_from_domain, + image_validator, + is_valid_https_url, + rebuild_safe_url, +) + + +class ProjectDetailView(DetailView): + model = Project + + def post(self, request, *args, **kwargs): + if "update_project" in request.POST: + from django.core.management import call_command + + project = self.get_object() # Use get_object() to retrieve the current object + call_command("update_projects", "--project_id", project.pk) + messages.success(request, "Requested refresh to projects") + return redirect("project_view", slug=project.slug) + + def get(self, request, *args, **kwargs): + project = self.get_object() + project.project_visit_count += 1 + project.save() + return super().get(request, *args, **kwargs) + + +class ProjectBadgeView(APIView): + def get(self, request, slug, format=None): + # Retrieve the project or return 404 + project = get_object_or_404(Project, slug=slug) + + # Increment the visit count + project.repo_visit_count += 1 + project.save() + + # Create an image with the updated visit count + img = Image.new("RGB", (200, 50), color=(73, 109, 137)) + d = ImageDraw.Draw(img) + font = ImageFont.load_default() + + # Updated line to calculate text size + text = f"Visits: {project.repo_visit_count}" + bbox = d.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + # Center the text in the image + position = ((200 - text_width) / 2, (50 - text_height) / 2) + d.text(position, text, font=font, fill=(255, 255, 0)) + + # Prepare the HTTP response with the image and cache control + response = HttpResponse(content_type="image/png") + img.save(response, "PNG") + + # Set headers to prevent caching + response["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" + response["Pragma"] = "no-cache" + response["Expires"] = "0" + + return response + + +class ProjectListView(ListView): + model = Project + context_object_name = "projects" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form"] = GitHubURLForm() + context["sort_by"] = self.request.GET.get("sort_by", "-created") + context["order"] = self.request.GET.get("order", "desc") + return context + + def post(self, request, *args, **kwargs): + if "update_projects" in request.POST: + print("Updating projects") + from django.core.management import call_command # Add this import + + call_command("update_projects") + messages.success(request, "Requested refresh to projects") + return redirect("project_list") + + form = GitHubURLForm(request.POST) + if form.is_valid(): + github_url = form.cleaned_data["github_url"] + api_url = github_url.replace("github.com", "api.github.com/repos") + response = requests.get(api_url) + if response.status_code == 200: + data = response.json() + # if the description is empty, use the name as the description + if not data["description"]: + data["description"] = data["name"] + project, created = Project.objects.get_or_create( + github_url=github_url, + defaults={ + "name": data["name"], + "slug": data["name"].lower(), + "description": data["description"], + "wiki_url": data["html_url"], + "homepage_url": data.get("homepage", ""), + "logo_url": data["owner"]["avatar_url"], + }, + ) + if created: + messages.success(request, "Project added successfully.") + else: + messages.info(request, "Project already exists.") + else: + messages.error(request, "Failed to fetch project from GitHub.") + return redirect("project_list") + context = self.get_context_data() + context["form"] = form + return self.render_to_response(context) + + def get_queryset(self): + queryset = super().get_queryset() + sort_by = self.request.GET.get("sort_by", "-created") + order = self.request.GET.get("order", "desc") + + if order == "asc" and sort_by.startswith("-"): + sort_by = sort_by[1:] + elif order == "desc" and not sort_by.startswith("-"): + sort_by = f"-{sort_by}" + + return queryset.order_by(sort_by) + + +class FacebookLogin(SocialLoginView): + adapter_class = FacebookOAuth2Adapter + client_class = OAuth2Client + + @property + def callback_url(self): + return self.request.build_absolute_uri(reverse("facebook_callback")) + + +class ListHunts(TemplateView): + model = Hunt + template_name = "hunt_list.html" + + def get(self, request, *args, **kwargs): + search = request.GET.get("search", "") + start_date = request.GET.get("start_date", None) + end_date = request.GET.get("end_date", None) + domain = request.GET.get("domain", None) + hunt_type = request.GET.get("hunt_type", "all") + + hunts = ( + Hunt.objects.values( + "id", + "name", + "url", + "logo", + "starts_on", + "starts_on__day", + "starts_on__month", + "starts_on__year", + "end_on", + "end_on__day", + "end_on__month", + "end_on__year", + ) + .annotate(total_prize=Sum("huntprize__value")) + .all() + ) + + filtered_bughunts = { + "all": hunts, + "ongoing": hunts.filter(result_published=False, is_published=True), + "ended": hunts.filter(result_published=True), + "draft": hunts.filter(result_published=False, is_published=False), + } + + hunts = filtered_bughunts.get(hunt_type, hunts) + + if search.strip() != "": + hunts = hunts.filter(Q(name__icontains=search)) + + if start_date != "" and start_date is not None: + start_date = datetime.strptime(start_date, "%m/%d/%Y").strftime("%Y-%m-%d %H:%M") + hunts = hunts.filter(starts_on__gte=start_date) + + if end_date != "" and end_date is not None: + end_date = datetime.strptime(end_date, "%m/%d/%Y").strftime("%Y-%m-%d %H:%M") + hunts = hunts.filter(end_on__gte=end_date) + + if domain != "Select Domain" and domain is not None: + domain = Domain.objects.filter(id=domain).first() + hunts = hunts.filter(domain=domain) + + context = {"hunts": hunts, "domains": Domain.objects.values("id", "name").all()} + + return render(request, self.template_name, context) + + def post(self, request, *args, **kwargs): + request.GET.search = request.GET.get("search", "") + request.GET.start_date = request.GET.get("start_date", "") + request.GET.end_date = request.GET.get("end_date", "") + request.GET.domain = request.GET.get("domain", "Select Domain") + request.GET.hunt_type = request.GET.get("type", "all") + + return self.get(request) + + +class DraftHunts(TemplateView): + model = Hunt + fields = ["url", "logo", "domain", "plan", "prize", "txn_id"] + template_name = "hunt_drafts.html" + + @method_decorator(login_required) + def get(self, request, *args, **kwargs): + try: + domain_admin = CompanyAdmin.objects.get(user=request.user) + if not domain_admin.is_active: + return HttpResponseRedirect("/") + if domain_admin.role == 0: + hunt = self.model.objects.filter(is_published=False) + else: + hunt = self.model.objects.filter(is_published=False, domain=domain_admin.domain) + context = {"hunts": hunt} + return render(request, self.template_name, context) + except: + return HttpResponseRedirect("/") + + +class UpcomingHunts(TemplateView): + model = Hunt + fields = ["url", "logo", "domain", "plan", "prize", "txn_id"] + template_name = "hunt_upcoming.html" + + @method_decorator(login_required) + def get(self, request, *args, **kwargs): + try: + domain_admin = CompanyAdmin.objects.get(user=request.user) + if not domain_admin.is_active: + return HttpResponseRedirect("/") + + if domain_admin.role == 0: + hunts = self.model.objects.filter(is_published=True) + else: + hunts = self.model.objects.filter(is_published=True, domain=domain_admin.domain) + new_hunt = [] + for hunt in hunts: + if ((hunt.starts_on - datetime.now(timezone.utc)).total_seconds()) > 0: + new_hunt.append(hunt) + context = {"hunts": new_hunt} + return render(request, self.template_name, context) + except: + return HttpResponseRedirect("/") + + +class OngoingHunts(TemplateView): + model = Hunt + fields = ["url", "logo", "domain", "plan", "prize", "txn_id"] + template_name = "hunt_ongoing.html" + + @method_decorator(login_required) + def get(self, request, *args, **kwargs): + try: + domain_admin = CompanyAdmin.objects.get(user=request.user) + if not domain_admin.is_active: + return HttpResponseRedirect("/") + if domain_admin.role == 0: + hunts = self.model.objects.filter(is_published=True) + else: + hunts = self.model.objects.filter(is_published=True, domain=domain_admin.domain) + new_hunt = [] + for hunt in hunts: + if ((hunt.starts_on - datetime.now(timezone.utc)).total_seconds()) > 0: + new_hunt.append(hunt) + context = {"hunts": new_hunt} + return render(request, self.template_name, context) + except: + return HttpResponseRedirect("/") + + +class PreviousHunts(TemplateView): + model = Hunt + fields = ["url", "logo", "domain", "plan", "prize", "txn_id"] + template_name = "hunt_previous.html" + + @method_decorator(login_required) + def get(self, request, *args, **kwargs): + try: + domain_admin = CompanyAdmin.objects.get(user=request.user) + if not domain_admin.is_active: + return HttpResponseRedirect("/") + if domain_admin.role == 0: + hunts = self.model.objects.filter(is_published=True) + else: + hunts = self.model.objects.filter(is_published=True, domain=domain_admin.domain) + new_hunt = [] + for hunt in hunts: + if ((hunt.starts_on - datetime.now(timezone.utc)).total_seconds()) > 0: + pass + elif ((hunt.end_on - datetime.now(timezone.utc)).total_seconds()) < 0: + new_hunt.append(hunt) + else: + pass + context = {"hunts": new_hunt} + return render(request, self.template_name, context) + except: + return HttpResponseRedirect("/") + + @method_decorator(login_required) + def post(self, request, *args, **kwargs): + form = UserProfileForm(request.POST, request.FILES, instance=request.user.userprofile) + if form.is_valid(): + form.save() + return redirect(reverse("profile", kwargs={"slug": kwargs.get("slug")})) + + +class CompanySettings(TemplateView): + model = CompanyAdmin + fields = ["user", "domain", "role", "is_active"] + template_name = "company_settings.html" + + @method_decorator(login_required) + def get(self, request, *args, **kwargs): + try: + domain_admin = CompanyAdmin.objects.get(user=request.user) + if not domain_admin.is_active: + return HttpResponseRedirect("/") + domain_admins = [] + domain_list = Domain.objects.filter(company=domain_admin.company) + if domain_admin.role == 0: + domain_admins = CompanyAdmin.objects.filter( + company=domain_admin.company, is_active=True + ) + else: + domain_admins = CompanyAdmin.objects.filter( + domain=domain_admin.domain, is_active=True + ) + context = { + "admins": domain_admins, + "user": domain_admin, + "domain_list": domain_list, + } + return render(request, self.template_name, context) + except: + return HttpResponseRedirect("/") + + @method_decorator(login_required) + def post(self, request, *args, **kwargs): + form = UserProfileForm(request.POST, request.FILES, instance=request.user.userprofile) + if form.is_valid(): + form.save() + return redirect(reverse("profile", kwargs={"slug": kwargs.get("slug")})) + + +class DomainList(TemplateView): + model = Domain + template_name = "company_domain_lists.html" + + @method_decorator(login_required) + def get(self, request, *args, **kwargs): + domain_admin = CompanyAdmin.objects.get(user=request.user) + if not domain_admin.is_active: + return HttpResponseRedirect("/") + domain = [] + if domain_admin.role == 0: + domain = self.model.objects.filter(company=domain_admin.company) + else: + domain = self.model.objects.filter(pk=domain_admin.domain.pk) + context = {"domains": domain} + return render(request, self.template_name, context) + + +class JoinCompany(TemplateView): + model = Company + template_name = "join.html" + + @method_decorator(login_required) + def get(self, request, *args, **kwargs): + wallet, created = Wallet.objects.get_or_create(user=request.user) + context = {"wallet": wallet} + return render(request, self.template_name, context) + + def post(self, request, *args, **kwargs): + name = request.POST["company"] + try: + company_exists = Company.objects.get(name=name) + return JsonResponse({"status": "There was some error"}) + except: + pass + url = request.POST["url"] + email = request.POST["email"] + product = request.POST["product"] + sub = Subscription.objects.get(name=product) + if name == "" or url == "" or email == "" or product == "": + return JsonResponse({"error": "Empty Fields"}) + paymentType = request.POST["paymentType"] + if paymentType == "wallet": + wallet, created = Wallet.objects.get_or_create(user=request.user) + if wallet.current_balance < sub.charge_per_month: + return JsonResponse({"error": "insufficient balance in Wallet"}) + wallet.withdraw(sub.charge_per_month) + company = Company() + company.admin = request.user + company.name = name + company.url = url + company.email = email + company.subscription = sub + company.save() + admin = CompanyAdmin() + admin.user = request.user + admin.role = 0 + admin.company = company + admin.is_active = True + admin.save() + return JsonResponse({"status": "Success"}) + # company.subscription = + elif paymentType == "card": + from django.conf import settings + + stripe.api_key = settings.STRIPE_TEST_SECRET_KEY + charge = stripe.Charge.create( + amount=int(Decimal(sub.charge_per_month) * 100), + currency="usd", + description="Example charge", + source=request.POST["stripeToken"], + ) + company = Company() + company.admin = request.user + company.name = name + company.url = url + company.email = email + company.subscription = sub + company.save() + admin = CompanyAdmin() + admin.user = request.user + admin.role = 0 + admin.company = company + admin.is_active = True + admin.save() + return JsonResponse({"status": "Success"}) + else: + return JsonResponse({"status": "There was some error"}) + + +class ContributorStatsView(TemplateView): + template_name = "contributor_stats.html" + today = False + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # Fetch all contributor stats records + stats = ContributorStats.objects.all() + if self.today: + # For "today" stats + user_stats = sorted( + ([stat.username, stat.prs] for stat in stats if stat.prs > 0), + key=lambda x: x[1], # Sort by PRs value + reverse=True, # Descending order + ) + else: + # Convert the stats to a dictionary format expected by the template + user_stats = { + stat.username: { + "commits": stat.commits, + "issues_opened": stat.issues_opened, + "issues_closed": stat.issues_closed, + "assigned_issues": stat.assigned_issues, + "prs": stat.prs, + "comments": stat.comments, + } + for stat in stats + } + + context["user_stats"] = user_stats + context["today"] = self.today + context["owner"] = "OWASP-BLT" + context["repo"] = "BLT" + context["start_date"] = (datetime.now().date() - timedelta(days=7)).isoformat() + context["end_date"] = datetime.now().date().isoformat() + + return context + + +class GoogleLogin(SocialLoginView): + adapter_class = GoogleOAuth2Adapter + client_class = OAuth2Client + + @property + def callback_url(self): + return self.request.build_absolute_uri(reverse("google_callback")) + + +class GithubLogin(SocialLoginView): + adapter_class = GitHubOAuth2Adapter + client_class = OAuth2Client + + @property + def callback_url(self): + return self.request.build_absolute_uri(reverse("github_callback")) + + +class FacebookConnect(SocialConnectView): + adapter_class = FacebookOAuth2Adapter + client_class = OAuth2Client + + @property + def callback_url(self): + return self.request.build_absolute_uri(reverse("facebook_callback")) + + +class GithubConnect(SocialConnectView): + adapter_class = GitHubOAuth2Adapter + client_class = OAuth2Client + + @property + def callback_url(self): + return self.request.build_absolute_uri(reverse("github_callback")) + + +class GoogleConnect(SocialConnectView): + adapter_class = GoogleOAuth2Adapter + client_class = OAuth2Client + + @property + def callback_url(self): + return self.request.build_absolute_uri(reverse("google_callback")) + + +class UserDeleteView(LoginRequiredMixin, View): + """ + Deletes the currently signed-in user and all associated data. + """ + + def get(self, request, *args, **kwargs): + form = UserDeleteForm() + return render(request, "user_deletion.html", {"form": form}) + + def post(self, request, *args, **kwargs): + form = UserDeleteForm(request.POST) + if form.is_valid(): + user = request.user + logout(request) + user.delete() + messages.success(request, "Account successfully deleted") + return redirect(reverse("index")) + return render(request, "user_deletion.html", {"form": form}) + + +class IssueBaseCreate(object): + def form_valid(self, form): + print( + "processing form_valid IssueBaseCreate for ip address: ", + get_client_ip(self.request), + ) + score = 3 + obj = form.save(commit=False) + obj.user = self.request.user + domain, created = Domain.objects.get_or_create( + name=obj.domain_name.replace("www.", ""), + defaults={"url": "http://" + obj.domain_name.replace("www.", "")}, + ) + obj.domain = domain + if self.request.POST.get("screenshot-hash"): + filename = self.request.POST.get("screenshot-hash") + extension = filename.split(".")[-1] + self.request.POST["screenshot-hash"] = ( + filename[:99] + str(uuid.uuid4()) + "." + extension + ) + + reopen = default_storage.open( + "uploads\/" + self.request.POST.get("screenshot-hash") + ".png", "rb" + ) + django_file = File(reopen) + obj.screenshot.save( + self.request.POST.get("screenshot-hash") + ".png", + django_file, + save=True, + ) + + obj.user_agent = self.request.META.get("HTTP_USER_AGENT") + obj.save() + p = Points.objects.create(user=self.request.user, issue=obj, score=score) + + def process_issue(self, user, obj, created, domain, tokenauth=False, score=3): + print("processing process_issue for ip address: ", get_client_ip(self.request)) + p = Points.objects.create(user=user, issue=obj, score=score) + messages.success(self.request, "Bug added ! +" + str(score)) + try: + auth = tweepy.Client( + settings.BEARER_TOKEN, + settings.APP_KEY, + settings.APP_KEY_SECRET, + settings.ACCESS_TOKEN, + settings.ACCESS_TOKEN_SECRET, + ) + + blt_url = "https://%s/issue/%d" % ( + settings.DOMAIN_NAME, + obj.id, + ) + domain_name = domain.get_name + twitter_account = ( + "@" + domain.get_or_set_x_url(domain_name) + " " + if domain.get_or_set_x_url(domain_name) + else "" + ) + + issue_title = obj.description + " " if not obj.is_hidden else "" + + message = "%sAn Issue %shas been reported on %s by %s on %s.\n Have look here %s" % ( + twitter_account, + issue_title, + domain_name, + user.username, + settings.PROJECT_NAME, + blt_url, + ) + + auth.create_tweet(text=message) + + except ( + TypeError, + tweepy.errors.HTTPException, + tweepy.errors.TweepyException, + ) as e: + print(e) + + if created: + try: + email_to = get_email_from_domain(domain) + except: + email_to = "support@" + domain.name + + domain.email = email_to + domain.save() + + name = email_to.split("@")[0] + + msg_plain = render_to_string( + "email/domain_added.txt", {"domain": domain.name, "name": name} + ) + msg_html = render_to_string( + "email/domain_added.txt", {"domain": domain.name, "name": name} + ) + + send_mail( + domain.name + " added to " + settings.PROJECT_NAME, + msg_plain, + settings.EMAIL_TO_STRING, + [email_to], + html_message=msg_html, + ) + + else: + email_to = domain.email + try: + name = email_to.split("@")[0] + except: + email_to = "support@" + domain.name + name = "support" + domain.email = email_to + domain.save() + if not tokenauth: + msg_plain = render_to_string( + "email/bug_added.txt", + { + "domain": domain.name, + "name": name, + "username": self.request.user, + "id": obj.id, + "description": obj.description, + "label": obj.get_label_display, + }, + ) + msg_html = render_to_string( + "email/bug_added.txt", + { + "domain": domain.name, + "name": name, + "username": self.request.user, + "id": obj.id, + "description": obj.description, + "label": obj.get_label_display, + }, + ) + else: + msg_plain = render_to_string( + "email/bug_added.txt", + { + "domain": domain.name, + "name": name, + "username": user, + "id": obj.id, + "description": obj.description, + "label": obj.get_label_display, + }, + ) + msg_html = render_to_string( + "email/bug_added.txt", + { + "domain": domain.name, + "name": name, + "username": user, + "id": obj.id, + "description": obj.description, + "label": obj.get_label_display, + }, + ) + send_mail( + "Bug found on " + domain.name, + msg_plain, + settings.EMAIL_TO_STRING, + [email_to], + html_message=msg_html, + ) + return HttpResponseRedirect("/") + + +class IssueCreate(IssueBaseCreate, CreateView): + model = Issue + fields = ["url", "description", "domain", "label", "markdown_description", "cve_id"] + template_name = "report.html" + + def get_initial(self): + print("processing post for ip address: ", get_client_ip(self.request)) + try: + json_data = json.loads(self.request.body) + if not self.request.GET._mutable: + self.request.POST._mutable = True + self.request.POST["url"] = json_data["url"] + self.request.POST["description"] = json_data["description"] + self.request.POST["markdown_description"] = json_data["markdown_description"] + self.request.POST["file"] = json_data["file"] + self.request.POST["label"] = json_data["label"] + self.request.POST["token"] = json_data["token"] + self.request.POST["type"] = json_data["type"] + self.request.POST["cve_id"] = json_data["cve_id"] + self.request.POST["cve_score"] = json_data["cve_score"] + + if self.request.POST.get("file"): + if isinstance(self.request.POST.get("file"), six.string_types): + import imghdr + + data = ( + "data:image/" + + self.request.POST.get("type") + + ";base64," + + self.request.POST.get("file") + ) + data = data.replace(" ", "") + data += "=" * ((4 - len(data) % 4) % 4) + if "data:" in data and ";base64," in data: + header, data = data.split(";base64,") + + try: + decoded_file = base64.b64decode(data) + except TypeError: + TypeError("invalid_image") + + file_name = str(uuid.uuid4())[:12] + extension = imghdr.what(file_name, decoded_file) + extension = "jpg" if extension == "jpeg" else extension + file_extension = extension + + complete_file_name = "%s.%s" % ( + file_name, + file_extension, + ) + + self.request.FILES["screenshot"] = ContentFile( + decoded_file, name=complete_file_name + ) + except: + tokenauth = False + initial = super(IssueCreate, self).get_initial() + if self.request.POST.get("screenshot-hash"): + initial["screenshot"] = "uploads\/" + self.request.POST.get("screenshot-hash") + ".png" + return initial + + def post(self, request, *args, **kwargs): + print("processing post for ip address: ", get_client_ip(request)) + url = request.POST.get("url").replace("www.", "").replace("https://", "") + + request.POST._mutable = True + request.POST.update(url=url) + request.POST._mutable = False + + if not settings.IS_TEST: + try: + if settings.DOMAIN_NAME in url: + print("Web site exists") + + elif request.POST["label"] == "7": + pass + + else: + full_url = "https://" + url + if is_valid_https_url(full_url): + safe_url = rebuild_safe_url(full_url) + try: + response = requests.get(safe_url, timeout=5) + if response.status_code == 200: + print("Web site exists") + else: + raise Exception + except Exception: + raise Exception + else: + raise Exception + except: + # TODO: it could be that the site is down so we can consider logging this differently + messages.error(request, "Domain does not exist") + + captcha_form = CaptchaForm(request.POST) + return render( + self.request, + "report.html", + {"form": self.get_form(), "captcha_form": captcha_form}, + ) + + screenshot = request.FILES.get("screenshots") + if not screenshot: + messages.error(request, "Screenshot is required") + captcha_form = CaptchaForm(request.POST) + return render( + request, + "report.html", + {"form": self.get_form(), "captcha_form": captcha_form}, + ) + + try: + img = Image.open(screenshot) + img.verify() + except (IOError, ValueError): + messages.error(request, "Invalid image file.") + captcha_form = CaptchaForm(request.POST) + return render( + request, + "report.html", + {"form": self.get_form(), "captcha_form": captcha_form}, + ) + + return super().post(request, *args, **kwargs) + + def form_valid(self, form): + print( + "processing form_valid in IssueCreate for ip address: ", + get_client_ip(self.request), + ) + reporter_ip = get_client_ip(self.request) + form.instance.reporter_ip_address = reporter_ip + + limit = 50 if self.request.user.is_authenticated else 30 + today = now().date() + recent_issues_count = Issue.objects.filter( + reporter_ip_address=reporter_ip, created__date=today + ).count() + + if recent_issues_count >= limit: + messages.error(self.request, "You have reached your issue creation limit for today.") + return render(self.request, "report.html", {"form": self.get_form()}) + form.instance.reporter_ip_address = reporter_ip + + @atomic + def create_issue(self, form): + tokenauth = False + obj = form.save(commit=False) + if self.request.user.is_authenticated: + obj.user = self.request.user + if not self.request.user.is_authenticated: + for token in Token.objects.all(): + if self.request.POST.get("token") == token.key: + obj.user = User.objects.get(id=token.user_id) + tokenauth = True + + captcha_form = CaptchaForm(self.request.POST) + if not captcha_form.is_valid() and not settings.TESTING: + messages.error(self.request, "Invalid Captcha!") + + return render( + self.request, + "report.html", + {"form": self.get_form(), "captcha_form": captcha_form}, + ) + parsed_url = urlparse(obj.url) + clean_domain = parsed_url.netloc + domain = Domain.objects.filter(url=clean_domain).first() + + domain_exists = False if domain is None else True + + if not domain_exists: + domain = Domain.objects.filter(name=clean_domain).first() + if domain is None: + domain = Domain.objects.create(name=clean_domain, url=clean_domain) + domain.save() + + hunt = self.request.POST.get("hunt", None) + if hunt is not None and hunt != "None": + hunt = Hunt.objects.filter(id=hunt).first() + obj.hunt = hunt + + obj.domain = domain + # obj.is_hidden = bool(self.request.POST.get("private", False)) + obj.cve_score = obj.get_cve_score() + obj.save() + + if not domain_exists and (self.request.user.is_authenticated or tokenauth): + p = Points.objects.create(user=self.request.user, domain=domain, score=1) + messages.success(self.request, "Domain added! + 1") + + if self.request.POST.get("screenshot-hash"): + reopen = default_storage.open( + "uploads\/" + self.request.POST.get("screenshot-hash") + ".png", + "rb", + ) + django_file = File(reopen) + obj.screenshot.save( + self.request.POST.get("screenshot-hash") + ".png", + django_file, + save=True, + ) + obj.user_agent = self.request.META.get("HTTP_USER_AGENT") + + if len(self.request.FILES.getlist("screenshots")) > 5: + messages.error(self.request, "Max limit of 5 images!") + obj.delete() + return render( + self.request, + "report.html", + {"form": self.get_form(), "captcha_form": captcha_form}, + ) + for screenshot in self.request.FILES.getlist("screenshots"): + img_valid = image_validator(screenshot) + if img_valid is True: + filename = screenshot.name + extension = filename.split(".")[-1] + screenshot.name = (filename[:10] + str(uuid.uuid4()))[:40] + "." + extension + default_storage.save(f"screenshots/{screenshot.name}", screenshot) + IssueScreenshot.objects.create( + image=f"screenshots/{screenshot.name}", issue=obj + ) + else: + messages.error(self.request, img_valid) + return render( + self.request, + "report.html", + {"form": self.get_form(), "captcha_form": captcha_form}, + ) + + obj_screenshots = IssueScreenshot.objects.filter(issue_id=obj.id) + screenshot_text = "" + for screenshot in obj_screenshots: + screenshot_text += "![0](" + screenshot.image.url + ") " + + team_members_id = [ + member["id"] + for member in User.objects.values("id").filter( + email__in=self.request.POST.getlist("team_members") + ) + ] + [self.request.user.id] + for member_id in team_members_id: + if member_id is None: + team_members_id.remove(member_id) # remove None values if user not exists + obj.team_members.set(team_members_id) + + obj.save() + + if self.request.user.is_authenticated: + total_issues = Issue.objects.filter(user=self.request.user).count() + user_prof = UserProfile.objects.get(user=self.request.user) + if total_issues <= 10: + user_prof.title = 1 + elif total_issues <= 50: + user_prof.title = 2 + elif total_issues <= 200: + user_prof.title = 3 + else: + user_prof.title = 4 + + user_prof.save() + + if tokenauth: + total_issues = Issue.objects.filter(user=User.objects.get(id=token.user_id)).count() + user_prof = UserProfile.objects.get(user=User.objects.get(id=token.user_id)) + if total_issues <= 10: + user_prof.title = 1 + elif total_issues <= 50: + user_prof.title = 2 + elif total_issues <= 200: + user_prof.title = 3 + else: + user_prof.title = 4 + + user_prof.save() + + redirect_url = "/report" + + if domain.github and os.environ.get("GITHUB_ACCESS_TOKEN"): + import json + + import requests + from giturlparse import parse + + github_url = domain.github.replace("https", "git").replace("http", "git") + ".git" + p = parse(github_url) + + url = "https://api.github.com/repos/%s/%s/issues" % (p.owner, p.repo) + + if not obj.user: + the_user = "Anonymous" + else: + the_user = obj.user + issue = { + "title": obj.description, + "body": obj.markdown_description + + "\n\n" + + screenshot_text + + "https://" + + settings.FQDN + + "/issue/" + + str(obj.id) + + " found by " + + str(the_user) + + " at url: " + + obj.url, + "labels": ["bug", settings.PROJECT_NAME_LOWER], + } + r = requests.post( + url, + json.dumps(issue), + headers={"Authorization": "token " + os.environ.get("GITHUB_ACCESS_TOKEN")}, + ) + response = r.json() + try: + obj.github_url = response["html_url"] + except Exception as e: + send_mail( + "Error in github issue creation for " + + str(domain.name) + + ", check your github settings", + "Error in github issue creation, check your github settings\n" + + " your current settings are: " + + str(domain.github) + + " and the error is: " + + str(e), + settings.EMAIL_TO_STRING, + [domain.email], + fail_silently=True, + ) + pass + obj.save() + + if not (self.request.user.is_authenticated or tokenauth): + self.request.session["issue"] = obj.id + self.request.session["created"] = domain_exists + self.request.session["domain"] = domain.id + messages.success(self.request, "Bug added!") + return HttpResponseRedirect("/") + + if tokenauth: + self.process_issue( + User.objects.get(id=token.user_id), obj, domain_exists, domain, True + ) + return JsonResponse("Created", safe=False) + else: + self.process_issue(self.request.user, obj, domain_exists, domain) + return HttpResponseRedirect("/") + + return create_issue(self, form) + + def get_context_data(self, **kwargs): + # if self.request is a get, clear out the form data + if self.request.method == "GET": + self.request.POST = {} + self.request.GET = {} + + print("processing get_context_data for ip address: ", get_client_ip(self.request)) + context = super(IssueCreate, self).get_context_data(**kwargs) + context["activities"] = Issue.objects.exclude( + Q(is_hidden=True) & ~Q(user_id=self.request.user.id) + )[0:10] + context["captcha_form"] = CaptchaForm() + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + context["leaderboard"] = ( + User.objects.filter( + points__created__month=datetime.now().month, + points__created__year=datetime.now().year, + ) + .annotate(total_score=Sum("points__score")) + .order_by("-total_score")[:10], + ) + + # automatically add specified hunt to dropdown of Bugreport + report_on_hunt = self.request.GET.get("hunt", None) + if report_on_hunt: + context["hunts"] = Hunt.objects.values("id", "name").filter( + id=report_on_hunt, is_published=True, result_published=False + ) + context["report_on_hunt"] = True + else: + context["hunts"] = Hunt.objects.values("id", "name").filter( + is_published=True, result_published=False + ) + context["report_on_hunt"] = False + + context["top_domains"] = ( + Issue.objects.values("domain__name") + .annotate(count=Count("domain__name")) + .order_by("-count")[:30] + ) + + return context + + +class UploadCreate(View): + template_name = "index.html" + + @method_decorator(csrf_exempt) + def dispatch(self, request, *args, **kwargs): + return super(UploadCreate, self).dispatch(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + data = request.FILES.get("image") + result = default_storage.save( + "uploads\/" + self.kwargs["hash"] + ".png", ContentFile(data.read()) + ) + return JsonResponse({"status": result}) + + +class InviteCreate(TemplateView): + template_name = "invite.html" + + def post(self, request, *args, **kwargs): + email = request.POST.get("email") + exists = False + domain = None + if email: + domain = email.split("@")[-1] + try: + full_url_domain = "https://" + domain + "/favicon.ico" + if is_valid_https_url(full_url_domain): + safe_url = rebuild_safe_url(full_url_domain) + response = requests.get(safe_url, timeout=5) + if response.status_code == 200: + exists = "exists" + except: + pass + context = { + "exists": exists, + "domain": domain, + "email": email, + } + return render(request, "invite.html", context) + + +class UserProfileDetailView(DetailView): + model = get_user_model() + slug_field = "username" + template_name = "profile.html" + + def get(self, request, *args, **kwargs): + try: + self.object = self.get_object() + except Http404: + messages.error(self.request, "That user was not found.") + return redirect("/") + return super(UserProfileDetailView, self).get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + user = self.object + context = super(UserProfileDetailView, self).get_context_data(**kwargs) + context["my_score"] = list( + Points.objects.filter(user=self.object).aggregate(total_score=Sum("score")).values() + )[0] + context["websites"] = ( + Domain.objects.filter(issue__user=self.object) + .annotate(total=Count("issue")) + .order_by("-total") + ) + context["activities"] = Issue.objects.filter(user=self.object, hunt=None).exclude( + Q(is_hidden=True) & ~Q(user_id=self.request.user.id) + )[0:3] + context["activity_screenshots"] = {} + for activity in context["activities"]: + context["activity_screenshots"][activity] = IssueScreenshot.objects.filter( + issue=activity.pk + ).first() + context["profile_form"] = UserProfileForm() + context["total_open"] = Issue.objects.filter(user=self.object, status="open").count() + context["total_closed"] = Issue.objects.filter(user=self.object, status="closed").count() + context["current_month"] = datetime.now().month + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + context["graph"] = ( + Issue.objects.filter(user=self.object) + .filter( + created__month__gte=(datetime.now().month - 6), + created__month__lte=datetime.now().month, + ) + .annotate(month=ExtractMonth("created")) + .values("month") + .annotate(c=Count("id")) + .order_by() + ) + context["total_bugs"] = Issue.objects.filter(user=self.object, hunt=None).count() + for i in range(0, 7): + context["bug_type_" + str(i)] = Issue.objects.filter( + user=self.object, hunt=None, label=str(i) + ) + + arr = [] + allFollowers = user.userprofile.follower.all() + for userprofile in allFollowers: + arr.append(User.objects.get(username=str(userprofile.user))) + context["followers"] = arr + + arr = [] + allFollowing = user.userprofile.follows.all() + for userprofile in allFollowing: + arr.append(User.objects.get(username=str(userprofile.user))) + context["following"] = arr + + context["followers_list"] = [ + str(prof.user.email) for prof in user.userprofile.follower.all() + ] + context["bookmarks"] = user.userprofile.issue_saved.all() + # tags + context["user_related_tags"] = ( + UserProfile.objects.filter(user=self.object).first().tags.all() + ) + context["issues_hidden"] = "checked" if user.userprofile.issues_hidden else "!checked" + return context + + @method_decorator(login_required) + def post(self, request, *args, **kwargs): + form = UserProfileForm(request.POST, request.FILES, instance=request.user.userprofile) + if request.FILES.get("user_avatar") and form.is_valid(): + form.save() + else: + hide = True if request.POST.get("issues_hidden") == "on" else False + user_issues = Issue.objects.filter(user=request.user) + user_issues.update(is_hidden=hide) + request.user.userprofile.issues_hidden = hide + request.user.userprofile.save() + return redirect(reverse("profile", kwargs={"slug": kwargs.get("slug")})) + + +class UserProfileDetailsView(DetailView): + model = get_user_model() + slug_field = "username" + template_name = "dashboard_profile.html" + + def get(self, request, *args, **kwargs): + try: + if request.user.is_authenticated: + self.object = self.get_object() + else: + return redirect("/accounts/login") + except Http404: + messages.error(self.request, "That user was not found.") + return redirect("/") + return super(UserProfileDetailsView, self).get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + user = self.object + context = super(UserProfileDetailsView, self).get_context_data(**kwargs) + context["my_score"] = list( + Points.objects.filter(user=self.object).aggregate(total_score=Sum("score")).values() + )[0] + context["websites"] = ( + Domain.objects.filter(issue__user=self.object) + .annotate(total=Count("issue")) + .order_by("-total") + ) + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + context["activities"] = Issue.objects.filter(user=self.object, hunt=None).exclude( + Q(is_hidden=True) & ~Q(user_id=self.request.user.id) + )[0:10] + context["profile_form"] = UserProfileForm() + context["total_open"] = Issue.objects.filter(user=self.object, status="open").count() + context["user_details"] = UserProfile.objects.get(user=self.object) + context["total_closed"] = Issue.objects.filter(user=self.object, status="closed").count() + context["current_month"] = datetime.now().month + context["graph"] = ( + Issue.objects.filter(user=self.object, hunt=None) + .filter( + created__month__gte=(datetime.now().month - 6), + created__month__lte=datetime.now().month, + ) + .annotate(month=ExtractMonth("created")) + .values("month") + .annotate(c=Count("id")) + .order_by() + ) + context["total_bugs"] = Issue.objects.filter(user=self.object).count() + for i in range(0, 7): + context["bug_type_" + str(i)] = Issue.objects.filter( + user=self.object, hunt=None, label=str(i) + ).exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) + + arr = [] + allFollowers = user.userprofile.follower.all() + for userprofile in allFollowers: + arr.append(User.objects.get(username=str(userprofile.user))) + context["followers"] = arr + + arr = [] + allFollowing = user.userprofile.follows.all() + for userprofile in allFollowing: + arr.append(User.objects.get(username=str(userprofile.user))) + context["following"] = arr + + context["followers_list"] = [ + str(prof.user.email) for prof in user.userprofile.follower.all() + ] + context["bookmarks"] = user.userprofile.issue_saved.all() + return context + + @method_decorator(login_required) + def post(self, request, *args, **kwargs): + form = UserProfileForm(request.POST, request.FILES, instance=request.user.userprofile) + if form.is_valid(): + form.save() + return redirect(reverse("profile", kwargs={"slug": kwargs.get("slug")})) + + +class DomainDetailView(ListView): + template_name = "domain.html" + model = Issue + + def get_context_data(self, *args, **kwargs): + context = super(DomainDetailView, self).get_context_data(*args, **kwargs) + # remove any arguments from the slug + self.kwargs["slug"] = self.kwargs["slug"].split("?")[0] + domain = get_object_or_404(Domain, name=self.kwargs["slug"]) + context["domain"] = domain + + parsed_url = urlparse("http://" + self.kwargs["slug"]) + + open_issue = ( + Issue.objects.filter(domain__name__contains=self.kwargs["slug"]) + .filter(status="open", hunt=None) + .exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) + ) + close_issue = ( + Issue.objects.filter(domain__name__contains=self.kwargs["slug"]) + .filter(status="closed", hunt=None) + .exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) + ) + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + + context["name"] = parsed_url.netloc.split(".")[-2:][0].title() + + paginator = Paginator(open_issue, 3) + page = self.request.GET.get("open") + try: + openissue_paginated = paginator.page(page) + except PageNotAnInteger: + openissue_paginated = paginator.page(1) + except EmptyPage: + openissue_paginated = paginator.page(paginator.num_pages) + + paginator = Paginator(close_issue, 3) + page = self.request.GET.get("close") + try: + closeissue_paginated = paginator.page(page) + except PageNotAnInteger: + closeissue_paginated = paginator.page(1) + except EmptyPage: + closeissue_paginated = paginator.page(paginator.num_pages) + + context["opened_net"] = open_issue + context["opened"] = openissue_paginated + context["closed_net"] = close_issue + context["closed"] = closeissue_paginated + context["leaderboard"] = ( + User.objects.filter(issue__url__contains=self.kwargs["slug"]) + .annotate(total=Count("issue")) + .order_by("-total") + ) + context["current_month"] = datetime.now().month + context["domain_graph"] = ( + Issue.objects.filter(domain=context["domain"], hunt=None) + .filter( + created__month__gte=(datetime.now().month - 6), + created__month__lte=datetime.now().month, + ) + .annotate(month=ExtractMonth("created")) + .values("month") + .annotate(c=Count("id")) + .order_by() + ) + for i in range(0, 7): + context["bug_type_" + str(i)] = Issue.objects.filter( + domain=context["domain"], hunt=None, label=str(i) + ) + context["total_bugs"] = Issue.objects.filter(domain=context["domain"], hunt=None).count() + context["pie_chart"] = ( + Issue.objects.filter(domain=context["domain"], hunt=None) + .values("label") + .annotate(c=Count("label")) + .order_by() + ) + context["activities"] = Issue.objects.filter(domain=context["domain"], hunt=None).exclude( + Q(is_hidden=True) & ~Q(user_id=self.request.user.id) + )[0:3] + context["activity_screenshots"] = {} + for activity in context["activities"]: + context["activity_screenshots"][activity] = IssueScreenshot.objects.filter( + issue=activity.pk + ).first() + context["twitter_url"] = "https://twitter.com/%s" % domain.get_or_set_x_url(domain.get_name) + + return context + + +class StatsDetailView(TemplateView): + template_name = "stats.html" + + def get_context_data(self, *args, **kwargs): + context = super(StatsDetailView, self).get_context_data(*args, **kwargs) + + response = requests.get(settings.EXTENSION_URL) + soup = BeautifulSoup(response.text, "html.parser") + + stats = "" + for item in soup.findAll("span", {"class": "e-f-ih"}): + stats = item.attrs["title"] + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + context["extension_users"] = stats.replace(" users", "") or "0" + + # Prepare stats data for display + context["stats"] = [ + {"label": "Bugs", "count": Issue.objects.all().count(), "icon": "fas fa-bug"}, + {"label": "Users", "count": User.objects.all().count(), "icon": "fas fa-users"}, + {"label": "Hunts", "count": Hunt.objects.all().count(), "icon": "fas fa-crosshairs"}, + {"label": "Domains", "count": Domain.objects.all().count(), "icon": "fas fa-globe"}, + { + "label": "Extension Users", + "count": int(context["extension_users"].replace(",", "")), + "icon": "fas fa-puzzle-piece", + }, + { + "label": "Subscriptions", + "count": Subscription.objects.all().count(), + "icon": "fas fa-envelope", + }, + { + "label": "Companies", + "count": Company.objects.all().count(), + "icon": "fas fa-building", + }, + { + "label": "Hunt Prizes", + "count": HuntPrize.objects.all().count(), + "icon": "fas fa-gift", + }, + { + "label": "Screenshots", + "count": IssueScreenshot.objects.all().count(), + "icon": "fas fa-camera", + }, + {"label": "Winners", "count": Winner.objects.all().count(), "icon": "fas fa-trophy"}, + {"label": "Points", "count": Points.objects.all().count(), "icon": "fas fa-star"}, + { + "label": "Invitations", + "count": InviteFriend.objects.all().count(), + "icon": "fas fa-envelope-open", + }, + { + "label": "User Profiles", + "count": UserProfile.objects.all().count(), + "icon": "fas fa-id-badge", + }, + {"label": "IPs", "count": IP.objects.all().count(), "icon": "fas fa-network-wired"}, + { + "label": "Company Admins", + "count": CompanyAdmin.objects.all().count(), + "icon": "fas fa-user-tie", + }, + { + "label": "Transactions", + "count": Transaction.objects.all().count(), + "icon": "fas fa-exchange-alt", + }, + { + "label": "Payments", + "count": Payment.objects.all().count(), + "icon": "fas fa-credit-card", + }, + { + "label": "Contributor Stats", + "count": ContributorStats.objects.all().count(), + "icon": "fas fa-chart-bar", + }, + {"label": "Monitors", "count": Monitor.objects.all().count(), "icon": "fas fa-desktop"}, + {"label": "Bids", "count": Bid.objects.all().count(), "icon": "fas fa-gavel"}, + { + "label": "Chatbot Logs", + "count": ChatBotLog.objects.all().count(), + "icon": "fas fa-robot", + }, + { + "label": "Suggestions", + "count": Suggestion.objects.all().count(), + "icon": "fas fa-lightbulb", + }, + { + "label": "Suggestion Votes", + "count": SuggestionVotes.objects.all().count(), + "icon": "fas fa-thumbs-up", + }, + { + "label": "Contributors", + "count": Contributor.objects.all().count(), + "icon": "fas fa-user-friends", + }, + { + "label": "Projects", + "count": Project.objects.all().count(), + "icon": "fas fa-project-diagram", + }, + { + "label": "Contributions", + "count": Contribution.objects.all().count(), + "icon": "fas fa-hand-holding-heart", + }, + { + "label": "Bacon Tokens", + "count": BaconToken.objects.all().count(), + "icon": "fas fa-coins", + }, + ] + context["stats"] = sorted(context["stats"], key=lambda x: int(x["count"]), reverse=True) + + def get_cumulative_data(queryset, date_field="created"): + data = list( + queryset.annotate(month=ExtractMonth(date_field)) + .values("month") + .annotate(count=Count("id")) + .order_by("month") + .values_list("count", flat=True) + ) + + cumulative_data = [] + cumulative_sum = 0 + for count in data: + cumulative_sum += count + cumulative_data.append(cumulative_sum) + + return cumulative_data + + context["sparklines_data"] = [ + get_cumulative_data(Issue.objects), + get_cumulative_data(User.objects, date_field="date_joined"), + get_cumulative_data(Hunt.objects), + get_cumulative_data(Domain.objects), + get_cumulative_data(Subscription.objects), + get_cumulative_data(Company.objects), + get_cumulative_data(HuntPrize.objects), + get_cumulative_data(IssueScreenshot.objects), + get_cumulative_data(Winner.objects), + get_cumulative_data(Points.objects), + get_cumulative_data(InviteFriend.objects), + get_cumulative_data(UserProfile.objects), + get_cumulative_data(IP.objects), + get_cumulative_data(CompanyAdmin.objects), + get_cumulative_data(Transaction.objects), + get_cumulative_data(Payment.objects), + get_cumulative_data(ContributorStats.objects), + get_cumulative_data(Monitor.objects), + get_cumulative_data(Bid.objects), + get_cumulative_data(ChatBotLog.objects), + get_cumulative_data(Suggestion.objects), + get_cumulative_data(SuggestionVotes.objects), + get_cumulative_data(Contributor.objects), + get_cumulative_data(Project.objects), + get_cumulative_data(Contribution.objects), + get_cumulative_data(BaconToken.objects), + ] + + return context + + +class AllIssuesView(ListView): + paginate_by = 20 + template_name = "list_view.html" + + def get_queryset(self): + username = self.request.GET.get("user") + if username is None: + self.activities = Issue.objects.filter(hunt=None).exclude( + Q(is_hidden=True) & ~Q(user_id=self.request.user.id) + ) + else: + self.activities = Issue.objects.filter(user__username=username, hunt=None).exclude( + Q(is_hidden=True) & ~Q(user_id=self.request.user.id) + ) + return self.activities + + def get_context_data(self, *args, **kwargs): + context = super(AllIssuesView, self).get_context_data(*args, **kwargs) + paginator = Paginator(self.activities, self.paginate_by) + page = self.request.GET.get("page") + + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + try: + activities_paginated = paginator.page(page) + except PageNotAnInteger: + activities_paginated = paginator.page(1) + except EmptyPage: + activities_paginated = paginator.page(paginator.num_pages) + + context["activities"] = activities_paginated + context["user"] = self.request.GET.get("user") + context["activity_screenshots"] = {} + for activity in self.activities: + context["activity_screenshots"][activity] = IssueScreenshot.objects.filter( + issue=activity + ).first() + return context + + +class SpecificIssuesView(ListView): + paginate_by = 20 + template_name = "list_view.html" + + def get_queryset(self): + username = self.request.GET.get("user") + label = self.request.GET.get("label") + query = 0 + statu = "none" + + if label == "General": + query = 0 + elif label == "Number": + query = 1 + elif label == "Functional": + query = 2 + elif label == "Performance": + query = 3 + elif label == "Security": + query = 4 + elif label == "Typo": + query = 5 + elif label == "Design": + query = 6 + elif label == "open": + statu = "open" + elif label == "closed": + statu = "closed" + + if username is None: + self.activities = Issue.objects.filter(hunt=None).exclude( + Q(is_hidden=True) & ~Q(user_id=self.request.user.id) + ) + elif statu != "none": + self.activities = Issue.objects.filter( + user__username=username, status=statu, hunt=None + ).exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) + else: + self.activities = Issue.objects.filter( + user__username=username, label=query, hunt=None + ).exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) + return self.activities + + def get_context_data(self, *args, **kwargs): + context = super(SpecificIssuesView, self).get_context_data(*args, **kwargs) + paginator = Paginator(self.activities, self.paginate_by) + page = self.request.GET.get("page") + + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + try: + activities_paginated = paginator.page(page) + except PageNotAnInteger: + activities_paginated = paginator.page(1) + except EmptyPage: + activities_paginated = paginator.page(paginator.num_pages) + + context["activities"] = activities_paginated + context["user"] = self.request.GET.get("user") + context["label"] = self.request.GET.get("label") + return context + + +class LeaderboardBase: + def get_leaderboard(self, month=None, year=None, api=False): + data = User.objects + + if year and not month: + data = data.filter(points__created__year=year) + + if year and month: + data = data.filter(Q(points__created__year=year) & Q(points__created__month=month)) + + data = ( + data.annotate(total_score=Sum("points__score")) + .order_by("-total_score") + .filter( + total_score__gt=0, + ) + ) + if api: + return data.values("id", "username", "total_score") + + return data + + def current_month_leaderboard(self, api=False): + """ + leaderboard which includes current month users scores + """ + return self.get_leaderboard( + month=int(datetime.now().month), year=int(datetime.now().year), api=api + ) + + def monthly_year_leaderboard(self, year, api=False): + """ + leaderboard which includes current year top user from each month + """ + + monthly_winner = [] + + # iterating over months 1-12 + for month in range(1, 13): + month_winner = self.get_leaderboard(month, year, api).first() + monthly_winner.append(month_winner) + + return monthly_winner + + +class GlobalLeaderboardView(LeaderboardBase, ListView): + """ + Returns: All users:score data in descending order + """ + + model = User + template_name = "leaderboard_global.html" + + def get_context_data(self, *args, **kwargs): + context = super(GlobalLeaderboardView, self).get_context_data(*args, **kwargs) + + user_related_tags = Tag.objects.filter(userprofile__isnull=False).distinct() + context["user_related_tags"] = user_related_tags + + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + context["leaderboard"] = self.get_leaderboard() + return context + + +class EachmonthLeaderboardView(LeaderboardBase, ListView): + """ + Returns: Grouped user:score data in months for current year + """ + + model = User + template_name = "leaderboard_eachmonth.html" + + def get_context_data(self, *args, **kwargs): + context = super(EachmonthLeaderboardView, self).get_context_data(*args, **kwargs) + + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + + year = self.request.GET.get("year") + + if not year: + year = datetime.now().year + + if isinstance(year, str) and not year.isdigit(): + raise Http404(f"Invalid query passed | Year:{year}") + + year = int(year) + + leaderboard = self.monthly_year_leaderboard(year) + month_winners = [] + + months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "Novermber", + "December", + ] + + for month_indx, usr in enumerate(leaderboard): + month_winner = {"user": usr, "month": months[month_indx]} + month_winners.append(month_winner) + + context["leaderboard"] = month_winners + + return context + + +class SpecificMonthLeaderboardView(LeaderboardBase, ListView): + """ + Returns: leaderboard for filtered month and year requested in the query + """ + + model = User + template_name = "leaderboard_specific_month.html" + + def get_context_data(self, *args, **kwargs): + context = super(SpecificMonthLeaderboardView, self).get_context_data(*args, **kwargs) + + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + + month = self.request.GET.get("month") + year = self.request.GET.get("year") + + if not month: + month = datetime.now().month + if not year: + year = datetime.now().year + + if isinstance(month, str) and not month.isdigit(): + raise Http404(f"Invalid query passed | Month:{month}") + if isinstance(year, str) and not year.isdigit(): + raise Http404(f"Invalid query passed | Year:{year}") + + month = int(month) + year = int(year) + + if not (month >= 1 and month <= 12): + raise Http404(f"Invalid query passed | Month:{month}") + + context["leaderboard"] = self.get_leaderboard(month, year, api=False) + return context + + +class ScoreboardView(ListView): + model = Domain + template_name = "scoreboard.html" + paginate_by = 20 + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + + # Annotate each domain with the count of open issues + annotated_domains = Domain.objects.annotate( + open_issues_count=Count("issue", filter=Q(issue__status="open")) + ).order_by("-open_issues_count") + + paginator = Paginator(annotated_domains, self.paginate_by) + page = self.request.GET.get("page") + + try: + scoreboard_paginated = paginator.page(page) + except PageNotAnInteger: + scoreboard_paginated = paginator.page(1) + except EmptyPage: + scoreboard_paginated = paginator.page(paginator.num_pages) + + context["scoreboard"] = scoreboard_paginated + context["user"] = self.request.GET.get("user") + return context + + +class HuntCreate(CreateView): + model = Hunt + fields = ["url", "logo", "name", "description", "prize", "plan"] + template_name = "hunt.html" + + def form_valid(self, form): + self.object = form.save(commit=False) + self.object.user = self.request.user + + domain, created = Domain.objects.get_or_create( + name=self.request.POST.get("url").replace("www.", ""), + defaults={"url": "http://" + self.request.POST.get("url").replace("www.", "")}, + ) + self.object.domain = domain + + self.object.save() + return super(HuntCreate, self).form_valid(form) + + +class InboundParseWebhookView(View): + def post(self, request, *args, **kwargs): + data = request.body + for event in json.loads(data): + try: + domain = Domain.objects.get(email__iexact=event.get("email")) + domain.email_event = event.get("event") + if event.get("event") == "click": + domain.clicks = int(domain.clicks or 0) + 1 + domain.save() + except Exception: + pass + + return JsonResponse({"detail": "Inbound Sendgrid Webhook recieved"}) + + +class CustomObtainAuthToken(ObtainAuthToken): + def post(self, request, *args, **kwargs): + response = super(CustomObtainAuthToken, self).post(request, *args, **kwargs) + token = Token.objects.get(key=response.data["token"]) + return Response({"token": token.key, "id": token.user_id}) + + +class CreateHunt(TemplateView): + model = Hunt + fields = ["url", "logo", "domain", "plan", "prize", "txn_id"] + template_name = "create_hunt.html" + + @method_decorator(login_required) + def get(self, request, *args, **kwargs): + try: + domain_admin = CompanyAdmin.objects.get(user=request.user) + if not domain_admin.is_active: + return HttpResponseRedirect("/") + domain = [] + if domain_admin.role == 0: + domain = Domain.objects.filter(company=domain_admin.company) + else: + domain = Domain.objects.filter(pk=domain_admin.domain.pk) + + context = {"domains": domain, "hunt_form": HuntForm()} + return render(request, self.template_name, context) + except: + return HttpResponseRedirect("/") + + @method_decorator(login_required) + def post(self, request, *args, **kwargs): + try: + domain_admin = CompanyAdmin.objects.get(user=request.user) + if ( + domain_admin.role == 1 + and ( + str(domain_admin.domain.pk) + == ((request.POST["domain"]).split("-"))[0].replace(" ", "") + ) + ) or domain_admin.role == 0: + wallet, created = Wallet.objects.get_or_create(user=request.user) + total_amount = ( + Decimal(request.POST["prize_winner"]) + + Decimal(request.POST["prize_runner"]) + + Decimal(request.POST["prize_second_runner"]) + ) + if total_amount > wallet.current_balance: + return HttpResponse("failed") + hunt = Hunt() + hunt.domain = Domain.objects.get( + pk=(request.POST["domain"]).split("-")[0].replace(" ", "") + ) + data = {} + data["content"] = request.POST["content"] + data["start_date"] = request.POST["start_date"] + data["end_date"] = request.POST["end_date"] + form = HuntForm(data) + if not form.is_valid(): + return HttpResponse("failed") + if not domain_admin.is_active: + return HttpResponse("failed") + if domain_admin.role == 1: + if hunt.domain != domain_admin.domain: + return HttpResponse("failed") + hunt.domain = Domain.objects.get( + pk=(request.POST["domain"]).split("-")[0].replace(" ", "") + ) + tzsign = 1 + offset = request.POST["tzoffset"] + if int(offset) < 0: + offset = int(offset) * (-1) + tzsign = -1 + start_date = form.cleaned_data["start_date"] + end_date = form.cleaned_data["end_date"] + if tzsign > 0: + start_date = start_date + timedelta( + hours=int(int(offset) / 60), minutes=int(int(offset) % 60) + ) + end_date = end_date + timedelta( + hours=int(int(offset) / 60), minutes=int(int(offset) % 60) + ) + else: + start_date = start_date - ( + timedelta(hours=int(int(offset) / 60), minutes=int(int(offset) % 60)) + ) + end_date = end_date - ( + timedelta(hours=int(int(offset) / 60), minutes=int(int(offset) % 60)) + ) + hunt.starts_on = start_date + hunt.prize_winner = Decimal(request.POST["prize_winner"]) + hunt.prize_runner = Decimal(request.POST["prize_runner"]) + hunt.prize_second_runner = Decimal(request.POST["prize_second_runner"]) + hunt.end_on = end_date + hunt.name = request.POST["name"] + hunt.description = request.POST["content"] + wallet.withdraw(total_amount) + wallet.save() + try: + is_published = request.POST["publish"] + hunt.is_published = True + except: + hunt.is_published = False + hunt.save() + return HttpResponse("success") + else: + return HttpResponse("failed") + except: + return HttpResponse("failed") + + +class DomainListView(ListView): + model = Domain + paginate_by = 20 + template_name = "domain_list.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + domain = Domain.objects.all() + + paginator = Paginator(domain, self.paginate_by) + page = self.request.GET.get("page") + + try: + domain_paginated = paginator.page(page) + except PageNotAnInteger: + domain_paginated = paginator.page(1) + except EmptyPage: + domain_paginated = paginator.page(paginator.num_pages) + + context["domain"] = domain_paginated + return context + + +class IssueView(DetailView): + model = Issue + slug_field = "id" + template_name = "issue.html" + + def get(self, request, *args, **kwargs): + print("getting issue id: ", self.kwargs["slug"]) + print("getting issue id: ", self.kwargs) + ipdetails = IP() + try: + id = int(self.kwargs["slug"]) + except ValueError: + return HttpResponseNotFound("Invalid ID: ID must be an integer") + + self.object = get_object_or_404(Issue, id=self.kwargs["slug"]) + ipdetails.user = self.request.user + ipdetails.address = get_client_ip(request) + ipdetails.issuenumber = self.object.id + ipdetails.path = request.path + ipdetails.agent = request.META["HTTP_USER_AGENT"] + ipdetails.referer = request.META.get("HTTP_REFERER", None) + + print("IP Address: ", ipdetails.address) + print("Issue Number: ", ipdetails.issuenumber) + + try: + if self.request.user.is_authenticated: + try: + objectget = IP.objects.get(user=self.request.user, issuenumber=self.object.id) + self.object.save() + except: + ipdetails.save() + self.object.views = (self.object.views or 0) + 1 + self.object.save() + else: + try: + objectget = IP.objects.get( + address=get_client_ip(request), issuenumber=self.object.id + ) + self.object.save() + except Exception as e: + print(e) + pass # pass this temporarly to avoid error + # messages.error(self.request, "That issue was not found 2." + str(e)) + # ipdetails.save() + # self.object.views = (self.object.views or 0) + 1 + # self.object.save() + except Exception as e: + pass # pass this temporarly to avoid error + # print(e) + # messages.error(self.request, "That issue was not found 1." + str(e)) + # return redirect("/") + return super(IssueView, self).get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + print("getting context data") + context = super(IssueView, self).get_context_data(**kwargs) + if self.object.user_agent: + user_agent = parse(self.object.user_agent) + context["browser_family"] = user_agent.browser.family + context["browser_version"] = user_agent.browser.version_string + context["os_family"] = user_agent.os.family + context["os_version"] = user_agent.os.version_string + context["users_score"] = list( + Points.objects.filter(user=self.object.user) + .aggregate(total_score=Sum("score")) + .values() + )[0] + + if self.request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=self.request.user) + context["issue_count"] = Issue.objects.filter(url__contains=self.object.domain_name).count() + context["all_comment"] = self.object.comments.all + context["all_users"] = User.objects.all() + context["likes"] = UserProfile.objects.filter(issue_upvoted=self.object).count() + context["likers"] = UserProfile.objects.filter(issue_upvoted=self.object) + context["dislikes"] = UserProfile.objects.filter(issue_downvoted=self.object).count() + context["dislikers"] = UserProfile.objects.filter(issue_downvoted=self.object) + + context["flags"] = UserProfile.objects.filter(issue_flaged=self.object).count() + context["flagers"] = UserProfile.objects.filter(issue_flaged=self.object) + + context["screenshots"] = IssueScreenshot.objects.filter(issue=self.object).all() + + return context diff --git a/website/forms.py b/website/forms.py index f72074ddd..dc405b0b0 100644 --- a/website/forms.py +++ b/website/forms.py @@ -54,12 +54,6 @@ class CaptchaForm(forms.Form): captcha = CaptchaField() -class QuickIssueForm(forms.Form): - url = forms.CharField() - label = forms.CharField() - description = forms.CharField() - - class MonitorForm(forms.ModelForm): created = forms.DateTimeField(widget=forms.HiddenInput(), required=False, label="Created") modified = forms.DateTimeField(widget=forms.HiddenInput(), required=False, label="Modified") diff --git a/website/management/commands/check_keywords.py b/website/management/commands/check_keywords.py new file mode 100644 index 000000000..9bbf0050e --- /dev/null +++ b/website/management/commands/check_keywords.py @@ -0,0 +1,56 @@ +import requests +from bs4 import BeautifulSoup +from django.core.mail import send_mail +from django.core.management.base import BaseCommand +from django.utils import timezone + +from website.models import Monitor + + +class Command(BaseCommand): + help = "Checks for keywords in monitored URLs" + + def handle(self, *args, **options): + monitors = Monitor.objects.all() + for monitor in monitors: + try: + response = requests.get(monitor.url) + response.raise_for_status() + + soup = BeautifulSoup(response.content, "html.parser") + page_content = soup.get_text() + + if monitor.keyword in page_content: + new_status = "UP" + else: + new_status = "DOWN" + + if monitor.status != new_status: + monitor.status = new_status + monitor.save() + + user = monitor.user + self.notify_user(user.username, monitor.url, user.email, new_status) + + monitor.last_checked_time = timezone.now() + monitor.save() + + self.stdout.write( + self.style.SUCCESS(f"Monitoring {monitor.url}: status {monitor.status}") + ) + except Exception as e: + self.stderr.write(self.style.ERROR(f"Error monitoring {monitor.url}: {str(e)}")) + + def notify_user(self, username, website, email, status): + subject = f"Website Status Update: {website} is {status}" + message = ( + f"Dear {username},\n\nThe website '{website}' you are monitoring is currently {status}." + ) + + send_mail( + subject, + message, + None, + [email], + fail_silently=False, + ) diff --git a/website/management/commands/update_projects.py b/website/management/commands/update_projects.py index 4bf094da6..52aab4e73 100644 --- a/website/management/commands/update_projects.py +++ b/website/management/commands/update_projects.py @@ -1,17 +1,99 @@ +import requests +from django.core.exceptions import MultipleObjectsReturned from django.core.management.base import BaseCommand +from django.utils.dateparse import parse_datetime -from website.models import Project +from website.models import Contributor, Project class Command(BaseCommand): - help = "Update projects with their contributors from GitHub" + help = "Update projects with their contributors and latest release from GitHub" + + def add_arguments(self, parser): + parser.add_argument( + "--project_id", + type=int, + help="Specify a project ID to update only that project", + ) def handle(self, *args, **kwargs): - projects = Project.objects.prefetch_related("contributors").all() + project_id = kwargs.get("project_id") + if project_id: + projects = Project.objects.filter(id=project_id).prefetch_related("contributors") + else: + projects = Project.objects.prefetch_related("contributors").all() + for project in projects: - contributors = Project.get_contributors(None, github_url=project.github_url) + owner_repo = project.github_url.rstrip("/").split("/")[-2:] + repo_name = f"{owner_repo[0]}/{owner_repo[1]}" + contributors = [] + + page = 1 + while True: + url = f"https://api.github.com/repos/{repo_name}/contributors?per_page=100&page={page}" + print(f"Fetching contributors from URL: {url}") + response = requests.get(url, headers={"Content-Type": "application/json"}) + + if response.status_code != 200: + break + + contributors_data = response.json() + if not contributors_data: + break + + for c in contributors_data: + try: + contributor, created = Contributor.objects.get_or_create( + github_id=c["id"], + defaults={ + "name": c["login"], + "github_url": c["html_url"], + "avatar_url": c["avatar_url"], + "contributor_type": c["type"], + "contributions": c["contributions"], + }, + ) + contributors.append(contributor) + except MultipleObjectsReturned: + contributor = Contributor.objects.filter(github_id=c["id"]).first() + contributors.append(contributor) + + page += 1 + + # Fetch stars, forks, and issues count + url = f"https://api.github.com/repos/{repo_name}" + response = requests.get(url, headers={"Content-Type": "application/json"}) + if response.status_code == 200: + repo_data = response.json() + project.stars = repo_data.get("stargazers_count", 0) + project.forks = repo_data.get("forks_count", 0) + project.total_issues = repo_data.get( + "open_issues_count", 0 + ) # Directly use open_issues_count + + # Fetch last commit date + commits_url = f"https://api.github.com/repos/{repo_name}/commits" + commits_response = requests.get( + commits_url, headers={"Content-Type": "application/json"} + ) + if commits_response.status_code == 200: + commits_data = commits_response.json() + if commits_data: + last_commit_date = ( + commits_data[0].get("commit", {}).get("committer", {}).get("date") + ) + project.last_updated = parse_datetime(last_commit_date) + + # Fetch latest release + url = f"https://api.github.com/repos/{repo_name}/releases/latest" + response = requests.get(url, headers={"Content-Type": "application/json"}) + if response.status_code == 200: + release_data = response.json() + project.release_name = release_data.get("name") or release_data.get("tag_name") + project.release_datetime = parse_datetime(release_data.get("published_at")) + project.contributors.set(contributors) - # serializer = ProjectSerializer(projects, many=True) - # projects_data = json.dumps(serializer.data, indent=4) # serialize the data to be printed + project.contributor_count = len(contributors) + project.save() + self.stdout.write(self.style.SUCCESS(f"Successfully updated {len(projects)} projects")) - # self.stdout.write(projects_data) # if need to return the data too in the terminal diff --git a/website/migrations/0135_add_project_metadata.py b/website/migrations/0135_add_project_metadata.py new file mode 100644 index 000000000..8506cf939 --- /dev/null +++ b/website/migrations/0135_add_project_metadata.py @@ -0,0 +1,25 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0134_blocked_modified"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="stars", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="project", + name="forks", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="project", + name="external_links", + field=models.JSONField(default=list, blank=True), + ), + ] diff --git a/website/migrations/0136_project_contributor_count.py b/website/migrations/0136_project_contributor_count.py new file mode 100644 index 000000000..e5e717527 --- /dev/null +++ b/website/migrations/0136_project_contributor_count.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.8 on 2024-10-12 22:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0135_add_project_metadata"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="contributor_count", + field=models.IntegerField(default=0), + ), + ] diff --git a/website/migrations/0137_project_release_datetime_project_release_name.py b/website/migrations/0137_project_release_datetime_project_release_name.py new file mode 100644 index 000000000..790fde458 --- /dev/null +++ b/website/migrations/0137_project_release_datetime_project_release_name.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.8 on 2024-10-12 22:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0136_project_contributor_count"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="release_datetime", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="project", + name="release_name", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/website/migrations/0138_project_last_updated.py b/website/migrations/0138_project_last_updated.py new file mode 100644 index 000000000..241689182 --- /dev/null +++ b/website/migrations/0138_project_last_updated.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.8 on 2024-10-12 23:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0137_project_release_datetime_project_release_name"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="last_updated", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/website/migrations/0138_project_total_issues.py b/website/migrations/0138_project_total_issues.py new file mode 100644 index 000000000..28bafa279 --- /dev/null +++ b/website/migrations/0138_project_total_issues.py @@ -0,0 +1,15 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0137_project_release_datetime_project_release_name"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="total_issues", + field=models.IntegerField(default=0), + ), + ] diff --git a/website/migrations/0139_merge_20241012_2351.py b/website/migrations/0139_merge_20241012_2351.py new file mode 100644 index 000000000..8b3e6a1e6 --- /dev/null +++ b/website/migrations/0139_merge_20241012_2351.py @@ -0,0 +1,12 @@ +# Generated by Django 5.0.8 on 2024-10-12 23:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0138_project_last_updated"), + ("website", "0138_project_total_issues"), + ] + + operations = [] diff --git a/website/migrations/0140_dailystatusreport.py b/website/migrations/0140_dailystatusreport.py new file mode 100644 index 000000000..6b6e63f25 --- /dev/null +++ b/website/migrations/0140_dailystatusreport.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0.8 on 2024-10-26 10:55 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0139_merge_20241012_2351"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="DailyStatusReport", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField()), + ("previous_work", models.TextField()), + ("next_plan", models.TextField()), + ("blockers", models.TextField()), + ("created", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/website/migrations/0141_project_project_visit_count_project_repo_visit_count.py b/website/migrations/0141_project_project_visit_count_project_repo_visit_count.py new file mode 100644 index 000000000..feec3c8c2 --- /dev/null +++ b/website/migrations/0141_project_project_visit_count_project_repo_visit_count.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.8 on 2024-10-28 03:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0140_dailystatusreport"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="project_visit_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="project", + name="repo_visit_count", + field=models.IntegerField(default=0), + ), + ] diff --git a/website/models.py b/website/models.py index a242cb67e..23c9b4da3 100644 --- a/website/models.py +++ b/website/models.py @@ -11,7 +11,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.cache import cache -from django.core.exceptions import MultipleObjectsReturned, ValidationError +from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.core.files.storage import default_storage from django.core.validators import URLValidator @@ -742,54 +742,25 @@ class Project(models.Model): wiki_url = models.URLField(null=True, blank=True) homepage_url = models.URLField(null=True, blank=True) logo_url = models.URLField() + stars = models.IntegerField(default=0) + forks = models.IntegerField(default=0) + contributor_count = models.IntegerField(default=0) + release_name = models.CharField(max_length=255, null=True, blank=True) + release_datetime = models.DateTimeField(null=True, blank=True) + external_links = models.JSONField(default=list, blank=True) created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) contributors = models.ManyToManyField(Contributor, related_name="projects", blank=True) tags = models.ManyToManyField(Tag, blank=True) + last_updated = models.DateTimeField(null=True, blank=True) + total_issues = models.IntegerField(default=0) + repo_visit_count = models.IntegerField(default=0) + project_visit_count = models.IntegerField(default=0) def __str__(self): return self.name - def get_contributors(self, github_url): - owner_repo = github_url.rstrip("/").split("/")[-2:] - repo_name = f"{owner_repo[0]}/{owner_repo[1]}" - contributors = [] - - page = 1 - while True: - url = f"https://api.github.com/repos/{repo_name}/contributors?per_page=100&page={page}" - print(f"Fetching contributors from URL: {url}") - response = requests.get(url, headers={"Content-Type": "application/json"}) - - if response.status_code != 200: - break - - contributors_data = response.json() - if not contributors_data: - break - - for c in contributors_data: - try: - contributor, created = Contributor.objects.get_or_create( - github_id=c["id"], - defaults={ - "name": c["login"], - "github_url": c["html_url"], - "avatar_url": c["avatar_url"], - "contributor_type": c["type"], - "contributions": c["contributions"], - }, - ) - contributors.append(contributor) - except MultipleObjectsReturned: - contributor = Contributor.objects.filter(github_id=c["id"]).first() - contributors.append(contributor) - - page += 1 - - return contributors if contributors else None - - def get_top_contributors(self, limit=5): + def get_top_contributors(self, limit=30): return self.contributors.order_by("-contributions")[:limit] @@ -899,3 +870,15 @@ class ActivityLog(models.Model): def __str__(self): return f"ActivityLog by {self.user.username} at {self.recorded_at}" + + +class DailyStatusReport(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + date = models.DateField() + previous_work = models.TextField() + next_plan = models.TextField() + blockers = models.TextField() + created = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Daily Status Report by {self.user.username} on {self.date}" diff --git a/website/serializers.py b/website/serializers.py index 41df4efda..a73857674 100644 --- a/website/serializers.py +++ b/website/serializers.py @@ -125,11 +125,20 @@ class Meta: class ProjectSerializer(serializers.ModelSerializer): + freshness = serializers.SerializerMethodField() + stars = serializers.IntegerField() + forks = serializers.IntegerField() + external_links = serializers.JSONField() + project_visit_count = serializers.IntegerField() + class Meta: model = Project fields = "__all__" read_only_fields = ("slug", "contributors") + def get_freshness(self, obj): + return obj.fetch_freshness() + class ContributorSerializer(serializers.ModelSerializer): class Meta: diff --git a/website/static/img/sizzle/app_finding_1.jpg b/website/static/img/sizzle/app_finding_1.jpg new file mode 100644 index 000000000..343d63d90 Binary files /dev/null and b/website/static/img/sizzle/app_finding_1.jpg differ diff --git a/website/static/img/sizzle/app_finding_2.jpg b/website/static/img/sizzle/app_finding_2.jpg new file mode 100644 index 000000000..d20a8defe Binary files /dev/null and b/website/static/img/sizzle/app_finding_2.jpg differ diff --git a/website/static/img/sizzle/running_sizzle_1.jpg b/website/static/img/sizzle/running_sizzle_1.jpg new file mode 100644 index 000000000..e9ff8b473 Binary files /dev/null and b/website/static/img/sizzle/running_sizzle_1.jpg differ diff --git a/website/static/img/sizzle/running_sizzle_2.jpg b/website/static/img/sizzle/running_sizzle_2.jpg new file mode 100644 index 000000000..cf7529569 Binary files /dev/null and b/website/static/img/sizzle/running_sizzle_2.jpg differ diff --git a/website/static/img/sizzle/running_sizzle_3.jpg b/website/static/img/sizzle/running_sizzle_3.jpg new file mode 100644 index 000000000..57a1c3d57 Binary files /dev/null and b/website/static/img/sizzle/running_sizzle_3.jpg differ diff --git a/website/static/img/sizzle/running_sizzle_4.jpg b/website/static/img/sizzle/running_sizzle_4.jpg new file mode 100644 index 000000000..fa4139334 Binary files /dev/null and b/website/static/img/sizzle/running_sizzle_4.jpg differ diff --git a/website/static/img/sizzle/running_sizzle_5.jpg b/website/static/img/sizzle/running_sizzle_5.jpg new file mode 100644 index 000000000..cf5185182 Binary files /dev/null and b/website/static/img/sizzle/running_sizzle_5.jpg differ diff --git a/website/static/img/sizzle/running_sizzle_6.jpg b/website/static/img/sizzle/running_sizzle_6.jpg new file mode 100644 index 000000000..1a5f1de2f Binary files /dev/null and b/website/static/img/sizzle/running_sizzle_6.jpg differ diff --git a/website/templates/_leaderboard_widget.html b/website/templates/_leaderboard_widget.html index a51598486..3f94914c3 100644 --- a/website/templates/_leaderboard_widget.html +++ b/website/templates/_leaderboard_widget.html @@ -87,20 +87,20 @@ } -
-
LEADERBOARD
-
+
+
+ LEADERBOARD +
+
1st Place for {% now "F" %} receives: -
+
No sponsored prizes this month
- + Sponsor a Prize
-
+
{% if not leaderboard %} Leaderboard has been reset for {% now "F" %}, be the first to find issues! {% endif %} @@ -165,9 +165,9 @@ View All Filter Monthly + class="btn btn-default btn-block submit_button p-2 rounded-2xl text-wrap">Filter Monthly View Monthly + class="btn btn-default btn-block submit_button p-2 rounded-2xl text-wrap">View Monthly
{% comment %} positions the leaderboard rightmost {% endcomment %} diff --git a/website/templates/domain.html b/website/templates/domain.html index 733d38841..9e7297fe7 100644 --- a/website/templates/domain.html +++ b/website/templates/domain.html @@ -1,274 +1,252 @@ {% extends "base.html" %} {% load static %} +{% load i18n %} {% load gravatar %} {% load email_obfuscator %} {% load socialaccount %} {% providers_media_js %} +{% block head %} + +{% endblock head %} {% block style %} {% endblock style %} {% block content %} {% include "includes/sidenav.html" %} - - -
-
-
- logo - logo -

- {{ name }} - {% if not domain.company %} -
- {% csrf_token %} - - - -
- {% endif %} -
- {% if domain.email %} - - [{{ domain.email|obfuscate_mailto }}] - {% if domain.email_event %} - Last email {{ domain.email_event|default:"" }} {{ domain.modified|timesince }} ago | - Clicks {{ domain.clicks|default:"0" }} - {% endif %} - - {% endif %} -

-
-
-
- - - - -
-
- {% if twitter_url %} - - {% endif %} -
-
- -
-
-
- {% if opened.has_previous %} - Prev +
+
+
+ {% if domain.get_logo %} + avatar + {% else %} + avatar {% endif %} - Page {{ opened.number }} of {{ opened.paginator.num_pages }} - {% if opened.has_next %}Next{% endif %}
- -
-
-
- {% for activity in opened %} - {% include "_activity.html" %} - {% endfor %} -
-
- {% if opened.has_previous %} - Prev - {% endif %} - Page {{ opened.number }} of {{ opened.paginator.num_pages }} - {% if opened.has_next %}Next{% endif %} -
-
-
+
+
+
+ Recent Activity +
-
-
- {% for activity in closed %} - {% include "_activity.html" %} - {% endfor %} +
+ +
+
+

+ {% for activity in opened %} + {% include "includes/_new_activity.html" %} + {% endfor %} +

+ {% if opened.has_previous %} + Prev + {% endif %} + Page {{ opened.number }} of {{ opened.paginator.num_pages }} + {% if opened.has_next %}Next{% endif %} +
+

+
+
-
- {% if closed.has_previous %} - Prev +
+
+
+
+
+
+
+ {{ name }} + {% if domain.email %} + + {{ domain.email|obfuscate_mailto }} + {% if domain.email_event %} + Last email {{ domain.email_event|default:"" }} {{ domain.modified|timesince }} ago +
+ Clicks {{ domain.clicks|default:"0" }} + {% endif %} +
{% endif %} - Page {{ closed.number }} of {{ closed.paginator.num_pages }} - {% if closed.has_next %}Next{% endif %}
-
-
+
+
+ {% if domain.company %} + {% csrf_token %} + + + + {% endif %} + +
+
+
+
+ Issue Stats +
+
+ + + + + + + +
+
+ Domain Stats +
+
+
+ + + +
+
+ Monthly Report +
+
+
-
-

Top Bug Hunters for {{ name }}

-
- {% if leaderboard %} - {% for leader in leaderboard %} -
- {% if leader.socialaccount_set.all.0.get_avatar_url %} - {{ leader.username }} +
+
+
+ Top Hunters +
+
+
+
+
- {% endfor %} - {% else %} - Leaderboard reset for {% now "F" %}. Be first to find issues! - {% endif %} -
-
-
- {% if domain.webshot %} -
- - {{ domain.url }} - + +
- {% endif %} +
- + + {% endblock content %} diff --git a/website/templates/includes/_new_activity.html b/website/templates/includes/_new_activity.html new file mode 100644 index 000000000..3b676127c --- /dev/null +++ b/website/templates/includes/_new_activity.html @@ -0,0 +1,91 @@ +{% load static %} +{% load gravatar %} +
+ +
+ {% for screenshot_activity,screenshot in activity_screenshots.items %} + {% if activity == screenshot_activity %} + + screenshot + + {% endif %} + {% endfor %} + {% if activity.screenshot %} + + screenshot + + {% endif %} +
+
diff --git a/website/templates/includes/header.html b/website/templates/includes/header.html index 7bb44cd54..fdb403635 100644 --- a/website/templates/includes/header.html +++ b/website/templates/includes/header.html @@ -130,15 +130,17 @@
-
- - -
+
+
+ + +
+
+
diff --git a/website/templates/profile.html b/website/templates/profile.html index 688a62696..bdda41c67 100644 --- a/website/templates/profile.html +++ b/website/templates/profile.html @@ -470,7 +470,7 @@ User Stats
-
+
diff --git a/website/templates/sizzle/sizzle.html b/website/templates/sizzle/sizzle.html new file mode 100644 index 000000000..dd3d4e2f5 --- /dev/null +++ b/website/templates/sizzle/sizzle.html @@ -0,0 +1,155 @@ +{% extends "base.html" %} +{% load static %} +{% block content %} + + +
+ + {% include "includes/sidenav.html" %} + +
+
+

Your Sizzle Report

+
+ +
+
+ + +
+
+
+ + {% if sizzle_data %} + {{ sizzle_data.date }} + {% else %} + No Data + {% endif %} +
+ + + + + + + + + + + + + + + +
Issue TitleStartedTotal
+ {% if sizzle_data %} + {{ sizzle_data.issue_title }} + {% else %} + No Data + {% endif %} + + {% if sizzle_data %} + {{ sizzle_data.start_time }} + {% else %} + No Data + {% endif %} + + {% if sizzle_data %} + {{ sizzle_data.duration }} + {% else %} + No Data + {% endif %} +
+
+
+
+
+ + + + +{% endblock content %} diff --git a/website/templates/sizzle/sizzle_daily_status.html b/website/templates/sizzle/sizzle_daily_status.html new file mode 100644 index 000000000..a395b30a7 --- /dev/null +++ b/website/templates/sizzle/sizzle_daily_status.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} +{% load static %} +{% block content %} + +
+ + {% include "includes/sidenav.html" %} + +
+
+

Your Sizzle Daily Status Report

+
+ + +
+
Daily Status Report
+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+
Your Daily Status Reports
+ {% for report in reports %} +
+

Date: {{ report.date }}

+

+ What did you work on previously? +

+

{{ report.previous_work }}

+

+ What do you plan to do next? +

+

{{ report.next_plan }}

+

+ Do you have any blockers? +

+

{{ report.blockers }}

+
+ {% empty %} +
+

You have not submitted any daily status reports yet.

+
+ {% endfor %} +
+
+
+{% endblock content %} diff --git a/website/templates/sizzle/sizzle_docs.html b/website/templates/sizzle/sizzle_docs.html new file mode 100644 index 000000000..3d5ab1458 --- /dev/null +++ b/website/templates/sizzle/sizzle_docs.html @@ -0,0 +1,282 @@ +{% extends "base.html" %} +{% load static %} +{% block content %} + +
+ + {% include "includes/sidenav.html" %} + +
+

How to Use Sizzle

+
+
+
Step 1
+
Download the App
+

+ To get started, download the app from GitHub using the following link: Click Here +

+
+
+
+
+
Step 2
+
Find the Latest Successful Build
+

Locate the most recent successful build and click on it to proceed with the download.

+
+ Finding the App on GitHub Actions +
+
+
+
Step 3
+
Download the App for Your Device
+

Select the appropriate version of the app for your device and download it.

+
+ Choosing Device Version to Download +
+
+
+
Step 4
+
Open the App and Login
+

Once downloaded, open the app and login using your credentials.

+
+ Logging into the App +
+
+
+
Step 5
+
Access the Side Menu and Click "Sizzle"
+

Open the side menu and tap on the "Sizzle" button to continue.

+
+ Accessing Side Menu +
+
+
+
Step 6
+
Enter Your GitHub Username
+

Enter your GitHub username in the provided field and click "Add Username".

+
+ Entering GitHub Username +
+
+
+
Step 7
+
Start Time Tracking
+

Click on the "Start Work" button to begin tracking your work time.

+
+ Starting Time Tracking +
+
+
+
Step 8
+
View and Select Assigned Issues
+

+ All assigned issues will be displayed here. Select any task to begin working and click on the "Run" button to start tracking time for that task. +

+
+ Viewing and Selecting Tasks +
+
+
+
Step 9
+
Track Time for the Task
+

+ Congratulations! You are now tracking time for the selected task. Click on the "Stop" button to stop tracking once completed. +

+
+ Tracking Time for Selected Task +
+
+
+
Step 10
+
View Total Time Spent
+

After stopping the timer, you can view the total time spent on the task on the "Sizzle" page of the website.

+
+
+
+
+ + +{% endblock content %} diff --git a/website/templates/sizzle/time_logs.html b/website/templates/sizzle/time_logs.html new file mode 100644 index 000000000..e73278d90 --- /dev/null +++ b/website/templates/sizzle/time_logs.html @@ -0,0 +1,244 @@ + +{% extends "base.html" %} +{% load static %} +{% block content %} + +
+ + {% include "includes/sidenav.html" %} + +
+ {% if not active_time_log %} + +
+ {% csrf_token %} +
+ + + + Enter a valid GitHub issue URL (e.g., https://github.com/user/repo/issues/1). + +
Please provide a valid GitHub Issue URL.
+
+ +
+ {% endif %} + + {% if active_time_log %} +
+

Active Time Log

+

+ GitHub Issue URL: + {{ active_time_log.github_issue_url }} +

+

+ Elapsed Time: 00:00:00 +

+ +
+ {% endif %} + +

Existing Time Logs

+ + + + + + + + + + + + {% for log in time_logs %} + {% if log.end_time %} + + + + + + + {% endif %} + {% empty %} + + + + {% endfor %} + +
List of all time logs with their details.
Start TimeEnd TimeDurationGitHub Issue URL
{{ log.start_time|date:"DATETIME_FORMAT" }}{{ log.end_time|date:"DATETIME_FORMAT" }}{{ log.duration }} + {{ log.github_issue_url }} +
No time logs available.
+
+ +{% endblock content %} diff --git a/website/templates/website/project_detail.html b/website/templates/website/project_detail.html index 08cc15d7a..e2aa82eb3 100644 --- a/website/templates/website/project_detail.html +++ b/website/templates/website/project_detail.html @@ -5,108 +5,306 @@ {% block content %} {% include "includes/sidenav.html" %}
+
{% if project.logo %} - {% endif %} -

{{ project.name }}

-
-

{{ project.description }}

- +
+

Project Statistics

+
+
+ + {{ project.stars }} +

Stars

+
+
+ + {{ project.forks }} +

Forks

+
+
+ + {{ project.watchers }} +

Watchers

+
+
+ + {{ project.total_issues }} +

Open Issues

+
+
+ + {{ project.open_pull_requests }} +

Open PRs

+
+
+ + {{ project.primary_language }} +

Language

+
+
+ + {{ project.license }} +

License

+
+
+ + {{ project.last_commit_date }} +

Last Commit

+
+
+ + {{ project.project_visit_count }} +

Project Visit Count

+
+
+ + {{ project.repo_visit_count }} +

Repo Visit Count

+
+
+
+ +
+

Repo View Count Badge

+

This badge displays the number of views your repository has received:

+
![Repo Views](https://blt.owasp.org/projects/{{project.slug}}/badge
+
+ +

Top Contributors

{% for contributor in project.get_top_contributors %}
- {{ contributor.name }} + + {{ contributor.name }} + +

{{ contributor.name }}

{% endfor %}
+ +
+

Latest Releases

+
    + {% for release in project.releases %} +
  • + {{ release.name }} + {{ release.published_at }} +
  • + {% endfor %} +
+
+ +
+ {% endblock content %} diff --git a/website/templates/website/project_list.html b/website/templates/website/project_list.html index 0dd92272d..ca8ea467e 100644 --- a/website/templates/website/project_list.html +++ b/website/templates/website/project_list.html @@ -1,11 +1,59 @@ {% extends "base.html" %} +{% load humanize %} {% block title %} Project List {% endblock title %} {% block content %} {% include "includes/sidenav.html" %}
-

Project List

+
+

Projects: {{ projects.count }}

+
+ {% csrf_token %} +
+
+ +
+ + +
+ {% csrf_token %} + +
+
+
+ + +
+
{% if messages %}
    {% for message in messages %} @@ -13,11 +61,6 @@

    Project List

    {% endfor %}
{% endif %} -
- {% csrf_token %} - {{ form.as_p }} - -
{% endblock content %} diff --git a/website/test_api.py b/website/test_api.py index d1d35047e..73e7ad9e2 100644 --- a/website/test_api.py +++ b/website/test_api.py @@ -1,9 +1,8 @@ -import datetime - from django.contrib.auth import get_user_model from django.core import mail from django.db.transaction import atomic from django.urls import reverse +from django.utils import timezone from django.utils.encoding import force_str from rest_framework import status from rest_framework.test import APITestCase @@ -112,20 +111,18 @@ def test_get_bug_hunt(self): self.assertEqual(response.status_code, status.HTTP_200_OK) if len(response.data): self.assertTrue( - response.data[0]["starts_on"] < datetime.datetime.now() - and response.data[0]["end_on"] > datetime.datetime.now(), + response.data[0]["starts_on"] < timezone.now() + and response.data[0]["end_on"] > timezone.now(), "Invalid Response", ) response = self.client.get("".join([url, "previousHunt=1/"])) self.assertEqual(response.status_code, status.HTTP_200_OK) if len(response.data): - self.assertLess(response.data[0]["end_on"], datetime.datetime.now(), "Invalid Response") + self.assertLess(response.data[0]["end_on"], timezone.now(), "Invalid Response") response = self.client.get("".join([url, "upcomingHunt=1/"])) self.assertEqual(response.status_code, status.HTTP_200_OK) if len(response.data): - self.assertGreater( - response.data[0]["starts_on"], datetime.datetime.now(), "Invalid Response" - ) + self.assertGreater(response.data[0]["starts_on"], timezone.now(), "Invalid Response") def test_get_issues(self): url = "/api/v1/issues/" diff --git a/website/tests_urls.py b/website/tests_urls.py index 43e65b86d..9c805c903 100644 --- a/website/tests_urls.py +++ b/website/tests_urls.py @@ -2,8 +2,11 @@ import os import chromedriver_autoinstaller +from allauth.socialaccount.models import SocialApp from django.conf import settings +from django.contrib.sites.models import Site from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from django.db import transaction from django.urls import reverse from selenium.webdriver.chrome.service import Service @@ -31,6 +34,42 @@ def tearDownClass(cls): cls.selenium.quit() super(UrlsTest, cls).tearDownClass() + def setUp(self): + site = Site.objects.get(pk=1) + site.domain = "localhost:8082" + site.name = "localhost" + site.save() + + # Delete existing SocialApp instances for the providers + SocialApp.objects.filter(provider__in=["github", "google", "facebook"]).delete() + + # Create SocialApp for GitHub + github_app = SocialApp.objects.create( + provider="github", + name="GitHub", + client_id="dummy_client_id", + secret="dummy_secret", + ) + github_app.sites.add(site) + + # Create SocialApp for Google + google_app = SocialApp.objects.create( + provider="google", + name="Google", + client_id="dummy_client_id", + secret="dummy_secret", + ) + google_app.sites.add(site) + + # Create SocialApp for Facebook + facebook_app = SocialApp.objects.create( + provider="facebook", + name="Facebook", + client_id="dummy_client_id", + secret="dummy_secret", + ) + facebook_app.sites.add(site) + def test_responses( self, allowed_http_codes=[200, 302, 405, 401, 404], @@ -92,13 +131,60 @@ def check_urls(urlpatterns, prefix=""): "/error/", "/tz_detect/set/", "/leaderboard/api/", + "/api/timelogsreport/", ] if not any(x in url for x in matches): - response = self.client.get(url) - self.assertIn(response.status_code, allowed_http_codes, msg=url) - self.selenium.get("%s%s" % (self.live_server_url, url)) + with transaction.atomic(): + response = self.client.get(url) + self.assertIn(response.status_code, allowed_http_codes, msg=url) + self.selenium.get("%s%s" % (self.live_server_url, url)) - for entry in self.selenium.get_log("browser"): - self.assertNotIn("SyntaxError", str(entry), msg=url) + for entry in self.selenium.get_log("browser"): + self.assertNotIn("SyntaxError", str(entry), msg=url) check_urls(module.urlpatterns) + + def test_github_login(self): + url = reverse("github_login") + response = self.client.get(url) + self.assertIn(response.status_code, [200, 302, 405, 401, 404], msg=url) + + def test_google_login(self): + url = reverse("google_login") + response = self.client.get(url) + self.assertIn(response.status_code, [200, 302, 405, 401, 404], msg=url) + + def test_facebook_login(self): + url = reverse("facebook_login") + response = self.client.get(url) + self.assertIn(response.status_code, [200, 302, 405, 401, 404], msg=url) + + def test_github_callback(self): + url = reverse("github_callback") + response = self.client.get(url) + self.assertIn(response.status_code, [200, 302, 405, 401, 404], msg=url) + + def test_google_callback(self): + url = reverse("google_callback") + response = self.client.get(url) + self.assertIn(response.status_code, [200, 302, 405, 401, 404], msg=url) + + def test_facebook_callback(self): + url = reverse("facebook_callback") + response = self.client.get(url) + self.assertIn(response.status_code, [200, 302, 405, 401, 404], msg=url) + + def test_github_connect(self): + url = reverse("github_connect") + response = self.client.get(url) + self.assertIn(response.status_code, [200, 302, 405, 401, 404], msg=url) + + def test_google_connect(self): + url = reverse("google_connect") + response = self.client.get(url) + self.assertIn(response.status_code, [200, 302, 405, 401, 404], msg=url) + + def test_facebook_connect(self): + url = reverse("facebook_connect") + response = self.client.get(url) + self.assertIn(response.status_code, [200, 302, 405, 401, 404], msg=url) diff --git a/website/utils.py b/website/utils.py new file mode 100644 index 000000000..7c7bf1421 --- /dev/null +++ b/website/utils.py @@ -0,0 +1,119 @@ +import re +import time +from collections import deque +from urllib.parse import urlparse, urlsplit, urlunparse + +import requests +from bs4 import BeautifulSoup +from django.core.exceptions import ValidationError +from django.core.validators import URLValidator + +WHITELISTED_IMAGE_TYPES = { + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "png": "image/png", +} + + +def get_client_ip(request): + """Extract the client's IP address from the request.""" + x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") + if x_forwarded_for: + ip = x_forwarded_for.split(",")[0] + else: + ip = request.META.get("REMOTE_ADDR") + return ip + + +def get_email_from_domain(domain_name): + new_urls = deque(["http://" + domain_name]) + processed_urls = set() + emails = set() + emails_out = set() + t_end = time.time() + 20 + + while len(new_urls) and time.time() < t_end: + url = new_urls.popleft() + processed_urls.add(url) + parts = urlsplit(url) + base_url = "{0.scheme}://{0.netloc}".format(parts) + path = url[: url.rfind("/") + 1] if "/" in parts.path else url + try: + response = requests.get(url) + except: + continue + new_emails = set( + re.findall(r"[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+", response.text, re.I) + ) + if new_emails: + emails.update(new_emails) + break + soup = BeautifulSoup(response.text) + for anchor in soup.find_all("a"): + link = anchor.attrs["href"] if "href" in anchor.attrs else "" + if link.startswith("/"): + link = base_url + link + elif not link.startswith("http"): + link = path + link + if link not in new_urls and link not in processed_urls and link.find(domain_name) > 0: + new_urls.append(link) + + for email in emails: + if email.find(domain_name) > 0: + emails_out.add(email) + try: + return list(emails_out)[0] + except: + return False + + +def image_validator(img): + try: + filesize = img.file.size + except: + filesize = img.size + + extension = img.name.split(".")[-1] + content_type = img.content_type + megabyte_limit = 3.0 + if not extension or extension.lower() not in WHITELISTED_IMAGE_TYPES.keys(): + error = "Invalid image types" + return error + elif filesize > megabyte_limit * 1024 * 1024: + error = "Max file size is %sMB" % str(megabyte_limit) + return error + + elif content_type not in WHITELISTED_IMAGE_TYPES.values(): + error = "invalid image content-type" + return error + else: + return True + + +def is_valid_https_url(url): + validate = URLValidator(schemes=["https"]) + try: + validate(url) + return True + except ValidationError: + return False + + +def rebuild_safe_url(url): + parsed_url = urlparse(url) + return urlunparse((parsed_url.scheme, parsed_url.netloc, parsed_url.path, "", "", "")) + + +def get_github_issue_title(github_issue_url): + """Helper function to fetch the title of a GitHub issue.""" + try: + repo_path = "/".join(github_issue_url.split("/")[3:5]) + issue_number = github_issue_url.split("/")[-1] + github_api_url = f"https://api.github.com/repos/{repo_path}/issues/{issue_number}" + response = requests.get(github_api_url) + if response.status_code == 200: + issue_data = response.json() + return issue_data.get("title", "No Title") + return f"Issue #{issue_number}" + except Exception: + return "No Title" diff --git a/website/views.py b/website/views.py index b89e3ad3d..621c54828 100644 --- a/website/views.py +++ b/website/views.py @@ -2,53 +2,35 @@ import io import json import os -import re -import time import urllib.error import urllib.parse import urllib.request import uuid -from collections import deque +from collections import defaultdict from datetime import datetime, timedelta, timezone from decimal import Decimal from pathlib import Path -from urllib.parse import urlparse, urlsplit, urlunparse +from urllib.parse import urlparse, urlunparse import humanize import requests import requests.exceptions import six import stripe -import tweepy - -# from django_cron import CronJobBase, Schedule from allauth.account.models import EmailAddress from allauth.account.signals import user_logged_in, user_signed_up -from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter -from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter -from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter -from allauth.socialaccount.providers.oauth2.client import OAuth2Client from bs4 import BeautifulSoup -from dj_rest_auth.registration.views import SocialConnectView, SocialLoginView from django.conf import settings from django.contrib import messages -from django.contrib.auth import get_user_model, logout -from django.contrib.auth.decorators import login_required -from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.models import User from django.contrib.sites.shortcuts import get_current_site from django.core import serializers from django.core.cache import cache -from django.core.exceptions import ValidationError -from django.core.files import File from django.core.files.base import ContentFile -from django.core.files.storage import default_storage from django.core.mail import send_mail from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator -from django.core.validators import URLValidator -from django.db.models import Count, Prefetch, Q, Sum -from django.db.models.functions import ExtractMonth -from django.db.transaction import atomic +from django.db.models import Prefetch, Q, Sum from django.dispatch import receiver from django.http import ( Http404, @@ -63,29 +45,29 @@ from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string from django.urls import reverse -from django.utils.decorators import method_decorator +from django.utils.dateparse import parse_datetime +from django.utils.html import escape from django.utils.timezone import now from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_GET -from django.views.generic import DetailView, ListView, TemplateView, View -from django.views.generic.edit import CreateView +from dotenv import load_dotenv from PIL import Image, ImageDraw, ImageFont +from requests.auth import HTTPBasicAuth from rest_framework import status from rest_framework.authtoken.models import Token -from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.decorators import api_view from rest_framework.response import Response -from user_agents import parse +from sendgrid import SendGridAPIClient from blt import settings from comments.models import Comment +from website.class_views import IssueBaseCreate from website.models import ( - IP, Bid, ChatBotLog, Company, CompanyAdmin, - ContributorStats, + DailyStatusReport, Domain, Hunt, InviteFriend, @@ -94,47 +76,21 @@ Monitor, Payment, Points, - Project, Subscription, Suggestion, SuggestionVotes, + TimeLog, UserProfile, Wallet, Winner, ) - -from .bot import conversation_chain, is_api_key_valid, load_vector_store -from .forms import ( - CaptchaForm, - GitHubURLForm, - HuntForm, - MonitorForm, - UserDeleteForm, - UserProfileForm, -) - -WHITELISTED_IMAGE_TYPES = { - "jpeg": "image/jpeg", - "jpg": "image/jpeg", - "png": "image/png", -} - -import os - -import requests -from django.contrib.auth.decorators import login_required, user_passes_test -from django.core.cache import cache -from django.shortcuts import redirect, render -from dotenv import load_dotenv -from PIL import Image -from requests.auth import HTTPBasicAuth -from sendgrid import SendGridAPIClient +from website.utils import get_github_issue_title, is_valid_https_url, rebuild_safe_url from .bitcoin_utils import create_bacon_token -from .forms import UserProfileForm +from .bot import conversation_chain, is_api_key_valid, load_vector_store +from .forms import HuntForm, MonitorForm, UserProfileForm from .models import BaconToken, Contribution, Tag, UserProfile -# Load environment variables load_dotenv() @@ -168,7 +124,6 @@ def add_domain_to_company(request): domain.company = company domain.save() messages.success(request, "Organization added successfully") - # back to the domain detail page return redirect("domain", slug=domain.url) else: messages.error(request, "Organization not found in the domain") @@ -177,18 +132,15 @@ def add_domain_to_company(request): domain.company = company domain.save() messages.success(request, "Organization added successfully") - # back to the domain detail page return redirect("domain", slug=domain.url) else: return redirect("index") def check_status(request): - # Check if the status is already cached status = cache.get("service_status") if not status: - # Initialize the status dictionary status = { "bitcoin": False, "bitcoin_block": None, @@ -196,7 +148,6 @@ def check_status(request): "github": False, } - # Check Bitcoin Core Node Status bitcoin_rpc_user = os.getenv("BITCOIN_RPC_USER") bitcoin_rpc_password = os.getenv("BITCOIN_RPC_PASSWORD") bitcoin_rpc_host = os.getenv("BITCOIN_RPC_HOST", "127.0.0.1") @@ -228,7 +179,6 @@ def check_status(request): except Exception as e: print(f"SendGrid Error: {e}") - # Check GitHub Repo Access github_token = os.getenv("GITHUB_ACCESS_TOKEN") if not github_token: @@ -257,10 +207,8 @@ def check_status(request): status["github"] = False print(f"GitHub API Error: {e}") - # Cache the status for 1 minute (60 seconds) cache.set("service_status", status, timeout=60) - # Pass the status to the template return render(request, "status_page.html", {"status": status}) @@ -295,180 +243,76 @@ def distribute_bacon(request, contribution_id): return render(request, "select_contribution.html", {"contributions": contributions}) -def image_validator(img): +def UpdateIssue(request): + if not request.POST.get("issue_pk"): + return HttpResponse("Missing issue ID") + issue = get_object_or_404(Issue, pk=request.POST.get("issue_pk")) try: - filesize = img.file.size + for token in Token.objects.all(): + if request.POST["token"] == token.key: + request.user = User.objects.get(id=token.user_id) + tokenauth = True except: - filesize = img.size - - extension = img.name.split(".")[-1] - content_type = img.content_type - megabyte_limit = 3.0 - if not extension or extension.lower() not in WHITELISTED_IMAGE_TYPES.keys(): - error = "Invalid image types" - return error - elif filesize > megabyte_limit * 1024 * 1024: - error = "Max file size is %sMB" % str(megabyte_limit) - return error - - elif content_type not in WHITELISTED_IMAGE_TYPES.values(): - error = "invalid image content-type" - return error - else: - return True - - -def is_valid_https_url(url): - validate = URLValidator(schemes=["https"]) # Only allow HTTPS URLs - try: - validate(url) - return True - except ValidationError: - return False - - -def rebuild_safe_url(url): - parsed_url = urlparse(url) - # Rebuild the URL with scheme, netloc, and path only - return urlunparse((parsed_url.scheme, parsed_url.netloc, parsed_url.path, "", "", "")) - - -class ProjectDetailView(DetailView): - model = Project + tokenauth = False + if ( + request.method == "POST" + and request.user.is_superuser + or (issue is not None and request.user == issue.user) + ): + if request.POST.get("action") == "close": + issue.status = "closed" + issue.closed_by = request.user + issue.closed_date = datetime.now() + msg_plain = msg_html = render_to_string( + "email/bug_updated.txt", + { + "domain": issue.domain.name, + "name": issue.user.username if issue.user else "Anonymous", + "id": issue.id, + "username": request.user.username, + "action": "closed", + }, + ) + subject = ( + issue.domain.name + + " bug # " + + str(issue.id) + + " closed by " + + request.user.username + ) -class ProjectListView(ListView): - model = Project - context_object_name = "projects" + elif request.POST.get("action") == "open": + issue.status = "open" + issue.closed_by = None + issue.closed_date = None + msg_plain = msg_html = render_to_string( + "email/bug_updated.txt", + { + "domain": issue.domain.name, + "name": issue.domain.email.split("@")[0], + "id": issue.id, + "username": request.user.username, + "action": "opened", + }, + ) + subject = ( + issue.domain.name + + " bug # " + + str(issue.id) + + " opened by " + + request.user.username + ) - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["form"] = GitHubURLForm() - return context + mailer = settings.EMAIL_TO_STRING + email_to = issue.user.email + send_mail(subject, msg_plain, mailer, [email_to], html_message=msg_html) + send_mail(subject, msg_plain, mailer, [issue.domain.email], html_message=msg_html) + issue.save() + return HttpResponse("Updated") - def post(self, request, *args, **kwargs): - form = GitHubURLForm(request.POST) - if form.is_valid(): - github_url = form.cleaned_data["github_url"] - api_url = github_url.replace("github.com", "api.github.com/repos") - response = requests.get(api_url) - if response.status_code == 200: - data = response.json() - project, created = Project.objects.get_or_create( - github_url=github_url, - defaults={ - "name": data["name"], - "slug": data["name"].lower(), - "description": data["description"], - "wiki_url": data["html_url"], - "homepage_url": data.get("homepage", ""), - "logo_url": data["owner"]["avatar_url"], - }, - ) - if created: - messages.success(request, "Project added successfully.") - else: - messages.info(request, "Project already exists.") - else: - messages.error(request, "Failed to fetch project from GitHub.") - return redirect("project_list") - context = self.get_context_data() - context["form"] = form - return self.render_to_response(context) - - -# # @cache_page(60 * 60 * 24) -# def index(request, template="index.html"): -# try: -# domains = random.sample(Domain.objects.all(), 3) -# except: -# domains = None -# try: -# if not EmailAddress.objects.get(email=request.user.email).verified: -# messages.error(request, "Please verify your email address") -# except: -# pass - -# latest_hunts_filter = request.GET.get("latest_hunts", None) - -# bug_count = Issue.objects.all().count() -# user_count = User.objects.all().count() -# hunt_count = Hunt.objects.all().count() -# domain_count = Domain.objects.all().count() - -# captcha_form = CaptchaForm() - -# wallet = None -# if request.user.is_authenticated: -# wallet, created = Wallet.objects.get_or_create(user=request.user) - -# activity_screenshots = {} -# for activity in Issue.objects.all(): -# activity_screenshots[activity] = IssueScreenshot.objects.filter(issue=activity).first() - -# top_companies = ( -# Issue.objects.values("domain__name") -# .annotate(count=Count("domain__name")) -# .order_by("-count")[:10] -# ) -# top_testers = ( -# Issue.objects.values("user__id", "user__username") -# .filter(user__isnull=False) -# .annotate(count=Count("user__username")) -# .order_by("-count")[:10] -# ) - -# if request.user.is_anonymous: -# activities = Issue.objects.exclude(Q(is_hidden=True))[0:10] -# else: -# activities = Issue.objects.exclude(Q(is_hidden=True) & ~Q(user_id=request.user.id))[0:10] - -# top_hunts = Hunt.objects.values( -# "id", -# "name", -# "url", -# "logo", -# "starts_on", -# "starts_on__day", -# "starts_on__month", -# "starts_on__year", -# "end_on", -# "end_on__day", -# "end_on__month", -# "end_on__year", -# ).annotate(total_prize=Sum("huntprize__value")) - -# if latest_hunts_filter is not None: -# top_hunts = top_hunts.filter(result_published=True).order_by("-created")[:3] -# else: -# top_hunts = top_hunts.filter(is_published=True, result_published=False).order_by( -# "-created" -# )[:3] - -# context = { -# "server_url": request.build_absolute_uri("/"), -# "activities": activities, -# "domains": domains, -# "hunts": Hunt.objects.exclude(txn_id__isnull=True)[:4], -# "leaderboard": User.objects.filter( -# points__created__month=datetime.now().month, -# points__created__year=datetime.now().year, -# ) -# .annotate(total_score=Sum("points__score")) -# .order_by("-total_score")[:10], -# "bug_count": bug_count, -# "user_count": user_count, -# "hunt_count": hunt_count, -# "domain_count": domain_count, -# "wallet": wallet, -# "captcha_form": captcha_form, -# "activity_screenshots": activity_screenshots, -# "top_companies": top_companies, -# "top_testers": top_testers, -# "top_hunts": top_hunts, -# "ended_hunts": False if latest_hunts_filter is None else True, -# } -# return render(request, template, context) + elif request.method == "POST": + return HttpResponse("invalid") def newhome(request, template="new_home.html"): @@ -480,19 +324,16 @@ def newhome(request, template="new_home.html"): else: messages.error(request, "No email associated with your account. Please add an email.") - # Fetch and paginate issues issues_queryset = Issue.objects.exclude(Q(is_hidden=True) & ~Q(user_id=request.user.id)) paginator = Paginator(issues_queryset, 15) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) - # Prefetch related screenshots only for issues on the current page issues_with_screenshots = page_obj.object_list.prefetch_related( Prefetch("screenshots", queryset=IssueScreenshot.objects.all()) ) bugs_screenshots = {issue: issue.screenshots.all()[:3] for issue in issues_with_screenshots} - # Filter leaderboard for current month and year current_time = now() leaderboard = User.objects.filter( points__created__month=current_time.month, points__created__year=current_time.year @@ -533,7 +374,6 @@ def github_callback(request): ALLOWED_HOSTS = ["github.com"] params = urllib.parse.urlencode(request.GET) url = f"{settings.CALLBACK_URL_FOR_GITHUB}?{params}" - return safe_redirect(url, ALLOWED_HOSTS) @@ -541,7 +381,6 @@ def google_callback(request): ALLOWED_HOSTS = ["accounts.google.com"] params = urllib.parse.urlencode(request.GET) url = f"{settings.CALLBACK_URL_FOR_GOOGLE}?{params}" - return safe_redirect(url, ALLOWED_HOSTS) @@ -549,76 +388,9 @@ def facebook_callback(request): ALLOWED_HOSTS = ["www.facebook.com"] params = urllib.parse.urlencode(request.GET) url = f"{settings.CALLBACK_URL_FOR_FACEBOOK}?{params}" - return safe_redirect(url, ALLOWED_HOSTS) -class FacebookLogin(SocialLoginView): - adapter_class = FacebookOAuth2Adapter - client_class = OAuth2Client - - @property - def callback_url(self): - # use the same callback url as defined in your Facebook app, this url - # must be absolute: - return self.request.build_absolute_uri(reverse("facebook_callback")) - - -class GoogleLogin(SocialLoginView): - adapter_class = GoogleOAuth2Adapter - client_class = OAuth2Client - - @property - def callback_url(self): - # use the same callback url as defined in your Google app, this url - # must be absolute: - return self.request.build_absolute_uri(reverse("google_callback")) - - -class GithubLogin(SocialLoginView): - adapter_class = GitHubOAuth2Adapter - client_class = OAuth2Client - - @property - def callback_url(self): - # use the same callback url as defined in your GitHub app, this url - # must be absolute: - return self.request.build_absolute_uri(reverse("github_callback")) - - -class FacebookConnect(SocialConnectView): - adapter_class = FacebookOAuth2Adapter - client_class = OAuth2Client - - @property - def callback_url(self): - # use the same callback url as defined in your Facebook app, this url - # must be absolute: - return self.request.build_absolute_uri(reverse("facebook_callback")) - - -class GithubConnect(SocialConnectView): - adapter_class = GitHubOAuth2Adapter - client_class = OAuth2Client - - @property - def callback_url(self): - # use the same callback url as defined in your GitHub app, this url - # must be absolute: - return self.request.build_absolute_uri(reverse("github_callback")) - - -class GoogleConnect(SocialConnectView): - adapter_class = GoogleOAuth2Adapter - client_class = OAuth2Client - - @property - def callback_url(self): - # use the same callback url as defined in your Google app, this url - # must be absolute: - return self.request.build_absolute_uri(reverse("google_callback")) - - @login_required(login_url="/accounts/login") def company_dashboard(request, template="index_company.html"): try: @@ -713,1530 +485,140 @@ def find_key(request, token): raise Http404("Token or key does not exist") -class UserDeleteView(LoginRequiredMixin, View): - """ - Deletes the currently signed-in user and all associated data. - """ +def profile(request): + try: + return redirect("/profile/" + request.user.username) + except Exception: + return redirect("/") - def get(self, request, *args, **kwargs): - form = UserDeleteForm() - return render(request, "user_deletion.html", {"form": form}) - def post(self, request, *args, **kwargs): - form = UserDeleteForm(request.POST) - if form.is_valid(): - user = request.user - logout(request) - user.delete() - messages.success(request, "Account successfully deleted") - return redirect(reverse("index")) - return render(request, "user_deletion.html", {"form": form}) - - -class IssueBaseCreate(object): - def form_valid(self, form): - print( - "processing form_valid IssueBaseCreate for ip address: ", - get_client_ip(self.request), - ) - score = 3 - obj = form.save(commit=False) - obj.user = self.request.user - domain, created = Domain.objects.get_or_create( - name=obj.domain_name.replace("www.", ""), - defaults={"url": "http://" + obj.domain_name.replace("www.", "")}, - ) - obj.domain = domain - if self.request.POST.get("screenshot-hash"): - filename = self.request.POST.get("screenshot-hash") - extension = filename.split(".")[-1] - self.request.POST["screenshot-hash"] = ( - filename[:99] + str(uuid.uuid4()) + "." + extension - ) +def delete_issue(request, id): + try: + # TODO: Refactor this for a direct query instead of looping through all tokens + for token in Token.objects.all(): + if request.POST["token"] == token.key: + request.user = User.objects.get(id=token.user_id) + tokenauth = True + except: + tokenauth = False + issue = Issue.objects.get(id=id) + if request.user.is_superuser or request.user == issue.user or tokenauth: + screenshots = issue.screenshots.all() + for screenshot in screenshots: + screenshot.delete() + if request.user.is_superuser or request.user == issue.user: + issue.delete() + messages.success(request, "Issue deleted") + if tokenauth: + return JsonResponse("Deleted", safe=False) + else: + return redirect("/") - reopen = default_storage.open( - "uploads\/" + self.request.POST.get("screenshot-hash") + ".png", "rb" - ) - django_file = File(reopen) - obj.screenshot.save( - self.request.POST.get("screenshot-hash") + ".png", - django_file, - save=True, - ) - obj.user_agent = self.request.META.get("HTTP_USER_AGENT") - obj.save() - p = Points.objects.create(user=self.request.user, issue=obj, score=score) +def remove_user_from_issue(request, id): + tokenauth = False + try: + for token in Token.objects.all(): + if request.POST["token"] == token.key: + request.user = User.objects.get(id=token.user_id) + tokenauth = True + except: + pass - def process_issue(self, user, obj, created, domain, tokenauth=False, score=3): - print("processing process_issue for ip address: ", get_client_ip(self.request)) - p = Points.objects.create(user=user, issue=obj, score=score) - messages.success(self.request, "Bug added ! +" + str(score)) - # tweet feature - try: - auth = tweepy.Client( - settings.BEARER_TOKEN, - settings.APP_KEY, - settings.APP_KEY_SECRET, - settings.ACCESS_TOKEN, - settings.ACCESS_TOKEN_SECRET, - ) + issue = Issue.objects.get(id=id) + if request.user.is_superuser or request.user == issue.user: + issue.remove_user() + messages.success(request, "User removed from the issue") + if tokenauth: + return JsonResponse("User removed from the issue", safe=False) + else: + return safe_redirect(request) + else: + messages.error(request, "Permission denied") + return safe_redirect(request) - blt_url = "https://%s/issue/%d" % ( - settings.DOMAIN_NAME, - obj.id, - ) - domain_name = domain.get_name - twitter_account = ( - "@" + domain.get_or_set_x_url(domain_name) + " " - if domain.get_or_set_x_url(domain_name) - else "" - ) - issue_title = obj.description + " " if not obj.is_hidden else "" +import requests +from bs4 import BeautifulSoup - message = "%sAn Issue %shas been reported on %s by %s on %s.\n Have look here %s" % ( - twitter_account, - issue_title, - domain_name, - user.username, - settings.PROJECT_NAME, - blt_url, - ) +from .models import ( + BaconToken, + Bid, + ChatBotLog, + Company, + CompanyAdmin, + Contribution, + Domain, + Hunt, + InviteFriend, + Issue, + IssueScreenshot, + Monitor, + Payment, + Points, + Subscription, + Suggestion, + SuggestionVotes, + User, + UserProfile, + Wallet, + Winner, +) - auth.create_tweet(text=message) - except ( - TypeError, - tweepy.errors.HTTPException, - tweepy.errors.TweepyException, - ) as e: - print(e) +def search(request, template="search.html"): + query = request.GET.get("query") + stype = request.GET.get("type") + context = None + if query is None: + return render(request, template) + query = query.strip() + if query[:6] == "issue:": + stype = "issue" + query = query[6:] + elif query[:7] == "domain:": + stype = "domain" + query = query[7:] + elif query[:5] == "user:": + stype = "user" + query = query[5:] + elif query[:6] == "label:": + stype = "label" + query = query[6:] + if stype == "issue" or stype is None: + context = { + "query": query, + "type": stype, + "issues": Issue.objects.filter(Q(description__icontains=query), hunt=None).exclude( + Q(is_hidden=True) & ~Q(user_id=request.user.id) + )[0:20], + } + elif stype == "domain": + context = { + "query": query, + "type": stype, + "domains": Domain.objects.filter(Q(url__icontains=query), hunt=None)[0:20], + } + elif stype == "user": + context = { + "query": query, + "type": stype, + "users": UserProfile.objects.filter(Q(user__username__icontains=query)) + .annotate(total_score=Sum("user__points__score")) + .order_by("-total_score")[0:20], + } + elif stype == "label": + context = { + "query": query, + "type": stype, + "issues": Issue.objects.filter(Q(label__icontains=query), hunt=None).exclude( + Q(is_hidden=True) & ~Q(user_id=request.user.id) + )[0:20], + } - if created: - try: - email_to = get_email_from_domain(domain) - except: - email_to = "support@" + domain.name - - domain.email = email_to - domain.save() - - name = email_to.split("@")[0] - - msg_plain = render_to_string( - "email/domain_added.txt", {"domain": domain.name, "name": name} - ) - msg_html = render_to_string( - "email/domain_added.txt", {"domain": domain.name, "name": name} - ) - - send_mail( - domain.name + " added to " + settings.PROJECT_NAME, - msg_plain, - settings.EMAIL_TO_STRING, - [email_to], - html_message=msg_html, - ) - - else: - email_to = domain.email - try: - name = email_to.split("@")[0] - except: - email_to = "support@" + domain.name - name = "support" - domain.email = email_to - domain.save() - if not tokenauth: - msg_plain = render_to_string( - "email/bug_added.txt", - { - "domain": domain.name, - "name": name, - "username": self.request.user, - "id": obj.id, - "description": obj.description, - "label": obj.get_label_display, - }, - ) - msg_html = render_to_string( - "email/bug_added.txt", - { - "domain": domain.name, - "name": name, - "username": self.request.user, - "id": obj.id, - "description": obj.description, - "label": obj.get_label_display, - }, - ) - else: - msg_plain = render_to_string( - "email/bug_added.txt", - { - "domain": domain.name, - "name": name, - "username": user, - "id": obj.id, - "description": obj.description, - "label": obj.get_label_display, - }, - ) - msg_html = render_to_string( - "email/bug_added.txt", - { - "domain": domain.name, - "name": name, - "username": user, - "id": obj.id, - "description": obj.description, - "label": obj.get_label_display, - }, - ) - send_mail( - "Bug found on " + domain.name, - msg_plain, - settings.EMAIL_TO_STRING, - [email_to], - html_message=msg_html, - ) - return HttpResponseRedirect("/") - - -def get_client_ip(request): - """Extract the client's IP address from the request.""" - x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") - if x_forwarded_for: - ip = x_forwarded_for.split(",")[0] - else: - ip = request.META.get("REMOTE_ADDR") - return ip - - -class IssueCreate(IssueBaseCreate, CreateView): - model = Issue - fields = ["url", "description", "domain", "label", "markdown_description", "cve_id"] - template_name = "report.html" - - def get_initial(self): - print("processing post for ip address: ", get_client_ip(self.request)) - try: - json_data = json.loads(self.request.body) - if not self.request.GET._mutable: - self.request.POST._mutable = True - self.request.POST["url"] = json_data["url"] - self.request.POST["description"] = json_data["description"] - self.request.POST["markdown_description"] = json_data["markdown_description"] - self.request.POST["file"] = json_data["file"] - self.request.POST["label"] = json_data["label"] - self.request.POST["token"] = json_data["token"] - self.request.POST["type"] = json_data["type"] - self.request.POST["cve_id"] = json_data["cve_id"] - self.request.POST["cve_score"] = json_data["cve_score"] - - if self.request.POST.get("file"): - if isinstance(self.request.POST.get("file"), six.string_types): - import imghdr - - # Check if the base64 string is in the "data:" format - data = ( - "data:image/" - + self.request.POST.get("type") - + ";base64," - + self.request.POST.get("file") - ) - data = data.replace(" ", "") - data += "=" * ((4 - len(data) % 4) % 4) - if "data:" in data and ";base64," in data: - # Break out the header from the base64 content - header, data = data.split(";base64,") - - # Try to decode the file. Return validation error if it fails. - try: - decoded_file = base64.b64decode(data) - except TypeError: - TypeError("invalid_image") - - # Generate file name: - file_name = str(uuid.uuid4())[:12] # 12 characters are more than enough. - # Get the file name extension: - extension = imghdr.what(file_name, decoded_file) - extension = "jpg" if extension == "jpeg" else extension - file_extension = extension - - complete_file_name = "%s.%s" % ( - file_name, - file_extension, - ) - - self.request.FILES["screenshot"] = ContentFile( - decoded_file, name=complete_file_name - ) - except: - tokenauth = False - initial = super(IssueCreate, self).get_initial() - if self.request.POST.get("screenshot-hash"): - initial["screenshot"] = "uploads\/" + self.request.POST.get("screenshot-hash") + ".png" - return initial - - # def get(self, request, *args, **kwargs): - # print("processing get for ip address: ", get_client_ip(request)) - - # captcha_form = CaptchaForm() - # return render( - # request, - # self.template_name, - # {"form": self.get_form(), "captcha_form": captcha_form}, - # ) - - def post(self, request, *args, **kwargs): - print("processing post for ip address: ", get_client_ip(request)) - # resolve domain - url = request.POST.get("url").replace("www.", "").replace("https://", "") - - request.POST._mutable = True - request.POST.update(url=url) # only domain.com will be stored in db - request.POST._mutable = False - - # disable domain search on testing - if not settings.IS_TEST: - try: - if settings.DOMAIN_NAME in url: - print("Web site exists") - - # skip domain validation check if bugreport server down - elif request.POST["label"] == "7": - pass - - else: - full_url = "https://" + url - if is_valid_https_url(full_url): - safe_url = rebuild_safe_url(full_url) - try: - response = requests.get(safe_url, timeout=5) - if response.status_code == 200: - print("Web site exists") - else: - raise Exception - except Exception: - raise Exception - else: - raise Exception - except: - # TODO: it could be that the site is down so we can consider logging this differently - messages.error(request, "Domain does not exist") - - captcha_form = CaptchaForm(request.POST) - return render( - self.request, - "report.html", - {"form": self.get_form(), "captcha_form": captcha_form}, - ) - # if not request.FILES.get("screenshots"): - # messages.error(request, "Screenshot is required") - # captcha_form = CaptchaForm(request.POST) - # return render( - # self.request, - # "report.html", - # {"form": self.get_form(), "captcha_form": captcha_form}, - # ) - - screenshot = request.FILES.get("screenshots") - if not screenshot: - messages.error(request, "Screenshot is required") - captcha_form = CaptchaForm(request.POST) - return render( - request, - "report.html", - {"form": self.get_form(), "captcha_form": captcha_form}, - ) - - try: - # Attempt to open the image to validate if it's a correct format - img = Image.open(screenshot) - img.verify() # Verify that it is, in fact, an image - except (IOError, ValueError): - messages.error(request, "Invalid image file.") - captcha_form = CaptchaForm(request.POST) - return render( - request, - "report.html", - {"form": self.get_form(), "captcha_form": captcha_form}, - ) - - return super().post(request, *args, **kwargs) - - def form_valid(self, form): - print( - "processing form_valid in IssueCreate for ip address: ", - get_client_ip(self.request), - ) - reporter_ip = get_client_ip(self.request) - form.instance.reporter_ip_address = reporter_ip - - # implement rate limit - limit = 50 if self.request.user.is_authenticated else 30 - today = now().date() - recent_issues_count = Issue.objects.filter( - reporter_ip_address=reporter_ip, created__date=today - ).count() - - if recent_issues_count >= limit: - messages.error(self.request, "You have reached your issue creation limit for today.") - return render(self.request, "report.html", {"form": self.get_form()}) - form.instance.reporter_ip_address = reporter_ip - - @atomic - def create_issue(self, form): - tokenauth = False - obj = form.save(commit=False) - if self.request.user.is_authenticated: - obj.user = self.request.user - if not self.request.user.is_authenticated: - for token in Token.objects.all(): - if self.request.POST.get("token") == token.key: - obj.user = User.objects.get(id=token.user_id) - tokenauth = True - - captcha_form = CaptchaForm(self.request.POST) - if not captcha_form.is_valid() and not settings.TESTING: - messages.error(self.request, "Invalid Captcha!") - - return render( - self.request, - "report.html", - {"form": self.get_form(), "captcha_form": captcha_form}, - ) - parsed_url = urlparse(obj.url) - clean_domain = parsed_url.netloc - domain = Domain.objects.filter(url=clean_domain).first() - - domain_exists = False if domain is None else True - - if not domain_exists: - domain = Domain.objects.filter(name=clean_domain).first() - if domain is None: - domain = Domain.objects.create(name=clean_domain, url=clean_domain) - domain.save() - - hunt = self.request.POST.get("hunt", None) - if hunt is not None and hunt != "None": - hunt = Hunt.objects.filter(id=hunt).first() - obj.hunt = hunt - - obj.domain = domain - # obj.is_hidden = bool(self.request.POST.get("private", False)) - obj.cve_score = obj.get_cve_score() - obj.save() - - if not domain_exists and (self.request.user.is_authenticated or tokenauth): - p = Points.objects.create(user=self.request.user, domain=domain, score=1) - messages.success(self.request, "Domain added! + 1") - - if self.request.POST.get("screenshot-hash"): - reopen = default_storage.open( - "uploads\/" + self.request.POST.get("screenshot-hash") + ".png", - "rb", - ) - django_file = File(reopen) - obj.screenshot.save( - self.request.POST.get("screenshot-hash") + ".png", - django_file, - save=True, - ) - obj.user_agent = self.request.META.get("HTTP_USER_AGENT") - - if len(self.request.FILES.getlist("screenshots")) > 5: - messages.error(self.request, "Max limit of 5 images!") - obj.delete() - return render( - self.request, - "report.html", - {"form": self.get_form(), "captcha_form": captcha_form}, - ) - for screenshot in self.request.FILES.getlist("screenshots"): - img_valid = image_validator(screenshot) - if img_valid is True: - filename = screenshot.name - extension = filename.split(".")[-1] - screenshot.name = (filename[:10] + str(uuid.uuid4()))[:40] + "." + extension - default_storage.save(f"screenshots/{screenshot.name}", screenshot) - IssueScreenshot.objects.create( - image=f"screenshots/{screenshot.name}", issue=obj - ) - else: - messages.error(self.request, img_valid) - return render( - self.request, - "report.html", - {"form": self.get_form(), "captcha_form": captcha_form}, - ) - - obj_screenshots = IssueScreenshot.objects.filter(issue_id=obj.id) - screenshot_text = "" - for screenshot in obj_screenshots: - screenshot_text += "![0](" + screenshot.image.url + ") " - - team_members_id = [ - member["id"] - for member in User.objects.values("id").filter( - email__in=self.request.POST.getlist("team_members") - ) - ] + [self.request.user.id] - for member_id in team_members_id: - if member_id is None: - team_members_id.remove(member_id) # remove None values if user not exists - obj.team_members.set(team_members_id) - - obj.save() - - if self.request.user.is_authenticated: - total_issues = Issue.objects.filter(user=self.request.user).count() - user_prof = UserProfile.objects.get(user=self.request.user) - if total_issues <= 10: - user_prof.title = 1 - elif total_issues <= 50: - user_prof.title = 2 - elif total_issues <= 200: - user_prof.title = 3 - else: - user_prof.title = 4 - - user_prof.save() - - if tokenauth: - total_issues = Issue.objects.filter(user=User.objects.get(id=token.user_id)).count() - user_prof = UserProfile.objects.get(user=User.objects.get(id=token.user_id)) - if total_issues <= 10: - user_prof.title = 1 - elif total_issues <= 50: - user_prof.title = 2 - elif total_issues <= 200: - user_prof.title = 3 - else: - user_prof.title = 4 - - user_prof.save() - - redirect_url = "/report" - - if domain.github and os.environ.get("GITHUB_ACCESS_TOKEN"): - import json - - import requests - from giturlparse import parse - - github_url = domain.github.replace("https", "git").replace("http", "git") + ".git" - p = parse(github_url) - - url = "https://api.github.com/repos/%s/%s/issues" % (p.owner, p.repo) - - if not obj.user: - the_user = "Anonymous" - else: - the_user = obj.user - issue = { - "title": obj.description, - "body": obj.markdown_description - + "\n\n" - + screenshot_text - + "https://" - + settings.FQDN - + "/issue/" - + str(obj.id) - + " found by " - + str(the_user) - + " at url: " - + obj.url, - "labels": ["bug", settings.PROJECT_NAME_LOWER], - } - r = requests.post( - url, - json.dumps(issue), - headers={"Authorization": "token " + os.environ.get("GITHUB_ACCESS_TOKEN")}, - ) - response = r.json() - try: - obj.github_url = response["html_url"] - except Exception as e: - send_mail( - "Error in github issue creation for " - + str(domain.name) - + ", check your github settings", - "Error in github issue creation, check your github settings\n" - + " your current settings are: " - + str(domain.github) - + " and the error is: " - + str(e), - settings.EMAIL_TO_STRING, - [domain.email], - fail_silently=True, - ) - pass - obj.save() - - if not (self.request.user.is_authenticated or tokenauth): - self.request.session["issue"] = obj.id - self.request.session["created"] = domain_exists - self.request.session["domain"] = domain.id - messages.success(self.request, "Bug added!") - return HttpResponseRedirect("/") - - if tokenauth: - self.process_issue( - User.objects.get(id=token.user_id), obj, domain_exists, domain, True - ) - return JsonResponse("Created", safe=False) - else: - self.process_issue(self.request.user, obj, domain_exists, domain) - return HttpResponseRedirect("/") - - return create_issue(self, form) - - def get_context_data(self, **kwargs): - # if self.request is a get, clear out the form data - if self.request.method == "GET": - self.request.POST = {} - self.request.GET = {} - - print("processing get_context_data for ip address: ", get_client_ip(self.request)) - context = super(IssueCreate, self).get_context_data(**kwargs) - context["activities"] = Issue.objects.exclude( - Q(is_hidden=True) & ~Q(user_id=self.request.user.id) - )[0:10] - context["captcha_form"] = CaptchaForm() - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - context["leaderboard"] = ( - User.objects.filter( - points__created__month=datetime.now().month, - points__created__year=datetime.now().year, - ) - .annotate(total_score=Sum("points__score")) - .order_by("-total_score")[:10], - ) - - # automatically add specified hunt to dropdown of Bugreport - report_on_hunt = self.request.GET.get("hunt", None) - if report_on_hunt: - context["hunts"] = Hunt.objects.values("id", "name").filter( - id=report_on_hunt, is_published=True, result_published=False - ) - context["report_on_hunt"] = True - else: - context["hunts"] = Hunt.objects.values("id", "name").filter( - is_published=True, result_published=False - ) - context["report_on_hunt"] = False - - context["top_domains"] = ( - Issue.objects.values("domain__name") - .annotate(count=Count("domain__name")) - .order_by("-count")[:30] - ) - - return context - - -class UploadCreate(View): - template_name = "index.html" - - @method_decorator(csrf_exempt) - def dispatch(self, request, *args, **kwargs): - return super(UploadCreate, self).dispatch(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - data = request.FILES.get("image") - result = default_storage.save( - "uploads\/" + self.kwargs["hash"] + ".png", ContentFile(data.read()) - ) - return JsonResponse({"status": result}) - - -class InviteCreate(TemplateView): - template_name = "invite.html" - - def post(self, request, *args, **kwargs): - email = request.POST.get("email") - exists = False - domain = None - if email: - domain = email.split("@")[-1] - try: - full_url_domain = "https://" + domain + "/favicon.ico" - if is_valid_https_url(full_url_domain): - safe_url = rebuild_safe_url(full_url_domain) - response = requests.get(safe_url, timeout=5) - if response.status_code == 200: - exists = "exists" - except: - pass - context = { - "exists": exists, - "domain": domain, - "email": email, - } - return render(request, "invite.html", context) - - -def profile(request): - try: - return redirect("/profile/" + request.user.username) - except Exception: - return redirect("/") - - -class UserProfileDetailView(DetailView): - model = get_user_model() - slug_field = "username" - template_name = "profile.html" - - def get(self, request, *args, **kwargs): - try: - self.object = self.get_object() - except Http404: - messages.error(self.request, "That user was not found.") - return redirect("/") - return super(UserProfileDetailView, self).get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - user = self.object - context = super(UserProfileDetailView, self).get_context_data(**kwargs) - context["my_score"] = list( - Points.objects.filter(user=self.object).aggregate(total_score=Sum("score")).values() - )[0] - context["websites"] = ( - Domain.objects.filter(issue__user=self.object) - .annotate(total=Count("issue")) - .order_by("-total") - ) - context["activities"] = Issue.objects.filter(user=self.object, hunt=None).exclude( - Q(is_hidden=True) & ~Q(user_id=self.request.user.id) - )[0:3] - context["activity_screenshots"] = {} - for activity in context["activities"]: - context["activity_screenshots"][activity] = IssueScreenshot.objects.filter( - issue=activity.pk - ).first() - context["profile_form"] = UserProfileForm() - context["total_open"] = Issue.objects.filter(user=self.object, status="open").count() - context["total_closed"] = Issue.objects.filter(user=self.object, status="closed").count() - context["current_month"] = datetime.now().month - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - context["graph"] = ( - Issue.objects.filter(user=self.object) - .filter( - created__month__gte=(datetime.now().month - 6), - created__month__lte=datetime.now().month, - ) - .annotate(month=ExtractMonth("created")) - .values("month") - .annotate(c=Count("id")) - .order_by() - ) - context["total_bugs"] = Issue.objects.filter(user=self.object, hunt=None).count() - for i in range(0, 7): - context["bug_type_" + str(i)] = Issue.objects.filter( - user=self.object, hunt=None, label=str(i) - ) - - arr = [] - allFollowers = user.userprofile.follower.all() - for userprofile in allFollowers: - arr.append(User.objects.get(username=str(userprofile.user))) - context["followers"] = arr - - arr = [] - allFollowing = user.userprofile.follows.all() - for userprofile in allFollowing: - arr.append(User.objects.get(username=str(userprofile.user))) - context["following"] = arr - - context["followers_list"] = [ - str(prof.user.email) for prof in user.userprofile.follower.all() - ] - context["bookmarks"] = user.userprofile.issue_saved.all() - # tags - context["user_related_tags"] = ( - UserProfile.objects.filter(user=self.object).first().tags.all() - ) - context["issues_hidden"] = "checked" if user.userprofile.issues_hidden else "!checked" - return context - - @method_decorator(login_required) - def post(self, request, *args, **kwargs): - form = UserProfileForm(request.POST, request.FILES, instance=request.user.userprofile) - if request.FILES.get("user_avatar") and form.is_valid(): - form.save() - else: - hide = True if request.POST.get("issues_hidden") == "on" else False - user_issues = Issue.objects.filter(user=request.user) - user_issues.update(is_hidden=hide) - request.user.userprofile.issues_hidden = hide - request.user.userprofile.save() - return redirect(reverse("profile", kwargs={"slug": kwargs.get("slug")})) - - -class UserProfileDetailsView(DetailView): - model = get_user_model() - slug_field = "username" - template_name = "dashboard_profile.html" - - def get(self, request, *args, **kwargs): - try: - if request.user.is_authenticated: - self.object = self.get_object() - else: - return redirect("/accounts/login") - except Http404: - messages.error(self.request, "That user was not found.") - return redirect("/") - return super(UserProfileDetailsView, self).get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - user = self.object - context = super(UserProfileDetailsView, self).get_context_data(**kwargs) - context["my_score"] = list( - Points.objects.filter(user=self.object).aggregate(total_score=Sum("score")).values() - )[0] - context["websites"] = ( - Domain.objects.filter(issue__user=self.object) - .annotate(total=Count("issue")) - .order_by("-total") - ) - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - context["activities"] = Issue.objects.filter(user=self.object, hunt=None).exclude( - Q(is_hidden=True) & ~Q(user_id=self.request.user.id) - )[0:10] - context["profile_form"] = UserProfileForm() - context["total_open"] = Issue.objects.filter(user=self.object, status="open").count() - context["user_details"] = UserProfile.objects.get(user=self.object) - context["total_closed"] = Issue.objects.filter(user=self.object, status="closed").count() - context["current_month"] = datetime.now().month - context["graph"] = ( - Issue.objects.filter(user=self.object, hunt=None) - .filter( - created__month__gte=(datetime.now().month - 6), - created__month__lte=datetime.now().month, - ) - .annotate(month=ExtractMonth("created")) - .values("month") - .annotate(c=Count("id")) - .order_by() - ) - context["total_bugs"] = Issue.objects.filter(user=self.object).count() - for i in range(0, 7): - context["bug_type_" + str(i)] = Issue.objects.filter( - user=self.object, hunt=None, label=str(i) - ).exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) - - arr = [] - allFollowers = user.userprofile.follower.all() - for userprofile in allFollowers: - arr.append(User.objects.get(username=str(userprofile.user))) - context["followers"] = arr - - arr = [] - allFollowing = user.userprofile.follows.all() - for userprofile in allFollowing: - arr.append(User.objects.get(username=str(userprofile.user))) - context["following"] = arr - - context["followers_list"] = [ - str(prof.user.email) for prof in user.userprofile.follower.all() - ] - context["bookmarks"] = user.userprofile.issue_saved.all() - return context - - @method_decorator(login_required) - def post(self, request, *args, **kwargs): - form = UserProfileForm(request.POST, request.FILES, instance=request.user.userprofile) - if form.is_valid(): - form.save() - return redirect(reverse("profile", kwargs={"slug": kwargs.get("slug")})) - - -def delete_issue(request, id): - try: - # TODO: Refactor this for a direct query instead of looping through all tokens - for token in Token.objects.all(): - if request.POST["token"] == token.key: - request.user = User.objects.get(id=token.user_id) - tokenauth = True - except: - tokenauth = False - issue = Issue.objects.get(id=id) - if request.user.is_superuser or request.user == issue.user or tokenauth: - screenshots = issue.screenshots.all() - for screenshot in screenshots: - screenshot.delete() - if request.user.is_superuser or request.user == issue.user: - issue.delete() - messages.success(request, "Issue deleted") - if tokenauth: - return JsonResponse("Deleted", safe=False) - else: - return redirect("/") - - -def remove_user_from_issue(request, id): - tokenauth = False - try: - for token in Token.objects.all(): - if request.POST["token"] == token.key: - request.user = User.objects.get(id=token.user_id) - tokenauth = True - except: - pass - - issue = Issue.objects.get(id=id) - if request.user.is_superuser or request.user == issue.user: - issue.remove_user() - messages.success(request, "User removed from the issue") - if tokenauth: - return JsonResponse("User removed from the issue", safe=False) - else: - return safe_redirect(request) - else: - messages.error(request, "Permission denied") - return safe_redirect(request) - - -class DomainDetailView(ListView): - template_name = "domain.html" - model = Issue - - def get_context_data(self, *args, **kwargs): - context = super(DomainDetailView, self).get_context_data(*args, **kwargs) - # remove any arguments from the slug - self.kwargs["slug"] = self.kwargs["slug"].split("?")[0] - domain = get_object_or_404(Domain, name=self.kwargs["slug"]) - context["domain"] = domain - - parsed_url = urlparse("http://" + self.kwargs["slug"]) - - open_issue = ( - Issue.objects.filter(domain__name__contains=self.kwargs["slug"]) - .filter(status="open", hunt=None) - .exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) - ) - close_issue = ( - Issue.objects.filter(domain__name__contains=self.kwargs["slug"]) - .filter(status="closed", hunt=None) - .exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) - ) - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - - context["name"] = parsed_url.netloc.split(".")[-2:][0].title() - - paginator = Paginator(open_issue, 10) - page = self.request.GET.get("open") - try: - openissue_paginated = paginator.page(page) - except PageNotAnInteger: - openissue_paginated = paginator.page(1) - except EmptyPage: - openissue_paginated = paginator.page(paginator.num_pages) - - paginator = Paginator(close_issue, 10) - page = self.request.GET.get("close") - try: - closeissue_paginated = paginator.page(page) - except PageNotAnInteger: - closeissue_paginated = paginator.page(1) - except EmptyPage: - closeissue_paginated = paginator.page(paginator.num_pages) - - context["opened_net"] = open_issue - context["opened"] = openissue_paginated - context["closed_net"] = close_issue - context["closed"] = closeissue_paginated - context["leaderboard"] = ( - User.objects.filter(issue__url__contains=self.kwargs["slug"]) - .annotate(total=Count("issue")) - .order_by("-total") - ) - context["current_month"] = datetime.now().month - context["domain_graph"] = ( - Issue.objects.filter(domain=context["domain"], hunt=None) - .filter( - created__month__gte=(datetime.now().month - 6), - created__month__lte=datetime.now().month, - ) - .annotate(month=ExtractMonth("created")) - .values("month") - .annotate(c=Count("id")) - .order_by() - ) - context["pie_chart"] = ( - Issue.objects.filter(domain=context["domain"], hunt=None) - .values("label") - .annotate(c=Count("label")) - .order_by() - ) - context["twitter_url"] = "https://twitter.com/%s" % domain.get_or_set_x_url(domain.get_name) - - return context - - -import requests -from bs4 import BeautifulSoup -from django.db.models.functions import ExtractMonth -from django.views.generic import TemplateView - -from .models import ( - IP, - BaconToken, - Bid, - ChatBotLog, - Company, - CompanyAdmin, - Contribution, - Contributor, - ContributorStats, - Domain, - Hunt, - HuntPrize, - InviteFriend, - Issue, - IssueScreenshot, - Monitor, - Payment, - Points, - Project, - Subscription, - Suggestion, - SuggestionVotes, - Transaction, - User, - UserProfile, - Wallet, - Winner, -) - - -class StatsDetailView(TemplateView): - template_name = "stats.html" - - def get_context_data(self, *args, **kwargs): - context = super(StatsDetailView, self).get_context_data(*args, **kwargs) - - response = requests.get(settings.EXTENSION_URL) - soup = BeautifulSoup(response.text, "html.parser") - - stats = "" - for item in soup.findAll("span", {"class": "e-f-ih"}): - stats = item.attrs["title"] - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - context["extension_users"] = stats.replace(" users", "") or "0" - - # Prepare stats data for display - context["stats"] = [ - {"label": "Bugs", "count": Issue.objects.all().count(), "icon": "fas fa-bug"}, - {"label": "Users", "count": User.objects.all().count(), "icon": "fas fa-users"}, - {"label": "Hunts", "count": Hunt.objects.all().count(), "icon": "fas fa-crosshairs"}, - {"label": "Domains", "count": Domain.objects.all().count(), "icon": "fas fa-globe"}, - { - "label": "Extension Users", - "count": int(context["extension_users"].replace(",", "")), - "icon": "fas fa-puzzle-piece", - }, - { - "label": "Subscriptions", - "count": Subscription.objects.all().count(), - "icon": "fas fa-envelope", - }, - { - "label": "Companies", - "count": Company.objects.all().count(), - "icon": "fas fa-building", - }, - { - "label": "Hunt Prizes", - "count": HuntPrize.objects.all().count(), - "icon": "fas fa-gift", - }, - { - "label": "Screenshots", - "count": IssueScreenshot.objects.all().count(), - "icon": "fas fa-camera", - }, - {"label": "Winners", "count": Winner.objects.all().count(), "icon": "fas fa-trophy"}, - {"label": "Points", "count": Points.objects.all().count(), "icon": "fas fa-star"}, - { - "label": "Invitations", - "count": InviteFriend.objects.all().count(), - "icon": "fas fa-envelope-open", - }, - { - "label": "User Profiles", - "count": UserProfile.objects.all().count(), - "icon": "fas fa-id-badge", - }, - {"label": "IPs", "count": IP.objects.all().count(), "icon": "fas fa-network-wired"}, - { - "label": "Company Admins", - "count": CompanyAdmin.objects.all().count(), - "icon": "fas fa-user-tie", - }, - { - "label": "Transactions", - "count": Transaction.objects.all().count(), - "icon": "fas fa-exchange-alt", - }, - { - "label": "Payments", - "count": Payment.objects.all().count(), - "icon": "fas fa-credit-card", - }, - { - "label": "Contributor Stats", - "count": ContributorStats.objects.all().count(), - "icon": "fas fa-chart-bar", - }, - {"label": "Monitors", "count": Monitor.objects.all().count(), "icon": "fas fa-desktop"}, - {"label": "Bids", "count": Bid.objects.all().count(), "icon": "fas fa-gavel"}, - { - "label": "Chatbot Logs", - "count": ChatBotLog.objects.all().count(), - "icon": "fas fa-robot", - }, - { - "label": "Suggestions", - "count": Suggestion.objects.all().count(), - "icon": "fas fa-lightbulb", - }, - { - "label": "Suggestion Votes", - "count": SuggestionVotes.objects.all().count(), - "icon": "fas fa-thumbs-up", - }, - { - "label": "Contributors", - "count": Contributor.objects.all().count(), - "icon": "fas fa-user-friends", - }, - { - "label": "Projects", - "count": Project.objects.all().count(), - "icon": "fas fa-project-diagram", - }, - { - "label": "Contributions", - "count": Contribution.objects.all().count(), - "icon": "fas fa-hand-holding-heart", - }, - { - "label": "Bacon Tokens", - "count": BaconToken.objects.all().count(), - "icon": "fas fa-coins", - }, - ] - context["stats"] = sorted(context["stats"], key=lambda x: int(x["count"]), reverse=True) - - def get_cumulative_data(queryset, date_field="created"): - data = list( - queryset.annotate(month=ExtractMonth(date_field)) - .values("month") - .annotate(count=Count("id")) - .order_by("month") - .values_list("count", flat=True) - ) - - cumulative_data = [] - cumulative_sum = 0 - for count in data: - cumulative_sum += count - cumulative_data.append(cumulative_sum) - - return cumulative_data - - # Prepare cumulative sparklines data - context["sparklines_data"] = [ - get_cumulative_data(Issue.objects), # Uses "created" - get_cumulative_data(User.objects, date_field="date_joined"), # Uses "date_joined" - get_cumulative_data(Hunt.objects), # Uses "created" - get_cumulative_data(Domain.objects), # Uses "created" - get_cumulative_data(Subscription.objects), # Uses "created" - get_cumulative_data(Company.objects), # Uses "created" - get_cumulative_data(HuntPrize.objects), # Uses "created" - get_cumulative_data(IssueScreenshot.objects), # Uses "created" - get_cumulative_data(Winner.objects), # Uses "created" - get_cumulative_data(Points.objects), # Uses "created" - get_cumulative_data(InviteFriend.objects), # Uses "created" - get_cumulative_data(UserProfile.objects), # Uses "created" - get_cumulative_data(IP.objects), # Uses "created" - get_cumulative_data(CompanyAdmin.objects), # Uses "created" - get_cumulative_data(Transaction.objects), # Uses "created" - get_cumulative_data(Payment.objects), # Uses "created" - get_cumulative_data(ContributorStats.objects), # Uses "created" - get_cumulative_data(Monitor.objects), # Uses "created" - get_cumulative_data(Bid.objects), # Uses "created" - get_cumulative_data(ChatBotLog.objects), # Uses "created" - get_cumulative_data(Suggestion.objects), # Uses "created" - get_cumulative_data(SuggestionVotes.objects), # Uses "created" - get_cumulative_data(Contributor.objects), # Uses "created" - get_cumulative_data(Project.objects), # Uses "created" - get_cumulative_data(Contribution.objects), # Uses "created" - get_cumulative_data(BaconToken.objects), # Uses "created" - ] - - return context - - -class AllIssuesView(ListView): - paginate_by = 20 - template_name = "list_view.html" - - def get_queryset(self): - username = self.request.GET.get("user") - if username is None: - self.activities = Issue.objects.filter(hunt=None).exclude( - Q(is_hidden=True) & ~Q(user_id=self.request.user.id) - ) - else: - self.activities = Issue.objects.filter(user__username=username, hunt=None).exclude( - Q(is_hidden=True) & ~Q(user_id=self.request.user.id) - ) - return self.activities - - def get_context_data(self, *args, **kwargs): - context = super(AllIssuesView, self).get_context_data(*args, **kwargs) - paginator = Paginator(self.activities, self.paginate_by) - page = self.request.GET.get("page") - - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - try: - activities_paginated = paginator.page(page) - except PageNotAnInteger: - activities_paginated = paginator.page(1) - except EmptyPage: - activities_paginated = paginator.page(paginator.num_pages) - - context["activities"] = activities_paginated - context["user"] = self.request.GET.get("user") - context["activity_screenshots"] = {} - for activity in self.activities: - context["activity_screenshots"][activity] = IssueScreenshot.objects.filter( - issue=activity - ).first() - return context - - -class SpecificIssuesView(ListView): - paginate_by = 20 - template_name = "list_view.html" - - def get_queryset(self): - username = self.request.GET.get("user") - label = self.request.GET.get("label") - query = 0 - statu = "none" - - if label == "General": - query = 0 - elif label == "Number": - query = 1 - elif label == "Functional": - query = 2 - elif label == "Performance": - query = 3 - elif label == "Security": - query = 4 - elif label == "Typo": - query = 5 - elif label == "Design": - query = 6 - elif label == "open": - statu = "open" - elif label == "closed": - statu = "closed" - - if username is None: - self.activities = Issue.objects.filter(hunt=None).exclude( - Q(is_hidden=True) & ~Q(user_id=self.request.user.id) - ) - elif statu != "none": - self.activities = Issue.objects.filter( - user__username=username, status=statu, hunt=None - ).exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) - else: - self.activities = Issue.objects.filter( - user__username=username, label=query, hunt=None - ).exclude(Q(is_hidden=True) & ~Q(user_id=self.request.user.id)) - return self.activities - - def get_context_data(self, *args, **kwargs): - context = super(SpecificIssuesView, self).get_context_data(*args, **kwargs) - paginator = Paginator(self.activities, self.paginate_by) - page = self.request.GET.get("page") - - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - try: - activities_paginated = paginator.page(page) - except PageNotAnInteger: - activities_paginated = paginator.page(1) - except EmptyPage: - activities_paginated = paginator.page(paginator.num_pages) - - context["activities"] = activities_paginated - context["user"] = self.request.GET.get("user") - context["label"] = self.request.GET.get("label") - return context - - -class LeaderboardBase: - """ - get: - 1) ?monthly=true will give list of winners for current month - 2) ?year=2022 will give list of winner of every month from month 1-12 else None - - """ - - def get_leaderboard(self, month=None, year=None, api=False): - """ - all user scores for specified month and year - """ - - data = User.objects - - if year and not month: - data = data.filter(points__created__year=year) - - if year and month: - data = data.filter(Q(points__created__year=year) & Q(points__created__month=month)) - - data = ( - data.annotate(total_score=Sum("points__score")) - .order_by("-total_score") - .filter( - total_score__gt=0, - ) - ) - if api: - return data.values("id", "username", "total_score") - - return data - - def current_month_leaderboard(self, api=False): - """ - leaderboard which includes current month users scores - """ - return self.get_leaderboard( - month=int(datetime.now().month), year=int(datetime.now().year), api=api - ) - - def monthly_year_leaderboard(self, year, api=False): - """ - leaderboard which includes current year top user from each month - """ - - monthly_winner = [] - - # iterating over months 1-12 - for month in range(1, 13): - month_winner = self.get_leaderboard(month, year, api).first() - monthly_winner.append(month_winner) - - return monthly_winner - - -class GlobalLeaderboardView(LeaderboardBase, ListView): - """ - Returns: All users:score data in descending order - """ - - model = User - template_name = "leaderboard_global.html" - - def get_context_data(self, *args, **kwargs): - context = super(GlobalLeaderboardView, self).get_context_data(*args, **kwargs) - - user_related_tags = Tag.objects.filter(userprofile__isnull=False).distinct() - context["user_related_tags"] = user_related_tags - - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - context["leaderboard"] = self.get_leaderboard() - return context - - -class EachmonthLeaderboardView(LeaderboardBase, ListView): - """ - Returns: Grouped user:score data in months for current year - """ - - model = User - template_name = "leaderboard_eachmonth.html" - - def get_context_data(self, *args, **kwargs): - context = super(EachmonthLeaderboardView, self).get_context_data(*args, **kwargs) - - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - - year = self.request.GET.get("year") - - if not year: - year = datetime.now().year - - if isinstance(year, str) and not year.isdigit(): - raise Http404(f"Invalid query passed | Year:{year}") - - year = int(year) - - leaderboard = self.monthly_year_leaderboard(year) - month_winners = [] - - months = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "Novermber", - "December", - ] - - for month_indx, usr in enumerate(leaderboard): - month_winner = {"user": usr, "month": months[month_indx]} - month_winners.append(month_winner) - - context["leaderboard"] = month_winners - - return context - - -class SpecificMonthLeaderboardView(LeaderboardBase, ListView): - """ - Returns: leaderboard for filtered month and year requested in the query - """ - - model = User - template_name = "leaderboard_specific_month.html" - - def get_context_data(self, *args, **kwargs): - context = super(SpecificMonthLeaderboardView, self).get_context_data(*args, **kwargs) - - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - - month = self.request.GET.get("month") - year = self.request.GET.get("year") - - if not month: - month = datetime.now().month - if not year: - year = datetime.now().year - - if isinstance(month, str) and not month.isdigit(): - raise Http404(f"Invalid query passed | Month:{month}") - if isinstance(year, str) and not year.isdigit(): - raise Http404(f"Invalid query passed | Year:{year}") - - month = int(month) - year = int(year) - - if not (month >= 1 and month <= 12): - raise Http404(f"Invalid query passed | Month:{month}") - - context["leaderboard"] = self.get_leaderboard(month, year, api=False) - return context - - -class ScoreboardView(ListView): - model = Domain - template_name = "scoreboard.html" - paginate_by = 20 - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) - - # Annotate each domain with the count of open issues - annotated_domains = Domain.objects.annotate( - open_issues_count=Count("issue", filter=Q(issue__status="open")) - ).order_by("-open_issues_count") - - paginator = Paginator(annotated_domains, self.paginate_by) - page = self.request.GET.get("page") - - try: - scoreboard_paginated = paginator.page(page) - except PageNotAnInteger: - scoreboard_paginated = paginator.page(1) - except EmptyPage: - scoreboard_paginated = paginator.page(paginator.num_pages) - - context["scoreboard"] = scoreboard_paginated - context["user"] = self.request.GET.get("user") - return context - - -def search(request, template="search.html"): - query = request.GET.get("query") - stype = request.GET.get("type") - context = None - if query is None: - return render(request, template) - query = query.strip() - if query[:6] == "issue:": - stype = "issue" - query = query[6:] - elif query[:7] == "domain:": - stype = "domain" - query = query[7:] - elif query[:5] == "user:": - stype = "user" - query = query[5:] - elif query[:6] == "label:": - stype = "label" - query = query[6:] - if stype == "issue" or stype is None: - context = { - "query": query, - "type": stype, - "issues": Issue.objects.filter(Q(description__icontains=query), hunt=None).exclude( - Q(is_hidden=True) & ~Q(user_id=request.user.id) - )[0:20], - } - elif stype == "domain": - context = { - "query": query, - "type": stype, - "domains": Domain.objects.filter(Q(url__icontains=query), hunt=None)[0:20], - } - elif stype == "user": - context = { - "query": query, - "type": stype, - "users": UserProfile.objects.filter(Q(user__username__icontains=query)) - .annotate(total_score=Sum("user__points__score")) - .order_by("-total_score")[0:20], - } - elif stype == "label": - context = { - "query": query, - "type": stype, - "issues": Issue.objects.filter(Q(label__icontains=query), hunt=None).exclude( - Q(is_hidden=True) & ~Q(user_id=request.user.id) - )[0:20], - } - - if request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=request.user) - return render(request, template, context) + if request.user.is_authenticated: + context["wallet"] = Wallet.objects.get(user=request.user) + return render(request, template, context) def search_issues(request, template="search.html"): @@ -2306,112 +688,6 @@ def search_issues(request, template="search.html"): return HttpResponse(json.dumps({"issues": issues}), content_type="application/json") -class HuntCreate(CreateView): - model = Hunt - fields = ["url", "logo", "name", "description", "prize", "plan"] - template_name = "hunt.html" - - def form_valid(self, form): - self.object = form.save(commit=False) - self.object.user = self.request.user - - domain, created = Domain.objects.get_or_create( - name=self.request.POST.get("url").replace("www.", ""), - defaults={"url": "http://" + self.request.POST.get("url").replace("www.", "")}, - ) - self.object.domain = domain - - self.object.save() - return super(HuntCreate, self).form_valid(form) - - -class IssueView(DetailView): - model = Issue - slug_field = "id" - template_name = "issue.html" - - def get(self, request, *args, **kwargs): - print("getting issue id: ", self.kwargs["slug"]) - print("getting issue id: ", self.kwargs) - ipdetails = IP() - try: - id = int(self.kwargs["slug"]) - except ValueError: - return HttpResponseNotFound("Invalid ID: ID must be an integer") - - self.object = get_object_or_404(Issue, id=self.kwargs["slug"]) - ipdetails.user = self.request.user - ipdetails.address = get_client_ip(request) - ipdetails.issuenumber = self.object.id - ipdetails.path = request.path - ipdetails.agent = request.META["HTTP_USER_AGENT"] - ipdetails.referer = request.META.get("HTTP_REFERER", None) - - print("IP Address: ", ipdetails.address) - print("Issue Number: ", ipdetails.issuenumber) - - try: - if self.request.user.is_authenticated: - try: - objectget = IP.objects.get(user=self.request.user, issuenumber=self.object.id) - self.object.save() - except: - ipdetails.save() - self.object.views = (self.object.views or 0) + 1 - self.object.save() - else: - try: - objectget = IP.objects.get( - address=get_client_ip(request), issuenumber=self.object.id - ) - self.object.save() - except Exception as e: - pass # pass this temporarly to avoid error - # print(e) - # messages.error(self.request, "That issue was not found 2." + str(e)) - # ipdetails.save() - # self.object.views = (self.object.views or 0) + 1 - # self.object.save() - except Exception as e: - pass # pass this temporarly to avoid error - # print(e) - # messages.error(self.request, "That issue was not found 1." + str(e)) - # return redirect("/") - return super(IssueView, self).get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - print("getting context data") - context = super(IssueView, self).get_context_data(**kwargs) - if self.object.user_agent: - user_agent = parse(self.object.user_agent) - context["browser_family"] = user_agent.browser.family - context["browser_version"] = user_agent.browser.version_string - context["os_family"] = user_agent.os.family - context["os_version"] = user_agent.os.version_string - context["users_score"] = list( - Points.objects.filter(user=self.object.user) - .aggregate(total_score=Sum("score")) - .values() - )[0] - - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - context["issue_count"] = Issue.objects.filter(url__contains=self.object.domain_name).count() - context["all_comment"] = self.object.comments.all - context["all_users"] = User.objects.all() - context["likes"] = UserProfile.objects.filter(issue_upvoted=self.object).count() - context["likers"] = UserProfile.objects.filter(issue_upvoted=self.object) - context["dislikes"] = UserProfile.objects.filter(issue_downvoted=self.object).count() - context["dislikers"] = UserProfile.objects.filter(issue_downvoted=self.object) - - context["flags"] = UserProfile.objects.filter(issue_flaged=self.object).count() - context["flagers"] = UserProfile.objects.filter(issue_flaged=self.object) - - context["screenshots"] = IssueScreenshot.objects.filter(issue=self.object).all() - - return context - - @login_required(login_url="/accounts/login") def flag_issue(request, issue_pk): context = {} @@ -2425,166 +701,36 @@ def flag_issue(request, issue_pk): issue_pk = issue.pk userprof.save() - total_flag_votes = UserProfile.objects.filter(issue_flaged=issue).count() - context["object"] = issue - context["flags"] = total_flag_votes - return render(request, "_flags.html", context) - - -def IssueEdit(request): - if request.method == "POST": - issue = Issue.objects.get(pk=request.POST.get("issue_pk")) - uri = request.POST.get("domain") - link = uri.replace("www.", "") - if request.user == issue.user or request.user.is_superuser: - domain, created = Domain.objects.get_or_create( - name=link, defaults={"url": "http://" + link} - ) - issue.domain = domain - if uri[:4] != "http" and uri[:5] != "https": - uri = "https://" + uri - issue.url = uri - issue.description = request.POST.get("description") - issue.label = request.POST.get("label") - issue.save() - if created: - return HttpResponse("Domain Created") - else: - return HttpResponse("Updated") - else: - return HttpResponse("Unauthorised") - else: - return HttpResponse("POST ONLY") - - -def get_email_from_domain(domain_name): - new_urls = deque(["http://" + domain_name]) - processed_urls = set() - emails = set() - emails_out = set() - t_end = time.time() + 20 - - while len(new_urls) and time.time() < t_end: - url = new_urls.popleft() - processed_urls.add(url) - parts = urlsplit(url) - base_url = "{0.scheme}://{0.netloc}".format(parts) - path = url[: url.rfind("/") + 1] if "/" in parts.path else url - try: - response = requests.get(url) - except: - continue - new_emails = set( - re.findall(r"[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+", response.text, re.I) - ) - if new_emails: - emails.update(new_emails) - break - soup = BeautifulSoup(response.text) - for anchor in soup.find_all("a"): - link = anchor.attrs["href"] if "href" in anchor.attrs else "" - if link.startswith("/"): - link = base_url + link - elif not link.startswith("http"): - link = path + link - if link not in new_urls and link not in processed_urls and link.find(domain_name) > 0: - new_urls.append(link) - - for email in emails: - if email.find(domain_name) > 0: - emails_out.add(email) - try: - return list(emails_out)[0] - except: - return False - - -class InboundParseWebhookView(View): - def post(self, request, *args, **kwargs): - data = request.body - for event in json.loads(data): - try: - domain = Domain.objects.get(email__iexact=event.get("email")) - domain.email_event = event.get("event") - if event.get("event") == "click": - domain.clicks = int(domain.clicks or 0) + 1 - domain.save() - except Exception: - pass - - return JsonResponse({"detail": "Inbound Sendgrid Webhook recieved"}) - - -def UpdateIssue(request): - if not request.POST.get("issue_pk"): - return HttpResponse("Missing issue ID") - issue = get_object_or_404(Issue, pk=request.POST.get("issue_pk")) - try: - for token in Token.objects.all(): - if request.POST["token"] == token.key: - request.user = User.objects.get(id=token.user_id) - tokenauth = True - except: - tokenauth = False - if ( - request.method == "POST" - and request.user.is_superuser - or (issue is not None and request.user == issue.user) - ): - if request.POST.get("action") == "close": - issue.status = "closed" - issue.closed_by = request.user - issue.closed_date = datetime.now() - - msg_plain = msg_html = render_to_string( - "email/bug_updated.txt", - { - "domain": issue.domain.name, - "name": issue.user.username if issue.user else "Anonymous", - "id": issue.id, - "username": request.user.username, - "action": "closed", - }, - ) - subject = ( - issue.domain.name - + " bug # " - + str(issue.id) - + " closed by " - + request.user.username - ) - - elif request.POST.get("action") == "open": - issue.status = "open" - issue.closed_by = None - issue.closed_date = None - msg_plain = msg_html = render_to_string( - "email/bug_updated.txt", - { - "domain": issue.domain.name, - "name": issue.domain.email.split("@")[0], - "id": issue.id, - "username": request.user.username, - "action": "opened", - }, - ) - subject = ( - issue.domain.name - + " bug # " - + str(issue.id) - + " opened by " - + request.user.username - ) - - mailer = settings.EMAIL_TO_STRING - email_to = issue.user.email - send_mail(subject, msg_plain, mailer, [email_to], html_message=msg_html) - send_mail(subject, msg_plain, mailer, [issue.domain.email], html_message=msg_html) - issue.save() - return HttpResponse("Updated") + total_flag_votes = UserProfile.objects.filter(issue_flaged=issue).count() + context["object"] = issue + context["flags"] = total_flag_votes + return render(request, "_flags.html", context) - elif request.method == "POST": - return HttpResponse("invalid") + +def IssueEdit(request): + if request.method == "POST": + issue = Issue.objects.get(pk=request.POST.get("issue_pk")) + uri = request.POST.get("domain") + link = uri.replace("www.", "") + if request.user == issue.user or request.user.is_superuser: + domain, created = Domain.objects.get_or_create( + name=link, defaults={"url": "http://" + link} + ) + issue.domain = domain + if uri[:4] != "http" and uri[:5] != "https": + uri = "https://" + uri + issue.url = uri + issue.description = request.POST.get("description") + issue.label = request.POST.get("label") + issue.save() + if created: + return HttpResponse("Domain Created") + else: + return HttpResponse("Updated") + else: + return HttpResponse("Unauthorised") + else: + return HttpResponse("POST ONLY") @receiver(user_logged_in) @@ -2667,15 +813,6 @@ def unsave_issue(request, issue_pk): return HttpResponse("OK") -def get_client_ip(request): - x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") - if x_forwarded_for: - ip = x_forwarded_for.split(",")[0] - else: - ip = request.META.get("REMOTE_ADDR") - return ip - - def get_score(request): users = [] temp_users = ( @@ -2707,7 +844,7 @@ def comment_on_issue(request, issue_pk): issue = Issue.objects.filter(pk=issue_pk).first() if request.method == "POST" and isinstance(request.user, User): - comment = request.POST.get("comment", "") + comment = escape(request.POST.get("comment", "")) replying_to_input = request.POST.get("replying_to_input", "").split("#") if issue is None: @@ -2754,7 +891,7 @@ def update_comment(request, issue_pk, comment_pk): issue = Issue.objects.filter(pk=issue_pk).first() comment = Comment.objects.filter(pk=comment_pk).first() if request.method == "POST" and isinstance(request.user, User): - comment.text = request.POST.get("comment", "") + comment.text = escape(request.POST.get("comment", "")) comment.save() context = { @@ -2780,13 +917,6 @@ def delete_comment(request): return render(request, "comments2.html", context) -class CustomObtainAuthToken(ObtainAuthToken): - def post(self, request, *args, **kwargs): - response = super(CustomObtainAuthToken, self).post(request, *args, **kwargs) - token = Token.objects.get(key=response.data["token"]) - return Response({"token": token.key, "id": token.user_id}) - - def create_tokens(request): for user in User.objects.all(): Token.objects.get_or_create(user=user) @@ -2804,9 +934,8 @@ def monitor_create_view(request): form = MonitorForm(request.POST) if form.is_valid(): monitor = form.save(commit=False) - monitor.user = request.user # Assuming you have a logged-in user + monitor.user = request.user monitor.save() - # Redirect to a success page or render a success message else: form = MonitorForm() return render(request, "Moniter.html", {"form": form}) @@ -2887,323 +1016,6 @@ def ads_txt(request): return HttpResponse("\n".join(lines), content_type="text/plain") -class CreateHunt(TemplateView): - model = Hunt - fields = ["url", "logo", "domain", "plan", "prize", "txn_id"] - template_name = "create_hunt.html" - - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - try: - domain_admin = CompanyAdmin.objects.get(user=request.user) - if not domain_admin.is_active: - return HttpResponseRedirect("/") - domain = [] - if domain_admin.role == 0: - domain = Domain.objects.filter(company=domain_admin.company) - else: - domain = Domain.objects.filter(pk=domain_admin.domain.pk) - - context = {"domains": domain, "hunt_form": HuntForm()} - return render(request, self.template_name, context) - except: - return HttpResponseRedirect("/") - - @method_decorator(login_required) - def post(self, request, *args, **kwargs): - try: - domain_admin = CompanyAdmin.objects.get(user=request.user) - if ( - domain_admin.role == 1 - and ( - str(domain_admin.domain.pk) - == ((request.POST["domain"]).split("-"))[0].replace(" ", "") - ) - ) or domain_admin.role == 0: - wallet, created = Wallet.objects.get_or_create(user=request.user) - total_amount = ( - Decimal(request.POST["prize_winner"]) - + Decimal(request.POST["prize_runner"]) - + Decimal(request.POST["prize_second_runner"]) - ) - if total_amount > wallet.current_balance: - return HttpResponse("failed") - hunt = Hunt() - hunt.domain = Domain.objects.get( - pk=(request.POST["domain"]).split("-")[0].replace(" ", "") - ) - data = {} - data["content"] = request.POST["content"] - data["start_date"] = request.POST["start_date"] - data["end_date"] = request.POST["end_date"] - form = HuntForm(data) - if not form.is_valid(): - return HttpResponse("failed") - if not domain_admin.is_active: - return HttpResponse("failed") - if domain_admin.role == 1: - if hunt.domain != domain_admin.domain: - return HttpResponse("failed") - hunt.domain = Domain.objects.get( - pk=(request.POST["domain"]).split("-")[0].replace(" ", "") - ) - tzsign = 1 - offset = request.POST["tzoffset"] - if int(offset) < 0: - offset = int(offset) * (-1) - tzsign = -1 - start_date = form.cleaned_data["start_date"] - end_date = form.cleaned_data["end_date"] - if tzsign > 0: - start_date = start_date + timedelta( - hours=int(int(offset) / 60), minutes=int(int(offset) % 60) - ) - end_date = end_date + timedelta( - hours=int(int(offset) / 60), minutes=int(int(offset) % 60) - ) - else: - start_date = start_date - ( - timedelta(hours=int(int(offset) / 60), minutes=int(int(offset) % 60)) - ) - end_date = end_date - ( - timedelta(hours=int(int(offset) / 60), minutes=int(int(offset) % 60)) - ) - hunt.starts_on = start_date - hunt.prize_winner = Decimal(request.POST["prize_winner"]) - hunt.prize_runner = Decimal(request.POST["prize_runner"]) - hunt.prize_second_runner = Decimal(request.POST["prize_second_runner"]) - hunt.end_on = end_date - hunt.name = request.POST["name"] - hunt.description = request.POST["content"] - wallet.withdraw(total_amount) - wallet.save() - try: - is_published = request.POST["publish"] - hunt.is_published = True - except: - hunt.is_published = False - hunt.save() - return HttpResponse("success") - else: - return HttpResponse("failed") - except: - return HttpResponse("failed") - - -class ListHunts(TemplateView): - model = Hunt - template_name = "hunt_list.html" - - def get(self, request, *args, **kwargs): - search = request.GET.get("search", "") - start_date = request.GET.get("start_date", None) - end_date = request.GET.get("end_date", None) - domain = request.GET.get("domain", None) - hunt_type = request.GET.get("hunt_type", "all") - - hunts = ( - Hunt.objects.values( - "id", - "name", - "url", - "logo", - "starts_on", - "starts_on__day", - "starts_on__month", - "starts_on__year", - "end_on", - "end_on__day", - "end_on__month", - "end_on__year", - ) - .annotate(total_prize=Sum("huntprize__value")) - .all() - ) - - filtered_bughunts = { - "all": hunts, - "ongoing": hunts.filter(result_published=False, is_published=True), - "ended": hunts.filter(result_published=True), - "draft": hunts.filter(result_published=False, is_published=False), - } - - hunts = filtered_bughunts.get(hunt_type, hunts) - - if search.strip() != "": - hunts = hunts.filter(Q(name__icontains=search)) - - if start_date != "" and start_date is not None: - start_date = datetime.strptime(start_date, "%m/%d/%Y").strftime("%Y-%m-%d %H:%M") - hunts = hunts.filter(starts_on__gte=start_date) - - if end_date != "" and end_date is not None: - end_date = datetime.strptime(end_date, "%m/%d/%Y").strftime("%Y-%m-%d %H:%M") - hunts = hunts.filter(end_on__gte=end_date) - - if domain != "Select Domain" and domain is not None: - domain = Domain.objects.filter(id=domain).first() - hunts = hunts.filter(domain=domain) - - context = {"hunts": hunts, "domains": Domain.objects.values("id", "name").all()} - - return render(request, self.template_name, context) - - def post(self, request, *args, **kwargs): - request.GET.search = request.GET.get("search", "") - request.GET.start_date = request.GET.get("start_date", "") - request.GET.end_date = request.GET.get("end_date", "") - request.GET.domain = request.GET.get("domain", "Select Domain") - request.GET.hunt_type = request.GET.get("type", "all") - - return self.get(request) - - -class DraftHunts(TemplateView): - model = Hunt - fields = ["url", "logo", "domain", "plan", "prize", "txn_id"] - template_name = "hunt_drafts.html" - - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - try: - domain_admin = CompanyAdmin.objects.get(user=request.user) - if not domain_admin.is_active: - return HttpResponseRedirect("/") - if domain_admin.role == 0: - hunt = self.model.objects.filter(is_published=False) - else: - hunt = self.model.objects.filter(is_published=False, domain=domain_admin.domain) - context = {"hunts": hunt} - return render(request, self.template_name, context) - except: - return HttpResponseRedirect("/") - - -class UpcomingHunts(TemplateView): - model = Hunt - fields = ["url", "logo", "domain", "plan", "prize", "txn_id"] - template_name = "hunt_upcoming.html" - - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - try: - domain_admin = CompanyAdmin.objects.get(user=request.user) - if not domain_admin.is_active: - return HttpResponseRedirect("/") - - if domain_admin.role == 0: - hunts = self.model.objects.filter(is_published=True) - else: - hunts = self.model.objects.filter(is_published=True, domain=domain_admin.domain) - new_hunt = [] - for hunt in hunts: - if ((hunt.starts_on - datetime.now(timezone.utc)).total_seconds()) > 0: - new_hunt.append(hunt) - context = {"hunts": new_hunt} - return render(request, self.template_name, context) - except: - return HttpResponseRedirect("/") - - -class OngoingHunts(TemplateView): - model = Hunt - fields = ["url", "logo", "domain", "plan", "prize", "txn_id"] - template_name = "hunt_ongoing.html" - - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - try: - domain_admin = CompanyAdmin.objects.get(user=request.user) - if not domain_admin.is_active: - return HttpResponseRedirect("/") - if domain_admin.role == 0: - hunts = self.model.objects.filter(is_published=True) - else: - hunts = self.model.objects.filter(is_published=True, domain=domain_admin.domain) - new_hunt = [] - for hunt in hunts: - if ((hunt.starts_on - datetime.now(timezone.utc)).total_seconds()) > 0: - new_hunt.append(hunt) - context = {"hunts": new_hunt} - return render(request, self.template_name, context) - except: - return HttpResponseRedirect("/") - - -class PreviousHunts(TemplateView): - model = Hunt - fields = ["url", "logo", "domain", "plan", "prize", "txn_id"] - template_name = "hunt_previous.html" - - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - try: - domain_admin = CompanyAdmin.objects.get(user=request.user) - if not domain_admin.is_active: - return HttpResponseRedirect("/") - if domain_admin.role == 0: - hunts = self.model.objects.filter(is_published=True) - else: - hunts = self.model.objects.filter(is_published=True, domain=domain_admin.domain) - new_hunt = [] - for hunt in hunts: - if ((hunt.starts_on - datetime.now(timezone.utc)).total_seconds()) > 0: - pass - elif ((hunt.end_on - datetime.now(timezone.utc)).total_seconds()) < 0: - new_hunt.append(hunt) - else: - pass - context = {"hunts": new_hunt} - return render(request, self.template_name, context) - except: - return HttpResponseRedirect("/") - - @method_decorator(login_required) - def post(self, request, *args, **kwargs): - form = UserProfileForm(request.POST, request.FILES, instance=request.user.userprofile) - if form.is_valid(): - form.save() - return redirect(reverse("profile", kwargs={"slug": kwargs.get("slug")})) - - -class CompanySettings(TemplateView): - model = CompanyAdmin - fields = ["user", "domain", "role", "is_active"] - template_name = "company_settings.html" - - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - try: - domain_admin = CompanyAdmin.objects.get(user=request.user) - if not domain_admin.is_active: - return HttpResponseRedirect("/") - domain_admins = [] - domain_list = Domain.objects.filter(company=domain_admin.company) - if domain_admin.role == 0: - domain_admins = CompanyAdmin.objects.filter( - company=domain_admin.company, is_active=True - ) - else: - domain_admins = CompanyAdmin.objects.filter( - domain=domain_admin.domain, is_active=True - ) - context = { - "admins": domain_admins, - "user": domain_admin, - "domain_list": domain_list, - } - return render(request, self.template_name, context) - except: - return HttpResponseRedirect("/") - - @method_decorator(login_required) - def post(self, request, *args, **kwargs): - form = UserProfileForm(request.POST, request.FILES, instance=request.user.userprofile) - if form.is_valid(): - form.save() - return redirect(reverse("profile", kwargs={"slug": kwargs.get("slug")})) - - @login_required(login_url="/accounts/login") def update_role(request): if request.method == "POST": @@ -3330,24 +1142,6 @@ def add_or_update_company(request): return HttpResponse("no access") -class DomainList(TemplateView): - model = Domain - template_name = "company_domain_lists.html" - - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - domain_admin = CompanyAdmin.objects.get(user=request.user) - if not domain_admin.is_active: - return HttpResponseRedirect("/") - domain = [] - if domain_admin.role == 0: - domain = self.model.objects.filter(company=domain_admin.company) - else: - domain = self.model.objects.filter(pk=domain_admin.domain.pk) - context = {"domains": domain} - return render(request, self.template_name, context) - - @login_required(login_url="/accounts/login") def add_or_update_domain(request): if request.method == "POST": @@ -3604,78 +1398,6 @@ def stripe_connected(request, username): return HttpResponse("error") -class JoinCompany(TemplateView): - model = Company - template_name = "join.html" - - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - wallet, created = Wallet.objects.get_or_create(user=request.user) - context = {"wallet": wallet} - return render(request, self.template_name, context) - - def post(self, request, *args, **kwargs): - name = request.POST["company"] - try: - company_exists = Company.objects.get(name=name) - return JsonResponse({"status": "There was some error"}) - except: - pass - url = request.POST["url"] - email = request.POST["email"] - product = request.POST["product"] - sub = Subscription.objects.get(name=product) - if name == "" or url == "" or email == "" or product == "": - return JsonResponse({"error": "Empty Fields"}) - paymentType = request.POST["paymentType"] - if paymentType == "wallet": - wallet, created = Wallet.objects.get_or_create(user=request.user) - if wallet.current_balance < sub.charge_per_month: - return JsonResponse({"error": "insufficient balance in Wallet"}) - wallet.withdraw(sub.charge_per_month) - company = Company() - company.admin = request.user - company.name = name - company.url = url - company.email = email - company.subscription = sub - company.save() - admin = CompanyAdmin() - admin.user = request.user - admin.role = 0 - admin.company = company - admin.is_active = True - admin.save() - return JsonResponse({"status": "Success"}) - # company.subscription = - elif paymentType == "card": - from django.conf import settings - - stripe.api_key = settings.STRIPE_TEST_SECRET_KEY - charge = stripe.Charge.create( - amount=int(Decimal(sub.charge_per_month) * 100), - currency="usd", - description="Example charge", - source=request.POST["stripeToken"], - ) - company = Company() - company.admin = request.user - company.name = name - company.url = url - company.email = email - company.subscription = sub - company.save() - admin = CompanyAdmin() - admin.user = request.user - admin.role = 0 - admin.company = company - admin.is_active = True - admin.save() - return JsonResponse({"status": "Success"}) - else: - return JsonResponse({"status": "There was some error"}) - - @login_required(login_url="/accounts/login") def view_hunt(request, pk, template="view_hunt.html"): hunt = get_object_or_404(Hunt, pk=pk) @@ -3740,7 +1462,6 @@ def submit_bug(request, pk, template="hunt_submittion.html"): if isinstance(request.POST.get("file"), six.string_types): import imghdr - # Check if the base64 string is in the "data:" format data = ( "data:image/" + request.POST.get("type") @@ -3900,23 +1621,18 @@ def handler500(request, exception=None): def users_view(request, *args, **kwargs): context = {} - # Get all tags related to all user profiles context["user_related_tags"] = Tag.objects.filter(userprofile__isnull=False).distinct() - # Get all tags in the system context["tags"] = Tag.objects.all() - # Check if a specific tag is being requested tag_name = request.GET.get("tag") if tag_name: - # Check if the requested tag exists in user_related_tags if context["user_related_tags"].filter(name=tag_name).exists(): context["tag"] = tag_name context["users"] = UserProfile.objects.filter(tags__name=tag_name) else: context["users"] = UserProfile.objects.none() # No users if the tag isn't found else: - # Default filter: Show users with the tag "BLT Contributor" context["tag"] = "BLT Contributors" context["users"] = UserProfile.objects.filter(tags__name="BLT Contributors") @@ -3961,7 +1677,6 @@ def get_bch_balance(address): print(f"An error occurred: {e}") return None - # Example BCH address bch_address = "bitcoincash:qr5yccf7j4dpjekyz3vpawgaarl352n7yv5d5mtzzc" balance = get_bch_balance(bch_address) @@ -3975,42 +1690,15 @@ def safe_redirect(request: HttpRequest): http_referer = request.META.get("HTTP_REFERER") if http_referer: referer_url = urlparse(http_referer) - # Check if the referer URL's host is the same as the site's host if referer_url.netloc == request.get_host(): - # Build a 'safe' version of the referer URL (without query string or fragment) safe_url = urlunparse( (referer_url.scheme, referer_url.netloc, referer_url.path, "", "", "") ) return redirect(safe_url) - # Redirect to the fallback path if 'HTTP_REFERER' is not provided or is not safe - # Build the fallback URL using the request's scheme and host fallback_url = f"{request.scheme}://{request.get_host()}/" return redirect(fallback_url) -class DomainListView(ListView): - model = Domain - paginate_by = 20 - template_name = "domain_list.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - domain = Domain.objects.all() - - paginator = Paginator(domain, self.paginate_by) - page = self.request.GET.get("page") - - try: - domain_paginated = paginator.page(page) - except PageNotAnInteger: - domain_paginated = paginator.page(1) - except EmptyPage: - domain_paginated = paginator.page(paginator.num_pages) - - context["domain"] = domain_paginated - return context - - @login_required(login_url="/accounts/login") def flag_issue(request, issue_pk): context = {} @@ -4133,124 +1821,9 @@ def subscribe_to_domains(request, pk): return JsonResponse("SUBSCRIBED", safe=False) -class IssueView(DetailView): - model = Issue - slug_field = "id" - template_name = "issue.html" - - def get(self, request, *args, **kwargs): - ipdetails = IP() - try: - id = int(self.kwargs["slug"]) - except ValueError: - return HttpResponseNotFound("Invalid ID: ID must be an integer") - - self.object = get_object_or_404(Issue, id=self.kwargs["slug"]) - ipdetails.user = self.request.user - ipdetails.address = get_client_ip(request) - ipdetails.issuenumber = self.object.id - ipdetails.path = request.get_full_path() - ipdetails.referer = request.META.get("HTTP_REFERER") - ipdetails.agent = request.META.get("HTTP_USER_AGENT") - - try: - if self.request.user.is_authenticated: - try: - objectget = IP.objects.get(user=self.request.user, issuenumber=self.object.id) - self.object.save() - except: - ipdetails.save() - self.object.views = (self.object.views or 0) + 1 - self.object.save() - else: - try: - objectget = IP.objects.get( - address=get_client_ip(request), issuenumber=self.object.id - ) - self.object.save() - except: - ipdetails.save() - self.object.views = (self.object.views or 0) + 1 - self.object.save() - except Exception as e: - print(e) - # TODO: this is only an error for ipv6 currently and doesn't require us to redirect the user - we'll sort this out later - # messages.error(self.request, "That issue was not found."+str(e)) - # return redirect("/") - return super(IssueView, self).get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super(IssueView, self).get_context_data(**kwargs) - if self.object.user_agent: - user_agent = parse(self.object.user_agent) - context["browser_family"] = user_agent.browser.family - context["browser_version"] = user_agent.browser.version_string - context["os_family"] = user_agent.os.family - context["os_version"] = user_agent.os.version_string - context["users_score"] = list( - Points.objects.filter(user=self.object.user) - .aggregate(total_score=Sum("score")) - .values() - )[0] - - if self.request.user.is_authenticated: - context["wallet"] = Wallet.objects.get(user=self.request.user) - context["isLiked"] = UserProfile.objects.filter( - issue_upvoted=self.object, user=self.request.user - ).exists() - context["isDisliked"] = UserProfile.objects.filter( - issue_downvoted=self.object, user=self.request.user - ).exists() - context["isFlagged"] = UserProfile.objects.filter( - issue_flaged=self.object, user=self.request.user - ).exists() - context["issue_count"] = Issue.objects.filter(url__contains=self.object.domain_name).count() - context["all_comment"] = self.object.comments.all().order_by("-created_date") - context["all_users"] = User.objects.all() - context["likes"] = UserProfile.objects.filter(issue_upvoted=self.object).count() - context["dislikes"] = UserProfile.objects.filter(issue_downvoted=self.object).count() - context["likers"] = UserProfile.objects.filter(issue_upvoted=self.object).all() - context["flags"] = UserProfile.objects.filter(issue_flaged=self.object).count() - context["flagers"] = UserProfile.objects.filter(issue_flaged=self.object) - context["more_issues"] = ( - Issue.objects.filter(user=self.object.user) - .exclude(id=self.object.id) - .values("id", "description", "markdown_description", "screenshots__image") - .order_by("views")[:4] - ) - # TODO test if email works - if isinstance(self.request.user, User): - context["subscribed_to_domain"] = self.object.domain.user_subscribed_domains.filter( - pk=self.request.user.userprofile.id - ).exists() - else: - context["subscribed_to_domain"] = False - - if isinstance(self.request.user, User): - context["bookmarked"] = self.request.user.userprofile.issue_saved.filter( - pk=self.object.id - ).exists() - context["screenshots"] = IssueScreenshot.objects.filter(issue=self.object).all() - context["status"] = Issue.objects.filter(id=self.object.id).get().status - context["github_issues_url"] = ( - str(Issue.objects.filter(id=self.object.id).get().domain.github) + "/issues" - ) - context["email_clicks"] = Issue.objects.filter(id=self.object.id).get().domain.clicks - context["email_events"] = Issue.objects.filter(id=self.object.id).get().domain.email_event - if not self.object.github_url: - context["github_link"] = "empty" - else: - context["github_link"] = self.object.github_url - - return context - - def create_github_issue(request, id): issue = get_object_or_404(Issue, id=id) screenshot_all = IssueScreenshot.objects.filter(issue=issue) - # referer = request.META.get("HTTP_REFERER") - # if not referer: - # return HttpResponseForbidden() if not os.environ.get("GITHUB_ACCESS_TOKEN"): return JsonResponse({"status": "Failed", "status_reason": "GitHub Access Token is missing"}) if issue.github_url: @@ -4357,15 +1930,13 @@ def handle_user_signup(request, user, **kwargs): def reward_sender_with_points(sender): - # Create or update points for the sender points, created = Points.objects.get_or_create(user=sender, defaults={"score": 0}) - points.score += 2 # Reward 2 points for each successful referral signup + points.score += 2 points.save() def referral_signup(request): referral_token = request.GET.get("ref") - # check the referral token is present on invitefriend model or not and if present then set the referral token in the session if referral_token: try: invite = InviteFriend.objects.get(referral_code=referral_token) @@ -4377,7 +1948,6 @@ def referral_signup(request): def invite_friend(request): - # check if the user is authenticated or not if not request.user.is_authenticated: return redirect("account_login") current_site = get_current_site(request) @@ -4424,120 +1994,6 @@ def trademark_detailview(request, slug): return render(request, "trademark_detailview.html", context) -# class CreateIssue(CronJobBase): -# RUN_EVERY_MINS = 1 - -# schedule = Schedule(run_every_mins=RUN_EVERY_MINS) -# code = "blt.create_issue" # a unique code - -# def do(self): -# from django.conf import settings -# import imaplib - -# mail = imaplib.IMAP4_SSL("imap.gmail.com", 993) -# error = False -# mail.login(settings.REPORT_EMAIL, settings.REPORT_EMAIL_PASSWORD) -# mail.list() -# # Out: list of "folders" aka labels in gmail. -# mail.select("inbox") # connect to inbox. -# typ, data = mail.search(None, "ALL", "UNSEEN") -# import email - - -# for num in data[0].split(): -# image = False -# screenshot_base64 = "" -# url = "" -# label = "" -# token = "None" -# typ, data = mail.fetch(num, "(RFC822)") -# raw_email = (data[0][1]).decode("utf-8") -# email_message = email.message_from_string(raw_email) -# maintype = email_message.get_content_maintype() -# error = False -# for part in email_message.walk(): -# if part.get_content_type() == "text/plain": # ignore attachments/html -# body = part.get_payload(decode=True) -# body_text = body.decode("utf-8") -# words = body_text.split() -# flag_word = False -# for word in words: -# if word.lower() == ":": -# continue -# if word.lower() == "url": -# continue -# if word.lower() == "type": -# flag_word = True -# continue -# if not flag_word: -# url = word -# continue -# if flag_word: -# label = word -# if part.get_content_maintype() == "multipart": -# continue -# if part.get("Content-Disposition") is None: -# continue -# image = True -# screenshot_base64 = part -# sender = email_message["From"].split()[-1] -# address = re.sub(r"[<>]", "", sender) -# for user in User.objects.all(): -# if user.email == address: -# token = Token.objects.get(user_id=user.id).key -# break -# if label.lower() == "general": -# label = 0 -# elif label.lower() == "number error": -# label = 1 -# elif label.lower() == "functional": -# label = 2 -# elif label.lower() == "performance": -# label = 3 -# elif label.lower() == "security": -# label = 4 -# elif label.lower() == "typo": -# label = 5 -# elif label.lower() == "design": -# label = 6 -# else: -# error = True -# if token == "None": -# error = "TokenTrue" -# if not image: -# error = True -# if error: -# send_mail( -# "Error In Your Report", -# "There was something wrong with the mail you sent regarding the issue to be created. Please check the content and try again later !", -# settings.EMAIL_TO_STRING, -# [address], -# fail_silently=False, -# ) -# elif error == "TokenTrue": -# send_mail( -# "You are not a user of " + settings.PROJECT_NAME, -# "You are not a Registered user at " + settings.PROJECT_NAME + " .Please first Signup at " + settings.PROJECT_NAME + " and Try Again Later ! .", -# settings.EMAIL_TO_STRING, -# [address], -# fail_silently=False, -# ) -# else: -# data = { -# "url": url, -# "description": email_message["Subject"], -# "file": str(screenshot_base64.get_payload(decode=False)), -# "token": token, -# "label": label, -# "type": "jpg", -# } -# headers = {"Content-Type": "application/x-www-form-urlencoded"} -# requests.post( -# "https://" + settings.FQDN + "/api/v1/createissues/", -# data=json.dumps(data), -# headers=headers, -# ) -# mail.logout() def update_bch_address(request): if request.method == "POST": selected_crypto = request.POST.get("selected_crypto") @@ -4573,45 +2029,6 @@ def sitemap(request): return render(request, "sitemap.html", {"random_domain": random_domain}) -class ContributorStatsView(TemplateView): - template_name = "contributor_stats.html" - today = False - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - # Fetch all contributor stats records - stats = ContributorStats.objects.all() - if self.today: - # For "today" stats - user_stats = sorted( - ([stat.username, stat.prs] for stat in stats if stat.prs > 0), - key=lambda x: x[1], # Sort by PRs value - reverse=True, # Descending order - ) - else: - # Convert the stats to a dictionary format expected by the template - user_stats = { - stat.username: { - "commits": stat.commits, - "issues_opened": stat.issues_opened, - "issues_closed": stat.issues_closed, - "assigned_issues": stat.assigned_issues, - "prs": stat.prs, - "comments": stat.comments, - } - for stat in stats - } - - context["user_stats"] = user_stats - context["today"] = self.today - context["owner"] = "OWASP-BLT" - context["repo"] = "BLT" - context["start_date"] = (datetime.now().date() - timedelta(days=7)).isoformat() - context["end_date"] = datetime.now().date().isoformat() - - return context - - @login_required def deletions(request): if request.method == "POST": @@ -4748,17 +2165,13 @@ def submit_pr(request): return render(request, "submit_pr.html") -# Global variable to store the vector store vector_store = None - -# Define the daily request limit as a variable DAILY_REQUEST_LIMIT = 10 @api_view(["POST"]) def chatbot_conversation(request): try: - # Rate Limit Check today = datetime.now(timezone.utc).date() rate_limit_key = f"global_daily_requests_{today}" request_count = cache.get(rate_limit_key, 0) @@ -4776,7 +2189,6 @@ def chatbot_conversation(request): ChatBotLog.objects.create(question=question, answer="Error: Invalid API Key") return Response({"error": "Invalid API Key"}, status=status.HTTP_400_BAD_REQUEST) - # Apply validation for question if not question or not isinstance(question, str): ChatBotLog.objects.create(question=question, answer="Error: Invalid question") return Response({"error": "Invalid question"}, status=status.HTTP_400_BAD_REQUEST) @@ -4809,9 +2221,7 @@ def chatbot_conversation(request): status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - # Handle the "exit" command if question.lower() == "exit": - # if buffer is present in the session then delete it if "buffer" in request.session: del request.session["buffer"] return Response({"answer": "Conversation memory cleared."}, status=status.HTTP_200_OK) @@ -4823,22 +2233,26 @@ def chatbot_conversation(request): try: response = crc.invoke({"question": question}) except Exception as e: - error_message = f"Error: {str(e)}" - ChatBotLog.objects.create(question=question, answer=error_message) - return Response({"error": error_message}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - # Increment the request count + ChatBotLog.objects.create(question=question, answer=f"Error: {str(e)}") + return Response( + {"error": "An internal error has occurred."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) cache.set(rate_limit_key, request_count + 1, timeout=86400) # Timeout set to one day request.session["buffer"] = memory.buffer - # Log the conversation ChatBotLog.objects.create(question=question, answer=response["answer"]) return Response({"answer": response["answer"]}, status=status.HTTP_200_OK) except Exception as e: - error_message = f"Error: {str(e)}" - ChatBotLog.objects.create(question=request.data.get("question", ""), answer=error_message) - return Response({"error": error_message}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + ChatBotLog.objects.create( + question=request.data.get("question", ""), answer=f"Error: {str(e)}" + ) + return Response( + {"error": "An internal error has occurred."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) def weekly_report(request): @@ -5003,3 +2417,145 @@ def add_suggestions(request): def view_suggestions(request): suggestion = Suggestion.objects.all() return render(request, "feature_suggestion.html", {"suggestions": suggestion}) + + +def sizzle(request): + if not request.user.is_authenticated: + messages.error(request, "Please login to access the Sizzle page.") + return redirect("index") + + sizzle_data = None + + last_data = TimeLog.objects.filter(user=request.user).order_by("-created").first() + + if last_data: + all_data = TimeLog.objects.filter( + user=request.user, created__date=last_data.created.date() + ).order_by("created") + + total_duration = sum((entry.duration for entry in all_data if entry.duration), timedelta()) + + total_duration_seconds = total_duration.total_seconds() + formatted_duration = ( + f"{int(total_duration_seconds // 60)} min {int(total_duration_seconds % 60)} sec" + ) + + github_issue_url = all_data.first().github_issue_url + + issue_title = get_github_issue_title(github_issue_url) + + start_time = all_data.first().start_time.strftime("%I:%M %p") + date = last_data.created.strftime("%d %B %Y") + + sizzle_data = { + "issue_title": issue_title, + "duration": formatted_duration, + "start_time": start_time, + "date": date, + } + + return render(request, "sizzle/sizzle.html", {"sizzle_data": sizzle_data}) + + +def TimeLogListAPIView(request): + print(request.user) + if not request.user.is_authenticated: + return JsonResponse({"error": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED) + + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") + + if not start_date_str or not end_date_str: + return JsonResponse( + {"error": "Both start_date and end_date are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + start_date = parse_datetime(start_date_str) + end_date = parse_datetime(end_date_str) + + if not start_date or not end_date: + return JsonResponse({"error": "Invalid date format."}, status=status.HTTP_400_BAD_REQUEST) + + time_logs = TimeLog.objects.filter( + user=request.user, created__range=[start_date, end_date] + ).order_by("created") + + grouped_logs = defaultdict(list) + for log in time_logs: + date_str = log.created.strftime("%Y-%m-%d") + grouped_logs[date_str].append(log) + + response_data = [] + for date, logs in grouped_logs.items(): + first_log = logs[0] + total_duration = sum((log.duration for log in logs if log.duration), timedelta()) + + total_duration_seconds = total_duration.total_seconds() + formatted_duration = ( + f"{int(total_duration_seconds // 60)} min {int(total_duration_seconds % 60)} sec" + ) + + issue_title = get_github_issue_title(first_log.github_issue_url) + + start_time = first_log.start_time.strftime("%I:%M %p") + formatted_date = first_log.created.strftime("%d %B %Y") + + day_data = { + "issue_title": issue_title, + "duration": formatted_duration, + "start_time": start_time, + "date": formatted_date, + } + + response_data.append(day_data) + + return JsonResponse(response_data, safe=False, status=status.HTTP_200_OK) + + +def sizzle_docs(request): + return render(request, "sizzle/sizzle_docs.html") + + +@login_required +def TimeLogListView(request): + time_logs = TimeLog.objects.filter(user=request.user).order_by("-start_time") + active_time_log = time_logs.filter(end_time__isnull=True).first() + # print the all details of the active time log + token, created = Token.objects.get_or_create(user=request.user) + return render( + request, + "sizzle/time_logs.html", + {"time_logs": time_logs, "active_time_log": active_time_log, "token": token.key}, + ) + + +@login_required +def sizzle_daily_log(request): + try: + if request.method == "GET": + reports = DailyStatusReport.objects.filter(user=request.user).order_by("-date") + return render(request, "sizzle/sizzle_daily_status.html", {"reports": reports}) + + if request.method == "POST": + previous_work = request.POST.get("previous_work") + next_plan = request.POST.get("next_plan") + blockers = request.POST.get("blockers") + print(previous_work, next_plan, blockers) + + DailyStatusReport.objects.create( + user=request.user, + date=now().date(), + previous_work=previous_work, + next_plan=next_plan, + blockers=blockers, + ) + + messages.success(request, "Daily status report submitted successfully.") + return redirect("sizzle") + + except Exception as e: + messages.error(request, f"An error occurred: {e}") + return redirect("sizzle") + + return HttpResponseBadRequest("Invalid request method.")