import logging
from contextlib import suppress
from datetime import datetime
from typing_extensions import Self
from ._util import _regex_it
from .api import _APIClient
from .base import CampaignObject
from .club import Club
from .config import get_from_cache, set_in_cache
from .constants import _TMIO
from .errors import TMIOException
from .player import Player
from .tmmap import TMMap
_log = logging.getLogger(__name__)
__all__ = (
"OfficialCampaignMedia",
"CampaignSearchResult",
"CampaignLeaderboard",
"Campaign",
)
[docs]class CampaignSearchResult(CampaignObject):
"""
.. versionadded :: 0.5
Represents a CampaignSearchResult.
Parameters
----------
club_id : int
The ID of the club that created the campaign.
date : datetime
The date the campaign was created.
campaign_id : int
The ID of the campaign.
map_count : int
The number of maps in the campaign.
name : str
The name of the campaign.
"""
def __init__(
self,
club_id: int,
date: datetime,
campaign_id: int,
map_count: int,
name: int,
):
self.club_id = club_id
self.date = date
self.campaign_id = campaign_id
self.map_count = map_count
self.name = name
@classmethod
def _from_dict(cls: Self, raw_data: dict) -> Self:
_log.debug("Creating a CampaignSearchResult class from given dictionary")
club_id = raw_data.get("clubid", 0)
date = datetime.utcfromtimestamp(raw_data.get("timestamp", 0))
campaign_id = raw_data.get("id", 0)
map_count = raw_data.get("mapcount", 1)
name = _regex_it(raw_data.get("name"))
args = [
club_id,
date,
campaign_id,
map_count,
name,
]
return cls(*args)
[docs] def get_campaign(self):
"""
.. versionadded :: 0.5
Gets the campaign object.
Returns
-------
:class:`Campaign`
The campaign object.
"""
return Campaign.get_campaign(self.campaign_id, self.club_id)
[docs]class CampaignLeaderboard(CampaignObject):
"""
.. versionadded :: 0.5
Represents a player on the Campaign's point leaderboards
Parameters
----------
player_name : str
The name of the player who got the position.
player_id : str
The ID of the player who got the position.
player_tag : str | None
The tag of the player who got the position.
position : int
The position the player achieved.
points : int
The points the player has
"""
def __init__(
self,
player_name: str,
player_id: str,
player_tag: str | None,
position: int,
points: int,
):
self.player_name = player_name
self.player_id = player_id
self.player_tag = player_tag
self.position = position
self.points = points
@classmethod
def _from_dict(cls: Self, raw_data: dict) -> Self:
player_name = raw_data["player"]["name"]
player_id = raw_data["player"]["id"]
player_tag = _regex_it(raw_data["player"].get("tag", None))
position = raw_data["position"]
points = raw_data["points"]
args = [
player_name,
player_id,
player_tag,
position,
points,
]
return cls(*args)
[docs] async def player(self) -> Player:
"""
.. versionadded :: 0.5
Gets the player this position belongs to.
Returns
-------
:class:`Player`
The player that holds this position.
"""
return await Player.get_player(self.player_id)
[docs]class Campaign(CampaignObject):
"""
.. versionadded :: 0.5
Represents a Campaign in Trackmania 2020.
Parameters
----------
campaign_id : int
The campaign's ID
club_id : int
The club's id.
image : str | None
The image URL of the campaign.
Returns the decal image if this is an official campaign.
NoneType if there is no decal image.
is_official : bool
Whether the camapaign is official (made by Nadeo).
leaderboard_uid : str
The campaign's leaderboard id.
maps : :class:`list[TMMap]`
The maps in the campaign.
map_count : int
The number of maps in the campaign.
media : :class:`OfficialCampaignMedia` | None
The media of the campaign only if it is on official campaign.
name : str
The name of the campaign
"""
def __init__(
self,
campaign_id: int,
club_id: int,
image: str,
is_official: bool,
leaderboard_uid: str,
maps: list[TMMap],
map_count: int,
media: OfficialCampaignMedia | None,
name: str,
):
self.campaign_id = campaign_id
self.club_id = club_id
self.image = image
self.is_official = is_official
self.leaderboard_uid = leaderboard_uid
self.maps = maps
self.map_count = map_count
self.media = media
self.name = name
@classmethod
def _from_dict(cls: Self, raw_data: dict, official: bool = False) -> Self:
_log.debug("Creating a Campaign class from given dictionary")
campaign_id = raw_data.get("id")
club_id = raw_data.get("clubid", 0)
image = raw_data.get("media") if raw_data.get("media") != "" else None
is_official = official
leaderboard_uid = raw_data.get("leaderboarduid")
maps = [TMMap._from_dict(map_data) for map_data in raw_data.get("playlist", [])]
map_count = len(raw_data.get("playlist", []))
if raw_data.get("mediae") is not None:
media = OfficialCampaignMedia._from_dict(raw_data.get("mediae"))
else:
media = None
name = _regex_it(raw_data.get("name"))
args = [
campaign_id,
club_id,
image,
is_official,
leaderboard_uid,
maps,
map_count,
media,
name,
]
return cls(*args)
[docs] @classmethod
async def get_campaign(cls: Self, campaign_id: int, club_id: int) -> Self | None:
"""
.. versionadded :: 0.5
Gets a campaign with the given campaign and club ids.
If it's an official campaign, the `club_id` should be = 0
Parameters
----------
campaign_id : int
The campaign's id.
club_id : int
The club the campaign belongs to.
= 0 if the campaign is an official nadeo campaign.
Returns
-------
:class:`Campaign` | None
The campaign object, None if it does not exist
"""
official = True if club_id == 0 else False
campaign_data = get_from_cache(f"campaign:{campaign_id}:{club_id}")
if campaign_data is not None:
return cls._from_dict(campaign_data, official=official)
api_client = _APIClient()
if club_id != 0:
campaign_data = await api_client.get(
_TMIO.build([_TMIO.TABS.CAMPAIGN, club_id, campaign_id])
)
else:
campaign_data = await api_client.get(
_TMIO.build([_TMIO.TABS.OFFICIAL_CAMPAIGN, campaign_id])
)
await api_client.close()
set_in_cache(f"campaign:{campaign_id}:{club_id}", campaign_data, ex=432000)
return cls._from_dict(campaign_data, official=official)
[docs] @classmethod
async def current_season(cls: Self) -> Self:
"""
.. versionadded :: 0.5
Gets the current seasonal campaign.
Returns
-------
:class:`Campaign`
The campaign.
"""
api_client = _APIClient()
campaign_data = await api_client.get(_TMIO.build([_TMIO.TABS.CAMPAIGNS, 0]))
await api_client.close()
with suppress(KeyError, TypeError):
raise TMIOException(campaign_data["error"])
campaign_id = campaign_data.get("campaigns", [])[0].get("id")
return await cls.get_campaign(campaign_id, 0)
[docs] @staticmethod
async def official_campaigns() -> list[CampaignSearchResult]:
"""
.. versionadded :: 0.5
Gets all official nadeo campaigns.
Returns
-------
:class:`list[CampaignSearchResult]`
The list of campaigns.
"""
official_campaigns = []
all_campaigns = get_from_cache("campaigns:all:0")
if all_campaigns is not None:
for campaign in all_campaigns.get("campaigns", []):
if campaign.get("clubid", -1) == 0:
official_campaigns.append(CampaignSearchResult._from_dict(campaign))
return official_campaigns
api_client = _APIClient()
all_campaigns = await api_client.get(_TMIO.build([_TMIO.TABS.CAMPAIGNS, 0]))
await api_client.close()
with suppress(KeyError, TypeError):
raise TMIOException(all_campaigns["error"])
set_in_cache("campaigns:all:0", all_campaigns, ex=432000)
for campaign in all_campaigns.get("campaigns", []):
if campaign.get("id", -1) == 0:
official_campaigns.append(CampaignSearchResult._from_dict(campaign))
return official_campaigns
[docs] @staticmethod
async def popular_campaigns(page: int = 0) -> list[CampaignSearchResult]:
"""
.. versionadded :: 0.5
Gets all popular campaigns, official campaigns excluded.
50 Results / Page (except Page 1 because of the official campaigns)
Parameters
----------
page : int, optional
The page number, by default 0
Returns
-------
:class:`list[CampaignSearchResult]`
The list of campaigns.
"""
campaigns_list = []
all_campaigns = get_from_cache(f"campaigns:all:{page}")
if all_campaigns is not None:
for campaign in all_campaigns.get("campaigns", []):
if campaign.get("clubid", -1) != 0:
campaigns_list.append(CampaignSearchResult._from_dict(campaign))
api_client = _APIClient()
all_campaigns = await api_client.get(_TMIO.build([_TMIO.TABS.CAMPAIGNS, page]))
await api_client.close()
with suppress(KeyError, TypeError):
raise TMIOException(all_campaigns["error"])
for campaign in all_campaigns.get("campaigns", []):
if campaign.get("clubid", -1) != 0:
campaigns_list.append(CampaignSearchResult._from_dict(campaign))
return campaigns_list
[docs] async def club(self) -> Club:
"""
.. versionadded :: 0.5
The club the Campaign belongs to.
Returns
-------
:class:`Club` | None
The Club the campaign belongs to. If it does not belong to a campaign returns None.
"""
if self.club_id == 0:
return None
else:
return await Club.get_club(self.club_id)
[docs] async def leaderboards(
self, offset: int = 0, length: int = 100
) -> list[CampaignLeaderboard]:
"""
.. versionadded :: 0.5
Gets the points leaderboards for the campaign.
Parameters
----------
offset : int, optional
The leaderboard offset to start at, by default 0
length : int, optional
The length of the leaderboard to get. Can only be between 0 and 100, by default 100
Returns
-------
:class:`list[CampaignLeaderboard]`
The leaderboard of the campaign based on points.
"""
leaderboards = []
leaderboard_data = get_from_cache(
f"campaign:{self.campaign_id}:{offset}:{length}"
)
if leaderboard_data is not None:
for lb_place in leaderboard_data.get("tops", []):
leaderboards.append(CampaignLeaderboard._from_dict(lb_place))
return leaderboards
api_client = _APIClient()
leaderboard_data = await api_client.get(
_TMIO.build([_TMIO.TABS.LEADERBOARD, self.leaderboard_uid])
+ f"?offset={offset}&length={length}"
)
await api_client.close()
with suppress(KeyError, TypeError):
raise TMIOException(leaderboard_data["error"])
set_in_cache(
f"campaign:{self.campaign_id}:{offset}:{length}",
leaderboard_data,
ex=432000,
)
for lb_place in leaderboard_data.get("tops", []):
leaderboards.append(CampaignLeaderboard._from_dict(lb_place))
return leaderboards
[docs] def get_map(self: Self, index: int = 0) -> TMMap:
"""
.. versionadded :: 0.5
Gets a map at a specific index.
Parameters
----------
index : int, optional
The index of the map requested, by default 0
Returns
-------
:class:`TMMap`
The map at the index.
"""
return self.maps[index]