add ping groups
parent
01040daf7c
commit
a90744145a
|
@ -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 *
|
||||
|
|
|
@ -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]
|
|
@ -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})>"
|
|
@ -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):
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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!')
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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?',
|
||||
|
|
|
@ -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 -%}
|
||||
|
|
|
@ -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]}}">
|
||||
|
|
|
@ -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);
|
Loading…
Reference in New Issue