From a728a7524581b4b21676564eba7c76a77ffaad11 Mon Sep 17 00:00:00 2001 From: Johnny Gear Date: Thu, 13 Nov 2025 22:03:20 -0600 Subject: [PATCH] Issues punishments --- db.py | 58 ++++++++++++++++++++++++++++++++++++++------ generate.py | 69 ++++++++++++++++++++++++++++++++++------------------- main.py | 39 ++++++++++++++++++++++++++---- mastodon.py | 5 ++-- orders.yml | 69 ++++++++++++++++++++++++++++------------------------- 5 files changed, 170 insertions(+), 70 deletions(-) diff --git a/db.py b/db.py index 518faa3..a5dcb4f 100644 --- a/db.py +++ b/db.py @@ -1,5 +1,6 @@ import os import sqlite3 +import datetime SQLITE_DB = os.environ.get('SQLITE_DB', 'db.sqlite3') @@ -16,17 +17,30 @@ TABLE_ORDER_STATUS = ''' CREATE TABLE IF NOT EXISTS order_status ( id INTEGER PRIMARY KEY AUTOINCREMENT, mastodon_id TEXT NOT NULL, - created_at DATETIME NOT NULL, + created_at TIMESTAMP NOT NULL, text TEXT NOT NULL, - confirmed_at DATETIME + confirmed_at TIMESTAMP, + punishment_id INTEGER, + FOREIGN KEY(punishment_id) REFERENCES punishment_status(id) + ); +''' + +TABLE_PUNISHMENT_STATUS = ''' + CREATE TABLE IF NOT EXISTS punishment_status ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mastodon_id TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + text TEXT NOT NULL, + confirmed_at TIMESTAMP ); ''' class Database: def __init__(self): - self.conn = sqlite3.connect(SQLITE_DB, detect_types=sqlite3.PARSE_DECLTYPES) + self.conn = sqlite3.connect(SQLITE_DB) self.conn.row_factory = sqlite3.Row self.table_init(TABLE_REPEATS) + self.table_init(TABLE_PUNISHMENT_STATUS) self.table_init(TABLE_ORDER_STATUS) def table_init(self, table_sql): @@ -38,6 +52,7 @@ class Database: c = self.conn.cursor() c.execute(sql, args) self.conn.commit() + return c.lastrowid def repeat_get(self): c = self.conn.cursor() @@ -64,7 +79,11 @@ class Database: (mastodon_id, created_at, text) VALUES (?, ?, ?); ''', - [mastodon_id, created_at, text] + [ + mastodon_id, + created_at, + text + ] ) def order_status_outstanding(self): @@ -72,7 +91,7 @@ class Database: sql = ''' SELECT id, mastodon_id, created_at, confirmed_at FROM order_status - WHERE confirmed_at IS NULL + WHERE confirmed_at IS NULL AND punishment_id IS NULL ''' c.execute(sql) return c.fetchall() @@ -84,5 +103,30 @@ class Database: SET confirmed_at=? WHERE id=?; ''', - [confirmed_at, id] - ) \ No newline at end of file + [ + confirmed_at, + id + ] + ) + + def punishment_status_put(self, order_status_id, mastodon_id, created_at, text): + punishment_status_id = self.update( + ''' + INSERT INTO punishment_status + (mastodon_id, created_at, text) + VALUES (?, ?, ?); + ''', + [ + mastodon_id, + created_at, + text + ] + ) + self.update( + ''' + UPDATE order_status + SET punishment_id=? + WHERE id=? + ''', + [punishment_status_id, order_status_id] + ) diff --git a/generate.py b/generate.py index 12b1f08..98b6d44 100644 --- a/generate.py +++ b/generate.py @@ -1,7 +1,11 @@ -import yaml, json +import yaml +import json +import logging import random from db import Database +logger = logging.getLogger(__name__) + def pick_order(orders): picked = random.choices(orders, weights=[o['weight'] for o in orders], @@ -29,33 +33,48 @@ def pick_order(orders): return (result, repeat) -def generate(filename="orders.yml"): +def read_config(filename="orders.yml"): with open(filename) as stream: - # Do we want to repeat? - db = Database() - repeat = db.repeat_get() - if repeat is not None: - if repeat['probability'] > random.random(): - db.repeat_increment() - return { - "orders": json.loads(repeat['orders']), - "count": repeat['count'] - } - else: - db.repeat_clear() - - # Pick a new order try: orders = yaml.safe_load(stream) except yaml.YAMLError as exc: - print(exc) + logger.error(exc) + return orders - (result, repeat_p) = pick_order(orders['orders']) +def generate_order(): + # Do we want to repeat? + db = Database() + repeat = db.repeat_get() + if repeat is not None: + if repeat['probability'] > random.random(): + db.repeat_increment() + return { + "orders": json.loads(repeat['orders']), + "count": repeat['count'] + } + else: + db.repeat_clear() + + orders_config = read_config() - # Log the repeat - if repeat_p > 0.0: - db.repeat_put(repeat_p, json.dumps(result)) - - return { - "orders": result - } + if orders_config['orders_probability'] > random.random(): + # No orders today + return { } + + # Pick new orders + (result, repeat_p) = pick_order(orders_config['orders']) + + # Log the repeat + if repeat_p > 0.0: + db.repeat_put(repeat_p, json.dumps(result)) + + return { + "orders": result + } + +def generate_punishment(): + orders_config = read_config() + + (result, repeat_p) = pick_order(orders_config['punishments']) + + return result diff --git a/main.py b/main.py index 946b97f..7f00880 100644 --- a/main.py +++ b/main.py @@ -1,22 +1,33 @@ +import os import sys import logging import argparse import asyncio import aiohttp +import datetime -from generate import generate +from generate import generate_order, generate_punishment from mastodon import Mastodon from db import Database logger = logging.getLogger(__name__) +ORDER_TIMEOUT = datetime.timedelta( + hours=os.getenv('ORDER_TIMEOUT', default=3) +) +MASTODON_USERNAME = os.getenv('MASTODON_USERNAME') + def make_session(): return aiohttp.ClientSession() async def immediate(): - orders_info = generate() + orders_info = generate_order() - post = "Here are today's orders for @johnnygear - \n\n" + if 'orders' not in orders_info: + logger.info("No orders for today") + return + + post = "Here are today's orders for @%s - \n\n" % MASTODON_USERNAME if "count" in orders_info and orders_info['count'] > 1: post += f"These are the same orders from the last {orders_info['count']} days\n\n" post += "\n".join(orders_info['orders']) @@ -45,7 +56,7 @@ async def check(): for d in context['descendants']: if ( d['in_reply_to_id'] == outstanding_order['mastodon_id'] and - d['account']['username'] == 'johnnygear' and + d['account']['username'] == MASTODON_USERNAME and len(d['media_attachments']) > 0 ): confirmed_at = d['created_at'] @@ -56,6 +67,26 @@ async def check(): if confirmed_at is None: logger.info('Order %s remains unconfirmed' % (outstanding_order['created_at'])) + oo_created_at = datetime.datetime.fromisoformat(outstanding_order['created_at']) + if(oo_created_at + ORDER_TIMEOUT < datetime.datetime.now(datetime.UTC)): + logger.info('Time to issue a punishment for %s' % outstanding_order['created_at']) + + punishment = generate_punishment() + + post = "@%s has failed to post proof of compliance. Punishment is as follows -\n\n" % MASTODON_USERNAME + post += "\n".join(punishment) + + punishment_status = await m.statusPost( + post, + in_reply_to_id=outstanding_order['mastodon_id'] + ) + db.punishment_status_put( + outstanding_order['id'], + punishment_status['id'], + punishment_status['created_at'], + post + ) + if __name__=='__main__': logging.basicConfig( format="%(asctime)s %(module)s [%(levelname)-4.4s] %(message)s", diff --git a/mastodon.py b/mastodon.py index 704204a..5f66025 100644 --- a/mastodon.py +++ b/mastodon.py @@ -49,13 +49,14 @@ class Mastodon: return await response.json() - async def statusPost(self, status): + async def statusPost(self, status, in_reply_to_id=None): return await self.post( self.instance, API_STATUSES, data={ 'status': status, - 'visibility': 'direct' + 'visibility': 'direct', + 'in_reply_to_id': in_reply_to_id }) async def statusContext(self, id): diff --git a/orders.yml b/orders.yml index 0adfa6a..4862c70 100644 --- a/orders.yml +++ b/orders.yml @@ -1,34 +1,39 @@ +orders_probability: 0.7 orders: + - weight: 20 + text: "MX Pants + MX Boots" + repeat: 0.7 + add: + - probability: 0.5 + text: "+ Collar" + - weight: 20 + text: "Race Pants + MX Boots" + repeat: 0.7 + add: + - probability: 0.5 + text: "+ Collar" + - weight: 40 + text: "Marine Uniform" + repeat: 0.7 + add: + - probability: 0.9 + text: "+ Leg shackles" + - probability: 0.5 + text: "+ Collar" - weight: 30 - text: "No orders today" - - weight: 70 - text: "Here are your orders:" - pick: - - weight: 20 - text: "MX Pants + MX Boots" - repeat: 0.7 - add: - - probability: 0.5 - text: "+ Collar" - - weight: 20 - text: "Race Pants + MX Boots" - repeat: 0.7 - add: - - probability: 0.5 - text: "+ Collar" - - weight: 40 - text: "Marine Uniform" - repeat: 0.7 - add: - - probability: 0.9 - text: "+ Leg shackles" - - probability: 0.5 - text: "+ Collar" - - weight: 30 - text: "Army Uniform" - repeat: 0.7 - add: - - probability: 0.9 - text: "+ Leg Shackles" - - probability: 0.5 - text: "+ Collar" + text: "Army Uniform" + repeat: 0.7 + add: + - probability: 0.9 + text: "+ Leg Shackles" + - probability: 0.5 + text: "+ Collar" +punishments: + - weight: 20 + text: "Kneel facing a wall for 30 minutes" + - weight: 5 + text: "Kneel facing a wall for 60 minutes" + - weight: 40 + text: "2 hours of ball stretcher time" + - weight: 5 + text: "4 hours of ball stretcher time"