diff --git a/backlogger/steam.py b/backlogger/steam.py new file mode 100644 index 0000000..898ad47 --- /dev/null +++ b/backlogger/steam.py @@ -0,0 +1,50 @@ +import re +import requests +from urllib.parse import urlencode + +STEAM_OPENID_URL = 'https://steamcommunity.com/openid/login' +STEAM_API_BASE = 'https://api.steampowered.com' + + +def build_auth_url(return_to, realm): + params = { + 'openid.ns': 'http://specs.openid.net/auth/2.0', + 'openid.mode': 'checkid_setup', + 'openid.return_to': return_to, + 'openid.realm': realm, + 'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select', + 'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select', + } + return f"{STEAM_OPENID_URL}?{urlencode(params)}" + + +def verify_and_get_steam_id(params): + """Verify OpenID assertion with Steam. Returns steam64 id string or None.""" + verify_params = {k: v for k, v in params.items()} + verify_params['openid.mode'] = 'check_authentication' + try: + resp = requests.post(STEAM_OPENID_URL, data=verify_params, timeout=10) + resp.raise_for_status() + except requests.RequestException: + return None + if 'is_valid:true' not in resp.text: + return None + claimed_id = params.get('openid.claimed_id', '') + match = re.search(r'/openid/id/(\d+)$', claimed_id) + return match.group(1) if match else None + + +def get_owned_games(api_key, steam_id): + """Return list of games sorted by playtime desc. Raises on API error.""" + url = f"{STEAM_API_BASE}/IPlayerService/GetOwnedGames/v1/" + params = { + 'key': api_key, + 'steamid': steam_id, + 'include_appinfo': 'true', + 'include_played_free_games': 'true', + 'format': 'json', + } + resp = requests.get(url, params=params, timeout=15) + resp.raise_for_status() + games = resp.json().get('response', {}).get('games', []) + return sorted(games, key=lambda g: g.get('playtime_forever', 0), reverse=True) diff --git a/backlogger/templates/backlogger/list.html b/backlogger/templates/backlogger/list.html index 1006a00..541dd46 100644 --- a/backlogger/templates/backlogger/list.html +++ b/backlogger/templates/backlogger/list.html @@ -164,7 +164,13 @@

Backlogger {{ items|length }}

- + Add item +
+ {% if request.GET.imported %} + ✓ {{ request.GET.imported }} game{{ request.GET.imported|pluralize }} imported + {% endif %} + ▶ Steam + + Add item +
diff --git a/backlogger/templates/backlogger/steam_import.html b/backlogger/templates/backlogger/steam_import.html new file mode 100644 index 0000000..1b43c4c --- /dev/null +++ b/backlogger/templates/backlogger/steam_import.html @@ -0,0 +1,248 @@ + + + + + + Import from Steam — Backlogger + + + + + + +
+ + {% if error %} +
{{ error }}
+
+

Want to try again?

+ + + Connect with Steam + +
+ + {% elif games %} +
+

Import from Steam {{ games|length }} games

+
+ +
+ {% csrf_token %} + +
+ + + +
+ + + + + + + + + + + {% for game in games %} + + + + + + {% endfor %} + +
GameHours played
+ {% if game.already_imported %} + + {% else %} + + {% endif %} + + {{ game.name }} + {% if game.already_imported %}already in backlog{% endif %} + + {{ game.hours }}h +
+ + +
+ + {% else %} +

Import from Steam

+
+

Connect your Steam account to import your library.

+ + + Connect with Steam + +
+ {% endif %} + +
+ + + + diff --git a/backlogger/urls.py b/backlogger/urls.py index c4978d7..b281a66 100644 --- a/backlogger/urls.py +++ b/backlogger/urls.py @@ -7,4 +7,7 @@ urlpatterns = [ path('add/', views.item_add, name='add'), path('/edit/', views.item_edit, name='edit'), path('/delete/', views.item_delete, name='delete'), + path('steam/login/', views.steam_login, name='steam_login'), + path('steam/callback/', views.steam_callback, name='steam_callback'), + path('steam/import/', views.steam_import, name='steam_import'), ] diff --git a/backlogger/views.py b/backlogger/views.py index 90c09b8..0926380 100644 --- a/backlogger/views.py +++ b/backlogger/views.py @@ -1,7 +1,10 @@ +from django.conf import settings from django.contrib.auth.decorators import login_required from django.shortcuts import render, get_object_or_404, redirect +from django.urls import reverse from .models import Item from .forms import ItemForm, SignupForm +from . import steam as steam_api def signup(request): @@ -77,3 +80,73 @@ def item_delete(request, pk): if request.method == 'POST': get_object_or_404(Item, pk=pk, user=request.user).delete() return redirect('backlogger:list') + + +@login_required +def steam_login(request): + callback = request.build_absolute_uri(reverse('backlogger:steam_callback')) + realm = f"{request.scheme}://{request.get_host()}" + return redirect(steam_api.build_auth_url(callback, realm)) + + +@login_required +def steam_callback(request): + steam_id = steam_api.verify_and_get_steam_id(request.GET.dict()) + if not steam_id: + return render(request, 'backlogger/steam_import.html', {'error': 'Steam verification failed. Please try again.'}) + + api_key = getattr(settings, 'STEAM_API_KEY', '') + if not api_key: + return render(request, 'backlogger/steam_import.html', {'error': 'Steam API key is not configured on the server.'}) + + try: + games = steam_api.get_owned_games(api_key, steam_id) + except Exception: + return render(request, 'backlogger/steam_import.html', {'error': 'Could not fetch your Steam library. Your profile may be set to private.'}) + + existing = set( + Item.objects.filter(user=request.user, category=Item.GAMES) + .values_list('name', flat=True) + ) + + game_list = [] + for g in games: + name = g.get('name', '') + hours = round(g.get('playtime_forever', 0) / 60, 1) + game_list.append({ + 'appid': g.get('appid'), + 'name': name, + 'hours': hours, + 'already_imported': name in existing, + }) + + request.session['steam_games'] = game_list + return render(request, 'backlogger/steam_import.html', {'games': game_list}) + + +@login_required +def steam_import(request): + if request.method != 'POST': + return redirect('backlogger:list') + + games_by_appid = {str(g['appid']): g for g in request.session.get('steam_games', [])} + selected = request.POST.getlist('appids') + + imported = 0 + for appid in selected: + game = games_by_appid.get(appid) + if not game or game['already_imported']: + continue + hours = game['hours'] + progress = min(100.0, hours) if hours > 0 else 0.0 + Item.objects.create( + user=request.user, + category=Item.GAMES, + name=game['name'], + hours_played=hours, + progress_percent=progress, + ) + imported += 1 + + del request.session['steam_games'] + return redirect(f"{reverse('backlogger:list')}?category=games&imported={imported}") diff --git a/kboris/settings.py b/kboris/settings.py index 0806b9d..72239a8 100644 --- a/kboris/settings.py +++ b/kboris/settings.py @@ -5,6 +5,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'dev-insecure-key') DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true' +STEAM_API_KEY = os.environ.get('STEAM_API_KEY', '') ALLOWED_HOSTS = [ 'k-boris.tech',