Verify Mastodon posts have a favorite
This commit is contained in:
parent
0a1f5967af
commit
cb6dd98ce1
14 changed files with 450 additions and 78 deletions
|
|
@ -36,6 +36,9 @@ class User(BaseModel):
|
||||||
mastodon_attn_list = TextField(null=True)
|
mastodon_attn_list = TextField(null=True)
|
||||||
mastodon_post_public = BooleanField(null=True, default=False)
|
mastodon_post_public = BooleanField(null=True, default=False)
|
||||||
|
|
||||||
|
verify_mastodon_favorite = BooleanField(null=False, default=False)
|
||||||
|
verify_delay = IntegerField(null=True)
|
||||||
|
|
||||||
def mastodon_account(self):
|
def mastodon_account(self):
|
||||||
if self.mastodon_server is None or self.mastodon_username is None:
|
if self.mastodon_server is None or self.mastodon_username is None:
|
||||||
return
|
return
|
||||||
|
|
@ -148,6 +151,7 @@ class OrderStatus(BaseModel):
|
||||||
confirmed_at = DateTimeField(null=True)
|
confirmed_at = DateTimeField(null=True)
|
||||||
created_at = DateTimeField()
|
created_at = DateTimeField()
|
||||||
due_at = DateTimeField(null=True)
|
due_at = DateTimeField(null=True)
|
||||||
|
verify_at = DateTimeField(null=True)
|
||||||
mastodon_id = TextField()
|
mastodon_id = TextField()
|
||||||
text = TextField()
|
text = TextField()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ def user_preferences_set(id, mastodon_post_public, mastodon_attn_list):
|
||||||
)
|
)
|
||||||
return q.execute()
|
return q.execute()
|
||||||
|
|
||||||
|
def user_has_doms(id):
|
||||||
|
return DomSubUsers.select().where(DomSubUsers.sub_id == id).count() > 0
|
||||||
|
|
||||||
def mastodon_server_get(name):
|
def mastodon_server_get(name):
|
||||||
return MastodonServer.get(name=name)
|
return MastodonServer.get(name=name)
|
||||||
|
|
||||||
|
|
@ -91,6 +94,9 @@ def domsubusers_delete(sub, dom):
|
||||||
def domsubusers_list(dom):
|
def domsubusers_list(dom):
|
||||||
return DomSubUsers.select().where(DomSubUsers.dom == dom)
|
return DomSubUsers.select().where(DomSubUsers.dom == dom)
|
||||||
|
|
||||||
|
def domsubusers_doms(sub):
|
||||||
|
return [dsu.dom for dsu in DomSubUsers.select(DomSubUsers.dom).where(DomSubUsers.sub_id == sub.id)]
|
||||||
|
|
||||||
def repeat_get(orders_pool_id):
|
def repeat_get(orders_pool_id):
|
||||||
try:
|
try:
|
||||||
return Repeat.get(orders_pool_id = orders_pool_id)
|
return Repeat.get(orders_pool_id = orders_pool_id)
|
||||||
|
|
@ -132,7 +138,7 @@ def skip_day_contains(user, date):
|
||||||
q = SkipDay.select().where((SkipDay.user == user) & (SkipDay.date == date))
|
q = SkipDay.select().where((SkipDay.user == user) & (SkipDay.date == date))
|
||||||
return len(q) > 0
|
return len(q) > 0
|
||||||
|
|
||||||
def order_status_put(orders_pool, user, mastodon_id, created_at, due_at, text, punishment_for=None):
|
def order_status_put(orders_pool, user, mastodon_id, created_at, due_at, text, punishment_for=None, verify_at=None):
|
||||||
return OrderStatus.create(
|
return OrderStatus.create(
|
||||||
orders_pool_id=orders_pool.id,
|
orders_pool_id=orders_pool.id,
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
|
|
@ -140,7 +146,8 @@ def order_status_put(orders_pool, user, mastodon_id, created_at, due_at, text, p
|
||||||
created_at=created_at,
|
created_at=created_at,
|
||||||
due_at=due_at,
|
due_at=due_at,
|
||||||
text=text,
|
text=text,
|
||||||
punishment_for=punishment_for
|
punishment_for=punishment_for,
|
||||||
|
verify_at=verify_at
|
||||||
)
|
)
|
||||||
|
|
||||||
def order_status_by_id(order_status_id):
|
def order_status_by_id(order_status_id):
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from settings import MASTODON_INSTANCE, MASTODON_ACCESS_TOKEN, MASTODON_VISIBILI
|
||||||
|
|
||||||
API_STATUSES = '/api/v1/statuses'
|
API_STATUSES = '/api/v1/statuses'
|
||||||
API_STATUS_CONTEXT = '/api/v1/statuses/%(id)s/context'
|
API_STATUS_CONTEXT = '/api/v1/statuses/%(id)s/context'
|
||||||
|
API_STATUS_FAVORITES = '/api/v1/statuses/%(id)s/favourited_by'
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -64,3 +65,11 @@ class Mastodon:
|
||||||
'id': id
|
'id': id
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def statusFavorites(self, id):
|
||||||
|
return await self.get(
|
||||||
|
self.instance,
|
||||||
|
API_STATUS_FAVORITES % {
|
||||||
|
'id' : id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
||||||
50
migrations/025_user_add_verify_mastodon_favorite.py
Normal file
50
migrations/025_user_add_verify_mastodon_favorite.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
"""Peewee migrations -- 025_user_add_verify_mastodon_favorite.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
migrator.add_fields(
|
||||||
|
'user',
|
||||||
|
|
||||||
|
verify_mastodon_favorite=pw.BooleanField(default=False),
|
||||||
|
verify_delay=pw.IntegerField(null=True))
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_fields('user', 'verify_mastodon_favorite', 'verify_delay')
|
||||||
49
migrations/026_order_status_verify_at.py
Normal file
49
migrations/026_order_status_verify_at.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Peewee migrations -- 026_order_status_verify_at.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
migrator.add_fields(
|
||||||
|
'order_status',
|
||||||
|
|
||||||
|
verify_at=pw.DateTimeField(null=True))
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_fields('order_status', 'verify_at')
|
||||||
165
orders.py
165
orders.py
|
|
@ -5,7 +5,7 @@ import asyncio
|
||||||
from db.constants import TIMELINE_ORDER_CONFIRMED, TIMELINE_ORDER_NOT_PUNISHED, TIMELINE_ORDER_PUNISHED
|
from db.constants import TIMELINE_ORDER_CONFIRMED, TIMELINE_ORDER_NOT_PUNISHED, TIMELINE_ORDER_PUNISHED
|
||||||
from util import make_session
|
from util import make_session
|
||||||
from generate import generate_order, generate_punishment
|
from generate import generate_order, generate_punishment
|
||||||
from db.queries import order_status_by_id, order_status_put, order_status_confirm, timeline_event_put
|
from db.queries import domsubusers_doms, order_status_by_id, order_status_put, order_status_confirm, timeline_event_put
|
||||||
from mastodon import Mastodon
|
from mastodon import Mastodon
|
||||||
from telegram.telegram import Telegram
|
from telegram.telegram import Telegram
|
||||||
from settings import ENV
|
from settings import ENV
|
||||||
|
|
@ -13,14 +13,17 @@ from util import timezone
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def order_mastodon_post(session, orders_pool, orders_str, repeats, due_at):
|
async def order_mastodon_post(session, orders_pool, orders_str, repeats, due_at, verify_at=None):
|
||||||
user = orders_pool.user
|
user = orders_pool.user
|
||||||
|
|
||||||
post = "Here are today's orders for @%s -\n\n" % user.mastodon_account()
|
post = "Here are today's orders for @%s -\n\n" % user.mastodon_account()
|
||||||
post += orders_str + "\n\n"
|
post += orders_str + "\n\n"
|
||||||
if repeats > 1:
|
if repeats > 1:
|
||||||
post += f"These are the same orders from the last {repeats} days\n\n"
|
post += f"These are the same orders from the last {repeats} days\n\n"
|
||||||
post += "Proof of compliance is due by " + due_at.strftime("%I:%M %p") + "\n\n"
|
post += "Proof of compliance is due by " + due_at.strftime("%I:%M %p") + "\n"
|
||||||
|
if verify_at is not None:
|
||||||
|
post += "Verification due by " + verify_at.strftime("%I:%M %p") + "\n"
|
||||||
|
post += "\n"
|
||||||
|
|
||||||
if ENV == 'dev':
|
if ENV == 'dev':
|
||||||
post += "⚠️ DEV"
|
post += "⚠️ DEV"
|
||||||
|
|
@ -30,12 +33,15 @@ async def order_mastodon_post(session, orders_pool, orders_str, repeats, due_at)
|
||||||
m = Mastodon(session)
|
m = Mastodon(session)
|
||||||
return await m.statusPost(post)
|
return await m.statusPost(post)
|
||||||
|
|
||||||
async def order_telegram_post(session, orders_pool, orders_str, repeats, due_at, m_url):
|
async def order_telegram_post(session, orders_pool, orders_str, repeats, due_at, m_url, verify_at=None):
|
||||||
post = "Here are your orders -\n\n"
|
post = "Here are your orders -\n\n"
|
||||||
post += orders_str + "\n\n"
|
post += orders_str + "\n\n"
|
||||||
if repeats > 1:
|
if repeats > 1:
|
||||||
post += f"These are the same orders from the last {repeats} days\n\n"
|
post += f"These are the same orders from the last {repeats} days\n\n"
|
||||||
post += "Proof of compliance is due by " + due_at.strftime("%I:%M %p") + "\n\n"
|
post += "Proof of compliance is due by " + due_at.strftime("%I:%M %p") + "\n"
|
||||||
|
if verify_at is not None:
|
||||||
|
post += "Verification due by " + verify_at.strftime("%I:%M %p") + "\n"
|
||||||
|
post += "\n"
|
||||||
post += m_url
|
post += m_url
|
||||||
if ENV == 'dev':
|
if ENV == 'dev':
|
||||||
post += "\n⚠️ DEV"
|
post += "\n⚠️ DEV"
|
||||||
|
|
@ -61,54 +67,64 @@ async def order_telegram_post_none(session, orders_pool):
|
||||||
|
|
||||||
async def order_issue(orders_pool):
|
async def order_issue(orders_pool):
|
||||||
async with make_session() as session:
|
async with make_session() as session:
|
||||||
if orders_pool.user.mastodon_username is None:
|
user = orders_pool.user
|
||||||
logger.info('Cannot issue order without mastodon username')
|
|
||||||
await order_telegram_post_need_mastodon(session, orders_pool)
|
|
||||||
return { "reason": "Cannot issue order without mastodon username" }
|
|
||||||
|
|
||||||
orders_info = generate_order(orders_pool)
|
if user.mastodon_username is None:
|
||||||
|
logger.info('Cannot issue order without mastodon username')
|
||||||
|
await order_telegram_post_need_mastodon(session, orders_pool)
|
||||||
|
return { "reason": "Cannot issue order without mastodon username" }
|
||||||
|
|
||||||
if 'orders' not in orders_info:
|
orders_info = generate_order(orders_pool)
|
||||||
logger.info(f"{orders_pool} - {orders_info['reason']}")
|
|
||||||
await order_telegram_post_none(session, orders_pool)
|
|
||||||
return { "reason": orders_info['reason'] }
|
|
||||||
|
|
||||||
orders_str = "\n".join(orders_info['orders'])
|
if 'orders' not in orders_info:
|
||||||
|
logger.info(f"{orders_pool} - {orders_info['reason']}")
|
||||||
|
await order_telegram_post_none(session, orders_pool)
|
||||||
|
return { "reason": orders_info['reason'] }
|
||||||
|
|
||||||
|
orders_str = "\n".join(orders_info['orders'])
|
||||||
|
|
||||||
created_at = datetime.datetime.now(tz=timezone())
|
created_at = datetime.datetime.now(tz=timezone())
|
||||||
if orders_pool.confirm_delay is not None:
|
due_at = None
|
||||||
due_at = created_at + datetime.timedelta(hours=orders_pool.confirm_delay)
|
verify_at = None
|
||||||
|
if orders_pool.confirm_delay is not None:
|
||||||
|
due_at = created_at + datetime.timedelta(hours=orders_pool.confirm_delay)
|
||||||
|
|
||||||
repeats_count = orders_info.get('count', 0)
|
if user.verify_mastodon_favorite:
|
||||||
|
verify_at = due_at + datetime.timedelta(hours=user.verify_delay)
|
||||||
|
|
||||||
m_status = await order_mastodon_post(
|
repeats_count = orders_info.get('count', 0)
|
||||||
session,
|
|
||||||
orders_pool,
|
|
||||||
orders_str,
|
|
||||||
repeats_count,
|
|
||||||
due_at
|
|
||||||
)
|
|
||||||
|
|
||||||
await order_telegram_post(
|
m_status = await order_mastodon_post(
|
||||||
session,
|
session,
|
||||||
orders_pool,
|
orders_pool,
|
||||||
orders_str,
|
orders_str,
|
||||||
repeats_count,
|
repeats_count,
|
||||||
due_at,
|
due_at,
|
||||||
m_status['url']
|
verify_at=verify_at
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
await order_telegram_post(
|
||||||
"order_status" : order_status_put(
|
session,
|
||||||
orders_pool,
|
orders_pool,
|
||||||
orders_pool.user,
|
orders_str,
|
||||||
m_status['id'],
|
repeats_count,
|
||||||
created_at,
|
due_at,
|
||||||
due_at,
|
m_status['url'],
|
||||||
orders_str
|
verify_at=verify_at
|
||||||
),
|
)
|
||||||
"mastodon_status": m_status
|
|
||||||
}
|
return {
|
||||||
|
"order_status" : order_status_put(
|
||||||
|
orders_pool,
|
||||||
|
orders_pool.user,
|
||||||
|
m_status['id'],
|
||||||
|
created_at,
|
||||||
|
due_at,
|
||||||
|
orders_str,
|
||||||
|
verify_at=verify_at
|
||||||
|
),
|
||||||
|
"mastodon_status": m_status
|
||||||
|
}
|
||||||
|
|
||||||
async def punishment_mastodon_post(session, orders_pool, punishment_str, reply_id=None):
|
async def punishment_mastodon_post(session, orders_pool, punishment_str, reply_id=None):
|
||||||
user = orders_pool.user
|
user = orders_pool.user
|
||||||
|
|
@ -174,11 +190,30 @@ async def punishment_issue(session, order_status):
|
||||||
"mastodon_status": punishment_status
|
"mastodon_status": punishment_status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def status_has_favorites(m, status_id, user):
|
||||||
|
favorites = await m.statusFavorites(status_id)
|
||||||
|
user_mastodon_account = user.mastodon_account()
|
||||||
|
|
||||||
|
accts = [f['acct'] for f in favorites]
|
||||||
|
|
||||||
|
for dom in domsubusers_doms(user):
|
||||||
|
dom_account = dom.mastodon_account()
|
||||||
|
if dom_account is not None and dom_account in accts:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for attn_user in user.mastodon_attn_list.split(" "):
|
||||||
|
if (attn_user[1:] != user_mastodon_account and
|
||||||
|
attn_user[1:] in accts):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
order_check_lock = asyncio.Lock()
|
order_check_lock = asyncio.Lock()
|
||||||
async def order_check(order_status_id):
|
async def order_check(order_status_id):
|
||||||
async with order_check_lock:
|
async with order_check_lock:
|
||||||
async with make_session() as session:
|
async with make_session() as session:
|
||||||
order_status = order_status_by_id(order_status_id)
|
order_status = order_status_by_id(order_status_id)
|
||||||
|
user = order_status.user
|
||||||
|
|
||||||
if order_status.punishment.count() > 0:
|
if order_status.punishment.count() > 0:
|
||||||
logger.info(f'Punishment already issued for {order_status.id}')
|
logger.info(f'Punishment already issued for {order_status.id}')
|
||||||
|
|
@ -188,12 +223,34 @@ async def order_check(order_status_id):
|
||||||
context = await m.statusContext(order_status.mastodon_id)
|
context = await m.statusContext(order_status.mastodon_id)
|
||||||
|
|
||||||
confirmed_at = None
|
confirmed_at = None
|
||||||
|
had_replies = False
|
||||||
|
had_reply_on_time = False
|
||||||
|
had_media_attachment = False
|
||||||
|
had_favorites = False
|
||||||
for d in context['descendants']:
|
for d in context['descendants']:
|
||||||
if (
|
if (
|
||||||
d['in_reply_to_id'] == order_status.mastodon_id and
|
d['in_reply_to_id'] == order_status.mastodon_id and
|
||||||
d['account']['acct'] == order_status.user.mastodon_account() and
|
d['account']['acct'] == order_status.user.mastodon_account()
|
||||||
len(d['media_attachments']) > 0
|
|
||||||
):
|
):
|
||||||
|
had_replies = True
|
||||||
|
|
||||||
|
if (datetime.datetime.fromisoformat(d['created_at']) <
|
||||||
|
datetime.datetime.fromisoformat(order_status.due_at)):
|
||||||
|
had_reply_on_time = True
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(d['media_attachments']) > 0:
|
||||||
|
had_media_attachment = True
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if user.verify_mastodon_favorite:
|
||||||
|
if await status_has_favorites(m, d['id'], order_status.user):
|
||||||
|
had_favorites = True
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
confirmed_at = d['created_at']
|
confirmed_at = d['created_at']
|
||||||
order_status_confirm(order_status.id, confirmed_at)
|
order_status_confirm(order_status.id, confirmed_at)
|
||||||
logger.info('Confirmed order %s' % (order_status.id))
|
logger.info('Confirmed order %s' % (order_status.id))
|
||||||
|
|
@ -214,15 +271,27 @@ async def order_check(order_status_id):
|
||||||
|
|
||||||
due_at = datetime.datetime.fromisoformat(order_status.due_at)
|
due_at = datetime.datetime.fromisoformat(order_status.due_at)
|
||||||
if(due_at < datetime.datetime.now(datetime.UTC)):
|
if(due_at < datetime.datetime.now(datetime.UTC)):
|
||||||
|
reason = None
|
||||||
|
if had_replies is False:
|
||||||
|
reason = "No replies were found"
|
||||||
|
elif had_reply_on_time is False:
|
||||||
|
reason = "Reply was after due date"
|
||||||
|
elif had_media_attachment is False:
|
||||||
|
reason = "No replies had a media attachment"
|
||||||
|
elif had_favorites is False:
|
||||||
|
reason = "No replies had a favorite from a dom or tagged account"
|
||||||
|
|
||||||
logger.info('Time to issue a punishment for %s' % order_status.id)
|
logger.info('Time to issue a punishment for %s' % order_status.id)
|
||||||
|
|
||||||
issue_result = await punishment_issue(session, order_status)
|
issue_result = await punishment_issue(session, order_status)
|
||||||
|
|
||||||
if 'order_status' in issue_result:
|
if 'order_status' in issue_result:
|
||||||
punishment_status = issue_result['order_status']
|
punishment_status = issue_result['order_status']
|
||||||
|
log_text = (reason + "\n") if reason is not None else ''
|
||||||
|
log_text += punishment_status.text.split("\n")[0]
|
||||||
timeline_event_put(
|
timeline_event_put(
|
||||||
TIMELINE_ORDER_PUNISHED,
|
TIMELINE_ORDER_PUNISHED,
|
||||||
punishment_status.text.split("\n")[0],
|
log_text,
|
||||||
punishment_status.user,
|
punishment_status.user,
|
||||||
punishment_status.pool,
|
punishment_status.pool,
|
||||||
punishment_status,
|
punishment_status,
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,9 @@ class OrderScheduler():
|
||||||
self.schedule_pool(orders_pool)
|
self.schedule_pool(orders_pool)
|
||||||
|
|
||||||
for order_status in order_status_outstanding():
|
for order_status in order_status_outstanding():
|
||||||
|
check_time = order_status.verify_at if order_status.verify_at is not None else order_status.due_at
|
||||||
self.scheduler.once(
|
self.scheduler.once(
|
||||||
datetime.datetime.fromisoformat(order_status.due_at) + GRACE_PERIOD,
|
check_time + GRACE_PERIOD,
|
||||||
self.scheduled_check,
|
self.scheduled_check,
|
||||||
args=(order_status.id,)
|
args=(order_status.id,)
|
||||||
)
|
)
|
||||||
|
|
@ -91,12 +92,14 @@ class OrderScheduler():
|
||||||
|
|
||||||
if 'order_status' in issue_result:
|
if 'order_status' in issue_result:
|
||||||
order_status = issue_result['order_status']
|
order_status = issue_result['order_status']
|
||||||
# Schedule check
|
if order_status.due_at is not None or order_status.verify_at is not None:
|
||||||
self.scheduler.once(
|
# Schedule check
|
||||||
order_status.due_at + GRACE_PERIOD,
|
check_time = order_status.verify_at if order_status.verify_at is not None else order_status.due_at
|
||||||
self.scheduled_check,
|
self.scheduler.once(
|
||||||
args=(order_status.id,)
|
check_time + GRACE_PERIOD,
|
||||||
)
|
self.scheduled_check,
|
||||||
|
args=(order_status.id,)
|
||||||
|
)
|
||||||
timeline_event_put(
|
timeline_event_put(
|
||||||
TIMELINE_ORDER_ISSUED,
|
TIMELINE_ORDER_ISSUED,
|
||||||
order_status.text.split("\n")[0],
|
order_status.text.split("\n")[0],
|
||||||
|
|
|
||||||
44
web/api.py
44
web/api.py
|
|
@ -6,7 +6,7 @@ from flask import Blueprint, jsonify, abort, request
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from db.constants import TIMELINE_ORDERS_POOL_CREATED, TIMELINE_ORDERS_POOL_DELETED, TIMELINE_ORDERS_POOL_UPDATED
|
from db.constants import TIMELINE_ORDERS_POOL_CREATED, TIMELINE_ORDERS_POOL_DELETED, TIMELINE_ORDERS_POOL_UPDATED
|
||||||
from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer
|
from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer
|
||||||
from db.queries import timeline_event_put, timeline_event_recent, user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_mastodon_server_set, user_preferences_set
|
from db.queries import timeline_event_put, timeline_event_recent, user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_has_doms, user_mastodon_server_set, user_preferences_set
|
||||||
from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE
|
from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE
|
||||||
from util import time_sqlite
|
from util import time_sqlite
|
||||||
|
|
||||||
|
|
@ -18,15 +18,24 @@ api = Blueprint('api', __name__)
|
||||||
@login_required
|
@login_required
|
||||||
def me():
|
def me():
|
||||||
user = current_user.db_user
|
user = current_user.db_user
|
||||||
|
has_doms = user_has_doms(user.id)
|
||||||
|
|
||||||
return jsonify({
|
result = {
|
||||||
"username": user.telegram_username,
|
"username": user.telegram_username,
|
||||||
"telegram_photo_url": user.telegram_photo_url,
|
"telegram_photo_url": user.telegram_photo_url,
|
||||||
"mastodon_server": user.mastodon_server.name if user.mastodon_server else None,
|
"mastodon_server": user.mastodon_server.name if user.mastodon_server else None,
|
||||||
"mastodon_username": user.mastodon_username,
|
"mastodon_username": user.mastodon_username,
|
||||||
"mastodon_attn_list": user.mastodon_attn_list,
|
"mastodon_attn_list": user.mastodon_attn_list,
|
||||||
"mastodon_post_public": user.mastodon_post_public
|
"mastodon_post_public": user.mastodon_post_public,
|
||||||
})
|
"has_doms": has_doms
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if not has_doms:
|
||||||
|
result["verify_mastodon_favorite"] = user.verify_mastodon_favorite
|
||||||
|
result["verify_delay"] = user.verify_delay
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
@api.route("/profile", methods=["POST",])
|
@api.route("/profile", methods=["POST",])
|
||||||
@login_required
|
@login_required
|
||||||
|
|
@ -149,15 +158,30 @@ def authorized_sub(func):
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
@api.route('/subs/<username>')
|
@api.route('/subs/<username>', methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@authorized_sub
|
@authorized_sub
|
||||||
def sub(username, sub):
|
def sub(username, sub):
|
||||||
return jsonify({
|
if user_has_doms(sub) and sub.id == current_user.db_user.id:
|
||||||
"username": sub.telegram_username,
|
abort(403)
|
||||||
"mastodon_server": sub.mastodon_server.name if sub.mastodon_server is not None else None,
|
return
|
||||||
"mastodon_username": sub.mastodon_username
|
|
||||||
})
|
if request.method == "POST":
|
||||||
|
sub.verify_mastodon_favorite = bool(request.json['verify_mastodon_favorite'])
|
||||||
|
if request.json['verify_delay'] is not None:
|
||||||
|
sub.verify_delay = int(request.json["verify_delay"])
|
||||||
|
|
||||||
|
sub.save()
|
||||||
|
|
||||||
|
return ('', 204)
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"username": sub.telegram_username,
|
||||||
|
"mastodon_server": sub.mastodon_server.name if sub.mastodon_server is not None else None,
|
||||||
|
"mastodon_username": sub.mastodon_username,
|
||||||
|
"verify_mastodon_favorite": sub.verify_mastodon_favorite,
|
||||||
|
"verify_delay": sub.verify_delay
|
||||||
|
})
|
||||||
|
|
||||||
@api.route('/orders/')
|
@api.route('/orders/')
|
||||||
@login_required
|
@login_required
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,8 @@ def login():
|
||||||
db_user.save()
|
db_user.save()
|
||||||
|
|
||||||
login_user(FlaskUser(db_user))
|
login_user(FlaskUser(db_user))
|
||||||
except:
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
flash("Login failed. Please try again.")
|
flash("Login failed. Please try again.")
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { Link, useLoaderData } from "react-router";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { fetchHeaders } from "./fetch";
|
import { fetchHeaders } from "./fetch";
|
||||||
|
import { ProfileVerification } from "./ProfileVerification";
|
||||||
|
|
||||||
const RE_MASTODON_ACCOUNTS = /^(@(\w+)(@([\w\.]+))? )*(@\w+)(@[\w\.]+)?$/;
|
const RE_MASTODON_ACCOUNTS = /^(@(\w+)(@([\w\.]+))? )*(@\w+)(@[\w\.]+)?$/;
|
||||||
|
|
||||||
|
|
@ -63,10 +64,13 @@ export const profileLoader = async () =>
|
||||||
export const Profile: React.FC = () => {
|
export const Profile: React.FC = () => {
|
||||||
const { username, telegram_photo_url, mastodon_server, mastodon_username } =
|
const { username, telegram_photo_url, mastodon_server, mastodon_username } =
|
||||||
useUserContext();
|
useUserContext();
|
||||||
const { mastodon_attn_list, mastodon_post_public } = useLoaderData<{
|
const {
|
||||||
mastodon_attn_list?: string;
|
has_doms,
|
||||||
mastodon_post_public?: boolean;
|
mastodon_attn_list,
|
||||||
}>();
|
mastodon_post_public,
|
||||||
|
verify_mastodon_favorite,
|
||||||
|
verify_delay,
|
||||||
|
} = useLoaderData<UserProfile>();
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
const mastodon_account = React.useMemo(
|
const mastodon_account = React.useMemo(
|
||||||
|
|
@ -155,6 +159,13 @@ export const Profile: React.FC = () => {
|
||||||
</form>
|
</form>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
{!has_doms ? (
|
||||||
|
<ProfileVerification
|
||||||
|
username={username}
|
||||||
|
verify_mastodon_favorite={verify_mastodon_favorite}
|
||||||
|
verify_delay={verify_delay}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
103
web/vite/src/ProfileVerification.tsx
Normal file
103
web/vite/src/ProfileVerification.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
NumberInput,
|
||||||
|
Paper,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { fetchHeaders } from "./fetch";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type ProfileVerificationProps = Pick<
|
||||||
|
UserProfile,
|
||||||
|
"verify_mastodon_favorite" | "verify_delay"
|
||||||
|
> & { username: string };
|
||||||
|
|
||||||
|
type OrderVerificationForm = Pick<
|
||||||
|
UserProfile,
|
||||||
|
"verify_mastodon_favorite" | "verify_delay"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const ProfileVerification: React.FC<ProfileVerificationProps> = ({
|
||||||
|
username,
|
||||||
|
verify_mastodon_favorite,
|
||||||
|
verify_delay,
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const form = useForm<OrderVerificationForm>({
|
||||||
|
mode: "uncontrolled",
|
||||||
|
initialValues: {
|
||||||
|
verify_mastodon_favorite,
|
||||||
|
verify_delay,
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
verify_delay: (value: number, values: OrderVerificationForm) => {
|
||||||
|
console.log("oh boy", values.verify_mastodon_favorite, value);
|
||||||
|
return !values.verify_mastodon_favorite || value
|
||||||
|
? null
|
||||||
|
: "You must set a verification delay";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = React.useCallback(
|
||||||
|
form.onSubmit((values) => {
|
||||||
|
setLoading(true);
|
||||||
|
fetch(`/api/subs/${username}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: fetchHeaders(),
|
||||||
|
body: JSON.stringify(values),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
notifications.show({
|
||||||
|
title: "Success",
|
||||||
|
message: "Your preferences have been saved",
|
||||||
|
color: "green",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notifications.show({
|
||||||
|
title: "Error",
|
||||||
|
message: "There was a problem saving your preferences",
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper bg="gray.1">
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Title order={4} mb="md">
|
||||||
|
Order Verification
|
||||||
|
</Title>
|
||||||
|
<Checkbox
|
||||||
|
{...form.getInputProps("verify_mastodon_favorite", {
|
||||||
|
type: "checkbox",
|
||||||
|
})}
|
||||||
|
label="Posts must be Favorited by a dom or tagged account"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
{...form.getInputProps("verify_delay")}
|
||||||
|
label="Verification Delay"
|
||||||
|
description="Hours to wait after order is due to verify post"
|
||||||
|
min={1}
|
||||||
|
max={168}
|
||||||
|
rightSection={<Text mr="xl">hours</Text>}
|
||||||
|
/>
|
||||||
|
<Button type="submit" loading={loading} mt="md">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Params, useLoaderData, useParams, Link } from "react-router";
|
import { Params, useLoaderData, useParams, Link } from "react-router";
|
||||||
import { OrderSetProps, OrderSets } from "./OrderSets";
|
import { OrderSetProps, OrderSets } from "./OrderSets";
|
||||||
|
import { ProfileVerification } from "./ProfileVerification";
|
||||||
|
import { Title } from "@mantine/core";
|
||||||
|
|
||||||
export const subOrderSetsLoader = async ({
|
export const subOrderSetsLoader = async ({
|
||||||
params: { username },
|
params: { username },
|
||||||
|
|
@ -12,11 +14,32 @@ export const SubOrderSets: React.FC = () => {
|
||||||
const { username: sub_username } = useParams();
|
const { username: sub_username } = useParams();
|
||||||
const orderSets = useLoaderData<OrderSetProps["orderSets"]>();
|
const orderSets = useLoaderData<OrderSetProps["orderSets"]>();
|
||||||
|
|
||||||
|
const [profile, setProfile] = React.useState<UserProfile | null>(null);
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetch(`/api/subs/${sub_username}`)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then(setProfile);
|
||||||
|
}, [sub_username]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrderSets
|
<>
|
||||||
username={sub_username}
|
<OrderSets
|
||||||
orderSets={orderSets}
|
username={sub_username}
|
||||||
linkBack={<Link to={`/dashboard/`}>Return to dashboard</Link>}
|
orderSets={orderSets}
|
||||||
/>
|
linkBack={<Link to={`/dashboard/`}>Return to dashboard</Link>}
|
||||||
|
/>
|
||||||
|
{profile ? (
|
||||||
|
<>
|
||||||
|
<Title order={1} mb="md">
|
||||||
|
Sub Profile
|
||||||
|
</Title>
|
||||||
|
<ProfileVerification
|
||||||
|
username={sub_username}
|
||||||
|
verify_mastodon_favorite={profile.verify_mastodon_favorite}
|
||||||
|
verify_delay={profile.verify_delay}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,14 @@ export const TimelineList: React.FC<{
|
||||||
extra,
|
extra,
|
||||||
}) => (
|
}) => (
|
||||||
<Timeline.Item key={id} active {...(TIMELINE_TYPE[type] ?? {})}>
|
<Timeline.Item key={id} active {...(TIMELINE_TYPE[type] ?? {})}>
|
||||||
<Text size="sm">{text}</Text>
|
<Text size="sm">
|
||||||
|
{text.split("\n").map((str) => (
|
||||||
|
<>
|
||||||
|
{str}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</Text>
|
||||||
<Flex mt={4} gap="xs">
|
<Flex mt={4} gap="xs">
|
||||||
{extra?.mastodon_status_url ? (
|
{extra?.mastodon_status_url ? (
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
|
|
|
||||||
12
web/vite/src/index.d.ts
vendored
12
web/vite/src/index.d.ts
vendored
|
|
@ -1,3 +1,15 @@
|
||||||
|
type UserProfile = {
|
||||||
|
username: string;
|
||||||
|
telegram_photo_url?: string;
|
||||||
|
mastodon_server?: string;
|
||||||
|
mastodon_username?: string;
|
||||||
|
mastodon_attn_list?: string;
|
||||||
|
mastodon_post_public: boolean;
|
||||||
|
has_doms: boolean;
|
||||||
|
verify_mastodon_favorite?: boolean;
|
||||||
|
verify_delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
type OrderSetOrderAddOn = {
|
type OrderSetOrderAddOn = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue