From abe7c99f9955cf0e3e13bfc1cc8d809b8969e647 Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Mon, 25 Mar 2024 02:54:09 -0400 Subject: [PATCH] add readme to setup --- .gitignore | 3 + example.py | 24 +++---- setup.py | 7 +- yatgl/__init__.py | 3 +- yatgl/client.py | 175 ++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 180 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index e0d88dc..0c6f019 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ # Python egg metadata, regenerated from source files by setuptools. /*.egg-info + +tester.py +*.log diff --git a/example.py b/example.py index 64d8e14..3381ffc 100644 --- a/example.py +++ b/example.py @@ -18,31 +18,27 @@ import asyncio -from yatgl import Client, Template - - -async def add_some_tgs(): - while True: - print('inserted 3 more telegrams into queue') - Client().queue_tg(Template('secret key', 'tgid'), '1') - Client().queue_tg(Template('secret key', 'tgid'), '2') - Client().queue_tg(Template('secret key', 'tgid'), '3') - await asyncio.sleep(500) +from yatgl import Client, NationGroup, Template async def main(): # Lazy initialization with singleton - Client(client_key='client key here') + Client(client_key='client key here', user_agent='nation here') # If you want, you can change the delay, like so: # Client(delay=200) - # Queue some telegrams - Client().queue_tg(Template('secret key', 'tgid'), 'nation here') + # You can queue telegrams manually... Client().queue_tg(Template('secret key', 'tgid'), 'nation here') try: - await asyncio.gather(Client().start(), add_some_tgs()) + # ...or you can use a mass telegram function + await Client().mass_telegram(Template('secret key', 'tgid'), NationGroup.NEW_FOUNDS) + + # or you can use multiple with asyncio.gather e.g. + # func1 = Client().mass_telegram(Template('secret key', 'tgid'), NationGroup.NEW_FOUNDS) + # func2 = Client().mass_telegram(Template('secret key', 'tgid'), NationGroup.NEW_REGION_MEMBERS, 'testregionia') + # await asyncio.gather(func1, func2) except KeyboardInterrupt: await Client().stop() diff --git a/setup.py b/setup.py index 9cb7f9e..06b490a 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . - """ from setuptools import setup @@ -24,7 +23,7 @@ setup(name='yatgl', - version='0.0.2', + version='1.0.0', description='An asynchronous NationStates Telegram API library.', long_description=long_description, long_description_content_type='text/markdown', @@ -33,6 +32,8 @@ license='GPLv3', packages=['yatgl'], install_requires=[ - 'aiohttp[speedups]' + 'aiohttp[speedups]', + 'beautifulsoup4', + 'lxml' ], zip_safe=False) diff --git a/yatgl/__init__.py b/yatgl/__init__.py index da308b3..4f319f5 100644 --- a/yatgl/__init__.py +++ b/yatgl/__init__.py @@ -14,7 +14,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . - """ -from .client import Client, Template +from .client import Client, NationGroup, Template diff --git a/yatgl/client.py b/yatgl/client.py index 18dcbd8..79830d3 100644 --- a/yatgl/client.py +++ b/yatgl/client.py @@ -17,11 +17,17 @@ """ import asyncio -from logging import getLogger from collections import deque +from collections.abc import Iterable +from enum import Enum +from logging import getLogger from typing import NamedTuple import aiohttp +from bs4 import BeautifulSoup + +API_URL = 'https://www.nationstates.net/cgi-bin/api.cgi' +VERSION = '0.0.3' logger = getLogger(__name__) @@ -37,6 +43,15 @@ class TelegramRequest(NamedTuple): recipient: str +class NationGroup(Enum): + NEW_WA_MEMBERS = 0 + ALL_WA_MEMBERS = 1 + NEW_FOUNDS = 2 + ALL_WA_DELEGATES = 3 + NEW_REGION_MEMBERS = 4 + ALL_REGION_MEMBERS = 5 + + class _ClientMeta(type): """ Singleton metaclass for Client, making sure that two action queues never exist at once @@ -69,11 +84,13 @@ class Client(metaclass=_ClientMeta): >>>asyncio.run(Client().start()) """ client_key: str = None + user_agent: str = None delay: int = 185 sent = set() queue: deque[TelegramRequest] = deque() - _task = None - _session: aiohttp.ClientSession + _tg_task = None + _queueing_task = None + _session: aiohttp.ClientSession = None def __init__(self, **kwargs): """ @@ -84,6 +101,8 @@ def __init__(self, **kwargs): """ if 'client_key' in kwargs: self.client_key = kwargs.pop('client_key') + if 'user_agent' in kwargs: + self.user_agent = kwargs.pop('user_agent') if 'delay' in kwargs: delay = kwargs.pop('delay') if delay < 30: @@ -106,19 +125,145 @@ async def start(self): """ if not self.client_key: raise AttributeError('No client key provided.') - if not self._task: - self._task = asyncio.create_task(self._process_stack()) - self._session = aiohttp.ClientSession() - await self._task + if not self.user_agent: + raise AttributeError('Please set a User Agent.') + if not self._tg_task: + self._tg_task = asyncio.create_task(self._process_stack()) + if not self._session or self._session.closed: + self._session = aiohttp.ClientSession() + await self._tg_task async def stop(self): """ - Stops sending telegrams if the client has started, otherwise does nothing. + Stops sending telegrams and/or queueing if the client has started, otherwise does nothing. """ - if self._task: - self._task.cancel() + if self._tg_task: + self._tg_task.cancel(), self._queueing_task.cancel() await self._session.close() - self._task = None + self._tg_task, self._queueing_task = None, None + + async def mass_telegram(self, template: Template, group: NationGroup, region: Iterable[str] = None): + """ + Starts the telegram queue while autoqueueing a certain group of nations using the API. + + Ensure that a client key has been provided. + + Note that when getting these nations, the client ignores ratelimits, which should be fine for most cases as + the requests are sparse enough that they're well under, but might break e.g. if targeting nations joining one of + 50 regions, in which it may be time to reevaluate your region's foreign policy. + :param template: The template to send to the nations. + :param group: The group of nations to target specified by the enum :class:`NationGroup`. + :param region: A list of regions. + """ + if not self._session or self._session.closed: + self._session = aiohttp.ClientSession() + self._queueing_task = asyncio.create_task(self._mass_queue(template, group, region)) + await asyncio.gather(self.start(), self._queueing_task) + + async def _mass_queue(self, template: Template, group: NationGroup, regions: str | Iterable[str] | None): + if group in {NationGroup.ALL_REGION_MEMBERS, NationGroup.NEW_REGION_MEMBERS} and not regions: + raise AttributeError('Region(s) not provided to client.') + elif isinstance(regions, str): + regions = [regions] + + # why did i code it like this? + if group.value % 2 == 1: + # here's where i throw good software principles out the book in favor of huge ass switch statements + match group: + case NationGroup.ALL_REGION_MEMBERS: + for region in regions: + for nation in await self._get_region_members(region): + self.queue_tg(template, nation) + + case NationGroup.ALL_WA_MEMBERS: + for nation in await self._get_wa_members(): + self.queue_tg(template, nation) + + case NationGroup.ALL_WA_DELEGATES: + for nation in await self._get_wa_delegates(): + self.queue_tg(template, nation) + else: + # generate a list of nations to not send messages to + existing = set() + if group is NationGroup.NEW_REGION_MEMBERS: + for region in regions: + existing.update(await self._get_region_members(region)) + elif group is NationGroup.NEW_WA_MEMBERS: + existing = set(await self._get_wa_members()) + + while True: + match group: + case NationGroup.NEW_REGION_MEMBERS: + for region in regions: + for nation in await self._get_region_members(region): + if nation not in existing: + self.queue_tg(template, nation) + existing.add(nation) + + case NationGroup.NEW_WA_MEMBERS: + for member in await self._get_wa_members(): + if member not in existing: + self.queue_tg(template, member) + existing.add(member) + + case NationGroup.NEW_FOUNDS: + for nation in await self._get_new_founds(): + if nation not in existing: + self.queue_tg(template, nation) + existing.add(nation) + + await asyncio.sleep(60) + + async def _get_region_members(self, region: str) -> list[str]: + data = { + 'q': 'nations', + 'region': region + } + headers = { + 'User-Agent': f'yatgl v{VERSION} Developed by nation=Notanam, used by nation={self.user_agent}' + } + + async with self._session.post(API_URL, data=data, headers=headers) as resp: + parsed = BeautifulSoup(await resp.text(), 'xml') + return parsed.REGION.NATIONS.string.split(':') + + async def _get_wa_members(self) -> list[str]: + data = { + 'q': 'members', + 'wa': '1' + } + headers = { + 'User-Agent': f'yatgl v{VERSION} Developed by nation=Notanam, used by nation={self.user_agent}' + } + + async with self._session.post(API_URL, data=data, headers=headers) as resp: + parsed = BeautifulSoup(await resp.text(), 'xml') + return parsed.WA.MEMBERS.string.split(',') + + async def _get_wa_delegates(self) -> list[str]: + data = { + 'q': 'delegates', + 'wa': '1' + } + headers = { + 'User-Agent': f'yatgl v{VERSION} Developed by nation=Notanam, used by nation={self.user_agent}' + } + + async with self._session.post(API_URL, data=data, headers=headers) as resp: + parsed = BeautifulSoup(await resp.text(), 'xml') + return parsed.WA.DELEGATES.string.split(',') + + async def _get_new_founds(self) -> list[str]: + data = { + 'q': 'newnations' + } + headers = { + 'User-Agent': f'yatgl v{VERSION} Developed by nation=Notanam, used by nation={self.user_agent}' + } + + async with self._session.post(API_URL, data=data, headers=headers) as resp: + parsed = BeautifulSoup(await resp.text(), 'xml') + return parsed.WORLD.NEWNATIONS.string.split(',') async def _process_stack(self): while True: @@ -137,8 +282,12 @@ async def _send_tg(self, telegram: TelegramRequest): 'key': telegram.template.secret_key, 'to': recipient } - async with self._session.post('https://www.nationstates.net/cgi-bin/api.cgi', data=data) as resp: - if await resp.text() == 'queued': + headers = { + 'User-Agent': f'yatgl v{VERSION} Developed by nation=Notanam, used by nation={self.user_agent}' + } + + async with self._session.post(API_URL, data=data, headers=headers) as resp: + if 'queued' in await resp.text(): self.sent.add(recipient) logger.info(f'Sent {telegram.template.tgid} to {telegram.recipient}') else: