nsdotpy.session
1# This file is part of NSDotPy, a wrapper around httpx that makes interacting 2# with the HTML nationstates.net site legally and efficiently easier. 3# 4# NSDotPy is free software: you can redistribute it and/or modify 5# it under the terms of the GNU Affero General Public License 6# as published by the Free Software Foundation either version 7# 3 of the License, or (at your option) any later version. 8# 9# NSDotPy is distributed in the hope that it will be useful but 10# WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12# See the GNU Affero General Public License for more details. 13# 14# You should have received a copy of the GNU Affero General Public License 15# along with NSDotPy. If not, see <https://www.gnu.org/licenses/>. 16 17# standard library imports 18import time # for ratelimiting and userclick 19import logging # for logging 20import logging.config # for logging configuration 21import mimetypes # for flag and banner uploading 22 23# external library imports 24import keyboard # for the required user input 25import httpx # for http stuff 26from bs4 import BeautifulSoup, Tag # for parsing html and xml 27from benedict import benedict 28 29# local imports 30from . import valid # for valid region tags 31 32 33def canonicalize(string: str) -> str: 34 """Converts a string to its canonical form used by the nationstates api. 35 36 Args: 37 string (str): The string to convert 38 39 Returns: 40 str: The canonical form of the string 41 """ 42 return string.lower().strip().replace(" ", "_") 43 44 45def uncanonicalize(string: str) -> str: 46 """Converts a string into a user friendly form from it's canonical nationstates api form. 47 48 Args: 49 string (str): The string to convert 50 51 Returns: 52 str: The uncanonicale form of the string 53 """ 54 return string.replace("_", " ").title() 55 56 57class NSSession: 58 def __init__( 59 self, 60 script_name: str, 61 script_version: str, 62 script_author: str, 63 script_user: str, 64 keybind: str = "space", 65 link_to_src: str = "", 66 logger: logging.Logger | None = None, 67 ): 68 """A wrapper around httpx that abstracts away 69 interacting with the HTML nationstates.net site. 70 Focused on legality, correctness, and ease of use. 71 72 Args: 73 script_name (str): Name of your script 74 script_version (str): Version number of your script 75 script_author (str): Author of your script 76 script_user (str): Nation name of the user running your script 77 keybind (str, optional): Keybind to count as a user click. Defaults to "space". 78 link_to_src (str, optional): Link to the source code of your script. 79 logger (logging.Logger | None, optional): Logger to use. Will create its own with name "NSDotPy" if none is specified. Defaults to None. 80 """ 81 self.VERSION = "2.3.0" 82 # Initialize logger 83 if not logger: 84 self._init_logger() 85 else: 86 self.logger = logger 87 # Create a new httpx session 88 self._session = httpx.Client( 89 http2=True, timeout=30 90 ) # ns can b slow, 30 seconds is hopefully a good sweet spot 91 # Set the user agent to the script name, version, author, and user as recommended in the script rules thread: 92 # https://forum.nationstates.net/viewtopic.php?p=16394966&sid=be37623536dbc8cee42d8d043945b887#p16394966 93 self._lock: bool = False 94 self._set_user_agent( 95 script_name, script_version, script_author, script_user, link_to_src 96 ) 97 # Initialize nationstates specific stuff 98 self._auth_region = "rwby" 99 self.chk: str = "" 100 self.localid: str = "" 101 self.pin: str = "" 102 self.nation: str = "" 103 self.region: str = "" 104 self.keybind = keybind 105 # Make sure the nations in the user agent actually exist 106 if not self._validate_nations({script_author, script_user}): 107 raise ValueError( 108 "One of, or both, of the nations in the user agent do not exist. Make sure you're only including the nation name in the constructor, e.g. 'Thorn1000' instead of 'Devved by Thorn1000'" 109 ) 110 self.logger.info(f"Initialized. Keybind to continue is {self.keybind}.") 111 112 def _validate_shard(self, shard, valid_shards, api): 113 """Helper function to validate a single shard.""" 114 if shard not in valid_shards: 115 raise ValueError(f"{shard} is not a valid shard for {api}") 116 117 def _validate_shards(self, api: str, shards: set[str]) -> None: 118 """Validates shards for a given API.""" 119 api_to_shard_set = { 120 "nation": valid.NATION_SHARDS 121 | valid.PRIVATE_NATION_SHARDS 122 | valid.PRIVATE_NATION_SHARDS, 123 "region": valid.REGION_SHARDS, 124 "world": valid.WORLD_SHARDS, 125 "wa": valid.WA_SHARDS, 126 } 127 128 valid_shards = api_to_shard_set.get(api) 129 if not valid_shards: 130 raise ValueError(f"Invalid API type: {api}") 131 132 for shard in shards: 133 self._validate_shard(shard, valid_shards, api) 134 135 def _set_user_agent( 136 self, 137 script_name: str, 138 script_version: str, 139 script_author: str, 140 script_user: str, 141 link_to_src: str, 142 ): 143 self.user_agent = ( 144 f"{script_name}/{script_version} (by:{script_author}; usedBy:{script_user})" 145 ) 146 if link_to_src: 147 self.user_agent = f"{self.user_agent}; src:{link_to_src}" 148 self.user_agent = f"{self.user_agent}; Written with NSDotPy/{self.VERSION} (by:Sweeze; src:github.com/sw33ze/NSDotPy)" 149 self._session.headers.update({"User-Agent": self.user_agent}) 150 151 def _validate_nations(self, nations: set[str]) -> bool: 152 """Checks if a list of nations exist using the NationStates API. 153 154 Args: 155 nations (set[str]): List of nations to check 156 157 Returns: 158 bool: True if all nations in the set exist, False otherwise. 159 """ 160 response = self.api_request("world", shard="nations") 161 world_nations = response.nations.split(",") 162 # check if all nations in the list exist in the world nations 163 return all(canonicalize(nation) in world_nations for nation in nations) 164 165 def _init_logger(self): 166 self.logger = logging.getLogger("NSDotPy") 167 config = { 168 "version": 1, 169 "disable_existing_loggers": False, 170 "formatters": { 171 "f": { 172 "format": "%(asctime)s %(message)s", 173 "datefmt": "%I:%M:%S %p", 174 } 175 }, 176 "handlers": { 177 "console": { 178 "class": "logging.StreamHandler", 179 "formatter": "f", 180 } 181 }, 182 "loggers": { 183 "NSDotPy": {"handlers": ["console"], "level": "INFO"}, 184 "httpx": {"handlers": ["console"], "level": "ERROR"}, 185 }, 186 } 187 logging.config.dictConfig(config) 188 189 def _get_auth_values(self, response: httpx.Response): 190 # make sure it's actually html first 191 if not response.headers["Content-Type"].startswith("text/html"): 192 return 193 # parse the html 194 soup = BeautifulSoup(response.text, "lxml") 195 # gathering chk and localid so i dont have to worry about authenticating l8r 196 if chk := soup.find("input", {"name": "chk"}): 197 self.chk = chk["value"].strip() # type: ignore 198 if localid := soup.find("input", {"name": "localid"}): 199 self.localid = localid["value"].strip() # type: ignore 200 if pin := self._session.cookies.get("pin"): 201 # you should never really need the pin but just in case i'll store it 202 # PAST ME WAS RIGHT, I NEEDED IT FOR THE PRIVATE API!! 203 self.pin = pin 204 if soup.find("a", {"class": "STANDOUT"}): 205 self.region = canonicalize( 206 soup.find_all("a", {"class": "STANDOUT"})[1].attrs["href"].split("=")[1] 207 ) 208 209 def _wait_for_input(self, key: str) -> int: 210 """Blocks execution until the user presses a key. Used as the one click = one request action. 211 212 Args: 213 key (str): The key to wait for 214 215 Returns: 216 int: Userclick parameter, milliseconds since the epoch""" 217 keyboard.wait(key) 218 # the trigger_on_release parameter is broken on windows 219 # because of a bug in keyboard so we have to do this 220 while keyboard.is_pressed(key): 221 pass 222 return int(time.time() * 1000) 223 224 def _get_detag_wfe(self) -> str: 225 """Gets the detagged WFE of the region you're in. 226 227 Returns: 228 str: The detagged WFE""" 229 self.logger.info(f"Getting detag WFE for {self.region}...") 230 response = self.request( 231 f"https://greywardens.xyz/tools/wfe_index/region={self.region}", 232 ) 233 soup = BeautifulSoup(response.text, "lxml") 234 # the safest bet for a detag wfe is the first wfe of the region 235 return soup.find_all("pre")[-1].text 236 237 def _validate_fields(self, data: dict): 238 max_lengths = { 239 "pretitle": 28, 240 "slogan": 55, 241 "currency": 40, 242 "animal": 40, 243 "demonym_noun": 44, 244 "demonym_adjective": 44, 245 "demonym_plural": 44, 246 } 247 248 # go through each key in the data dict and make sure they're below the max length 249 for key, value in data.items(): 250 if key not in max_lengths: 251 continue 252 if len(value) > max_lengths[key]: 253 raise ValueError(f"{key} is too long, max length is {max_lengths[key]}") 254 if len(value) < 2 and key != "slogan": 255 raise ValueError(f"{key} should have a minimum length of 2 characters.") 256 # check if pretitle contains any non-alphanumeric characters (except spaces) 257 if key == "pretitle" and not value.replace(" ", "").isalnum(): 258 raise ValueError( 259 "Pretitle should only contain alphanumeric characters or spaces." 260 ) 261 262 def _wait_for_ratelimit(self, head: dict, constant_rate_limit: bool): 263 if "X-Pin" in head: 264 self.pin = head["X-Pin"] 265 if waiting_time := head.get("Retry-After"): 266 self.logger.warning(f"Rate limited. Waiting {waiting_time} seconds.") 267 time.sleep(int(waiting_time)) 268 # slow down requests so we dont hit the rate limit in the first place 269 requests_left = int(head["RateLimit-Remaining"]) 270 if requests_left < 10 or constant_rate_limit: 271 seconds_until_reset = int(head["RateLimit-Reset"]) 272 time.sleep(seconds_until_reset / requests_left) 273 274 def _html_request( 275 self, url, data={}, files=None, follow_redirects=False 276 ) -> httpx.Response: 277 data |= {"chk": self.chk, "localid": self.localid} 278 userclick = self._wait_for_input(self.keybind) 279 # userclick is the number of milliseconds since the epoch, admin uses this for help enforcing the simultaneity rule 280 response = self._session.post( 281 f"{url}/userclick={userclick}", 282 data=data, 283 files=files, 284 follow_redirects=follow_redirects, 285 ) 286 if response.status_code >= 400: 287 with open("error.html", "w") as f: 288 f.write(response.text) 289 raise httpx.HTTPError( 290 f"Received status code {response.status_code} from {response.url}. Error page saved to error.html." 291 ) 292 self._get_auth_values(response) 293 return response 294 295 # --- end private methods --- # 296 297 def refresh_auth_values(self): 298 self.logger.info("Refreshing authentication values...") 299 response = self.request( 300 f"https://www.nationstates.net/page=display_region/region={self._auth_region}", 301 data={"theme": "century"}, 302 ) 303 self._get_auth_values(response) 304 305 def request( 306 self, 307 url: str, 308 data: dict = {}, 309 files: dict = {}, 310 follow_redirects: bool = False, 311 ) -> httpx.Response: 312 """Sends a request to the given url with the given data and files. 313 314 Args: 315 url (str): URL to send the request to 316 data (dict, optional): Payload to send with the request 317 files (dict, optional): Payload to send with requests that upload files 318 319 Returns: 320 httpx.Response: The response from the server 321 """ 322 if any( 323 banned_page in canonicalize(url) 324 for banned_page in { 325 "page=telegrams", 326 "page=dilemmas", 327 "page=compose_telegram", 328 "page=store", 329 "page=help", 330 } 331 ): 332 raise ValueError( 333 "You cannot use a tool to interact with telegrams, issues, getting help, or the store. Read up on the script rules: https://forum.nationstates.net/viewtopic.php?p=16394966#p16394966" 334 ) 335 if "api.cgi" in canonicalize(url): 336 # you should be using api_request for api requests 337 raise ValueError("You should be using api_request() for api requests.") 338 elif "nationstates" in canonicalize(url): 339 # do all the things that need to be done for html requests 340 if self._lock: 341 # if lock is true then we're already in the middle of a 342 # request and we're in danger of breaking the simultaneity rule 343 # so raise an error 344 raise PermissionError( 345 "You're already in the middle of a request. Stop trying to violate simultaneity." 346 ) 347 self._lock = True 348 response = self._html_request(url, data, files, follow_redirects) 349 self._lock = False 350 else: 351 # if its not nationstates then just pass the request through 352 response = self._session.post( 353 url, data=data, follow_redirects=follow_redirects 354 ) 355 return response 356 357 def api_request( 358 self, 359 api: str, 360 *, 361 target: str = "", 362 shard: str | set[str] = "", 363 password: str = "", 364 constant_rate_limit: bool = False, 365 ) -> benedict: 366 """Sends a request to the nationstates api with the given data. 367 368 Args: 369 api (str): The api to send the request to. Must be "nation", "region", "world", or "wa" 370 target (str, optional): The nation, region, or wa council to target. Required for non-world api requests. 371 shard (str, optional): The shard, or shards, you're requesting for. Must be a valid shard for the given api. Only required for world and wa api requests. 372 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 373 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 374 375 Returns: 376 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 377 """ 378 # TODO: probably move this responsibility to a third party api library to avoid reinventing the wheel 379 # if one exists of sufficient quality thats AGPLv3 compatible 380 if api not in {"nation", "region", "world", "wa"}: 381 raise ValueError("api must be 'nation', 'region', 'world', or 'wa'") 382 if api != "world" and not target: 383 raise ValueError("target must be specified for non-world api requests") 384 if api in {"wa", "world"} and not shard: 385 raise ValueError("shard must be specified for world and wa api requests") 386 # end argument validation 387 # shard validation 388 if type(shard) == str: 389 shard = {shard} 390 self._validate_shards(api, shard) # type: ignore 391 # end shard validation 392 data = { 393 "v": "12", 394 } 395 if api != "world": 396 data[api] = target 397 if shard: 398 data["q"] = "+".join(shard) 399 url = "https://www.nationstates.net/cgi-bin/api.cgi" 400 if password: 401 self._session.headers["X-Password"] = password 402 if self.pin: 403 self._session.headers["X-Pin"] = self.pin 404 # rate limiting section 405 response = self._session.post(url, data=data) 406 # if the server tells us to wait, wait 407 self._wait_for_ratelimit(response.headers, constant_rate_limit) 408 response.raise_for_status() 409 parsed_response = benedict.from_xml(response.text, keyattr_dynamic=True) 410 parsed_response.standardize() 411 parsed_response: benedict = parsed_response[api] # type: ignore 412 return parsed_response 413 414 def api_issue( 415 self, 416 nation: str, 417 issue: int, 418 option: int, 419 password: str = "", 420 constant_rate_limit: bool = False, 421 ) -> benedict: 422 """Answers an issue via the API. 423 424 Args: 425 nation (str): The nation to perform the command with. 426 issue (int): the ID of the issue. 427 option (int): the issue option to choose. 428 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 429 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 430 431 Returns: 432 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 433 """ 434 if not (password or self.pin): 435 raise ValueError("must specify authentication") 436 data = { 437 "v": "12", 438 "c": "issue", 439 "nation": canonicalize(nation), 440 "issue": issue, 441 "option": option, 442 } 443 url = "https://www.nationstates.net/cgi-bin/api.cgi" 444 if password: 445 self._session.headers["X-Password"] = password 446 if self.pin: 447 self._session.headers["X-Pin"] = self.pin 448 # rate limiting section 449 response = self._session.get(url, params=data) 450 # if the server tells us to wait, wait 451 self._wait_for_ratelimit(response.headers, constant_rate_limit) 452 response.raise_for_status() 453 parsed_response = benedict.from_xml(response.text, keyattr_dynamic=True) 454 parsed_response.standardize() 455 parsed_response: benedict = parsed_response["nation"] # type: ignore 456 return parsed_response 457 458 def api_command( 459 self, 460 nation: str, 461 command: str, 462 data: dict, 463 password: str = "", 464 mode: str = "", 465 constant_rate_limit: bool = False, 466 ) -> benedict: 467 """Sends a non-issue command to the nationstates api with the given data and password. 468 469 Args: 470 nation (str): The nation to perform the command with. 471 command (str): The command to perform. Must be "giftcard", "dispatch", "rmbpost" 472 data (str, optional): The unique data to send with the parameters of the command; consult the API docs for more information. 473 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 474 mode (str, optional): Whether to prepare or to execute the command. If value is given, does one of the two and returns result, if no value is given, does both and returns result of execute. 475 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 476 477 Returns: 478 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 479 """ 480 if command not in {"giftcard", "dispatch", "rmbpost"}: 481 raise ValueError("command must be 'giftcard', 'dispatch', or 'rmbpost'") 482 if not (password or self.pin): 483 raise ValueError("must specify authentication") 484 if mode not in {"", "prepare", "execute"}: 485 raise ValueError("mode must be prepare or execute") 486 data["v"] = "12" 487 data["nation"] = canonicalize(nation) 488 data["c"] = command 489 data["mode"] = mode if mode else "prepare" # if no mode than first prepare 490 url = "https://www.nationstates.net/cgi-bin/api.cgi" 491 if password: 492 self._session.headers["X-Password"] = password 493 if self.pin: 494 self._session.headers["X-Pin"] = self.pin 495 # rate limiting section 496 response = self._session.get(url, params=data) 497 # if the server tells us to wait, wait 498 self._wait_for_ratelimit(response.headers, constant_rate_limit) 499 response.raise_for_status() 500 parsed_response = benedict.from_xml(response.text, keyattr_dynamic=True) 501 parsed_response.standardize() 502 parsed_response: benedict = parsed_response["nation"] # type: ignore 503 if mode == "": 504 # if no mode was specified earlier, repeat command with execute and token 505 data["token"] = parsed_response["success"] 506 return self.api_command(nation, command, data, mode="execute") 507 else: 508 return parsed_response 509 510 def api_giftcard( 511 self, 512 nation: str, 513 card_id: int, 514 season: int, 515 recipient: str, 516 password: str = "", 517 constant_rate_limit: bool = False, 518 ) -> benedict: 519 """Gifts a card using the API. 520 521 Args: 522 nation (str): The nation to perform the command with. 523 card_id (int): The ID of the card to gift. 524 season (int): The season of the card to gift. 525 recipient (str): The nation to gift the card to. 526 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 527 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 528 529 Returns: 530 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 531 """ 532 data = {"cardid": card_id, "season": season, "to": canonicalize(recipient)} 533 return self.api_command( 534 nation, "giftcard", data, password, constant_rate_limit=constant_rate_limit 535 ) 536 537 def api_dispatch( 538 self, 539 nation: str, 540 action: str, 541 title: str = "", 542 text: str = "", 543 category: int = 0, 544 subcategory: int = 0, 545 dispatchid: int = 0, 546 password: str = "", 547 constant_rate_limit: bool = False, 548 ) -> benedict: 549 """Add, edit, or remove a dispatch. 550 551 Args: 552 nation (str): The nation to perform the command with. 553 action (str): The action to take. Must be "add", "edit", "remove" 554 title (str, optional): The dispatch title when adding or editing. 555 text (str, optional): The dispatch text when adding or editing. 556 category: (int, optional), The category ID when adding or editing. 557 subcategory (int, optional): The subcategory ID when adding or editing. 558 dispatchid (int, optional): The dispatch ID when editing or removing. 559 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 560 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 561 562 Returns: 563 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 564 """ 565 # TODO: maybe consider splitting these three functions? 566 # TODO: maybe create enums for category and subcategory 567 if action not in {"add", "edit", "remove"}: 568 raise ValueError("action must be 'add', 'edit', or 'remove'") 569 if action != "remove" and not all({title, text, category, subcategory}): 570 raise ValueError("must specify title, text, category, and subcategory") 571 if action != "add" and not dispatchid: 572 raise ValueError("must specify a dispatch id") 573 574 data = {"dispatch": action} 575 if title: 576 data["title"] = title 577 if text: 578 data["text"] = text 579 if category: 580 data["category"] = category 581 if subcategory: 582 data["subcategory"] = subcategory 583 if dispatchid: 584 data["dispatchid"] = dispatchid 585 return self.api_command( 586 nation, "dispatch", data, password, constant_rate_limit=constant_rate_limit 587 ) 588 589 def api_rmb( 590 self, 591 nation: str, 592 region: str, 593 text: str, 594 password: str = "", 595 constant_rate_limit: bool = False, 596 ) -> benedict: 597 """Post a message on the regional message board via the API. 598 599 Args: 600 nation (str): The nation to perform the command with. 601 region (str): the region to post the message in. 602 text (str): the text to post. 603 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 604 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 605 606 Returns: 607 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 608 """ 609 data = {"region": region, "text": text} 610 return self.api_command( 611 nation, "rmbpost", data, password, constant_rate_limit=constant_rate_limit 612 ) 613 614 def login(self, nation: str, password: str) -> bool: 615 """Logs in to the nationstates site. 616 617 Args: 618 nation (str): Nation name 619 password (str): Nation password 620 621 Returns: 622 bool: True if login was successful, False otherwise 623 """ 624 self.logger.info(f"Logging in to {nation}") 625 url = f"https://www.nationstates.net/page=display_region/region={self._auth_region}" 626 # shoutouts to roavin for telling me i had to have page=display_region in the url so it'd work with a userclick parameter 627 628 data = { 629 "nation": canonicalize(nation), 630 "password": password, 631 "theme": "century", 632 "logging_in": "1", 633 "submit": "Login", 634 } 635 636 response = self.request(url, data) 637 638 soup = BeautifulSoup(response.text, "lxml") 639 # checks if the body tag has your nation name in it; if it does, you're logged in 640 if not soup.find("body", {"data-nname": canonicalize(nation)}): 641 return False 642 643 self.nation = canonicalize(nation) 644 return True 645 646 def change_nation_flag(self, flag_filename: str) -> bool: 647 """Changes the nation flag to the given image. 648 649 Args: 650 flag_filename (str): Filename of the flag to change to 651 652 Returns: 653 bool: True if the flag was changed, False otherwise 654 """ 655 self.logger.info(f"Changing flag on {self.nation}") 656 # THIS WAS SO FUCKING FRUSTRATING BUT IT WORKS NOW AND IM NEVER TOUCHING THIS BULLSHIT UNLESS NS BREAKS IT AGAIN 657 url = "https://www.nationstates.net/cgi-bin/upload.cgi" 658 659 data = { 660 "nationname": self.nation, 661 } 662 files = { 663 "file": ( 664 flag_filename, 665 open(flag_filename, "rb"), 666 mimetypes.guess_type(flag_filename)[0], 667 ) 668 } 669 670 response = self.request(url, data=data, files=files) 671 672 if "page=settings" in response.headers["location"]: 673 self.refresh_auth_values() 674 return True 675 elif "Just a moment..." in response.text: 676 self.logger.warning( 677 "Cloudflare blocked you idiot get fucked have fun with that like I had to lmaoooooooooo" 678 ) 679 return False 680 681 def change_nation_settings( 682 self, 683 *, 684 email: str = "", 685 pretitle: str = "", 686 slogan: str = "", 687 currency: str = "", 688 animal: str = "", 689 demonym_noun: str = "", 690 demonym_adjective: str = "", 691 demonym_plural: str = "", 692 new_password: str = "", 693 ) -> bool: 694 """Given a logged in session, changes customizable fields and settings of the logged in nation. 695 Variables must be explicitly named in the call to the function, e.g. "session.change_nation_settings(pretitle='Join Lily', currency='Join Lily')" 696 697 Args: 698 email (str, optional): New email for WA apps. 699 pretitle (str, optional): New pretitle of the nation. Max length of 28. Nation must have minimum population of 250 million. 700 slogan (str, optional): New Slogan/Motto of the nation. Max length of 55. 701 currency (str, optional): New currency of the nation. Max length of 40. 702 animal (str, optional): New national animal of the nation. Max length of 40. 703 demonym_noun (str, optional): Noun the nation will refer to its citizens as. Max length of 44. 704 demonym_adjective (str, optional): Adjective the nation will refer to its citizens as. Max length of 44. 705 demonym_plural (str, optional): Plural form of "demonym_noun". Max length of 44. 706 new_password (str, optional): New password to assign to the nation. 707 708 Returns: 709 bool: True if changes were successful, False otherwise. 710 """ 711 self.logger.info(f"Changing settings on {self.nation}") 712 url = "https://www.nationstates.net/template-overall=none/page=settings" 713 714 data = { 715 "type": pretitle, 716 "slogan": slogan, 717 "currency": currency, 718 "animal": animal, 719 "demonym2": demonym_noun, 720 "demonym": demonym_adjective, 721 "demonym2pl": demonym_plural, 722 "email": email, 723 "password": new_password, 724 "confirm_password": new_password, 725 "update": " Update ", 726 } 727 # remove keys that have empty values 728 data = {k: v for k, v in data.items() if v} 729 # make sure everything is following the proper length limits and only contains acceptable characters 730 self._validate_fields(data) 731 732 response = self.request(url, data) 733 return "Your settings have been successfully updated." in response.text 734 735 def move_to_region(self, region: str, password: str = "") -> bool: 736 """Moves the nation to the given region. 737 738 Args: 739 region (str): Region to move to 740 password (str, optional): Region password, if the region is passworded 741 742 Returns: 743 bool: True if the move was successful, False otherwise 744 """ 745 self.logger.info(f"Moving {self.nation} to {region}") 746 url = "https://www.nationstates.net/template-overall=none/page=change_region" 747 748 data = {"region_name": region, "move_region": "1"} 749 if password: 750 data["password"] = password 751 response = self.request(url, data) 752 753 if "Success!" in response.text: 754 self.region = canonicalize(region) 755 return True 756 return False 757 758 def vote(self, pollid: str, option: str) -> bool: 759 """Votes on a poll. 760 761 Args: 762 pollid (str): ID of the poll to vote on, e.g. "199747" 763 option (str): Option to vote for (starts at 0) 764 765 Returns: 766 bool: True if the vote was successful, False otherwise 767 """ 768 self.logger.info(f"Voting on poll {pollid} with {self.nation}") 769 url = f"https://www.nationstates.net/template-overall=none/page=poll/p={pollid}" 770 771 data = {"pollid": pollid, "q1": option, "poll_submit": "1"} 772 response = self.request(url, data) 773 774 return "Your vote has been lodged." in response.text 775 776 # below are functions that are related to the WA 777 778 def join_wa(self, nation: str, app_id: str) -> bool: 779 """Joins the WA with the given nation. 780 781 Args: 782 nation (str): Nation to join the WA with 783 app_id (str): ID of the WA application to use 784 785 Returns: 786 bool: True if the join was successful, False otherwise 787 """ 788 self.logger.info(f"Joining WA with {nation}") 789 url = "https://www.nationstates.net/cgi-bin/join_un.cgi" 790 791 data = {"nation": canonicalize(nation), "appid": app_id.strip()} 792 response = self.request(url, data) 793 794 if "?welcome=1" in response.headers["location"]: 795 # since we're just getting thrown into a cgi script, we'll have to manually grab authentication values 796 self.refresh_auth_values() 797 return True 798 return False 799 800 def resign_wa(self): 801 """Resigns from the WA. 802 803 Returns: 804 bool: True if the resignation was successful, False otherwise 805 """ 806 self.logger.info("Resigning from WA") 807 url = "https://www.nationstates.net/template-overall=none/page=UN_status" 808 809 data = {"action": "leave_UN", "submit": "1"} 810 response = self.request(url, data) 811 812 return "From this moment forward, your nation is on its own." in response.text 813 814 def apply_wa(self, reapply: bool = True) -> bool: 815 """Applies to the WA. 816 817 Args: 818 reapply (bool, optional): Whether to reapply if you've been sent an application that's still valid. Defaults to True. 819 820 Returns: 821 bool: True if the application was successful, False otherwise 822 """ 823 self.logger.info(f"Applying to WA with {self.nation}") 824 url = "https://www.nationstates.net/template-overall=none/page=UN_status" 825 826 data = {"action": "join_UN"} 827 if reapply: 828 data["resend"] = "1" 829 else: 830 data["submit"] = "1" 831 832 response = self.request(url, data) 833 return ( 834 "Your application to join the World Assembly has been received!" 835 in response.text 836 ) 837 838 def endorse(self, nation: str, endorse: bool = True) -> bool: 839 """Endorses the given nation. 840 841 Args: 842 nation (str): Nation to endorse 843 endorse (bool, optional): True=endorse, False=unendorse. Defaults to True. 844 845 Returns: 846 bool: True if the endorsement was successful, False otherwise 847 """ 848 self.logger.info( 849 f"{('Unendorsing', 'Endorsing')[endorse]} {nation} with {self.nation}" 850 ) 851 url = "https://www.nationstates.net/cgi-bin/endorse.cgi" 852 853 data = { 854 "nation": canonicalize(nation), 855 "action": "endorse" if endorse else "unendorse", 856 } 857 response = self.request(url, data) 858 859 return f"nation={canonicalize(nation)}" in response.headers["location"] 860 861 def clear_dossier(self) -> bool: 862 """Clears a logged in nation's dossier. 863 864 Returns: 865 bool: Whether it was successful or not 866 """ 867 868 self.logger.info(f"Clearing dossier on {self.nation}") 869 url = "https://www.nationstates.net/template-overall=none/page=dossier" 870 data = {"clear_dossier": "1"} 871 response = self.request(url, data) 872 873 return "Dossier cleared of nations." in response.text 874 875 def add_to_dossier(self, nations: list[str] | str) -> bool: 876 """Adds nations to the logged in nation's dossier. 877 878 Args: 879 nations (list[str] | str): List of nations to add, or a single nation 880 881 Returns: 882 bool: Whether it was successful or not 883 """ 884 885 self.logger.info(f"Adding {nations} to dossier on {self.nation}") 886 url = "https://www.nationstates.net/dossier.cgi" 887 data = { 888 "currentnation": canonicalize(self.nation), 889 "action_append": "Upload Nation Dossier File", 890 } 891 files = { 892 "file": ( 893 "dossier.txt", 894 "\n".join(nations).strip() if type(nations) is list else nations, 895 "text/plain", 896 ), 897 } 898 response = self.request(url, data, files=files) 899 900 self.refresh_auth_values() 901 return "appended=" in response.headers["location"] 902 903 def wa_vote(self, council: str, vote: str) -> bool: 904 """Votes on the current WA resolution. 905 906 Args: 907 council (str): Must be "ga" for general assembly, "sc" for security council. 908 vote (str): Must be "for" or "against". 909 910 Returns: 911 bool: Whether the vote was successful or not 912 """ 913 self.logger.info( 914 f"Voting {vote} on {council.upper()} resolution with {self.nation}" 915 ) 916 if council not in {"ga", "sc"}: 917 raise ValueError("council must be 'ga' or 'sc'") 918 if vote not in {"for", "against"}: 919 raise ValueError("vote must be 'for' or 'against'") 920 self.logger.info("Voting on WA resolution") 921 922 url = f"https://www.nationstates.net/template-overall=none/page={council}" 923 data = { 924 "vote": f"Vote {vote.capitalize()}", 925 } 926 response = self.request(url, data) 927 928 return "Your vote has been lodged." in response.text 929 930 def refound_nation(self, nation: str, password: str) -> bool: 931 """Refounds a nation. 932 933 Args: 934 nation (str): Name of the nation to refound 935 password (str): Password to the nation 936 937 Returns: 938 bool: Whether the nation was successfully refounded or not 939 """ 940 url = "https://www.nationstates.net/template-overall=none/" 941 data = { 942 "logging_in": "1", 943 "restore_password": password, 944 "restore_nation": "1", 945 "nation": nation, 946 } 947 response = self.request(url, data=data) 948 if response.status_code == 302: 949 self.nation = nation 950 self.refresh_auth_values() 951 return True 952 return False 953 954 # methods for region control 955 956 def create_region( 957 self, 958 region_name: str, 959 wfe: str, 960 *, 961 password: str = "", 962 frontier: bool = False, 963 executive_delegacy: bool = False, 964 ) -> bool: 965 """Creates a new region. 966 967 Args: 968 region_name (str): Name of the region 969 wfe (str): WFE of the region 970 password (str, optional): Password to the region. Defaults to "". 971 frontier (bool, optional): Whether or not the region is a frontier. Defaults to False. 972 executive_delegacy (bool, optional): Whether or not the region has an executive WA delegacy. Defaults to False. Ignored if frontier is True. 973 974 Returns: 975 bool: Whether the region was successfully created or not 976 """ 977 self.logger.info(f"Creating new region {region_name}") 978 url = "https://www.nationstates.net/template-overall=none/page=create_region" 979 data = { 980 "page": "create_region", 981 "region_name": region_name.strip(), 982 "desc": wfe.strip(), 983 "create_region": "1", 984 } 985 if password: 986 data |= {"pw": "1", "rpassword": password} 987 if frontier: 988 data |= {"is_frontier": "1"} 989 elif executive_delegacy: 990 data |= {"delegate_control": "1"} 991 response = self.request(url, data) 992 return "Success! You have founded " in response.text 993 994 def upload_to_region(self, type: str, filename: str) -> str: 995 """Uploads a file to the current region. 996 997 Args: 998 type (str): Type of file to upload. Must be "flag" or "banner". 999 filename (str): Name of the file to upload. e.g. "myflag.png" 1000 1001 Raises: 1002 ValueError: If type is not "flag" or "banner" 1003 1004 Returns: 1005 str: Empty string if the upload failed, otherwise the ID of the uploaded file 1006 """ 1007 self.logger.info(f"Uploading {filename} to {self.region}") 1008 if type not in {"flag", "banner"}: 1009 raise ValueError("type must be 'flag' or 'banner'") 1010 url = "https://www.nationstates.net/cgi-bin/upload.cgi" 1011 data = { 1012 "uploadtype": f"r{type}", 1013 "page": "region_control", 1014 "region": self.region, 1015 "expect": "json", 1016 } 1017 files = { 1018 f"file_upload_r{type}": ( 1019 filename, 1020 open(filename, "rb"), 1021 mimetypes.guess_type(filename)[0], 1022 ) 1023 } 1024 response = self.request(url, data, files=files) 1025 return "" if "id" not in response.json() else response.json()["id"] 1026 1027 def set_flag_and_banner( 1028 self, flag_id: str = "", banner_id: str = "", flag_mode: str = "" 1029 ) -> bool: 1030 """Sets the uploaded flag and/or banner for the current region. 1031 1032 Args: 1033 flag_id (str, optional): ID of the flag, uploaded with upload_to_region(). Defaults to "". 1034 banner_id (str, optional): ID of the banner, uploaded with upload_to_region(). Defaults to "". 1035 flagmode (str, optional): Must be "flag" which will have a shadow, or "logo" which will not, or "" to not change it. Defaults to "". 1036 1037 Raises: 1038 ValueError: If flagmode is not "flag", "logo", or "" 1039 1040 Returns: 1041 bool: Whether the change was successful or not 1042 """ 1043 if flag_mode not in {"flag", "logo", ""}: 1044 raise ValueError("flagmode must be 'flag', 'logo', or ''") 1045 self.logger.info(f"Setting flag and banner for {self.region}") 1046 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1047 data = { 1048 "newflag": flag_id, 1049 "newbanner": banner_id, 1050 "saveflagandbannerchanges": "1", 1051 "flagmode": flag_mode, 1052 } 1053 # remove entries with empty values 1054 data = {k: v for k, v in data.items() if v} 1055 1056 response = self.request(url, data) 1057 1058 return "Regional banner/flag updated!" in response.text 1059 1060 def change_wfe(self, wfe: str = "") -> bool: 1061 """Changes the WFE of the current region. 1062 1063 Args: 1064 wfe (str, optional): World Factbook Entry to change to. Defaults to the oldest WFE the region has, for detags. 1065 1066 Returns: 1067 bool: True if successful, False otherwise 1068 """ 1069 self.logger.info(f"Changing WFE for {self.region}") 1070 if not wfe: 1071 wfe = self._get_detag_wfe() # haku im sorry for hitting your site so much 1072 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1073 data = { 1074 "message": wfe.encode("iso-8859-1", "xmlcharrefreplace") 1075 .decode() 1076 .strip(), # lol. 1077 "setwfebutton": "1", 1078 } 1079 response = self.request(url, data) 1080 return "World Factbook Entry updated!" in response.text 1081 1082 # methods for embassies 1083 1084 def request_embassy(self, target: str) -> bool: 1085 """Requests an embassy with a region. 1086 1087 Args: 1088 target (str): The region to request the embassy with. 1089 1090 Returns: 1091 bool: Whether the request was successfully sent or not 1092 """ 1093 self.logger.info(f"Requesting embassy with {target}") 1094 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1095 data = { 1096 "requestembassyregion": target, 1097 "requestembassy": "1", # it's silly that requesting needs this but not closing, aborting, or cancelling 1098 } 1099 response = self.request(url, data) 1100 return "Your proposal for the construction of embassies with" in response.text 1101 1102 def close_embassy(self, target: str) -> bool: 1103 """Closes an embassy with a region. 1104 1105 Args: 1106 target (str): The region with which to close the embassy. 1107 1108 Returns: 1109 bool: Whether the embassy was successfully closed or not 1110 """ 1111 self.logger.info(f"Closing embassy with {target}") 1112 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1113 data = {"cancelembassyregion": target} 1114 response = self.request(url, data) 1115 return " has been scheduled for demolition." in response.text 1116 1117 def abort_embassy(self, target: str) -> bool: 1118 """Aborts an embassy with a region. 1119 1120 Args: 1121 target (str): The region with which to abort the embassy. 1122 1123 Returns: 1124 bool: Whether the embassy was successfully aborted or not 1125 """ 1126 self.logger.info(f"Aborting embassy with {target}") 1127 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1128 data = {"abortembassyregion": target} 1129 response = self.request(url, data) 1130 return " aborted." in response.text 1131 1132 def cancel_embassy(self, target: str) -> bool: 1133 """Cancels an embassy with a region. 1134 1135 Args: 1136 target (str): The region with which to cancel the embassy. 1137 1138 Returns: 1139 bool: Whether the embassy was successfully cancelled or not 1140 """ 1141 self.logger.info(f"Cancelling embassy with {target}") 1142 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1143 data = {"cancelembassyclosureregion": target} 1144 response = self.request(url, data) 1145 return "Embassy closure order cancelled." in response.text 1146 1147 # end methods for embassies 1148 1149 def tag(self, action: str, tag: str) -> bool: 1150 """Adds or removes a tag to the current region. 1151 1152 Args: 1153 action (str): The action to take. Must be "add" or "remove". 1154 tag (str): The tag to add or remove. 1155 1156 Raises: 1157 ValueError: If action is not "add" or "remove", or if tag is not a valid tag. 1158 1159 Returns: 1160 bool: Whether the tag was successfully added or removed 1161 """ 1162 if action not in {"add", "remove"}: 1163 raise ValueError("action must be 'add' or 'remove'") 1164 if canonicalize(tag) not in valid.REGION_TAGS: 1165 raise ValueError(f"{tag} is not a valid tag") 1166 self.logger.info(f"{action.capitalize()}ing tag {tag} for {self.region}") 1167 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1168 data = { 1169 f"{action}_tag": canonicalize(tag), 1170 "updatetagsbutton": "1", 1171 } 1172 response = self.request(url, data) 1173 return "Region Tags updated!" in response.text 1174 1175 def eject(self, nation: str) -> bool: 1176 """Ejects a nation from the current region. Note that a 1 second delay is required before ejecting another nation. 1177 1178 Args: 1179 nation (str): The nation to eject. 1180 1181 Returns: 1182 bool: Whether the nation was successfully ejected or not 1183 """ 1184 self.logger.info(f"Ejecting {nation} from {self.region}") 1185 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1186 data = {"nation_name": nation, "eject": "1"} 1187 response = self.request(url, data) 1188 return "has been ejected from " in response.text 1189 1190 def banject(self, nation: str) -> bool: 1191 """Bans a nation from the current region. Note that a 1 second delay is required before banjecting another nation. 1192 1193 Args: 1194 nation (str): The nation to banject. 1195 1196 Returns: 1197 bool: Whether the nation was successfully banjected or not 1198 """ 1199 self.logger.info(f"Banjecting {nation} from {self.region}") 1200 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1201 data = {"nation_name": nation, "ban": "1"} 1202 response = self.request(url, data) 1203 return "has been ejected and banned from " in response.text 1204 1205 # end methods for region control 1206 1207 def junk_card(self, id: str, season: str) -> bool: 1208 """Junks a card from the current nation's deck. 1209 Args: 1210 id (str): ID of the card to junk 1211 season (str): Season of the card to junk 1212 Returns: 1213 bool: Whether the card was successfully junked or not 1214 """ 1215 self.logger.info(f"Junking card {id} from season {season}") 1216 url = "https://www.nationstates.net/template-overall=none/page=deck" 1217 1218 data = {"page": "ajax3", "a": "junkcard", "card": id, "season": season} 1219 response = self.request(url, data) 1220 1221 return "Your Deck" in response.text 1222 1223 def open_pack(self) -> bool: 1224 """Opens a card pack. 1225 1226 Returns: 1227 bool: Whether the bid was successfully removed or not 1228 """ 1229 self.logger.info("Opening trading card pack") 1230 url = "https://www.nationstates.net/template-overall=none/page=deck" 1231 data = {"open_loot_box": "1"} 1232 response = self.request(url, data) 1233 return "Tap cards to reveal..." in response.text 1234 1235 def ask(self, price: str, card_id: str, season: str) -> bool: 1236 """Puts an ask at price on a card in a season 1237 1238 Args: 1239 price (str): Price to ask 1240 card_id (str): ID of the card 1241 season (str): Season of the card 1242 1243 Returns: 1244 bool: Whether the ask was successfully lodged or not 1245 """ 1246 self.logger.info(f"Asking for {price} on {card_id} season {season}") 1247 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1248 1249 data = {"auction_ask": price, "auction_submit": "ask"} 1250 response = self.request(url, data) 1251 return f"Your ask of {price} has been lodged." in response.text 1252 1253 def bid(self, price: str, card_id: str, season: str) -> bool: 1254 """Places a bid on a card in a season 1255 1256 Args: 1257 price (str): Amount of bank to bid 1258 card_id (str): ID of the card 1259 season (str): Season of the card 1260 1261 Returns: 1262 bool: Whether the bid was successfully lodged or not 1263 """ 1264 self.logger.info(f"Putting a bid for {price} on {card_id} season {season}") 1265 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1266 1267 data = {"auction_bid": price, "auction_submit": "bid"} 1268 response = self.request(url, data) 1269 1270 return f"Your bid of {price} has been lodged." in response.text 1271 1272 def remove_ask(self, price: str, card_id: str, season: str) -> bool: 1273 """Removes an ask on card_id in season at price 1274 1275 Args: 1276 price (str): Price of the ask to remove 1277 card_id (str): ID of the card 1278 season (str): Season of the card 1279 1280 Returns: 1281 bool: Whether the ask was successfully removed or not 1282 """ 1283 1284 self.logger.info(f"removing an ask for {price} on {card_id} season {season}") 1285 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1286 1287 data = {"new_price": price, "remove_ask_price": price} 1288 response = self.request(url, data) 1289 return f"Removed your ask for {price}" in response.text 1290 1291 def remove_bid(self, price: str, card_id: str, season: str) -> bool: 1292 """Removes a bid on a card 1293 1294 Args: 1295 price (str): Price of the bid to remove 1296 card_id (str): ID of the card 1297 season (str): Season of the card 1298 1299 Returns: 1300 bool: Whether the bid was successfully removed or not 1301 """ 1302 1303 self.logger.info(f"Removing a bid for {price} on {card_id} season {season}") 1304 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1305 1306 data = {"new_price": price, "remove_bid_price": price} 1307 response = self.request(url, data) 1308 1309 return f"Removed your bid for {price}" in response.text 1310 1311 def expand_deck(self, price: str) -> bool: 1312 """Upgrades deck capacity 1313 1314 Args: 1315 price (str): Price of the Upgrade 1316 1317 Returns: 1318 bool: Whether the upgrade was successfully removed or not 1319 """ 1320 1321 self.logger.info(f"Upgrading your deck at a cost of {price}") 1322 url = "https://www.nationstates.net/template-overall=none/page=deck" 1323 1324 data = {"embiggen_deck": price} 1325 response = self.request(url, data) 1326 1327 return "Increased deck capacity from" in response.text 1328 1329 def add_to_collection(self, card_id: str, card_season: str, collection_id: str): 1330 """Adds a card to collection_id 1331 1332 Args: 1333 card_id (str): Card ID 1334 card_season (str): Cards season 1335 collection_id (str): The ID of the collection you want to add to 1336 1337 Returns: 1338 bool: Whether the adding was successfully added or not 1339 """ 1340 self.logger.info(f"Adding {card_id} of season {card_season} to {collection_id}") 1341 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={card_season}" 1342 1343 data = { 1344 "manage_collections": "1", 1345 "modify_card_in_collection": "1", 1346 f"collection_{collection_id}": "1", 1347 "save_collection": "1", 1348 } 1349 response = self.request(url, data) 1350 1351 return "Updated collections." in response.text 1352 1353 def remove_from_collection( 1354 self, card_id: str, card_season: str, collection_id: str 1355 ): 1356 """Removes a card from collection_id 1357 1358 Args: 1359 card_id (str): Card ID 1360 card_season (str): Cards season 1361 collection_id (str): The ID of the collection you want to remove from 1362 1363 Returns: 1364 bool: Whether the removal was successfully added or not 1365 """ 1366 self.logger.info( 1367 f"Removing {card_id} of season {card_season} from {collection_id}" 1368 ) 1369 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={card_season}" 1370 1371 data = { 1372 "manage_collections": "1", 1373 "modify_card_in_collection": "1", 1374 "start": "0", 1375 f"collection_{collection_id}": "0", 1376 "save_collection": "1", 1377 } 1378 response = self.request(url, data) 1379 1380 return "Updated collections." in response.text 1381 1382 def create_collection(self, name: str): 1383 """Creates a collection named name 1384 1385 Args: 1386 name (str): The name of the collection you want to create 1387 1388 Returns: 1389 bool: Whether the creating was successfully added or not 1390 """ 1391 self.logger.info(f"Creating {name} collection") 1392 url = "https://www.nationstates.net/template-overall=none/page=deck" 1393 1394 data = {"edit": "1", "collection_name": name, "save_collection": "1"} 1395 response = self.request(url, data) 1396 1397 return "Created collection!" in response.text 1398 1399 def delete_collection(self, name: str): 1400 """Deletes a collection named name 1401 1402 Args: 1403 name (str): The name of the collection you want to delete 1404 1405 Returns: 1406 bool: Whether the deleting was successfully added or not 1407 """ 1408 self.logger.info(f"Deleting {name} collection") 1409 url = "https://www.nationstates.net/template-overall=none/page=deck" 1410 1411 data = {"edit": "1", "collection_name": name, "delete_collection": "1"} 1412 response = self.request(url, data) 1413 1414 return "Created collection!" in response.text 1415 1416 def can_nation_be_founded(self, name: str): 1417 """Checks if a nation can be founded 1418 1419 Args: 1420 name (str): The name of the nation you want to check 1421 1422 Returns: 1423 bool: Whether the nation can be founded or not 1424 """ 1425 self.logger.info(f"Checking {name} in boneyard") 1426 url = "https://www.nationstates.net/template-overall=none/page=boneyard" 1427 1428 data = {"nation": name, "submit": "1"} 1429 response = self.request(url, data) 1430 1431 return ( 1432 "Available! This name may be used to found a new nation." in response.text 1433 ) 1434 1435 def join_nday_faction(self, id: str): 1436 """Joins a faction in the N-Day event 1437 1438 Args: 1439 id (str): The ID of the faction you want to join 1440 1441 Returns: 1442 bool: Whether the joining was successful or not 1443 """ 1444 self.logger.info(f"Joining faction {id}") 1445 1446 url = ( 1447 f"https://www.nationstates.net/template-overall=none/page=faction/fid={id}" 1448 ) 1449 data = {"join_faction": "1"} 1450 response = self.request(url, data) 1451 1452 return " has joined " in response.text 1453 1454 def leave_nday_faction(self, id: str): 1455 """Leaves a faction in the N-Day event 1456 1457 Args: 1458 id (str): The ID of the faction you want to leave 1459 1460 Returns: 1461 bool: Whether the leaving was successful or not 1462 """ 1463 self.logger.info(f"Leaving faction {id}") 1464 1465 url = ( 1466 f"https://www.nationstates.net/template-overall=none/page=faction/fid={id}" 1467 ) 1468 data = {"leave_faction": "1"} 1469 response = self.request(url, data) 1470 1471 return " has left " in response.text 1472 1473 1474if __name__ == "__main__": 1475 print("this is a module/library, not a script")
34def canonicalize(string: str) -> str: 35 """Converts a string to its canonical form used by the nationstates api. 36 37 Args: 38 string (str): The string to convert 39 40 Returns: 41 str: The canonical form of the string 42 """ 43 return string.lower().strip().replace(" ", "_")
Converts a string to its canonical form used by the nationstates api.
Arguments:
- string (str): The string to convert
Returns:
str: The canonical form of the string
46def uncanonicalize(string: str) -> str: 47 """Converts a string into a user friendly form from it's canonical nationstates api form. 48 49 Args: 50 string (str): The string to convert 51 52 Returns: 53 str: The uncanonicale form of the string 54 """ 55 return string.replace("_", " ").title()
Converts a string into a user friendly form from it's canonical nationstates api form.
Arguments:
- string (str): The string to convert
Returns:
str: The uncanonicale form of the string
58class NSSession: 59 def __init__( 60 self, 61 script_name: str, 62 script_version: str, 63 script_author: str, 64 script_user: str, 65 keybind: str = "space", 66 link_to_src: str = "", 67 logger: logging.Logger | None = None, 68 ): 69 """A wrapper around httpx that abstracts away 70 interacting with the HTML nationstates.net site. 71 Focused on legality, correctness, and ease of use. 72 73 Args: 74 script_name (str): Name of your script 75 script_version (str): Version number of your script 76 script_author (str): Author of your script 77 script_user (str): Nation name of the user running your script 78 keybind (str, optional): Keybind to count as a user click. Defaults to "space". 79 link_to_src (str, optional): Link to the source code of your script. 80 logger (logging.Logger | None, optional): Logger to use. Will create its own with name "NSDotPy" if none is specified. Defaults to None. 81 """ 82 self.VERSION = "2.3.0" 83 # Initialize logger 84 if not logger: 85 self._init_logger() 86 else: 87 self.logger = logger 88 # Create a new httpx session 89 self._session = httpx.Client( 90 http2=True, timeout=30 91 ) # ns can b slow, 30 seconds is hopefully a good sweet spot 92 # Set the user agent to the script name, version, author, and user as recommended in the script rules thread: 93 # https://forum.nationstates.net/viewtopic.php?p=16394966&sid=be37623536dbc8cee42d8d043945b887#p16394966 94 self._lock: bool = False 95 self._set_user_agent( 96 script_name, script_version, script_author, script_user, link_to_src 97 ) 98 # Initialize nationstates specific stuff 99 self._auth_region = "rwby" 100 self.chk: str = "" 101 self.localid: str = "" 102 self.pin: str = "" 103 self.nation: str = "" 104 self.region: str = "" 105 self.keybind = keybind 106 # Make sure the nations in the user agent actually exist 107 if not self._validate_nations({script_author, script_user}): 108 raise ValueError( 109 "One of, or both, of the nations in the user agent do not exist. Make sure you're only including the nation name in the constructor, e.g. 'Thorn1000' instead of 'Devved by Thorn1000'" 110 ) 111 self.logger.info(f"Initialized. Keybind to continue is {self.keybind}.") 112 113 def _validate_shard(self, shard, valid_shards, api): 114 """Helper function to validate a single shard.""" 115 if shard not in valid_shards: 116 raise ValueError(f"{shard} is not a valid shard for {api}") 117 118 def _validate_shards(self, api: str, shards: set[str]) -> None: 119 """Validates shards for a given API.""" 120 api_to_shard_set = { 121 "nation": valid.NATION_SHARDS 122 | valid.PRIVATE_NATION_SHARDS 123 | valid.PRIVATE_NATION_SHARDS, 124 "region": valid.REGION_SHARDS, 125 "world": valid.WORLD_SHARDS, 126 "wa": valid.WA_SHARDS, 127 } 128 129 valid_shards = api_to_shard_set.get(api) 130 if not valid_shards: 131 raise ValueError(f"Invalid API type: {api}") 132 133 for shard in shards: 134 self._validate_shard(shard, valid_shards, api) 135 136 def _set_user_agent( 137 self, 138 script_name: str, 139 script_version: str, 140 script_author: str, 141 script_user: str, 142 link_to_src: str, 143 ): 144 self.user_agent = ( 145 f"{script_name}/{script_version} (by:{script_author}; usedBy:{script_user})" 146 ) 147 if link_to_src: 148 self.user_agent = f"{self.user_agent}; src:{link_to_src}" 149 self.user_agent = f"{self.user_agent}; Written with NSDotPy/{self.VERSION} (by:Sweeze; src:github.com/sw33ze/NSDotPy)" 150 self._session.headers.update({"User-Agent": self.user_agent}) 151 152 def _validate_nations(self, nations: set[str]) -> bool: 153 """Checks if a list of nations exist using the NationStates API. 154 155 Args: 156 nations (set[str]): List of nations to check 157 158 Returns: 159 bool: True if all nations in the set exist, False otherwise. 160 """ 161 response = self.api_request("world", shard="nations") 162 world_nations = response.nations.split(",") 163 # check if all nations in the list exist in the world nations 164 return all(canonicalize(nation) in world_nations for nation in nations) 165 166 def _init_logger(self): 167 self.logger = logging.getLogger("NSDotPy") 168 config = { 169 "version": 1, 170 "disable_existing_loggers": False, 171 "formatters": { 172 "f": { 173 "format": "%(asctime)s %(message)s", 174 "datefmt": "%I:%M:%S %p", 175 } 176 }, 177 "handlers": { 178 "console": { 179 "class": "logging.StreamHandler", 180 "formatter": "f", 181 } 182 }, 183 "loggers": { 184 "NSDotPy": {"handlers": ["console"], "level": "INFO"}, 185 "httpx": {"handlers": ["console"], "level": "ERROR"}, 186 }, 187 } 188 logging.config.dictConfig(config) 189 190 def _get_auth_values(self, response: httpx.Response): 191 # make sure it's actually html first 192 if not response.headers["Content-Type"].startswith("text/html"): 193 return 194 # parse the html 195 soup = BeautifulSoup(response.text, "lxml") 196 # gathering chk and localid so i dont have to worry about authenticating l8r 197 if chk := soup.find("input", {"name": "chk"}): 198 self.chk = chk["value"].strip() # type: ignore 199 if localid := soup.find("input", {"name": "localid"}): 200 self.localid = localid["value"].strip() # type: ignore 201 if pin := self._session.cookies.get("pin"): 202 # you should never really need the pin but just in case i'll store it 203 # PAST ME WAS RIGHT, I NEEDED IT FOR THE PRIVATE API!! 204 self.pin = pin 205 if soup.find("a", {"class": "STANDOUT"}): 206 self.region = canonicalize( 207 soup.find_all("a", {"class": "STANDOUT"})[1].attrs["href"].split("=")[1] 208 ) 209 210 def _wait_for_input(self, key: str) -> int: 211 """Blocks execution until the user presses a key. Used as the one click = one request action. 212 213 Args: 214 key (str): The key to wait for 215 216 Returns: 217 int: Userclick parameter, milliseconds since the epoch""" 218 keyboard.wait(key) 219 # the trigger_on_release parameter is broken on windows 220 # because of a bug in keyboard so we have to do this 221 while keyboard.is_pressed(key): 222 pass 223 return int(time.time() * 1000) 224 225 def _get_detag_wfe(self) -> str: 226 """Gets the detagged WFE of the region you're in. 227 228 Returns: 229 str: The detagged WFE""" 230 self.logger.info(f"Getting detag WFE for {self.region}...") 231 response = self.request( 232 f"https://greywardens.xyz/tools/wfe_index/region={self.region}", 233 ) 234 soup = BeautifulSoup(response.text, "lxml") 235 # the safest bet for a detag wfe is the first wfe of the region 236 return soup.find_all("pre")[-1].text 237 238 def _validate_fields(self, data: dict): 239 max_lengths = { 240 "pretitle": 28, 241 "slogan": 55, 242 "currency": 40, 243 "animal": 40, 244 "demonym_noun": 44, 245 "demonym_adjective": 44, 246 "demonym_plural": 44, 247 } 248 249 # go through each key in the data dict and make sure they're below the max length 250 for key, value in data.items(): 251 if key not in max_lengths: 252 continue 253 if len(value) > max_lengths[key]: 254 raise ValueError(f"{key} is too long, max length is {max_lengths[key]}") 255 if len(value) < 2 and key != "slogan": 256 raise ValueError(f"{key} should have a minimum length of 2 characters.") 257 # check if pretitle contains any non-alphanumeric characters (except spaces) 258 if key == "pretitle" and not value.replace(" ", "").isalnum(): 259 raise ValueError( 260 "Pretitle should only contain alphanumeric characters or spaces." 261 ) 262 263 def _wait_for_ratelimit(self, head: dict, constant_rate_limit: bool): 264 if "X-Pin" in head: 265 self.pin = head["X-Pin"] 266 if waiting_time := head.get("Retry-After"): 267 self.logger.warning(f"Rate limited. Waiting {waiting_time} seconds.") 268 time.sleep(int(waiting_time)) 269 # slow down requests so we dont hit the rate limit in the first place 270 requests_left = int(head["RateLimit-Remaining"]) 271 if requests_left < 10 or constant_rate_limit: 272 seconds_until_reset = int(head["RateLimit-Reset"]) 273 time.sleep(seconds_until_reset / requests_left) 274 275 def _html_request( 276 self, url, data={}, files=None, follow_redirects=False 277 ) -> httpx.Response: 278 data |= {"chk": self.chk, "localid": self.localid} 279 userclick = self._wait_for_input(self.keybind) 280 # userclick is the number of milliseconds since the epoch, admin uses this for help enforcing the simultaneity rule 281 response = self._session.post( 282 f"{url}/userclick={userclick}", 283 data=data, 284 files=files, 285 follow_redirects=follow_redirects, 286 ) 287 if response.status_code >= 400: 288 with open("error.html", "w") as f: 289 f.write(response.text) 290 raise httpx.HTTPError( 291 f"Received status code {response.status_code} from {response.url}. Error page saved to error.html." 292 ) 293 self._get_auth_values(response) 294 return response 295 296 # --- end private methods --- # 297 298 def refresh_auth_values(self): 299 self.logger.info("Refreshing authentication values...") 300 response = self.request( 301 f"https://www.nationstates.net/page=display_region/region={self._auth_region}", 302 data={"theme": "century"}, 303 ) 304 self._get_auth_values(response) 305 306 def request( 307 self, 308 url: str, 309 data: dict = {}, 310 files: dict = {}, 311 follow_redirects: bool = False, 312 ) -> httpx.Response: 313 """Sends a request to the given url with the given data and files. 314 315 Args: 316 url (str): URL to send the request to 317 data (dict, optional): Payload to send with the request 318 files (dict, optional): Payload to send with requests that upload files 319 320 Returns: 321 httpx.Response: The response from the server 322 """ 323 if any( 324 banned_page in canonicalize(url) 325 for banned_page in { 326 "page=telegrams", 327 "page=dilemmas", 328 "page=compose_telegram", 329 "page=store", 330 "page=help", 331 } 332 ): 333 raise ValueError( 334 "You cannot use a tool to interact with telegrams, issues, getting help, or the store. Read up on the script rules: https://forum.nationstates.net/viewtopic.php?p=16394966#p16394966" 335 ) 336 if "api.cgi" in canonicalize(url): 337 # you should be using api_request for api requests 338 raise ValueError("You should be using api_request() for api requests.") 339 elif "nationstates" in canonicalize(url): 340 # do all the things that need to be done for html requests 341 if self._lock: 342 # if lock is true then we're already in the middle of a 343 # request and we're in danger of breaking the simultaneity rule 344 # so raise an error 345 raise PermissionError( 346 "You're already in the middle of a request. Stop trying to violate simultaneity." 347 ) 348 self._lock = True 349 response = self._html_request(url, data, files, follow_redirects) 350 self._lock = False 351 else: 352 # if its not nationstates then just pass the request through 353 response = self._session.post( 354 url, data=data, follow_redirects=follow_redirects 355 ) 356 return response 357 358 def api_request( 359 self, 360 api: str, 361 *, 362 target: str = "", 363 shard: str | set[str] = "", 364 password: str = "", 365 constant_rate_limit: bool = False, 366 ) -> benedict: 367 """Sends a request to the nationstates api with the given data. 368 369 Args: 370 api (str): The api to send the request to. Must be "nation", "region", "world", or "wa" 371 target (str, optional): The nation, region, or wa council to target. Required for non-world api requests. 372 shard (str, optional): The shard, or shards, you're requesting for. Must be a valid shard for the given api. Only required for world and wa api requests. 373 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 374 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 375 376 Returns: 377 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 378 """ 379 # TODO: probably move this responsibility to a third party api library to avoid reinventing the wheel 380 # if one exists of sufficient quality thats AGPLv3 compatible 381 if api not in {"nation", "region", "world", "wa"}: 382 raise ValueError("api must be 'nation', 'region', 'world', or 'wa'") 383 if api != "world" and not target: 384 raise ValueError("target must be specified for non-world api requests") 385 if api in {"wa", "world"} and not shard: 386 raise ValueError("shard must be specified for world and wa api requests") 387 # end argument validation 388 # shard validation 389 if type(shard) == str: 390 shard = {shard} 391 self._validate_shards(api, shard) # type: ignore 392 # end shard validation 393 data = { 394 "v": "12", 395 } 396 if api != "world": 397 data[api] = target 398 if shard: 399 data["q"] = "+".join(shard) 400 url = "https://www.nationstates.net/cgi-bin/api.cgi" 401 if password: 402 self._session.headers["X-Password"] = password 403 if self.pin: 404 self._session.headers["X-Pin"] = self.pin 405 # rate limiting section 406 response = self._session.post(url, data=data) 407 # if the server tells us to wait, wait 408 self._wait_for_ratelimit(response.headers, constant_rate_limit) 409 response.raise_for_status() 410 parsed_response = benedict.from_xml(response.text, keyattr_dynamic=True) 411 parsed_response.standardize() 412 parsed_response: benedict = parsed_response[api] # type: ignore 413 return parsed_response 414 415 def api_issue( 416 self, 417 nation: str, 418 issue: int, 419 option: int, 420 password: str = "", 421 constant_rate_limit: bool = False, 422 ) -> benedict: 423 """Answers an issue via the API. 424 425 Args: 426 nation (str): The nation to perform the command with. 427 issue (int): the ID of the issue. 428 option (int): the issue option to choose. 429 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 430 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 431 432 Returns: 433 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 434 """ 435 if not (password or self.pin): 436 raise ValueError("must specify authentication") 437 data = { 438 "v": "12", 439 "c": "issue", 440 "nation": canonicalize(nation), 441 "issue": issue, 442 "option": option, 443 } 444 url = "https://www.nationstates.net/cgi-bin/api.cgi" 445 if password: 446 self._session.headers["X-Password"] = password 447 if self.pin: 448 self._session.headers["X-Pin"] = self.pin 449 # rate limiting section 450 response = self._session.get(url, params=data) 451 # if the server tells us to wait, wait 452 self._wait_for_ratelimit(response.headers, constant_rate_limit) 453 response.raise_for_status() 454 parsed_response = benedict.from_xml(response.text, keyattr_dynamic=True) 455 parsed_response.standardize() 456 parsed_response: benedict = parsed_response["nation"] # type: ignore 457 return parsed_response 458 459 def api_command( 460 self, 461 nation: str, 462 command: str, 463 data: dict, 464 password: str = "", 465 mode: str = "", 466 constant_rate_limit: bool = False, 467 ) -> benedict: 468 """Sends a non-issue command to the nationstates api with the given data and password. 469 470 Args: 471 nation (str): The nation to perform the command with. 472 command (str): The command to perform. Must be "giftcard", "dispatch", "rmbpost" 473 data (str, optional): The unique data to send with the parameters of the command; consult the API docs for more information. 474 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 475 mode (str, optional): Whether to prepare or to execute the command. If value is given, does one of the two and returns result, if no value is given, does both and returns result of execute. 476 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 477 478 Returns: 479 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 480 """ 481 if command not in {"giftcard", "dispatch", "rmbpost"}: 482 raise ValueError("command must be 'giftcard', 'dispatch', or 'rmbpost'") 483 if not (password or self.pin): 484 raise ValueError("must specify authentication") 485 if mode not in {"", "prepare", "execute"}: 486 raise ValueError("mode must be prepare or execute") 487 data["v"] = "12" 488 data["nation"] = canonicalize(nation) 489 data["c"] = command 490 data["mode"] = mode if mode else "prepare" # if no mode than first prepare 491 url = "https://www.nationstates.net/cgi-bin/api.cgi" 492 if password: 493 self._session.headers["X-Password"] = password 494 if self.pin: 495 self._session.headers["X-Pin"] = self.pin 496 # rate limiting section 497 response = self._session.get(url, params=data) 498 # if the server tells us to wait, wait 499 self._wait_for_ratelimit(response.headers, constant_rate_limit) 500 response.raise_for_status() 501 parsed_response = benedict.from_xml(response.text, keyattr_dynamic=True) 502 parsed_response.standardize() 503 parsed_response: benedict = parsed_response["nation"] # type: ignore 504 if mode == "": 505 # if no mode was specified earlier, repeat command with execute and token 506 data["token"] = parsed_response["success"] 507 return self.api_command(nation, command, data, mode="execute") 508 else: 509 return parsed_response 510 511 def api_giftcard( 512 self, 513 nation: str, 514 card_id: int, 515 season: int, 516 recipient: str, 517 password: str = "", 518 constant_rate_limit: bool = False, 519 ) -> benedict: 520 """Gifts a card using the API. 521 522 Args: 523 nation (str): The nation to perform the command with. 524 card_id (int): The ID of the card to gift. 525 season (int): The season of the card to gift. 526 recipient (str): The nation to gift the card to. 527 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 528 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 529 530 Returns: 531 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 532 """ 533 data = {"cardid": card_id, "season": season, "to": canonicalize(recipient)} 534 return self.api_command( 535 nation, "giftcard", data, password, constant_rate_limit=constant_rate_limit 536 ) 537 538 def api_dispatch( 539 self, 540 nation: str, 541 action: str, 542 title: str = "", 543 text: str = "", 544 category: int = 0, 545 subcategory: int = 0, 546 dispatchid: int = 0, 547 password: str = "", 548 constant_rate_limit: bool = False, 549 ) -> benedict: 550 """Add, edit, or remove a dispatch. 551 552 Args: 553 nation (str): The nation to perform the command with. 554 action (str): The action to take. Must be "add", "edit", "remove" 555 title (str, optional): The dispatch title when adding or editing. 556 text (str, optional): The dispatch text when adding or editing. 557 category: (int, optional), The category ID when adding or editing. 558 subcategory (int, optional): The subcategory ID when adding or editing. 559 dispatchid (int, optional): The dispatch ID when editing or removing. 560 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 561 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 562 563 Returns: 564 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 565 """ 566 # TODO: maybe consider splitting these three functions? 567 # TODO: maybe create enums for category and subcategory 568 if action not in {"add", "edit", "remove"}: 569 raise ValueError("action must be 'add', 'edit', or 'remove'") 570 if action != "remove" and not all({title, text, category, subcategory}): 571 raise ValueError("must specify title, text, category, and subcategory") 572 if action != "add" and not dispatchid: 573 raise ValueError("must specify a dispatch id") 574 575 data = {"dispatch": action} 576 if title: 577 data["title"] = title 578 if text: 579 data["text"] = text 580 if category: 581 data["category"] = category 582 if subcategory: 583 data["subcategory"] = subcategory 584 if dispatchid: 585 data["dispatchid"] = dispatchid 586 return self.api_command( 587 nation, "dispatch", data, password, constant_rate_limit=constant_rate_limit 588 ) 589 590 def api_rmb( 591 self, 592 nation: str, 593 region: str, 594 text: str, 595 password: str = "", 596 constant_rate_limit: bool = False, 597 ) -> benedict: 598 """Post a message on the regional message board via the API. 599 600 Args: 601 nation (str): The nation to perform the command with. 602 region (str): the region to post the message in. 603 text (str): the text to post. 604 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 605 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 606 607 Returns: 608 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 609 """ 610 data = {"region": region, "text": text} 611 return self.api_command( 612 nation, "rmbpost", data, password, constant_rate_limit=constant_rate_limit 613 ) 614 615 def login(self, nation: str, password: str) -> bool: 616 """Logs in to the nationstates site. 617 618 Args: 619 nation (str): Nation name 620 password (str): Nation password 621 622 Returns: 623 bool: True if login was successful, False otherwise 624 """ 625 self.logger.info(f"Logging in to {nation}") 626 url = f"https://www.nationstates.net/page=display_region/region={self._auth_region}" 627 # shoutouts to roavin for telling me i had to have page=display_region in the url so it'd work with a userclick parameter 628 629 data = { 630 "nation": canonicalize(nation), 631 "password": password, 632 "theme": "century", 633 "logging_in": "1", 634 "submit": "Login", 635 } 636 637 response = self.request(url, data) 638 639 soup = BeautifulSoup(response.text, "lxml") 640 # checks if the body tag has your nation name in it; if it does, you're logged in 641 if not soup.find("body", {"data-nname": canonicalize(nation)}): 642 return False 643 644 self.nation = canonicalize(nation) 645 return True 646 647 def change_nation_flag(self, flag_filename: str) -> bool: 648 """Changes the nation flag to the given image. 649 650 Args: 651 flag_filename (str): Filename of the flag to change to 652 653 Returns: 654 bool: True if the flag was changed, False otherwise 655 """ 656 self.logger.info(f"Changing flag on {self.nation}") 657 # THIS WAS SO FUCKING FRUSTRATING BUT IT WORKS NOW AND IM NEVER TOUCHING THIS BULLSHIT UNLESS NS BREAKS IT AGAIN 658 url = "https://www.nationstates.net/cgi-bin/upload.cgi" 659 660 data = { 661 "nationname": self.nation, 662 } 663 files = { 664 "file": ( 665 flag_filename, 666 open(flag_filename, "rb"), 667 mimetypes.guess_type(flag_filename)[0], 668 ) 669 } 670 671 response = self.request(url, data=data, files=files) 672 673 if "page=settings" in response.headers["location"]: 674 self.refresh_auth_values() 675 return True 676 elif "Just a moment..." in response.text: 677 self.logger.warning( 678 "Cloudflare blocked you idiot get fucked have fun with that like I had to lmaoooooooooo" 679 ) 680 return False 681 682 def change_nation_settings( 683 self, 684 *, 685 email: str = "", 686 pretitle: str = "", 687 slogan: str = "", 688 currency: str = "", 689 animal: str = "", 690 demonym_noun: str = "", 691 demonym_adjective: str = "", 692 demonym_plural: str = "", 693 new_password: str = "", 694 ) -> bool: 695 """Given a logged in session, changes customizable fields and settings of the logged in nation. 696 Variables must be explicitly named in the call to the function, e.g. "session.change_nation_settings(pretitle='Join Lily', currency='Join Lily')" 697 698 Args: 699 email (str, optional): New email for WA apps. 700 pretitle (str, optional): New pretitle of the nation. Max length of 28. Nation must have minimum population of 250 million. 701 slogan (str, optional): New Slogan/Motto of the nation. Max length of 55. 702 currency (str, optional): New currency of the nation. Max length of 40. 703 animal (str, optional): New national animal of the nation. Max length of 40. 704 demonym_noun (str, optional): Noun the nation will refer to its citizens as. Max length of 44. 705 demonym_adjective (str, optional): Adjective the nation will refer to its citizens as. Max length of 44. 706 demonym_plural (str, optional): Plural form of "demonym_noun". Max length of 44. 707 new_password (str, optional): New password to assign to the nation. 708 709 Returns: 710 bool: True if changes were successful, False otherwise. 711 """ 712 self.logger.info(f"Changing settings on {self.nation}") 713 url = "https://www.nationstates.net/template-overall=none/page=settings" 714 715 data = { 716 "type": pretitle, 717 "slogan": slogan, 718 "currency": currency, 719 "animal": animal, 720 "demonym2": demonym_noun, 721 "demonym": demonym_adjective, 722 "demonym2pl": demonym_plural, 723 "email": email, 724 "password": new_password, 725 "confirm_password": new_password, 726 "update": " Update ", 727 } 728 # remove keys that have empty values 729 data = {k: v for k, v in data.items() if v} 730 # make sure everything is following the proper length limits and only contains acceptable characters 731 self._validate_fields(data) 732 733 response = self.request(url, data) 734 return "Your settings have been successfully updated." in response.text 735 736 def move_to_region(self, region: str, password: str = "") -> bool: 737 """Moves the nation to the given region. 738 739 Args: 740 region (str): Region to move to 741 password (str, optional): Region password, if the region is passworded 742 743 Returns: 744 bool: True if the move was successful, False otherwise 745 """ 746 self.logger.info(f"Moving {self.nation} to {region}") 747 url = "https://www.nationstates.net/template-overall=none/page=change_region" 748 749 data = {"region_name": region, "move_region": "1"} 750 if password: 751 data["password"] = password 752 response = self.request(url, data) 753 754 if "Success!" in response.text: 755 self.region = canonicalize(region) 756 return True 757 return False 758 759 def vote(self, pollid: str, option: str) -> bool: 760 """Votes on a poll. 761 762 Args: 763 pollid (str): ID of the poll to vote on, e.g. "199747" 764 option (str): Option to vote for (starts at 0) 765 766 Returns: 767 bool: True if the vote was successful, False otherwise 768 """ 769 self.logger.info(f"Voting on poll {pollid} with {self.nation}") 770 url = f"https://www.nationstates.net/template-overall=none/page=poll/p={pollid}" 771 772 data = {"pollid": pollid, "q1": option, "poll_submit": "1"} 773 response = self.request(url, data) 774 775 return "Your vote has been lodged." in response.text 776 777 # below are functions that are related to the WA 778 779 def join_wa(self, nation: str, app_id: str) -> bool: 780 """Joins the WA with the given nation. 781 782 Args: 783 nation (str): Nation to join the WA with 784 app_id (str): ID of the WA application to use 785 786 Returns: 787 bool: True if the join was successful, False otherwise 788 """ 789 self.logger.info(f"Joining WA with {nation}") 790 url = "https://www.nationstates.net/cgi-bin/join_un.cgi" 791 792 data = {"nation": canonicalize(nation), "appid": app_id.strip()} 793 response = self.request(url, data) 794 795 if "?welcome=1" in response.headers["location"]: 796 # since we're just getting thrown into a cgi script, we'll have to manually grab authentication values 797 self.refresh_auth_values() 798 return True 799 return False 800 801 def resign_wa(self): 802 """Resigns from the WA. 803 804 Returns: 805 bool: True if the resignation was successful, False otherwise 806 """ 807 self.logger.info("Resigning from WA") 808 url = "https://www.nationstates.net/template-overall=none/page=UN_status" 809 810 data = {"action": "leave_UN", "submit": "1"} 811 response = self.request(url, data) 812 813 return "From this moment forward, your nation is on its own." in response.text 814 815 def apply_wa(self, reapply: bool = True) -> bool: 816 """Applies to the WA. 817 818 Args: 819 reapply (bool, optional): Whether to reapply if you've been sent an application that's still valid. Defaults to True. 820 821 Returns: 822 bool: True if the application was successful, False otherwise 823 """ 824 self.logger.info(f"Applying to WA with {self.nation}") 825 url = "https://www.nationstates.net/template-overall=none/page=UN_status" 826 827 data = {"action": "join_UN"} 828 if reapply: 829 data["resend"] = "1" 830 else: 831 data["submit"] = "1" 832 833 response = self.request(url, data) 834 return ( 835 "Your application to join the World Assembly has been received!" 836 in response.text 837 ) 838 839 def endorse(self, nation: str, endorse: bool = True) -> bool: 840 """Endorses the given nation. 841 842 Args: 843 nation (str): Nation to endorse 844 endorse (bool, optional): True=endorse, False=unendorse. Defaults to True. 845 846 Returns: 847 bool: True if the endorsement was successful, False otherwise 848 """ 849 self.logger.info( 850 f"{('Unendorsing', 'Endorsing')[endorse]} {nation} with {self.nation}" 851 ) 852 url = "https://www.nationstates.net/cgi-bin/endorse.cgi" 853 854 data = { 855 "nation": canonicalize(nation), 856 "action": "endorse" if endorse else "unendorse", 857 } 858 response = self.request(url, data) 859 860 return f"nation={canonicalize(nation)}" in response.headers["location"] 861 862 def clear_dossier(self) -> bool: 863 """Clears a logged in nation's dossier. 864 865 Returns: 866 bool: Whether it was successful or not 867 """ 868 869 self.logger.info(f"Clearing dossier on {self.nation}") 870 url = "https://www.nationstates.net/template-overall=none/page=dossier" 871 data = {"clear_dossier": "1"} 872 response = self.request(url, data) 873 874 return "Dossier cleared of nations." in response.text 875 876 def add_to_dossier(self, nations: list[str] | str) -> bool: 877 """Adds nations to the logged in nation's dossier. 878 879 Args: 880 nations (list[str] | str): List of nations to add, or a single nation 881 882 Returns: 883 bool: Whether it was successful or not 884 """ 885 886 self.logger.info(f"Adding {nations} to dossier on {self.nation}") 887 url = "https://www.nationstates.net/dossier.cgi" 888 data = { 889 "currentnation": canonicalize(self.nation), 890 "action_append": "Upload Nation Dossier File", 891 } 892 files = { 893 "file": ( 894 "dossier.txt", 895 "\n".join(nations).strip() if type(nations) is list else nations, 896 "text/plain", 897 ), 898 } 899 response = self.request(url, data, files=files) 900 901 self.refresh_auth_values() 902 return "appended=" in response.headers["location"] 903 904 def wa_vote(self, council: str, vote: str) -> bool: 905 """Votes on the current WA resolution. 906 907 Args: 908 council (str): Must be "ga" for general assembly, "sc" for security council. 909 vote (str): Must be "for" or "against". 910 911 Returns: 912 bool: Whether the vote was successful or not 913 """ 914 self.logger.info( 915 f"Voting {vote} on {council.upper()} resolution with {self.nation}" 916 ) 917 if council not in {"ga", "sc"}: 918 raise ValueError("council must be 'ga' or 'sc'") 919 if vote not in {"for", "against"}: 920 raise ValueError("vote must be 'for' or 'against'") 921 self.logger.info("Voting on WA resolution") 922 923 url = f"https://www.nationstates.net/template-overall=none/page={council}" 924 data = { 925 "vote": f"Vote {vote.capitalize()}", 926 } 927 response = self.request(url, data) 928 929 return "Your vote has been lodged." in response.text 930 931 def refound_nation(self, nation: str, password: str) -> bool: 932 """Refounds a nation. 933 934 Args: 935 nation (str): Name of the nation to refound 936 password (str): Password to the nation 937 938 Returns: 939 bool: Whether the nation was successfully refounded or not 940 """ 941 url = "https://www.nationstates.net/template-overall=none/" 942 data = { 943 "logging_in": "1", 944 "restore_password": password, 945 "restore_nation": "1", 946 "nation": nation, 947 } 948 response = self.request(url, data=data) 949 if response.status_code == 302: 950 self.nation = nation 951 self.refresh_auth_values() 952 return True 953 return False 954 955 # methods for region control 956 957 def create_region( 958 self, 959 region_name: str, 960 wfe: str, 961 *, 962 password: str = "", 963 frontier: bool = False, 964 executive_delegacy: bool = False, 965 ) -> bool: 966 """Creates a new region. 967 968 Args: 969 region_name (str): Name of the region 970 wfe (str): WFE of the region 971 password (str, optional): Password to the region. Defaults to "". 972 frontier (bool, optional): Whether or not the region is a frontier. Defaults to False. 973 executive_delegacy (bool, optional): Whether or not the region has an executive WA delegacy. Defaults to False. Ignored if frontier is True. 974 975 Returns: 976 bool: Whether the region was successfully created or not 977 """ 978 self.logger.info(f"Creating new region {region_name}") 979 url = "https://www.nationstates.net/template-overall=none/page=create_region" 980 data = { 981 "page": "create_region", 982 "region_name": region_name.strip(), 983 "desc": wfe.strip(), 984 "create_region": "1", 985 } 986 if password: 987 data |= {"pw": "1", "rpassword": password} 988 if frontier: 989 data |= {"is_frontier": "1"} 990 elif executive_delegacy: 991 data |= {"delegate_control": "1"} 992 response = self.request(url, data) 993 return "Success! You have founded " in response.text 994 995 def upload_to_region(self, type: str, filename: str) -> str: 996 """Uploads a file to the current region. 997 998 Args: 999 type (str): Type of file to upload. Must be "flag" or "banner". 1000 filename (str): Name of the file to upload. e.g. "myflag.png" 1001 1002 Raises: 1003 ValueError: If type is not "flag" or "banner" 1004 1005 Returns: 1006 str: Empty string if the upload failed, otherwise the ID of the uploaded file 1007 """ 1008 self.logger.info(f"Uploading {filename} to {self.region}") 1009 if type not in {"flag", "banner"}: 1010 raise ValueError("type must be 'flag' or 'banner'") 1011 url = "https://www.nationstates.net/cgi-bin/upload.cgi" 1012 data = { 1013 "uploadtype": f"r{type}", 1014 "page": "region_control", 1015 "region": self.region, 1016 "expect": "json", 1017 } 1018 files = { 1019 f"file_upload_r{type}": ( 1020 filename, 1021 open(filename, "rb"), 1022 mimetypes.guess_type(filename)[0], 1023 ) 1024 } 1025 response = self.request(url, data, files=files) 1026 return "" if "id" not in response.json() else response.json()["id"] 1027 1028 def set_flag_and_banner( 1029 self, flag_id: str = "", banner_id: str = "", flag_mode: str = "" 1030 ) -> bool: 1031 """Sets the uploaded flag and/or banner for the current region. 1032 1033 Args: 1034 flag_id (str, optional): ID of the flag, uploaded with upload_to_region(). Defaults to "". 1035 banner_id (str, optional): ID of the banner, uploaded with upload_to_region(). Defaults to "". 1036 flagmode (str, optional): Must be "flag" which will have a shadow, or "logo" which will not, or "" to not change it. Defaults to "". 1037 1038 Raises: 1039 ValueError: If flagmode is not "flag", "logo", or "" 1040 1041 Returns: 1042 bool: Whether the change was successful or not 1043 """ 1044 if flag_mode not in {"flag", "logo", ""}: 1045 raise ValueError("flagmode must be 'flag', 'logo', or ''") 1046 self.logger.info(f"Setting flag and banner for {self.region}") 1047 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1048 data = { 1049 "newflag": flag_id, 1050 "newbanner": banner_id, 1051 "saveflagandbannerchanges": "1", 1052 "flagmode": flag_mode, 1053 } 1054 # remove entries with empty values 1055 data = {k: v for k, v in data.items() if v} 1056 1057 response = self.request(url, data) 1058 1059 return "Regional banner/flag updated!" in response.text 1060 1061 def change_wfe(self, wfe: str = "") -> bool: 1062 """Changes the WFE of the current region. 1063 1064 Args: 1065 wfe (str, optional): World Factbook Entry to change to. Defaults to the oldest WFE the region has, for detags. 1066 1067 Returns: 1068 bool: True if successful, False otherwise 1069 """ 1070 self.logger.info(f"Changing WFE for {self.region}") 1071 if not wfe: 1072 wfe = self._get_detag_wfe() # haku im sorry for hitting your site so much 1073 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1074 data = { 1075 "message": wfe.encode("iso-8859-1", "xmlcharrefreplace") 1076 .decode() 1077 .strip(), # lol. 1078 "setwfebutton": "1", 1079 } 1080 response = self.request(url, data) 1081 return "World Factbook Entry updated!" in response.text 1082 1083 # methods for embassies 1084 1085 def request_embassy(self, target: str) -> bool: 1086 """Requests an embassy with a region. 1087 1088 Args: 1089 target (str): The region to request the embassy with. 1090 1091 Returns: 1092 bool: Whether the request was successfully sent or not 1093 """ 1094 self.logger.info(f"Requesting embassy with {target}") 1095 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1096 data = { 1097 "requestembassyregion": target, 1098 "requestembassy": "1", # it's silly that requesting needs this but not closing, aborting, or cancelling 1099 } 1100 response = self.request(url, data) 1101 return "Your proposal for the construction of embassies with" in response.text 1102 1103 def close_embassy(self, target: str) -> bool: 1104 """Closes an embassy with a region. 1105 1106 Args: 1107 target (str): The region with which to close the embassy. 1108 1109 Returns: 1110 bool: Whether the embassy was successfully closed or not 1111 """ 1112 self.logger.info(f"Closing embassy with {target}") 1113 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1114 data = {"cancelembassyregion": target} 1115 response = self.request(url, data) 1116 return " has been scheduled for demolition." in response.text 1117 1118 def abort_embassy(self, target: str) -> bool: 1119 """Aborts an embassy with a region. 1120 1121 Args: 1122 target (str): The region with which to abort the embassy. 1123 1124 Returns: 1125 bool: Whether the embassy was successfully aborted or not 1126 """ 1127 self.logger.info(f"Aborting embassy with {target}") 1128 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1129 data = {"abortembassyregion": target} 1130 response = self.request(url, data) 1131 return " aborted." in response.text 1132 1133 def cancel_embassy(self, target: str) -> bool: 1134 """Cancels an embassy with a region. 1135 1136 Args: 1137 target (str): The region with which to cancel the embassy. 1138 1139 Returns: 1140 bool: Whether the embassy was successfully cancelled or not 1141 """ 1142 self.logger.info(f"Cancelling embassy with {target}") 1143 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1144 data = {"cancelembassyclosureregion": target} 1145 response = self.request(url, data) 1146 return "Embassy closure order cancelled." in response.text 1147 1148 # end methods for embassies 1149 1150 def tag(self, action: str, tag: str) -> bool: 1151 """Adds or removes a tag to the current region. 1152 1153 Args: 1154 action (str): The action to take. Must be "add" or "remove". 1155 tag (str): The tag to add or remove. 1156 1157 Raises: 1158 ValueError: If action is not "add" or "remove", or if tag is not a valid tag. 1159 1160 Returns: 1161 bool: Whether the tag was successfully added or removed 1162 """ 1163 if action not in {"add", "remove"}: 1164 raise ValueError("action must be 'add' or 'remove'") 1165 if canonicalize(tag) not in valid.REGION_TAGS: 1166 raise ValueError(f"{tag} is not a valid tag") 1167 self.logger.info(f"{action.capitalize()}ing tag {tag} for {self.region}") 1168 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1169 data = { 1170 f"{action}_tag": canonicalize(tag), 1171 "updatetagsbutton": "1", 1172 } 1173 response = self.request(url, data) 1174 return "Region Tags updated!" in response.text 1175 1176 def eject(self, nation: str) -> bool: 1177 """Ejects a nation from the current region. Note that a 1 second delay is required before ejecting another nation. 1178 1179 Args: 1180 nation (str): The nation to eject. 1181 1182 Returns: 1183 bool: Whether the nation was successfully ejected or not 1184 """ 1185 self.logger.info(f"Ejecting {nation} from {self.region}") 1186 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1187 data = {"nation_name": nation, "eject": "1"} 1188 response = self.request(url, data) 1189 return "has been ejected from " in response.text 1190 1191 def banject(self, nation: str) -> bool: 1192 """Bans a nation from the current region. Note that a 1 second delay is required before banjecting another nation. 1193 1194 Args: 1195 nation (str): The nation to banject. 1196 1197 Returns: 1198 bool: Whether the nation was successfully banjected or not 1199 """ 1200 self.logger.info(f"Banjecting {nation} from {self.region}") 1201 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1202 data = {"nation_name": nation, "ban": "1"} 1203 response = self.request(url, data) 1204 return "has been ejected and banned from " in response.text 1205 1206 # end methods for region control 1207 1208 def junk_card(self, id: str, season: str) -> bool: 1209 """Junks a card from the current nation's deck. 1210 Args: 1211 id (str): ID of the card to junk 1212 season (str): Season of the card to junk 1213 Returns: 1214 bool: Whether the card was successfully junked or not 1215 """ 1216 self.logger.info(f"Junking card {id} from season {season}") 1217 url = "https://www.nationstates.net/template-overall=none/page=deck" 1218 1219 data = {"page": "ajax3", "a": "junkcard", "card": id, "season": season} 1220 response = self.request(url, data) 1221 1222 return "Your Deck" in response.text 1223 1224 def open_pack(self) -> bool: 1225 """Opens a card pack. 1226 1227 Returns: 1228 bool: Whether the bid was successfully removed or not 1229 """ 1230 self.logger.info("Opening trading card pack") 1231 url = "https://www.nationstates.net/template-overall=none/page=deck" 1232 data = {"open_loot_box": "1"} 1233 response = self.request(url, data) 1234 return "Tap cards to reveal..." in response.text 1235 1236 def ask(self, price: str, card_id: str, season: str) -> bool: 1237 """Puts an ask at price on a card in a season 1238 1239 Args: 1240 price (str): Price to ask 1241 card_id (str): ID of the card 1242 season (str): Season of the card 1243 1244 Returns: 1245 bool: Whether the ask was successfully lodged or not 1246 """ 1247 self.logger.info(f"Asking for {price} on {card_id} season {season}") 1248 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1249 1250 data = {"auction_ask": price, "auction_submit": "ask"} 1251 response = self.request(url, data) 1252 return f"Your ask of {price} has been lodged." in response.text 1253 1254 def bid(self, price: str, card_id: str, season: str) -> bool: 1255 """Places a bid on a card in a season 1256 1257 Args: 1258 price (str): Amount of bank to bid 1259 card_id (str): ID of the card 1260 season (str): Season of the card 1261 1262 Returns: 1263 bool: Whether the bid was successfully lodged or not 1264 """ 1265 self.logger.info(f"Putting a bid for {price} on {card_id} season {season}") 1266 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1267 1268 data = {"auction_bid": price, "auction_submit": "bid"} 1269 response = self.request(url, data) 1270 1271 return f"Your bid of {price} has been lodged." in response.text 1272 1273 def remove_ask(self, price: str, card_id: str, season: str) -> bool: 1274 """Removes an ask on card_id in season at price 1275 1276 Args: 1277 price (str): Price of the ask to remove 1278 card_id (str): ID of the card 1279 season (str): Season of the card 1280 1281 Returns: 1282 bool: Whether the ask was successfully removed or not 1283 """ 1284 1285 self.logger.info(f"removing an ask for {price} on {card_id} season {season}") 1286 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1287 1288 data = {"new_price": price, "remove_ask_price": price} 1289 response = self.request(url, data) 1290 return f"Removed your ask for {price}" in response.text 1291 1292 def remove_bid(self, price: str, card_id: str, season: str) -> bool: 1293 """Removes a bid on a card 1294 1295 Args: 1296 price (str): Price of the bid to remove 1297 card_id (str): ID of the card 1298 season (str): Season of the card 1299 1300 Returns: 1301 bool: Whether the bid was successfully removed or not 1302 """ 1303 1304 self.logger.info(f"Removing a bid for {price} on {card_id} season {season}") 1305 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1306 1307 data = {"new_price": price, "remove_bid_price": price} 1308 response = self.request(url, data) 1309 1310 return f"Removed your bid for {price}" in response.text 1311 1312 def expand_deck(self, price: str) -> bool: 1313 """Upgrades deck capacity 1314 1315 Args: 1316 price (str): Price of the Upgrade 1317 1318 Returns: 1319 bool: Whether the upgrade was successfully removed or not 1320 """ 1321 1322 self.logger.info(f"Upgrading your deck at a cost of {price}") 1323 url = "https://www.nationstates.net/template-overall=none/page=deck" 1324 1325 data = {"embiggen_deck": price} 1326 response = self.request(url, data) 1327 1328 return "Increased deck capacity from" in response.text 1329 1330 def add_to_collection(self, card_id: str, card_season: str, collection_id: str): 1331 """Adds a card to collection_id 1332 1333 Args: 1334 card_id (str): Card ID 1335 card_season (str): Cards season 1336 collection_id (str): The ID of the collection you want to add to 1337 1338 Returns: 1339 bool: Whether the adding was successfully added or not 1340 """ 1341 self.logger.info(f"Adding {card_id} of season {card_season} to {collection_id}") 1342 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={card_season}" 1343 1344 data = { 1345 "manage_collections": "1", 1346 "modify_card_in_collection": "1", 1347 f"collection_{collection_id}": "1", 1348 "save_collection": "1", 1349 } 1350 response = self.request(url, data) 1351 1352 return "Updated collections." in response.text 1353 1354 def remove_from_collection( 1355 self, card_id: str, card_season: str, collection_id: str 1356 ): 1357 """Removes a card from collection_id 1358 1359 Args: 1360 card_id (str): Card ID 1361 card_season (str): Cards season 1362 collection_id (str): The ID of the collection you want to remove from 1363 1364 Returns: 1365 bool: Whether the removal was successfully added or not 1366 """ 1367 self.logger.info( 1368 f"Removing {card_id} of season {card_season} from {collection_id}" 1369 ) 1370 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={card_season}" 1371 1372 data = { 1373 "manage_collections": "1", 1374 "modify_card_in_collection": "1", 1375 "start": "0", 1376 f"collection_{collection_id}": "0", 1377 "save_collection": "1", 1378 } 1379 response = self.request(url, data) 1380 1381 return "Updated collections." in response.text 1382 1383 def create_collection(self, name: str): 1384 """Creates a collection named name 1385 1386 Args: 1387 name (str): The name of the collection you want to create 1388 1389 Returns: 1390 bool: Whether the creating was successfully added or not 1391 """ 1392 self.logger.info(f"Creating {name} collection") 1393 url = "https://www.nationstates.net/template-overall=none/page=deck" 1394 1395 data = {"edit": "1", "collection_name": name, "save_collection": "1"} 1396 response = self.request(url, data) 1397 1398 return "Created collection!" in response.text 1399 1400 def delete_collection(self, name: str): 1401 """Deletes a collection named name 1402 1403 Args: 1404 name (str): The name of the collection you want to delete 1405 1406 Returns: 1407 bool: Whether the deleting was successfully added or not 1408 """ 1409 self.logger.info(f"Deleting {name} collection") 1410 url = "https://www.nationstates.net/template-overall=none/page=deck" 1411 1412 data = {"edit": "1", "collection_name": name, "delete_collection": "1"} 1413 response = self.request(url, data) 1414 1415 return "Created collection!" in response.text 1416 1417 def can_nation_be_founded(self, name: str): 1418 """Checks if a nation can be founded 1419 1420 Args: 1421 name (str): The name of the nation you want to check 1422 1423 Returns: 1424 bool: Whether the nation can be founded or not 1425 """ 1426 self.logger.info(f"Checking {name} in boneyard") 1427 url = "https://www.nationstates.net/template-overall=none/page=boneyard" 1428 1429 data = {"nation": name, "submit": "1"} 1430 response = self.request(url, data) 1431 1432 return ( 1433 "Available! This name may be used to found a new nation." in response.text 1434 ) 1435 1436 def join_nday_faction(self, id: str): 1437 """Joins a faction in the N-Day event 1438 1439 Args: 1440 id (str): The ID of the faction you want to join 1441 1442 Returns: 1443 bool: Whether the joining was successful or not 1444 """ 1445 self.logger.info(f"Joining faction {id}") 1446 1447 url = ( 1448 f"https://www.nationstates.net/template-overall=none/page=faction/fid={id}" 1449 ) 1450 data = {"join_faction": "1"} 1451 response = self.request(url, data) 1452 1453 return " has joined " in response.text 1454 1455 def leave_nday_faction(self, id: str): 1456 """Leaves a faction in the N-Day event 1457 1458 Args: 1459 id (str): The ID of the faction you want to leave 1460 1461 Returns: 1462 bool: Whether the leaving was successful or not 1463 """ 1464 self.logger.info(f"Leaving faction {id}") 1465 1466 url = ( 1467 f"https://www.nationstates.net/template-overall=none/page=faction/fid={id}" 1468 ) 1469 data = {"leave_faction": "1"} 1470 response = self.request(url, data) 1471 1472 return " has left " in response.text
59 def __init__( 60 self, 61 script_name: str, 62 script_version: str, 63 script_author: str, 64 script_user: str, 65 keybind: str = "space", 66 link_to_src: str = "", 67 logger: logging.Logger | None = None, 68 ): 69 """A wrapper around httpx that abstracts away 70 interacting with the HTML nationstates.net site. 71 Focused on legality, correctness, and ease of use. 72 73 Args: 74 script_name (str): Name of your script 75 script_version (str): Version number of your script 76 script_author (str): Author of your script 77 script_user (str): Nation name of the user running your script 78 keybind (str, optional): Keybind to count as a user click. Defaults to "space". 79 link_to_src (str, optional): Link to the source code of your script. 80 logger (logging.Logger | None, optional): Logger to use. Will create its own with name "NSDotPy" if none is specified. Defaults to None. 81 """ 82 self.VERSION = "2.3.0" 83 # Initialize logger 84 if not logger: 85 self._init_logger() 86 else: 87 self.logger = logger 88 # Create a new httpx session 89 self._session = httpx.Client( 90 http2=True, timeout=30 91 ) # ns can b slow, 30 seconds is hopefully a good sweet spot 92 # Set the user agent to the script name, version, author, and user as recommended in the script rules thread: 93 # https://forum.nationstates.net/viewtopic.php?p=16394966&sid=be37623536dbc8cee42d8d043945b887#p16394966 94 self._lock: bool = False 95 self._set_user_agent( 96 script_name, script_version, script_author, script_user, link_to_src 97 ) 98 # Initialize nationstates specific stuff 99 self._auth_region = "rwby" 100 self.chk: str = "" 101 self.localid: str = "" 102 self.pin: str = "" 103 self.nation: str = "" 104 self.region: str = "" 105 self.keybind = keybind 106 # Make sure the nations in the user agent actually exist 107 if not self._validate_nations({script_author, script_user}): 108 raise ValueError( 109 "One of, or both, of the nations in the user agent do not exist. Make sure you're only including the nation name in the constructor, e.g. 'Thorn1000' instead of 'Devved by Thorn1000'" 110 ) 111 self.logger.info(f"Initialized. Keybind to continue is {self.keybind}.")
A wrapper around httpx that abstracts away interacting with the HTML nationstates.net site. Focused on legality, correctness, and ease of use.
Arguments:
- script_name (str): Name of your script
- script_version (str): Version number of your script
- script_author (str): Author of your script
- script_user (str): Nation name of the user running your script
- keybind (str, optional): Keybind to count as a user click. Defaults to "space".
- link_to_src (str, optional): Link to the source code of your script.
- logger (logging.Logger | None, optional): Logger to use. Will create its own with name "NSDotPy" if none is specified. Defaults to None.
306 def request( 307 self, 308 url: str, 309 data: dict = {}, 310 files: dict = {}, 311 follow_redirects: bool = False, 312 ) -> httpx.Response: 313 """Sends a request to the given url with the given data and files. 314 315 Args: 316 url (str): URL to send the request to 317 data (dict, optional): Payload to send with the request 318 files (dict, optional): Payload to send with requests that upload files 319 320 Returns: 321 httpx.Response: The response from the server 322 """ 323 if any( 324 banned_page in canonicalize(url) 325 for banned_page in { 326 "page=telegrams", 327 "page=dilemmas", 328 "page=compose_telegram", 329 "page=store", 330 "page=help", 331 } 332 ): 333 raise ValueError( 334 "You cannot use a tool to interact with telegrams, issues, getting help, or the store. Read up on the script rules: https://forum.nationstates.net/viewtopic.php?p=16394966#p16394966" 335 ) 336 if "api.cgi" in canonicalize(url): 337 # you should be using api_request for api requests 338 raise ValueError("You should be using api_request() for api requests.") 339 elif "nationstates" in canonicalize(url): 340 # do all the things that need to be done for html requests 341 if self._lock: 342 # if lock is true then we're already in the middle of a 343 # request and we're in danger of breaking the simultaneity rule 344 # so raise an error 345 raise PermissionError( 346 "You're already in the middle of a request. Stop trying to violate simultaneity." 347 ) 348 self._lock = True 349 response = self._html_request(url, data, files, follow_redirects) 350 self._lock = False 351 else: 352 # if its not nationstates then just pass the request through 353 response = self._session.post( 354 url, data=data, follow_redirects=follow_redirects 355 ) 356 return response
Sends a request to the given url with the given data and files.
Arguments:
- url (str): URL to send the request to
- data (dict, optional): Payload to send with the request
- files (dict, optional): Payload to send with requests that upload files
Returns:
httpx.Response: The response from the server
358 def api_request( 359 self, 360 api: str, 361 *, 362 target: str = "", 363 shard: str | set[str] = "", 364 password: str = "", 365 constant_rate_limit: bool = False, 366 ) -> benedict: 367 """Sends a request to the nationstates api with the given data. 368 369 Args: 370 api (str): The api to send the request to. Must be "nation", "region", "world", or "wa" 371 target (str, optional): The nation, region, or wa council to target. Required for non-world api requests. 372 shard (str, optional): The shard, or shards, you're requesting for. Must be a valid shard for the given api. Only required for world and wa api requests. 373 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 374 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 375 376 Returns: 377 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 378 """ 379 # TODO: probably move this responsibility to a third party api library to avoid reinventing the wheel 380 # if one exists of sufficient quality thats AGPLv3 compatible 381 if api not in {"nation", "region", "world", "wa"}: 382 raise ValueError("api must be 'nation', 'region', 'world', or 'wa'") 383 if api != "world" and not target: 384 raise ValueError("target must be specified for non-world api requests") 385 if api in {"wa", "world"} and not shard: 386 raise ValueError("shard must be specified for world and wa api requests") 387 # end argument validation 388 # shard validation 389 if type(shard) == str: 390 shard = {shard} 391 self._validate_shards(api, shard) # type: ignore 392 # end shard validation 393 data = { 394 "v": "12", 395 } 396 if api != "world": 397 data[api] = target 398 if shard: 399 data["q"] = "+".join(shard) 400 url = "https://www.nationstates.net/cgi-bin/api.cgi" 401 if password: 402 self._session.headers["X-Password"] = password 403 if self.pin: 404 self._session.headers["X-Pin"] = self.pin 405 # rate limiting section 406 response = self._session.post(url, data=data) 407 # if the server tells us to wait, wait 408 self._wait_for_ratelimit(response.headers, constant_rate_limit) 409 response.raise_for_status() 410 parsed_response = benedict.from_xml(response.text, keyattr_dynamic=True) 411 parsed_response.standardize() 412 parsed_response: benedict = parsed_response[api] # type: ignore 413 return parsed_response
Sends a request to the nationstates api with the given data.
Arguments:
- api (str): The api to send the request to. Must be "nation", "region", "world", or "wa"
- target (str, optional): The nation, region, or wa council to target. Required for non-world api requests.
- shard (str, optional): The shard, or shards, you're requesting for. Must be a valid shard for the given api. Only required for world and wa api requests.
- password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site.
- constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False.
Returns:
benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support.
415 def api_issue( 416 self, 417 nation: str, 418 issue: int, 419 option: int, 420 password: str = "", 421 constant_rate_limit: bool = False, 422 ) -> benedict: 423 """Answers an issue via the API. 424 425 Args: 426 nation (str): The nation to perform the command with. 427 issue (int): the ID of the issue. 428 option (int): the issue option to choose. 429 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 430 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 431 432 Returns: 433 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 434 """ 435 if not (password or self.pin): 436 raise ValueError("must specify authentication") 437 data = { 438 "v": "12", 439 "c": "issue", 440 "nation": canonicalize(nation), 441 "issue": issue, 442 "option": option, 443 } 444 url = "https://www.nationstates.net/cgi-bin/api.cgi" 445 if password: 446 self._session.headers["X-Password"] = password 447 if self.pin: 448 self._session.headers["X-Pin"] = self.pin 449 # rate limiting section 450 response = self._session.get(url, params=data) 451 # if the server tells us to wait, wait 452 self._wait_for_ratelimit(response.headers, constant_rate_limit) 453 response.raise_for_status() 454 parsed_response = benedict.from_xml(response.text, keyattr_dynamic=True) 455 parsed_response.standardize() 456 parsed_response: benedict = parsed_response["nation"] # type: ignore 457 return parsed_response
Answers an issue via the API.
Arguments:
- nation (str): The nation to perform the command with.
- issue (int): the ID of the issue.
- option (int): the issue option to choose.
- password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site.
- constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False.
Returns:
benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support.
459 def api_command( 460 self, 461 nation: str, 462 command: str, 463 data: dict, 464 password: str = "", 465 mode: str = "", 466 constant_rate_limit: bool = False, 467 ) -> benedict: 468 """Sends a non-issue command to the nationstates api with the given data and password. 469 470 Args: 471 nation (str): The nation to perform the command with. 472 command (str): The command to perform. Must be "giftcard", "dispatch", "rmbpost" 473 data (str, optional): The unique data to send with the parameters of the command; consult the API docs for more information. 474 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 475 mode (str, optional): Whether to prepare or to execute the command. If value is given, does one of the two and returns result, if no value is given, does both and returns result of execute. 476 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 477 478 Returns: 479 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 480 """ 481 if command not in {"giftcard", "dispatch", "rmbpost"}: 482 raise ValueError("command must be 'giftcard', 'dispatch', or 'rmbpost'") 483 if not (password or self.pin): 484 raise ValueError("must specify authentication") 485 if mode not in {"", "prepare", "execute"}: 486 raise ValueError("mode must be prepare or execute") 487 data["v"] = "12" 488 data["nation"] = canonicalize(nation) 489 data["c"] = command 490 data["mode"] = mode if mode else "prepare" # if no mode than first prepare 491 url = "https://www.nationstates.net/cgi-bin/api.cgi" 492 if password: 493 self._session.headers["X-Password"] = password 494 if self.pin: 495 self._session.headers["X-Pin"] = self.pin 496 # rate limiting section 497 response = self._session.get(url, params=data) 498 # if the server tells us to wait, wait 499 self._wait_for_ratelimit(response.headers, constant_rate_limit) 500 response.raise_for_status() 501 parsed_response = benedict.from_xml(response.text, keyattr_dynamic=True) 502 parsed_response.standardize() 503 parsed_response: benedict = parsed_response["nation"] # type: ignore 504 if mode == "": 505 # if no mode was specified earlier, repeat command with execute and token 506 data["token"] = parsed_response["success"] 507 return self.api_command(nation, command, data, mode="execute") 508 else: 509 return parsed_response
Sends a non-issue command to the nationstates api with the given data and password.
Arguments:
- nation (str): The nation to perform the command with.
- command (str): The command to perform. Must be "giftcard", "dispatch", "rmbpost"
- data (str, optional): The unique data to send with the parameters of the command; consult the API docs for more information.
- password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site.
- mode (str, optional): Whether to prepare or to execute the command. If value is given, does one of the two and returns result, if no value is given, does both and returns result of execute.
- constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False.
Returns:
benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support.
511 def api_giftcard( 512 self, 513 nation: str, 514 card_id: int, 515 season: int, 516 recipient: str, 517 password: str = "", 518 constant_rate_limit: bool = False, 519 ) -> benedict: 520 """Gifts a card using the API. 521 522 Args: 523 nation (str): The nation to perform the command with. 524 card_id (int): The ID of the card to gift. 525 season (int): The season of the card to gift. 526 recipient (str): The nation to gift the card to. 527 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 528 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 529 530 Returns: 531 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 532 """ 533 data = {"cardid": card_id, "season": season, "to": canonicalize(recipient)} 534 return self.api_command( 535 nation, "giftcard", data, password, constant_rate_limit=constant_rate_limit 536 )
Gifts a card using the API.
Arguments:
- nation (str): The nation to perform the command with.
- card_id (int): The ID of the card to gift.
- season (int): The season of the card to gift.
- recipient (str): The nation to gift the card to.
- password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site.
- constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False.
Returns:
benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support.
538 def api_dispatch( 539 self, 540 nation: str, 541 action: str, 542 title: str = "", 543 text: str = "", 544 category: int = 0, 545 subcategory: int = 0, 546 dispatchid: int = 0, 547 password: str = "", 548 constant_rate_limit: bool = False, 549 ) -> benedict: 550 """Add, edit, or remove a dispatch. 551 552 Args: 553 nation (str): The nation to perform the command with. 554 action (str): The action to take. Must be "add", "edit", "remove" 555 title (str, optional): The dispatch title when adding or editing. 556 text (str, optional): The dispatch text when adding or editing. 557 category: (int, optional), The category ID when adding or editing. 558 subcategory (int, optional): The subcategory ID when adding or editing. 559 dispatchid (int, optional): The dispatch ID when editing or removing. 560 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 561 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 562 563 Returns: 564 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 565 """ 566 # TODO: maybe consider splitting these three functions? 567 # TODO: maybe create enums for category and subcategory 568 if action not in {"add", "edit", "remove"}: 569 raise ValueError("action must be 'add', 'edit', or 'remove'") 570 if action != "remove" and not all({title, text, category, subcategory}): 571 raise ValueError("must specify title, text, category, and subcategory") 572 if action != "add" and not dispatchid: 573 raise ValueError("must specify a dispatch id") 574 575 data = {"dispatch": action} 576 if title: 577 data["title"] = title 578 if text: 579 data["text"] = text 580 if category: 581 data["category"] = category 582 if subcategory: 583 data["subcategory"] = subcategory 584 if dispatchid: 585 data["dispatchid"] = dispatchid 586 return self.api_command( 587 nation, "dispatch", data, password, constant_rate_limit=constant_rate_limit 588 )
Add, edit, or remove a dispatch.
Arguments:
- nation (str): The nation to perform the command with.
- action (str): The action to take. Must be "add", "edit", "remove"
- title (str, optional): The dispatch title when adding or editing.
- text (str, optional): The dispatch text when adding or editing.
- category: (int, optional), The category ID when adding or editing.
- subcategory (int, optional): The subcategory ID when adding or editing.
- dispatchid (int, optional): The dispatch ID when editing or removing.
- password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site.
- constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False.
Returns:
benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support.
590 def api_rmb( 591 self, 592 nation: str, 593 region: str, 594 text: str, 595 password: str = "", 596 constant_rate_limit: bool = False, 597 ) -> benedict: 598 """Post a message on the regional message board via the API. 599 600 Args: 601 nation (str): The nation to perform the command with. 602 region (str): the region to post the message in. 603 text (str): the text to post. 604 password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site. 605 constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False. 606 607 Returns: 608 benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support. 609 """ 610 data = {"region": region, "text": text} 611 return self.api_command( 612 nation, "rmbpost", data, password, constant_rate_limit=constant_rate_limit 613 )
Post a message on the regional message board via the API.
Arguments:
- nation (str): The nation to perform the command with.
- region (str): the region to post the message in.
- text (str): the text to post.
- password (str, optional): The password to use for authenticating private api requests. Defaults to "". Not required if already signed in, whether through the api or through the HTML site.
- constant_rate_limit (bool, optional): If True, will always rate limit. If False, will only rate limit when there's less than 10 requests left in the current bucket. Defaults to False.
Returns:
benedict: A benedict object containing the response from the server. Acts like a dictionary, with keypath and keylist support.
615 def login(self, nation: str, password: str) -> bool: 616 """Logs in to the nationstates site. 617 618 Args: 619 nation (str): Nation name 620 password (str): Nation password 621 622 Returns: 623 bool: True if login was successful, False otherwise 624 """ 625 self.logger.info(f"Logging in to {nation}") 626 url = f"https://www.nationstates.net/page=display_region/region={self._auth_region}" 627 # shoutouts to roavin for telling me i had to have page=display_region in the url so it'd work with a userclick parameter 628 629 data = { 630 "nation": canonicalize(nation), 631 "password": password, 632 "theme": "century", 633 "logging_in": "1", 634 "submit": "Login", 635 } 636 637 response = self.request(url, data) 638 639 soup = BeautifulSoup(response.text, "lxml") 640 # checks if the body tag has your nation name in it; if it does, you're logged in 641 if not soup.find("body", {"data-nname": canonicalize(nation)}): 642 return False 643 644 self.nation = canonicalize(nation) 645 return True
Logs in to the nationstates site.
Arguments:
- nation (str): Nation name
- password (str): Nation password
Returns:
bool: True if login was successful, False otherwise
647 def change_nation_flag(self, flag_filename: str) -> bool: 648 """Changes the nation flag to the given image. 649 650 Args: 651 flag_filename (str): Filename of the flag to change to 652 653 Returns: 654 bool: True if the flag was changed, False otherwise 655 """ 656 self.logger.info(f"Changing flag on {self.nation}") 657 # THIS WAS SO FUCKING FRUSTRATING BUT IT WORKS NOW AND IM NEVER TOUCHING THIS BULLSHIT UNLESS NS BREAKS IT AGAIN 658 url = "https://www.nationstates.net/cgi-bin/upload.cgi" 659 660 data = { 661 "nationname": self.nation, 662 } 663 files = { 664 "file": ( 665 flag_filename, 666 open(flag_filename, "rb"), 667 mimetypes.guess_type(flag_filename)[0], 668 ) 669 } 670 671 response = self.request(url, data=data, files=files) 672 673 if "page=settings" in response.headers["location"]: 674 self.refresh_auth_values() 675 return True 676 elif "Just a moment..." in response.text: 677 self.logger.warning( 678 "Cloudflare blocked you idiot get fucked have fun with that like I had to lmaoooooooooo" 679 ) 680 return False
Changes the nation flag to the given image.
Arguments:
- flag_filename (str): Filename of the flag to change to
Returns:
bool: True if the flag was changed, False otherwise
682 def change_nation_settings( 683 self, 684 *, 685 email: str = "", 686 pretitle: str = "", 687 slogan: str = "", 688 currency: str = "", 689 animal: str = "", 690 demonym_noun: str = "", 691 demonym_adjective: str = "", 692 demonym_plural: str = "", 693 new_password: str = "", 694 ) -> bool: 695 """Given a logged in session, changes customizable fields and settings of the logged in nation. 696 Variables must be explicitly named in the call to the function, e.g. "session.change_nation_settings(pretitle='Join Lily', currency='Join Lily')" 697 698 Args: 699 email (str, optional): New email for WA apps. 700 pretitle (str, optional): New pretitle of the nation. Max length of 28. Nation must have minimum population of 250 million. 701 slogan (str, optional): New Slogan/Motto of the nation. Max length of 55. 702 currency (str, optional): New currency of the nation. Max length of 40. 703 animal (str, optional): New national animal of the nation. Max length of 40. 704 demonym_noun (str, optional): Noun the nation will refer to its citizens as. Max length of 44. 705 demonym_adjective (str, optional): Adjective the nation will refer to its citizens as. Max length of 44. 706 demonym_plural (str, optional): Plural form of "demonym_noun". Max length of 44. 707 new_password (str, optional): New password to assign to the nation. 708 709 Returns: 710 bool: True if changes were successful, False otherwise. 711 """ 712 self.logger.info(f"Changing settings on {self.nation}") 713 url = "https://www.nationstates.net/template-overall=none/page=settings" 714 715 data = { 716 "type": pretitle, 717 "slogan": slogan, 718 "currency": currency, 719 "animal": animal, 720 "demonym2": demonym_noun, 721 "demonym": demonym_adjective, 722 "demonym2pl": demonym_plural, 723 "email": email, 724 "password": new_password, 725 "confirm_password": new_password, 726 "update": " Update ", 727 } 728 # remove keys that have empty values 729 data = {k: v for k, v in data.items() if v} 730 # make sure everything is following the proper length limits and only contains acceptable characters 731 self._validate_fields(data) 732 733 response = self.request(url, data) 734 return "Your settings have been successfully updated." in response.text
Given a logged in session, changes customizable fields and settings of the logged in nation. Variables must be explicitly named in the call to the function, e.g. "session.change_nation_settings(pretitle='Join Lily', currency='Join Lily')"
Arguments:
- email (str, optional): New email for WA apps.
- pretitle (str, optional): New pretitle of the nation. Max length of 28. Nation must have minimum population of 250 million.
- slogan (str, optional): New Slogan/Motto of the nation. Max length of 55.
- currency (str, optional): New currency of the nation. Max length of 40.
- animal (str, optional): New national animal of the nation. Max length of 40.
- demonym_noun (str, optional): Noun the nation will refer to its citizens as. Max length of 44.
- demonym_adjective (str, optional): Adjective the nation will refer to its citizens as. Max length of 44.
- demonym_plural (str, optional): Plural form of "demonym_noun". Max length of 44.
- new_password (str, optional): New password to assign to the nation.
Returns:
bool: True if changes were successful, False otherwise.
736 def move_to_region(self, region: str, password: str = "") -> bool: 737 """Moves the nation to the given region. 738 739 Args: 740 region (str): Region to move to 741 password (str, optional): Region password, if the region is passworded 742 743 Returns: 744 bool: True if the move was successful, False otherwise 745 """ 746 self.logger.info(f"Moving {self.nation} to {region}") 747 url = "https://www.nationstates.net/template-overall=none/page=change_region" 748 749 data = {"region_name": region, "move_region": "1"} 750 if password: 751 data["password"] = password 752 response = self.request(url, data) 753 754 if "Success!" in response.text: 755 self.region = canonicalize(region) 756 return True 757 return False
Moves the nation to the given region.
Arguments:
- region (str): Region to move to
- password (str, optional): Region password, if the region is passworded
Returns:
bool: True if the move was successful, False otherwise
759 def vote(self, pollid: str, option: str) -> bool: 760 """Votes on a poll. 761 762 Args: 763 pollid (str): ID of the poll to vote on, e.g. "199747" 764 option (str): Option to vote for (starts at 0) 765 766 Returns: 767 bool: True if the vote was successful, False otherwise 768 """ 769 self.logger.info(f"Voting on poll {pollid} with {self.nation}") 770 url = f"https://www.nationstates.net/template-overall=none/page=poll/p={pollid}" 771 772 data = {"pollid": pollid, "q1": option, "poll_submit": "1"} 773 response = self.request(url, data) 774 775 return "Your vote has been lodged." in response.text
Votes on a poll.
Arguments:
- pollid (str): ID of the poll to vote on, e.g. "199747"
- option (str): Option to vote for (starts at 0)
Returns:
bool: True if the vote was successful, False otherwise
779 def join_wa(self, nation: str, app_id: str) -> bool: 780 """Joins the WA with the given nation. 781 782 Args: 783 nation (str): Nation to join the WA with 784 app_id (str): ID of the WA application to use 785 786 Returns: 787 bool: True if the join was successful, False otherwise 788 """ 789 self.logger.info(f"Joining WA with {nation}") 790 url = "https://www.nationstates.net/cgi-bin/join_un.cgi" 791 792 data = {"nation": canonicalize(nation), "appid": app_id.strip()} 793 response = self.request(url, data) 794 795 if "?welcome=1" in response.headers["location"]: 796 # since we're just getting thrown into a cgi script, we'll have to manually grab authentication values 797 self.refresh_auth_values() 798 return True 799 return False
Joins the WA with the given nation.
Arguments:
- nation (str): Nation to join the WA with
- app_id (str): ID of the WA application to use
Returns:
bool: True if the join was successful, False otherwise
801 def resign_wa(self): 802 """Resigns from the WA. 803 804 Returns: 805 bool: True if the resignation was successful, False otherwise 806 """ 807 self.logger.info("Resigning from WA") 808 url = "https://www.nationstates.net/template-overall=none/page=UN_status" 809 810 data = {"action": "leave_UN", "submit": "1"} 811 response = self.request(url, data) 812 813 return "From this moment forward, your nation is on its own." in response.text
Resigns from the WA.
Returns:
bool: True if the resignation was successful, False otherwise
815 def apply_wa(self, reapply: bool = True) -> bool: 816 """Applies to the WA. 817 818 Args: 819 reapply (bool, optional): Whether to reapply if you've been sent an application that's still valid. Defaults to True. 820 821 Returns: 822 bool: True if the application was successful, False otherwise 823 """ 824 self.logger.info(f"Applying to WA with {self.nation}") 825 url = "https://www.nationstates.net/template-overall=none/page=UN_status" 826 827 data = {"action": "join_UN"} 828 if reapply: 829 data["resend"] = "1" 830 else: 831 data["submit"] = "1" 832 833 response = self.request(url, data) 834 return ( 835 "Your application to join the World Assembly has been received!" 836 in response.text 837 )
Applies to the WA.
Arguments:
- reapply (bool, optional): Whether to reapply if you've been sent an application that's still valid. Defaults to True.
Returns:
bool: True if the application was successful, False otherwise
839 def endorse(self, nation: str, endorse: bool = True) -> bool: 840 """Endorses the given nation. 841 842 Args: 843 nation (str): Nation to endorse 844 endorse (bool, optional): True=endorse, False=unendorse. Defaults to True. 845 846 Returns: 847 bool: True if the endorsement was successful, False otherwise 848 """ 849 self.logger.info( 850 f"{('Unendorsing', 'Endorsing')[endorse]} {nation} with {self.nation}" 851 ) 852 url = "https://www.nationstates.net/cgi-bin/endorse.cgi" 853 854 data = { 855 "nation": canonicalize(nation), 856 "action": "endorse" if endorse else "unendorse", 857 } 858 response = self.request(url, data) 859 860 return f"nation={canonicalize(nation)}" in response.headers["location"]
Endorses the given nation.
Arguments:
- nation (str): Nation to endorse
- endorse (bool, optional): True=endorse, False=unendorse. Defaults to True.
Returns:
bool: True if the endorsement was successful, False otherwise
862 def clear_dossier(self) -> bool: 863 """Clears a logged in nation's dossier. 864 865 Returns: 866 bool: Whether it was successful or not 867 """ 868 869 self.logger.info(f"Clearing dossier on {self.nation}") 870 url = "https://www.nationstates.net/template-overall=none/page=dossier" 871 data = {"clear_dossier": "1"} 872 response = self.request(url, data) 873 874 return "Dossier cleared of nations." in response.text
Clears a logged in nation's dossier.
Returns:
bool: Whether it was successful or not
876 def add_to_dossier(self, nations: list[str] | str) -> bool: 877 """Adds nations to the logged in nation's dossier. 878 879 Args: 880 nations (list[str] | str): List of nations to add, or a single nation 881 882 Returns: 883 bool: Whether it was successful or not 884 """ 885 886 self.logger.info(f"Adding {nations} to dossier on {self.nation}") 887 url = "https://www.nationstates.net/dossier.cgi" 888 data = { 889 "currentnation": canonicalize(self.nation), 890 "action_append": "Upload Nation Dossier File", 891 } 892 files = { 893 "file": ( 894 "dossier.txt", 895 "\n".join(nations).strip() if type(nations) is list else nations, 896 "text/plain", 897 ), 898 } 899 response = self.request(url, data, files=files) 900 901 self.refresh_auth_values() 902 return "appended=" in response.headers["location"]
Adds nations to the logged in nation's dossier.
Arguments:
- nations (list[str] | str): List of nations to add, or a single nation
Returns:
bool: Whether it was successful or not
904 def wa_vote(self, council: str, vote: str) -> bool: 905 """Votes on the current WA resolution. 906 907 Args: 908 council (str): Must be "ga" for general assembly, "sc" for security council. 909 vote (str): Must be "for" or "against". 910 911 Returns: 912 bool: Whether the vote was successful or not 913 """ 914 self.logger.info( 915 f"Voting {vote} on {council.upper()} resolution with {self.nation}" 916 ) 917 if council not in {"ga", "sc"}: 918 raise ValueError("council must be 'ga' or 'sc'") 919 if vote not in {"for", "against"}: 920 raise ValueError("vote must be 'for' or 'against'") 921 self.logger.info("Voting on WA resolution") 922 923 url = f"https://www.nationstates.net/template-overall=none/page={council}" 924 data = { 925 "vote": f"Vote {vote.capitalize()}", 926 } 927 response = self.request(url, data) 928 929 return "Your vote has been lodged." in response.text
Votes on the current WA resolution.
Arguments:
- council (str): Must be "ga" for general assembly, "sc" for security council.
- vote (str): Must be "for" or "against".
Returns:
bool: Whether the vote was successful or not
931 def refound_nation(self, nation: str, password: str) -> bool: 932 """Refounds a nation. 933 934 Args: 935 nation (str): Name of the nation to refound 936 password (str): Password to the nation 937 938 Returns: 939 bool: Whether the nation was successfully refounded or not 940 """ 941 url = "https://www.nationstates.net/template-overall=none/" 942 data = { 943 "logging_in": "1", 944 "restore_password": password, 945 "restore_nation": "1", 946 "nation": nation, 947 } 948 response = self.request(url, data=data) 949 if response.status_code == 302: 950 self.nation = nation 951 self.refresh_auth_values() 952 return True 953 return False
Refounds a nation.
Arguments:
- nation (str): Name of the nation to refound
- password (str): Password to the nation
Returns:
bool: Whether the nation was successfully refounded or not
957 def create_region( 958 self, 959 region_name: str, 960 wfe: str, 961 *, 962 password: str = "", 963 frontier: bool = False, 964 executive_delegacy: bool = False, 965 ) -> bool: 966 """Creates a new region. 967 968 Args: 969 region_name (str): Name of the region 970 wfe (str): WFE of the region 971 password (str, optional): Password to the region. Defaults to "". 972 frontier (bool, optional): Whether or not the region is a frontier. Defaults to False. 973 executive_delegacy (bool, optional): Whether or not the region has an executive WA delegacy. Defaults to False. Ignored if frontier is True. 974 975 Returns: 976 bool: Whether the region was successfully created or not 977 """ 978 self.logger.info(f"Creating new region {region_name}") 979 url = "https://www.nationstates.net/template-overall=none/page=create_region" 980 data = { 981 "page": "create_region", 982 "region_name": region_name.strip(), 983 "desc": wfe.strip(), 984 "create_region": "1", 985 } 986 if password: 987 data |= {"pw": "1", "rpassword": password} 988 if frontier: 989 data |= {"is_frontier": "1"} 990 elif executive_delegacy: 991 data |= {"delegate_control": "1"} 992 response = self.request(url, data) 993 return "Success! You have founded " in response.text
Creates a new region.
Arguments:
- region_name (str): Name of the region
- wfe (str): WFE of the region
- password (str, optional): Password to the region. Defaults to "".
- frontier (bool, optional): Whether or not the region is a frontier. Defaults to False.
- executive_delegacy (bool, optional): Whether or not the region has an executive WA delegacy. Defaults to False. Ignored if frontier is True.
Returns:
bool: Whether the region was successfully created or not
995 def upload_to_region(self, type: str, filename: str) -> str: 996 """Uploads a file to the current region. 997 998 Args: 999 type (str): Type of file to upload. Must be "flag" or "banner". 1000 filename (str): Name of the file to upload. e.g. "myflag.png" 1001 1002 Raises: 1003 ValueError: If type is not "flag" or "banner" 1004 1005 Returns: 1006 str: Empty string if the upload failed, otherwise the ID of the uploaded file 1007 """ 1008 self.logger.info(f"Uploading {filename} to {self.region}") 1009 if type not in {"flag", "banner"}: 1010 raise ValueError("type must be 'flag' or 'banner'") 1011 url = "https://www.nationstates.net/cgi-bin/upload.cgi" 1012 data = { 1013 "uploadtype": f"r{type}", 1014 "page": "region_control", 1015 "region": self.region, 1016 "expect": "json", 1017 } 1018 files = { 1019 f"file_upload_r{type}": ( 1020 filename, 1021 open(filename, "rb"), 1022 mimetypes.guess_type(filename)[0], 1023 ) 1024 } 1025 response = self.request(url, data, files=files) 1026 return "" if "id" not in response.json() else response.json()["id"]
Uploads a file to the current region.
Arguments:
- type (str): Type of file to upload. Must be "flag" or "banner".
- filename (str): Name of the file to upload. e.g. "myflag.png"
Raises:
- ValueError: If type is not "flag" or "banner"
Returns:
str: Empty string if the upload failed, otherwise the ID of the uploaded file
1061 def change_wfe(self, wfe: str = "") -> bool: 1062 """Changes the WFE of the current region. 1063 1064 Args: 1065 wfe (str, optional): World Factbook Entry to change to. Defaults to the oldest WFE the region has, for detags. 1066 1067 Returns: 1068 bool: True if successful, False otherwise 1069 """ 1070 self.logger.info(f"Changing WFE for {self.region}") 1071 if not wfe: 1072 wfe = self._get_detag_wfe() # haku im sorry for hitting your site so much 1073 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1074 data = { 1075 "message": wfe.encode("iso-8859-1", "xmlcharrefreplace") 1076 .decode() 1077 .strip(), # lol. 1078 "setwfebutton": "1", 1079 } 1080 response = self.request(url, data) 1081 return "World Factbook Entry updated!" in response.text
Changes the WFE of the current region.
Arguments:
- wfe (str, optional): World Factbook Entry to change to. Defaults to the oldest WFE the region has, for detags.
Returns:
bool: True if successful, False otherwise
1085 def request_embassy(self, target: str) -> bool: 1086 """Requests an embassy with a region. 1087 1088 Args: 1089 target (str): The region to request the embassy with. 1090 1091 Returns: 1092 bool: Whether the request was successfully sent or not 1093 """ 1094 self.logger.info(f"Requesting embassy with {target}") 1095 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1096 data = { 1097 "requestembassyregion": target, 1098 "requestembassy": "1", # it's silly that requesting needs this but not closing, aborting, or cancelling 1099 } 1100 response = self.request(url, data) 1101 return "Your proposal for the construction of embassies with" in response.text
Requests an embassy with a region.
Arguments:
- target (str): The region to request the embassy with.
Returns:
bool: Whether the request was successfully sent or not
1103 def close_embassy(self, target: str) -> bool: 1104 """Closes an embassy with a region. 1105 1106 Args: 1107 target (str): The region with which to close the embassy. 1108 1109 Returns: 1110 bool: Whether the embassy was successfully closed or not 1111 """ 1112 self.logger.info(f"Closing embassy with {target}") 1113 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1114 data = {"cancelembassyregion": target} 1115 response = self.request(url, data) 1116 return " has been scheduled for demolition." in response.text
Closes an embassy with a region.
Arguments:
- target (str): The region with which to close the embassy.
Returns:
bool: Whether the embassy was successfully closed or not
1118 def abort_embassy(self, target: str) -> bool: 1119 """Aborts an embassy with a region. 1120 1121 Args: 1122 target (str): The region with which to abort the embassy. 1123 1124 Returns: 1125 bool: Whether the embassy was successfully aborted or not 1126 """ 1127 self.logger.info(f"Aborting embassy with {target}") 1128 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1129 data = {"abortembassyregion": target} 1130 response = self.request(url, data) 1131 return " aborted." in response.text
Aborts an embassy with a region.
Arguments:
- target (str): The region with which to abort the embassy.
Returns:
bool: Whether the embassy was successfully aborted or not
1133 def cancel_embassy(self, target: str) -> bool: 1134 """Cancels an embassy with a region. 1135 1136 Args: 1137 target (str): The region with which to cancel the embassy. 1138 1139 Returns: 1140 bool: Whether the embassy was successfully cancelled or not 1141 """ 1142 self.logger.info(f"Cancelling embassy with {target}") 1143 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1144 data = {"cancelembassyclosureregion": target} 1145 response = self.request(url, data) 1146 return "Embassy closure order cancelled." in response.text
Cancels an embassy with a region.
Arguments:
- target (str): The region with which to cancel the embassy.
Returns:
bool: Whether the embassy was successfully cancelled or not
1150 def tag(self, action: str, tag: str) -> bool: 1151 """Adds or removes a tag to the current region. 1152 1153 Args: 1154 action (str): The action to take. Must be "add" or "remove". 1155 tag (str): The tag to add or remove. 1156 1157 Raises: 1158 ValueError: If action is not "add" or "remove", or if tag is not a valid tag. 1159 1160 Returns: 1161 bool: Whether the tag was successfully added or removed 1162 """ 1163 if action not in {"add", "remove"}: 1164 raise ValueError("action must be 'add' or 'remove'") 1165 if canonicalize(tag) not in valid.REGION_TAGS: 1166 raise ValueError(f"{tag} is not a valid tag") 1167 self.logger.info(f"{action.capitalize()}ing tag {tag} for {self.region}") 1168 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1169 data = { 1170 f"{action}_tag": canonicalize(tag), 1171 "updatetagsbutton": "1", 1172 } 1173 response = self.request(url, data) 1174 return "Region Tags updated!" in response.text
Adds or removes a tag to the current region.
Arguments:
- action (str): The action to take. Must be "add" or "remove".
- tag (str): The tag to add or remove.
Raises:
- ValueError: If action is not "add" or "remove", or if tag is not a valid tag.
Returns:
bool: Whether the tag was successfully added or removed
1176 def eject(self, nation: str) -> bool: 1177 """Ejects a nation from the current region. Note that a 1 second delay is required before ejecting another nation. 1178 1179 Args: 1180 nation (str): The nation to eject. 1181 1182 Returns: 1183 bool: Whether the nation was successfully ejected or not 1184 """ 1185 self.logger.info(f"Ejecting {nation} from {self.region}") 1186 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1187 data = {"nation_name": nation, "eject": "1"} 1188 response = self.request(url, data) 1189 return "has been ejected from " in response.text
Ejects a nation from the current region. Note that a 1 second delay is required before ejecting another nation.
Arguments:
- nation (str): The nation to eject.
Returns:
bool: Whether the nation was successfully ejected or not
1191 def banject(self, nation: str) -> bool: 1192 """Bans a nation from the current region. Note that a 1 second delay is required before banjecting another nation. 1193 1194 Args: 1195 nation (str): The nation to banject. 1196 1197 Returns: 1198 bool: Whether the nation was successfully banjected or not 1199 """ 1200 self.logger.info(f"Banjecting {nation} from {self.region}") 1201 url = "https://www.nationstates.net/template-overall=none/page=region_control/" 1202 data = {"nation_name": nation, "ban": "1"} 1203 response = self.request(url, data) 1204 return "has been ejected and banned from " in response.text
Bans a nation from the current region. Note that a 1 second delay is required before banjecting another nation.
Arguments:
- nation (str): The nation to banject.
Returns:
bool: Whether the nation was successfully banjected or not
1208 def junk_card(self, id: str, season: str) -> bool: 1209 """Junks a card from the current nation's deck. 1210 Args: 1211 id (str): ID of the card to junk 1212 season (str): Season of the card to junk 1213 Returns: 1214 bool: Whether the card was successfully junked or not 1215 """ 1216 self.logger.info(f"Junking card {id} from season {season}") 1217 url = "https://www.nationstates.net/template-overall=none/page=deck" 1218 1219 data = {"page": "ajax3", "a": "junkcard", "card": id, "season": season} 1220 response = self.request(url, data) 1221 1222 return "Your Deck" in response.text
Junks a card from the current nation's deck.
Arguments:
- id (str): ID of the card to junk
- season (str): Season of the card to junk
Returns:
bool: Whether the card was successfully junked or not
1224 def open_pack(self) -> bool: 1225 """Opens a card pack. 1226 1227 Returns: 1228 bool: Whether the bid was successfully removed or not 1229 """ 1230 self.logger.info("Opening trading card pack") 1231 url = "https://www.nationstates.net/template-overall=none/page=deck" 1232 data = {"open_loot_box": "1"} 1233 response = self.request(url, data) 1234 return "Tap cards to reveal..." in response.text
Opens a card pack.
Returns:
bool: Whether the bid was successfully removed or not
1236 def ask(self, price: str, card_id: str, season: str) -> bool: 1237 """Puts an ask at price on a card in a season 1238 1239 Args: 1240 price (str): Price to ask 1241 card_id (str): ID of the card 1242 season (str): Season of the card 1243 1244 Returns: 1245 bool: Whether the ask was successfully lodged or not 1246 """ 1247 self.logger.info(f"Asking for {price} on {card_id} season {season}") 1248 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1249 1250 data = {"auction_ask": price, "auction_submit": "ask"} 1251 response = self.request(url, data) 1252 return f"Your ask of {price} has been lodged." in response.text
Puts an ask at price on a card in a season
Arguments:
- price (str): Price to ask
- card_id (str): ID of the card
- season (str): Season of the card
Returns:
bool: Whether the ask was successfully lodged or not
1254 def bid(self, price: str, card_id: str, season: str) -> bool: 1255 """Places a bid on a card in a season 1256 1257 Args: 1258 price (str): Amount of bank to bid 1259 card_id (str): ID of the card 1260 season (str): Season of the card 1261 1262 Returns: 1263 bool: Whether the bid was successfully lodged or not 1264 """ 1265 self.logger.info(f"Putting a bid for {price} on {card_id} season {season}") 1266 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1267 1268 data = {"auction_bid": price, "auction_submit": "bid"} 1269 response = self.request(url, data) 1270 1271 return f"Your bid of {price} has been lodged." in response.text
Places a bid on a card in a season
Arguments:
- price (str): Amount of bank to bid
- card_id (str): ID of the card
- season (str): Season of the card
Returns:
bool: Whether the bid was successfully lodged or not
1273 def remove_ask(self, price: str, card_id: str, season: str) -> bool: 1274 """Removes an ask on card_id in season at price 1275 1276 Args: 1277 price (str): Price of the ask to remove 1278 card_id (str): ID of the card 1279 season (str): Season of the card 1280 1281 Returns: 1282 bool: Whether the ask was successfully removed or not 1283 """ 1284 1285 self.logger.info(f"removing an ask for {price} on {card_id} season {season}") 1286 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1287 1288 data = {"new_price": price, "remove_ask_price": price} 1289 response = self.request(url, data) 1290 return f"Removed your ask for {price}" in response.text
Removes an ask on card_id in season at price
Arguments:
- price (str): Price of the ask to remove
- card_id (str): ID of the card
- season (str): Season of the card
Returns:
bool: Whether the ask was successfully removed or not
1292 def remove_bid(self, price: str, card_id: str, season: str) -> bool: 1293 """Removes a bid on a card 1294 1295 Args: 1296 price (str): Price of the bid to remove 1297 card_id (str): ID of the card 1298 season (str): Season of the card 1299 1300 Returns: 1301 bool: Whether the bid was successfully removed or not 1302 """ 1303 1304 self.logger.info(f"Removing a bid for {price} on {card_id} season {season}") 1305 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={season}" 1306 1307 data = {"new_price": price, "remove_bid_price": price} 1308 response = self.request(url, data) 1309 1310 return f"Removed your bid for {price}" in response.text
Removes a bid on a card
Arguments:
- price (str): Price of the bid to remove
- card_id (str): ID of the card
- season (str): Season of the card
Returns:
bool: Whether the bid was successfully removed or not
1312 def expand_deck(self, price: str) -> bool: 1313 """Upgrades deck capacity 1314 1315 Args: 1316 price (str): Price of the Upgrade 1317 1318 Returns: 1319 bool: Whether the upgrade was successfully removed or not 1320 """ 1321 1322 self.logger.info(f"Upgrading your deck at a cost of {price}") 1323 url = "https://www.nationstates.net/template-overall=none/page=deck" 1324 1325 data = {"embiggen_deck": price} 1326 response = self.request(url, data) 1327 1328 return "Increased deck capacity from" in response.text
Upgrades deck capacity
Arguments:
- price (str): Price of the Upgrade
Returns:
bool: Whether the upgrade was successfully removed or not
1330 def add_to_collection(self, card_id: str, card_season: str, collection_id: str): 1331 """Adds a card to collection_id 1332 1333 Args: 1334 card_id (str): Card ID 1335 card_season (str): Cards season 1336 collection_id (str): The ID of the collection you want to add to 1337 1338 Returns: 1339 bool: Whether the adding was successfully added or not 1340 """ 1341 self.logger.info(f"Adding {card_id} of season {card_season} to {collection_id}") 1342 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={card_season}" 1343 1344 data = { 1345 "manage_collections": "1", 1346 "modify_card_in_collection": "1", 1347 f"collection_{collection_id}": "1", 1348 "save_collection": "1", 1349 } 1350 response = self.request(url, data) 1351 1352 return "Updated collections." in response.text
Adds a card to collection_id
Arguments:
- card_id (str): Card ID
- card_season (str): Cards season
- collection_id (str): The ID of the collection you want to add to
Returns:
bool: Whether the adding was successfully added or not
1354 def remove_from_collection( 1355 self, card_id: str, card_season: str, collection_id: str 1356 ): 1357 """Removes a card from collection_id 1358 1359 Args: 1360 card_id (str): Card ID 1361 card_season (str): Cards season 1362 collection_id (str): The ID of the collection you want to remove from 1363 1364 Returns: 1365 bool: Whether the removal was successfully added or not 1366 """ 1367 self.logger.info( 1368 f"Removing {card_id} of season {card_season} from {collection_id}" 1369 ) 1370 url = f"https://www.nationstates.net/template-overall=none/page=deck/card={card_id}/season={card_season}" 1371 1372 data = { 1373 "manage_collections": "1", 1374 "modify_card_in_collection": "1", 1375 "start": "0", 1376 f"collection_{collection_id}": "0", 1377 "save_collection": "1", 1378 } 1379 response = self.request(url, data) 1380 1381 return "Updated collections." in response.text
Removes a card from collection_id
Arguments:
- card_id (str): Card ID
- card_season (str): Cards season
- collection_id (str): The ID of the collection you want to remove from
Returns:
bool: Whether the removal was successfully added or not
1383 def create_collection(self, name: str): 1384 """Creates a collection named name 1385 1386 Args: 1387 name (str): The name of the collection you want to create 1388 1389 Returns: 1390 bool: Whether the creating was successfully added or not 1391 """ 1392 self.logger.info(f"Creating {name} collection") 1393 url = "https://www.nationstates.net/template-overall=none/page=deck" 1394 1395 data = {"edit": "1", "collection_name": name, "save_collection": "1"} 1396 response = self.request(url, data) 1397 1398 return "Created collection!" in response.text
Creates a collection named name
Arguments:
- name (str): The name of the collection you want to create
Returns:
bool: Whether the creating was successfully added or not
1400 def delete_collection(self, name: str): 1401 """Deletes a collection named name 1402 1403 Args: 1404 name (str): The name of the collection you want to delete 1405 1406 Returns: 1407 bool: Whether the deleting was successfully added or not 1408 """ 1409 self.logger.info(f"Deleting {name} collection") 1410 url = "https://www.nationstates.net/template-overall=none/page=deck" 1411 1412 data = {"edit": "1", "collection_name": name, "delete_collection": "1"} 1413 response = self.request(url, data) 1414 1415 return "Created collection!" in response.text
Deletes a collection named name
Arguments:
- name (str): The name of the collection you want to delete
Returns:
bool: Whether the deleting was successfully added or not
1417 def can_nation_be_founded(self, name: str): 1418 """Checks if a nation can be founded 1419 1420 Args: 1421 name (str): The name of the nation you want to check 1422 1423 Returns: 1424 bool: Whether the nation can be founded or not 1425 """ 1426 self.logger.info(f"Checking {name} in boneyard") 1427 url = "https://www.nationstates.net/template-overall=none/page=boneyard" 1428 1429 data = {"nation": name, "submit": "1"} 1430 response = self.request(url, data) 1431 1432 return ( 1433 "Available! This name may be used to found a new nation." in response.text 1434 )
Checks if a nation can be founded
Arguments:
- name (str): The name of the nation you want to check
Returns:
bool: Whether the nation can be founded or not
1436 def join_nday_faction(self, id: str): 1437 """Joins a faction in the N-Day event 1438 1439 Args: 1440 id (str): The ID of the faction you want to join 1441 1442 Returns: 1443 bool: Whether the joining was successful or not 1444 """ 1445 self.logger.info(f"Joining faction {id}") 1446 1447 url = ( 1448 f"https://www.nationstates.net/template-overall=none/page=faction/fid={id}" 1449 ) 1450 data = {"join_faction": "1"} 1451 response = self.request(url, data) 1452 1453 return " has joined " in response.text
Joins a faction in the N-Day event
Arguments:
- id (str): The ID of the faction you want to join
Returns:
bool: Whether the joining was successful or not
1455 def leave_nday_faction(self, id: str): 1456 """Leaves a faction in the N-Day event 1457 1458 Args: 1459 id (str): The ID of the faction you want to leave 1460 1461 Returns: 1462 bool: Whether the leaving was successful or not 1463 """ 1464 self.logger.info(f"Leaving faction {id}") 1465 1466 url = ( 1467 f"https://www.nationstates.net/template-overall=none/page=faction/fid={id}" 1468 ) 1469 data = {"leave_faction": "1"} 1470 response = self.request(url, data) 1471 1472 return " has left " in response.text
Leaves a faction in the N-Day event
Arguments:
- id (str): The ID of the faction you want to leave
Returns:
bool: Whether the leaving was successful or not