This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class BattleNetAccount(ProviderAccount):
def to_str(self):
battletag = self.account.extra_data.get("battletag")
return battletag or super(BattleNetAccount, self).to_str()
class BattleNetProvider(OAuth2Provider):
id = "battlenet"
name = "Battle.net"
account_class = BattleNetAccount
def extract_uid(self, data):
uid = str(data["id"])
if data.get("region") == "cn":
# China is on a different account system. UIDs can clash with US.
return uid + "-cn"
return uid
def extract_common_fields(self, data):
return {"username": data.get("battletag")}
def get_default_scope(self):
# Optional scopes: "sc2.profile", "wow.profile"
return []
provider_classes = [BattleNetProvider]

View File

@@ -0,0 +1,65 @@
import json
from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.providers.oauth2.client import OAuth2Error
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase
from .provider import BattleNetProvider
from .views import _check_errors
class BattleNetTests(OAuth2TestsMixin, TestCase):
provider_id = BattleNetProvider.id
_uid = 123456789
_battletag = "LuckyDragon#1953"
def get_mocked_response(self):
data = {"battletag": self._battletag, "id": self._uid}
return MockedResponse(200, json.dumps(data))
def test_valid_response_no_battletag(self):
data = {"id": 12345}
response = MockedResponse(200, json.dumps(data))
self.assertEqual(_check_errors(response), data)
def test_invalid_data(self):
response = MockedResponse(200, json.dumps({}))
with self.assertRaises(OAuth2Error):
# No id, raises
_check_errors(response)
def test_profile_invalid_response(self):
data = {"code": 403, "type": "Forbidden", "detail": "Account Inactive"}
response = MockedResponse(401, json.dumps(data))
with self.assertRaises(OAuth2Error):
# no id, 4xx code, raises
_check_errors(response)
def test_error_response(self):
body = json.dumps({"error": "invalid_token"})
response = MockedResponse(400, body)
with self.assertRaises(OAuth2Error):
# no id, 4xx code, raises
_check_errors(response)
def test_service_not_found(self):
response = MockedResponse(596, "<h1>596 Service Not Found</h1>")
with self.assertRaises(OAuth2Error):
# bad json, 5xx code, raises
_check_errors(response)
def test_invalid_response(self):
response = MockedResponse(200, "invalid json data")
with self.assertRaises(OAuth2Error):
# bad json, raises
_check_errors(response)
def test_extra_data(self):
self.login(self.get_mocked_response())
account = SocialAccount.objects.get(uid=str(self._uid))
self.assertEqual(account.extra_data["battletag"], self._battletag)
self.assertEqual(account.extra_data["id"], self._uid)
self.assertEqual(account.extra_data["region"], "us")

View File

@@ -0,0 +1,6 @@
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from .provider import BattleNetProvider
urlpatterns = default_urlpatterns(BattleNetProvider)

View File

@@ -0,0 +1,4 @@
from django.core.validators import RegexValidator
BattletagUsernameValidator = RegexValidator(r"^[\w.]+#\d+$")

View File

@@ -0,0 +1,152 @@
"""
OAuth2 Adapter for Battle.net
Resources:
* Battle.net OAuth2 documentation:
https://dev.battle.net/docs/read/oauth
* Battle.net API documentation:
https://dev.battle.net/io-docs
* Original announcement:
https://us.battle.net/en/forum/topic/13979297799
* The Battle.net API forum:
https://us.battle.net/en/forum/15051532/
"""
import requests
from django.conf import settings
from allauth.socialaccount.providers.oauth2.client import OAuth2Error
from allauth.socialaccount.providers.oauth2.views import (
OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView,
)
from .provider import BattleNetProvider
class Region:
APAC = "apac"
CN = "cn"
EU = "eu"
KR = "kr"
SEA = "sea"
TW = "tw"
US = "us"
def _check_errors(response):
try:
data = response.json()
except ValueError: # JSONDecodeError on py3
raise OAuth2Error("Invalid JSON from Battle.net API: %r" % (response.text))
if response.status_code >= 400 or "error" in data:
# For errors, we expect the following format:
# {"error": "error_name", "error_description": "Oops!"}
# For example, if the token is not valid, we will get:
# {
# "error": "invalid_token",
# "error_description": "Invalid access token: abcdef123456"
# }
# For the profile API, this may also look like the following:
# {"code": 403, "type": "Forbidden", "detail": "Account Inactive"}
error = data.get("error", "") or data.get("type", "")
desc = data.get("error_description", "") or data.get("detail", "")
raise OAuth2Error("Battle.net error: %s (%s)" % (error, desc))
# The expected output from the API follows this format:
# {"id": 12345, "battletag": "Example#12345"}
# The battletag is optional.
if "id" not in data:
# If the id is not present, the output is not usable (no UID)
raise OAuth2Error("Invalid data from Battle.net API: %r" % (data))
return data
class BattleNetOAuth2Adapter(OAuth2Adapter):
"""
OAuth2 adapter for Battle.net
https://dev.battle.net/docs/read/oauth
Region is set to us by default, but can be overridden with the
`region` GET parameter when performing a login.
Can be any of eu, us, kr, sea, tw or cn
"""
provider_id = BattleNetProvider.id
valid_regions = (
Region.APAC,
Region.CN,
Region.EU,
Region.KR,
Region.SEA,
Region.TW,
Region.US,
)
@property
def battlenet_region(self):
# Check by URI query parameter first.
region = self.request.GET.get("region", "").lower()
if region == Region.SEA:
# South-East Asia uses the same region as US everywhere
return Region.US
if region in self.valid_regions:
return region
# Second, check the provider settings.
region = (
getattr(settings, "SOCIALACCOUNT_PROVIDERS", {})
.get("battlenet", {})
.get("REGION", "us")
)
if region in self.valid_regions:
return region
return Region.US
@property
def battlenet_base_url(self):
region = self.battlenet_region
if region == Region.CN:
return "https://www.battlenet.com.cn"
return "https://%s.battle.net" % (region)
@property
def access_token_url(self):
return self.battlenet_base_url + "/oauth/token"
@property
def authorize_url(self):
return self.battlenet_base_url + "/oauth/authorize"
@property
def profile_url(self):
return self.battlenet_base_url + "/oauth/userinfo"
def complete_login(self, request, app, token, **kwargs):
params = {"access_token": token.token}
response = requests.get(self.profile_url, params=params)
data = _check_errors(response)
# Add the region to the data so that we can have it in `extra_data`.
data["region"] = self.battlenet_region
return self.get_provider().sociallogin_from_response(request, data)
def get_callback_url(self, request, app):
r = super(BattleNetOAuth2Adapter, self).get_callback_url(request, app)
region = request.GET.get("region", "").lower()
# Pass the region down to the callback URL if we specified it
if region and region in self.valid_regions:
r += "?region=%s" % (region)
return r
oauth2_login = OAuth2LoginView.adapter_view(BattleNetOAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(BattleNetOAuth2Adapter)