Skip to content

Commit 9d8cde7

Browse files
authored
Merge pull request #9 from doluk/fullwarapi_public
Fullwarapi public
2 parents 38a5989 + aba4217 commit 9d8cde7

File tree

1 file changed

+221
-0
lines changed

1 file changed

+221
-0
lines changed

coc/ext/fullwarapi/__init__.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"""An extension that helps interact with the FullWar API."""
2+
3+
import asyncio
4+
import base64
5+
import logging
6+
import json
7+
8+
from collections import namedtuple
9+
from datetime import datetime
10+
from typing import List
11+
12+
import aiohttp
13+
14+
from ...http import json_or_text
15+
from ...utils import correct_tag
16+
from ...wars import ClanWar
17+
18+
LOG = logging.getLogger(__name__)
19+
20+
AccessToken = namedtuple("AccessToken", ["token", "expires_at"])
21+
22+
23+
def extract_expiry_from_jwt_token(token):
24+
if isinstance(token, str):
25+
token = token.encode("utf-8")
26+
elif not isinstance(token, bytes):
27+
# token was wrong somehow
28+
return None
29+
30+
try:
31+
signing, _ = token.rsplit(b".", 1)
32+
_, payload = signing.split(b".", 1)
33+
except ValueError:
34+
return None # not enough segments
35+
36+
if len(payload) % 4 > 0:
37+
payload += b"=" * (4 - len(payload) % 4)
38+
39+
bytes_payload = base64.urlsafe_b64decode(payload)
40+
dict_payload = json.loads(bytes_payload)
41+
try:
42+
expiry = dict_payload["exp"]
43+
return datetime.fromtimestamp(expiry)
44+
except KeyError:
45+
return None
46+
47+
48+
async def login(username: str, password: str, clash_client) -> "FullWarClient":
49+
"""Eases logging into the API client.
50+
51+
For more information on this project, please join the discord server - <discord.gg/Eaja7gJ>
52+
53+
You must have your username and password as given on the server.
54+
If unsure as to what this means, please reach out to an admin.
55+
56+
Parameters
57+
-----------
58+
username : str
59+
Your username as given on the discord server.
60+
password : str
61+
Your password as given on the discord server
62+
loop : Optional[:class:`asyncio.AbstractEventLoop`]
63+
The :class:`asyncio.AbstractEventLoop` to use for HTTP requests.
64+
An :func:`asyncio.get_event_loop()` will be used if ``None`` is passed
65+
"""
66+
if not isinstance(username, str) or not isinstance(password, str):
67+
raise TypeError("username and password must both be a string")
68+
if not username or not password:
69+
raise ValueError("username or password must not be an empty string.")
70+
71+
loop = asyncio.get_running_loop()
72+
return FullWarClient(username, password, clash_client, loop)
73+
74+
75+
class FullWarClient:
76+
"""An extension that helps interact with the Full War API.
77+
78+
For more information on this project, please join the discord server - <discord.gg/Eaja7gJ>
79+
80+
You must have your username and password as given on the server.
81+
If unsure as to what this means, please reach out to an admin.
82+
83+
Parameters
84+
-----------
85+
username : str
86+
Your username as given on the discord server.
87+
password : str
88+
Your password as given on the discord server
89+
clash_client: coc.Client
90+
Client to use
91+
92+
loop : Optional[:class:`asyncio.AbstractEventLoop`]
93+
The :class:`asyncio.AbstractEventLoop` to use for HTTP requests.
94+
An :func:`asyncio.get_event_loop()` will be used if ``None`` is passed
95+
96+
"""
97+
98+
BASE_URL = "https://fw-api.teamutils.com"
99+
100+
__slots__ = ("username", "password", "clash_client", "loop", "key", "http_session")
101+
102+
def __init__(self, username: str, password: str, clash_client, loop: asyncio.AbstractEventLoop = None):
103+
self.username = username
104+
self.password = password
105+
self.clash_client = clash_client
106+
107+
self.loop = loop or asyncio.get_event_loop()
108+
self.key = None # set in get_key()
109+
110+
self.http_session = aiohttp.ClientSession(loop=self.loop)
111+
112+
async def close(self):
113+
"""Close the client session established"""
114+
await self.http_session.close()
115+
116+
async def _request(self, method, url, *, token_request: bool = False, **kwargs):
117+
url = self.BASE_URL + url
118+
119+
if not token_request:
120+
key = await self._get_key()
121+
122+
headers = {"authorization": "Bearer {}".format(key)}
123+
kwargs["headers"] = headers
124+
125+
async with self.http_session.request(method, url, **kwargs) as response:
126+
LOG.debug("%s (%s) has returned %s", url, method, response.status)
127+
data = await json_or_text(response)
128+
LOG.debug(data)
129+
130+
if 200 <= response.status < 300:
131+
LOG.debug("%s has received %s", url, data)
132+
return data
133+
134+
if response.status == 401:
135+
await self._refresh_key()
136+
return await self._request(method, url, **kwargs)
137+
138+
async def _get_key(self):
139+
if not self.key or self.key.expires_at < datetime.utcnow():
140+
await self._refresh_key()
141+
142+
return self.key.token
143+
144+
async def _refresh_key(self):
145+
data = {
146+
"username": self.username,
147+
"password": self.password,
148+
}
149+
150+
payload = await self._request("POST", "/login", token_request=True, json=data)
151+
self.key = AccessToken(payload["access_token"], extract_expiry_from_jwt_token(payload["access_token"]))
152+
153+
async def war_result(self, clan_tag: str, preparation_start: int = 0) -> ClanWar:
154+
"""Get a stored war result.
155+
156+
Parameters
157+
----------
158+
client: coc.Client
159+
instance of the clash client
160+
clan_tag: str
161+
The clan tag to find war result for.
162+
preparation_start: int
163+
Preparation start of a specific war result to find.
164+
165+
Returns
166+
--------
167+
#NOOOOO idea if this is correct xD
168+
Optional[:class:`ClanWar`]
169+
War result, or ``None`` if no war found.
170+
"""
171+
data = await self._request("GET",
172+
f"/war_result?clan_tag={correct_tag(clan_tag, '%23')}"
173+
f"&prep_start={str(preparation_start)}")
174+
try:
175+
return ClanWar(data=data["response"], client=self.clash_client)
176+
except (IndexError, KeyError, TypeError, ValueError):
177+
return None
178+
179+
async def war_result_log(self, clan_tag: str, preparation_start: int = 0) -> List[ClanWar]:
180+
"""Get all stored war results for a clan.
181+
182+
Parameters
183+
----------
184+
client: coc.Client
185+
instance of the clash client
186+
clan_tag: str
187+
The clan tag to find war result for.
188+
preparation_start: int
189+
Preparation start of a specific war result to find.
190+
191+
Returns
192+
--------
193+
#NOOOOO idea if this is correct xD
194+
Optional[:class:`ClanWar`]
195+
List of war results, or ``None`` if no wars found.
196+
"""
197+
data = await self._request("GET",
198+
f"/war_result_log?clan_tag={correct_tag(clan_tag, '%23')}")
199+
try:
200+
responses = data["log"]
201+
202+
generator = (ClanWar(data=response["response"], client=self.clash_client) for response in responses)
203+
return generator
204+
except (IndexError, KeyError, TypeError, ValueError):
205+
return None
206+
207+
208+
async def register_war(self, clan_tag: str, preparation_start: int = 0):
209+
"""Registers a war.
210+
211+
Parameters
212+
----------
213+
clan_tag : str
214+
The clan to register a war for
215+
preparation_start: int
216+
Preparation time of the war
217+
"""
218+
return await self._request("POST",
219+
f"/war_result?clan_tag={correct_tag(clan_tag, '%23')}"
220+
f"&prep_start={str(preparation_start)}")
221+

0 commit comments

Comments
 (0)