add private chats
parent
1e007e235c
commit
aa14d0e6d9
|
@ -221,6 +221,7 @@
|
||||||
.fa-chart-simple:before{content:"\e473"}
|
.fa-chart-simple:before{content:"\e473"}
|
||||||
.fa-memo:before{content:"\e1d8"}
|
.fa-memo:before{content:"\e1d8"}
|
||||||
.fa-file-pen:before{content:"\f31c"}
|
.fa-file-pen:before{content:"\f31c"}
|
||||||
|
.fa-pen:before{content:"\f304"}
|
||||||
|
|
||||||
/* do not remove - fixes hand, talking, marsey-love components
|
/* do not remove - fixes hand, talking, marsey-love components
|
||||||
from breaking out of the comment box
|
from breaking out of the comment box
|
||||||
|
@ -3484,7 +3485,9 @@ pre {
|
||||||
.text-small {
|
.text-small {
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
}
|
}
|
||||||
|
.text-smaller {
|
||||||
|
font-size: 9px !important;
|
||||||
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.text-small-sm {
|
.text-small-sm {
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
|
|
|
@ -135,6 +135,11 @@ socket.on('speak', function(json) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let chat_id = 'chat'
|
||||||
|
const chat_id_el = document.getElementById('chat_id')
|
||||||
|
if (chat_id_el)
|
||||||
|
chat_id = chat_id_el.value
|
||||||
|
|
||||||
function send() {
|
function send() {
|
||||||
const text = ta.value.trim();
|
const text = ta.value.trim();
|
||||||
const input = document.getElementById('file');
|
const input = document.getElementById('file');
|
||||||
|
@ -148,6 +153,7 @@ function send() {
|
||||||
"message": text,
|
"message": text,
|
||||||
"quotes": document.getElementById('quotes_id').value,
|
"quotes": document.getElementById('quotes_id').value,
|
||||||
"file": sending,
|
"file": sending,
|
||||||
|
"chat_id": chat_id,
|
||||||
});
|
});
|
||||||
ta.value = ''
|
ta.value = ''
|
||||||
is_typing = false
|
is_typing = false
|
||||||
|
@ -200,6 +206,18 @@ ta.addEventListener("keydown", function(e) {
|
||||||
|
|
||||||
socket.on('online', function(data){
|
socket.on('online', function(data){
|
||||||
const online_li = data[0]
|
const online_li = data[0]
|
||||||
|
if (location.pathname.startsWith('/chat/')) {
|
||||||
|
for (const marker of document.getElementsByClassName('online-marker')) {
|
||||||
|
marker.classList.add('d-none')
|
||||||
|
}
|
||||||
|
for (const u of online_li) {
|
||||||
|
for (const marker of document.getElementsByClassName(`online-marker-${u[4]}`)) {
|
||||||
|
marker.classList.remove('d-none')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const muted_li = Object.keys(data[1])
|
const muted_li = Object.keys(data[1])
|
||||||
|
|
||||||
document.getElementsByClassName('board-chat-count')[0].innerHTML = online_li.length
|
document.getElementsByClassName('board-chat-count')[0].innerHTML = online_li.length
|
||||||
|
|
|
@ -121,7 +121,7 @@ function postToastSwitch(t, url, button1, button2, cls, extraActionsOnSuccess) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!location.pathname.endsWith('/submit') && !location.pathname.endsWith('/chat'))
|
if (!location.pathname.endsWith('/submit') && !location.pathname.startsWith('/chat'))
|
||||||
{
|
{
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (!((e.ctrlKey || e.metaKey) && e.key === "Enter")) return;
|
if (!((e.ctrlKey || e.metaKey) && e.key === "Enter")) return;
|
||||||
|
@ -399,7 +399,7 @@ if (is_pwa) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const gbrowser = document.getElementById('gbrowser').value
|
const gbrowser = document.getElementById('gbrowser').value
|
||||||
if (location.pathname != '/chat' && (gbrowser == 'iphone' || gbrowser == 'mac')) {
|
if (!location.pathname.startsWith('/chat') && (gbrowser == 'iphone' || gbrowser == 'mac')) {
|
||||||
const videos = document.querySelectorAll('video')
|
const videos = document.querySelectorAll('video')
|
||||||
|
|
||||||
for (const video of videos) {
|
for (const video of videos) {
|
||||||
|
@ -783,3 +783,14 @@ for (const el of document.getElementsByClassName('tor-disabled')) {
|
||||||
window.alert("File uploads are not allowed through TOR!")
|
window.alert("File uploads are not allowed through TOR!")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleElement(id, id2) {
|
||||||
|
for (let el of document.getElementsByClassName('toggleable')) {
|
||||||
|
if (el.id != id) {
|
||||||
|
el.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById(id).classList.toggle('d-none');
|
||||||
|
document.getElementById(id2).focus()
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ function update_ghost_div_textarea(text)
|
||||||
{
|
{
|
||||||
let ghostdiv
|
let ghostdiv
|
||||||
|
|
||||||
if (location.pathname == '/chat')
|
if (location.pathname.startsWith('/chat') )
|
||||||
ghostdiv = document.getElementById("ghostdiv-chat");
|
ghostdiv = document.getElementById("ghostdiv-chat");
|
||||||
else
|
else
|
||||||
ghostdiv = text.parentNode.getElementsByClassName("ghostdiv")[0];
|
ghostdiv = text.parentNode.getElementsByClassName("ghostdiv")[0];
|
||||||
|
|
|
@ -1,14 +1,3 @@
|
||||||
function toggleElement(id, id2) {
|
|
||||||
for (let el of document.getElementsByClassName('toggleable')) {
|
|
||||||
if (el.id != id) {
|
|
||||||
el.classList.add('d-none');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById(id).classList.toggle('d-none');
|
|
||||||
document.getElementById(id2).focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
let TRANSFER_TAX = document.getElementById('tax').innerHTML
|
let TRANSFER_TAX = document.getElementById('tax').innerHTML
|
||||||
|
|
||||||
function updateTax(mobile=false) {
|
function updateTax(mobile=false) {
|
||||||
|
|
|
@ -40,3 +40,4 @@ if FEATURES['IP_LOGGING']:
|
||||||
from .ip_logs import *
|
from .ip_logs import *
|
||||||
|
|
||||||
from .edit_logs import *
|
from .edit_logs import *
|
||||||
|
from .private_chats import *
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
import time
|
||||||
|
|
||||||
|
from sqlalchemy import Column, ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql.sqltypes import *
|
||||||
|
|
||||||
|
from files.classes import Base
|
||||||
|
from files.helpers.lazy import lazy
|
||||||
|
|
||||||
|
class Chat(Base):
|
||||||
|
__tablename__ = "chats"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
owner_id = Column(Integer, ForeignKey("users.id"))
|
||||||
|
name = Column(String)
|
||||||
|
created_utc = Column(Integer)
|
||||||
|
|
||||||
|
memberships = relationship("ChatMembership")
|
||||||
|
|
||||||
|
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__}(id={self.id})>"
|
||||||
|
|
||||||
|
|
||||||
|
class ChatMembership(Base):
|
||||||
|
__tablename__ = "chat_memberships"
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
|
||||||
|
chat_id = Column(Integer, ForeignKey("chats.id"), primary_key=True)
|
||||||
|
created_utc = Column(Integer)
|
||||||
|
|
||||||
|
user = relationship("User")
|
||||||
|
|
||||||
|
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}, chat_id={self.chat_id})>"
|
||||||
|
|
||||||
|
@lazy
|
||||||
|
def unread_count(v):
|
||||||
|
return g.db.query(ChatNotification).filter_by(user_id=v.id, read=False, chat_id=self.chat_id).count()
|
||||||
|
|
||||||
|
|
||||||
|
class ChatLeave(Base):
|
||||||
|
__tablename__ = "chat_leaves"
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
|
||||||
|
chat_id = Column(Integer, ForeignKey("chats.id"), primary_key=True)
|
||||||
|
created_utc = Column(Integer)
|
||||||
|
|
||||||
|
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}, chat_id={self.chat_id})>"
|
||||||
|
|
||||||
|
|
||||||
|
class ChatNotification(Base):
|
||||||
|
__tablename__ = "chat_notifications"
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
|
||||||
|
chat_message_id = Column(Integer, ForeignKey("chat_messages.id"), primary_key=True)
|
||||||
|
chat_id = Column(Integer, ForeignKey("chats.id"))
|
||||||
|
created_utc = Column(Integer)
|
||||||
|
|
||||||
|
chat_message = relationship("ChatMessage")
|
||||||
|
|
||||||
|
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}, chat_message_id={self.message_id})>"
|
||||||
|
|
||||||
|
|
||||||
|
class ChatMessage(Base):
|
||||||
|
__tablename__ = "chat_messages"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"))
|
||||||
|
chat_id = Column(Integer, ForeignKey("chats.id"))
|
||||||
|
quotes = Column(Integer, ForeignKey("chat_messages.id"))
|
||||||
|
text = Column(String)
|
||||||
|
text_censored = Column(String)
|
||||||
|
text_html = Column(String)
|
||||||
|
text_html_censored = Column(String)
|
||||||
|
created_utc = Column(Integer)
|
||||||
|
|
||||||
|
user = relationship("User")
|
||||||
|
|
||||||
|
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__}(id={self.id})>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
@lazy
|
||||||
|
def username(self):
|
||||||
|
return self.user.username
|
||||||
|
|
||||||
|
@property
|
||||||
|
@lazy
|
||||||
|
def hat(self):
|
||||||
|
return self.user.hat_active(None)[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
@lazy
|
||||||
|
def namecolor(self):
|
||||||
|
return self.user.name_color
|
||||||
|
|
||||||
|
@property
|
||||||
|
@lazy
|
||||||
|
def patron(self):
|
||||||
|
return self.user.patron
|
||||||
|
|
||||||
|
@property
|
||||||
|
@lazy
|
||||||
|
def pride_username(self):
|
||||||
|
return self.user.pride_username(None)
|
|
@ -14,6 +14,7 @@ from files.classes import Base
|
||||||
from files.classes.casino_game import CasinoGame
|
from files.classes.casino_game import CasinoGame
|
||||||
from files.classes.group import *
|
from files.classes.group import *
|
||||||
from files.classes.hole import Hole
|
from files.classes.hole import Hole
|
||||||
|
from files.classes.private_chats import ChatNotification
|
||||||
from files.classes.currency_logs import CurrencyLog
|
from files.classes.currency_logs import CurrencyLog
|
||||||
from files.helpers.config.const import *
|
from files.helpers.config.const import *
|
||||||
from files.helpers.config.modaction_types import *
|
from files.helpers.config.modaction_types import *
|
||||||
|
@ -815,18 +816,28 @@ class User(Base):
|
||||||
Comment.deleted_utc == 0,
|
Comment.deleted_utc == 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
return notifs.count() + self.modmail_notifications_count + self.post_notifications_count + self.modaction_notifications_count + self.offsite_notifications_count
|
return notifs.count() + self.chats_notifications_count + self.modmail_notifications_count + self.post_notifications_count + self.modaction_notifications_count + self.offsite_notifications_count
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@lazy
|
@lazy
|
||||||
def normal_notifications_count(self):
|
def normal_notifications_count(self):
|
||||||
return self.notifications_count \
|
return self.notifications_count \
|
||||||
|
- self.chats_notifications_count \
|
||||||
- self.message_notifications_count \
|
- self.message_notifications_count \
|
||||||
- self.modmail_notifications_count \
|
- self.modmail_notifications_count \
|
||||||
- self.post_notifications_count \
|
- self.post_notifications_count \
|
||||||
- self.modaction_notifications_count \
|
- self.modaction_notifications_count \
|
||||||
- self.offsite_notifications_count
|
- self.offsite_notifications_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
@lazy
|
||||||
|
def chats_notifications_count(self):
|
||||||
|
return g.db.query(ChatNotification).filter_by(user_id=self.id).count()
|
||||||
|
|
||||||
|
@lazy
|
||||||
|
def chat_notifications_count(self, chat_id):
|
||||||
|
return g.db.query(ChatNotification).filter_by(user_id=self.id, chat_id=chat_id).count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@lazy
|
@lazy
|
||||||
def message_notifications_count(self):
|
def message_notifications_count(self):
|
||||||
|
@ -929,6 +940,8 @@ class User(Base):
|
||||||
# only meaningful when notifications_count > 0; otherwise falsely '' ~ normal
|
# only meaningful when notifications_count > 0; otherwise falsely '' ~ normal
|
||||||
if self.normal_notifications_count > 0:
|
if self.normal_notifications_count > 0:
|
||||||
return ''
|
return ''
|
||||||
|
elif self.chats_notifications_count > 0:
|
||||||
|
return 'chats'
|
||||||
elif self.message_notifications_count > 0:
|
elif self.message_notifications_count > 0:
|
||||||
return 'messages'
|
return 'messages'
|
||||||
elif self.modmail_notifications_count > 0:
|
elif self.modmail_notifications_count > 0:
|
||||||
|
@ -947,6 +960,7 @@ class User(Base):
|
||||||
colors = {
|
colors = {
|
||||||
'': '#dc3545',
|
'': '#dc3545',
|
||||||
'messages': '#d8910d',
|
'messages': '#d8910d',
|
||||||
|
'chats': '#008080',
|
||||||
'modmail': '#f15387',
|
'modmail': '#f15387',
|
||||||
'posts': '#0000ff',
|
'posts': '#0000ff',
|
||||||
'modactions': '#1ad80d',
|
'modactions': '#1ad80d',
|
||||||
|
@ -1337,7 +1351,7 @@ class User(Base):
|
||||||
|
|
||||||
@lazy
|
@lazy
|
||||||
def pride_username(self, v):
|
def pride_username(self, v):
|
||||||
return not (v and v.poor) and self.has_badge(303)
|
return bool(not (v and v.poor) and self.has_badge(303))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@lazy
|
@lazy
|
||||||
|
|
|
@ -225,6 +225,7 @@ PERMS = { # Minimum admin_level to perform action.
|
||||||
'MODS_EVERY_HOLE': 5,
|
'MODS_EVERY_HOLE': 5,
|
||||||
'MODS_EVERY_GROUP': 5,
|
'MODS_EVERY_GROUP': 5,
|
||||||
'VIEW_EMAILS': 5,
|
'VIEW_EMAILS': 5,
|
||||||
|
'VIEW_CHATS': 5,
|
||||||
'INFINITE_CURRENCY': 5,
|
'INFINITE_CURRENCY': 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -546,6 +547,7 @@ NOTIFICATION_SPAM_AGE_THRESHOLD = 0
|
||||||
COMMENT_SPAM_LENGTH_THRESHOLD = 0
|
COMMENT_SPAM_LENGTH_THRESHOLD = 0
|
||||||
|
|
||||||
DEFAULT_UNDER_SIEGE_THRESHOLDS = {
|
DEFAULT_UNDER_SIEGE_THRESHOLDS = {
|
||||||
|
"private chat": 0,
|
||||||
"chat": 0,
|
"chat": 0,
|
||||||
"normal comment": 0,
|
"normal comment": 0,
|
||||||
"wall comment": 0,
|
"wall comment": 0,
|
||||||
|
@ -794,6 +796,7 @@ elif SITE in {'watchpeopledie.tv', 'marsey.world'}:
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_UNDER_SIEGE_THRESHOLDS = {
|
DEFAULT_UNDER_SIEGE_THRESHOLDS = {
|
||||||
|
"private chat": 1440,
|
||||||
"chat": 1440,
|
"chat": 1440,
|
||||||
"normal comment": 10,
|
"normal comment": 10,
|
||||||
"wall comment": 1440,
|
"wall comment": 1440,
|
||||||
|
|
|
@ -11,6 +11,8 @@ valid_username_patron_regex = re.compile("^[\w-]{1,25}$", flags=re.A)
|
||||||
mention_regex = re.compile('(?<![:/\w])@([\w-]{1,30})' + NOT_IN_CODE_OR_LINKS, flags=re.A)
|
mention_regex = re.compile('(?<![:/\w])@([\w-]{1,30})' + NOT_IN_CODE_OR_LINKS, flags=re.A)
|
||||||
group_mention_regex = re.compile('(?<![:/\w])!([\w-]{3,25})' + NOT_IN_CODE_OR_LINKS, flags=re.A|re.I)
|
group_mention_regex = re.compile('(?<![:/\w])!([\w-]{3,25})' + NOT_IN_CODE_OR_LINKS, flags=re.A|re.I)
|
||||||
|
|
||||||
|
chat_adding_regex = re.compile('\+@[\w-]{1,30}' + NOT_IN_CODE_OR_LINKS, flags=re.A)
|
||||||
|
|
||||||
everyone_regex = re.compile('(^|\s|>)!(everyone)' + NOT_IN_CODE_OR_LINKS, flags=re.A)
|
everyone_regex = re.compile('(^|\s|>)!(everyone)' + NOT_IN_CODE_OR_LINKS, flags=re.A)
|
||||||
|
|
||||||
valid_password_regex = re.compile("^.{8,100}$", flags=re.A)
|
valid_password_regex = re.compile("^.{8,100}$", flags=re.A)
|
||||||
|
|
|
@ -53,6 +53,7 @@ from .special import *
|
||||||
from .push_notifs import *
|
from .push_notifs import *
|
||||||
if FEATURES['PING_GROUPS']:
|
if FEATURES['PING_GROUPS']:
|
||||||
from .groups import *
|
from .groups import *
|
||||||
|
from .private_chats import *
|
||||||
|
|
||||||
if IS_LOCALHOST:
|
if IS_LOCALHOST:
|
||||||
from sys import argv
|
from sys import argv
|
||||||
|
|
|
@ -17,6 +17,7 @@ from files.helpers.alerts import push_notif
|
||||||
from files.helpers.can_see import *
|
from files.helpers.can_see import *
|
||||||
from files.routes.wrappers import *
|
from files.routes.wrappers import *
|
||||||
from files.classes.orgy import *
|
from files.classes.orgy import *
|
||||||
|
from files.classes.private_chats import *
|
||||||
|
|
||||||
from files.__main__ import app, cache, limiter
|
from files.__main__ import app, cache, limiter
|
||||||
|
|
||||||
|
@ -32,16 +33,22 @@ socketio = SocketIO(
|
||||||
muted = cache.get(f'muted') or {}
|
muted = cache.get(f'muted') or {}
|
||||||
|
|
||||||
messages = cache.get(f'messages') or {}
|
messages = cache.get(f'messages') or {}
|
||||||
online = {}
|
|
||||||
typing = []
|
|
||||||
|
|
||||||
cache.set('loggedin_chat', len(online), timeout=86400)
|
online = {
|
||||||
|
"chat": {},
|
||||||
|
"messages": set()
|
||||||
|
}
|
||||||
|
|
||||||
|
typing = {
|
||||||
|
"chat": []
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.set('loggedin_chat', len(online["chat"]), timeout=86400)
|
||||||
|
|
||||||
def auth_required_socketio(f):
|
def auth_required_socketio(f):
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
v = get_logged_in_user()
|
v = get_logged_in_user()
|
||||||
if not v: return '', 401
|
if not v or v.is_permabanned: return ''
|
||||||
if v.is_permabanned: return '', 403
|
|
||||||
return make_response(f(*args, v=v, **kwargs))
|
return make_response(f(*args, v=v, **kwargs))
|
||||||
wrapper.__name__ = f.__name__
|
wrapper.__name__ = f.__name__
|
||||||
return wrapper
|
return wrapper
|
||||||
|
@ -49,8 +56,7 @@ def auth_required_socketio(f):
|
||||||
def is_not_banned_socketio(f):
|
def is_not_banned_socketio(f):
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
v = get_logged_in_user()
|
v = get_logged_in_user()
|
||||||
if not v: return '', 401
|
if not v or v.is_suspended: return ''
|
||||||
if v.is_suspended: return '', 403
|
|
||||||
return make_response(f(*args, v=v, **kwargs))
|
return make_response(f(*args, v=v, **kwargs))
|
||||||
wrapper.__name__ = f.__name__
|
wrapper.__name__ = f.__name__
|
||||||
return wrapper
|
return wrapper
|
||||||
|
@ -62,6 +68,10 @@ def refresh_chat():
|
||||||
emit('refresh_chat', namespace='/', to="chat")
|
emit('refresh_chat', namespace='/', to="chat")
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@app.get("/chat/")
|
||||||
|
def chat_redirect():
|
||||||
|
return redirect("/chat")
|
||||||
|
|
||||||
@app.get("/chat")
|
@app.get("/chat")
|
||||||
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
|
@ -83,8 +93,105 @@ def chat(v):
|
||||||
@socketio.on('speak')
|
@socketio.on('speak')
|
||||||
@is_not_banned_socketio
|
@is_not_banned_socketio
|
||||||
def speak(data, v):
|
def speak(data, v):
|
||||||
if not v.allowed_in_chat:
|
if request.referrer.startswith(f'{SITE_FULL}/chat/'):
|
||||||
return '', 403
|
chat_id = int(data['chat_id'])
|
||||||
|
|
||||||
|
chat = g.db.get(Chat, chat_id)
|
||||||
|
if not chat:
|
||||||
|
abort(404, "Chat not found!")
|
||||||
|
|
||||||
|
is_member = g.db.query(ChatMembership.user_id).filter_by(user_id=v.id, chat_id=chat_id).one_or_none()
|
||||||
|
if not is_member: return ''
|
||||||
|
|
||||||
|
image = None
|
||||||
|
if data['file']:
|
||||||
|
name = f'/chat_images/{time.time()}'.replace('.','') + '.webp'
|
||||||
|
with open(name, 'wb') as f:
|
||||||
|
f.write(data['file'])
|
||||||
|
image = process_image(name, v)
|
||||||
|
|
||||||
|
text = data['message'].strip()[:CHAT_LENGTH_LIMIT]
|
||||||
|
if image: text += f'\n\n{image}'
|
||||||
|
if not text: return ''
|
||||||
|
|
||||||
|
text_html = sanitize(text, count_emojis=True, chat=True)
|
||||||
|
if isinstance(text_html , tuple): return ''
|
||||||
|
|
||||||
|
execute_under_siege(v, None, text, "private chat")
|
||||||
|
|
||||||
|
quotes = data['quotes']
|
||||||
|
if quotes: quotes = int(quotes)
|
||||||
|
else: quotes = None
|
||||||
|
|
||||||
|
chat_message = ChatMessage(
|
||||||
|
user_id=v.id,
|
||||||
|
chat_id=chat_id,
|
||||||
|
quotes=quotes,
|
||||||
|
text=text,
|
||||||
|
text_censored=censor_slurs_profanities(text, 'chat', True),
|
||||||
|
text_html=text_html,
|
||||||
|
text_html_censored=censor_slurs_profanities(text_html, 'chat'),
|
||||||
|
)
|
||||||
|
g.db.add(chat_message)
|
||||||
|
g.db.flush()
|
||||||
|
|
||||||
|
if v.id == chat.owner_id and chat_adding_regex.fullmatch(text):
|
||||||
|
user = get_user(text[2:], graceful=True, attributes=[User.id])
|
||||||
|
if user:
|
||||||
|
user_id = user.id
|
||||||
|
existing = g.db.query(ChatMembership.user_id).filter_by(user_id=user_id, chat_id=chat_id).one_or_none()
|
||||||
|
leave = g.db.query(ChatLeave.user_id).filter_by(user_id=user_id, chat_id=chat_id).one_or_none()
|
||||||
|
if not existing and not leave:
|
||||||
|
chat_membership = ChatMembership(
|
||||||
|
user_id=user.id,
|
||||||
|
chat_id=chat_id,
|
||||||
|
)
|
||||||
|
g.db.add(chat_membership)
|
||||||
|
g.db.flush()
|
||||||
|
|
||||||
|
to_notify = [x[0] for x in g.db.query(ChatMembership.user_id).filter(
|
||||||
|
ChatMembership.chat_id == chat_id,
|
||||||
|
ChatMembership.user_id.notin_(online[request.referrer]),
|
||||||
|
)]
|
||||||
|
for uid in to_notify:
|
||||||
|
n = ChatNotification(
|
||||||
|
user_id=uid,
|
||||||
|
chat_message_id=chat_message.id,
|
||||||
|
chat_id=chat_id,
|
||||||
|
)
|
||||||
|
g.db.add(n)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": chat_message.id,
|
||||||
|
"quotes": chat_message.quotes,
|
||||||
|
"hat": chat_message.hat,
|
||||||
|
"user_id": chat_message.user_id,
|
||||||
|
"username": chat_message.username,
|
||||||
|
"namecolor": chat_message.namecolor,
|
||||||
|
"patron": chat_message.patron,
|
||||||
|
"pride_username": chat_message.pride_username,
|
||||||
|
"text": chat_message.text,
|
||||||
|
"text_censored": chat_message.text_censored,
|
||||||
|
"text_html": chat_message.text_html,
|
||||||
|
"text_html_censored": chat_message.text_html_censored,
|
||||||
|
"created_utc": chat_message.created_utc,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.shadowbanned or execute_blackjack(v, None, text, "chat"):
|
||||||
|
emit('speak', data)
|
||||||
|
else:
|
||||||
|
emit('speak', data, room=request.referrer, broadcast=True)
|
||||||
|
|
||||||
|
try: g.db.commit()
|
||||||
|
except: g.db.rollback()
|
||||||
|
g.db.close()
|
||||||
|
stdout.flush()
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if not v.allowed_in_chat: return ''
|
||||||
|
|
||||||
image = None
|
image = None
|
||||||
if data['file']:
|
if data['file']:
|
||||||
|
@ -95,13 +202,12 @@ def speak(data, v):
|
||||||
|
|
||||||
global messages
|
global messages
|
||||||
|
|
||||||
text = data['message'][:CHAT_LENGTH_LIMIT]
|
text = data['message'].strip()[:CHAT_LENGTH_LIMIT]
|
||||||
if image: text += f'\n\n{image}'
|
if image: text += f'\n\n{image}'
|
||||||
if not text: return '', 400
|
if not text: return ''
|
||||||
|
|
||||||
text_html = sanitize(text, count_emojis=True, chat=True)
|
text_html = sanitize(text, count_emojis=True, chat=True)
|
||||||
if isinstance(text_html , tuple):
|
if isinstance(text_html , tuple): return ''
|
||||||
return text_html
|
|
||||||
|
|
||||||
execute_under_siege(v, None, text, "chat")
|
execute_under_siege(v, None, text, "chat")
|
||||||
|
|
||||||
|
@ -116,14 +222,14 @@ def speak(data, v):
|
||||||
self_only = True
|
self_only = True
|
||||||
else:
|
else:
|
||||||
del muted[vname]
|
del muted[vname]
|
||||||
refresh_online()
|
refresh_online("chat")
|
||||||
|
|
||||||
if SITE == 'rdrama.net' and v.admin_level < PERMS['BYPASS_ANTISPAM_CHECKS']:
|
if SITE == 'rdrama.net' and v.admin_level < PERMS['BYPASS_ANTISPAM_CHECKS']:
|
||||||
def shut_up():
|
def shut_up():
|
||||||
self_only = True
|
self_only = True
|
||||||
muted_until = int(time.time() + 600)
|
muted_until = int(time.time() + 600)
|
||||||
muted[vname] = muted_until
|
muted[vname] = muted_until
|
||||||
refresh_online()
|
refresh_online("chat")
|
||||||
|
|
||||||
if not self_only:
|
if not self_only:
|
||||||
identical = [x for x in list(messages.values())[-5:] if v.id == x['user_id'] and text == x['text']]
|
identical = [x for x in list(messages.values())[-5:] if v.id == x['user_id'] and text == x['text']]
|
||||||
|
@ -160,7 +266,7 @@ def speak(data, v):
|
||||||
username = i.group(1).lower()
|
username = i.group(1).lower()
|
||||||
muted_until = int(int(i.group(2)) * 60 + time.time())
|
muted_until = int(int(i.group(2)) * 60 + time.time())
|
||||||
muted[username] = muted_until
|
muted[username] = muted_until
|
||||||
refresh_online()
|
refresh_online("chat")
|
||||||
|
|
||||||
if self_only or v.shadowbanned or execute_blackjack(v, None, text, "chat"):
|
if self_only or v.shadowbanned or execute_blackjack(v, None, text, "chat"):
|
||||||
emit('speak', data)
|
emit('speak', data)
|
||||||
|
@ -169,37 +275,43 @@ def speak(data, v):
|
||||||
messages[id] = data
|
messages[id] = data
|
||||||
messages = dict(list(messages.items())[-250:])
|
messages = dict(list(messages.items())[-250:])
|
||||||
|
|
||||||
typing = []
|
typing["chat"] = []
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def refresh_online():
|
def refresh_online(room):
|
||||||
for k, val in list(online.items()):
|
for k, val in list(online[room].items()):
|
||||||
if time.time() > val[0]:
|
if time.time() > val[0]:
|
||||||
del online[k]
|
del online[room][k]
|
||||||
if val[1] in typing:
|
if val[1] in typing[room]:
|
||||||
typing.remove(val[1])
|
typing[room].remove(val[1])
|
||||||
|
|
||||||
data = [list(online.values()), muted]
|
data = [list(online[room].values()), muted]
|
||||||
emit("online", data, room="chat", broadcast=True)
|
emit("online", data, room=room, broadcast=True)
|
||||||
cache.set('loggedin_chat', len(online), timeout=86400)
|
cache.set('loggedin_chat', len(online[room]), timeout=86400)
|
||||||
|
|
||||||
@socketio.on('connect')
|
@socketio.on('connect')
|
||||||
@auth_required_socketio
|
@auth_required_socketio
|
||||||
def connect(v):
|
def connect(v):
|
||||||
if request.referrer == f'{SITE_FULL}/notifications/messages':
|
if request.referrer == f'{SITE_FULL}/notifications/messages':
|
||||||
join_room(v.id)
|
join_room(v.id)
|
||||||
return ''
|
online["messages"].add(v.id)
|
||||||
elif request.referrer and request.referrer.startswith(f'{SITE_FULL}/chat/'):
|
|
||||||
join_room(request.referrer)
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
join_room("chat")
|
if request.referrer and request.referrer.startswith(f'{SITE_FULL}/chat/'):
|
||||||
|
room = request.referrer
|
||||||
|
else:
|
||||||
|
room = "chat"
|
||||||
|
|
||||||
if v.username in typing:
|
join_room(room)
|
||||||
typing.remove(v.username)
|
|
||||||
|
|
||||||
emit('typing', typing, room="chat")
|
if not typing.get(room):
|
||||||
|
typing[room] = []
|
||||||
|
|
||||||
|
if v.username in typing.get(room):
|
||||||
|
typing[room].remove(v.username)
|
||||||
|
|
||||||
|
emit('typing', typing[room], room=room)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@socketio.on('disconnect')
|
@socketio.on('disconnect')
|
||||||
|
@ -207,40 +319,60 @@ def connect(v):
|
||||||
def disconnect(v):
|
def disconnect(v):
|
||||||
if request.referrer == f'{SITE_FULL}/notifications/messages':
|
if request.referrer == f'{SITE_FULL}/notifications/messages':
|
||||||
leave_room(v.id)
|
leave_room(v.id)
|
||||||
return ''
|
online["messages"].remove(v.id)
|
||||||
elif request.referrer and request.referrer.startswith(f'{SITE_FULL}/chat/'):
|
|
||||||
leave_room(request.referrer)
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
online.pop(v.id, None)
|
if request.referrer and request.referrer.startswith(f'{SITE_FULL}/chat/'):
|
||||||
|
room = request.referrer
|
||||||
|
else:
|
||||||
|
room = "chat"
|
||||||
|
|
||||||
if v.username in typing:
|
online[room].pop(v.id, None)
|
||||||
typing.remove(v.username)
|
|
||||||
|
|
||||||
leave_room("chat")
|
if v.username in typing[room]:
|
||||||
refresh_online()
|
typing[room].remove(v.username)
|
||||||
|
|
||||||
|
leave_room(room)
|
||||||
|
refresh_online(room)
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@socketio.on('heartbeat')
|
@socketio.on('heartbeat')
|
||||||
@auth_required_socketio
|
@auth_required_socketio
|
||||||
def heartbeat(v):
|
def heartbeat(v):
|
||||||
|
if request.referrer and request.referrer.startswith(f'{SITE_FULL}/chat/'):
|
||||||
|
room = request.referrer
|
||||||
|
else:
|
||||||
|
room = "chat"
|
||||||
|
|
||||||
|
if not online.get(room):
|
||||||
|
online[room] = {}
|
||||||
|
|
||||||
expire_utc = int(time.time()) + 3610
|
expire_utc = int(time.time()) + 3610
|
||||||
already_there = online.get(v.id)
|
already_there = online[room].get(v.id)
|
||||||
online[v.id] = (expire_utc, v.username, v.name_color, v.patron, v.id, bool(v.has_badge(303)))
|
online[room][v.id] = (expire_utc, v.username, v.name_color, v.patron, v.id, bool(v.has_badge(303)))
|
||||||
if not already_there:
|
if not already_there:
|
||||||
refresh_online()
|
refresh_online(room)
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@socketio.on('typing')
|
@socketio.on('typing')
|
||||||
@is_not_banned_socketio
|
@is_not_banned_socketio
|
||||||
def typing_indicator(data, v):
|
def typing_indicator(data, v):
|
||||||
if data and v.username not in typing:
|
if request.referrer and request.referrer.startswith(f'{SITE_FULL}/chat/'):
|
||||||
typing.append(v.username)
|
room = request.referrer
|
||||||
elif not data and v.username in typing:
|
else:
|
||||||
typing.remove(v.username)
|
room = "chat"
|
||||||
|
|
||||||
emit('typing', typing, room="chat", broadcast=True)
|
if not typing.get(room):
|
||||||
|
typing[room] = []
|
||||||
|
|
||||||
|
if data and v.username not in typing[room]:
|
||||||
|
typing[room].append(v.username)
|
||||||
|
elif not data and v.username in typing[room]:
|
||||||
|
typing[room].remove(v.username)
|
||||||
|
|
||||||
|
emit('typing', typing[room], room=room, broadcast=True)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
@ -329,7 +461,7 @@ def messagereply(v):
|
||||||
execute_blackjack(v, c, c.body_html, 'message')
|
execute_blackjack(v, c, c.body_html, 'message')
|
||||||
execute_under_siege(v, c, c.body_html, 'message')
|
execute_under_siege(v, c, c.body_html, 'message')
|
||||||
|
|
||||||
if user_id and user_id not in {v.id, MODMAIL_ID} | BOT_IDs:
|
if user_id and user_id not in {v.id, MODMAIL_ID} | BOT_IDs and user_id not in online["messages"]:
|
||||||
if can_see(user, v):
|
if can_see(user, v):
|
||||||
notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=user_id).one_or_none()
|
notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=user_id).one_or_none()
|
||||||
if not notif:
|
if not notif:
|
||||||
|
|
|
@ -5,6 +5,7 @@ from sqlalchemy.orm import load_only
|
||||||
|
|
||||||
from files.classes.mod_logs import ModAction
|
from files.classes.mod_logs import ModAction
|
||||||
from files.classes.hole_logs import HoleAction
|
from files.classes.hole_logs import HoleAction
|
||||||
|
from files.classes.private_chats import *
|
||||||
from files.helpers.config.const import *
|
from files.helpers.config.const import *
|
||||||
from files.helpers.config.modaction_types import *
|
from files.helpers.config.modaction_types import *
|
||||||
from files.helpers.get import *
|
from files.helpers.get import *
|
||||||
|
@ -24,11 +25,14 @@ def clear(v):
|
||||||
Notification.read == False,
|
Notification.read == False,
|
||||||
Notification.user_id == v.id,
|
Notification.user_id == v.id,
|
||||||
).options(load_only(Notification.comment_id)).all()
|
).options(load_only(Notification.comment_id)).all()
|
||||||
|
|
||||||
for n in notifs:
|
for n in notifs:
|
||||||
n.read = True
|
n.read = True
|
||||||
g.db.add(n)
|
g.db.add(n)
|
||||||
|
|
||||||
|
chat_notifs = g.db.query(ChatNotification).filter_by(user_id=v.id)
|
||||||
|
for chat_notif in chat_notifs:
|
||||||
|
g.db.delete(chat_notif)
|
||||||
|
|
||||||
v.last_viewed_modmail_notifs = int(time.time())
|
v.last_viewed_modmail_notifs = int(time.time())
|
||||||
v.last_viewed_post_notifs = int(time.time())
|
v.last_viewed_post_notifs = int(time.time())
|
||||||
v.last_viewed_log_notifs = int(time.time())
|
v.last_viewed_log_notifs = int(time.time())
|
||||||
|
@ -131,6 +135,16 @@ def notifications_messages(v):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/notifications/chats")
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
|
@auth_required
|
||||||
|
def notifications_chats(v):
|
||||||
|
criteria1 = (Chat.id == ChatMembership.chat_id, ChatMembership.user_id == v.id)
|
||||||
|
criteria2 = (Chat.id == ChatNotification.chat_id, ChatNotification.user_id == v.id)
|
||||||
|
chats = g.db.query(Chat, func.count(ChatNotification.chat_id)).join(ChatMembership, and_(*criteria1)).outerjoin(ChatNotification, and_(*criteria2)).group_by(Chat).order_by(func.count(ChatNotification.chat_id).desc(), Chat.name).all()
|
||||||
|
return render_template("notifications.html", v=v, chats=chats)
|
||||||
|
|
||||||
@app.get("/notifications/modmail")
|
@app.get("/notifications/modmail")
|
||||||
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
|
||||||
|
from files.classes.private_chats import *
|
||||||
|
from files.routes.wrappers import *
|
||||||
|
from files.helpers.config.const import *
|
||||||
|
from files.helpers.get import *
|
||||||
|
|
||||||
|
from files.__main__ import app, limiter
|
||||||
|
|
||||||
|
@app.post("/@<username>/chat")
|
||||||
|
@limiter.limit('1/second', scope=rpath)
|
||||||
|
@limiter.limit('1/second', scope=rpath, key_func=get_ID)
|
||||||
|
@limiter.limit("10/minute;20/hour;50/day", deduct_when=lambda response: response.status_code < 400)
|
||||||
|
@limiter.limit("10/minute;20/hour;50/day", deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
|
@auth_required
|
||||||
|
def chat_user(v, username):
|
||||||
|
user = get_user(username, v=v, include_blocks=True)
|
||||||
|
|
||||||
|
if hasattr(user, 'is_blocking') and user.is_blocking:
|
||||||
|
abort(403, f"You're blocking @{user.username}")
|
||||||
|
|
||||||
|
if v.admin_level <= PERMS['MESSAGE_BLOCKED_USERS'] and hasattr(user, 'is_blocked') and user.is_blocked:
|
||||||
|
abort(403, f"@{user.username} is blocking you!")
|
||||||
|
|
||||||
|
if user.has_muted(v):
|
||||||
|
abort(403, f"@{user.username} is muting notifications from you, so you can't chat with them!")
|
||||||
|
|
||||||
|
|
||||||
|
sq = g.db.query(Chat.id).join(Chat.memberships).filter(ChatMembership.user_id.in_((v.id, user.id))).group_by(Chat.id).having(func.count(Chat.id) == 2).subquery()
|
||||||
|
existing = g.db.query(Chat.id).join(Chat.memberships).filter(Chat.id == sq.c.id).group_by(Chat.id).having(func.count(Chat.id) == 2).one_or_none()
|
||||||
|
if existing:
|
||||||
|
return redirect(f"/chat/{existing.id}")
|
||||||
|
|
||||||
|
chat = Chat(owner_id=v.id, name=f"Chat with @{user.username}")
|
||||||
|
g.db.add(chat)
|
||||||
|
g.db.flush()
|
||||||
|
|
||||||
|
chat_membership = ChatMembership(
|
||||||
|
user_id=v.id,
|
||||||
|
chat_id=chat.id,
|
||||||
|
)
|
||||||
|
g.db.add(chat_membership)
|
||||||
|
|
||||||
|
chat_membership = ChatMembership(
|
||||||
|
user_id=user.id,
|
||||||
|
chat_id=chat.id,
|
||||||
|
)
|
||||||
|
g.db.add(chat_membership)
|
||||||
|
|
||||||
|
return redirect(f"/chat/{chat.id}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/chat/<int:chat_id>")
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
|
@auth_required
|
||||||
|
def private_chat(v, chat_id):
|
||||||
|
chat = g.db.get(Chat, chat_id)
|
||||||
|
if not chat:
|
||||||
|
abort(404, "Chat not found!")
|
||||||
|
|
||||||
|
if v.admin_level < PERMS['VIEW_CHATS']:
|
||||||
|
is_member = g.db.query(ChatMembership.user_id).filter_by(user_id=v.id, chat_id=chat_id).one_or_none()
|
||||||
|
if not is_member:
|
||||||
|
abort(403, "You're not a member of this chat!")
|
||||||
|
|
||||||
|
displayed_messages = g.db.query(ChatMessage).filter_by(chat_id=chat.id).limit(250).all()
|
||||||
|
|
||||||
|
notifs_msgs = g.db.query(ChatNotification, ChatMessage).join(ChatNotification.chat_message).filter(
|
||||||
|
ChatNotification.user_id == v.id,
|
||||||
|
ChatNotification.chat_id == chat.id,
|
||||||
|
).all()
|
||||||
|
for notif, msg in notifs_msgs:
|
||||||
|
msg.unread = True
|
||||||
|
g.db.delete(notif)
|
||||||
|
|
||||||
|
g.db.commit() #to clear notif count
|
||||||
|
return render_template("private_chat.html", v=v, messages=displayed_messages, chat=chat)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/chat/<int:chat_id>/name")
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
|
@auth_required
|
||||||
|
def change_chat_name(v, chat_id):
|
||||||
|
chat = g.db.get(Chat, chat_id)
|
||||||
|
if not chat:
|
||||||
|
abort(404, "Chat not found!")
|
||||||
|
|
||||||
|
if v.id != chat.owner_id:
|
||||||
|
abort(403, "Only the chat owner can change its name!")
|
||||||
|
|
||||||
|
new_name = request.values.get("new_name").strip()
|
||||||
|
chat.name = new_name
|
||||||
|
g.db.add(chat)
|
||||||
|
|
||||||
|
return redirect(f"/chat/{chat.id}")
|
||||||
|
|
||||||
|
@app.post("/chat/<int:chat_id>/leave")
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
|
@auth_required
|
||||||
|
def leave_chat(v, chat_id):
|
||||||
|
chat = g.db.get(Chat, chat_id)
|
||||||
|
if not chat:
|
||||||
|
abort(404, "Chat not found!")
|
||||||
|
|
||||||
|
if v.id == chat.owner_id:
|
||||||
|
abort(403, "The chat owner can't leave it!")
|
||||||
|
|
||||||
|
membership = g.db.query(ChatMembership).filter_by(user_id=v.id, chat_id=chat_id).one_or_none()
|
||||||
|
if not membership:
|
||||||
|
abort(400, "You're not a member of this chat!")
|
||||||
|
|
||||||
|
g.db.delete(membership)
|
||||||
|
|
||||||
|
chat_leave = ChatLeave(
|
||||||
|
user_id=v.id,
|
||||||
|
chat_id=chat_id,
|
||||||
|
)
|
||||||
|
g.db.add(chat_leave)
|
||||||
|
|
||||||
|
return {"message": "Chat left successfully!"}
|
|
@ -12,11 +12,10 @@
|
||||||
<div class="container pb-4 pb-md-2">
|
<div class="container pb-4 pb-md-2">
|
||||||
<div class="row justify-content-around" id="main-content-row">
|
<div class="row justify-content-around" id="main-content-row">
|
||||||
<div class="col h-100 {% block customPadding %}custom-gutters{% endblock %}" id="main-content-col">
|
<div class="col h-100 {% block customPadding %}custom-gutters{% endblock %}" id="main-content-col">
|
||||||
|
{{macros.chat_users_online()}}
|
||||||
|
|
||||||
{{macros.chat_users_online()}}
|
<div id="chat-group-template" class="d-none">
|
||||||
|
{{macros.chat_group_template()}}
|
||||||
<div id="chat-group-template" class="d-none">
|
|
||||||
{{macros.chat_group_template()}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -533,6 +533,31 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<br><br><br><br><br><br><br><br>
|
||||||
|
{% elif request.path.startswith('/chat/') %}
|
||||||
|
<div class="mx-2">
|
||||||
|
<h5 class="mt-3">Members</h5>
|
||||||
|
<ul class="col text-left d-lg-none bg-white mb-4 pb-2" style="max-width:300px;list-style-type:none">
|
||||||
|
{% for membership in chat.memberships %}
|
||||||
|
{% set user = membership.user %}
|
||||||
|
{% set patron = '' %}
|
||||||
|
{% if user.patron %}
|
||||||
|
{% set patron = patron + 'class="patron chat-patron" style="background-color:#' ~ user.name_color ~ '"' %}
|
||||||
|
{% endif %}
|
||||||
|
{% if user.pride_username(None) %}
|
||||||
|
{% set patron = patron + ' pride_username' %}
|
||||||
|
{% endif %}
|
||||||
|
<li style="margin-top: 0.35rem">
|
||||||
|
<a class="font-weight-bold" target="_blank" href="/@{{user.username}}" style="color:#{{user.name_color}}">
|
||||||
|
<img loading="lazy" class="mr-1" src="/pp/{{user.id}}">
|
||||||
|
<span {{patron | safe}}>{{user.username}}</span>
|
||||||
|
</a>
|
||||||
|
<i class="d-none ml-1 text-smaller text-success online-marker online-marker-{{user.id}} fas fa-circle" data-bs-toggle="tooltip" data-bs-placement="top" title="Here now"></i>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<br><br><br><br><br><br><br><br>
|
<br><br><br><br><br><br><br><br>
|
||||||
{% elif has_sidebar %}
|
{% elif has_sidebar %}
|
||||||
{% include "sidebar_" ~ SITE_NAME ~ ".html" %}
|
{% include "sidebar_" ~ SITE_NAME ~ ".html" %}
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
All {% if v.normal_notifications_count %}<span class="font-weight-bold" style="color:#dc3545">({{v.normal_notifications_count}})</span>{% endif %}
|
All {% if v.normal_notifications_count %}<span class="font-weight-bold" style="color:#dc3545">({{v.normal_notifications_count}})</span>{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-3{% if request.path == '/notifications/chats' %} active{% endif %}" href="/notifications/chats">
|
||||||
|
Chats {% if v.chats_notifications_count %}<span class="font-weight-bold" style="color:#008080">({{v.chats_notifications_count}})</span>{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link py-3{% if request.path == '/notifications/messages' %} active{% endif %}" href="/notifications/messages">
|
<a class="nav-link py-3{% if request.path == '/notifications/messages' %} active{% endif %}" href="/notifications/messages">
|
||||||
Messages {% if v.message_notifications_count %}<span class="font-weight-bold" style="color:#d8910d">({{v.message_notifications_count}})</span>{% endif %}
|
Messages {% if v.message_notifications_count %}<span class="font-weight-bold" style="color:#d8910d">({{v.message_notifications_count}})</span>{% endif %}
|
||||||
|
@ -56,7 +61,24 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="notifs px-3 p-md-0">
|
<div class="notifs px-3 p-md-0">
|
||||||
{% if request.path == '/notifications/posts' %}
|
{% if request.path == '/notifications/chats' %}
|
||||||
|
<table class="mt-4 ml-3" style="max-width:300px">
|
||||||
|
<tbody>
|
||||||
|
{% for chat, notif_count in chats %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/chat/{{chat.id}}" style="text-decoration:none!important">
|
||||||
|
{{chat.name}}
|
||||||
|
{% if notif_count %}
|
||||||
|
<span class="notif-chats notif-count ml-1" style="padding-left:4.5px;background:#008080">{{notif_count}}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% elif request.path == '/notifications/posts' %}
|
||||||
{% with listing=notifications %}
|
{% with listing=notifications %}
|
||||||
<div class="mt-4 posts">
|
<div class="mt-4 posts">
|
||||||
{% include "post_listing.html" %}
|
{% include "post_listing.html" %}
|
||||||
|
@ -129,7 +151,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pagenav %}
|
{% block pagenav %}
|
||||||
{% if notifications %}
|
{% if notifications and request.path != '/notifications/chats' %}
|
||||||
{{macros.pagination()}}
|
{{macros.pagination()}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
{%- extends 'root.html' -%}
|
||||||
|
{% block pagetitle -%}Chat{%- endblock %}
|
||||||
|
{% block pagetype %}chat{% endblock %}
|
||||||
|
{% block body_attributes %}class="has_header"{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<link rel="stylesheet" href="{{'css/chat.css' | asset}}">
|
||||||
|
{% include "header.html" %}
|
||||||
|
{% include "modals/expanded_image.html" %}
|
||||||
|
{% include "modals/emoji.html" %}
|
||||||
|
{% include "modals/gif.html" %}
|
||||||
|
{% set vlink = '<a href="/id/' ~ v.id ~ '"' %}
|
||||||
|
<div class="container pb-4 pb-md-2">
|
||||||
|
<div class="row justify-content-around" id="main-content-row">
|
||||||
|
<div class="col h-100 {% block customPadding %}custom-gutters{% endblock %}" id="main-content-col">
|
||||||
|
<h5 class="mt-4 mb-3 ml-1 toggleable" style="display:inline-block">{{chat.name}}</h5>
|
||||||
|
|
||||||
|
{% if v.id == chat.owner_id %}
|
||||||
|
<button class="px-2 toggleable" type="button" data-nonce="{{g.nonce}}" data-onclick="toggleElement('chat-name-form', 'chat-name')">
|
||||||
|
<i class="fas fa-pen text-small text-muted"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<form id="chat-name-form" class="d-none mt-4" action="/chat/{{chat.id}}/name" method="post">
|
||||||
|
<input hidden name="formkey" value="{{v|formkey}}" class="notranslate" translate="no">
|
||||||
|
<input id="chat-name" autocomplete="off" class="form-control d-inline-block" name="new_name" value="{{chat.name}}" style="max-width:300px">
|
||||||
|
<button type="submit" class="btn btn-primary" style="margin-top:-5px">Save</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<button type="submit" class="btn btn-danger ml-3 px-2 pt-1" style="margin-top:-3px;padding-bottom: 0.3rem" data-nonce="{{g.nonce}}" data-onclick="areyousure(this)" data-areyousure="postToastReload(this, '/chat/{{chat.id}}/leave')">Leave Chat</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="border-right d-md-none fl-r mt-4 pt-1 mr-3">
|
||||||
|
<span data-bs-html="true" data-bs-toggle="tooltip" data-bs-placement="bottom" title="<b>Members</b> {% for membership in chat.memberships %}<br>@{{membership.user.username}}{% endfor %}" class="text-muted">
|
||||||
|
<i class="fas fa-user fa-sm mr-1"></i>
|
||||||
|
<span class="board-chat-count" style="cursor:default">{{chat.memberships|length}}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chat-group-template" class="d-none">
|
||||||
|
{{macros.chat_group_template()}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chat-line-template" class="d-none">
|
||||||
|
{{macros.chat_line_template()}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{macros.chat_window(vlink)}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col text-left d-none d-lg-block pt-3 pb-5" style="max-width:300px">
|
||||||
|
<h5>Members</h5>
|
||||||
|
<div class="mt-2">
|
||||||
|
{% for membership in chat.memberships %}
|
||||||
|
{% set user = membership.user %}
|
||||||
|
{% set patron = '' %}
|
||||||
|
{% if user.patron %}
|
||||||
|
{% set patron = patron + 'class="patron chat-patron" style="background-color:#' ~ user.name_color ~ '"' %}
|
||||||
|
{% endif %}
|
||||||
|
{% if user.pride_username(None) %}
|
||||||
|
{% set patron = patron + ' pride_username' %}
|
||||||
|
{% endif %}
|
||||||
|
<li style="margin-top: 0.35rem">
|
||||||
|
<a class="font-weight-bold" target="_blank" href="/@{{user.username}}" style="color:#{{user.name_color}}">
|
||||||
|
<img loading="lazy" class="mr-1" src="/pp/{{user.id}}">
|
||||||
|
<span {{patron | safe}}>{{user.username}}</span>
|
||||||
|
</a>
|
||||||
|
<i class="d-none ml-1 text-smaller text-success online-marker online-marker-{{user.id}} fas fa-circle" data-bs-toggle="tooltip" data-bs-placement="top" title="Here now"></i>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input id="chat_id" hidden value="{{chat.id}}">
|
||||||
|
<input id="vid" hidden value="{{v.id}}">
|
||||||
|
<input id="slurreplacer" hidden value="{{v.slurreplacer}}">
|
||||||
|
<input id="admin_level" hidden value="{{v.admin_level}}">
|
||||||
|
<input id="blocked_user_ids" hidden value="{{(v.userblocks|string)[1:-1]}}">
|
||||||
|
<script defer src="{{'js/vendor/socketio.js' | asset}}"></script>
|
||||||
|
<script defer src="{{'js/flash.js' | asset}}"></script>
|
||||||
|
<script defer src="{{'js/vendor/lozad.js' | asset}}"></script>
|
||||||
|
<script defer src="{{'js/vendor/lite-youtube.js' | asset}}"></script>
|
||||||
|
<script defer src="{{'js/chat.js' | asset}}"></script>
|
||||||
|
{% endblock %}
|
|
@ -187,6 +187,11 @@
|
||||||
<button type="button" id="button-sub" class="btn btn-primary {% if is_following or u.is_blocked %}d-none{% endif %}" data-nonce="{{g.nonce}}" data-onclick="postToastSwitch(this,'/follow/{{u.username}}','button-unsub','button-sub','d-none')">Follow</button>
|
<button type="button" id="button-sub" class="btn btn-primary {% if is_following or u.is_blocked %}d-none{% endif %}" data-nonce="{{g.nonce}}" data-onclick="postToastSwitch(this,'/follow/{{u.username}}','button-unsub','button-sub','d-none')">Follow</button>
|
||||||
<button type="button" id="button-unsub" class="btn btn-secondary {% if not is_following %}d-none{% endif %}" data-nonce="{{g.nonce}}" data-onclick="postToastSwitch(this,'/unfollow/{{u.username}}','button-unsub','button-sub','d-none')">Unfollow</button>
|
<button type="button" id="button-unsub" class="btn btn-secondary {% if not is_following %}d-none{% endif %}" data-nonce="{{g.nonce}}" data-onclick="postToastSwitch(this,'/unfollow/{{u.username}}','button-unsub','button-sub','d-none')">Unfollow</button>
|
||||||
|
|
||||||
|
<form action="/@{{u.username}}/chat" method="post">
|
||||||
|
<input hidden name="formkey" value="{{v|formkey}}" class="notranslate" translate="no">
|
||||||
|
<button type="submit" class="btn btn-primary">Chat</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<button type="button" class="btn btn-primary" data-nonce="{{g.nonce}}" data-onclick="toggleElement('message', 'input-message')">Message</button>
|
<button type="button" class="btn btn-primary" data-nonce="{{g.nonce}}" data-onclick="toggleElement('message', 'input-message')">Message</button>
|
||||||
|
|
||||||
{% if FEATURES['USERS_SUICIDE'] -%}
|
{% if FEATURES['USERS_SUICIDE'] -%}
|
||||||
|
@ -513,6 +518,11 @@
|
||||||
<button type="button" id="button-sub2" class="btn btn-primary {% if is_following or u.is_blocked %}d-none{% endif %}" data-nonce="{{g.nonce}}" data-onclick="postToastSwitch(this,'/follow/{{u.username}}','button-unsub2','button-sub2','d-none')">Follow</button>
|
<button type="button" id="button-sub2" class="btn btn-primary {% if is_following or u.is_blocked %}d-none{% endif %}" data-nonce="{{g.nonce}}" data-onclick="postToastSwitch(this,'/follow/{{u.username}}','button-unsub2','button-sub2','d-none')">Follow</button>
|
||||||
<button type="button" id="button-unsub2" class="btn btn-secondary {% if not is_following %}d-none{% endif %}" data-nonce="{{g.nonce}}" data-onclick="postToastSwitch(this,'/unfollow/{{u.username}}','button-unsub2','button-sub2','d-none')">Unfollow</button>
|
<button type="button" id="button-unsub2" class="btn btn-secondary {% if not is_following %}d-none{% endif %}" data-nonce="{{g.nonce}}" data-onclick="postToastSwitch(this,'/unfollow/{{u.username}}','button-unsub2','button-sub2','d-none')">Unfollow</button>
|
||||||
|
|
||||||
|
<form action="/@{{u.username}}/chat" method="post">
|
||||||
|
<input hidden name="formkey" value="{{v|formkey}}" class="notranslate" translate="no">
|
||||||
|
<button type="submit" class="btn btn-primary">Chat</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<button type="button" class="btn btn-primary" data-nonce="{{g.nonce}}" data-onclick="toggleElement('message-mobile', 'input-message-mobile')">Message</button>
|
<button type="button" class="btn btn-primary" data-nonce="{{g.nonce}}" data-onclick="toggleElement('message-mobile', 'input-message-mobile')">Message</button>
|
||||||
{% if FEATURES['USERS_SUICIDE'] -%}
|
{% if FEATURES['USERS_SUICIDE'] -%}
|
||||||
<button type="button" class="btn btn-primary" data-nonce="{{g.nonce}}" data-onclick="postToastSwitch(this,'/@{{u.username}}/suicide')">Get Them Help</button>
|
<button type="button" class="btn btn-primary" data-nonce="{{g.nonce}}" data-onclick="postToastSwitch(this,'/@{{u.username}}/suicide')">Get Them Help</button>
|
||||||
|
|
|
@ -351,17 +351,30 @@
|
||||||
{% macro chat_window(vlink)%}
|
{% macro chat_window(vlink)%}
|
||||||
<div id="shrink">
|
<div id="shrink">
|
||||||
<div id="chat-window" class="container p-0">
|
<div id="chat-window" class="container p-0">
|
||||||
{% set messages_list = messages.items()|list %}
|
{% if request.path == '/chat' %}
|
||||||
{% for id, m in messages_list %}
|
{% for id, m in messages.items()|list %}
|
||||||
{% set same = loop.index > 1 and m.user_id == messages_list[loop.index-2][1].user_id %}
|
{% set same = loop.index > 1 and m.user_id == messages_list[loop.index-2][1].user_id %}
|
||||||
{% if not same %}
|
{% if not same %}
|
||||||
{% if loop.index > 1 %}
|
{% if loop.index > 1 %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{{chat_group_template(id, m)}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{chat_group_template(id, m)}}
|
{{chat_line_template(id, m, vlink)}}
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
{{chat_line_template(id, m, vlink)}}
|
{% else %}
|
||||||
{% endfor %}
|
{% for m in messages %}
|
||||||
|
{% set id = m.id %}
|
||||||
|
{% set same = loop.index > 1 and m.user_id == messages[loop.index-2].user_id %}
|
||||||
|
{% if not same %}
|
||||||
|
{% if loop.index > 1 %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{{chat_group_template(id, m)}}
|
||||||
|
{% endif %}
|
||||||
|
{{chat_line_template(id, m, vlink)}}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
create table chats (
|
||||||
|
id integer primary key,
|
||||||
|
owner_id integer not null,
|
||||||
|
name varchar(40) not null,
|
||||||
|
created_utc integer not null
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table chats
|
||||||
|
add constraint chats_owner_fkey foreign key (owner_id) references users(id);
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.chats_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.chats_id_seq OWNED BY public.chats.id;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.chats ALTER COLUMN id SET DEFAULT nextval('public.chats_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
create table chat_memberships (
|
||||||
|
user_id integer not null,
|
||||||
|
chat_id integer not null,
|
||||||
|
created_utc integer not null
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table chat_memberships
|
||||||
|
add constraint chat_memberships_pkey primary key (user_id, chat_id);
|
||||||
|
|
||||||
|
alter table chat_memberships
|
||||||
|
add constraint chat_memberships_user_fkey foreign key (user_id) references users(id);
|
||||||
|
|
||||||
|
alter table chat_memberships
|
||||||
|
add constraint chat_memberships_chat_fkey foreign key (chat_id) references chats(id);
|
||||||
|
|
||||||
|
|
||||||
|
create table chat_leaves (
|
||||||
|
user_id integer not null,
|
||||||
|
chat_id integer not null,
|
||||||
|
created_utc integer not null
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table chat_leaves
|
||||||
|
add constraint chat_leaves_pkey primary key (user_id, chat_id);
|
||||||
|
|
||||||
|
alter table chat_leaves
|
||||||
|
add constraint chat_leaves_user_fkey foreign key (user_id) references users(id);
|
||||||
|
|
||||||
|
alter table chat_leaves
|
||||||
|
add constraint chat_leaves_chat_fkey foreign key (chat_id) references chats(id);
|
||||||
|
|
||||||
|
|
||||||
|
create table chat_messages (
|
||||||
|
id integer primary key,
|
||||||
|
user_id integer not null,
|
||||||
|
chat_id integer not null,
|
||||||
|
quotes integer,
|
||||||
|
text varchar(1000) not null,
|
||||||
|
text_censored varchar(1200) not null,
|
||||||
|
text_html varchar(5000) not null,
|
||||||
|
text_html_censored varchar(6000) not null,
|
||||||
|
created_utc integer not null
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table chat_messages
|
||||||
|
add constraint chat_messages_user_fkey foreign key (user_id) references users(id);
|
||||||
|
|
||||||
|
alter table chat_messages
|
||||||
|
add constraint chat_messages_chat_fkey foreign key (chat_id) references chats(id);
|
||||||
|
|
||||||
|
alter table chat_messages
|
||||||
|
add constraint chat_messages_quotes_fkey foreign key (quotes) references chat_messages(id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.chat_messages_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.chat_messages_id_seq OWNED BY public.chat_messages.id;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.chat_messages ALTER COLUMN id SET DEFAULT nextval('public.chat_messages_id_seq'::regclass);
|
||||||
|
|
||||||
|
create table chat_notifications (
|
||||||
|
user_id integer not null,
|
||||||
|
chat_message_id integer not null,
|
||||||
|
chat_id integer not null,
|
||||||
|
created_utc integer not null
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table chat_notifications
|
||||||
|
add constraint chat_notifications_user_fkey foreign key (user_id) references users(id);
|
||||||
|
|
||||||
|
alter table chat_notifications
|
||||||
|
add constraint chat_notifications_message_fkey foreign key (chat_message_id) references chat_messages(id);
|
||||||
|
|
||||||
|
alter table chat_notifications
|
||||||
|
add constraint chat_notifications_chat_fkey foreign key (chat_id) references chats(id);
|
Loading…
Reference in New Issue