Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# begin with the base Alpine python image
FROM python:3.14.6-alpine3.23
FROM python:3.14.5-alpine3.23
# create a directory to store the application
WORKDIR /usr/local/ns-assembly

Expand Down
Empty file added conf/app/assembly.yaml
Empty file.
19 changes: 11 additions & 8 deletions src/__main__.py → old_src/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,23 +223,25 @@ async def _create_thread_ifv_for_proposal(proposal:classes.wa.Proposal) -> None:

# define a method of fetching proposals
async def _fetch_proposals() -> None:
"""Fetch World Assembly proposals from the NS API, for both councils, into the database."""
"""Return World Assembly proposals from the NS API, for both councils, and create threads"""
for council in [1,2]: # for each council
logger.debug('New council')

proposals = await ns_api.parse_proposals(council) # load a parsed version of the proposals from the API
logger.info('Proposal data fetched from API')

legal_proposals = []

for proposal in proposals: # for each proposal
if proposal.legal and proposal.quorum:
logger.debug('Proposal legal and at quorum')

await ns_postgres.nsqueue_add(proposal) # add it to the NSQueue table
logger.info('Proposal added to NSQueue')

await _create_thread_ifv_for_proposal(proposal) # create a thread and add it to the IFVQueue table
logger.info('Proposal thread being created')
logger.info('Proposal data parsed and stored')

legal_proposals.append(proposal)
logger.info('Proposal data parsed')
return legal_proposals

async def _new_sse_event(payload:str) -> None:
logger.debug('New SSE event received, checking proposals...')
Expand Down Expand Up @@ -271,7 +273,7 @@ async def _check_perms(ctx:discord.ApplicationContext, check_kind:str) -> bool:
async def _get_queue_embed(council:int) -> discord.Embed:
"""Get an embed with the World Assembly proposal queue included."""

queue = await ns_postgres.nsqueue_get_all_legal_by_council_limited(council = council) # fetch all proposals in the NSQueue table
queue = await _fetch_proposals() # fetch all proposals in the NSQueue table
logger.debug('Proposals fetched from DB')

table = 'Stance | Name | Status | Proposal Link | IFV Author | IFV Link\n' # create a table, starting with the header
Expand Down Expand Up @@ -364,7 +366,8 @@ async def _announce_queue(ctx: discord.ApplicationContext, council:int, ping_use
logger.info('Queue embed fetched')

if ping_users:
ping = await ns_postgres.botperms_get_by_kind('user').identifier
ping_object = await ns_postgres.botperms_get_by_kind('user')
ping = ping_object.identifier
logger.debug('Ping role id found')

await ctx.respond(f'<@&{ping}>', embed = embed, ephemeral = False, allowed_mentions = discord.AllowedMentions(roles = True), view=IFVView(council = council))
Expand Down Expand Up @@ -489,7 +492,7 @@ async def announce_sc_queue(ctx: discord.ApplicationContext,ping_users:bool) ->
@bot.event
async def on_application_command_error(ctx:discord.ApplicationContext, error:discord.DiscordException):
logger.error(error)
ctx.respond('<@1271403487045095465> An unspecified error occurred.')
await ctx.respond('<@1271403487045095465> An unspecified error occurred.')

async def main() -> None:
try:
Expand Down
File renamed without changes.
2 changes: 2 additions & 0 deletions src/classes/auth.py → old_src/classes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ def fromAttributeValues(self, kind:str, identifier:int):
self.kind = kind
self.identifier = identifier
self.initialized = True
return self
def fromSQLValues(self, values:tuple[str, int]):
self.kind = values[0]
self.identifier = values[1]
self.initialized = True
return self
def toSQLValues(self) -> tuple[str, int]:
if self.initialized:
return (self.kind, self.identifier)
Expand Down
2 changes: 2 additions & 0 deletions src/classes/ifv.py → old_src/classes/ifv.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ def fromAttributeValues(self, id:str, name:str, thread:str | None = None, ifvaut
self.ifvauthor = ifvauthor
self.ifvlink = ifvlink
self.initialized = True
return self
def fromSQLValues(self, values:tuple[str, str, str | None, str | None, str | None]):
self.id = values[0]
self.name = values[1]
self.thread = values[2]
self.ifvauthor = values[3]
self.ifvlink = values[4]
self.initialized = True
return self
def toSQLValues(self) -> tuple[str, str, str, str | None, str | None, str | None]:
if self.initialized:
return (self.id,self.name,self.thread,self.ifvauthor,self.ifvlink)
Expand Down
2 changes: 2 additions & 0 deletions src/classes/sse.py → old_src/classes/sse.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def fromAttributeValues(self, event:int, time:int, category:str, data:list, acto
self.category = category
self.data = data
self.initialized = True
return self
def fromSQLValues(self, values:tuple[int, int, str, list, str | None, str | None, str | None, str | None]):
self.event = values[0]
self.time = values[1]
Expand All @@ -39,6 +40,7 @@ def fromSQLValues(self, values:tuple[int, int, str, list, str | None, str | None
self.category = values[6]
self.data = values[7]
self.initialized = True
return self
def toSQLValues(self) -> tuple[int, int, str, list, str | None, str | None, str | None, str | None]:
if self.initialized:
return (self.event,self.time,self.actor,self.receptor,self.origin,self.destination,self.category,self.data)
Expand Down
2 changes: 2 additions & 0 deletions src/classes/wa.py → old_src/classes/wa.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def fromAttributeValues(self, id:str, council:int, name:str, category:str, autho
self.legal = legal
self.quorum = quorum
self.initialized = True
return self
def fromSQLValues(self, values:tuple[str, int, str, str, str, bool, bool, list[str | None]]):
self.id = values[0]
self.council = values[1]
Expand All @@ -41,6 +42,7 @@ def fromSQLValues(self, values:tuple[str, int, str, str, str, bool, bool, list[s
self.legal = values[6]
self.quorum = values[7]
self.initialized = True
return self
def toSQLValues(self) -> tuple[str, int, str, str, str, bool, bool, list[str | None]]:
if self.initialized:
return (self.id,self.council,self.name,self.category,self.author,self.coauthors,self.legal,self.quorum)
Expand Down
3 changes: 2 additions & 1 deletion src/customio/__init__.py → old_src/customio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
# simple program to combine other modules into the 'customio' library
from .db import *
from .env import *
from .ns import *
from .ns import *
from .conf import *
27 changes: 27 additions & 0 deletions old_src/customio/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'''This file is part of assembly.
Copyright (C) 2026 HippoProgrammer

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.'''

import yaml
import os
from .exceptions import *

def load_config_from_file(path:str):
if os.path.isfile(path):
with open(path, 'r') as file:
config = yaml.safe_load(file)
return config
else:
raise exceptions.InvalidPathException('Config path is not a valid file. Cannot start')
61 changes: 0 additions & 61 deletions src/customio/db.py → old_src/customio/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,67 +113,6 @@ async def setup_all(self) -> None:
await self._open_connection_pool()
async def cleanup(self) -> None:
await self._close_connection_pool()
# NSQueue table
async def nsqueue_add(self,proposal:classes.wa.Proposal) -> None:
"""Add a Proposal to the NSQueue"""
try: # protect against PoolTimeouts
async with self.connection_pool.connection() as conn: # get a connection from the pool
self.logger.debug('DB connection opened from pool')
async with conn.cursor() as cur: # open a cursor
self.logger.debug('Cursor opened')

await cur.execute("""
INSERT INTO NSQueue (ID, Council, Name, Category, Author, Coauthors, Legal, Quorum)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (ID) DO NOTHING;
""",proposal.toSQLValues()) # insert data into the table, but if it already exists ignore it
self.logger.info('Successful query')

await conn.commit()
except psycopg_pool.PoolTimeout:
self.connection_self.connection_pool.check()
async def nsqueue_get_by_id(self, id:str) -> classes.wa.Proposal:
"""Get a proposal by ID from the NSQueue"""
try:
async with self.connection_pool.connection() as conn: # get a connection from the pool
self.logger.debug('DB connection opened from pool')
async with conn.cursor() as cur: # open a cursor
self.logger.debug('Cursor opened')

await cur.execute("""
SELECT * FROM NSQueue
WHERE ID = %s;
""", [id]) # select all proposals with the supplied ID
self.logger.info('Successful query')

SQLproposal = await cur.fetchone() # fetch the proposal (as ID is unique there is only one)
proposal = classes.wa.Proposal().fromSQLValues(SQLproposal) # convert it into a Proposal object
return proposal
except psycopg_pool.PoolTimeout:
self.connection_self.connection_pool.check()
async def nsqueue_get_all_legal_by_council_limited(self, council:int = 1, limit:int = 7) -> list[classes.wa.Proposal]:
"""Get all proposals that are legal from the NSQueue, up to the specified limit"""
try:
async with self.connection_pool.connection() as conn: # get a connection from the pool
self.logger.debug('DB connection opened from pool')
async with conn.cursor() as cur: # open a cursor
self.logger.debug('Cursor opened')

await cur.execute("""
SELECT * FROM NSQueue
WHERE Legal AND Council = %s
LIMIT %s;
""", [council, limit]) # select all legal proposals, up to the queue limit of seven
self.logger.info('Successful query')

SQLqueue = await cur.fetchall() # fetch them all
queue = []
for item in SQLqueue:
queue.append(classes.wa.Proposal().fromSQLValues(item)) # convert them into a list of Proposal objects
return queue

except psycopg_pool.PoolTimeout:
self.connection_self.connection_pool.check()
# IFVQueue table
async def ifvqueue_add(self, ifv:classes.ifv.IFV) -> None:
"""Add an IFV to the IFVQueue"""
Expand Down
9 changes: 3 additions & 6 deletions src/customio/env.py → old_src/customio/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import os
import logging
from .exceptions import *

# set up a logger
logger = logging.getLogger('assembly.customio.env') # get the logger for this script
Expand All @@ -27,13 +28,9 @@ def load_secrets_from_envvars() -> tuple[str, str]:

# sanity-check envvars
if not os.path.isfile(token_file):
msg = 'ASSEMBLY_TOKEN_FILE environment variable is not a valid path, cannot start'
logger.error(msg)
raise Exception(msg)
raise exceptions.InvalidPathException('ASSEMBLY_TOKEN_FILE environment variable is not a valid path, cannot start')
if not os.path.isfile(pgpass_file):
msg = 'POSTGRES_PASS_FILE environment variable is not a valid path, cannot start'
logger.error(msg)
raise Exception(msg)
raise exceptions.InvalidPathException('POSTGRES_PASS_FILE environment variable is not a valid path, cannot start')

# read token file
with open(token_file,'r') as file:
Expand Down
19 changes: 19 additions & 0 deletions old_src/customio/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'''This file is part of assembly.
Copyright (C) 2026 HippoProgrammer

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.'''

# renamed Exceptions for use by custom libraries
class InvalidPathException(Exception):
pass
File renamed without changes.
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
py-cord[speed]==2.8.0 # Discord API wrapper
psycopg[binary,pool]==3.3.4 # Postgres connector
aiohttp[speedups]==3.14.1 # Async HTTP requests
validators==0.35.0 # String sanitization and validation
validators==0.35.0 # String sanitization and validation
aiohttp[speedups]==3.13.5 # Async HTTP requests
validators==0.35.0 # String sanitization and validation
PyYAML==6.0.3 # YAML parser
11 changes: 0 additions & 11 deletions sql/init/04-apptables.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
\c ns_assembly

CREATE TABLE IF NOT EXISTS NSQueue (
ID TEXT PRIMARY KEY,
Council SMALLINT CHECK (Council = 1 OR Council = 2),
Name TEXT NOT NULL,
Category TEXT NOT NULL,
Author TEXT NOT NULL,
Coauthors TEXT ARRAY[3],
Legal BOOL NOT NULL,
Quorum BOOL NOT NULL
); -- create a table for storing queued proposal information, direct from the NS API

CREATE TABLE IF NOT EXISTS IFVQueue (
ID TEXT PRIMARY KEY,
Name TEXT NOT NULL,
Expand Down
1 change: 0 additions & 1 deletion sql/init/05-appindexes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,5 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
\c ns_assembly

CREATE INDEX IF NOT EXISTS NSQueue_ID_index ON NSQueue (ID); -- create relevant indexes on frequently-queried tables
CREATE INDEX IF NOT EXISTS IFVQueue_Author_index on IFVQueue (IFVAuthor);
EOSQL
1 change: 1 addition & 0 deletions sql/init/06-akarischema.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
ALTER ROLE ns_akari IN DATABASE ns_akari SET search_path TO akari;

GRANT ALL PRIVILEGES ON SCHEMA akari TO ns_akari;
GRANT ALL PRIVILEGES ON SCHEMA public TO ns_akari;
GRANT ALL PRIVILEGES ON SCHEMA akari TO ns_assembly_app;
ALTER DEFAULT PRIVILEGES FOR ROLE ns_akari IN SCHEMA akari GRANT ALL ON TABLES TO ns_akari;
ALTER DEFAULT PRIVILEGES FOR ROLE ns_akari IN SCHEMA akari GRANT SELECT, TRIGGER ON TABLES TO ns_assembly_app;
Expand Down
39 changes: 39 additions & 0 deletions src/classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from copy import deepcopy

class DataStruct:
def __init__(self, structure: dict) -> None:
self.structure = structure
self.data = deepcopy(self.structure)
def _from_values(self, values: dict):
for key in (set(self.structure) & set(values)):
self.structure[key] = values[key]
def from_attribute_values(self, **kwargs):
self._from_values(kwargs)
def from_sql_values(self, values: dict):
for key in (set(self.structure) & set(values)):
# fix to match above

class IFV:
def __init__(self):
self.initialized = False
def fromAttributeValues(self, id:str, name:str, thread:str | None = None, ifvauthor:str | None = None, ifvlink:str | None = None):
self.id = id
self.name = name
self.thread = thread
self.ifvauthor = ifvauthor
self.ifvlink = ifvlink
self.initialized = True
return self
def fromSQLValues(self, values:tuple[str, str, str | None, str | None, str | None]):
self.id = values[0]
self.name = values[1]
self.thread = values[2]
self.ifvauthor = values[3]
self.ifvlink = values[4]
self.initialized = True
return self
def toSQLValues(self) -> tuple[str, str, str, str | None, str | None, str | None]:
if self.initialized:
return (self.id,self.name,self.thread,self.ifvauthor,self.ifvlink)
else:
raise exceptions.UninitializedException()
Loading
Loading