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: