2022-06-16 10:12:37 +09:00
|
|
|
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
|
2022-03-07 23:51:59 +09:00
|
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
2022-02-16 18:55:30 +09:00
|
|
|
import time
|
2022-02-13 13:00:10 +09:00
|
|
|
from datetime import datetime
|
|
|
|
|
2022-02-18 10:32:34 +09:00
|
|
|
from quart import current_app, escape
|
2022-02-13 13:00:10 +09:00
|
|
|
|
2022-02-20 16:20:43 +09:00
|
|
|
from anonstream.broadcast import broadcast, broadcast_users_update
|
2022-06-14 20:40:16 +09:00
|
|
|
from anonstream.events import notify_event_sockets
|
2022-07-15 01:31:11 +09:00
|
|
|
from anonstream.helpers.chat import generate_nonce_hash, get_scrollback, insert_emotes
|
2022-05-28 14:37:41 +09:00
|
|
|
from anonstream.utils.chat import get_message_for_websocket, get_approx_linespan
|
2022-02-18 10:32:34 +09:00
|
|
|
|
2022-02-18 21:24:19 +09:00
|
|
|
CONFIG = current_app.config
|
2022-02-18 10:32:34 +09:00
|
|
|
MESSAGES_BY_ID = current_app.messages_by_id
|
|
|
|
MESSAGES = current_app.messages
|
|
|
|
USERS_BY_TOKEN = current_app.users_by_token
|
|
|
|
USERS = current_app.users
|
2022-02-16 18:55:30 +09:00
|
|
|
|
2022-02-20 13:23:32 +09:00
|
|
|
class Rejected(ValueError):
|
2022-02-15 19:11:53 +09:00
|
|
|
pass
|
|
|
|
|
2022-02-20 16:20:43 +09:00
|
|
|
def get_all_messages_for_websocket():
|
2022-02-18 10:32:34 +09:00
|
|
|
return list(map(
|
2022-02-20 16:20:43 +09:00
|
|
|
lambda message: get_message_for_websocket(
|
2022-02-18 10:32:34 +09:00
|
|
|
user=USERS_BY_TOKEN[message['token']],
|
|
|
|
message=message,
|
|
|
|
),
|
2022-02-18 21:24:19 +09:00
|
|
|
get_scrollback(MESSAGES),
|
2022-02-18 10:32:34 +09:00
|
|
|
))
|
2022-02-15 19:11:53 +09:00
|
|
|
|
2022-02-20 13:23:32 +09:00
|
|
|
def add_chat_message(user, nonce, comment, ignore_empty=False):
|
2022-02-20 16:20:43 +09:00
|
|
|
# Special case: if the comment is empty, do nothing and return
|
2022-02-20 13:23:32 +09:00
|
|
|
if ignore_empty and len(comment) == 0:
|
2022-02-21 11:05:19 +09:00
|
|
|
return False
|
2022-02-20 13:23:32 +09:00
|
|
|
|
2022-05-28 14:37:41 +09:00
|
|
|
timestamp_ms = time.time_ns() // 1_000_000
|
|
|
|
timestamp = timestamp_ms // 1000
|
|
|
|
|
|
|
|
# Check user
|
|
|
|
while user['linespan']:
|
|
|
|
linespan_timestamp, _ = user['linespan'][0]
|
|
|
|
if timestamp - linespan_timestamp >= CONFIG['FLOOD_LINE_DURATION']:
|
|
|
|
user['linespan'].popleft()
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
total_recent_linespan = sum(map(
|
|
|
|
lambda linespan_tuple: linespan_tuple[1],
|
|
|
|
user['linespan'],
|
|
|
|
))
|
|
|
|
if total_recent_linespan > CONFIG['FLOOD_LINE_THRESHOLD']:
|
|
|
|
raise Rejected(
|
|
|
|
f'Chat overuse in the last '
|
|
|
|
f'{CONFIG["FLOOD_LINE_DURATION"]:.0f} seconds'
|
|
|
|
)
|
|
|
|
|
2022-02-20 16:20:43 +09:00
|
|
|
# Check message
|
2022-02-18 10:32:34 +09:00
|
|
|
message_id = generate_nonce_hash(nonce)
|
|
|
|
if message_id in MESSAGES_BY_ID:
|
2022-02-16 18:55:30 +09:00
|
|
|
raise Rejected('Discarded suspected duplicate message')
|
2022-02-15 19:11:53 +09:00
|
|
|
if len(comment) == 0:
|
|
|
|
raise Rejected('Message was empty')
|
2022-05-28 14:40:53 +09:00
|
|
|
if len(comment.strip()) == 0:
|
|
|
|
raise Rejected('Message was practically empty')
|
2022-06-17 09:06:45 +09:00
|
|
|
if len(comment) > CONFIG['CHAT_COMMENT_MAX_LENGTH']:
|
|
|
|
raise Rejected(
|
|
|
|
f'Message exceeded {CONFIG["CHAT_COMMENT_MAX_LENGTH"]} chars'
|
|
|
|
)
|
2022-05-28 14:37:41 +09:00
|
|
|
|
2022-06-17 09:06:45 +09:00
|
|
|
if comment.count('\n') + 1 > CONFIG['CHAT_COMMENT_MAX_LINES']:
|
|
|
|
raise Rejected(
|
|
|
|
f'Message exceeded {CONFIG["CHAT_COMMENT_MAX_LINES"]} lines'
|
|
|
|
)
|
2022-05-28 14:37:41 +09:00
|
|
|
linespan = get_approx_linespan(comment)
|
2022-06-17 09:06:45 +09:00
|
|
|
if linespan > CONFIG['CHAT_COMMENT_MAX_LINES']:
|
|
|
|
raise Rejected(
|
|
|
|
f'Message would span {CONFIG["CHAT_COMMENT_MAX_LINES"]} '
|
|
|
|
f'or more lines'
|
|
|
|
)
|
2022-05-28 14:37:41 +09:00
|
|
|
|
|
|
|
# Record linespan
|
|
|
|
linespan_tuple = (timestamp, linespan)
|
|
|
|
user['linespan'].append(linespan_tuple)
|
2022-02-15 19:11:53 +09:00
|
|
|
|
2022-02-20 16:20:43 +09:00
|
|
|
# Create and add message
|
2022-02-16 18:55:30 +09:00
|
|
|
try:
|
2022-02-18 10:32:34 +09:00
|
|
|
last_message = next(reversed(MESSAGES))
|
2022-02-16 18:55:30 +09:00
|
|
|
except StopIteration:
|
2022-02-18 10:32:34 +09:00
|
|
|
seq = timestamp_ms
|
2022-02-16 18:55:30 +09:00
|
|
|
else:
|
2022-02-18 10:32:34 +09:00
|
|
|
if timestamp_ms > last_message['seq']:
|
|
|
|
seq = timestamp_ms
|
|
|
|
else:
|
|
|
|
seq = last_message['seq'] + 1
|
2022-02-16 18:55:30 +09:00
|
|
|
dt = datetime.utcfromtimestamp(timestamp)
|
2022-07-15 01:31:11 +09:00
|
|
|
markup = insert_emotes(escape(comment))
|
2022-02-22 14:27:42 +09:00
|
|
|
message = {
|
2022-02-13 13:00:10 +09:00
|
|
|
'id': message_id,
|
2022-02-18 10:32:34 +09:00
|
|
|
'seq': seq,
|
2022-02-16 18:55:30 +09:00
|
|
|
'token': user['token'],
|
|
|
|
'timestamp': timestamp,
|
2022-02-13 13:00:10 +09:00
|
|
|
'date': dt.strftime('%Y-%m-%d'),
|
|
|
|
'time_minutes': dt.strftime('%H:%M'),
|
|
|
|
'time_seconds': dt.strftime('%H:%M:%S'),
|
2022-02-15 19:11:53 +09:00
|
|
|
'nomarkup': comment,
|
2022-02-13 13:00:10 +09:00
|
|
|
'markup': markup,
|
|
|
|
}
|
2022-02-22 14:27:42 +09:00
|
|
|
MESSAGES_BY_ID[message_id] = message
|
2022-02-15 19:11:53 +09:00
|
|
|
|
2022-07-03 17:50:29 +09:00
|
|
|
# Limit number of stored messages
|
2022-02-18 21:24:19 +09:00
|
|
|
while len(MESSAGES_BY_ID) > CONFIG['MAX_CHAT_MESSAGES']:
|
2022-06-22 17:31:06 +09:00
|
|
|
MESSAGES_BY_ID.popitem(last=False)
|
2022-02-18 21:24:19 +09:00
|
|
|
|
2022-07-03 17:50:29 +09:00
|
|
|
# Deverify user every n messages
|
|
|
|
if CONFIG['CHAT_DEVERIFY_CLOCK'] is not None:
|
|
|
|
user['clock'] = (user['clock'] + 1) % CONFIG['CHAT_DEVERIFY_CLOCK']
|
|
|
|
if user['clock'] == 0:
|
|
|
|
user['verified'] = False
|
|
|
|
|
2022-06-14 20:40:16 +09:00
|
|
|
# Notify event sockets that a chat message was added
|
|
|
|
notify_event_sockets({
|
|
|
|
'type': 'message',
|
|
|
|
'event': message,
|
|
|
|
})
|
|
|
|
|
2022-02-20 16:20:43 +09:00
|
|
|
# Broadcast a users update to all websockets,
|
|
|
|
# in case this message is from a new user
|
|
|
|
broadcast_users_update()
|
|
|
|
|
|
|
|
# Broadcast message to websockets
|
2022-02-19 12:14:37 +09:00
|
|
|
broadcast(
|
2022-02-18 10:32:34 +09:00
|
|
|
USERS,
|
2022-02-15 19:11:53 +09:00
|
|
|
payload={
|
2022-02-22 14:27:42 +09:00
|
|
|
'type': 'message',
|
|
|
|
'message': get_message_for_websocket(user, message),
|
2022-02-18 14:16:54 +09:00
|
|
|
},
|
2022-02-15 19:11:53 +09:00
|
|
|
)
|
|
|
|
|
2022-02-21 11:05:19 +09:00
|
|
|
return True
|
2022-06-14 05:43:51 +09:00
|
|
|
|
|
|
|
def delete_chat_messages(seqs):
|
|
|
|
seq_set = set(seqs)
|
|
|
|
message_ids = set()
|
|
|
|
for message_id, message in MESSAGES_BY_ID.items():
|
|
|
|
if len(seq_set) == 0:
|
|
|
|
break
|
|
|
|
try:
|
|
|
|
seq_set.remove(message['seq'])
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
message_ids.add(message_id)
|
|
|
|
for message_id in message_ids:
|
|
|
|
MESSAGES_BY_ID.pop(message_id)
|
|
|
|
broadcast(
|
|
|
|
USERS,
|
|
|
|
payload={
|
|
|
|
'type': 'delete',
|
|
|
|
'seqs': seqs,
|
|
|
|
},
|
|
|
|
)
|