add ping groups

pull/134/head
Aevann 2023-02-24 08:31:06 +02:00
parent 01040daf7c
commit a90744145a
14 changed files with 350 additions and 10 deletions

View File

@ -30,3 +30,5 @@ from .transactions import *
from .sub_logs import *
from .media import *
from .push_subscriptions import *
from .group import *
from .group_membership import *

View File

@ -0,0 +1,45 @@
import time
from sqlalchemy import Column
from sqlalchemy.orm import relationship
from sqlalchemy.types import Integer
from files.classes import Base
from files.helpers.lazy import lazy
from files.helpers.config.const import *
from .group_membership import *
class Group(Base):
__tablename__ = "groups"
name = Column(String, primary_key=True)
created_utc = Column(Integer)
memberships = relationship("GroupMembership", primaryjoin="GroupMembership.group_name==Group.name", order_by="GroupMembership.created_utc")
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return self.name
@property
@lazy
def owner(self):
return self.memberships[0].user
@property
@lazy
def members(self):
return [x.user for x in self.memberships if x.approved_utc]
@property
@lazy
def member_ids(self):
return [x.id for x in self.members]
@property
@lazy
def applied_ids(self):
return [x.user_id for x in self.memberships if not x.approved_utc]

View File

@ -0,0 +1,23 @@
import time
from sqlalchemy import Column, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.types import Integer, String
from files.classes import Base
class GroupMembership(Base):
__tablename__ = "group_memberships"
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
group_name = Column(String, ForeignKey("groups.name"), primary_key=True)
created_utc = Column(Integer)
approved_utc = Column(Integer)
user = relationship("User", uselist=False)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<{self.__class__.__name__}(user_id={self.user_id}, group={self.group})>"

View File

@ -5,7 +5,7 @@ import gevent
from flask import g
from pywebpush import webpush
from files.classes import Comment, Notification, PushSubscription
from files.classes import Comment, Notification, PushSubscription, Group
from .config.const import *
from .regex import *
@ -126,10 +126,13 @@ def NOTIFY_USERS(text, v):
if word in text and id not in notify_users:
notify_users.add(id)
if v.id != AEVANN_ID and '!biofoids' in text and SITE == 'rdrama.net':
if v.id not in BIOFOIDS:
abort(403, "Only members of the ping group can ping it!")
notify_users.update(BIOFOIDS)
if FEATURES['PING_GROUPS']:
for i in group_mention_regex.finditer(text):
group = g.db.get(Group, i.group(2))
if group:
if v.id not in group.member_ids:
abort(403, "Only members of the ping group can ping it!")
notify_users.update(group.member_ids)
names = set(m.group(2) for m in mention_regex.finditer(text))
for user in get_users(names, graceful=True):

View File

@ -517,6 +517,7 @@ FEATURES = {
'PATRON_ICONS': False,
'ASSET_SUBMISSIONS': False,
'NSFW_MARKING': True,
'PING_GROUPS': True,
}
WERKZEUG_ERROR_DESCRIPTIONS = {
@ -621,6 +622,7 @@ HOLE_NAME = 'hole'
HOLE_STYLE_FLAIR = False
HOLE_REQUIRED = False
HOLE_COST = 0
GROUP_COST = 10000
HOLE_INACTIVITY_DELETION = False
PRIVILEGED_USER_BOTS = ()
@ -1043,5 +1045,3 @@ AUDIO_FORMATS = ('mp3','wav','ogg','aac','m4a','flac')
if not IS_LOCALHOST and SECRET_KEY == DEFAULT_CONFIG_VALUE:
from warnings import warn
warn("Secret key is the default value! Please change it to a secure random number. Thanks <3", RuntimeWarning)
BIOFOIDS = {2654,12966,10682,11375,18523,2432,1054,2054,15869,18339,2113,114,35}

View File

@ -7,7 +7,9 @@ from .config.const import *
valid_username_chars = 'a-zA-Z0-9_\-'
valid_username_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$", flags=re.A)
mention_regex = re.compile('(^|\s|>)@(([a-zA-Z0-9_\-]){1,30})(?![^<]*<\/(code|pre|a)>)', flags=re.A)
mention_regex = re.compile('(^|\s|>)@([a-zA-Z0-9_\-]{1,30})(?![^<]*<\/(code|pre|a)>)', flags=re.A)
group_mention_regex = re.compile('(^|\s|>)!([a-z0-9_\-]{3,25})(?![^<]*<\/(code|pre|a)>)', flags=re.A)
valid_password_regex = re.compile("^.{8,100}$", flags=re.A)

View File

@ -47,3 +47,5 @@ if FEATURES['ASSET_SUBMISSIONS']:
from .asset_submissions import *
from .special import *
from .push_notifs import *
if FEATURES['PING_GROUPS']:
from .groups import *

View File

@ -0,0 +1,115 @@
from files.classes import *
from files.helpers.alerts import *
from files.helpers.regex import *
from files.helpers.get import *
from files.routes.wrappers import *
from files.__main__ import app, limiter
@app.get("/ping_groups")
@limiter.limit(DEFAULT_RATELIMIT, key_func=get_ID)
@auth_required
def ping_groups(v:User):
groups = g.db.query(Group).order_by(Group.created_utc).all()
return render_template('groups.html', v=v, groups=groups, cost=GROUP_COST, msg=get_msg(), error=get_error())
@app.post("/create_group")
@limiter.limit(DEFAULT_RATELIMIT, key_func=get_ID)
@is_not_permabanned
def create_group(v):
name = request.values.get('name')
if not name: abort(400)
name = name.strip().lower()
if not valid_sub_regex.fullmatch(name):
return redirect(f"/ping_groups?error=Name does not match the required format!")
group = g.db.get(Group, name)
if not group:
if not v.charge_account('coins', GROUP_COST):
return redirect(f"/ping_groups?error=You don't have enough coins!")
g.db.add(v)
if v.shadowbanned: abort(500)
group = Group(name=name)
g.db.add(group)
g.db.flush()
group_membership = GroupMembership(
user_id=v.id,
group_name=group.name,
created_utc=time.time(),
approved_utc=time.time()
)
g.db.add(group_membership)
admins = [x[0] for x in g.db.query(User.id).filter(User.admin_level >= PERMS['NOTIFICATIONS_HOLE_CREATION'], User.id != v.id).all()]
for admin in admins:
send_repeatable_notification(admin, f":!marseyparty: !{group} has been created by @{v.username} :marseyparty:")
return redirect(f'/ping_groups?msg=!{group} created successfully!')
@app.post("/!<group_name>/join")
@limiter.limit(DEFAULT_RATELIMIT, key_func=get_ID)
@auth_required
def join_group(v:User, group_name):
group = g.db.get(Group, group_name)
if not group: abort(404)
existing = g.db.query(GroupMembership).filter_by(user_id=v.id, group_name=group_name).one_or_none()
if not existing:
join = GroupMembership(user_id=v.id, group_name=group_name)
g.db.add(join)
send_repeatable_notification(group.owner.id, f"@{v.username} has applied to join !{group}. You can approve or reject the application [here](/!{group}/applications).")
return redirect(f"/ping_groups?msg=Application submitted to !{group}'s owner (@{group.owner.username}) successfully!")
@app.get("/!<group_name>/applications")
@limiter.limit(DEFAULT_RATELIMIT, key_func=get_ID)
@auth_required
def applications(v:User, group_name):
group = g.db.get(Group, group_name)
if not group: abort(404)
applications = g.db.query(GroupMembership).filter_by(group_name=group_name, approved_utc=None).order_by(GroupMembership.created_utc.desc()).all()
return render_template('group_applications.html', v=v, group=group, applications=applications, msg=get_msg())
@app.post("/!<group_name>/<user_id>/approve")
@limiter.limit(DEFAULT_RATELIMIT, key_func=get_ID)
@auth_required
def group_approve(v:User, group_name, user_id):
group = g.db.get(Group, group_name)
if not group: abort(404)
if v.id != group.owner.id:
abort(403, f"Only the group owner (@{group.owner.username}) can approve applications!")
application = g.db.query(GroupMembership).filter_by(user_id=user_id, group_name=group.name).one_or_none()
if not application:
abort(404, "There is no application to approve!")
if not application.approved_utc:
application.approved_utc = time.time()
g.db.add(application)
send_repeatable_notification(application.user_id, f"@{v.username} (!{group}'s owner) has approved your application!")
return redirect(f'/!{group}/applications?msg=@{application.user.username} has been approved successfully!')
@app.post("/!<group_name>/<user_id>/reject")
@limiter.limit(DEFAULT_RATELIMIT, key_func=get_ID)
@auth_required
def group_reject(v:User, group_name, user_id):
group = g.db.get(Group, group_name)
if not group: abort(404)
if v.id != group.owner.id:
abort(403, f"Only the group owner (@{group.owner.username}) can reject applications!")
application = g.db.query(GroupMembership).filter_by(user_id=user_id, group_name=group.name).one_or_none()
if not application:
abort(404, "There is no application to reject!")
g.db.delete(application)
send_repeatable_notification(application.user_id, f"@{v.username} (!{group}'s owner) has rejected your application!")
return redirect(f'/!{group}/applications?msg=@{application.user.username} has been rejected successfully!')

View File

@ -0,0 +1,44 @@
{% extends "default.html" %}
{% block pagetitle %}!{{group}} Applications{% endblock %}
{% block content %}
{% if error %}{{macros.alert(error, true)}}{% endif %}
{% if msg %}{{macros.alert(msg, false)}}{% endif %}
<h5 class="my-3">!{{group}} Applications</h5>
<div class="overflow-x-auto mt-1"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Name</th>
<th>Applied on</th>
{% if v.id == group.owner.id %}
<th></th>
<th></th>
{% endif %}
</tr>
</thead>
<tbody>
{% for application in applications %}
<tr>
<td>
{% with user=application.user %}
{% include "user_in_table.html" %}
{% endwith %}
</td>
<td data-time="{{application.created_utc}}"></td>
{% if v.id == group.owner.id %}
<td>
<form class="d-inline" action="/!{{group}}/{{application.user_id}}/approve" method="post" data-nonce="{{g.nonce}}">
<input hidden name="formkey" value="{{v|formkey}}">
<input autocomplete="off" class="btn btn-success ml-auto" type="submit" data-nonce="{{g.nonce}}" data-onclick="disable(this)" value="Approve">
</form>
<form class="d-inline ml-3" action="/!{{group}}/{{application.user_id}}/reject" method="post" data-nonce="{{g.nonce}}">
<input hidden name="formkey" value="{{v|formkey}}">
<input autocomplete="off" class="btn btn-danger ml-auto" type="submit" data-nonce="{{g.nonce}}" data-onclick="disable(this)" value="Reject">
</form>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -0,0 +1,76 @@
{% extends "default.html" %}
{% block pagetitle %}Ping Groups{% endblock %}
{% block content %}
{% if error %}{{macros.alert(error, true)}}{% endif %}
{% if msg %}{{macros.alert(msg, false)}}{% endif %}
<div class="px-3">
<h3 class="my-3">Ping Groups</h3>
<div class="overflow-x-auto">
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Members</th>
<th>Created on</th>
<th></th>
</tr>
</thead>
{% for group in groups %}
<tr>
<td>{{loop.index}}</td>
<td>{{group.name}}</td>
<td>
{% for user in group.members %}
<span class="mr-2">
{% include "user_in_table.html" %}
</span>
{% endfor %}
</td>
<td data-time="{{group.created_utc}}"></td>
<td>
{% if v.id in group.applied_ids %}
<a class="btn btn-primary" href="/!{{group.name}}/applications">Application pending</a>
{% elif v.id not in group.member_ids %}
<form action="/!{{group}}/join" method="post" data-nonce="{{g.nonce}}">
<input hidden name="formkey" value="{{v|formkey}}">
<input autocomplete="off" class="btn btn-primary ml-auto" type="submit" data-nonce="{{g.nonce}}" data-onclick="disable(this)" value="Apply to Join">
</form>
{% else %}
<a class="btn btn-primary" href="/!{{group.name}}/applications">Applications</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
<h3 class="mt-5 mb-3">Create Ping Group</h3>
<form class="mt-3" action="/create_group" method="post">
<div class="container">
<div class="row mb-4 pb-6">
<div class="col col-md-6 px-0 py-3 py-md-0">
<div class="body">
<input hidden name="formkey" value="{{v|formkey}}">
<label for="title">Group Name</label>
<input minlength="3" maxlength="25" pattern='[a-zA-Z0-9_\-]*' class="form-control" type="text" name="name" required>
<small class="form-text text-muted">3-25 characters, including letters, numbers, _ , and -</small>
<div class="footer">
<div class="d-flex">
{% if error %}
<p class="mb-0">
<span class="text-danger text-small" style="vertical-align: sub;">{{error}}</span>
</p>
{% endif %}
<button type="submit" class="btn btn-primary ml-auto" {% if v.coins < cost %}disabled{% endif %}>Create Group</button>
</div>
<p class="mt-2 mr-1" style="float: right"><b>Cost</b>: {{cost}} coins</p>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -7,6 +7,12 @@
{%- if SITE_NAME == 'rDrama' -%}
{%- do MEGATHREAD_INDEX.extend([
(
'Ping Groups',
'List of ping groups',
'fa-users', '#dc3545',
'/ping_groups',
),
(
'Bugs / Suggestions',
'Something broken? Improvements?',

View File

@ -52,7 +52,7 @@
{% else %}
<a id="sidebar--directory--btn" class="btn btn-primary btn-block mb-3" href="/directory">
<span id="sidebar--directory--head">DIRECTORY</span>
<span id="sidebar--directory--subhead">Submit Marseys & Art | Info Megathreads</span>
<span id="sidebar--directory--subhead">Ping Groups | Submit Marseys & Art | Info Megathreads</span>
</a>
<a class="btn btn-primary btn-block mb-3" href="/holes">BROWSE {{HOLE_NAME|upper}}S</a>
{% if v and v.can_create_hole -%}

View File

@ -1,7 +1,7 @@
{%- include 'admin/shadowbanned_tooltip.html' -%}
{% if user %}
<a data-sort-key="{{user.username.lower()}}" style="color:#{{user.name_color}};font-weight:bold" href="/@{{user.username}}">
<div class="profile-pic-20-wrapper mb-2 mr-1 float-left">
<div class="profile-pic-20-wrapper mb-2 {% if request.path != '/ping_groups' %}mr-1 float-left{% endif %}">
<img loading="lazy" src="{{user.profile_url}}" class="pp20">
{% if user.hat_active(v)[0] -%}
<img class="profile-pic-20-hat hat" loading="lazy" src="{{user.hat_active(v)[0]}}?h=7" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{user.hat_active(v)[1]}}">

View File

@ -0,0 +1,22 @@
create table groups (
name character varying(25) primary key,
created_utc integer not null
);
create index groups_index on groups using btree (created_utc asc);
create table group_memberships (
user_id integer not null,
group_name varchar(25) not null,
created_utc integer not null,
approved_utc integer
);
alter table only group_memberships
add constraint group_memberships_pkey primary key (user_id, group_name);
alter table only group_memberships
add constraint group_memberships_user_fkey foreign key (user_id) references public.users(id);
alter table only group_memberships
add constraint group_memberships_group_fkey foreign key (group_name) references public.groups(name);