diff --git a/Spybot2/settings.py b/Spybot2/settings.py index a596d83..fa7aa0b 100644 --- a/Spybot2/settings.py +++ b/Spybot2/settings.py @@ -66,6 +66,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.forms', + 'bootstrap4', ] MIDDLEWARE = [ @@ -217,4 +219,12 @@ # 'root': { # 'handlers': ['console'], # } -# } \ No newline at end of file +# } + +FORM_RENDERER = 'spybot.forms.CustomFormRenderer' + +BOOTSTRAP4 = { + 'include_jquery': False, + 'javascript_in_head': False, + 'label_class': 'form-label', +} \ No newline at end of file diff --git a/frontend/main.js b/frontend/main.js index 5dbc186..0f6e57b 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -5,6 +5,7 @@ import * as passkeys from './passkeys'; import "@tabler/core/dist/css/tabler.min.css" import "@tabler/core/dist/css/tabler-vendors.min.css" +import './modal' window.passkeys = passkeys; diff --git a/frontend/modal.js b/frontend/modal.js new file mode 100644 index 0000000..e140aa4 --- /dev/null +++ b/frontend/modal.js @@ -0,0 +1,20 @@ + +const modal = new bootstrap.Modal(document.querySelector("#modal"), {}); + +htmx.on("htmx:afterSwap", (e) => { + if (e.detail.target.id === "dialog") { + modal.show() + } +}) + +htmx.on("htmx:beforeSwap", (e) => { + // Empty response targeting #dialog => hide the modal + if (e.detail.target.id === "dialog" && !e.detail.xhr.response) { + modal.hide() + e.detail.shouldSwap = false + } +}) + +htmx.on("hidden.bs.modal", () => { + document.getElementById("dialog").innerHTML = "" +}) diff --git a/poetry.lock b/poetry.lock index d8fef9d..2f44e78 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "asgiref" version = "3.8.1" description = "ASGI specs, helper code, and adapters" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -18,6 +19,7 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] name = "bcrypt" version = "4.1.3" description = "Modern password hashing for your software and your servers" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -54,10 +56,33 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +category = "main" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "certifi" version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -69,6 +94,7 @@ files = [ name = "cffi" version = "1.16.0" description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -133,6 +159,7 @@ pycparser = "*" name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -232,6 +259,7 @@ files = [ name = "cryptography" version = "42.0.8" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -286,6 +314,7 @@ test-randomorder = ["pytest-randomly"] name = "django" version = "4.2.13" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -302,10 +331,27 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-bootstrap4" +version = "24.3" +description = "Django extensions by Zostera" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_bootstrap4-24.3-py3-none-any.whl", hash = "sha256:b555d87740a571036f100ad6026b1f62aabcb913404fb7f08f521881019b14bc"}, + {file = "django_bootstrap4-24.3.tar.gz", hash = "sha256:819bc0ba7b25fcdeb12eb04353962436dbe95b228ba4cf4b49f5d3fee53692e1"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.8.0" +Django = ">=4.1" + [[package]] name = "django-crontab" version = "0.7.1" description = "dead simple crontab powered job scheduling for django" +category = "main" optional = false python-versions = "*" files = [ @@ -319,6 +365,7 @@ Django = ">=1.8" name = "django-environ" version = "0.11.2" description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." +category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -327,14 +374,15 @@ files = [ ] [package.extras] -develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] -docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +docs = ["furo (>=2021.8.17b43,<2021.9.0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] [[package]] name = "docopt" version = "0.6.2" description = "Pythonic argument parser, that will make you smile" +category = "main" optional = false python-versions = "*" files = [ @@ -345,6 +393,7 @@ files = [ name = "fido2" version = "1.1.3" description = "FIDO2/WebAuthn library for implementing clients and servers." +category = "main" optional = false python-versions = ">=3.8,<4.0" files = [ @@ -362,6 +411,7 @@ pcsc = ["pyscard (>=1.9,<3)"] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -373,6 +423,7 @@ files = [ name = "lxml" version = "5.2.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -531,6 +582,7 @@ source = ["Cython (>=3.0.10)"] name = "mysqlclient" version = "2.2.4" description = "Python interface to MySQL" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -549,6 +601,7 @@ files = [ name = "num2words" version = "0.5.13" description = "Modules to convert numbers to words. Easily extensible." +category = "main" optional = false python-versions = "*" files = [ @@ -563,6 +616,7 @@ docopt = ">=0.6.2" name = "paramiko" version = "3.4.0" description = "SSH2 protocol library" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -584,6 +638,7 @@ invoke = ["invoke (>=2.0)"] name = "pycparser" version = "2.22" description = "C parser in Python" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -595,6 +650,7 @@ files = [ name = "pynacl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -621,6 +677,7 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] name = "requests" version = "2.32.3" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -638,10 +695,23 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + [[package]] name = "sqlparse" version = "0.5.0" description = "A non-validating SQL parser." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -657,6 +727,7 @@ doc = ["sphinx"] name = "ts3" version = "2.0.0b3" description = "TS3 Server Query API and TS3 Client Query API" +category = "main" optional = false python-versions = "*" files = [ @@ -671,6 +742,7 @@ paramiko = "*" name = "tzdata" version = "2024.1" description = "Provider of IANA time zone data" +category = "main" optional = false python-versions = ">=2" files = [ @@ -682,6 +754,7 @@ files = [ name = "ua-parser" version = "0.18.0" description = "Python port of Browserscope's user agent parser" +category = "main" optional = false python-versions = "*" files = [ @@ -693,6 +766,7 @@ files = [ name = "unittest-xml-reporting" version = "3.2.0" description = "unittest-based test runner with Ant/JUnit like XML reporting." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -707,6 +781,7 @@ lxml = "*" name = "urllib3" version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -724,6 +799,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "user-agents" version = "2.2.0" description = "A library to identify devices (phones, tablets) and their capabilities by parsing browser user agent strings." +category = "main" optional = false python-versions = "*" files = [ @@ -737,4 +813,4 @@ ua-parser = ">=0.10.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "9c3d9437a5c87b270c756d42adc7902256660f66fd34b07bd58aa2266b4d3ca6" +content-hash = "2170d1d9041752540c0498ec3272debfe83af2cce00af973754e89aa34213943" diff --git a/pyproject.toml b/pyproject.toml index 842f732..f937a90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ num2words = "^0.5.12" requests = "^2.31.0" fido2 = "^1.1.3" user-agents = "^2.2.0" +django-bootstrap4 = "^24.3" [tool.poetry.group.test.dependencies] diff --git a/spybot/forms.py b/spybot/forms.py index f7631ac..77588d6 100644 --- a/spybot/forms.py +++ b/spybot/forms.py @@ -1,5 +1,12 @@ from django import forms +from django.core.exceptions import ValidationError +from django.forms.renderers import TemplatesSetting +from spybot.remote.steam_api import get_steam_user_playing_info, get_steam_account_info + + +class CustomFormRenderer(TemplatesSetting): + form_template_name = 'spybot/form_snippet.html' class TimeRangeForm(forms.Form): RANGES = ( @@ -25,3 +32,42 @@ def clean(self): cleaned_data[name] = field.initial return cleaned_data + + +class AddSteamIDForm(forms.Form): + steamid = forms.CharField(label="Account ID (what comes after https://steamcommunity.com/profiles/)", initial="123456", required=True) + name = forms.CharField(label="Account name") + + def clean_steamid(self): + cleaned_data = super().clean() + steamid = cleaned_data.get("steamid") + name = cleaned_data.get("name") + + steam_info = get_steam_account_info(steamid) + if steam_info is None: + raise ValidationError("Can't load steam information for this user") + + return cleaned_data + + + """ + +
+ + https://steamcommunity.com/profiles/ + + +
+ + + + + + + {% block content_body_end %} + {% endblock %} diff --git a/spybot/templates/spybot/form_snippet.html b/spybot/templates/spybot/form_snippet.html new file mode 100644 index 0000000..e9cd6c0 --- /dev/null +++ b/spybot/templates/spybot/form_snippet.html @@ -0,0 +1,35 @@ +{% for field in form %} +
+
+
+ {{ field.label_tag }} + {{ field }} +
+
+ {{ field.errors }} +
+{% endfor %} + +{##} +{##} +{#
#} +{#
#} +{#
#} +{# #} +{#
#} +{# #} +{# https://steamcommunity.com/profiles/#} +{# #} +{# #} +{#
#} +{#
#} +{#
#} +{#
#} +{#
#} +{#
#} +{#
#} +{# #} +{# #} +{#
#} +{#
#} +{#
#} \ No newline at end of file diff --git a/spybot/templates/spybot/home/recent_events/fragment.html b/spybot/templates/spybot/home/recent_events/fragment.html index 05d48aa..4155eee 100644 --- a/spybot/templates/spybot/home/recent_events/fragment.html +++ b/spybot/templates/spybot/home/recent_events/fragment.html @@ -6,7 +6,7 @@
- {{ event.text|make_list|first }} + {{ event.text|striptags|make_list|first }}
{{ event.text|safe }} diff --git a/spybot/templates/spybot/profile.html b/spybot/templates/spybot/profile.html deleted file mode 100644 index 3587a0f..0000000 --- a/spybot/templates/spybot/profile.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends 'spybot/base/base.html' %} -{% load util %} -{% load tabler_icons %} -{% load ts_filters %} - -{% block content %} -
-
-
-

Logged in as {{ logged_in_user.name }}

-
-
-
-
Passkeys
-
- - - - - - - - - - - - {% for key in passkeys %} - - - - - - - - - {% endfor %} -
NamePlatformLast usedCreated
{{ key.name }}{{ key.platform }}{{ key.last_used }}{{ key.added_on }}Delete
-
-
- -
- -
-
-
- - -{% endblock content %} - -{% block header %} - -{% endblock header %} \ No newline at end of file diff --git a/spybot/templates/spybot/profile/add_steamid_modal.html b/spybot/templates/spybot/profile/add_steamid_modal.html new file mode 100644 index 0000000..0eeec3f --- /dev/null +++ b/spybot/templates/spybot/profile/add_steamid_modal.html @@ -0,0 +1,56 @@ +{% load tabler_icons %} +{% load bootstrap4 %} + +
\ No newline at end of file diff --git a/spybot/templates/spybot/profile/profile.html b/spybot/templates/spybot/profile/profile.html new file mode 100644 index 0000000..c2d8f32 --- /dev/null +++ b/spybot/templates/spybot/profile/profile.html @@ -0,0 +1,71 @@ +{% extends 'spybot/base/base.html' %} +{% load util %} +{% load tabler_icons %} +{% load ts_filters %} + +{% block content %} +
+
+
+
+

Logged in as {{ logged_in_user.name }}

+
+
+
+
+
+
Passkeys
+
+ + + + + + + + + + + + {% for key in passkeys %} + + + + + + + + + {% endfor %} +
NamePlatformLast usedCreated
{{ key.name }}{{ key.platform }}{{ key.last_used }}{{ key.added_on }}Delete
+
+
+ +
+
+
+
+ {% include 'spybot/profile/steamids_fragment.html' %} +
+
+ +{% endblock content %} + +{% block content_body_end %} + +{% endblock content_body_end %} + +{% block header %} + +{% endblock header %} \ No newline at end of file diff --git a/spybot/templates/spybot/profile/steamids_fragment.html b/spybot/templates/spybot/profile/steamids_fragment.html new file mode 100644 index 0000000..b8e597d --- /dev/null +++ b/spybot/templates/spybot/profile/steamids_fragment.html @@ -0,0 +1,29 @@ +{% with data=profile_steamids %} +
+
Linked Steam Accounts
+
+ + + + + + + + + + {% for sid in data.steamids %} + + + + + + + {% endfor %} +
Account NameSteam ID
{{ sid.account_name }}{{ sid.steam_id }}Delete
+
+
+ +
+
+{% endwith %} \ No newline at end of file diff --git a/spybot/urls.py b/spybot/urls.py index 9737d58..b4011cb 100644 --- a/spybot/urls.py +++ b/spybot/urls.py @@ -17,6 +17,9 @@ path('recent_events_fragment', views.recent_events_fragment, name='recent_events_fragment'), path('profile', views.profile, name='profile'), path('profile/passkey/', views.profile_passkey, name='profile_passkey'), + path('profile/add_steamid', views.profile_add_steamid, name='profile_add_steamid'), + path('profile/steamid/', views.profile_delete_steamid, name='profile_delete_steamid'), + path('profile/steamids', views.profile_steamids_fragment, name='profile_steamids_fragment'), path('login', views.login, name='login'), path('login_teamspeak', views.login_teamspeak, name='login_teamspeak'), path('link_auth', auth.link_login, name='link_login'), diff --git a/spybot/views/fragments/profile_steamids.py b/spybot/views/fragments/profile_steamids.py new file mode 100644 index 0000000..9341a46 --- /dev/null +++ b/spybot/views/fragments/profile_steamids.py @@ -0,0 +1,15 @@ +from django.contrib.auth.decorators import login_required +from django.shortcuts import render + +from spybot.models import SteamID + + +@login_required +def profile_steamids_data(request): + steamids = SteamID.objects.filter(merged_user=request.user).all() + return { "profile_steamids" : { "steamids": steamids }} + + +@login_required +def fragment(request): + return render(request, 'spybot/', profile_steamids_data(request)) diff --git a/spybot/views/views.py b/spybot/views/views.py index cf626d9..caf056e 100644 --- a/spybot/views/views.py +++ b/spybot/views/views.py @@ -11,15 +11,17 @@ from spybot import visualization -from spybot.forms import TimeRangeForm +from spybot.forms import TimeRangeForm, AddSteamIDForm +from spybot.remote.steam_api import get_steam_user_playing_info from spybot.views.fragments.activity_chart import activity_chart_data -from spybot.models import TSChannel, TSUserActivity, NewsEvent, MergedUser, UserPasskey +from spybot.models import TSChannel, TSUserActivity, NewsEvent, MergedUser, UserPasskey, SteamID from spybot.templatetags import ts_filters from Spybot2 import settings import requests from spybot.views.common import get_user, get_context +from spybot.views.fragments.profile_steamids import profile_steamids_data def get_passkeys(user: MergedUser) -> List[UserPasskey]: @@ -268,18 +270,9 @@ def get_steam_game(mu: MergedUser): for sid in steam_ids: steam_id = sid.steam_id - #print(f"Trying steamID {steam_id} for user {mu.name}") - steam_api_key = settings.STEAM_API_KEY - req = "http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={key}&steamids={id}" \ - .format(key=steam_api_key, id=steam_id) - - steam_data = requests.get(req) - steam_info = steam_data.json().get('response').get('players')[0] - game_id = steam_info.get('gameid', 0) - game_name = steam_info.get('gameextrainfo', "") - if game_id != 0: + game_id, game_name = get_steam_user_playing_info(steam_id) + if game_id is not None: return game_id, game_name - return 0, "" @@ -295,8 +288,8 @@ def recent_events_fragment(request): def profile(request): user = get_user(request) passkeys = get_passkeys(user) - return render(request, 'spybot/profile.html', {**get_context(request), 'user': user, 'passkeys': passkeys}) - + steamids = profile_steamids_data(request) + return render(request, 'spybot/profile/profile.html', {**steamids, **get_context(request), 'user': user, 'passkeys': passkeys}) def login(request): user = get_user(request) @@ -319,3 +312,42 @@ def profile_passkey(request, id: str): passkey.delete() return HttpResponse('') return None + + +def create_steamid(user: MergedUser, steam_id: str, account_name:str): + sid = SteamID(steam_id=int(steam_id), account_name=account_name, merged_user=user) + sid.save() + + +@login_required +def profile_add_steamid(request): + user = get_user(request) + if request.method == "POST": + form = AddSteamIDForm(request.POST) + if form.is_valid(): + create_steamid(user, form.cleaned_data['steamid'], form.cleaned_data['name']) + return HttpResponse(status=204, headers={'HX-Trigger': 'steamids_changed'}) + else: + form = AddSteamIDForm() + return render(request, + 'spybot/profile/add_steamid_modal.html', + {**get_context(request), 'user': user, 'form': form} + ) + + +def profile_delete_steamid(request, id): + if request.method == "DELETE": + print(f"trying to delete steamid with id {id}") + steamid = get_object_or_404(SteamID, id=id) + if steamid.merged_user != request.user: + return HttpResponseForbidden() + + steamid.delete() + return HttpResponse(status=200, headers={'HX-Trigger': 'steamids_changed'}) + return None + + +def profile_steamids_fragment(request): + user = get_user(request) + steamids = profile_steamids_data(request) + return render(request, 'spybot/profile/steamids_fragment.html', {**steamids, **get_context(request), 'user': user})